From 341269e035d68e4b42ca5deec334dded4d00648a Mon Sep 17 00:00:00 2001 From: Joey Hess Date: Thu, 15 Aug 2013 04:14:33 -0400 Subject: [PATCH] git-annex (4.20130815) unstable; urgency=low * assistant, watcher: .gitignore files and other git ignores are now honored, when git 1.8.4 or newer is installed. (Thanks, Adam Spiers, for getting the necessary support into git for this.) * importfeed: Ignores transient problems with feeds. Only exits nonzero when a feed has repeatedly had a problems for at least 1 day. * importfeed: Fix handling of dots in extensions. * Windows: Added support for encrypted special remotes. * Windows: Fixed permissions problem that prevented removing files from directory special remote. Directory special remotes now fully usable. # imported from the archive --- .ghci | 1 + Annex.hs | 247 + Annex/Branch.hs | 363 + Annex/BranchState.hs | 43 + Annex/CatFile.hs | 92 + Annex/CheckAttr.hs | 35 + Annex/CheckIgnore.hs | 32 + Annex/Content.hs | 511 + Annex/Content/Direct.hs | 251 + Annex/Direct.hs | 232 + Annex/Environment.hs | 65 + Annex/Exception.hs | 39 + Annex/FileMatcher.hs | 101 + Annex/Journal.hs | 104 + Annex/Link.hs | 105 + Annex/LockPool.hs | 56 + Annex/Perms.hs | 105 + Annex/Queue.hs | 62 + Annex/ReplaceFile.hs | 39 + Annex/Ssh.hs | 177 + Annex/TaggedPush.hs | 57 + Annex/UUID.hs | 74 + Annex/Version.hs | 53 + Annex/Wanted.hs | 32 + Assistant.hs | 147 + Assistant/Alert.hs | 311 + Assistant/Alert/Utility.hs | 130 + Assistant/BranchChange.hs | 19 + Assistant/Changes.hs | 47 + Assistant/Commits.hs | 23 + Assistant/Common.hs | 14 + Assistant/DaemonStatus.hs | 259 + Assistant/DeleteRemote.hs | 98 + Assistant/Drop.hs | 112 + Assistant/Install.hs | 101 + Assistant/Install/AutoStart.hs | 39 + Assistant/Install/Menu.hs | 47 + Assistant/MakeRemote.hs | 175 + Assistant/Monad.hs | 141 + Assistant/NamedThread.hs | 91 + Assistant/NetMessager.hs | 176 + Assistant/Pairing.hs | 92 + Assistant/Pairing/MakeRemote.hs | 91 + Assistant/Pairing/Network.hs | 130 + Assistant/Pushes.hs | 40 + Assistant/ScanRemotes.hs | 41 + Assistant/Ssh.hs | 293 + Assistant/Sync.hs | 222 + Assistant/Threads/Committer.hs | 492 + Assistant/Threads/ConfigMonitor.hs | 86 + Assistant/Threads/DaemonStatus.hs | 29 + Assistant/Threads/Glacier.hs | 43 + Assistant/Threads/Merger.hs | 118 + Assistant/Threads/MountWatcher.hs | 192 + Assistant/Threads/NetWatcher.hs | 131 + Assistant/Threads/PairListener.hs | 148 + Assistant/Threads/Pusher.hs | 48 + Assistant/Threads/SanityChecker.hs | 138 + Assistant/Threads/TransferPoller.hs | 56 + Assistant/Threads/TransferScanner.hs | 180 + Assistant/Threads/TransferWatcher.hs | 126 + Assistant/Threads/Transferrer.hs | 140 + Assistant/Threads/Watcher.hs | 352 + Assistant/Threads/WebApp.hs | 101 + Assistant/Threads/XMPPClient.hs | 370 + Assistant/Threads/XMPPPusher.hs | 81 + Assistant/TransferQueue.hs | 223 + Assistant/TransferSlots.hs | 78 + Assistant/TransferrerPool.hs | 82 + Assistant/Types/Alert.hs | 75 + Assistant/Types/BranchChange.hs | 19 + Assistant/Types/Buddies.hs | 80 + Assistant/Types/Changes.hs | 77 + Assistant/Types/Commits.hs | 19 + Assistant/Types/DaemonStatus.hs | 96 + Assistant/Types/NamedThread.hs | 17 + Assistant/Types/NetMessager.hs | 155 + Assistant/Types/Pushes.hs | 24 + Assistant/Types/ScanRemotes.hs | 25 + Assistant/Types/ThreadName.hs | 14 + Assistant/Types/ThreadedMonad.hs | 38 + Assistant/Types/TransferQueue.hs | 29 + Assistant/Types/TransferSlots.hs | 34 + Assistant/Types/TransferrerPool.hs | 23 + Assistant/Types/UrlRenderer.hs | 26 + Assistant/WebApp.hs | 73 + Assistant/WebApp/Common.hs | 17 + Assistant/WebApp/Configurators.hs | 44 + Assistant/WebApp/Configurators/AWS.hs | 239 + Assistant/WebApp/Configurators/Delete.hs | 126 + Assistant/WebApp/Configurators/Edit.hs | 223 + Assistant/WebApp/Configurators/IA.hs | 211 + Assistant/WebApp/Configurators/Local.hs | 412 + Assistant/WebApp/Configurators/Pairing.hs | 327 + Assistant/WebApp/Configurators/Preferences.hs | 103 + Assistant/WebApp/Configurators/Ssh.hs | 382 + Assistant/WebApp/Configurators/WebDAV.hs | 147 + Assistant/WebApp/Configurators/XMPP.hs | 220 + Assistant/WebApp/Control.hs | 71 + Assistant/WebApp/DashBoard.hs | 150 + Assistant/WebApp/Documentation.hs | 42 + Assistant/WebApp/Form.hs | 98 + Assistant/WebApp/Notifications.hs | 95 + Assistant/WebApp/OtherRepos.hs | 68 + Assistant/WebApp/Page.hs | 69 + Assistant/WebApp/RepoList.hs | 255 + Assistant/WebApp/SideBar.hs | 104 + Assistant/WebApp/Types.hs | 221 + Assistant/WebApp/Utility.hs | 120 + Assistant/WebApp/routes | 100 + Assistant/XMPP.hs | 273 + Assistant/XMPP/Buddies.hs | 87 + Assistant/XMPP/Client.hs | 84 + Assistant/XMPP/Git.hs | 382 + Backend.hs | 120 + Backend/SHA.hs | 146 + Backend/URL.hs | 44 + Backend/WORM.hs | 41 + Build/BundledPrograms.hs | 48 + Build/Configure.hs | 183 + Build/DesktopFile.hs | 82 + Build/EvilSplicer.hs | 549 + Build/InstallDesktopFile.hs | 19 + Build/NullSoftInstaller.hs | 139 + Build/OSXMkLibs.hs | 157 + Build/Standalone.hs | 54 + Build/TestConfig.hs | 143 + Build/make-sdist.sh | 21 + Build/mdwn2man | 43 + BuildFlags.hs | 54 + CHANGELOG | 1 + COPYRIGHT | 1 + Checks.hs | 49 + CmdLine.hs | 138 + Command.hs | 123 + Command/Add.hs | 251 + Command/AddUnused.hs | 41 + Command/AddUrl.hs | 174 + Command/Assistant.hs | 71 + Command/Commit.hs | 29 + Command/ConfigList.hs | 25 + Command/Content.hs | 48 + Command/Copy.hs | 38 + Command/Dead.hs | 40 + Command/Describe.hs | 32 + Command/Direct.hs | 63 + Command/Drop.hs | 161 + Command/DropKey.hs | 39 + Command/DropUnused.hs | 43 + Command/EnableRemote.hs | 56 + Command/Find.hs | 61 + Command/Fix.hs | 55 + Command/FromKey.hs | 46 + Command/Fsck.hs | 509 + Command/FuzzTest.hs | 288 + Command/Get.hs | 90 + Command/Group.hs | 35 + Command/Help.hs | 62 + Command/Import.hs | 42 + Command/ImportFeed.hs | 222 + Command/InAnnex.hs | 27 + Command/Indirect.hs | 103 + Command/Init.hs | 31 + Command/InitRemote.hs | 101 + Command/Lock.hs | 29 + Command/Log.hs | 171 + Command/Map.hs | 247 + Command/Merge.hs | 38 + Command/Migrate.hs | 77 + Command/Move.hs | 173 + Command/PreCommit.hs | 53 + Command/ReKey.hs | 71 + Command/RecvKey.hs | 78 + Command/Reinject.hs | 58 + Command/RmUrl.hs | 30 + Command/Semitrust.hs | 32 + Command/SendKey.hs | 51 + Command/Status.hs | 345 + Command/Sync.hs | 344 + Command/Test.hs | 24 + Command/TransferInfo.hs | 64 + Command/TransferKey.hs | 59 + Command/TransferKeys.hs | 142 + Command/Trust.hs | 32 + Command/Unannex.hs | 102 + Command/Ungroup.hs | 35 + Command/Uninit.hs | 109 + Command/Unlock.hs | 50 + Command/Untrust.hs | 32 + Command/Unused.hs | 366 + Command/Upgrade.hs | 28 + Command/Version.hs | 37 + Command/Vicfg.hs | 194 + Command/Watch.hs | 35 + Command/WebApp.hs | 220 + Command/Whereis.hs | 54 + Command/XMPPGit.hs | 43 + Common.hs | 34 + Common/Annex.hs | 8 + Config.hs | 84 + Config/Cost.hs | 82 + Config/Files.hs | 69 + Creds.hs | 151 + Crypto.hs | 163 + Fields.hs | 35 + Git.hs | 140 + Git/AutoCorrect.hs | 71 + Git/Branch.hs | 103 + Git/BuildVersion.hs | 21 + Git/CatFile.hs | 107 + Git/CheckAttr.hs | 64 + Git/CheckIgnore.hs | 71 + Git/Command.hs | 124 + Git/Config.hs | 155 + Git/Construct.hs | 277 + Git/CurrentRepo.hs | 67 + Git/DiffTree.hs | 89 + Git/FilePath.hs | 58 + Git/Filename.hs | 28 + Git/HashObject.hs | 43 + Git/Index.hs | 27 + Git/LsFiles.hs | 193 + Git/LsTree.hs | 60 + Git/Merge.hs | 21 + Git/Queue.hs | 159 + Git/Ref.hs | 108 + Git/Remote.hs | 33 + Git/Sha.hs | 39 + Git/SharedRepository.hs | 27 + Git/Types.hs | 83 + Git/UnionMerge.hs | 110 + Git/UpdateIndex.hs | 80 + Git/Url.hs | 70 + Git/Version.hs | 43 + GitAnnex.hs | 162 + GitAnnex/Options.hs | 67 + GitAnnexShell.hs | 182 + INSTALL | 1 + Init.hs | 201 + Limit.hs | 253 + Locations.hs | 358 + Logs/Group.hs | 86 + Logs/Location.hs | 76 + Logs/PreferredContent.hs | 117 + Logs/Presence.hs | 122 + Logs/Remote.hs | 100 + Logs/Transfer.hs | 388 + Logs/Trust.hs | 122 + Logs/UUID.hs | 99 + Logs/UUIDBased.hs | 114 + Logs/Unused.hs | 45 + Logs/Web.hs | 103 + Makefile | 216 + Messages.hs | 245 + Messages/JSON.hs | 37 + NEWS | 1 + Option.hs | 79 + README | 6 + Remote.hs | 261 + Remote/Bup.hs | 282 + Remote/Directory.hs | 247 + Remote/Git.hs | 507 + Remote/Glacier.hs | 294 + Remote/Helper/AWS.hs | 66 + Remote/Helper/Chunked.hs | 127 + Remote/Helper/Encryptable.hs | 137 + Remote/Helper/Hooks.hs | 94 + Remote/Helper/Special.hs | 40 + Remote/Helper/Ssh.hs | 73 + Remote/Hook.hs | 155 + Remote/List.hs | 103 + Remote/Rsync.hs | 289 + Remote/S3.hs | 336 + Remote/Web.hs | 95 + Remote/WebDAV.hs | 345 + Seek.hs | 169 + Setup.hs | 63 + Test.hs | 1183 +++ Types.hs | 31 + Types/Backend.hs | 26 + Types/BranchState.hs | 16 + Types/Command.hs | 77 + Types/Crypto.hs | 69 + Types/FileMatcher.hs | 13 + Types/GitConfig.hs | 146 + Types/Group.hs | 27 + Types/Key.hs | 90 + Types/KeySource.hs | 29 + Types/Messages.hs | 24 + Types/Option.hs | 17 + Types/Remote.hs | 90 + Types/StandardGroups.hs | 96 + Types/TrustLevel.hs | 41 + Types/UUID.hs | 24 + Upgrade.hs | 31 + Upgrade/V0.hs | 49 + Upgrade/V1.hs | 241 + Upgrade/V2.hs | 137 + Usage.hs | 111 + Utility/Applicative.hs | 16 + Utility/Base64.hs | 24 + Utility/Batch.hs | 40 + Utility/CoProcess.hs | 93 + Utility/CopyFile.hs | 48 + Utility/DBus.hs | 84 + Utility/Daemon.hs | 121 + Utility/DataUnits.hs | 160 + Utility/DirWatcher.hs | 145 + Utility/DirWatcher/Types.hs | 24 + Utility/Directory.hs | 102 + Utility/DiskFree.hs | 38 + Utility/Dot.hs | 63 + Utility/Env.hs | 63 + Utility/Exception.hs | 58 + Utility/ExternalSHA.hs | 67 + Utility/FSEvents.hs | 92 + Utility/FileMode.hs | 135 + Utility/FileSystemEncoding.hs | 93 + Utility/Format.hs | 173 + Utility/FreeDesktop.hs | 144 + Utility/Gpg.hs | 262 + Utility/Gpg/Types.hs | 30 + Utility/HumanNumber.hs | 21 + Utility/HumanTime.hs | 26 + Utility/INotify.hs | 182 + Utility/InodeCache.hs | 91 + Utility/JSONStream.hs | 44 + Utility/Kqueue.hs | 267 + Utility/LogFile.hs | 68 + Utility/Lsof.hs | 120 + Utility/Matcher.hs | 169 + Utility/Metered.hs | 116 + Utility/Misc.hs | 138 + Utility/Monad.hs | 69 + Utility/Mounts.hsc | 93 + Utility/Network.hs | 21 + Utility/NotificationBroadcaster.hs | 86 + Utility/OSX.hs | 44 + Utility/Parallel.hs | 35 + Utility/PartialPrelude.hs | 68 + Utility/Path.hs | 238 + Utility/Percentage.hs | 33 + Utility/Process.hs | 324 + Utility/QuickCheck.hs | 45 + Utility/Rsync.hs | 152 + Utility/SRV.hs | 106 + Utility/SafeCommand.hs | 120 + Utility/Shell.hs | 26 + Utility/TList.hs | 66 + Utility/Tense.hs | 57 + Utility/ThreadLock.hs | 19 + Utility/ThreadScheduler.hs | 69 + Utility/Tmp.hs | 88 + Utility/Touch.hsc | 120 + Utility/Url.hs | 174 + Utility/UserInfo.hs | 55 + Utility/Verifiable.hs | 37 + Utility/WebApp.hs | 281 + Utility/Yesod.hs | 71 + Utility/libdiskfree.c | 73 + Utility/libdiskfree.h | 1 + Utility/libkqueue.c | 74 + Utility/libkqueue.h | 3 + Utility/libmounts.c | 103 + Utility/libmounts.h | 38 + configure.hs | 6 + debian/NEWS | 36 + debian/changelog | 2218 ++++ debian/compat | 1 + debian/control | 87 + debian/copyright | 784 ++ debian/doc-base | 9 + debian/menu | 2 + debian/rules | 14 + doc/Android.mdwn | 53 + ..._77bafc01b47d4cf8f96bde2b6704ed71._comment | 8 + ..._dc7b428f525a082834cb87221fc627ff._comment | 8 + ..._81940ea56ace3dcd5fa84dfccd88ad96._comment | 10 + ..._37aa87a451d4390ed367402eec740855._comment | 12 + doc/Android/oldcomments.mdwn | 2 + ..._20e3d513b8b97496d76aca4619026cd6._comment | 16 + ..._c96b8f1cc1583a74eb2483f48357f023._comment | 15 + ..._6551f5fa081494b079c10a33c9b0d8ad._comment | 10 + ..._7c633d245651ec08f63194fe1fc194ae._comment | 8 + ..._60c2403140085f9caf48a33b59a36ab4._comment | 8 + ..._9af73451be09f03cfff81fdf9481ffc4._comment | 27 + ..._f76561a654b534df3a807b1c045710b2._comment | 8 + ..._1b46cdf154ddadfe17e4b6e4054dc619._comment | 17 + ..._cc9caa5dd22dd67e5c1d22d697096dd2._comment | 15 + ..._5903f6a4a81a6534fa8cfafb3b6c37bb._comment | 8 + ..._36afd354f9669a154d7b6b2c4d43ded9._comment | 8 + ..._de98154792e8611a134429f06d82bcb1._comment | 8 + ..._7ab509c25243009bfbffd796ec64e77b._comment | 10 + ..._026d1a01d5753d71ac3dfc002f2a5eec._comment | 10 + ..._f0a044fb649d43e32c96b08edbc336c3._comment | 12 + ..._6b9ae35b1ceeba14cd7a74e142870705._comment | 34 + ..._c91db1215f529aa68bfb0576c3c5eddc._comment | 10 + ..._c2422b7dd9d526b3616e49f48cf178c2._comment | 10 + ..._0e4980c27b13dbc28477c02a82898248._comment | 14 + ..._86f7b5444e2eaea7f8f7b9160f671a1d._comment | 10 + ..._9d78009435736a178d5a3f5a9bc0ed6a._comment | 8 + ..._7b9523ddb20dc4a929e556c3ed0c7406._comment | 18 + ..._a56628a622da752806c42c5b8b54ceef._comment | 8 + ..._19656ec99b8f6aa64c1d01a3c9ae9bd0._comment | 8 + ..._55e703ae105d0c0ee9ac50df8cc59dfb._comment | 10 + doc/android/DCIM.png | Bin 0 -> 95786 bytes doc/android/appinstalled.png | Bin 0 -> 16805 bytes doc/android/apps.png | Bin 0 -> 53971 bytes doc/android/install.png | Bin 0 -> 55106 bytes doc/android/newwindow.png | Bin 0 -> 1009 bytes doc/android/terminal.png | Bin 0 -> 20565 bytes doc/android/webapp.png | Bin 0 -> 64097 bytes doc/assistant.mdwn | 42 + doc/assistant/addsshserver.png | Bin 0 -> 31740 bytes doc/assistant/archival_walkthrough.mdwn | 32 + doc/assistant/buddylist.png | Bin 0 -> 4347 bytes doc/assistant/cloudnudge.png | Bin 0 -> 7332 bytes doc/assistant/combinerepos.png | Bin 0 -> 10677 bytes ..._f2c4857b7b000e005f0c19279db14eaf._comment | 8 + ..._befa1f48e5a43a7965060491430a6bc4._comment | 9 + doc/assistant/controlmenu.png | Bin 0 -> 8863 bytes doc/assistant/crashrecovery.png | Bin 0 -> 6594 bytes doc/assistant/dashboard.png | Bin 0 -> 41061 bytes doc/assistant/deleterepository.png | Bin 0 -> 22780 bytes doc/assistant/example.png | Bin 0 -> 110994 bytes doc/assistant/iaitem.png | Bin 0 -> 34868 bytes doc/assistant/inotify_max_limit_alert.png | Bin 0 -> 12583 bytes doc/assistant/local_pairing_walkthrough.mdwn | 60 + .../addrepository.png | Bin 0 -> 2259 bytes ..._b33deed054d3aa8cfa6c9e3958643f16._comment | 8 + ..._39f1162b4d43b61e957e7497df4b9e2b._comment | 8 + ..._588869692b290483f58f3a7aa2bfb55f._comment | 17 + ..._f6bf82c263fefe38701709d9dbd974cc._comment | 10 + ..._bada601ea4b7104f162a3e00def4be2b._comment | 19 + ..._01ba0f9bfa0ed066c4b73d2d6028eecc._comment | 8 + ..._17d44229e4fa46c50815672b96a9735a._comment | 10 + ..._b9d4c29cf2cca0427808df6af08fb789._comment | 8 + .../local_pairing_walkthrough/pairing.png | Bin 0 -> 6771 bytes .../local_pairing_walkthrough/pairrequest.png | Bin 0 -> 5383 bytes .../local_pairing_walkthrough/secret.png | Bin 0 -> 5132 bytes .../local_pairing_walkthrough/secretempty.png | Bin 0 -> 9575 bytes doc/assistant/logs.png | Bin 0 -> 33631 bytes doc/assistant/makerepo.png | Bin 0 -> 32061 bytes doc/assistant/menu.png | Bin 0 -> 22921 bytes doc/assistant/osx-app.png | Bin 0 -> 2604 bytes doc/assistant/preferences.png | Bin 0 -> 22815 bytes doc/assistant/quickstart.mdwn | 30 + doc/assistant/release_notes.mdwn | 334 + ..._bd8f376c9d0c1d5ed07fb013907a60ee._comment | 14 + ..._75e0774ad042717fbd059a8a9ec2db1e._comment | 12 + ..._b3bfd8e547e20c51f7c32c6c9424e936._comment | 10 + ..._c6caa2b521b456bb4ce594d64919cffe._comment | 8 + doc/assistant/remote_sharing_walkthrough.mdwn | 12 + ..._e0187b0a926904b363065ab0f850f0b2._comment | 10 + ..._dabcbc9aaf0bdb82716f5a5d55807a21._comment | 8 + doc/assistant/repogroups.png | Bin 0 -> 15636 bytes doc/assistant/repositories.png | Bin 0 -> 63405 bytes doc/assistant/rsync.net.png | Bin 0 -> 61465 bytes doc/assistant/running.png | Bin 0 -> 24664 bytes .../share_with_a_friend_walkthrough.mdwn | 58 + .../buddylist.png | Bin 0 -> 5114 bytes .../pairing.png | Bin 0 -> 6892 bytes .../repolist.png | Bin 0 -> 8525 bytes .../xmppalert.png | Bin 0 -> 4070 bytes doc/assistant/thanks.mdwn | 243 + doc/assistant/thumbnail.png | Bin 0 -> 3491 bytes doc/assistant/xmpp.png | Bin 0 -> 27753 bytes doc/assistant/xmppnudge.png | Bin 0 -> 6156 bytes doc/assistant/xmpppairingend.png | Bin 0 -> 34379 bytes doc/automatic_conflict_resolution.mdwn | 23 + doc/backends.mdwn | 40 + ..._375bb1fb5973e8fa67b763f2dd6e404b._comment | 13 + ..._1f2626eca9004b31a0b7fc1a0df8027b._comment | 24 + ..._fdcbf8727fdefb9942a54689234b9698._comment | 12 + doc/bare_repositories.mdwn | 48 + ..._148e1da70d37d311634a0309a4ff8dcd._comment | 22 + doc/bugs.mdwn | 18 + .../3.20121112:_build_error_in_assistant.mdwn | 432 + ..._b42f40ffd83321ab5cc0ef24ced15e98._comment | 8 + ..._b1d2aa10ea84c5c370b3e76507fc8761._comment | 476 + ..._b38e40d36bba95b16afbce68e7f25a80._comment | 8 + ....20121112_build_fails_on_Ubuntu_12.04.mdwn | 97 + ..._ce2efd2196e7682f4cdbabdb0616d449._comment | 8 + ..._2a6faf662ebb85a8f1c89adcdfb9adb6._comment | 10 + ..._37f34baa34068def1adf794d0942e462._comment | 8 + ..._2f8a859fef9edc8eb93bf1cc74296702._comment | 8 + ..._39__not_in_scope_getAddBoxComR__39__.mdwn | 33 + ...4___because_testpack_won__39__t_build.mdwn | 57 + ..._b7140e2bf1ea9c73ecc9e214095968e7._comment | 8 + ..._6be87b2fb2ed828e7b4bf785729e910e._comment | 9 + doc/bugs/4.20130601_xmpp_sync_error.mdwn | 125 + ..._5b50d97e44cbd5b31ff24537ec3f8603._comment | 14 + ...ository_on_USB_drive_causes_sync_loop.mdwn | 22 + ..._81839a6de7450734ee75b51e47a0898e._comment | 10 + ..._907ce31a31df94984c2bd7aaafe5b10b._comment | 8 + ..._d8a86ae0ae5fa1f91e0b40b8b2ba0406._comment | 10 + ..._1f08fd5dd4f5d8723c2b5391cc3b60f9._comment | 22 + ...e_repository_next_to_the_existing_one.mdwn | 21 + ..._cb781d34889d583663e855c4074f8e0e._comment | 16 + ..._c0c87957d7c7a09664e60571a2ca0e8c._comment | 12 + ...ox.com_remote_on_Android_fails_for_me.mdwn | 20 + ..._0303ce880415d7e043533551c2b24694._comment | 10 + doc/bugs/Adding_git_ssh_remote_fails.mdwn | 32 + ..._05c0bd9ac7c6f0045217fd72fc1f0a1b._comment | 10 + ..._df05456cafdd89e8ceea830199f42d45._comment | 10 + ...cond_remote_repository_over_ssh_fails.mdwn | 41 + ..._308d5f517bf00c8edc53db438de52355._comment | 14 + ...downloads_but_does_not_checkout_files.mdwn | 74 + ..._a_specific_directory_on_a_USB_remote.mdwn | 30 + ..._13ecedfbb34c3564af3a790b8bf0f591._comment | 25 + ...roid_app_permission_denial_on_startup.mdwn | 18 + ..._dc06737997c8883ef0a12dbecd9ac30f._comment | 8 + ..._b444cd6717658116533745c51481dd3d._comment | 8 + ..._66181f34ed7496d1f6601b39e5ae3c65._comment | 13 + ..._ddf5761bf14de30ac97030ad338601ae._comment | 14 + ..._8b9fafa73ebf5f803c7da9531cfb5b34._comment | 10 + ..._58501bb043b4c5836d7472ffd6baa72c._comment | 23 + ..._d3a04dc7bbc1816cccc8d85c73ffb689._comment | 8 + ..._eeabbc0cc434ed84c36a3f4e03fcef36._comment | 10 + ..._4203b496bee1bdd424466ed63b5d31cf._comment | 10 + ..._74373eb2cc46b76659e3c463d6682d15._comment | 10 + ..._0923d2a09df01d152ec4784c92689c96._comment | 8 + ..._b60928e54a5b620899cf29820b9b8e70._comment | 10 + .../Android_daily_build_missing_webapp.mdwn | 23 + ...thinks_file_exists_afer_being_dropped.mdwn | 27 + ..._1d100441fd1ef529eb854b350fece9ee._comment | 29 + ..._166c459c2b27859cf457e17da685fe75._comment | 14 + ..._9d985b6e7973bfaaf8b4f5349d8c13ee._comment | 8 + ..._3e084cff454b95c7170c0225a53f0c30._comment | 11 + ...les_into_git_until_daily_sanity_check.mdwn | 106 + ...ctually_sync_file_contents_by_default.mdwn | 16 + ..._8577fdaa4d49e6241c4372b159694c9c._comment | 12 + ..._027521e48283c68b39315bb8213f6e45._comment | 10 + ..._fd8f6938596aace60b04fb35c4069e37._comment | 10 + ..._ca908021ab5a2a50fd0d4a7e8d12498f._comment | 9 + ..._73532556cfc354ad5f37a3f3a048fb32._comment | 12 + ..._ced397b9e6119a0798a282ee07e885df._comment | 61 + ..._8acb66850e5db8337cf3f2b2dd236ccc._comment | 10 + ..._7eb530851ae6fa1a69813725c4e8fcec._comment | 59 + ..._c7d51a26e1febc3894d02546940d64e5._comment | 19 + ..._has_just_transferred_elsewhere_again.mdwn | 26 + .../Assistant_dropping_from_backup_repo.mdwn | 28 + ..._c13d86fb2541676ee4ca1446b99e0e68._comment | 8 + ...nd_eats_up_all_of_RAM_after_X_restart.mdwn | 24 + ...tant_uses_obsolete_GDU_volume_monitor.mdwn | 28 + ..._0e1db417a5815ea903c1f7ccd07308c4._comment | 8 + ..._28b0cfcba8902c9c16dbe6c4b07984c4._comment | 10 + ..._952b3f78da756ff5f89235db94bec67f._comment | 53 + ..._d86aba42d014c4b4f708dcb5fe86e055._comment | 10 + ..._9aaf296ef53da317d6dc6728705d5c56._comment | 16 + ..._0d5f8a05a1505660f7ff1bc4ac6ff271._comment | 8 + ..._3dfdfd49597c85575cb689adb70d2de6._comment | 8 + ..._943a446c60ed9d7d4f240ba7f00fe925._comment | 8 + ..._9563859850fb40b1cc2c20c516c12960._comment | 16 + ..._cf6221c585ee3dbf039bdaea71842d9b._comment | 9 + ..._Android___39__git_annex_webapp__39__.mdwn | 35 + ..._173393b0b3d2d8c622c0d8a2eaace421._comment | 8 + ...ds___39__hxt__39___added_-_3.20121127.mdwn | 36 + doc/bugs/Build_error_on_Mac_OSX_10.6.mdwn | 11 + doc/bugs/Build_failure_at_commit_1efe4f3.mdwn | 45 + ...ot_find_module___96__Text.Blaze__39__.mdwn | 105 + ..._Not_in_scope:___96__myHomeDir__39___.mdwn | 56 + ...ind_module___96__Data.XML.Types__39__.mdwn | 82 + ...constructor_or_class___96__Html__39__.mdwn | 189 + ...26____47__bin_breaks_the_desktop_link.mdwn | 15 + ..._c0f0a2878070ed86900815c6b6a5fa5e._comment | 8 + ..._53f2de3d3993821d8502fd08a0fcce12._comment | 8 + doc/bugs/Cabal_cannot_solve_dependencies.mdwn | 36 + ..._1d41ac79867226dcb71f1c7b38da062d._comment | 21 + ..._50e72633a4462f6f6eb33d57b137fdcc._comment | 48 + ..._886f2d1f7c47a3973b8dc7d7c412289a._comment | 10 + .../Cabal_dependency_monadIO_missing.mdwn | 17 + ..._14be660aa57fadec0d81b32a8b52c66f._comment | 75 + ..._4f4d8e1e00a2a4f7e8a8ab082e16adac._comment | 8 + ...39__t_always_use__annex-rsync-options.mdwn | 35 + ...__git-annex_get__34___with_3.20111203.mdwn | 27 + ...rive__39___repo_even_if_set_as_client.mdwn | 21 + ..._25eb2d7d0a9cdd1c55df0cec68472723._comment | 10 + ..._9e9b96e5113a50533251e946c2560d81._comment | 17 + ..._6b091198ddd6ed709b076df1296aeb77._comment | 11 + ..._118b588685b535cca4c02eb6ef297c67._comment | 21 + ..._5cead277493e1c020e16be6f9245fe33._comment | 12 + ..._0f135f97c2808dce094628dc6608e617._comment | 8 + ..._1d6f47f9e6cf935f19d68af6d5aa92fa._comment | 10 + ..._c5758fdb32348b9cd804ff17d27864e1._comment | 16 + ...epo__47__.git__47__X__34___for_many_X.mdwn | 34 + ..._7f54e24c8e721d69bdb1e5a4181641b8._comment | 10 + ..._6e91bc254f79ccf80d385ba7d35ffa9c._comment | 14 + ..._4cf34da6050dd96f94ffc3652aa39715._comment | 12 + ..._cafcc24e98a89f10adaed5e09f75b659._comment | 19 + ...e_some_filenames_have_a_colon_in_them.mdwn | 20 + ..._5fc1347f4bcc13c9f8dbc5ecd4847fc7._comment | 12 + ..._38696178e658d1d32deec37dbea66a3d._comment | 8 + ..._t_rename___34__here__34___repository.mdwn | 32 + ...Can__39__t_set_repositories_directory.mdwn | 15 + ..._beb5d5b66a8d0fab12be44a7d877e9b0._comment | 8 + ..._366aa798a5e55350d32b63b31c19112b._comment | 19 + ..._812554d58ad9274a50b2a33d5f4d2ec3._comment | 10 + ..._bec5f147441ad18c97845b44c90c728b._comment | 28 + ...nc_remote_with_encryption__61__shared.mdwn | 54 + ..._ca7ec2041bbec330476fb040b1e66a92._comment | 8 + ..._c476847665a5320214721497d8fad15b._comment | 8 + ...annot_build_the_latest_with_GHC_7.6.1.mdwn | 18 + ..._b25859c159d62f2e92b92f505535131b._comment | 14 + ..._4c9eab9120718457fdc1ae9051e44bca._comment | 16 + ..._61aec9801e1f76db4a286536ffacc3ed._comment | 12 + ..._6381ff0ea419831d9bbed27511cad1e9._comment | 16 + doc/bugs/Cannot_clone_an_annex.mdwn | 69 + ..._b40a2652361a79c6c6eab0fc21be8e46._comment | 8 + .../Cannot_copy_to_a_git-annex_remote.mdwn | 14 + ..._258a376cff4c62bc4be919322bb1bd88._comment | 10 + ..._d9b830a1fdea8760cb7da1d36b3cd34d._comment | 12 + ..._09d76e5f9480b9a35644a8f08790cd97._comment | 10 + ..._7b586c705a937d09a1b44bd6af2d4686._comment | 8 + ..._07dbd8f64982f1921077e23f468122cf._comment | 25 + ..._926fd494f0b27103a99083cd5d0702d5._comment | 8 + ..._80444a509cc340f5eb3cd08b193fd389._comment | 10 + ..._4c6b99cd67b4aa742da5101fb1b379f7._comment | 8 + ..._f45cdd2b6acc5f458b67539fced0e529._comment | 12 + ..._5a455dd14fb9d3ff408bb3f81e366c38._comment | 10 + ..._foo_not___126____47__bar__47____34__.mdwn | 29 + ..._6f7b5c164ff64f00b8814b2ee334709f._comment | 13 + ..._807ef1250237bf4426e3a24c1f9ba357._comment | 10 + doc/bugs/Committer_crashed.mdwn | 32 + ...mpile_needs_more_than_1.5gb_of_memory.mdwn | 16 + ..._0806b5132c55d7a5a17fbdad7e3f2291._comment | 16 + ...ilure_trying_to_unannex_a_large_annex.mdwn | 56 + ..._1c202695ab7fe62cdc8770e1fb428d0c._comment | 10 + ...lPath_too_long_for_Unix_domain_socket.mdwn | 53 + ..._60f58e205604eebe668b1e05dcfbf9a7._comment | 24 + .../Could_not_find_module_Data.Default.mdwn | 33 + doc/bugs/Could_not_resolve_dependencies.mdwn | 40 + ...h_trying_to_sync_with_a_repo_over_ssh.mdwn | 43 + ..._9705f295ad8101f3f0ede18e590b56ef._comment | 8 + ..._0d751d81ac618f8d7e3f1dd20c830542._comment | 8 + .../Crash_when_adding_jabber_account_.mdwn | 32 + ..._2dc61ebcfa8919fb839656999c155c52._comment | 10 + ..._e49af3b8a937d82eda1509b6f67b21d4._comment | 8 + ..._e59f8813bf1a7c4e3c8c120fe82348b9._comment | 10 + ..._716ac138cb69eecd0fb586699b4aeb2a._comment | 8 + ...h_an_invalid_name_throws_an_exception.mdwn | 18 + ...S3_does_not_check_for_presence_of_GPG.mdwn | 18 + ...using_git-annex_webapp_--listen__41__.mdwn | 33 + doc/bugs/DS__95__Store_not_gitignored.mdwn | 26 + ..._b93ac0ea3be82c361ceb4352e742ba39._comment | 8 + ..._4136e1f4aba7aa7562dafcf6a213e10c._comment | 58 + ...p_ssh_keys_after_removing_remote_repo.mdwn | 18 + ..._88fbf70eae48484988dbb433a437c717._comment | 14 + .../Detection_assumes_that_shell_is_bash.mdwn | 24 + ...t_to_troubleshoot_XMPP_login_failures.mdwn | 11 + ..._4205bccf515169031e4a9ed8e905262c._comment | 10 + ...keeps_re-checksuming_duplicated_files.mdwn | 25 + ..._cb10385a4f046bfe676720ded3409379._comment | 14 + ..._4bcf1a897181e40c9c8969d597a844f0._comment | 8 + ..._6a6d22d218f036c9977072973ed99aa8._comment | 11 + ..._eaa7ffb3a1d9ffd6d89de301bd2cd5b2._comment | 11 + ...sitories_end_up_with_unstaged_changes.mdwn | 46 + ..._300a2b246182be3079db20a7e3322261._comment | 8 + ...sitories_still_use_symlinks_sometimes.mdwn | 32 + .../Disconcerting_warning_from_git-annex.mdwn | 6 + ..._58cebd377bfdf247b6c4fee27a3ba461._comment | 8 + ..._dc7407044d4c739d05248300c58d8ef2._comment | 8 + doc/bugs/Displayed_copy_speed_is_wrong.mdwn | 8 + ..._74de3091e8bfd7acd6795e61f39f07c6._comment | 8 + ..._8b240de1d5ae9229fa2d77d1cc15a552._comment | 8 + ..._ssh_server_with_multiple_directories.mdwn | 19 + ..._e8affeca873c2ef73255f8f77e0ac16f._comment | 10 + ...___versions_3.20120315_and_3.20120430.mdwn | 79 + ...ng_remote_repository_using_ssh_on_OSX.mdwn | 36 + ..._559555934d79ae6be383063abcaae22e._comment | 10 + ..._a9f4f9db042ab6f6c15d6954651971b2._comment | 12 + ..._55a496d0a0be80ba723b17bf9faa3bc0._comment | 8 + ...ing___34__hGetLine:_end_of_file__34__.mdwn | 31 + ..._8742f7ac27b5f4ad6261d04a174a691c._comment | 10 + ..._b8e720340000537de6713c49b7733b2f._comment | 21 + ..._489fa3a717519cd5d8b4c1a9d143d8c6._comment | 8 + ..._b0796d3b1913e1b6f7b34d75a591be42._comment | 16 + ..._d8ca17ccaa5ee48d590736af8e77d88a._comment | 8 + ..._aa7a690aaf75d21f52051a31d7fce70e._comment | 8 + ..._dc235dc2d024b7f340721bb578630e00._comment | 10 + ..._5d1e6ea5b5725c773acc6e288add812c._comment | 8 + ..._6389b4f03ebc916358bc6674398d70c4._comment | 14 + ..._bcacc9fb3751042968118ebe33802e27._comment | 10 + ..._6d4c9f0e133ebd94fc11346df446402e._comment | 16 + ...nnexed_file_to_a_.gitignored_location.mdwn | 21 + ...ateSymbolicLink:_already_exists__34__.mdwn | 46 + ...w_file_gets_symlinked_to_a_git_object.mdwn | 78 + ..._d4e7ed56b16494a95e6c904c746cc91f._comment | 8 + ..._656b2a2cc44e9102c86bdd57045549d5._comment | 10 + ...__40__calling_nonexistant_shell__41__.mdwn | 26 + ..._fb8a379ed7f4b88bd55245ce5b18042c._comment | 8 + ...te_remote_repo_if_no_global_email_set.mdwn | 55 + ...om_locally_paired_annexes_when_edited.mdwn | 36 + ..._bdc97db9dc9954331e4c400baf9e5541._comment | 10 + ...dg-open_on_linux__47__bsd__63____41__.mdwn | 26 + doc/bugs/GIT_DIR_support_incomplete.mdwn | 17 + doc/bugs/GPG_passphrase_repeated_prompt.mdwn | 24 + ..._6ef1c9725befc84ad57bce196ef630ef._comment | 16 + doc/bugs/GPG_problem_on_Mac.mdwn | 34 + ..._9ccfa12e7a9569a7ae9a3b819917c275._comment | 9 + ..._a5e07131e2bc1a646c8439fc2506128b._comment | 29 + ..._388238360f2423f84881e904443efb86._comment | 12 + ...hen_submodule_is_not_in_the_same_path.mdwn | 63 + .../Glacier_remote_uploads_duplicates.mdwn | 36 + ..._8aef582a0f0d0c7f764b425fc45de3b4._comment | 25 + ..._150ce8b7c4424a83c4b1760da5a89d27._comment | 8 + ..._718af5048c5f894eee134547a2e0a644._comment | 8 + ..._184ad0f8c2847309632f8c18b918cd42._comment | 10 + ..._6980a912d3582c2f2511e4827e9e76b3._comment | 21 + ..._feea067d6856af2840604782b29af86a._comment | 12 + ..._e96187bad3dae2f5f95118f6df87a1ec._comment | 10 + ...de_archive_directory_at_the_same_time.mdwn | 16 + ..._e8bb3d6a2318402b985caed08282d473._comment | 12 + ..._ead9fa75a12ef36be9a92637b144e74f._comment | 14 + .../Hanging_on_install_on_Mountain_lion.mdwn | 26 + ..._f57ff027b19ca16e2ecf1fc6aee9ef4a._comment | 10 + ..._2ff78d2090d0fd3418ab50b27c6028ce._comment | 8 + ..._523d3c0c71f80536850a001b90fd0e9e._comment | 8 + ..._6c360c64093b016c2150206dc3ad1709._comment | 10 + ..._7b77fd9b7dc236c345f2f6149c8138ee._comment | 18 + ..._08289596445d7588e43d35490fbfe5f4._comment | 8 + ..._2a336fe7b8aed07cbdaa868bd34078f9._comment | 8 + ..._ea7a40c3b6748738421aed00a6f7ca10._comment | 10 + ..._00962da9288976f8a48d0cbc08e1d9e2._comment | 15 + ..._5d53d23e529f33f6e7deb10643831613._comment | 11 + ..._f00c8761e3184975b6645c0c3e241365._comment | 10 + ...eating_repository_when_using_--listen.mdwn | 46 + ..._8cbe786de8cf8b407418149b9c811aab._comment | 14 + .../Hard_links_not_synced_in_direct_mode.mdwn | 125 + ..._aaa781664ae0c62c4f6530cb075ed367._comment | 17 + ..._213aa10909d1fd0f20ed078a7ed93e79._comment | 8 + ..._e6b783d9aaae20c0d35e9888d878716a._comment | 10 + ..._b008ae7b1cf8685d92c9a87a7609de1e._comment | 18 + ..._949c891209713a2c0a5e66af11ed4c79._comment | 14 + ...on_switch_to_indirect_mode_and_status.mdwn | 61 + ..._94c678e1348280a96f11d7456c240d3a._comment | 8 + ..._09450d58df2373174a1f0d90b08e9eb3._comment | 14 + ...orrect_version_on_64_Standalone_Build.mdwn | 11 + ..._1964e4cad33a9f98b2eedbf095e899ff._comment | 12 + ...Install_of_git-annex-3.20121112_fails.mdwn | 20 + ..._80fc80151d4390bd8a4332f30723962e._comment | 8 + ..._2613320a41a74dc757a3277c8c328bd0._comment | 62 + ..._c364764d0c56e8dc3cac276905d99841._comment | 10 + ..._f1057340dfa978071d3bbc9e2af1e612._comment | 19 + ..._9007b1a3abd647945604968db19cb841._comment | 8 + ..._0bb3ac5375f29ce9d3d0be93879267e3._comment | 11 + ..._ae4443b8cd069080d1f77fca16aa8b04._comment | 10 + .../Internal_Server_Error:_Unknown_UUID.mdwn | 37 + ..._f42f703a5d267557abf5e932f0890d4a._comment | 37 + ..._eb1999f99c5babf3fcb1ff5d72ea6db6._comment | 18 + ..._bda72b0d615843d18d6ef21f833432a8._comment | 9 + ..._651440cda405ad40a04479f5d87d581e._comment | 10 + ..._21fa189b631c246ac5df16a49c3c0178._comment | 8 + ..._1f712693d2ded5abceb869fdb7f47ef3._comment | 12 + ..._7a5ead0ce5c9429d4723ccce4f6a6d6c._comment | 14 + ..._a4683fd73ae452a9cd7f61d9930f6266._comment | 10 + ...rror_unknown_UUID__59___cannot_modify.mdwn | 26 + ..._repo_after_deleted_an_encrypted_one..mdwn | 28 + ...server_error_adding_USB_drive_on_OS_X.mdwn | 25 + ..._b2ef077d87a9da624f20649c21401b5b._comment | 17 + ..._ef849e25b0264808bff800d9d3836119._comment | 10 + ..._ae3cbd0eb69cbeb9b349e0060d056d43._comment | 18 + ..._0ff2897805928b14829b7b369a3aed91._comment | 16 + ..._414a45573aeb5201f4d80433955669d5._comment | 12 + ..._cause_all_following_switches_to_fail.mdwn | 50 + ..._limit_uploads_to_an_S3_backend__63__.mdwn | 19 + ..._ef97e735ce308f7bcc03f5d9fda588bf._comment | 10 + ..._539b89de8743e435386b86119d1e982f._comment | 8 + .../Issue_on_OSX_with_some_system_limits.mdwn | 26 + ..._5fc1eedb5231edc37c87a2d9b91313b9._comment | 12 + ..._b14e697c211843163285aaa8de5bf4c6._comment | 12 + ..._18ddf8b5934dd6fb1676cd6adc7d103b._comment | 19 + ..._c25a8eb369e546f65e1a72d89f43066f._comment | 12 + ..._6407a3e7aa0316cba2994bfef0e3c633._comment | 37 + ..._f01887695e8b8386e125464c6d401565._comment | 8 + ..._c7776d5b2d073e0d2ae36515185c25aa._comment | 17 + ..._easy_to_turn_git-annex_into_a_zombie.mdwn | 25 + ..._d5fba6c061fb21795021ea83070dbfa2._comment | 8 + ..._12cba707239018989e8d5b6f456fa754._comment | 10 + ...roken_with___34__git_annex_sync__34__.mdwn | 21 + ..._380a49b3c132f9f529729a1cb5a69621._comment | 8 + ..._282f5f89fb4a46e1fad0980e0b2994a0._comment | 8 + ..._7ff98958146b7f6396226bdd878ec86e._comment | 10 + ..._f9e460a09e7e5f53c16c20ded2649201._comment | 8 + ...daemon_leaves_ssh_mux_sessions_behind.mdwn | 38 + ..._17879b98a5e79ace03b543064751e46e._comment | 8 + ..._2dc877e281750004b16619ea7b931160._comment | 8 + ...esult_in_stale_symlinks_and_data_loss.mdwn | 50 + ..._fbb410a54bb0bd82d0953ef58a88600e._comment | 24 + ..._8007c9ba42a951a4426255ec3c37d961._comment | 13 + ..._73ecd4cb8ee58a8dfe7cab0e893dbe5b._comment | 8 + ..._e8a10886a564f35414c30a04335d9d32._comment | 8 + ..._6a318edfe45c80343d017dc7b4837acb._comment | 8 + ..._f7a1d9f9d40aff531d873a95d2196edd._comment | 8 + ..._1724ffdf986301bf37ef7a6d16b6ea8a._comment | 10 + ..._5470e2f50e6506139ecb1b342371c509._comment | 10 + ...rsions_didn__39__t_show_up_on_hackage.mdwn | 11 + ..._74b56dea2100450e322e726bb55bb310._comment | 8 + ...s_support_for_glibc_2.13_debian_stable.txt | 43 + ..._dc7f726a0b60f64392cbbd1b4317bab5._comment | 10 + ..._4a0198d714bd3b52ba9baa68dc45f535._comment | 12 + doc/bugs/Local_files_not_found.mdwn | 50 + ..._5e1fcc0597594fa493ffa28aa32e1df8._comment | 12 + ...40__ssh__41___fails_to_pair__47__sync.mdwn | 175 + ..._bab9cd5bdcffec3c48b9e8657cd9bbf7._comment | 14 + ..._104898dce3c67c082a9f2b36e2f45ff8._comment | 10 + ...l_pairing_fails:_PairListener_crashed.mdwn | 18 + ..._d9c5d2147cf6d8d8477eb13b72081d46._comment | 12 + ..._60a21105145ac228f486bc4beb2ea54d._comment | 32 + doc/bugs/Lost_S3_Remote.mdwn | 59 + ..._6e80e6db6671581d471fc9a54181c04c._comment | 10 + ..._c99c65882a3924f4890e500f9492b442._comment | 8 + ..._1e434d5a20a692cd9dc7f6f8f20f30dd._comment | 8 + .../Makefile_is_missing_dependancies.mdwn | 47 + ..._5a3da5f79c8563c7a450aa29728abe7c._comment | 47 + ..._416f12dbd0c2b841fac8164645b81df5._comment | 8 + ..._c38b6f4abc9b9ad413c3b83ca04386c3._comment | 25 + ..._cc13873175edf191047282700315beee._comment | 30 + ..._0a1c52e2c96d19b9c3eb7e99b8c2434f._comment | 59 + ..._24119fc5d5963ce9dd669f7dcf006859._comment | 10 + ..._96fd4725df4b54e670077a18d3ac4943._comment | 12 + ..._a3555e3286cdc2bfeb9cde0ff727ba74._comment | 8 + ...Manual_content_mode_isn__39__t_manual.mdwn | 89 + doc/bugs/Manual_mode_weirdness.mdwn | 37 + ..._f8ab3bac9e9a6768e5fd5a052f0d920f._comment | 8 + ..._e810daa488fad32ca8bdaae620051da8._comment | 8 + ...cc26206c4a539999b04664136c6f785211a41.mdwn | 35 + ..._after_local_pairing_with_older_annex.mdwn | 31 + ..._8229df64a872bee7590f75eb78f78c4a._comment | 10 + ..._f37be896396915b1c85cff8811dceb4a._comment | 12 + ..._df7fc1078059538a76f384a40541e91f._comment | 10 + ..._70c444c61f41df2f59294c10f94f0c09._comment | 8 + ...ness_with_the_assistant_branch_on_OSX.mdwn | 15 + ..._377525e70640751e1ead445aeed15efa._comment | 8 + ...t-annex_will_not_build_on_OpenIndiana.mdwn | 36 + ..._f3c336ecfee51e074ea3a9fc95301de5._comment | 8 + ..._102c0e998934e84deca92fd1c90145fa._comment | 8 + ..._1449dd796ce9f2209f085d4b017a5f33._comment | 19 + ..._c4aa8a4379b2c056ca9b7afcff412bbc._comment | 10 + ..._6ca4dd2ad51182edf7198f38b336b9b6._comment | 8 + ...heme_does_not_follow_git__39__s_rules.mdwn | 31 + ...nstall_c2hs_-_3.20121127_and_previous.mdwn | 37 + ...way_to_re-inject_a_file_into_an_annex.mdwn | 12 + ..._c871605e187f539f3bfe7478433e7fb5._comment | 8 + ..._e6f1e9eee8b8dfb60ca10c8cfd807ac9._comment | 10 + ..._be62be5fe819acc0cb8b878802decd46._comment | 14 + ..._480a4f72445a636eab1b1c0f816d365c._comment | 8 + doc/bugs/No_progress_bars_with_S3.mdwn | 26 + ..._33a601201a9fdd2357f1c03e32fa6b9c._comment | 8 + ..._52361805ced99c22d663b3b1e8a5b221._comment | 8 + ..._5903c1c40c4562f4fbaccd1640fedb18._comment | 8 + ..._80799c33e513384894b390fe34ab312a._comment | 8 + doc/bugs/No_version_information_from_cli.mdwn | 18 + ...lias_permissions_and_versions_problem.mdwn | 37 + ..._4fabe32e7e626e6ca23aa0b6f449c4c6._comment | 14 + ..._064d60fcc8366a70958540bc145e611a._comment | 11 + ..._6c72d4f40ea0a9566a1185901beff5ba._comment | 14 + ..._8a11f404bb72a1aeb2290744cce2d00d._comment | 12 + ..._30888607199d6a48b76d0c48f5aa4f64._comment | 8 + doc/bugs/OSX_app_issues.mdwn | 6 + ..._54d8f3e429df9a9958370635c890abf0._comment | 11 + ..._6d23232fbb15d0ee3ab532a4884f81ed._comment | 10 + ..._5db2baa771fd01a284eac8a16c1c8c67._comment | 8 + ..._bb2ceb95a844449795addee6986d0763._comment | 26 + ..._62170597c7f441d84d48986857998858._comment | 10 + ..._f3bc5a4e4895ac9351786f0bdd8005ba._comment | 11 + ..._fd560811c57df5cbc3976639642b8b19._comment | 8 + ..._93e0bb53ac2d7daef53426fbdc5f92d9._comment | 15 + ..._141eac2f3fb25fe18b4268786f00ad6a._comment | 8 + ..._f4d5b2645d7f29b80925159efb94a998._comment | 8 + ..._2e6dfca0fd8df04066769653724eae28._comment | 17 + ..._e1bbe83a1b9a7385ed6d443d0cc22bc7._comment | 18 + doc/bugs/OSX_app_issues/old.mdwn | 1 + ..._bb823dc3cd6dc914ed14c176afa0b2f3._comment | 8 + ..._a30e69fed14b0809184ffe05358ab871._comment | 10 + ..._23d47b3696e537d60df1d383f33f19e4._comment | 15 + ..._be5738b42b13ec8cd828c5fa66f030e8._comment | 10 + ..._5783a4716cd104e1f1c276aa0b9cb153._comment | 41 + ..._e126d87a263f3aa6261f72ee7ff086fc._comment | 8 + ..._56c7fcafc7dca8be28ebf9e37a8f6b71._comment | 23 + ..._e58bd3d66f0f43c159d2b37172f152de._comment | 8 + ..._01f2c968bad66b0ff0c09eb468325deb._comment | 8 + ..._0b7cd3d5952c5abf36a89a68a4afc1e7._comment | 8 + ..._82d9963e1fbf17644ce697e5a43943f5._comment | 16 + ..._c2de94a48e7958b9efffd89dda9144ff._comment | 59 + ..._29af9df9ea295d114574e76e15b8e737._comment | 8 + ..._88ddc846eb4e4a2d54028a3412ba28d6._comment | 12 + ..._6d6341b05123cd317c4eac96353c8662._comment | 10 + ..._aff4ab761c4d196732baa046af45fe24._comment | 11 + ..._43bd5985d8a3a5e7f826a34e5dd9216e._comment | 10 + ..._08613b2e2318680508483d204a43da76._comment | 76 + ..._4cda124b57ddc87645d5822f14ed5c59._comment | 8 + ..._0d1df34f83a8dac9c438d93806236818._comment | 8 + ..._12bd83e7e2327c992448e87bdb85d17e._comment | 15 + ..._bc44d5aea5f77e331a32913ada293730._comment | 27 + ..._cea97dbbfb566a9fe463365ca4511119._comment | 16 + ..._911f187d46890093a54859032ada2442._comment | 10 + ..._acd73cc5c4caa88099e2d2f19947aadf._comment | 8 + ..._08b091a58106ca6050ac669579ed9ff4._comment | 11 + ..._8464c839cb169a4c6e72bebdc2065e9a._comment | 8 + ...rror:__LSOpenURLsWithRole__40____41__.mdwn | 26 + ..._0dfa839f1ba689b23f811787515b8cff._comment | 8 + ..._612b947eb5474f6d792a833e33105665._comment | 8 + ..._549b8bcae6f1f8b21932b734e32fbdd1._comment | 8 + ..._23078dfea127fa3ef20696eb10ce964c._comment | 10 + ..._7da5ef8325b8787bbf1c6e2c17b1142e._comment | 8 + .../OS_X_10.8:_Can__39__t_reopen_webapp.mdwn | 31 + ..._2653fe701a1bb20254f3d6b90f10a43b._comment | 18 + ..._d9ce701d077e40f39b142ce2cc570a3b._comment | 8 + ..._14964ab68253dc1a8903d14a821b8b40._comment | 10 + ..._4a579e9a13305ab4157f4b3eba46b92d._comment | 16 + ..._2a710960dc3a177ce62ef92f8546c496._comment | 12 + ..._a4ad73530cd0f6621bcc6394d5f39af7._comment | 41 + doc/bugs/Old_repository_stuck.mdwn | 9 + ...Error_when_push.default___61___simple.mdwn | 62 + ..._971224d2c0c0ce8d4530b1991508f849._comment | 12 + ..._6866f96277dbe83a8aadcdeb426b6750._comment | 8 + ...__44___but_believes_it_has_succeeded..mdwn | 180 + ...P_sends_URLs_with_incorrect_separator.mdwn | 186 + ...use_a_USB_disk_annex_created_on_Linux.mdwn | 16 + ..._f224f4155d857a59595658357f97dac1._comment | 12 + ..._a_unix-style_local_remote_configured.mdwn | 20 + ...t_is_not_used__44___even_if_available.mdwn | 67 + ...__41___even_if_repo_exists_on_Android.mdwn | 40 + ..._9f10bf273b15e93f1eea029f091f26cb._comment | 12 + ..._in_fsck_whereis_find_and_status_cmds.mdwn | 80 + ..._3aef6ca929fad198f2dda0868f2d49cb._comment | 18 + ..._f2c1aa84a0d04e840cb34ae15eb1cb03._comment | 8 + ..._480c39648e3ca6fc58c30377bdb25a8c._comment | 16 + ..._b31496b37046a9886f632ba4f11c56e3._comment | 8 + ..._d25ff424dda1f6021c1ba20f79d71ffc._comment | 12 + ...mment_in_ssh_public_key_ssh-rsa__34__.mdwn | 23 + .../Partial_direct__47__indirect_repo.mdwn | 24 + ..._42344fce051d759f95215c985e9d1135._comment | 12 + ..._8ba64f2750d0ef4adf595674c723bc65._comment | 8 + ..._bd4985864b7dcd70a609ca7bc2617e4a._comment | 8 + ..._39__typechange__39___and_direct_mode.mdwn | 32 + ..._84cb8c651584ec2887f6e1b7dc107190._comment | 8 + ...d_now_asks_for_a_commit_message__34__.mdwn | 18 + doc/bugs/Prevent_accidental_merges.mdwn | 14 + ..._4c46a193915eab8f308a04175cb2e40a._comment | 8 + .../Problem_with_bup:_cannot_lock_refs.mdwn | 52 + doc/bugs/Problems_building_on_Mac_OS_X.mdwn | 62 + ..._1c199b826fdd84b5184b1466ad03a9a4._comment | 8 + doc/bugs/Problems_running_make_on_osx.mdwn | 49 + ..._94e4ac430140042a2d0fb5a16d86b4e5._comment | 8 + ..._56f1143fa191361d63b441741699e17f._comment | 8 + ..._ec5131624d0d2285d3b6880e47033f97._comment | 8 + ..._88ed095a448096bf8a69015a04e64df1._comment | 16 + ..._89a960b6706ed703b390a81a8bc4e311._comment | 8 + ..._6b8867b8e48bf807c955779c9f8f0909._comment | 71 + ..._5c2dd6002aadaab30841b77a5f5aed34._comment | 8 + ..._62fccb04b0e4b695312f7a3f32fb96ee._comment | 43 + ..._64fab50d95de619eb2e8f08f90237de1._comment | 24 + ..._4253988ed178054c8b6400beeed68a29._comment | 11 + ..._34120e82331ace01a6a4960862d38f2d._comment | 17 + ..._7db27d1a22666c831848bc6c06d66a84._comment | 10 + ..._cc53d1681d576186dbc868dd9801d551._comment | 8 + ..._68f0f8ae953589ae26d57310b40c878d._comment | 57 + ..._c52be386f79f14c8570a8f1397c68581._comment | 12 + ..._7f1330a1e541b0f3e2192e596d7f7bee._comment | 107 + ..._0c46f5165ceb5a7b9ea9689c33b3a4f8._comment | 9 + ..._237a137cce58a28abcc736cbf2c420b0._comment | 22 + ..._efafa203addf8fa79e33e21a87fb5a2b._comment | 8 + ..._cc283b485b3c95ba7eebc8f0c96969b3._comment | 66 + doc/bugs/Problems_with_syncing_gnucash.mdwn | 568 + ..._ca195af3ba4a286eb5ab687634192fa4._comment | 8 + ..._754fb430381ad88e6248ecb902b32118._comment | 20 + ..._25881998c6f149c70b1358f37b7c66ba._comment | 20 + doc/bugs/Provide_64-bit_standalone_build.mdwn | 6 + ..._1850bb3eb464f1d3c122cfeb4ccaf265._comment | 15 + doc/bugs/Proxy_support.mdwn | 18 + ...mote_repo_and_set_operation_with_find.mdwn | 6 + ...positories_have_to_be_setup_encrypted.mdwn | 27 + ..._95f73315657bc35a8d3ff9b4ba207af0._comment | 8 + ..._sides_have_written_to_the_repository.mdwn | 70 + ..._92211091daf9827a4ec7e5b5a6769d59._comment | 8 + ..._f0fa97a9eba1c624f6f8720ba8a160b7._comment | 8 + ..._e3d677ea4170c07cd31efe6dc85fa5f3._comment | 8 + ...e_cannot_be_reactivated_by_the_webapp.mdwn | 30 + doc/bugs/Repository_deletion_error.mdwn | 46 + ..._31673d0300986b6098d1af2cc4b180c6._comment | 10 + doc/bugs/Resource_exhausted.mdwn | 45 + ..._a5ef7a62d4ed9365f9448520bb17e3b5._comment | 9 + ..._cdba2015e603f3c21f3e1697dd6fafcd._comment | 18 + ..._747d16d050fdcf69dd3d2bc5ca469a2e._comment | 39 + ..._1e9b74e60da57c3d5f08c1eb3801c1d2._comment | 10 + ..._f55d933bce77fd2185ebd0cc46fe57ec._comment | 64 + ..._26c98fca45b029a527f9684873db4be5._comment | 18 + ..._8bab413b472f900e04977db2bc3951b6._comment | 8 + ..._e9bec0b80179b1229b6af0979a21c727._comment | 9 + ...somewhere_in_the___39__get__39___code.mdwn | 24 + ..._66b21720cd1b2a4f66ef24252d3e6305._comment | 8 + ..._18c9f55c5af1f4f690a7727df71ab561._comment | 8 + ...ks_for_ssh_key_password_for_each_file.mdwn | 30 + ..._fd95e0bb61e80a72b4ac1304ef6c2e77._comment | 16 + ...mote_created_via_webapp_remains_empty.mdwn | 138 + ..._cccf9d58c0ebb8d31cacdd029ea8e23a._comment | 12 + ...e_same_key_for_encryption_and_hashing.mdwn | 10 + ..._dc5ae7af499203cfd903e866595b8fea._comment | 18 + ..._c62daf5b3bfcd2f684262c96ef6628c1._comment | 12 + ..._e1f39c4af5bdb0daabf000da80858cd9._comment | 10 + ..._bb6b814ab961818d514f6553455d2bf3._comment | 8 + ..._5bb128f6d2ca4b5e4d881fae297fa1f8._comment | 8 + ..._63fb74da342751fc35e1850409c506f6._comment | 8 + doc/bugs/S3_memory_leaks.mdwn | 14 + doc/bugs/S3_upload_not_using_multipart.mdwn | 53 + ...SH_too_old_on_OS_X_10.6.8__63____41__.mdwn | 27 + ..._0c57a2196d35eb1ecfb0c51273bba05c._comment | 10 + ...lts_on_Fedora_18_with_SELinux_enabled.mdwn | 65 + ..._f708d87aa65cd38c20087859d3ab2dc7._comment | 12 + ..._fb7188db031147992f3c906783ebbee0._comment | 59 + ...tificates_with_jabber_fail_miserably..mdwn | 22 + ..._13d27ba41d9ef78c8db534b6bc26314e._comment | 12 + ..._018eed99e71680be9e7c0844020419bb._comment | 8 + ...d_ignore_.thumbnails__47___on_android.mdwn | 28 + ...en_network_fails___40__esp._DNS__41__.mdwn | 50 + ..._dd792bd98a48554a65150c06401ed3e5._comment | 12 + .../Small_archive_behaving_like_archive.mdwn | 33 + ..._718dc246cbbbeae04436fa033011ab12._comment | 13 + ..._34___instead_applies_it_to_all_files.mdwn | 75 + ..._2fe6d735bc075275a6b8890fac48ee58._comment | 18 + doc/bugs/Stale_lock_files_on_Android.mdwn | 37 + doc/bugs/Stress_test.mdwn | 45 + ..._1694e990eab6592159309c231c6dcc16._comment | 12 + ..._ab4cb6eefd279e6c1f229e089f703581._comment | 25 + ..._c4c764488ac082f5c48d3a6b4b5fba42._comment | 17 + ..._42125bba09a0ea9821cda7183e458100._comment | 47 + ..._8240e61106b494d3600ad91f16eb5b1c._comment | 20 + ..._c38d84e0dcc834931804c44bce7f7b7a._comment | 11 + ..._60ce20ee255451c4ea809ba475561adb._comment | 15 + ..._1371562e201393986cd41597f6f288cb._comment | 14 + ..._a14be7699da224a8f6c9b34f1b911219._comment | 8 + ..._a01995bdca7ade7dde9842b53fbc4e0c._comment | 57 + ..._9f7efe81b7e40aaa04a865394c53e20f._comment | 52 + ...omps_on___39__regular__39___git_files.mdwn | 27 + ..._0d2cb3b8509cd0eba50aafa14afefc02._comment | 63 + ...mode_to_direct_mode_breaks_duplicates.mdwn | 30 + ...er_is_not_honoring_--listen_parameter.mdwn | 21 + ..._4dd773372979dd95538bfba6516a11eb._comment | 9 + ..._broken_links_instead_of_proper_files.mdwn | 51 + ..._a2bedb2e77451b02fc66fc9ef5c4405c._comment | 11 + .../Test_failure_on_debian_dropunused.mdwn | 31 + doc/bugs/The_assistant_hangs_forever.mdwn | 46 + ..._b0291e32860e0da0b66837d14ed5aab6._comment | 8 + ..._a2950cf91b8a4e4f2951f5522ef0e9c4._comment | 18 + ..._db95f78519d5ffbad793906028730dab._comment | 12 + ..._28b13fd3165b38a2fbc9e1a461c38921._comment | 22 + ..._81a79c8840ff26307a9c6edad5b850f9._comment | 9 + ..._b739719b14705f4d7e1d412b3cab090c._comment | 17 + ..._2b300d960697c5b967c1f109dfd6dfbf._comment | 16 + ..._8623220d08b1a72ed8b669a2d9cc0f75._comment | 15 + ...d_login___40__nearly__41___impossible.mdwn | 29 + ..._8305becdc6e70abdaf17e42f263173fc._comment | 12 + ..._d75896a6e204d1abdda04923aa668d04._comment | 8 + ..._a36a4a64a04c01c2db467b09300e6ebd._comment | 8 + ..._c9d6631c304acb289e485fb901e1f274._comment | 35 + ..._10282c4352075c8d148b8674973b7b16._comment | 22 + ..._ceb68da01d9e2fe9a70fab6244116da0._comment | 15 + ..._cca4abde86a8be5e2919c4738f5bdd0c._comment | 18 + ..._2fa5d7d9110c91b0a3a833cb3d9f53fd._comment | 10 + ..._bf21d28142e4c304aa0bc740955ddea0._comment | 10 + ..._45537758fa937f16fc82120bf8b234e8._comment | 8 + ..._a38497772834a4b12137390b461ce70b._comment | 12 + ..._b685050ee6fbb1a685e33f9656a10e84._comment | 8 + ..._17bc0220c20553c848875475c5fd4ae6._comment | 12 + ..._76472bc58bb790f773c46ec2c39fcf88._comment | 11 + ..._dcd9286e314779c25764484beff40561._comment | 10 + ..._2146eec77b87b615100d0d003e8dce75._comment | 15 + ..._2bd6f4e04903ee251d43d0a97bd40b6e._comment | 8 + ..._7db8ed002eb6313b07f09bd1a34019e3._comment | 10 + ..._1bcb2a238006044bc78849e56cb21a01._comment | 9 + ..._26c6937cf78e7141e0e3b20f25ed8f7a._comment | 8 + ...dfb2e706da2cb1451193c658dc676b0530968.mdwn | 23 + ...sn__39__t_allow_deleting_repositories.mdwn | 33 + ..._1b80f9cfedd25e34997fa07e08d15012._comment | 8 + ..._53499da1185c56d8fd25f86ba41d96ce._comment | 11 + ..._3e07b8386d2c7afce2a78d24b9c260b9._comment | 14 + .../TransferScanner_crash_on_Android.mdwn | 24 + ..._6c3584ade1ee6cccddddeaa8e1697945._comment | 20 + ..._06574e05149a677d666a722061586658._comment | 10 + ..._54ae097d30bb7a49fe151f38c9bac033._comment | 8 + ...oad_to_remote_although_remote_is_dead.mdwn | 51 + ..._108b3984891f82429430b503cddfb3c1._comment | 10 + ..._fa5b1bc26ed3e5bfe48441490c94fe3a._comment | 8 + ..._0a785b5dfbf4eef30854d6bedb12b7d1._comment | 10 + ...Trouble_initializing_git_annex_on_NFS.mdwn | 16 + ..._e26952373150d63b8a5d3643a2762de1._comment | 10 + ..._f80b10ed395738e50e345fc22c708ae5._comment | 10 + ..._f99e0f05950fc2fc80fdecd35e17012c._comment | 8 + ..._e42146d2dcc4052266dd61d204aeb551._comment | 8 + doc/bugs/True_backup_support.mdwn | 7 + ..._50aa0bc1e2502622585682cb703e0b85._comment | 10 + ..._d6030c6c49b227e022f05d590746d4ca._comment | 8 + .../Truncated_file_transferred_via_S3.mdwn | 614 ++ ..._5962358e6067448f633cc0eaf42f9ca7._comment | 10 + ..._75a2c272c36fc4fe8f9a79a3fd3ac4e5._comment | 19 + ..._3dae1914c8c90fdad0c21e1fc795f2ca._comment | 8 + ..._3c5fe109f2196cfc196c30da3b62bafd._comment | 10 + ..._f86f83c89300f255e730ddd23f876f61._comment | 16 + ..._on_Android_due_to_weird_rename_error.mdwn | 37 + ..._928289956111d1b22f9d55f15b54f72f._comment | 10 + ..._6a0cb836b93ba4cb1e07b11d5d2a7094._comment | 8 + .../Unable_to_switch_back_to_direct_mode.mdwn | 55 + ..._4585b251f011a153c62f377c324cf963._comment | 24 + ..._5848ebbab38d1244347f7e7351b3a30d._comment | 10 + ..._1c5c7b0c7bc336e00f43e257b87a6208._comment | 8 + ..._b0bfd68998bc3e11d8e089646b8292a6._comment | 12 + ..._to_sync_a_second_machine_through_Box.mdwn | 46 + ..._cb43a2bc976e3eb1cfc3ee9d4d34e78e._comment | 22 + ..._3375e9bfab3fed271413bd9bb5fa0121._comment | 30 + ..._c4420e1a3db321b4135b1626d3582adb._comment | 12 + ..._f4b2c88bb5938dacdd04dfe9a68560de._comment | 18 + ..._6dcc95ffb3fc7bbbedd6be5df0111c85._comment | 8 + ...to_use_remotes_with_space_in_the_path.mdwn | 32 + .../Unfortunate_interaction_with_Calibre.mdwn | 24 + ..._7cb5561f11dfc7726a537ddde2477489._comment | 8 + ..._b8ae4bc589c787dacc08ab2ee5491d6e._comment | 8 + ..._977c5f6b82f9e18cdd81d57005bb8b89._comment | 8 + ..._ff7d2e9a39dfe12b975d04650ac57cc4._comment | 8 + ..._fc4d5301797589e92cc9a24697b2155d._comment | 15 + doc/bugs/Unknown_remote_type_webdav.mdwn | 8 + ...ncy_on_certificate___62____61___1.3.3.mdwn | 64 + ...ository_on_the_server_don__39__t_work.mdwn | 49 + ..._2143f0540fdcd7efeb25b5a3b54fe0fd._comment | 12 + ..._bca95245b457631d08b47591da6163ad._comment | 9 + ..._f54bb003096752dae0442660267a1e37._comment | 11 + ..._38bb916ed5b90b92ffa91a452ff052a9._comment | 10 + ..._5b6ef464ab1ad061f27122db40191e26._comment | 8 + ..._3727bda5082cb1f2b1f746f9f80ced7d._comment | 8 + ..._a7139f19f0b73c024cd9218eb01e6104._comment | 8 + ..._Github_as_remote_throws_proxy_errors.mdwn | 27 + ..._10616b17c3fb8286fdc64c841023f8a1._comment | 9 + ..._8a72887d33e492a041f8246d93d0c778._comment | 8 + doc/bugs/Using_a_revoked_GPG_key.mdwn | 31 + ..._7bb01d081282e5b02b7720b2953fe5be._comment | 8 + ..._9c0c40360f0058a4bd346c1362e302b6._comment | 8 + ..._8f69f58107246595f5603f35c4aa7395._comment | 8 + doc/bugs/WEBDAV_443.mdwn | 307 + ..._9ee2c5ed44295455af890caee7b06f1a._comment | 18 + ..._863a7d315212c9a8ab8f6fafa5d1b7f5._comment | 8 + ..._c17a4e23011e0a917dbe0ecf7e9f0cb5._comment | 8 + ..._3414416ff455d2fd1a7c7e7c4554b54d._comment | 11 + ..._e1da141eefb0445c217e5f5c119356da._comment | 8 + ..._41c3134bcc222b97bf183559723713d9._comment | 27 + ..._89621b526065b5bef753ce75db1af7b5._comment | 14 + ..._131a1b65c8008cf9f02c93d4fb75720b._comment | 10 + ..._b4f894a0b9ebb84ab73f6ffcf0778090._comment | 29 + ..._c6572ca1eaaf89b01c0ed99a4058412f._comment | 10 + ..._a357969cde382a91e13920ee1e9f711c._comment | 8 + ..._213815d6b827d467c60f3e8af925813b._comment | 14 + ..._b775be4b722fc7124d9fbe2d5d01cc9f._comment | 22 + ..._c4ea745da437e56b2426d1c2c00dfcec._comment | 17 + ..._ef05c0ae88fee9c626922c6064ffdf1e._comment | 8 + ..._eecabe8d5ed564cb540450770ca7d0b6._comment | 8 + ..._7f77ba8ebd90186d3b3949ae529ba393._comment | 12 + ..._87ebdc92b48d672964fb3f248c53600f._comment | 9 + ...WORM:_Handle_long_filenames_correctly.mdwn | 4 + ..._77aa9cafbe20367a41377f3edccc9ddb._comment | 10 + ..._fe735d728878d889ccd34ec12b3a7dea._comment | 8 + ..._2bf0f02d27190578e8f4a32ddb195a0a._comment | 10 + ..._8f7ba9372463863dda5aae13205861bf._comment | 8 + ...539999b04664136c6f785211a41_segfaults.mdwn | 31 + ..._6c872dff4fcc63c16bf69d1e96891c89._comment | 8 + ..._5cad24007f819e4be193123dab0d511a._comment | 10 + ..._d449bf656a59d424833f9ab5a7fb4e82._comment | 8 + ..._ffb1ce41477ad60840abd7a89a133067._comment | 8 + ..._cebbc138c6861c086bb7937b54f5adbc._comment | 8 + ..._5e27737a5bb0e9e46c98708700318e67._comment | 8 + ..._1f92da712232d050e085a4f39063d7a6._comment | 21 + ..._4153dc8029c545f8e86584a38bd536fb._comment | 15 + ..._f85b6eb5bfd28ffc6973fb4ab0fe4337._comment | 8 + ..._c747c488461c98cd285b51d3afc2c3eb._comment | 10 + ...her_crashed:_addWatch:_does_not_exist.mdwn | 25 + ..._24f511a8103727894c6e96798a559870._comment | 10 + ..._e14eddbc09cadbf1e4dbbb0c07e0e5b0._comment | 10 + ..._513fae4d379008f954a307be8df34976._comment | 13 + ..._172eaeb3bb8b502379695aba35f96120._comment | 12 + ..._8adb9de82cc8581422734acc66dd094c._comment | 11 + ..._02f0beef1188bfa336bf4220eb5c6286._comment | 13 + ..._47__storage__47__sdcard1_-_bug__63__.mdwn | 43 + ..._71b052be40fbdaca09ca3ede8c59ac7a._comment | 8 + ..._0f7cc02e0193c969c9b6ceb27e71af8a._comment | 20 + doc/bugs/WebDAV_HandshakeFailed_.mdwn | 7 + ..._40499110ea43bc99ad9dd9f642da434c._comment | 15 + ..._506712e8cc5b47b9bd69edf67ae54da7._comment | 10 + ..._5641481d9e9ed2b711b1516f1abc5c30._comment | 8 + ..._1d609de93fa66ce9dc802e67b5922243._comment | 8 + ..._62761882d30c1b02930c938cb8e30ed4._comment | 10 + ..._acda8fae848ec486ce2a0b3dff3bd0a5._comment | 8 + ..._6c51b6c7dd477d8911dd9a7a5c41ea2e._comment | 8 + ..._e834f791d3000669fab25732a7c72ab3._comment | 13 + ...Webapp_fails_to_resolve_ipv6_hostname.mdwn | 15 + ...aviour_of_direct_and_indirect_annexes.mdwn | 57 + ..._56474a69c2f174d83be9137d3c045a47._comment | 33 + ...4___git_annex_uses_9x_times_diskspace.mdwn | 49 + .../comment_0._comment | 34 + ..._037a6dd6e15ef5f789a1f364f7507b53._comment | 45 + ..._614e4110188fc6474e7da50fc4281e13._comment | 10 + ..._dcb74fb91e1c2f0db4efd68c8bcbc96c._comment | 20 + ..._38671ba8d302f4d32460d1478abd2111._comment | 45 + ..._483244b1ed5744308022465f45c091fd._comment | 14 + ..._d2c63723fa4bf828873770a42ffaab20._comment | 8 + ..._52f0db73dc38c3e3a73f6c7a420bf016._comment | 8 + ..._93596b4d5a48ffcf4bc11ba9c83cf7ca._comment | 9 + ..._de94e80dde6d12485140bb079d74d775._comment | 14 + ..._5f34c3d449247b4bce4665b3ea4d054c._comment | 25 + ..._b43ae8aec23ba3acaf70edc0de058710._comment | 17 + ..._13b8e0a62f6b6d02960687e206a8b016._comment | 15 + ..._818b94a74b01a210d1106dd35bc932d8._comment | 10 + doc/bugs/Windows_build_test_failures.mdwn | 1230 +++ ..._and_wget__44___but_not_required_DLLs.mdwn | 15 + ..._a7bf0f027f2209e5632e292afd7214d0._comment | 10 + ...e_letters_cause_git_annex_get_to_fail.mdwn | 129 + ..._c87bae87b7902db60a3fef41e1fca85d._comment | 9 + ...S3__44___GPG_ask_for_a_new_passphrase.mdwn | 21 + ..._a4fc30bf7d39cae337286e9e815e6cba._comment | 13 + ..._e5d42b623017acedf6a3890ce15680a3._comment | 12 + ..._e5150b65b514896e14b9ad3d951963f7._comment | 10 + ..._47c2fc167b0c396edc40468fb7c7bfee._comment | 8 + ...emote_annexes_have_at_least_numcopies.mdwn | 39 + ...ong_port_while_configuring_ssh_remote.mdwn | 35 + ...dding_4923_files__34___is_really_slow.mdwn | 100 + ..._5f3b9f00bc31ce71d695c008971ed7fd._comment | 16 + ..._708b02dd06a1eed6b5ded9eb7aa9e7a8._comment | 16 + ..._6a735b7875d2a0c92df6786dd649985d._comment | 28 + ..._7e768908ba6983ea13af27635c4a947f._comment | 12 + ...rating_in_a_symlink__39__ed_directory.mdwn | 18 + ...etes_all_files_with_identical_content.mdwn | 49 + ..._2eb20b65582fa7f271b1d0bb5560d08c._comment | 10 + ..._b14e1d31dd6a8fb930fcc0bec798e194._comment | 19 + ..._1892bcfbe3c462aa74552a241d65cad9._comment | 8 + ..._dfa0e31996eaa14e2945c1d11670c4d9._comment | 15 + ..._e2a9336cf1080c158765d4adfe72f26b._comment | 9 + .../__34__fatal:_bad_config_file__34__.mdwn | 14 + ...4__git_annex_watch__34___adds_map.dot.mdwn | 23 + .../__34__make_test__34___fails_silently.mdwn | 4 + ..._f868e34f41d828d4571968d1ab07820a._comment | 16 + ..._fb9e8e2716b0dea15b0d4807ae7cd114._comment | 8 + ...it_add__39___for_parent_relative_path.mdwn | 15 + ...ulling_in___39__archive__39___content.mdwn | 24 + ..._56f9cd5cc2e089b32cb076dc2e2a8ca5._comment | 22 + ..._21c0f7f328cb51080fbd97e086c47a30._comment | 37 + ..._3287b2f25f3b5ae4c27f4748694563ee._comment | 8 + ..._e515eca68a70d40c522805d7e0d7c0e6._comment | 24 + ..._b27f4c103dda050b6e9cf03ea3157abc._comment | 10 + ..._2cc7083dab944705bf91fc00319b75e6._comment | 9 + ..._1175f9be789d4c1907f0be98e435bd2f._comment | 10 + ..._78e6164ef67a9560a3a9ead1f7a72473._comment | 15 + ..._1d578fd13022dcd6382b415a7f6e097a._comment | 8 + ...kcheck_that_satisfies___62____61__2.1.mdwn | 40 + ...ause_syncing_with_specific_repository.mdwn | 8 + ..._symlinks_are_fixed_in_the_background.mdwn | 51 + ...6___run_on_non-annexed_files_is_no-op.mdwn | 15 + ..._9b671e583eec5adf870dccd1e97b5dbc._comment | 8 + ..._d11744202213d6f897f4234bc4c70c18._comment | 9 + ..._a729deb465ff44f5a9b87c963cd6235a._comment | 15 + ..._3f735503df9a08472d42fabd219c2ec5._comment | 31 + ..._2c61eabbba7fd2a52ba02d59a0a76a42._comment | 8 + ...git_annex_import__96___clobbers_mtime.mdwn | 60 + ..._d173f2903faf4bff115a0be02c146ce9._comment | 8 + ..._3563d9eeb9806f8ca1b9b340925837f5._comment | 18 + ..._d5c7488db16b71c4f337662c897278ca._comment | 95 + ..._git_annex_sync__96___ignores_remotes.mdwn | 106 + ..._39421e6935233cd8f45949ebdef369fe._comment | 9 + ..._53fb15d6fbf96d43564ff7c866239d18._comment | 8 + ...ch_repositories_on_android__in_webapp.mdwn | 21 + ..._d488d71a72eb54d7711d2a867db6172f._comment | 8 + ..._85b31db6d0fb2d20018db3d8c8258bf4._comment | 8 + .../acl_not_honoured_in_rsync_remote.mdwn | 57 + ..._aa6fe1d7b029eae7ee71c97e0f0937a6._comment | 8 + ..._ffb9424e966ee10a4fe2d446b3042cb2._comment | 10 + ..._to___34__git_annex_dropunused__34___.mdwn | 21 + .../add_script-friendly_output_options.mdwn | 19 + ...kes___39__git_annex_unused__39___slow.mdwn | 81 + ..._d350c39c67031c500e3224e92c0029ea._comment | 19 + ..._b2d2b1caa51ffec3d87c36b373cb8d4a._comment | 20 + ..._12b20cbbc2b4cd1ab8af7e3eec9589b4._comment | 30 + ..._a50b43c15d2650df90f0fa1ced47f532._comment | 10 + ..._7328bc51bd001f2b732a92a2ae175839._comment | 114 + ..._880ef2ee797221332dbb629b2d55522f._comment | 10 + ..._826fd82cdf9b1c79c9b555ca26c2c176._comment | 8 + ...g_an_rsync.net_repo_give_an_gpg_error.mdwn | 22 + ..._f55cfc133be72ac10cae93c877c487df._comment | 21 + ..._24dd024ac4b21a82a781343b8fe3891e._comment | 11 + ...__and_reports_fail__44___but_succeeds.mdwn | 197 + ..._1f5e0bc93631baf0f8c1bec2e68493c5._comment | 20 + ...th_--file_doesn__39__t_actually_relax.mdwn | 26 + ...k_with_spaces_in_filenames_and_--fast.mdwn | 29 + ..._eea9477ea1157cb88c8a07d8da5f0dba._comment | 10 + ...s_repository_with_the_same_name_twice.mdwn | 24 + ..._ba7801403e7138684704a3471c8bc4a6._comment | 12 + ..._8c19a4ddedbe7ddb8bdcf84acac68cc8._comment | 14 + ...axy_nexus_java.lang.SecurityException.mdwn | 41 + ...-rsync-options_shell-split_carelessly.mdwn | 16 + ..._2636e0d224317f2e6db94658d8a094c4._comment | 23 + ...s_not_overriden_by_--numcopies_option.mdwn | 16 + doc/bugs/annex_add_in_annex.mdwn | 6 + ...__34__No_such_file_or_directory__34__.mdwn | 68 + doc/bugs/annex_get_over_SSH_is_very_slow.mdwn | 33 + ...nnex__47__uninit_should_handle_copies.mdwn | 20 + ..._c896ff6589f62178b60e606771e4f2bf._comment | 10 + ..._9249609f83f8e9c7521cd2f007c1a39e._comment | 8 + .../another_build_error_in_assistant.mdwn | 79 + doc/bugs/archiving_git_repositories.mdwn | 1 + ..._51f546a571303118446a9e0b3e6482c9._comment | 10 + doc/bugs/assistant_-_GTalk_collision.mdwn | 17 + ..._ab2c1f36113d40f27e1893d32f214296._comment | 12 + ..._91dff34c629a3b3a97a2313ff077e4ae._comment | 14 + ..._fefb73f6e570f96b4d82779d6622f690._comment | 8 + ...__34__too_many_open_files__34___error.mdwn | 32 + ..._9904c30a4c24a699d71e90ce5e9b89cf._comment | 8 + ..._1650539846521ae11837e4ac73348af6._comment | 9 + ..._b91415e4ee74eb12bc6e6faddd00af6e._comment | 12 + ...epo_cost_info_when_queueing_downloads.mdwn | 16 + ...es_not_list_remote___39__origin__39__.mdwn | 26 + ..._ffa008240c61b50396aa92f467731db6._comment | 12 + ..._a53f80090bc2a0f32b8d8307cb24b563._comment | 8 + ...es_not_warn_on_files_it_failed_to_add.mdwn | 46 + ..._13b2f93b7d09c8fd6c22829a0dc6428b._comment | 10 + ..._94e46bc0044b8a91a9fd51058825aa8f._comment | 60 + ..._10a38bdbf31dd4071e4bc4ac746d9c56._comment | 11 + ..._b8fdf502c7e80aece5a9544a2078c85c._comment | 36 + ..._a2ff7668f2a0d549b362d7de97fac8a1._comment | 10 + ..._60d72f34a6cfd1c081f74aa610f4305a._comment | 33 + ..._53a73e662c9356b759fbfa1e5a3bd927._comment | 14 + ..._10b65168b6a54d960427966d7e3d05f5._comment | 19 + ..._b640e8fa6aafb041d66bbf8857a8fa3d._comment | 44 + ...t_doesn__39__t_sync_empty_directories.mdwn | 30 + ..._78a3bde607f43c0f518bd2d3d7196022._comment | 8 + ..._83777384b72732b1d0a19b32686d3d1f._comment | 8 + ...nt_doesn__39__t_sync_file_permissions.mdwn | 45 + ..._fc8d3ea209a2ab39c1aeff52452d4c58._comment | 10 + ..._1a364c422e0dd7418f74e1cc3d543a3c._comment | 8 + doc/bugs/assistant_hangs_during_commit.mdwn | 27 + ..._aacc15c589d2795254387e427b3afe0c._comment | 8 + ..._b9f1bf9fa919603dca28182c80d39a11._comment | 10 + ..._fb5be10fcf5e7c89da5c34f48539612f._comment | 12 + ..._9ba7efe9112578729d02ac4e6557b3cc._comment | 10 + ..._73b24c901c73d41e0e0abe91267d4920._comment | 16 + ..._1a30b8c82e58222f1366aa368c23e6d3._comment | 10 + ..._56868b2a504ad0a60e8a8c1928330175._comment | 8 + doc/bugs/assistant_ignore_.gitignore.mdwn | 31 + ..._3458b1342cb2e3ccc01eeedc7f0e48fc._comment | 8 + ...__group__44___tries_to_transfer_files.mdwn | 56 + ..._e3f545d9adc27a4e7340bf16177c4fe0._comment | 12 + ..._1403076dbc47733607f0c8b2856e2381._comment | 37 + ..._af83717bfb260bea6d52ff71c6b34743._comment | 8 + ..._b4f811611d14e7392009c539fa6b8574._comment | 8 + ...t_::1_which_breaks_IPv6_enabled_hosts.mdwn | 30 + ..._91a62a2ce14a1027d2ac8b8e88df5f0c._comment | 12 + ..._4982cd373eaaeee180be03c6e9fda7b1._comment | 10 + ..._85d264e311acaa91dac0597ee8deda82._comment | 8 + ...g_file_renames__44___not_fixing_files.mdwn | 68 + ..._e0dafc410ffd617d445bb9403c7bfafe._comment | 9 + ..._2af247c8a1fcbde10795a990ef3303e9._comment | 9 + ...emotes_even_when_all_remotes_disabled.mdwn | 33 + ...othing_to_drop_or_copy_to_that_remote.mdwn | 5 + ..._10a9570a5d762ba2da271b38dc63edb6._comment | 12 + ..._57d50955b038c2e2405068536c7e83f3._comment | 8 + ..._a66f34daaba421c87eb404ef933e5191._comment | 10 + ..._094a3272eca1c6d2b4d264911ffe96e5._comment | 19 + ..._0161410d042a3421addd4a1fc7c1cd01._comment | 10 + .../authentication_to_rsync.net_fails.mdwn | 27 + ..._version_upgrade_leaves_repo_unusable.mdwn | 72 + ..._with_file_names_with_newline_in_them.mdwn | 5 + ..._92dfe6e9089c79eb64e2177fb135ef55._comment | 10 + ...bad_comment_in_ssh_public_key_ssh-rsa.mdwn | 22 + ..._15cce6e6f455e83f4362a38c561bc973._comment | 17 + ..._e9e1f38880a32610b3fbce475bffc3e4._comment | 12 + doc/bugs/bare_git_repos.mdwn | 29 + ...nd_constraints_issues_with_3.20121112.mdwn | 45 + ...e75fb23fca94ad86c3f0ee816bb0ad2ecb27c.mdwn | 25 + .../build_is_broken_at_commit_cc0e5b7.mdwn | 13 + ...ff14054e65ecbe801eb66786a55fa5245cb30.mdwn | 43 + ..._latest_release_0.20110522-1-gde817ba.mdwn | 14 + doc/bugs/build_problem_on_OSX.mdwn | 18 + doc/bugs/building_on_lenny.mdwn | 80 + ...mote_failed_with_localhost_+_username.mdwn | 49 + ..._0e669c3039b089fa8a815d3ec11465d2._comment | 20 + ...dule___96__Data.AssocList__39____34__.mdwn | 23 + ..._0da9fd67c3cc01b316f95a1df4eb62ae._comment | 8 + ...bal_configure_is_broken_on_OSX_builds.mdwn | 14 + ...fails_with_complaint_about_regex-base.mdwn | 34 + ...___40__non-git-annex__41___repository.mdwn | 208 + ..._dd202a7764d9df998868d595a86ffb21._comment | 30 + ..._ca065c82ac8e3215b581660f3e44f459._comment | 51 + ..._927a01f9961c71bedb42c519a31b5fe5._comment | 14 + ...t_annex_get_from_annex_in_direct_mode.mdwn | 21 + ..._20c31a844d8351a99cf69e05d2836e0e._comment | 19 + ..._f26e0f763f9027d9dfc08cd840ced153._comment | 10 + ...t_from_the_command_line_anymore__63__.mdwn | 31 + ...ant_with___34__host:port__34___syntax.mdwn | 30 + ..._397eb359c3f8ef30460a9556b6f55848._comment | 14 + ...file__44___get___34__user_error__34__.mdwn | 32 + ..._14aa717c1befcbbf526f25ca2f0af825._comment | 14 + ..._7f7ac59e7f3dce9d7a7d0c3379c2edcf._comment | 18 + ..._5ebf03120b12edb3fbb8954546e7603e._comment | 8 + ..._1ba6d2614778949520b47896fd98b598._comment | 8 + ..._4a6e55861a63b350a02edb888b4da99b._comment | 21 + doc/bugs/cannot_connect_to_xmpp_server.mdwn | 32 + ..._5072de8fcca9fe70bc235ea8c8ee2877._comment | 8 + ..._dabd74bba1f38b326a2d0c86d3027cd9._comment | 17 + ..._0245b426cc0ab64f8c167b8806b03f5d._comment | 10 + ..._307df11b5bcf289d7999e1e7f7c461c9._comment | 10 + ..._f24378cf30a7d32594da90749fabec3c._comment | 12 + ..._4b07093be844ac62b611cee1dfde5aa7._comment | 8 + ..._fe1ed152a485c4aebfa9b9f300101835._comment | 8 + ..._2d311f520aee04287df6bddfd8535734._comment | 28 + ..._d9f916f012184738446c5996ee9d2270._comment | 13 + ..._0b5f9350e2367301241c7668a15815ef._comment | 8 + ..._f00b6ae154004e405f0bd23b7359962e._comment | 8 + ..._41b86468013da15f46be29da520afa10._comment | 8 + doc/bugs/case-insensitive.mdwn | 20 + doc/bugs/case_sensitivity_on_FAT.mdwn | 49 + doc/bugs/check_for_curl_in_configure.hs.mdwn | 92 + ....git-annex.assistant.plist_is_invalid.mdwn | 15 + ...rgument___40__invalid_character__41__.mdwn | 228 + ...on_OSX_as_mntent.h_doesn__39__t_exist.mdwn | 8 + ..._processes_can_lead_to_locking_issues.mdwn | 53 + .../configurable_path_to_git-annex-shell.mdwn | 7 + ..._fb6771f902b57f2b690e7cc46fdac47e._comment | 10 + ..._2b856f4f0b65c2331be7d565f0e4e8a8._comment | 8 + ..._aea42acc039a82efc6bb3a8f173a632e._comment | 12 + ..._builds_a_broken_git-annex_executable.mdwn | 57 + ...d_detect_uuidgen_instead_of_just_uuid.mdwn | 6 + doc/bugs/conflicting_haskell_packages.mdwn | 17 + ..._e552a6cc6d7d1882e14130edfc2d6b3b._comment | 24 + doc/bugs/conq:_invalid_command_syntax.mdwn | 30 + ..._f33b83025ce974e496f83f248275a66a._comment | 10 + ..._195106ca8dedad5f4d755f625e38e8af._comment | 9 + ..._55af43e2f43a4c373f7a0a33678d0b1c._comment | 15 + doc/bugs/copy_doesn__39__t_scale.mdwn | 38 + ..._7c12499c9ac28a9883c029f8c659eb57._comment | 10 + ..._f85d8023cdbc203bb439644cf7245d4e._comment | 15 + ..._4592765c3d77bb5664b8d16867e9d79c._comment | 11 + ...ast_confusing_with_broken_locationlog.mdwn | 6 + ..._435f87d54052f264096a8f23e99eae06._comment | 8 + ..._9be0aef403a002c1706d17deee45763c._comment | 24 + ..._26d60661196f63fd01ee4fbb6e2340e7._comment | 11 + ..._ead55b915d3b92a62549b2957ad211c8._comment | 35 + ..._191de89d3988083d9cf001799818ff4a._comment | 10 + ..._b3e3b338ccfa0a32510c78ba1b1bb617._comment | 8 + ..._04a9f4468c3246c8eff3dbe21dd90101._comment | 8 + ..._6a41bf7e2db83db3a01722b516fb6886._comment | 18 + ..._9f5f1dbffb2dd24f4fcf8c2027bf0384._comment | 8 + ..._b596b5cfd3377e58dbbb5d509d026b90._comment | 14 + ..._d7112c315fb016a8a399e24e9b6461d8._comment | 12 + ..._4ea29a6f8152eddf806c536de33ef162._comment | 14 + ..._0d85f114a103bd6532a3b3b24466012e._comment | 8 + ..._d38d5bee6d360b0ea852f39e3a7b1bc6._comment | 12 + ..._29c3de4bf5fbd990b230c443c0303cbe._comment | 10 + ..._2cee4f6bd6db7518fd61453c595162c6._comment | 8 + ...y_where_a_mountpoint_should_have_been.mdwn | 27 + ..._41cfd5e48426a6ef52bef70a06a6f46a._comment | 11 + ..._bd584ccbe128427fca99e61d66d301c9._comment | 18 + ..._5bb0347215b321444643646f25a35759._comment | 10 + ..._73848a9c783ecf3d9fccdd41b20fbe36._comment | 56 + .../creating_a_remote_server_repository.mdwn | 24 + ..._de1a370347428245bcfca60eaca96779._comment | 10 + ...s_directory_not_automatically_created.mdwn | 3 + doc/bugs/cyclic_drop.mdwn | 104 + ..._add__34___on_android_in_direct_mode_.mdwn | 19 + ..._eb6db7f6a156a065e2724c2de5fc4366._comment | 10 + ..._59a96cade9e4881767562a139fc7fb4b._comment | 40 + ..._bf9d2562d66f0f6a9478ac178606cf4e._comment | 12 + ..._ad0dbdc448fff2e126ffec9aac6d7463._comment | 23 + ..._e828585e56b10710598143483ce362b6._comment | 12 + ...ct_mode_assistant_in_subdir_confusion.mdwn | 6 + ..._351143deec29e712f8718a373ad650d7._comment | 8 + doc/bugs/direct_mode_renames.mdwn | 15 + ..._f18c335e0d6f4259d2470935ef391cb8._comment | 8 + ..._bcac9fd7b3f4a2ac28bee59bae674fa0._comment | 79 + ..._c9088060fb9133b66951f1a3075981e8._comment | 18 + ..._5bf34466187cfc9b34bd3ca8c89a07c6._comment | 20 + ..._d6201f2d86d5b44051a7fd7a8c9de583._comment | 8 + ..._61c5f0889f30a68ac3b57c4ea564ee0e._comment | 8 + doc/bugs/done.mdwn | 4 + doc/bugs/dotdot_problem.mdwn | 4 + ...fails_to_see_copies_that_whereis_sees.mdwn | 69 + ..._f5a9d99d90daf5eba4773d361fa1807a._comment | 14 + ..._040aa454cd8acd2857ef36884465576f._comment | 16 + ..._f5d8faab325ee26800ecad5aba49b54b._comment | 10 + ...ng_from_web_remotes_doesn__39__t_work.mdwn | 139 + ...opping_files_with_a_URL_backend_fails.mdwn | 13 + ...9__t_handle_double_spaces_in_filename.mdwn | 87 + ...ed_doesn__39__t_work_in_my_case__63__.mdwn | 70 + doc/bugs/encfs_accused_of_being_crippled.mdwn | 41 + ..._5c5be012e1171ef108f38825d72791b6._comment | 23 + doc/bugs/encrypted_S3_stalls.mdwn | 9 + ...keyid_still_uses_symmetric_encryption.mdwn | 46 + ..._2f4ec4b7b92a0f0a0c4c0758da4a05a5._comment | 13 + ..._7c0aeae6b1b2b0338735b0231c5db7d4._comment | 16 + doc/bugs/encryption_key_is_surprising.mdwn | 24 + ..._5b172830ac31d51a1687bc8b1db489f9._comment | 10 + ..._5b7e6bb36c3333dfd71808e8b4544746._comment | 8 + ..._8ec86b8c35bce15337a143e275961cd5._comment | 8 + ..._c5e49b3a0eceabe6d14f5226d7ba4c7a._comment | 8 + ..._cd7cbf0c0ee9cafec344dfbf1acd9590._comment | 8 + ..._01381524114d885961704acc3f172536._comment | 8 + ..._c1eb59e1c5f583dcef7cea17623a2435._comment | 8 + ...ding_git-annex_3.20120624_using_cabal.mdwn | 159 + ...rror_on_only_repository_copy_deletion.mdwn | 16 + ..._af394ac0956ab33a77256bcb02ef2a0f._comment | 14 + doc/bugs/error_propigation.mdwn | 3 + ...epositories_with_non-ASCII_characters.mdwn | 62 + ..._38cc2d2ed907649df085de8ad83cb9dd._comment | 14 + ...or_with_file_names_starting_with_dash.mdwn | 15 + ...eous_shell_escaping_for_rsync_remotes.mdwn | 15 + doc/bugs/fails_to_handle_lot_of_files.mdwn | 445 + ..._09d8e4e66d8273fab611bd29e82dc7fc._comment | 8 + ..._fd2ec05f4b5a7a6ae6bd9f5dbc3156de._comment | 8 + ...hould__34___exist_in_remote_is_silent.mdwn | 37 + ..._c686df2824d3f588c0bfb339c99168b7._comment | 29 + ..._22edfac4ce25cd9f4e4c85e0a8a52bc1._comment | 14 + ..._74fc0e41a6bd5c4d8c4b2f15e5ed8d2f._comment | 17 + ..._7d642fc65040a7b583cdece33db01826._comment | 8 + ..._49be366b6af6db595fa538373a61e650._comment | 10 + ...ure_to_return_to_indirect_mode_on_usb.mdwn | 19 + ..._d7822b90c68bf845572b0a04a378d0bb._comment | 10 + doc/bugs/fat_support.mdwn | 13 + ..._04bcc4795d431e8cb32293aab29bbfe2._comment | 12 + ..._bb4a97ebadb5c53809fc78431eabd7c8._comment | 14 + ..._df3b943bc1081a8f3f7434ae0c8e061e._comment | 11 + ..._90a8a15bedd94480945a374f9d706b86._comment | 10 + ..._64bbf89de0836673224b83fdefa0407b._comment | 8 + ..._a3b6000330c9c376611c228d746a1d55._comment | 8 + ..._a0ac7f2c44efc8116940c7b94b35e9d0._comment | 7 + ..._acc947643a635eb10a1bff92083a3506._comment | 10 + doc/bugs/fatal:_empty_ident_name.mdwn | 51 + ..._ceae87308fb75a1f79c7c8d63ec47226._comment | 8 + ..._68832ee3e0e7244ce62bccabe2e52630._comment | 25 + ..._ed31ad316747343d7730e4c2d7dacd24._comment | 10 + ..._b812d6f30e8a866bce7260a9ee3218e3._comment | 13 + ..._47__locking_issues_with_the_assitant.mdwn | 54 + ..._fadf06f5ab34e36ab130536ec55afc8e._comment | 12 + ..._4a337f7b1140c45e5dd660b40202f696._comment | 10 + ..._05e1398e78218ced9c2da6a2510949e8._comment | 21 + ..._9226f0adf091154c0d8a08b340b71869._comment | 8 + ..._44d3e2096b7d45a1062222bee83a346d._comment | 8 + ..._f2e1d188b7b2d2daf0d832c59a68583e._comment | 8 + ..._998fe58994ecf855310e4b8e6cce9e18._comment | 8 + ..._4ce243cb0ea8ff810a4949a5320e4afc._comment | 13 + ..._c713f6316d889c8fc52326f21375c1c4._comment | 8 + ..._6dd23bab7983b8b1f938dd4f21a16f5a._comment | 8 + ..._961c8f968eff0b39a85b607ee3f7630d._comment | 16 + ...relates_to_the_new_inotify_flag__41__.mdwn | 36 + doc/bugs/free_space_checking.mdwn | 21 + ..._a868e805be43c5a7c19c41f1af8e41e6._comment | 10 + ..._8a65f6d3dcf5baa3f7f2dbe1346e2615._comment | 8 + ..._0fc6ff79a357b1619d13018ccacc7c10._comment | 8 + ...ix_the_permissions_of_.git__47__annex.mdwn | 8 + ...n_less_copies_than_required_are_found.mdwn | 57 + doc/bugs/fsck_output.mdwn | 46 + ...uble-check_when_a_content-check_fails.mdwn | 3 + ..._03af24b70adbcd9f4b94d009f6b71d0a._comment | 13 + ..._41214a7d18c66b694645248d6ebeadbf._comment | 25 + ..._e7ddd77ea35994f2051f840e9b4c7e0c._comment | 11 + ..._36a70d5a378983a76fcdbb7fba044044._comment | 32 + ..._899c4afbc988d81984c5c3397285bb01._comment | 12 + ..._dbff51d00c5645eb1832aa4644889c5e._comment | 10 + ...ile_content_is_bad_when_it_isn__39__t.mdwn | 35 + ..._cafb58eca97a0a66110ac39b169d8de3._comment | 8 + ..._failed__44___but_remote_has_the_file.mdwn | 40 + ..._55c8b73ce05dfca11a393bb296b99b9a._comment | 10 + ..._474c67a421dca4c245e7bfe495d3f6d3._comment | 18 + ..._845e8a23d63fb0b071c63ee736697d26._comment | 20 + ..._7dec21cb67e7f4dbdb49da97f2443e8f._comment | 35 + ...47___web_remotes_if_the_file_is_empty.mdwn | 26 + ...tic__41__:_strange_closure_type_30799.mdwn | 75 + ..._1c19e716069911f17bbebd196d9e4b61._comment | 10 + ..._a4d66f29d257044e548313e014ca3dc3._comment | 66 + ..._f5f1081eb18143383b2fb1f57d8640f5._comment | 38 + ..._b1f818b85c3540591c48e7ba8560d070._comment | 10 + ..._67406dd8d9bd4944202353508468c907._comment | 13 + ...nrecognized_option___96__--uuid__39__.mdwn | 53 + ..._13510e954e36484e196e7395a3a9bf1f._comment | 10 + ..._7edc478a76983a3b3c68d01f24dce613._comment | 9 + ..._t_honour_Rsync__39__s_bwlimit_option.mdwn | 37 + ..._8cda861c11ef2fff3442e5a0df741939._comment | 12 + ..._15e06f6db9a14a8217dea25e24ddc23a._comment | 12 + ..._d36045e2b466882108c5bf09580755fa._comment | 8 + ...not_decode_byte___39____92__xfc__39__.mdwn | 34 + ..._f1a7352b04f395e06e0094c1f51b6fff._comment | 12 + ..._c1890067079cd99667f31cbb4d2e4545._comment | 8 + ..._213c96085c60c8e52cd803df07240158._comment | 13 + .../git-annex:_Not_in_a_git_repository._.mdwn | 22 + ..._e10363a912953a646b87c824d1c6e5d4._comment | 10 + ..._9e96063a664b2be8a36d7940e7632d3f._comment | 8 + ..._8c9bd76b0e1200723ec13fbef943a2cc._comment | 10 + ..._8c49979b8a815f0d6f9de39ee9a88730._comment | 8 + ...urce_vanished___40__Broken_pipe__41__.mdwn | 14 + ..._e962317a939bf76097ae1a3b53b146e6._comment | 14 + ..._b32472b4c9b61e7a33dca802ecafb05b._comment | 11 + ..._fcfea3216831df9afbd855fbd842c27e._comment | 20 + ..._30d0b40efa59eeecb8a4be6d1baa1520._comment | 10 + ..._4af107f3184bc2abd2c9693167018628._comment | 8 + ..._f96027f1e3c405809fae42ce8533c6d1._comment | 8 + ..._b6fe89deb468a7e4f63f7faab147e3fb._comment | 12 + ..._ebec5d9266604f03959dc16d933ff4a4._comment | 13 + ...t-annex:_fd:14:_hGetLine:_end_of_file.mdwn | 49 + ..._36756f5d9d591cc52113c5cc0c1eae91._comment | 8 + ...ntryForID:_failed___40__Success__41__.mdwn | 13 + ..._11a1615962325327466895d03e3d2379._comment | 8 + ..._eac51c3299e9fc04025675360969d537._comment | 8 + ..._c23dc02c7487d63b0905f1b7f3ca59f5._comment | 9 + ..._0e8b28de5c173bc60ecc0126fb2209ca._comment | 10 + ...t-annex_3.20130216.1_tests_are_broken.mdwn | 43 + ...les_with___39__:__39___in_their_names.mdwn | 38 + ...it-annex_add_should_repack_as_it_goes.mdwn | 32 + ..._dbcaa0be4cd764128fb7263a95f73a32._comment | 22 + ..._6a27551c4fb7f62ed9f627134c755d01._comment | 14 + ..._ff8b589fbcf25c98abd1c58830074650._comment | 8 + doc/bugs/git-annex_branch_corruption.mdwn | 95 + doc/bugs/git-annex_branch_push_race.mdwn | 45 + doc/bugs/git-annex_broken_on_Android_4.3.mdwn | 3 + ..._0ffb3833ce2c2e0320468dc9a09866d7._comment | 10 + ...ositories_with_a_partial_set_of_files.mdwn | 29 + ...nex_directory_hashing_problems_on_osx.mdwn | 100 + ..._f3594de3ba2ab17771a4b116031511bb._comment | 8 + ..._97de7252bf5d2a4f1381f4b2b4e24ef8._comment | 13 + ..._f1c53c3058a587185e7a78d84987539d._comment | 8 + ..._4f56aea35effe5c10ef37d7ad7adb48c._comment | 8 + ..._cc2a53c31332fe4b828ef1e72c2a4d49._comment | 10 + ..._37f1d669c1fa53ee371f781c7bb820ae._comment | 17 + ..._8a4ab1af59098f4950726cf53636c2b3._comment | 22 + ..._515d5c5fbf5bd0c188a4f1e936d913e2._comment | 9 + ..._db64c91dd1322a0ab168190686db494f._comment | 14 + ..._ff555c271637af065203ca99c9eeaf89._comment | 8 + ..._9a7b09de132097100c1a68ea7b846727._comment | 8 + ..._7e328b970169fffb8bce373d1522743b._comment | 19 + ..._98f632652b0db9131b0173d3572f4d62._comment | 10 + ..._52d41afd7fd0b71a4c8e84ab1b4df5bd._comment | 8 + ..._c2cd8a69c37539c0511bae02016180ca._comment | 8 + ..._174952fc3e3be12912e5fcfe78f2dd13._comment | 185 + ..._a18ada7ac74c63be5753fdb2fe68dae5._comment | 18 + ..._039e945617a6c1852c96974a402db29c._comment | 8 + ..._eacd0b18475c05ab9feed8cf7290b79a._comment | 37 + ..._e55117cb628dc532e468519252571474._comment | 14 + ..._0f4f471102e394ebb01da40e4d0fd9f6._comment | 68 + ..._68e2d6ccdb9622b879e4bc7005804623._comment | 12 + ..._45b11ddd200261115b653c7a14d28aa9._comment | 8 + ...iles_containing_ISO8859-15_characters.mdwn | 48 + ..._b84e831298c03b12471fb75da597e365._comment | 8 + .../git-annex_dropunused_has_no_effect.mdwn | 12 + ..._66b581eb7111a9e98c6406ec75b899cf._comment | 12 + ..._11c46cd2087511c3d22b7ce7c149b3e9._comment | 16 + ..._b1c3d8c6ec4b20727aaa9c4b746531b0._comment | 10 + ..._f05a9a3760858c5ee5c98dd8ab059c28._comment | 8 + ...t-annex_fix_not_noticing_file_renames.mdwn | 36 + ..._4edd95200d59ec5a5426167b8da8e3f9._comment | 24 + ..._a9a44debefb3bdd4b8ed2d1cf53f2338._comment | 8 + ..._0efb11f35b872b75a3fbc4ebb71ac827._comment | 10 + ...nex_get:_requested_key_is_not_present.mdwn | 41 + ..._d4baa6607a61d0e6a7cea1325a5ddf95._comment | 26 + ..._b49725488c3db5e00ede7b65ed9d62fa._comment | 110 + ..._c17a7138579b93c6f14e3444c11664ac._comment | 8 + ..._git_when_staging__47__commiting_logs.mdwn | 34 + ...nex_immediately_re-gets_dropped_files.mdwn | 27 + ..._09e616a4866e726a48be4febe6375cc8._comment | 8 + ...ncorrectly_parses_bare_IPv6_addresses.mdwn | 59 + ...rsync_remotes_with_encryption_enabled.mdwn | 103 + ...esystem_can_still_failed_due_to_case_.mdwn | 32 + ..._850695231926dfe94f11342d3af7f63c._comment | 54 + ..._c2a2f801a3e18ad597ff0acf2f104557._comment | 22 + doc/bugs/git-annex_opens_too_many_files.mdwn | 38 + ..._37f6f5838c41c533df4be1f927b9b03d._comment | 26 + ..._347ef233b9845b84d7c4d49ed166e797._comment | 10 + ..._d5f644d97cd2db471deb5dcd728cae60._comment | 8 + ...nnex_sync_broken_on_squeeze_backports.mdwn | 20 + ...iles_are_in_repositories_they_are_not.mdwn | 29 + ..._6722fd627ec4add9f2b16546bd8ef341._comment | 8 + ..._508e475f764e1cb453b756eb50bc3a15._comment | 34 + ..._1656ba18c519a262c57ef626a3449e77._comment | 12 + ..._347dc3b6e5bc6c4195ec09d54bc1398e._comment | 24 + ..._a9c93bfc3278ef8b1117eac2af859bc3._comment | 12 + ..._804dd62beef64f7d4e203bdb28cbe660._comment | 11 + ..._4ef107d70647780eb5347cae6f467fed._comment | 12 + .../git-annex_webapp_command_not_found.mdwn | 25 + ..._6fa63ae1a7affb2351eda57ab3b4eda1._comment | 10 + ..._d25232bb5eaff725281869d7681e81ad._comment | 8 + doc/bugs/git_annex_add_..._adds_too_much.mdwn | 25 + ..._eats_files_when_filename_is_too_long.mdwn | 14 + ..._9650284913bec2a00cf551b90ab5d8ff._comment | 21 + ..._c6c8d2a1f444d85c582bc5396b08e148._comment | 8 + ..._5776864d78d56849001dd12e3adb9cbe._comment | 8 + ..._371ec7b4ae73280ede31edfe90b42a95._comment | 9 + ..._4fb04f646de591640f8504c0caf61acd._comment | 12 + ..._b4055409fe48da95bb3101c0242ef0bc._comment | 8 + ...nex_add_error_with_Andrew_File_System.mdwn | 26 + ..._bc783e551fc0e8da87bc95bff5b8f73a._comment | 8 + doc/bugs/git_annex_add_memory_leak.mdwn | 39 + ...ex_add_removes_file_with_no_data_left.mdwn | 103 + ..._9cc749a6efd4359a99316036f5bc867f._comment | 12 + ..._1fed5be9db29866e4dc3d3bb12907bf3._comment | 8 + ..._06d517ac4ef8def4629a40d7c3549bac._comment | 8 + ..._8f081aeba7065d143a453dc128543f59._comment | 18 + ..._54a4b10723fd8a80dd486377ff15ce0d._comment | 10 + ..._f1964e4e07991a251c2795da0361a4e2._comment | 28 + ..._73c38d843c30f00f6fd8883db8e55f62._comment | 10 + ..._7ede5ee312f3abdf78979c0d52a7871a._comment | 12 + ..._e37cf18708f09619442c3a9532d12ed9._comment | 13 + ..._a744ef7dd3a224a911ebb24858bc2fd6._comment | 8 + ..._f97141b255073b90120895148220c2d7._comment | 10 + ..._dd2be11dfd190129d491f5f891e7cd1a._comment | 10 + ...it_annex_assistant_--autostart_failed.mdwn | 39 + ..._746545273b53849c42ff6272324e5155._comment | 10 + ..._5bdf6f94da12e551ae12e7f550a84d62._comment | 8 + ..._bfd646f69946a5fe926b270cf94f87cb._comment | 8 + ...annex_content_fails_with_a_parse_error.txt | 32 + ..._2b60b6ae0115de13ecf837b34dadcd1d._comment | 8 + ...annex_copy_--fast_does_not_copy_files.mdwn | 22 + ...EMOTE_._doesn__39__t_work_as_expected.mdwn | 18 + ...ying_to_connect_to_remotes_uninvolved.mdwn | 25 + ..._f1330935a07460c9c8bc82ee8d4709c5._comment | 12 + doc/bugs/git_annex_does_nothing_useful.mdwn | 67 + ..._457354dc0018333002dc5049935c0feb._comment | 8 + ..._8a6d244165dd238ddf9dd629795de2f6._comment | 10 + ..._30d06bc0f1c37d988a1a31962b57533c._comment | 18 + ..._fc4f51ddcbc69631e2835b86c3489c8e._comment | 7 + ..._9bb1647e6c59f1ed7b13b81ecc33f920._comment | 13 + ..._d434f5c614a27b75d73530b5b918b851._comment | 14 + ..._998e33219d29ea41b0b2a5d2955a9862._comment | 46 + ..._c72e2571e5b8c06bbfa2276a7ad1e8a6._comment | 16 + ..._bc8b42432ba25de8f972c192bc3cdff6._comment | 44 + ..._e7469a4c5e45078ade775f5cbdd17cfc._comment | 67 + ..._bc9e6fd284440a59ffe4e4ed1f73f7d7._comment | 30 + ..._38a2dbeee3750d79ca9a943a02fceb29._comment | 17 + ...ex_doesn__39__t_work_in_Max_OS_X_10.9.mdwn | 218 + ..._4fb9d3de245dddab65fb1a53a67a095c._comment | 8 + ..._f513259a2641e00b049203014ab940c8._comment | 12 + ..._54ee7b90467fee8b0457e9c447747500._comment | 10 + ..._7e6223c2dae3346e17276c7bbb01d53e._comment | 12 + ..._13b6e595d595da7f036e81258a65541e._comment | 8 + .../git_annex_fork_bombs_on_gpg_file.mdwn | 25 + ..._6e29b60cd77f3288e33ad270f95f410e._comment | 8 + ..._ad13e3221ae06086e86800316912d951._comment | 12 + ..._41746b731eae7f280bb668c776022bcb._comment | 8 + ..._56ca8590110abffeed6d826c54ca1136._comment | 10 + ..._73ae438a37e4c5f56fe291448e1c64dd._comment | 13 + ..._aa237adebe7674b8cdb9a967bb5f96a8._comment | 8 + ..._ab403d7abbbbabd498b954b0b9742755._comment | 12 + ..._a35d04440b1220faf9088107c3f17762._comment | 10 + ..._8345331b9b313769ba401da2ffd89332._comment | 10 + ..._7eb535ca38b3e84d44d0f8cbf5e61b8b._comment | 18 + ..._a3aa4231a82917c56cbdf52b65db7133._comment | 21 + ..._178fd4e4d6abbca192fcd6d592615fca._comment | 12 + ..._7d80f131f43312bb061df2be7fa956ef._comment | 10 + ...n_direct_mode_does_not_checksum_files.mdwn | 18 + ..._a6cde4aa495512344fa7f50e10749c68._comment | 8 + ..._4ac3b87ec0bc0514c4eff9f5a75b9f5d._comment | 26 + ..._d18b1fdc866edf2786d2c6b7ec55119f._comment | 11 + ..._31e4fcbf63c11cc374a849daf3ce1dbc._comment | 8 + ...t_annex_fsck_is_a_no-op_in_bare_repos.mdwn | 21 + ..._fc59fbd1cdf8ca97b0a4471d9914aaa1._comment | 8 + ..._273a45e6977d40d39e0d9ab924a83240._comment | 9 + ...when_remote_is_an_ssh_url_with_a_port.mdwn | 13 + ...bout_remotes_with_dots_in_their_names.mdwn | 34 + ...needs_some___34__error_checking__34__.mdwn | 65 + ...git_annex_initremote_walks_.git-annex.mdwn | 19 + ...problems_with_urls_containing___126__.mdwn | 46 + ...te_leaves_old_backend_versions_around.mdwn | 19 + ..._f3e418144e5a5a9b3eda459546fc2bb0._comment | 8 + ...use___39__git_add_-f__39___internally.mdwn | 11 + ..._7683bf02cf9e97830fb4690314501568._comment | 8 + ...ect_mode_does_not_honor_skip-worktree.mdwn | 35 + ..._69baeb997086c885f34fd1dc385cf5d6._comment | 12 + ..._fb8c0bebb9aaa75ee7eaf6999b1db49e._comment | 10 + ..._6bfd4e9a7853af93e72b717249de9439._comment | 8 + ...uninit_loses_content_when_interrupted.mdwn | 33 + ..._fd9d2abbc90fb4f470b2212bc1f4a2dd._comment | 8 + ..._0e99f6ef4f8b342ef0ebc64dbf8e2ce6._comment | 12 + ...s_files_not_previously_added_to_annex.mdwn | 32 + ..._ce4e3b1bf0d53119d049cf7dd621c5c4._comment | 10 + ..._3aa125635609fce41ab0c98cefb81f98._comment | 9 + doc/bugs/git_annex_unlock_is_not_atomic.mdwn | 7 + ...rts_due_to_filename_encoding_problems.mdwn | 15 + ..._8ba4fdb9f2d3bd44db5e910526cb9124._comment | 8 + ..._2a4a2b3e287a0444a1c8e8d98768a206._comment | 8 + ..._dacfdb8322045fc4ceefc9128bf7c505._comment | 17 + ..._7889a3ff5ce80c6322448aa674df8525._comment | 10 + ..._6d28c2537ce24eeb3496ca349823defd._comment | 19 + ..._4bf14ecef622988e80976c0fb55c24b9._comment | 10 + ..._d2e5382fe0f38fb9dd9ee69901c68151._comment | 8 + ..._b282757537cda863d3dc6d0bbfd6b656._comment | 8 + ..._branches_which_makes_it_inconsistent.mdwn | 106 + ..._a636ffe55b11c46a0afcc0b9a3a88cd4._comment | 10 + ..._5e1ad57420efd16ae09c9e5cad55b5f2._comment | 13 + ...nex_unused_failes_on_empty_repository.mdwn | 15 + ...nused_seems_to_check_for_current_path.mdwn | 39 + ...acter___40__and_probably_others__41__.mdwn | 33 + ..._861506e40e0d04d2be98bbfe9188be89._comment | 12 + ...ade_output_is_inconsistent_and_spammy.mdwn | 15 + ..._3a01c81efba321b0e46d1bc0426ad8d1._comment | 10 + ...rsion_should_without_being_in_a_repo_.mdwn | 7 + ..._e7b26eeb1a765fd83280ef907c0deef2._comment | 8 + ...app_--listen_on_a_remote_linux_server.mdwn | 50 + ..._db99c00830d3f15ebe790c4dc8b60bd7._comment | 8 + doc/bugs/git_annex_webapp_runs_on_wine.mdwn | 47 + ..._c71dfa42780c0fc78f88ce054e5f3ee3._comment | 16 + ..._f28441b18b0be90c1e58348455ce09d9._comment | 23 + ...won__39__t_copy_files_to_my_usb_drive.mdwn | 60 + ..._7707017fbf3d92ee21d600fe0aefce4f._comment | 10 + ..._f3392ec3ca7392823cbad2cc9b77f54e._comment | 9 + ..._b3d016a487b12748fe2c4d14300eb158._comment | 20 + ..._61f600511a3172f0707e5809fc444d0c._comment | 9 + ..._8cf029ac7bf3c19dcb0b613eed3b52ac._comment | 10 + ..._e40d88eba7d8aec1530ce1d32d1c85f2._comment | 11 + ..._b101fab9e690d1b335a1a29abab68d6c._comment | 10 + ..._b30d32086314a7e357f3dd6608828ee5._comment | 9 + ...nix_breaks_git_commit_after_uninstall.mdwn | 42 + ..._c8b1bab40d3bb2468a5bba7b116e854e._comment | 8 + ..._4173770375fca51dcaf9b974296d041a._comment | 8 + ...nd_has_tons_of_redundant_-a_paramters.mdwn | 15 + ...0__child_of_git-annex_assistant__41__.mdwn | 34 + ..._5e3f4b63db5cd32b63fb3e6a78f9b093._comment | 10 + .../git_rename_detection_on_file_move.mdwn | 13 + ..._5ec2f965c80cc5dd31ee3c4edb695664._comment | 8 + ..._0531dcfa833b0321a7009526efe3df33._comment | 26 + ..._7101d07400ad5935f880dc00d89bf90e._comment | 27 + ..._57010bcaca42089b451ad8659a1e018e._comment | 8 + ..._79d96599f757757f34d7b784e6c0e81c._comment | 34 + ..._d61f5693d947b9736b29fca1dbc7ad76._comment | 12 + ..._f63de6fe2f7189c8c2908cc41c4bc963._comment | 19 + ..._7f20d0b2f6ed1c34021a135438037306._comment | 10 + ..._6a00500b24ba53248c78e1ffc8d1a591._comment | 21 + ..._75e0973f6d573df615e01005ebcea87d._comment | 9 + doc/bugs/gix-annex_help_is_homicidal.mdwn | 23 + doc/bugs/glacier_from_multiple_repos.mdwn | 14 + doc/bugs/googlemail.mdwn | 16 + .../gpg_bundled_with_OSX_build_fails.mdwn | 25 + ..._ec911f920db6c354ba998ffbb5886606._comment | 10 + ..._bf2a3ab1bbe258bd501ec4b776882adf._comment | 12 + ..._c0142427400323c00bd8294415ae32c5._comment | 15 + ..._b56db4b5afc276f88a2b980e22fda8a0._comment | 10 + ..._a4eda81e5f927c463593bc48fbe84077._comment | 12 + ..._2f0b9331d16a208883bac586258a7b50._comment | 8 + ..._c05c484a6134f93796cff08de0f63e80._comment | 16 + ..._f2cb5467ebe80cf67e1155b771b73978._comment | 8 + ..._27bbda7e31f55b29e1473555ee17e613._comment | 8 + doc/bugs/gpg_error_on_android.mdwn | 39 + ..._870583fd1b7a33b688b9a228077d1333._comment | 629 ++ ..._9ce5511a109bde50d8cf87bad0268b4a._comment | 26 + ..._b345e80f38d38f82cfcfce3102138fb8._comment | 46 + ..._032f42235b7f26854e725041ca33384b._comment | 10 + ...es_to_100__37___cpu_on_bad_input_data.mdwn | 18 + ..._889218fb7c0115b03d9bad0c07296097._comment | 36 + .../gpg_hangs_on_glacier_remote_creation.mdwn | 78 + ..._41ca74a4e4aaf4f6b012a92677037651._comment | 14 + ..._dd11fd25c8bb1f2d7e1292c07abf553e._comment | 591 ++ ..._543d8a13756c1355a5752867bdcbefd3._comment | 20 + ..._6441cf25e6bd62c96d7e766da9bdd7fb._comment | 25 + ..._72e152294e36bc5f2d78e8e2ebed6a23._comment | 8 + ..._890e85df05903795e01efbd7879f9c87._comment | 8 + ..._042047f9fcc45abbfa47c3973d79f08e._comment | 10 + doc/bugs/gpg_needs_--use-agent.mdwn | 53 + doc/bugs/hGetContents:_user_error.mdwn | 38 + ..._30178f151f8c60d2ff856ca543dc506c._comment | 10 + ..._f74eeed4a007058a22183fd678ecd6c6._comment | 8 + ..._515e562228a89a13d6d857a874f4a468._comment | 8 + ..._8c6ed5e459c5c66b77db446c6317114c._comment | 8 + ..._f80bce48c3f96b0cd6892af43ee88a96._comment | 8 + ..._69dc09e4ae726856dafbeec34170671c._comment | 8 + ..._3f66b03f773341fad94ec16b4f55edaa._comment | 32 + ..._a697e2d36abfc999e65c9f587c0de56e._comment | 10 + ..._da7c5905a64bb6779970f9394155e629._comment | 10 + ...__40__or_this_a_general_problem__41__.mdwn | 113 + ..._rysnc_installed__44___not_recognized.mdwn | 19 + ..._3ff000eb3efde41426c7b086ae627dcf._comment | 12 + ..._34e592ab057df2df54e13d3f5cae64f0._comment | 14 + ..._05ffbae13d8f9b08315f40bb9b206f46._comment | 21 + ..._99d1f151263ca3433dd4afa8a928b1fe._comment | 30 + ..._6ef1a377b0b4d3efeffdf9693d0b496b._comment | 12 + ..._d9e36828ad55f3181a1c650010f23d6b._comment | 14 + doc/bugs/immediately_drops_files.mdwn | 222 + ...es___34____95__foo__34___as_extension.mdwn | 17 + ...ver_error_creating_repo_on_ssh_server.mdwn | 26 + ..._4a2c9338d5c779496049d78e29cf5cbd._comment | 8 + ..._choosing_encrypted_rsync_repo_option.mdwn | 28 + ..._14a2f775f43a86129ce3649a06f8ba0b._comment | 8 + ..._7b277320fcffd8d03e0d3d31398eb571._comment | 16 + ..._ba9dd8f2cc46640383d4339a3661571f._comment | 16 + ..._274ae39d55545bde0be931d7a6c42c94._comment | 12 + ..._242291d46acc61bdfc112e3316de528b._comment | 10 + ..._76b936263e82ca6c415a16ed57e770b4._comment | 8 + ..._9ccd3749fd9f32b0906c0b9428cc514f._comment | 10 + ..._4e8982668b5044b2286d55c90adb9da3._comment | 8 + ..._aaf0ee250972d737a2ca57de5b5f1c0a._comment | 12 + ...nterrupting_migration_causes_problems.mdwn | 52 + .../javascript_functions_qouting_issue.mdwn | 44 + ...journal_commit_error_when_using_annex.mdwn | 21 + ..._38f60ca3503ea1530c4bd2cde5c9182f._comment | 10 + ..._6de455a67f37d9ee0a307a78123781bf._comment | 10 + ...ant_causes_resource_starvation_on_OSX.mdwn | 30 + ..._91c911c29fd126ddc365c561591f627e._comment | 10 + ..._c316aead931a6a2377a4515bbb34ac5b._comment | 8 + ..._committer_thread_loops_occassionally.mdwn | 53 + ..._f8d1720aa26c719609720acf0772606e._comment | 11 + ..._0527569ea2924721d19dadcf4fe0ec5a._comment | 8 + ..._5b67ff08a897ea3d2266ccc910ab4278._comment | 8 + doc/bugs/make_SHA512E_the_default.mdwn | 29 + ..._install_can__39__t_be_used_with_sudo.mdwn | 20 + ...l_doesn__39__t_create_git-annex-shell.mdwn | 62 + ..._8c20edd8c6483500f807528d616c6dfd._comment | 14 + ..._8b2cf0fe7219e0bc83fd326adbf26c8a._comment | 31 + ..._25fe06eb127e59a4a07aeb52a5cfeabe._comment | 8 + ..._ec78032ba62d6918baa2c0b07ead5b50._comment | 8 + ...making_annex-merge_try_a_fast-forward.mdwn | 35 + ...ot_respecting_annex_ssh_options__63__.mdwn | 38 + ..._c63a1ed5909d53f116f06e60aba74dc6._comment | 10 + ...d_files_not_showing_up_in_unused_list.mdwn | 62 + ..._2cfbf6693b051c758fe5efa5ee885829._comment | 16 + ..._acb1abeb32c3aba8ba65151afbea753c._comment | 10 + ...or_bug:_errors_are_not_verbose_enough.mdwn | 26 + ...ng_dependency_in_git-annex-3.20130216.mdwn | 29 + .../missing_kde__47__gnome_menu_item..mdwn | 29 + doc/bugs/moreinfo.mdwn | 6 + ...is_not_in_Haskell_Platform_2012.4.0.0.mdwn | 12 + ..._2c4b3757bb8de563edca65aeabcbbc5a._comment | 29 + ...d_repo_results_in_errors_on_drop_move.mdwn | 59 + ...file_changed_to_annexed_on_typechange.mdwn | 38 + ..._6ac691645edb483797bee05043fd83b3._comment | 8 + ..._5d67e3a60b7cc30c2b1857f50895d363._comment | 8 + ..._78f1e081b92f418c20893d86a8715501._comment | 8 + ..._1e2a59e0eec89ef1a57d1488ff40dcf0._comment | 12 + ..._5e74431048b07631e0dbeca90fdb365b._comment | 47 + ..._3724e1c1a5fc6d3589452478249792ec._comment | 8 + ..._7f841ea7bf7d44f3d810ca097ac9eb47._comment | 8 + ...rd_output__41___from_git_annex_status.mdwn | 89 + ..._fcd230cbb2ac363c469b98021042c011._comment | 10 + ..._23207ecabd4b41d9551d0491fa71e96b._comment | 12 + ..._6ea92adfe955b6a5cd2a39fea78b3bf6._comment | 119 + ..._d0e55585f1612148163039d157253258._comment | 11 + ..._5506dc1b08516677886da4aa97263864._comment | 12 + ..._073449cc2cb73efd2b2d3d778a5573de._comment | 14 + ..._3516e71ba3b07427a10cbb4965712aa6._comment | 24 + ..._ea2e4704adb2f304f9c11c61eb62e919._comment | 8 + ..._4d17fedead7977541371a3f2c192e030._comment | 46 + ...o_have_annex_on_a_separate_filesystem.mdwn | 32 + ...ata_isn__39__t_unused_after_migration.mdwn | 66 + .../on--git-dir_and_--work-tree_options.mdwn | 31 + ..._13a7653d96ddf91f4492a9f3555a69aa._comment | 16 + ..._31f154011ec26a463de7b1e307e49cb6._comment | 8 + ..._33433bcfb1946b52f1f41b9158ab452d._comment | 8 + doc/bugs/ordering.mdwn | 12 + doc/bugs/pasting_into_annex_on_OSX.mdwn | 28 + ..._4eab52bb6eda92e39bdaa8eee8f31a7f._comment | 8 + ..._f1b58adfec179b75c1fc2bf578a3b5c4._comment | 8 + ..._270aa7680c3b899a92ce6543eaba666a._comment | 17 + ..._ec11a80d5b0f78c7a927f8aa71a6c57a._comment | 8 + ..._1928bd25e5e6874a3b83c2f2adc776f5._comment | 7 + ..._0fe288f54b781a0c51395cb32f0e2f9d._comment | 8 + doc/bugs/problem_commit_normal_links.mdwn | 59 + .../problem_with_upgrade_v2_-__62___v3.mdwn | 3 + ..._5f60006c9bb095167d817f234a14d20b._comment | 8 + ..._cd0123392b16d89db41b45464165c247._comment | 23 + ..._86d9e7244ae492bcbe62720b8c4fc4a9._comment | 16 + ..._91439d4dbbf1461e281b276eb0003691._comment | 8 + ..._ca33a9ca0df33f7c1b58353d7ffb943d._comment | 8 + ..._f360f0006bc9115bc5a3e2eb9fe58abd._comment | 10 + doc/bugs/problems_with_utf8_names.mdwn | 81 + ..._3c7e3f021c2c94277eecf9c8af6cec5f._comment | 17 + ..._bad4c4c5f54358d1bc0ab2adc713782a._comment | 10 + ..._4f936a5d3f9c7df64c8a87e62b7fbfdc._comment | 8 + ..._93bee35f5fa7744834994bc7a253a6f9._comment | 10 + ..._519cda534c7aea7f5ad5acd3f76e21fa._comment | 11 + ..._52e0bfff2b177b6f92e226b25d2f3ff1._comment | 8 + ..._0cc588f787d6eecfa19a8f6cee4b07b5._comment | 8 + ..._ff5c6da9eadfee20c18c86b648a62c47._comment | 10 + ...nishing_when_assistant_gets_restarted.mdwn | 34 + ..._53b4f388c47c1b3f6ffa4fc2155b30fc._comment | 21 + ..._e66532b23b089c9ea61122d6664cddb9._comment | 10 + ..._c9d692c867acc076f64f1213ea03ca11._comment | 8 + ...d___96__git_annex_sync__96___behavior.mdwn | 113 + ..._1a0b964f93c753838d6ccbdc8f79b39e._comment | 8 + ..._d22dcd7f95c5dc1c381c3c746781efce._comment | 8 + ..._a25140eb90f6b24c1a3ca39c901694e2._comment | 10 + ..._825e15183008ff7d97a81cacc3f55fb4._comment | 8 + ..._e858fc7c729cd39740354fb12627d556._comment | 10 + ..._9881b0f2dfb0907a60c0da296bc3da3f._comment | 10 + ..._ca017b9d3bafea4cb31448c802f3834e._comment | 8 + ...ve_file_in_place_on_checksum_mismatch.mdwn | 15 + ...heir_symlink_targets_don__39__t_exist.mdwn | 65 + ..._8a1d16b2aaba224e94be3d9dcc036d91._comment | 12 + ..._434ed328a22a6657dba3b2929a56e499._comment | 18 + ..._1837b70ace42882db3ab82e001680934._comment | 29 + ..._ca9c87a10f29e41572540edeb99652f2._comment | 11 + ..._69eafc4201e3014ef1b5d74fe319e462._comment | 10 + ..._b7a64db9abe006af8c30169ad849efe9._comment | 76 + ..._197ac6070f256131c6e18a07aa3834fa._comment | 14 + ..._fe07832333b536c71b7dcb46a4a44bd0._comment | 19 + ..._540bca4e6fdfc10eeab875ecc0f2b3f3._comment | 10 + ..._3f236b35e9820cd88bb77fcd57d6975e._comment | 43 + ..._3cc5dae0351201522711a7caeecd60d5._comment | 10 + ..._3c3883cb66d02a15d5de84d22aa113da._comment | 38 + ..._c8cece9559bd2dc6154cd28772369e48._comment | 10 + ..._device_configurator_chokes_on_spaces.mdwn | 18 + ...t_the_file__39__s_content_from_remote.mdwn | 27 + ..._d6aad1831674586fe4cdf61dd2a4bbb9._comment | 15 + ..._8591e174c1a8cddfae9371407a58ff1c._comment | 10 + doc/bugs/restart_daemon_required.mdwn | 22 + ..._f79ac16cc9f1e3b08cd121bf5efb29c3._comment | 8 + ..._50c1b268a3cc4514681059eabca674e3._comment | 8 + ..._1716e0f3c7c44dc77ebf7f00fdd8f9e3._comment | 310 + ..._3ce776786eca83fcb8ff94c8f6ff3eb9._comment | 15 + doc/bugs/rsync_remote_shows_no_progress.mdwn | 15 + ..._a7f5d646a924c462b987561cf6fc4318._comment | 8 + ...es_which_have_names_containing_spaces.mdwn | 50 + doc/bugs/scp_interrupt_to_background.mdwn | 2 + ...on_without_having_to_be_in_a_git_repo.mdwn | 11 + doc/bugs/signal_weirdness.mdwn | 48 + doc/bugs/smarter_flood_filling.mdwn | 31 + doc/bugs/softlink_mtime.mdwn | 54 + ...ssh_connection_caching_broken_on_NTFS.mdwn | 66 + ..._54e7e12514f4c109fd57a4eb744b731a._comment | 14 + doc/bugs/submodule_path_problem.mdwn | 56 + ..._69aec9207d2e9da4bc042d3f4963d80e._comment | 48 + ..._53d9eb28cb70b51637470175a80ddf35._comment | 8 + ..._aa5e0f99000a5b4988bccbb2ca28353b._comment | 20 + ..._ab1508a5a04e2106aad5e7985775a6fa._comment | 8 + ..._8c7539d1c11b81f5d46aa8e1c61745ae._comment | 14 + ..._cacc91afcb1739dfca3a60590bb70356._comment | 67 + ...ave_the_32bit_version_installed__41__.mdwn | 50 + ..._6208e70a21a048d5423926d16e32d421._comment | 9 + ..._8765b6190e79251637bb59ba28f245c1._comment | 21 + ...h_the_annex_directory_exposed_to_http.mdwn | 20 + .../test_suite_failure_on_samba_mount.mdwn | 278 + ...st_suite_shouldn__39__t_fail_silently.mdwn | 3 + ..._is_no_global_.gitconfig_for_the_user.mdwn | 50 + ...-_after_an_update_of_haskell_platform.mdwn | 23 + ..._20a6fe046111e9ae56fd4d9c9f41f536._comment | 8 + ..._6fdc5f8b07908c6eda8a97690408f44e._comment | 45 + ..._014474a133c7ff0131029d8721afc710._comment | 46 + ..._9c537e251dc99667fe87870804d802c2._comment | 10 + ...a41_disables_the_watch_command_on_OSX.mdwn | 22 + .../three_character_directories_created.mdwn | 56 + ..._dd91de24dab4f2eaded1f7d659869d4d._comment | 8 + ..._f6375964a6c8bb1e6c5b7238effca66d._comment | 62 + ..._776e0a9b938d8b260a5111594b442536._comment | 8 + ..._e288bacdb336c4886adb6eeb4dca1e92._comment | 8 + ..._359b80948ac92a0f1eb695599456486c._comment | 10 + .../three_way_sync_via_S3_and_Jabber.mdwn | 119 + ..._fc5ec5505f141bb9135e772d1094bc4d._comment | 12 + ..._0df2210c30dec6d88d7858d93eec19a3._comment | 10 + ..._41682b2e72e657e0f23af244f8345e85._comment | 10 + ..._c7b4ea9aea6839763eb8b89e8d6a5ad5._comment | 14 + ..._063f5e5e554ad6710f16394906d87616._comment | 33 + ..._197ad39b4a46936afeeb04eb26cf1ef3._comment | 138 + ..._0b0d829ccd255be0177ae9d8f6b10e63._comment | 61 + ..._37a8e19440c764317589bc4248cbccdf._comment | 10 + ..._12eb333327d31ca2bfee3f3c5e26d641._comment | 24 + ..._e6b1084b2f18d8e536c8692e165754a3._comment | 12 + ..._2120a1c3e5f490a55f68bb1bef5efd0d._comment | 183 + doc/bugs/tmp_file_handling.mdwn | 13 + ..._0300c11ee3f94a9e7c832671e16f5511._comment | 13 + ..._cc14c7a79a544e47654e4cd8abc85edd._comment | 8 + ...s_problems_on_non-linux_based_systems.mdwn | 19 + ..._1d38283c9ea87174f3bbef9a58f5cb88._comment | 10 + ..._bf112edd075fbebe4fc959a387946eb9._comment | 8 + ..._a46080fbe82adf0986c5dc045e382501._comment | 8 + ..._760437bf3ba972a775bb190fb4b38202._comment | 8 + ..._060ba5ea88dcab2f4a0c199f13ef4f67._comment | 10 + ..._548303d6ffb21a9370b6904f41ff49c1._comment | 42 + ..._7ca00527ab5db058aadec4fe813e51fd._comment | 8 + ..._881aecb9ae671689453f6d5d780d844b._comment | 8 + doc/bugs/transferkey_fails_due_to_gpg.mdwn | 51 + ..._f6434400d528a0fa59c056995ff2e6f3._comment | 12 + ..._c540b05b62a3186a87efcb180ea2a52d._comment | 12 + ..._9ad2ef73169dbd2866da2f4259ab0f00._comment | 8 + ..._7631b8842efba6a4aad87386ce9443a7._comment | 8 + ...dy_to_add_remote_server__34___message.mdwn | 16 + ..._repository_group_of___34__here__34__.mdwn | 13 + ..._do_not_work_when_git_index_is_broken.mdwn | 14 + ..._1931e733f0698af5603a8b92267203d4._comment | 8 + ..._40920b88537b7715395808d8aa94bf03._comment | 8 + ...nannex_command_doesn__39__t_all_files.mdwn | 30 + ..._object_even_if_referred_to_by_others.mdwn | 20 + ..._0ce72d0f67082f202cfa58b7c00f2fd3._comment | 39 + ..._647f49ffcaa348660659f9954a59b3ae._comment | 16 + ..._3f7f4b55b7ec2641a70109788e0b5672._comment | 10 + ..._313d393c416495aa0f8573113e41c2f7._comment | 431 + ..._c0e7742672db2629bd906cebefe74f72._comment | 10 + ..._c56171665db3ed14109a09097d49ac5d._comment | 8 + .../unannex_vs_unlock_hook_confusion.mdwn | 15 + doc/bugs/undefined.mdwn | 5 + doc/bugs/unfinished_repos_in_webapp.mdwn | 27 + ..._9628b100e39489be9f28ef75276a7341._comment | 11 + ..._ba0fbff536b1d067c4098db401dc49f2._comment | 10 + ..._fd554aa7d93117177784a29270ccf790._comment | 12 + doc/bugs/unhappy_without_UTF8_locale.mdwn | 41 + ...d_indirect_don__39__t_work_on_android.mdwn | 23 + ..._fec69c4c41987b9469eaa8f745c0a124._comment | 8 + ..._54c3fa77a069b36d03c41aad08fee9af._comment | 8 + ...t_abort_when_hard_link_creation_fails.mdwn | 47 + .../uninit_does_not_work_in_old_repos.mdwn | 20 + ..._bc0619c6e17139df74639448aa6a0f72._comment | 8 + ...if_git-annex_add_didn__39__t_complete.mdwn | 15 + ..._when_branch_git-annex_is_checked_out.mdwn | 15 + ...ails_silently_with_directory_symlinks.mdwn | 53 + ..._os_x_10.6_-_cp:_illegal_option_--_-_.mdwn | 22 + ..._a634a9f1c023bf836183de64abab1224._comment | 10 + ..._d9ae61a7c3f1eb243ca650945b40f21d._comment | 19 + ..._fe229c03c14e8eb2b57389e0e193ed99._comment | 8 + ..._fa12afe295de63c4aa7eb043b715325a._comment | 15 + ...hen_lock_of_uncommitted_file_loses_it.mdwn | 7 + ...ed_.git-annex__47____42___directories.mdwn | 16 + ..._9ca2da52f3c8add0276b72d6099516a6._comment | 10 + ..._e14e84b770305893f2fc6e4938359f47._comment | 18 + ..._ec04e306c96fd20ab912aea54a8340aa._comment | 8 + ...rab_archive_content_onto_client_again.mdwn | 434 + ..._51097a6b84edcc607abc0e6e21ca21f2._comment | 8 + ..._c34a4009213c410bba3c147ae0552029._comment | 15 + ..._634542867fd28962c47b7bc3ea022175._comment | 8 + ..._301f3ff2d203ac4c58a037e553b2c14d._comment | 18 + ..._82ecdc88ccc1f87386b128adc4ff9af4._comment | 14 + ..._158b2ba3da815910505899606177d415._comment | 16 + ..._b068924802f3917e3e005350cb0cc2a2._comment | 8 + ..._f4772858c927d4a62edc3caf59b5da10._comment | 8 + ..._d0923d2950357f4444c5ef94ff196ba3._comment | 8 + ..._7fb30cb80aecc60e48c64846aa185206._comment | 9 + ...ploads_queued_to_annex-ignore_remotes.mdwn | 34 + ..._fa1c98f38253db8c2be3604c72eb3726._comment | 17 + ...te_format_generates_irritating_output.mdwn | 28 + ..._fceba878f1097e27f056580e8d6d5b31._comment | 26 + ..._416992874813f120721a56d88b2bef65._comment | 9 + ..._a20f470c5226ac5693eb15146a02b3f5._comment | 8 + ..._a81f06191bc03a7aad5929af99f0634e._comment | 28 + ..._7438caecf78b4fb5d21f9f31dff95cf2._comment | 14 + doc/bugs/utf8.mdwn | 187 + ..._f298b8b480d3ab2dd9c279589afcd0ea._comment | 10 + ..._a8864a46f8154680beeea27449ac6f09._comment | 142 + ..._2202c3479d19d306f31aac5a47b55e7d._comment | 10 + ..._7044d2c5bb1c91ee37eb9868963a1ff2._comment | 41 + ..._656b3caa16ae93b092fb5804fa575a3b._comment | 8 + ..._25b3d4c47c45b72129b17b171a45c5f9._comment | 8 + ..._2aaab9253bbc75012292c7b5a7d55696._comment | 173 + ..._416ad6fb5f7379732129dc5283a7e550._comment | 23 + ..._cd55f6bbeb145fd554f331dcff64f5e1._comment | 10 + ..._bb583a419d6fa4e33e5364c4468b35c6._comment | 8 + ..._cd8a22cfb70d9d21f0a5339ccc52ee93._comment | 14 + ..._14eefd4bee283802e9c462fa20b7835c._comment | 19 + ..._58d8b5bdb9f11e8c344e86a675a075dd._comment | 11 + ..._00fa9672ce55b6bfa885b8a13287ac25._comment | 10 + ..._a01e26fa0fafbc291020f53dbfdf6443._comment | 10 + ..._b7c084be01ce985be51e48503fcba468._comment | 8 + ...st.log_and_remote.log_merge_wackiness.mdwn | 36 + ...__34___but_podcasting_feature_working.mdwn | 28 + ...rnal_Server_Error__internal_liftAnnex.mdwn | 20 + ..._57547f9a480df2c3f7b3997b0fb7039a._comment | 12 + ..._99f12da3ef01141dc7a9105fcf966793._comment | 10 + ...40__handle_is_closed__41___on_Android.mdwn | 24 + ..._e8866dc15f8fc049229e7451addad1d5._comment | 12 + ..._ee616b0251ffaace9844cfd7af896c35._comment | 10 + ..._6b8bd314b647ea3a485f5faf4856f9a9._comment | 35 + ..._7009b6727ba40bc9bd1b1f939e75d093._comment | 8 + ..._00ddf7ade6cca758afa838be0b9588cb._comment | 20 + ..._6137ef0ad01d5600dab6fccbeed9a88b._comment | 14 + ..._4b79d7ea338d9f70eb80b8cc2c5a21e4._comment | 8 + ...and_on_OSX_--_hangs_with_a_small_repo.mdwn | 41 + ..._63f04694610839db0c2381005b15da35._comment | 14 + ..._8afe4720e301cf7ccf11ff0a23261936._comment | 8 + doc/bugs/watch_command_on_OSX_10.7.mdwn | 37 + doc/bugs/watcher_commits_unlocked_files.mdwn | 37 + ..._f70e1912fde0eee59e208307df06b503._comment | 8 + doc/bugs/webapp_hang.mdwn | 144 + ..._08aa908a64d0fe2d50438d01545c3f01._comment | 8 + ..._2a21ac5657128a454f9deb77c4d18057._comment | 21 + ..._error_upon_creating_the_initial_repo.mdwn | 26 + ..._1bcf0f565eacac851bd21cd428c8e0a5._comment | 33 + ..._7dd2483b5b07df8f3b37a34651c05962._comment | 8 + ...uires_reload_for_notification_bubbles.mdwn | 41 + ..._b15480e5dec1ffbebb8cde1ca8d7c9d5._comment | 11 + ..._8dad57a852e1db804aa38f90f3bb398b._comment | 8 + ...___34__Added_x_files__34___a_bit_ugly.mdwn | 15 + doc/bugs/weird_local_clone_confuses.mdwn | 20 + ...uts_no_informaiton_for_unlocked_files.mdwn | 44 + doc/bugs/windows_install_failure.mdwn | 30 + ..._f339574c7cfa35c1f0dfd515fde457f5._comment | 8 + ..._1d3364d8f5c4963f3a7e473298ec6ed1._comment | 8 + ...rt_-_can__39__t_directly_access_files.mdwn | 250 + ..._03ef9d33839173044dcc4f2b37f575d2._comment | 8 + ..._c65e5491c82908af46fe2c97e048d210._comment | 20 + ...when_adding_17_files_at_once_or_more_.mdwn | 197 + ...po_can__39__t_pull_newly_added_files_.mdwn | 559 + ..._b4f5e2d6a0d690f6b0089fa80a3c920b._comment | 8 + ..._c2092add1430667108a3fdc5e1c9b5f5._comment | 8 + ..._f0ea453951daf84dbddc653ac64822b6._comment | 8 + ..._35a8be5ecc9d1b72c38f8ddb47678160._comment | 8 + ..._29e72997b88f91f84639587b4cede34c._comment | 76 + ..._2de7f6532de4cbc21737ce53a89d6525._comment | 10 + ..._80d130b5af829763be77c61a9c5ca306._comment | 29 + ..._ec199db851952b40e8b18922da574ea4._comment | 8 + ..._d269fcadea9d5a668e3c6d6cf019f56a._comment | 353 + ..._908d1b981d56107f29d8972bf11aefc8._comment | 12 + ...en_running___96__git_annex_init__96__.mdwn | 5 + .../wishlist:_generic_annex.cost-command.mdwn | 17 + ...it_annex_reinject_work_in_direct_mode.mdwn | 18 + ...e_commit_messages_in_git-annex_branch.mdwn | 39 + ...int_more_info_with___39__unused__39__.mdwn | 37 + ...gs_like_description__44___trust_level.mdwn | 4 + ..._14311384788312b96e550749ab7de9ea._comment | 10 + ..._342d1ac07573c7ef4e27f003a692e261._comment | 32 + doc/bugs/wishlist:_simple_url_for_webapp.mdwn | 36 + ..._552aad504fbb68d1f85abfde8c535e69._comment | 10 + ...rt_drop__44___find_on_special_remotes.mdwn | 18 + ..._f11ed642a83d965076778a162f701e84._comment | 8 + ...__.config__47__git-annex__47__program.mdwn | 20 + ..._44c11918d00ead38d40556aade98c0af._comment | 12 + doc/bugs/xdg-user-dir_error.mdwn | 8 + ...s_one_account_per_distinct_repository.mdwn | 107 + ..._820732c4dcb15186b4f635c50fdb0805._comment | 19 + ...sod-default_is_needed_as_a_dependancy.mdwn | 10 + doc/bugs/yesod-form_missing.mdwn | 23 + doc/coding_style.mdwn | 92 + doc/comments.mdwn | 9 + doc/contact.mdwn | 11 + doc/copies.mdwn | 38 + doc/design.mdwn | 6 + doc/design/assistant.mdwn | 61 + doc/design/assistant/OSX.mdwn | 13 + ..._9290f6e6f265e906b08631224392b7bf._comment | 14 + doc/design/assistant/android.mdwn | 42 + ..._316bde8d22628e5e9d4f8dabce1d2ad4._comment | 14 + ..._8be9a74e5fc4641c2bf2e1bb7673dd59._comment | 8 + ..._3dd386ac1b757c73d14f14377b9eedd4._comment | 8 + ..._5dca47a4599d6e88d19193701c5a571b._comment | 46 + ..._054f06311e2b51d73be569f181eb004f._comment | 10 + ..._bb3d36e9d29f2fa77bee6d47ef9917fe._comment | 21 + ..._fee32a831eeb5736fe1dce52e30320c8._comment | 16 + ..._d8e9b0a5287fc96b19dc2cb9da3586ce._comment | 17 + ..._79a7b5bb5f4aaeea4a4e8ced0561701a._comment | 11 + ..._55ea70a6929523d26248ff6409b04a6e._comment | 10 + doc/design/assistant/blog.mdwn | 10 + .../blog/day_100__cursed_clouds.mdwn | 19 + .../day_102__very_high_level_programming.mdwn | 37 + ..._c028b403261dd66bcf83e6ffd134b80b._comment | 8 + .../assistant/blog/day_103__bugfix_day.mdwn | 25 + doc/design/assistant/blog/day_104__misc.mdwn | 18 + ..._13d7fad2d3f8eab10314784c035e2a16._comment | 8 + .../assistant/blog/day_105__lazy_Sunday.mdwn | 43 + .../assistant/blog/day_106__lazy_Monday.mdwn | 10 + .../assistant/blog/day_107__memory_leak.mdwn | 11 + .../day_108__another_zombie_outbreak.mdwn | 33 + ..._194c48d65993462f809a2cfaa774a3e2._comment | 11 + ..._ef5ee5933fcadcb81cc81b816db14bda._comment | 8 + .../assistant/blog/day_109__dropping.mdwn | 16 + doc/design/assistant/blog/day_10__lsof.mdwn | 54 + ..._9b8c28c85c979f32e5c295b6a03c048e._comment | 9 + .../blog/day_110__more_dropping.mdwn | 55 + .../blog/day_111__config_monitor.mdwn | 18 + ...ow_for_something_completely_different.mdwn | 50 + ..._5e4fe1538d9ae1c450b0a6602fc6d29b._comment | 10 + ..._c5a734f611ecc95729904e645583ee43._comment | 8 + ..._46b16dcd0fce07036cd8ed6ed9d2b055._comment | 8 + ..._1fe036e4c65fb4211aa2c394f535344a._comment | 8 + ..._e4ba3568c4efd98f212dd47427a1cf47._comment | 10 + .../blog/day_113__notifier_work.mdwn | 22 + doc/design/assistant/blog/day_114__xmpp.mdwn | 56 + ..._c2b0617a2fc3dc4f19a6be6947913842._comment | 8 + ..._d14375dfb5791615802dab3c5438f8e2._comment | 8 + ..._6d72ea32c111e605be30ad2153fc71c9._comment | 10 + ..._e51d6f854db5f9e74a1aa58bd8923795._comment | 12 + .../assistant/blog/day_115__my_new_form.mdwn | 17 + .../assistant/blog/day_116__the_segfault.mdwn | 25 + .../blog/day_117__new_topologies.mdwn | 41 + .../blog/day_118__monadic_discontinuity.mdwn | 15 + .../blog/day_119__time_for_testing.mdwn | 12 + .../assistant/blog/day_11__freebsd.mdwn | 50 + .../assistant/blog/day_120__test_day.mdwn | 2 + .../assistant/blog/day_121__buddy_list.mdwn | 10 + .../assistant/blog/day_122__xmpp_pairing.mdwn | 29 + ..._e95efb23eb2e67e3f11a5c7de56424a7._comment | 10 + ..._30e251e73146512bde8b2f69eddeef2e._comment | 8 + .../blog/day_123__xmpp_insanity.mdwn | 49 + ...ay_124__git_push_over_xmpp_groundwork.mdwn | 28 + .../blog/day_125__xmpp_push_continues.mdwn | 15 + .../blog/day_126__mr_watson_come_here.mdwn | 52 + ..._ee1361e6b235f4e1c00596ba516b519a._comment | 10 + ..._8eb366ae7efb347bd3bbd9a98e0821b3._comment | 8 + .../assistant/blog/day_127__xmpp_syncs.mdwn | 35 + .../blog/day_128__last_xmpp_day.mdwn | 49 + ..._fd8c1d6358cb50f4dad8ba11d33d861f._comment | 10 + ..._43664b73c71c41d71bc95e665f128106._comment | 8 + ..._d369b04f686009a9dbb57b999107a55e._comment | 11 + ..._095855d301e7ccd3689ffe507cfb63ee._comment | 8 + ..._da7b0586b0b28e1e0fe4126f6543a7bc._comment | 9 + ..._2f9ba367e19d77bf52f372b6f0f5938a._comment | 8 + .../assistant/blog/day_129__release.mdwn | 4 + .../assistant/blog/day_12__freebsd_redux.mdwn | 23 + ..._5da32cf53f1de27bfe6cec2d294db3e1._comment | 8 + ..._696d6e22034acf5bb60d80124b72ef2f._comment | 8 + .../assistant/blog/day_130__what_now.mdwn | 36 + ..._402f00cc034351d8253a797dd4de55bf._comment | 8 + .../blog/day_131__webdav_groundwork.mdwn | 28 + .../blog/day_132__webdav_continued.mdwn | 22 + .../blog/day_133__webdav_working.mdwn | 31 + .../blog/day_134__box.com_configurator.mdwn | 8 + .../blog/day_135__progress_revisited.mdwn | 37 + doc/design/assistant/blog/day_136__misc.mdwn | 14 + .../assistant/blog/day_137__Glacier.mdwn | 30 + doc/design/assistant/blog/day_138__back.mdwn | 25 + ..._65a8499b284bed38d2bde1886a54a311._comment | 8 + .../assistant/blog/day_139__catch_up.mdwn | 11 + .../blog/day_13__kqueue_continued.mdwn | 34 + .../blog/day_140__release_monday.mdwn | 25 + .../blog/day_141__release_tuesday.mdwn | 6 + ..._a5adea7a726df12f9121c744a036f08d._comment | 10 + .../assistant/blog/day_142__filling_in.mdwn | 9 + .../assistant/blog/day_143__what_next.mdwn | 22 + ..._40cf25a2ebdd43d8974a28e180e100e5._comment | 13 + ..._af9ccbbc5131e6333c029415141bdb51._comment | 10 + .../assistant/blog/day_144__webapp_work.mdwn | 8 + .../blog/day_145__more_webapp_work.mdwn | 12 + .../assistant/blog/day_146__meanwhile.mdwn | 22 + .../assistant/blog/day_147__direct_mode.mdwn | 36 + ..._0bd69532afce9dc04e3d88bfd0aed4b2._comment | 16 + ..._3b26f0d081c3bf1037bb872d529ce825._comment | 8 + .../assistant/blog/day_148__direct_mode.mdwn | 42 + .../assistant/blog/day_149__rainy_day.mdwn | 15 + .../blog/day_14__kqueue_kqueue_kqueue.mdwn | 23 + .../blog/day_14__thinking_about_syncing.mdwn | 44 + doc/design/assistant/blog/day_150__12:12.mdwn | 53 + .../blog/day_151__direct_mode_toggle.mdwn | 59 + .../assistant/blog/day_152__bugfixes.mdwn | 18 + ..._46863a875f9daa6f2c9248b66ff91929._comment | 9 + ..._a586e617bc024c8a9ff60f1b8345d74d._comment | 8 + .../assistant/blog/day_153__hibernation.mdwn | 26 + .../blog/day_154__direct_mode_merging.mdwn | 21 + .../assistant/blog/day_155__bugfixes.mdwn | 15 + ...ay_156_and_157__direct_mode_assistant.mdwn | 45 + .../assistant/blog/day_158__fsevents.mdwn | 20 + ..._b278372ac6399f64d5fa9da178278a6d._comment | 8 + ..._2d5ce9b2807068c3517e185945662bd2._comment | 8 + .../blog/day_159__fsevents_and_assistant.mdwn | 16 + ..._b85f446c3fa8d703a2a8882825c6f33f._comment | 8 + ..._a150b404e0c39e0bb2f7dd00cda63cdc._comment | 8 + ..._37abc41bae23a1d7de0d19d952aec492._comment | 8 + .../assistant/blog/day_15__its_aliiive.mdwn | 33 + .../day_160__finishing_up_direct_mode.mdwn | 10 + .../assistant/blog/day_161__release_day.mdwn | 8 + ..._e82c67f3ce216618149537bba1e0b850._comment | 19 + ..._b1fe96fd818935c0497b78bb8ad32ffa._comment | 14 + ..._40bac0e1756aa77bb966c4654857141c._comment | 44 + ..._af65656b0d1179636937595868bb97b0._comment | 30 + ..._0c05caaaf9588e124585041bf5f45d75._comment | 20 + ..._5dfb5f428633d6062925f61af2b8829b._comment | 23 + ..._ac4effb381b08d94d4a2d2482e92c89a._comment | 13 + ..._32600e89e3098e52a1280895e03b3f86._comment | 13 + ..._07e5d0c3cad0ce2bd4943e53b61f1767._comment | 8 + doc/design/assistant/blog/day_162__UI.mdwn | 17 + .../blog/day_163__free_features.mdwn | 32 + .../assistant/blog/day_164__bugfixes.mdwn | 17 + .../assistant/blog/day_165__release_day.mdwn | 16 + .../blog/day_166__a_short_long_day.mdwn | 25 + .../day_167__safe_direct_mode_transfers.mdwn | 12 + ..._f1aa64fe803d8c14b250a4e98b88142a._comment | 8 + ..._5ce1db84c9ead713f1272c4975645b93._comment | 8 + .../blog/day_168__back_to_theme.mdwn | 18 + ..._f248780bfcbd0384d9d72c2633a4ea46._comment | 12 + ..._5beba073373b8e75a32d1fcfdc1a0782._comment | 13 + .../blog/day_169__direct_mode_is_safe.mdwn | 24 + ..._65f87656c4e6bc7cdb614f53961341c9._comment | 8 + ..._a116a402a126c62be54c06afd82439ab._comment | 19 + .../blog/day_16__more_robust_syncing.mdwn | 44 + ..._23e7a90429e4431f90787cd016ebe188._comment | 8 + ..._8e7e7cd27791bb47625e60a284e9c802._comment | 10 + .../blog/day_170__bugfixes_and_release.mdwn | 8 + doc/design/assistant/blog/day_171__logs.mdwn | 23 + .../assistant/blog/day_172__short_day.mdwn | 22 + ..._b75e26b77a23a45da1c4c3bca1399246._comment | 12 + .../assistant/blog/day_173__snow_day.mdwn | 22 + .../blog/day_174__last_weekend_before_AU.mdwn | 25 + ..._05a8fd47f54373331741cc869a53b0c3._comment | 10 + ..._fc8e65eef954c4caa8321c2fe8b711b7._comment | 8 + ..._399534f540d85cac067fbb7be9d373b4._comment | 8 + .../blog/day_175__pacific_features.mdwn | 15 + ..._c3ee4386f872b7c76aaecfa638b368cb._comment | 9 + .../blog/day_176__thread_management.mdwn | 13 + .../assistant/blog/day_178__bus_hacking.mdwn | 10 + .../blog/day_179__brief_updates.mdwn | 19 + ..._920a84457d40358507a3eb817a4568d9._comment | 8 + .../blog/day_17__push_queue_prune.mdwn | 19 + doc/design/assistant/blog/day_180__back.mdwn | 7 + .../assistant/blog/day_181__triage.mdwn | 23 + .../assistant/blog/day_182__it_begins.mdwn | 50 + .../assistant/blog/day_183__plan_b.mdwn | 19 + .../day_184__just_wanna_run_something.mdwn | 46 + ..._689adac7e26cb0b0a4e7ecc787cfd716._comment | 16 + .../blog/day_185__android_liftoff.mdwn | 20 + ..._b7d28010a72619a7e9a5ad4f2a0d6c07._comment | 9 + ..._ddeb24e86fafb7dae93142cc02767aad._comment | 10 + .../blog/day_186__Android_success.mdwn | 33 + ..._1629da240ca7db5f8a32059f561fd435._comment | 8 + .../blog/day_187__porting_utilities.mdwn | 22 + ..._0e6a3f4fe8e09f247fa04156bc60f8c7._comment | 8 + .../day_188__crippled_filesystem_support.mdwn | 37 + ..._32a296fce23ae4b1e18bd5a9964bf619._comment | 14 + .../blog/day_189__more_crippling.mdwn | 44 + .../assistant/blog/day_18__merging.mdwn | 82 + .../assistant/blog/day_190-191__weekend.mdwn | 28 + ..._dbd692d12c14d08acd7d73a655b34e8b._comment | 10 + ..._c813830e53471a9732e010a748d574fc._comment | 28 + .../blog/day_192_193__more_porting.mdwn | 44 + .../assistant/blog/day_194__nice_moment.mdwn | 37 + .../blog/day_195__real_android_app.mdwn | 32 + ..._0112007552b30cd9bfeac614a1e399c4._comment | 10 + ..._230d3c169c713f613b9d607d84ce5092._comment | 12 + ..._8d74ad2a61c02272758d157282ad56ec._comment | 10 + ..._4f6bc0680f2debd638933968a26975e0._comment | 10 + ..._71539c62608866464e8faa76bc522a55._comment | 9 + ..._e1b205289721ae79ac7fbed2f44018b2._comment | 10 + ..._4bc0aeae4fa1116944644c64feaf9697._comment | 8 + ..._17bb6e7565d4c757f6c1e3514c22f47d._comment | 10 + ..._cd8a6bec0f7c6843dd11d3266f25f864._comment | 44 + ..._2d2eee4bcbbd1d069a80bff5edc90c3c._comment | 13 + ..._3d96568c469a8c53a982f304eae5e7d4._comment | 10 + ..._e8667c47d07fc842cf0fe2ebbfbc1c58._comment | 8 + ..._cf8da7720ddc20b05955ee671ca4acd5._comment | 8 + ..._f4709bdbc739182819b648fd6aa00531._comment | 36 + ..._e66af12c7eca0d457b8406e9fb4b69be._comment | 8 + .../blog/day_196__android_bugfixes.mdwn | 26 + .../blog/day_197__template_haskell.mdwn | 36 + ..._82d9f9508929d84abf7b718c59436ae8._comment | 18 + .../assistant/blog/day_198__bugfixes.mdwn | 11 + ..._5a15b5bad0f9ba2423d2aebe440ac0ea._comment | 19 + ..._36d94b838e5e65c85e7afaabe8a578f1._comment | 12 + ..._ae9b74341a3bc6e1e84d2c0ca4c5f612._comment | 10 + ..._5a4827227c03bcff3b1e4c44b531f816._comment | 12 + ..._9c5f4c85217e898be4c57c615e53c36f._comment | 8 + ..._bccf1abfb7f56d97673158f3ccfce511._comment | 9 + ..._6f1b51b002cc5d2b505d80e3e04bf6f3._comment | 8 + ..._8a3542437663028b17442818eba3f7c5._comment | 9 + .../day_199__wrapping_up_Android_for_now.mdwn | 26 + ..._ec57358afc7e78d2860aa4237793832d._comment | 11 + .../blog/day_19__random_improvements.mdwn | 50 + doc/design/assistant/blog/day_1__inotify.mdwn | 57 + .../assistant/blog/day_200__release_day.mdwn | 19 + ..._40cfe9bfd9e611fd734dbb5aad348aa3._comment | 10 + ..._b26890fdae575d42170988073fb2e45d._comment | 8 + ..._710a30c5d31bf549833ecfe9a0997c94._comment | 8 + ..._b6f62ab7e810ba6d3a43f0ead370c79a._comment | 8 + ..._a68e1ed7829b49086c567d97ddc09912._comment | 8 + ..._39d3ad0a029fe56e96f97d28d17fbbd2._comment | 8 + ..._5b752d6a8d74e61190f09384b6108206._comment | 31 + ..._881274ae0d6230bb4cafa4151ad72b49._comment | 12 + ..._e220059be77cf0ef396f37a4f9ccf9b5._comment | 8 + ..._ec2152151188dd252cdb61c68cfc12e4._comment | 10 + ..._42572411617c287368482bb9dcf94324._comment | 18 + ..._6b69aa81a9ba4e07e547ed1869946d51._comment | 15 + ..._b070a2e4151d9fbf43d7906efa78515f._comment | 12 + .../blog/day_201__real_Android_wrapup.mdwn | 38 + ..._88b9950c51324f0bb89c5646b3170952._comment | 19 + .../blog/day_201__real_Android_wrapup/fib.png | Bin 0 -> 69535 bytes .../blog/day_201__working_web_server.mdwn | 31 + .../blog/day_203__procrastination.mdwn | 25 + .../blog/day_204__deprocrastination.mdwn | 62 + .../day_205_206__rainy_day__snow_day.mdwn | 12 + doc/design/assistant/blog/day_207__XMPP.mdwn | 7 + .../assistant/blog/day_208__bugfixes.mdwn | 17 + .../assistant/blog/day_209__The_Bug.mdwn | 23 + .../blog/day_20__data_transfer_design.mdwn | 22 + .../assistant/blog/day_210__spring.mdwn | 29 + .../blog/day_211__zooming_along.mdwn | 24 + .../blog/day_212__accidental_all_nighter.mdwn | 24 + ..._6ee1f8056eedb6eb18013faf8f5ec212._comment | 8 + ..._07c83d75bb105bb77ada07359ed0ea7a._comment | 8 + ..._2c904d33f4f14807fbe718a01e98800a._comment | 8 + ..._59ec5c1cab75df87293800a7a03fe9c6._comment | 8 + ..._13893f106e835dcc52e03c7c6740c35b._comment | 8 + doc/design/assistant/blog/day_213__costs.mdwn | 34 + .../assistant/blog/day_214__release_day.mdwn | 5 + .../blog/day_215__dashboard_UI_refresh.mdwn | 25 + .../blog/day_216__more_bugfixes.mdwn | 42 + ..._299462bcdd0e4f6cd7895b5f40ca00ad._comment | 10 + ..._1913d65dfe4ba08379d82a4a2ca91c40._comment | 8 + ..._92c774599a78540ad398afcd1d05f7ce._comment | 20 + .../assistant/blog/day_217__nothing.mdwn | 2 + .../assistant/blog/day_219__bug_triage.mdwn | 14 + ..._c6b977a969cacdce62987a439b7686f5._comment | 16 + .../blog/day_21__transfer_tracking.mdwn | 28 + .../assistant/blog/day_220__performance.mdwn | 40 + .../blog/day_221__this_and_that.mdwn | 28 + doc/design/assistant/blog/day_222__back.mdwn | 16 + ..._f05b48231a1ee0cffba7d66e112e5551._comment | 8 + ..._4d5f003ccd81580017ebf0dc31bc9cda._comment | 8 + .../blog/day_223__progress_revisited.mdwn | 24 + .../blog/day_224__annex.largefiles.mdwn | 23 + ..._408e4021b18f7ff5548d2d19ab558922._comment | 8 + .../blog/day_225__back_from_the_dead.mdwn | 47 + ..._9ac37c3b5c4c72ec8a39dce00bcbe420._comment | 8 + ..._26125dd9ef2bd10b597d14b2c6180952._comment | 8 + .../assistant/blog/day_226__poll_results.mdwn | 28 + ..._1ed980472214be6d0a8cf55f37797fda._comment | 8 + ..._6823b0a9a8037f1a5214db4db98fb16e._comment | 8 + .../day_227__bigfixing_all_day_today.mdwn | 21 + ...228__more_work_on_repository_removals.mdwn | 27 + .../blog/day_229__rainy_day_bugfixes.mdwn | 17 + .../day_22__horrible_option_parsing_hack.mdwn | 34 + doc/design/assistant/blog/day_230__Mom.mdwn | 35 + ..._696bba2246c8a9e6ce4aed3071bcc96c._comment | 8 + ..._2fa295ab6db0828cb725cfcfb6777822._comment | 8 + ..._fafd7abec629290418334ddb015bf62c._comment | 10 + ..._450cac0f2e82c94fd34b527ae05ef1b8._comment | 8 + .../assistant/blog/day_231__insert_title.mdwn | 26 + .../blog/day_232__headless_webapp.mdwn | 22 + ..._0fdd77d143ecba6fdb9f75cb6fc37bfb._comment | 16 + ..._0784a2a73c3e2945f3d3f2577b3b9c9c._comment | 8 + ..._ccb9fa22422fb913b6a496ebe65c49fb._comment | 8 + ..._ceba4468760a2525960327698431cee2._comment | 8 + doc/design/assistant/blog/day_233__taxes.mdwn | 11 + ..._9473ffdc42595af9c293fbcd5a1cdb54._comment | 14 + ..._5feed8d7053ba03812ccda8c61fd9775._comment | 8 + .../blog/day_234__clean_shutdown.mdwn | 29 + .../assistant/blog/day_235__birthday.mdwn | 31 + ..._db558b071067c1e63cde05cca0551094._comment | 8 + ..._d1a2c1124781118267599457ae9e0512._comment | 8 + ..._b853508d1d15234958b9f4a39277e45c._comment | 8 + ..._73aad3398a43bc4d28bca9bf635fa757._comment | 8 + .../assistant/blog/day_236__evil_splicer.mdwn | 29 + .../day_237__gnome-keyring_craziness.mdwn | 29 + ..._0cb088b732881d1fa92493aa1fd93d43._comment | 8 + ..._b855fd710954beebaafe6d2bd03eb368._comment | 8 + .../blog/day_238__back_to_Android.mdwn | 11 + .../day_239__bugfixes_and_frustration.mdwn | 28 + .../blog/day_23__transfer_watching.mdwn | 25 + .../assistant/blog/day_240__it_builds.mdwn | 37 + ..._151840ae0020ea63b2f041488c905386._comment | 25 + .../assistant/blog/day_241__cleanup.mdwn | 14 + ..._0e283cdf66a25b3cc9423fe651084cb9._comment | 8 + .../assistant/blog/day_242__more_porting.mdwn | 4 + .../assistant/blog/day_243__in_the_field.mdwn | 21 + .../blog/day_244__android_porting.mdwn | 6 + doc/design/assistant/blog/day_245__misc.mdwn | 15 + ..._3a2976617bb0cdc206fb1397a2ef1177._comment | 8 + ..._e0f9704e91fedca8ff26356f354cc1c3._comment | 10 + ..._93003a0d0983efbdc046d7459be194b0._comment | 8 + .../blog/day_246__bug_treadmill.mdwn | 18 + ..._f76f653364fe2b97e85e8356c93b0fce._comment | 8 + .../blog/day_247__performance_tuning.mdwn | 16 + .../blog/day_248__Internet_Archive.mdwn | 28 + .../assistant/blog/day_249__quiet_day.mdwn | 7 + .../blog/day_24__airport_digressions.mdwn | 99 + .../assistant/blog/day_250__stymied.mdwn | 23 + ..._330a10d447ccc3db03fcbfe571dbb404._comment | 8 + .../blog/day_251__xmpp_improvements.mdwn | 34 + .../assistant/blog/day_252__release_day.mdwn | 6 + doc/design/assistant/blog/day_253__OMG.mdwn | 22 + ..._bbdc61092771163e65a90a4755a807d8._comment | 8 + .../blog/day_254__Android_app_polishing.mdwn | 35 + ..._37f4ff5227566ce4b3fa69fc32568841._comment | 14 + ..._58bbb105bdbb72bba85c3622195f43b9._comment | 12 + .../blog/day_255__Debian_release_day.mdwn | 26 + doc/design/assistant/blog/day_256__8bit.mdwn | 27 + ..._f9b50263e3997d4c5b9836a2e0a346d7._comment | 8 + .../assistant/blog/day_257__rainy_day.mdwn | 6 + .../blog/day_258__beginning_of_the_end.mdwn | 24 + .../day_259__Android_dominos_toppling.mdwn | 15 + ..._0b4a6e4893b0157e4768b46468dbbb87._comment | 10 + ..._1ebc5aff5d217e1392cb7c8bb6c5156b._comment | 14 + ..._eed7792f6142f3fc74d3c384bb16559b._comment | 8 + .../blog/day_25__transfer_queueing.mdwn | 41 + ..._59fd4f1ffe96c412f613dc86276e7dbd._comment | 10 + ..._93bf768a67117e873af5732ecf08dc78._comment | 7 + .../day_260__Windows_dev_environment.mdwn | 46 + ...day_261__Windows_first_stage_complete.mdwn | 29 + .../blog/day_262__DOS_path_separators.mdwn | 14 + ..._45ecae90b22e31202c21083980d6b567._comment | 10 + .../assistant/blog/day_263_catching_up.mdwn | 11 + ..._9023da0573dfc81644d68128adb331a7._comment | 8 + ...ay_264__Windows_second_stage_complete.mdwn | 21 + ..._42a7502d6ece75520eb59a76fdb1e2f0._comment | 9 + ..._f2b11322ac87e2a36cddc035b2c3c1ea._comment | 8 + ..._ea6ee05acb946fc7e8d95e62647cfa2a._comment | 8 + ..._9ce106baf28b7f75f7f6febd7bfcea70._comment | 8 + .../assistant/blog/day_265__correctness.mdwn | 23 + ..._e8959a6df87eb92310947e66c7471e97._comment | 12 + ..._0cb953fcc085eedb34e65c227309ede7._comment | 8 + ..._df57628a8969af2995732e7ea2a0fae3._comment | 10 + .../assistant/blog/day_266__release_day.mdwn | 6 + ..._92c8d1d9216b46b07dfe69bbc77a923e._comment | 8 + .../blog/day_267__windows_autobuilder.mdwn | 9 + ..._978b584d86395f2f621b0e1f7c5e70d7._comment | 21 + ..._8f978d2811c8fbf11e3d12f245bdb52b._comment | 10 + .../blog/day_268__core_monad_change.mdwn | 9 + .../assistant/blog/day_269__bugfixes.mdwn | 14 + .../assistant/blog/day_26__dying_drives.mdwn | 28 + .../blog/day_270__release_and_xmpp.mdwn | 39 + .../assistant/blog/day_271__more_xmpp.mdwn | 31 + .../assistant/blog/day_272__fuzz_tester.mdwn | 37 + .../assistant/blog/day_273-274__fun.mdwn | 19 + .../blog/day_275__working_hard_or.mdwn | 12 + .../blog/day_276__fuzzing_continues.mdwn | 12 + ..._f5dd0658511a1063c2eb025b0fe98426._comment | 14 + ..._a56c4c26a9e7bb8cfe3f598dbeed0813._comment | 10 + ...ay_277__private_static_protected_void.mdwn | 19 + .../assistant/blog/day_278__winding_down.mdwn | 11 + .../blog/day_279__final_release_prep.mdwn | 14 + .../blog/day_27__robust_transfers.mdwn | 31 + .../day_28-35__threaded_runtime_tarpit.mdwn | 17 + doc/design/assistant/blog/day_280__yesod.mdwn | 7 + ..._a42213a8cef71f2b54db18606028136d._comment | 8 + doc/design/assistant/blog/day_281__back.mdwn | 37 + ..._128809c5a2a9f5cc345a10fdbf55be01._comment | 8 + ..._6d0bbdf6ebaff9da399804570f0e606d._comment | 10 + .../blog/day_282-283__caught_up.mdwn | 18 + .../assistant/blog/day_284__porting.mdwn | 13 + ...285__fixed_the_archive_directory_loop.mdwn | 23 + ..._1065e756dc6d66aefd214eb8ac5ebe1d._comment | 25 + .../blog/day_286__Windows_test_suite.mdwn | 19 + .../assistant/blog/day_287__niceness.mdwn | 13 + .../blog/day_288__success_stories.mdwn | 32 + ..._9ddf57b8ae0241268bb33bec1b169e4c._comment | 20 + ..._50b8a597bd8677608f2ef176443f23f3._comment | 10 + ..._f2df427cf3608377e9a52d8bdeadb26f._comment | 21 + ..._8762efed97f21eeba8f0a7be45bd924a._comment | 35 + ..._55e1bb15c3a93d582d110f8173ceefc2._comment | 9 + ..._5749aef8b585b293385b20b75c40f9d8._comment | 31 + ..._911c6d2764906cad7d6324835441ed34._comment | 12 + ..._eb6aa8af5aa70877255a11d132d51aba._comment | 10 + ..._9a57de4cea407a73b2d023d85afdccc6._comment | 12 + ..._1767c86067bee35941004282b96b8e95._comment | 10 + ..._22b28ca3d4d3283ad8c21ae052fb9752._comment | 11 + ..._1d47f3e1b9f0081649cedae4288bac83._comment | 8 + ..._31d3f58cad83cb1ecc4821a15ca258d8._comment | 14 + ..._b512bd2bf29dfaab6b36bf204518fdb6._comment | 8 + ..._343333356de20e170edb8020faa7400d._comment | 10 + ..._4e4034bec789543b562ac263df3e21dd._comment | 15 + ..._0c52794c77a9b7afc5112f5edf9cb793._comment | 8 + ..._7ca419aa3a187857b19268572d5df297._comment | 18 + ..._3edd56b3b04f19faba8d75cca285a662._comment | 10 + ..._146331ae2de25a6dc3595dffab9514de._comment | 12 + ..._72be9307e75eb120451f3d6ab7c8165e._comment | 8 + ..._c27eb0a4181e85a3eed41130402350bf._comment | 12 + .../blog/day_289__back_in_the_swing.mdwn | 16 + .../blog/day_290__https_release.mdwn | 17 + doc/design/assistant/blog/day_291__--all.mdwn | 32 + ..._eaa9fef19a035bef9c439e87d47c834b._comment | 17 + ..._90bbc26bf92048de7cbaf5fb719c9593._comment | 11 + ..._75006e9909425dcbf86415a9f7c90372._comment | 10 + ..._5440449bbc5a353f7430f72e19c35e92._comment | 8 + .../assistant/blog/day_292__bugfixes.mdwn | 24 + ..._bbac3878d80f7540d229183c56664784._comment | 8 + ..._8c9e5291ceb257f3a938af0ad967c5d7._comment | 10 + ..._02f875e8edd30f47939249f16d92712b._comment | 8 + .../assistant/blog/day_293__gpg_builds.mdwn | 32 + ..._4f152de8ea5aca4ec381d439e2a821f7._comment | 12 + ..._42f625638638bc875379f6c604d6f673._comment | 8 + .../assistant/blog/day_294__release_day.mdwn | 7 + .../blog/day_295__balls_in_the_air.mdwn | 13 + .../day_296__new_crowdfunding_campaign.mdwn | 41 + ..._cccad1a5103c504d21d0f8e69bb39e1b._comment | 8 + ..._4fef7bd9c8e15cd57df365fadb95717f._comment | 8 + ..._0b9258a1f5079e53c60138f06d0c63b1._comment | 8 + ..._46183b97ca904bc06e46569c30db2edc._comment | 8 + .../assistant/blog/day_297__back_to_work.mdwn | 16 + ..._e300feb821bfe7b76b2cec4376d16ffa._comment | 8 + .../assistant/blog/day_298__exceptional.mdwn | 21 + .../assistant/blog/day_299__bugfixing.mdwn | 8 + doc/design/assistant/blog/day_2__races.mdwn | 45 + .../assistant/blog/day_300__new_logo.mdwn | 36 + ..._9fc64e33863b9fce00f6a03417a91e36._comment | 9 + ..._e8aac0298f90004e81492d2c7f85eda0._comment | 8 + ..._6308c767f6e4bf090102191c91520d04._comment | 8 + .../blog/day_301__direct_unannex.mdwn | 21 + .../assistant/blog/day_302_release_day.mdwn | 6 + ..._fe6e572ba706e95188463d9f3e004d03._comment | 17 + doc/design/assistant/blog/day_303__oops.mdwn | 8 + .../blog/day_304__dropunused_safety.mdwn | 28 + ..._1bbcf6c74b6437c44ff8604401fb1432._comment | 10 + .../blog/day_305__interesting_bugs.mdwn | 21 + .../assistant/blog/day_306__offtopic.mdwn | 2 + .../assistant/blog/day_307__buuuugs.mdwn | 31 + .../assistant/blog/day_308__ssh-agent.mdwn | 16 + ..._5f0fc810cf1e1cd9b3ddba3cd19bb19d._comment | 12 + .../assistant/blog/day_309__filenames.mdwn | 17 + .../assistant/blog/day_310__release_day.mdwn | 18 + ..._1e008583cebd8e373e83729529914db7._comment | 8 + .../blog/day_311__Windows_porting.mdwn | 10 + .../blog/day_312__DebConf_midpoint.mdwn | 30 + .../blog/day_36__minimal_test_case.mdwn | 9 + doc/design/assistant/blog/day_37__back.mdwn | 64 + .../blog/day_39__twice_is_enemy_action.mdwn | 66 + .../assistant/blog/day_3__more_races.mdwn | 26 + ..._d6015338f602b574a3805de5481fc45e._comment | 8 + ..._4d6b23fc6442e0ee0303523cb69d0fba._comment | 8 + ..._03f5b2344c2a47dea60086f217d60f9b._comment | 14 + ..._860e90e989ec022100001c65e353a91e._comment | 8 + doc/design/assistant/blog/day_40__dbus.mdwn | 100 + ..._43ed2a79629868b018ec9f54a32bcacc._comment | 12 + ..._6799f2baf6a6ce14b1fa76a8402840c0._comment | 10 + ..._fa1d7444bdafcb990cacf2ace7ee6ef1._comment | 10 + ..._3399ddad951c1a950281bb6941fc3f6f._comment | 8 + ..._40b6b9d741d3081203f0cc94eb8dc3ea._comment | 12 + doc/design/assistant/blog/day_41__foo.mdwn | 46 + ..._ace21fa257a4c2fd412b6ff2944a23e8._comment | 10 + .../assistant/blog/day_42__the_answer.mdwn | 27 + .../blog/day_43__simple_scanner.mdwn | 37 + .../assistant/blog/day_44__webapp_basics.mdwn | 83 + ..._d5fb67f373038e9f583cb2e1992bef67._comment | 18 + .../assistant/blog/day_45__long_polling.mdwn | 66 + ..._994bec0978324e268666073e8ff4f6ae._comment | 8 + ..._dfa164c86290899139491acccddd8b2b._comment | 10 + .../blog/day_45__long_polling/full.png | Bin 0 -> 55185 bytes .../blog/day_45__long_polling/phone.png | Bin 0 -> 41602 bytes .../blog/day_46__notification_pools.mdwn | 68 + .../blog/day_47__alert_messages.mdwn | 14 + doc/design/assistant/blog/day_48__intro.mdwn | 8 + .../blog/day_49__first_run_experience.mdwn | 39 + ..._e146cf06c8dd6303dd6a991f152a73fe._comment | 8 + ..._5d6adcf6782c02283bef6189582ee467._comment | 12 + ..._7ac2e34c2a7bc9b57488ca0c91307d32._comment | 14 + ..._549b07bb02c07a5b1b95445b01758db2._comment | 14 + doc/design/assistant/blog/day_4__speed.mdwn | 47 + ..._bf3c9c33cc0dea5eaeb6f2af110b924b._comment | 8 + ..._33aba4c9abaa3e6a05a2c87ab7df9d0e._comment | 8 + .../blog/day_50__directory_name.mdwn | 20 + ..._782cec95a8558a05b2b38a2d2302214d._comment | 8 + ..._2b8ceb0a26f25e8ed2711bcbe7225a58._comment | 8 + .../assistant/blog/day_51__desktop.mdwn | 34 + .../assistant/blog/day_52__file_browser.mdwn | 21 + ..._cd000c2d56b60cc1f17b221322a32aa7._comment | 8 + ..._21d1da67cf9105a545583ba2302c10fb._comment | 7 + .../blog/day_54__adding_removable_drives.mdwn | 99 + ..._5de4f220a3534f55b1f2208d1d812d63._comment | 10 + ..._8dae1ed0a70acf9628b88692dc32ac5f._comment | 10 + doc/design/assistant/blog/day_55__alerts.mdwn | 10 + ..._6319045500a8a5e049304fdec5ff4cf4._comment | 8 + .../blog/day_56__transfer_control.mdwn | 8 + doc/design/assistant/blog/day_57__afk.mdwn | 40 + ..._70e1c9f925f040c1700d3e26bab373d5._comment | 9 + ..._c70d3faccfcebf47deb25e270498cb56._comment | 18 + ..._89020ebc6d31485339bdea41a872df3c._comment | 11 + ..._8b1f65f141ffd9813e7f5a3380f7f520._comment | 27 + .../blog/day_58__more_transfer_control.mdwn | 26 + doc/design/assistant/blog/day_59__dinner.mdwn | 10 + ..._0c1e2d69496473e7e4a2956a2814f5dd._comment | 9 + .../assistant/blog/day_5__committing.mdwn | 57 + .../assistant/blog/day_60__taking_stock.mdwn | 40 + ..._6722f81ee084f1ea9e8fe47f34576397._comment | 8 + .../day_61__network_connection_detection.mdwn | 36 + ..._09b58f41a8d48f218619711ee19511ac._comment | 8 + .../blog/day_62__smarter_syncing.mdwn | 21 + .../blog/day_63__transfer_retries.mdwn | 26 + ..._990d4eb6066c4e2b9ddb3cabef32e4b9._comment | 10 + .../blog/day_64__syncing_robustly.mdwn | 33 + .../blog/day_65__transfer_polish.mdwn | 33 + .../assistant/blog/day_66__the_merge.mdwn | 19 + ..._eeccf4e73cc321542a1fe4780805a81e._comment | 12 + ..._a34e89316d1662826848f31061c4e46b._comment | 8 + ..._09e244d23d05052fa2b11a7181888366._comment | 8 + ..._3961a03e167903959b96b054835613f6._comment | 8 + ..._12a57af9f580918818b4a9f68396d5c4._comment | 23 + ..._8ce638960012367c888e018a5f05db19._comment | 8 + ..._f461b856b940e6914bcd2b681cf9505f._comment | 13 + ..._6e73aca1fc1747d0e742e054b88b5d78._comment | 12 + ..._d85f1ce23ae16d5a8eb88d2c3999acb7._comment | 19 + ..._c06dab4d78122c85beeaf300ffc3e376._comment | 8 + .../assistant/blog/day_67__progress_bars.mdwn | 10 + .../assistant/blog/day_68__transfers.mdwn | 15 + ..._5282960c0b553fbc0f411345b9745324._comment | 14 + .../assistant/blog/day_69__build_fixes.mdwn | 7 + doc/design/assistant/blog/day_6__polish.mdwn | 50 + .../blog/day_70__adding_ssh_remotes.mdwn | 66 + ..._2fac85357ac8feccff82beabd3791439._comment | 13 + ..._e9e496005fd1bf5a10c9e286b83e51fa._comment | 8 + ..._913e6ae7c8f7db90b9767ec35fc84205._comment | 23 + ..._634ca3c236e2062289e7df5f0d77a3c5._comment | 8 + ..._e365bbcbb7f66ce2b35fcd5b969ab315._comment | 16 + ..._b15499722a655489f9ea60ff9d4c47c6._comment | 12 + ..._8ea48276f060e75d9f40617d2a1ccd08._comment | 12 + ..._9b8bf7e9fa715977fbeb98087deefd1a._comment | 10 + ..._42e09eacdc10c7cf579bfc6470b5117c._comment | 8 + ..._6c02f31063b3d399d1b4f823bd6543ce._comment | 16 + ..._dd0447cb3b39d3a8c1a7cc00f17d8bc2._comment | 10 + .../assistant/blog/day_71__ssh_probing.mdwn | 26 + ..._56a0c29f7454cfca5cc30b2849e6e942._comment | 8 + ..._f3bd3e366c92c833c7e217da125481b8._comment | 8 + ...mote_ssh_server_configurator_finished.mdwn | 34 + .../blog/day_73__rsync.net_configurator.mdwn | 17 + .../blog/day_74__bits_and_peices.mdwn | 7 + .../blog/day_75__zeromq_and_pairing.mdwn | 50 + .../assistant/blog/day_76__pairing.mdwn | 16 + ..._09665f269343422cd18051fad1a8c19e._comment | 24 + ..._8e1b2233579bc26bfd758bbf6b3bdc07._comment | 10 + ..._a8b6a8432da20c468c633da8e7cbc2f3._comment | 8 + ..._36a428a2e1803f4391b821d1892f0cd7._comment | 10 + ..._11f332fe2050d8c1416e71f9e85ba280._comment | 8 + ..._973aeb656b78eca97474ea1a3f5b57b7._comment | 12 + ..._03d2b3343f34377a4d6171e06b7609f6._comment | 8 + .../assistant/blog/day_77_alert_buttons.mdwn | 21 + .../blog/day_78__pairing_continued.mdwn | 8 + .../blog/day_79__pairing_finished.mdwn | 33 + .../assistant/blog/day_7__bugfixes.mdwn | 45 + .../blog/day_7__bugfixes/profile.png | Bin 0 -> 47098 bytes .../blog/day_7__bugfixes/profile2.png | Bin 0 -> 230937 bytes .../blog/day_80__default_backend.mdwn | 14 + ...enabling_pre-existing_special_remotes.mdwn | 34 + .../blog/day_82__git-annex_branch_work.mdwn | 26 + doc/design/assistant/blog/day_83__3-way.mdwn | 73 + .../blog/day_84__deferred_downloads.mdwn | 33 + .../blog/day_85__more_foundation_work.mdwn | 17 + .../blog/day_86__towards_the_beta.mdwn | 33 + .../blog/day_87__more_progress_progress.mdwn | 28 + ...ay_88__progressbars_still_progressing.mdwn | 18 + .../assistant/blog/day_89__final_polish.mdwn | 24 + doc/design/assistant/blog/day_8__speed.mdwn | 67 + ..._a3dba537b276d5737abc8cb93f1965f4._comment | 10 + doc/design/assistant/blog/day_90__beta.mdwn | 16 + ..._5f2a3b18ad7558abe04f51534a29ff13._comment | 9 + ..._961c4eba97f4eac75174244d6b2b00c0._comment | 8 + ..._c76675a4633cbbe347ed42c222918d38._comment | 24 + ..._f0b8f77cb691e747fe35bcf2f51b5baa._comment | 8 + ..._99fbc9feac62e66a12b0d357cf86ccc1._comment | 8 + doc/design/assistant/blog/day_91__break.mdwn | 7 + doc/design/assistant/blog/day_92__S3.mdwn | 23 + ..._eda656247d11cea7fbed2e33137a39e5._comment | 10 + ..._8249d2d9521e44c674da3fda74be077a._comment | 10 + .../blog/day_93__OSX_standalone_app.mdwn | 23 + .../assistant/blog/day_93__easy_install.mdwn | 34 + ..._d4f7de723c98577ef28d89ee6b87fd13._comment | 10 + ..._6337b341c1cfb2132b59704394e57b36._comment | 8 + .../blog/day_95__repository_groups.mdwn | 21 + .../blog/day_96__revisiting_file_adds.mdwn | 24 + ..._da3ca47041168b6c82aeb2c18acc5017._comment | 8 + .../assistant/blog/day_97__stuffing.mdwn | 14 + .../blog/day_98__preferred_content.mdwn | 44 + ..._2136618e3515d0ac6369a41f1934ec2a._comment | 17 + ..._5f6db00e69628bf2f72b0e6f2981a49b._comment | 14 + doc/design/assistant/blog/day_99_shotgun.mdwn | 70 + ..._12bb8f54bb13ea20ac4187a2301d77ca._comment | 10 + .../assistant/blog/day_9__correctness.mdwn | 30 + ..._564a39cb976767e2c0a9c74fabe10be4._comment | 8 + ..._77924e9d50b40f05e792e427a25849a6._comment | 9 + ..._92bd86cd06d579e23800af2e5c66a291._comment | 8 + ..._0d12b51ccdfc2a94d3e59a5628521e0a._comment | 10 + ..._208f9dd3e1d92555b05c29159538a901._comment | 14 + ..._90cc6b60718896fb175919417600fdf9._comment | 8 + doc/design/assistant/chunks.mdwn | 7 + doc/design/assistant/cloud.mdwn | 45 + ..._4997778abc171999499487b71b31c9ba._comment | 16 + ..._08da8bc74a4845e354dca99184cffd70._comment | 8 + ..._faafd1266301997b1822d215ec8e8d8c._comment | 8 + ..._3eb557d5439831f6e0032944d12c02cf._comment | 12 + ..._f2233fad55c20686cf299bf6788f1f23._comment | 10 + ..._a38f0f21c2346e65b786d791b6829f9b._comment | 12 + ..._5e991177d6577384f39a36ae02f5f574._comment | 13 + ..._f8625c6f43b58847840df338a73b7972._comment | 7 + ..._c37ef5931b0f5c1f808083e0d636a208._comment | 11 + ..._68c98a27083567f20c2e6bc2a760991b._comment | 9 + ..._8e6788c817c60371d2a2f158e1a65f87._comment | 8 + ..._97bdfacac5ac492281c9454ee4c0228e._comment | 8 + ..._53137b2df4913496c0afb2d895aa4ee2._comment | 8 + ..._ff1b0ba57e22ed757ec3fc5400b5e43e._comment | 8 + ..._a48fcfbf97f0a373ea375cd8f07f0fc8._comment | 8 + ..._099da245e3276fa84f5e14312d186621._comment | 8 + ..._6d3552414fdcc2ed3244567e6c67989d._comment | 7 + ..._05223be50c889b2ed6bc4abf74116450._comment | 9 + ..._fbbd93b55803ae21e6ba4b6568c2fafd._comment | 9 + ..._f4e9af3fed6c27e8ff39badb9794064d._comment | 12 + ..._c7ad07cade1f44f9a8b61f92225bb9c5._comment | 10 + ..._609d38e993267195a80fecd84c93d1e2._comment | 8 + ..._22b818e1a2a825efb78139271a14f944._comment | 10 + ..._d052e2142da8b4838fb1edf791ea23ae._comment | 10 + doc/design/assistant/configurators.mdwn | 20 + doc/design/assistant/deltas.mdwn | 9 + ..._bdb477af913c9782c0e8509e6b294b6e._comment | 8 + ..._71889d15ba20ebb0fe13080c68162a5b._comment | 11 + doc/design/assistant/desymlink.mdwn | 145 + ..._f1bfe250b7f872359f7075998b6e42e3._comment | 11 + ..._5e876edfe9853645f761b5ed9b5021aa._comment | 9 + ..._538561d74371e53c2f8df7f5ebdf58a8._comment | 8 + ..._586ecaa800e6c162377c937da5e65440._comment | 12 + ..._8fc703de67814cf2aec2a908852298a4._comment | 10 + ..._1b473ad89494afb82250af4b6df5f5c9._comment | 22 + doc/design/assistant/disaster_recovery.mdwn | 53 + .../assistant/encrypted_git_remotes.mdwn | 21 + doc/design/assistant/gpgkeys.mdwn | 24 + doc/design/assistant/inotify.mdwn | 196 + ..._3d3ff74447452d65c10ccc3dbfc323cd._comment | 7 + ..._a3c0fa6d97397c508b4b8aafdcee8f6f._comment | 7 + ..._b346e870c1cd80e4b0a313c3a9fed6b3._comment | 8 + ..._32be58b4c3b17a4ea539690d2fb45159._comment | 12 + ..._0cdd3046d90ad2012025d846ece0731e._comment | 8 + ..._e197d5d0d853572ec1f2e5985762e60d._comment | 9 + doc/design/assistant/leftovers.mdwn | 17 + ..._b20c88bb3c583a32023c1f6b6dc9486d._comment | 8 + .../assistant/more_cloud_providers.mdwn | 24 + doc/design/assistant/pairing.mdwn | 83 + doc/design/assistant/partial_content.mdwn | 36 + ..._58c4faa321a5bb71adf9fdee079849f4._comment | 18 + doc/design/assistant/polls.mdwn | 1 + doc/design/assistant/polls/Android.mdwn | 18 + ..._fa6c409833f28c67da105d25f4a440e0._comment | 8 + .../polls/Android_default_directory.mdwn | 7 + ..._d39655091ac3ed51a9d4325d86b23ad7._comment | 10 + ..._2f1eaae95075db26488517720afd1c63._comment | 8 + ..._b484012f60789be73d7d5b338cff6203._comment | 10 + .../assistant/polls/goals_for_April.mdwn | 17 + ..._9f81fa96db5970a4be0828c74a6d2d55._comment | 22 + ..._d8956d220ccacff3d2f6cbeb15718459._comment | 28 + ..._aadad6dfd56d068d2e377606910c006f._comment | 8 + .../polls/prioritizing_special_remotes.mdwn | 16 + ..._dd9280df27848a7ff132f5809dab0a79._comment | 8 + ..._370e0b9c43486ee96c825f9155eebde4._comment | 8 + ..._883a003b9c552b89f191135c582f99aa._comment | 14 + ..._746006c3fffc7f917c4526fd688051f7._comment | 8 + ...ing_me_from_using_git-annex_assistant.mdwn | 16 + ..._10a4839a05be39ced54ffbe880a588bb._comment | 25 + ..._ac91d866f11c66dd8c86e2cd1a368c85._comment | 10 + ..._e244c1bf334b1cc9ad0cc760bf8fe5de._comment | 29 + ..._1a0faf4bdc78741937e8a2f5cb5bbec6._comment | 12 + ..._8d8a11dbfae7a7bc574bdf37f87e0684._comment | 16 + ..._c437adeaccf0b3d134e0f81c64e25b9f._comment | 14 + ..._6e3fce3a32ab346dc3d0fd4b69967536._comment | 8 + ..._1b7233d88593d0d99b26ea3e7af20d9c._comment | 8 + ..._a23d5a0e2718b8e486f036fe8a413b36._comment | 10 + ..._f4c84a9d701d52cf2f2e45f3d764a90c._comment | 18 + ..._00a0de8190d946caaeeca3b44646146f._comment | 16 + ..._199c9807499470771af6cbca6d034cfa._comment | 8 + ..._35f6f121e54260cb960211a6e2e51e8e._comment | 14 + ..._acbe4f63b5d552ac5ae5a12c6f42dc18._comment | 25 + ..._0d988280865caae498a3b693b6342e37._comment | 16 + ..._ac8fe3768c30dd7999c183500f8567bb._comment | 19 + ..._36832de705a2bebf8dc6e65dcd661731._comment | 15 + ..._3618067e473577a112e36970ca71e0ab._comment | 12 + ..._07a13b6f000ddc0ac4472b863d8b50bd._comment | 14 + ..._e15eb407d988fda363296c8b566cc8fb._comment | 12 + doc/design/assistant/progressbars.mdwn | 43 + ..._3ea263b1f334e8e38e14f00a96202988._comment | 8 + doc/design/assistant/rate_limiting.mdwn | 57 + doc/design/assistant/screenshot/firstrun.png | Bin 0 -> 54347 bytes doc/design/assistant/screenshot/intro.png | Bin 0 -> 50730 bytes doc/design/assistant/sshpassword.mdwn | 12 + doc/design/assistant/syncing.mdwn | 260 + ..._c70156174ff19b503978d623bd2df36f._comment | 19 + ..._eb992b5b2c7a5ce23443e2a6007e5ff9._comment | 8 + ..._e1b5e8a24556de16d1cacd27ee0c1bd1._comment | 80 + ..._8b08b5c30e5aea3fc4599f856fd25df5._comment | 8 + doc/design/assistant/todo.mdwn | 4 + doc/design/assistant/transfer_control.mdwn | 123 + ..._d5adaef4712913dc0263d4ebafb79320._comment | 15 + doc/design/assistant/webapp.mdwn | 65 + ..._bab6f6fa720273c0f9700a3765150189._comment | 8 + ..._3cf0cf460c7869d0cc22940fcc84aec4._comment | 10 + ..._428e153135f7a64215730719207d82c4._comment | 8 + ..._f4068a7abbb77ba6a3297cbcf1e503e9._comment | 10 + doc/design/assistant/windows.mdwn | 33 + ..._f4b829318b182e1cec29f13babb6498e._comment | 10 + doc/design/assistant/xmpp.mdwn | 136 + ..._f20650f93d7f0ca39b9ba3ce0380193f._comment | 10 + ..._8c22839a8f5912b4a817415c4a359697._comment | 8 + ..._773102522f21844cffc841e6cde9229e._comment | 8 + doc/design/assistant/xmpp_security.mdwn | 26 + ..._c714e86553c02600249795efb224be8a._comment | 10 + doc/design/encryption.mdwn | 123 + ..._4715ffafb3c4a9915bc33f2b26aaa9c1._comment | 12 + ..._a610b3d056a059899178859a3a821ea5._comment | 10 + ..._cca186a9536cd3f6e86994631b14231c._comment | 12 + ..._8f3ba3e504b058791fc6e6f9c38154cf._comment | 10 + ..._520e60aa53217b5ba428d4c05d897dee._comment | 16 + ..._d677fead0fe0c543f48f07d85f83f592._comment | 14 + ..._c1c38a09b1276e29adc3ba564dc0fe4e._comment | 14 + doc/direct_mode.mdwn | 77 + ..._93fc31e8dc0ad16248a2593a1482d375._comment | 8 + ..._7f7086b34ed136851963f145868a1d23._comment | 12 + ..._8020d74bddf0e38b0a297e5dae7c217b._comment | 12 + ..._97c26bd82f623a3b2d56bab4afff0126._comment | 13 + ..._42363bf0367f935b3eee8ad3d2eaf5cf._comment | 10 + ..._5f03b1686c1fb3f7606a5bc724ac3812._comment | 8 + ..._5355ac418bfb26e990762b80f4c36b77._comment | 12 + ..._6cd15e2c5fd0bef48f60c6993322c2fc._comment | 9 + doc/distributed_version_control.mdwn | 21 + doc/download.mdwn | 40 + ..._ec2578241a966cfcdd43f2a26a5c8709._comment | 13 + ..._ee0d158ac59903737dbc4ef632f11fe3._comment | 7 + doc/encryption.mdwn | 55 + ..._1afca8d7182075d46db41f6ad3dd5911._comment | 10 + doc/favicon.ico | Bin 0 -> 405 bytes doc/favicon.png | Bin 0 -> 714 bytes doc/feeds.mdwn | 4 + doc/footer/column_a.mdwn | 7 + doc/footer/column_b.mdwn | 7 + doc/forum.mdwn | 8 + ...-print0_option_as_in___34__find__34__.mdwn | 5 + doc/forum/A_really_stupid_question.mdwn | 3 + ..._40e02556de0b00b94f245a0196b5a89f._comment | 31 + ...ssing_files_directly_on__a_USB_device.mdwn | 11 + .../Accessing_files_in_bare_repository.mdwn | 5 + ..._7eb66e3806f9524e043fae2da9d57d64._comment | 10 + ..._f0165d66865ad14f7eb5d50e900c1df4._comment | 10 + ..._0e7ea5161b6da6e9bb9425bdb953de33._comment | 8 + ..._f804b9bf71f7d04bd23ce32d813dc340._comment | 8 + ..._6de649d38febd2240eb5b703da77c2d6._comment | 15 + ..._7e8dd09915ddc3267377e900891cb02c._comment | 24 + ..._80eae4a73f38d1a7e35f97c33b6401f8._comment | 8 + ..._5ec13e98d3ecb69426e974d34f712f9b._comment | 8 + ..._dccbf5793998c6381e23eb8ff6497ebf._comment | 17 + ..._42d923916232c81f3b8bdbefa34a89d3._comment | 18 + ..._43a0a7d222faee582aeb3150a59cef87._comment | 8 + ..._ec1024235c1c74c113483a833df84654._comment | 12 + ..._c156b8c1ae0f2905566bbdb13b84e577._comment | 37 + doc/forum/Add_a___34__local__34___remote.txt | 13 + ..._c68ad724b465c4be5243be687168c0b3._comment | 12 + doc/forum/Assistant_not_syncing_to_Rsync.mdwn | 15 + ..._2178a7fc0d66643e84597b0938ef65f2._comment | 10 + ..._650651398443e128c2adc6a2a2d320d0._comment | 12 + ..._e6d0c9620b148acc72342862a8b4cfef._comment | 10 + ..._b91f9febdb8b69d8b487ba4ea08c119a._comment | 11 + ..._c5ad7c1546a17d8459c995c9c8c26414._comment | 31 + ..._4c12587f972eced91c5128d4885800b5._comment | 30 + ..._6ecaaee9316bcf0c65688676d60fc055._comment | 8 + ..._daa9a9a6188afa0394833e1b682f7cd4._comment | 10 + doc/forum/Auto_archiving.mdwn | 17 + ...es_due_to_external_modification__63__.mdwn | 46 + ..._dab1099ee56541c194de319c593f1268._comment | 9 + ..._b5faccf132fb47e3cda778a6600fd9ef._comment | 8 + ...ic_commit_messages_for_git_annex_sync.mdwn | 1 + ..._ea2ec57bc695da4df8a30a35d433959d._comment | 15 + ..._af71f53dbbca35d5a5c66ff131887ada._comment | 8 + ...lly_syncronise_centralised_repository.mdwn | 14 + ..._6a2047daa9faf4309d2ed27d5cc48b76._comment | 10 + ..._3be7b45bc2284019f17a81375637a576._comment | 10 + doc/forum/Behaviour_of_fsck.mdwn | 13 + ..._0e40f158b3f4ccdcaab1408d858b68b8._comment | 8 + ..._ead36a23c3e6efa1c41e4555f93e014e._comment | 19 + ..._97848f9a3db89c0427cfb671ba13300e._comment | 19 + ..._e4911dc6793f98fb81151daacbe49968._comment | 8 + ...manage_files_on_removable_media__63__.mdwn | 18 + ...uilding_a_Debian_package_of_git-annex.mdwn | 27 + ..._0848513c46f3efa21bc34784554ae88a._comment | 10 + .../Building_git-annex-3.20121112-19309.mdwn | 78 + ..._b115e28c77fe748ee6643c41f766beb4._comment | 12 + ..._8c6ae1fd74f14da12ccfa77dbd27fc65._comment | 16 + ..._2f30b301c14f3a7fa0f52715d6140353._comment | 13 + ..._1e3c3903a71a2ff7109372aa4dd5742a._comment | 8 + ...esolve_dependencies___40__yesod__41__.mdwn | 19 + ..._2eb4f410b54a25fcc895893a3c631c43._comment | 8 + ..._44cd6f6dd674df105d6f0b3f320f3236._comment | 19 + ..._992af6855901df79a2018a07941cb8b6._comment | 8 + .../Calculating_Annex_Cost_by_Ping_Times.mdwn | 1 + ..._9b4a6bc8d52ecbbdd537e8cf76757a80._comment | 15 + ..._7e04f85c6ba74c18c8dde148aef9bf80._comment | 8 + ...in_the_git-annex_git_repository__63__.mdwn | 6 + ..._c8f9923d8dc76b8bed25dce5ae09b520._comment | 8 + ...git-annex_merge_to_work_from_git_hook.mdwn | 41 + ..._8b71cb6772b219c27c17392d5099907a._comment | 12 + doc/forum/Can__39__t_get_pairing_to_work.mdwn | 5 + ..._b981977b4fb942fd109c37fcf40f35d7._comment | 22 + ..._341e2ff6c88ace1b1422e16781edf580._comment | 8 + ..._0c8cce48f179f2564ff0844bb7ef6bd1._comment | 8 + ..._169d77b30cea05125068ee1eeb2ef328._comment | 25 + ..._70e6c4f4f01277be1767b38ca8374793._comment | 11 + ..._2cd014a76fac6e08269dfd8146957418._comment | 10 + ..._b9b715084d5a5562998b1724699d49e5._comment | 8 + doc/forum/Can__39__t_init_git_annex.mdwn | 15 + ..._c4d2ab1ecf69718a2211c3ea7b27092b._comment | 10 + ..._fca9ed3707e097bee2cd642424681005._comment | 8 + ..._a294b5e7e52aa9f66a708866be16f137._comment | 10 + ..._fcf678d5188821d63b4c9ea5b59474a8._comment | 13 + ..._c83f7dea7d5304e226e52eb3c43ef14a._comment | 9 + ..._06a01dd51ffbfd006c0afb8eab40b530._comment | 8 + ..._53c33484bded57abc60f0449331c7b05._comment | 11 + ..._9e0ff44f6e62581bfc83f9f1da3e0100._comment | 14 + ..._7f96b5ef05e2faf4a3dbe8bfc39b810e._comment | 10 + ..._65ab8463716f4ddd7721a5bcfcd18fa0._comment | 8 + ..._31a45f6a72266190b3ed7a7b02e03d5b._comment | 8 + .../Can__39__t_install:_Mac_OS_10.8.2.mdwn | 36 + ..._c44023d81e9e4f7c9341af0e4271a1e4._comment | 10 + ..._dfbcd39eedff28dc9ed866a8f1411ef3._comment | 30 + ..._b37b2a9906ffb956cca91adb4bb4e521._comment | 8 + ..._afddf16f8faedc78d458835480f10dc3._comment | 15 + ...motes_that_aren__39__t_tracked__63___.mdwn | 13 + ..._35e5a963b9e58ed7773dfcb884f8ecbd._comment | 10 + .../Cannot_find_git-annex_in_server.mdwn | 10 + ..._bf7e98e6130698ad0dc92e3a6a63ade3._comment | 15 + ..._168dda4aed09f90a510bc453e8a7cda7._comment | 10 + ...unch_webapp_on_ubuntu_12.04_using_ppa.mdwn | 6 + ..._9345551f5772c3a6f1490b00e1edbf69._comment | 8 + ..._0b688a442b6a911a0353e73097a24cb6._comment | 12 + ..._7e246caa00005560bb489c927c663046._comment | 12 + ..._1d8025aabe8bc72711a77f691f67da5f._comment | 8 + ..._7c2f95da65190016192424e7c622122f._comment | 8 + ..._9b8465cefe609e7a696e7573b8892e38._comment | 8 + ..._af6472762a598a454ba52ac0caa059aa._comment | 10 + .../Centralized_repository_with_webapp.mdwn | 13 + ..._dcb9b07fd154f4d4fdef4809cc37ce77._comment | 18 + ..._08c84f2703f89dc12982eba9dd2a06d1._comment | 11 + .../Check_if_remote_is_using_GPG__63__.mdwn | 1 + ..._db8ce8ef50fc33a28860ee475988450f._comment | 14 + ..._11c7033904c9c7a1df766e915632c386._comment | 8 + ..._a7ab70ad87a334c36761ddb3d830d99b._comment | 8 + .../Check_when_your_last_fsck_was__63__.mdwn | 4 + ..._ee98a1fcd796fe4fd7af6f77d0c1837d._comment | 10 + ..._up_after_aborted_sync_in_direct_mode.mdwn | 19 + ..._3440b2e1662d3b113c18283afcbf4520._comment | 8 + ..._9a61ba8ac4a375f1d69cd09b5a6f8091._comment | 14 + ..._6b9d8c48547f3d0a911310622ba91df7._comment | 13 + doc/forum/Coming_from_git_world.mdwn | 9 + ..._098bef38c2688607e869425a557cc482._comment | 8 + ..._98d75a1415e0c3689ab4231855e61233._comment | 12 + ..._5e7079e9bf3e4d97191333c66ac00e52._comment | 10 + ..._357443dc601ae38784c01cf18552f4d5._comment | 14 + ..._ed1847dd3f47a9d013b8dd0455fb80ff._comment | 8 + ..._09c6bb83a73d34dff2b8bc185a14a1db._comment | 18 + ..._6c731bb9a8d21dd9ab8c09612b23f908._comment | 16 + ..._e719d99af5afd90da3d3db692eff28dc._comment | 11 + ..._85a42106944dba9995fb3f4bfee3443a._comment | 10 + ..._90623294b910ceca3dc8ebd41b50fc9b._comment | 38 + ..._28dbee30eb54877418f72eb8935302d8._comment | 8 + ..._6edb36ea9535030fa3766937398e5bc7._comment | 8 + ...tes___40__specifically_S3__41____63__.mdwn | 1 + ..._9c6c4ca0c9dc6976ba7cf27e84683bf0._comment | 8 + doc/forum/DBus_on_Ubuntu_12.04__63__.mdwn | 3 + ..._dc14a40b64b7eda94d1a3fd766cd39cc._comment | 28 + ..._608a30e274e6a691a39f69503720e320._comment | 10 + ..._791b9978b410c1aff7fd8ef05c38f5f9._comment | 40 + ..._8665c95299916138c4af375626d9ec7d._comment | 8 + .../DS__95__Store_files_are_not_added.mdwn | 3 + ..._30687306da9bd35ec02a806193c5e240._comment | 7 + doc/forum/Debugging_Git_Annex.mdwn | 4 + ..._ce63b2ee641a2338f1ad5ded9e6f09a8._comment | 7 + ..._1d70ff052d00f33c34fd45730ea13040._comment | 12 + doc/forum/Default_text__47__html_handler.mdwn | 2 + ..._4730061916c7e12b7a41906152f847ee._comment | 12 + .../Delete_unused_files__47__metadata.mdwn | 7 + ..._3efc19895c8dec89b71ae3778b583fea._comment | 11 + ..._23597d9468347b3d94257f3c02afe1b8._comment | 8 + doc/forum/Detached_git_work_tree__63__.mdwn | 11 + ..._656c737772bf92be2c7a2f33bd2bb0f0._comment | 10 + ..._28ac35a325fba250721d9f1b7c994960._comment | 8 + ..._7128c26bbc8efea04a5a317edf0ca9f2._comment | 13 + ..._a3c22f905748ff2c803e8621c74a87a0._comment | 8 + ..._8063921241760458349e7cb0cadf3d4e._comment | 8 + ..._4510a787255cb03e7d0c3e7b830b7d52._comment | 16 + ..._ffd9c67ecc5b46ae98996018573f5591._comment | 10 + ..._36ca007643c983604fc4aed6ec8cb3d2._comment | 8 + ..._b7a2da4fbace7156e11c48a496a19dc9._comment | 8 + ..._f9fa237a693d28178f0451799209f7e2._comment | 8 + ...between_copy__44___move_and_get__63__.mdwn | 24 + ..._26ee8192af3a62178c1ccf17c6da5ca5._comment | 10 + ...pointing_to_same_special_remote__63__.mdwn | 6 + ..._359f46805e6508d03aadd90429937546._comment | 10 + doc/forum/Direct_special_remotes.mdwn | 26 + ..._50357130a1c57ad2fab70f71925faf02._comment | 8 + ..._e94a722ca056a068bcc16eb822008602._comment | 18 + ..._187036bbfee0508e2914afb51ead3c71._comment | 16 + ..._6bfbf60f2061d49b7d34c844e7e1dea2._comment | 66 + ..._69c34c655e4b153dfc0d1b8580091124._comment | 8 + ..._b054cfc3d3f81873f3faae7eb4f5337c._comment | 8 + ..._work_when_the_buddy_is_offline__63__.mdwn | 1 + ..._f290dd8547176793934f8077374e1c0a._comment | 17 + ..._c358eb51047f333e582bd824be5e0e65._comment | 8 + ..._a2332c0e7b29110b9aed2ab69ce9d8c4._comment | 12 + ...oes_git-annex_version_big_files__63__.mdwn | 5 + ..._0b44003c1dc53adb807298ae452f8004._comment | 8 + ..._ca40b67abd7bd36155d16d0396d7472c._comment | 14 + ..._32de3501feedce51b43ed9dcc399c7a9._comment | 15 + ..._8c65a7f8bda3c876971c2801fb6a76a1._comment | 8 + ...s_migrate_ensure_data_integrity__63__.mdwn | 7 + ..._cef50b32c46f4406c6f918c5866ddc15._comment | 11 + ..._f389b924c8531b35fdf5dedd10fc8000._comment | 8 + ...tand_how_to_delete__47__recover_files.mdwn | 25 + ..._b307bfb0b70d649897f411eb753bd50a._comment | 14 + ..._58a6a1476274b8c4feb3d43ecd998759._comment | 41 + ..._4b857f481db7b2437ac9f8137a8510e2._comment | 11 + ..._828db3bf2863d98c0b0fb4074aa7f066._comment | 33 + ..._cb2063d6a4e08a5c12bf3723d0fa74e0._comment | 8 + ..._1759bcd5708f591f91b9c410f6dc5c54._comment | 14 + ..._2a389f01eb5131042ea1e71a73c9787a._comment | 21 + ...39__t_understand_local_vs._known_keys.mdwn | 19 + ..._10749c0d76e824217dd1ff8c8a6e42a5._comment | 10 + ..._db9f1b6d9638c2b0a7e241c2727e8cfb._comment | 13 + doc/forum/Drop_with_assistant.mdwn | 5 + ..._048f5a31c549afb19b76a65bddd0cd24._comment | 13 + ..._527d7b6a8efa85b904111f179912d926._comment | 8 + ..._c50857506869bb1cd306b66acf37fba8._comment | 14 + ..._1ea37445d5eb96c3efa182e88e07b867._comment | 8 + ..._c08908ea5232cbe067c73ecd12d0e218._comment | 10 + ..._015134228cb865f97326fbb7193636ea._comment | 8 + ..._950759930667588f21659cd6d7065fbb._comment | 17 + ..._773e540e46adc43487323e8d38ceb2d9._comment | 23 + ..._d85d120d7219ea6c179c2619a17bdae9._comment | 15 + ...ypted_ssh_remote__44___synced_folders.mdwn | 87 + ..._7b9b4ef614c90e0b222d24678d1b9026._comment | 10 + .../Error_adding_ssh_remote_in_assistant.mdwn | 15 + ..._eecc0660db4083cc91c5330587f74610._comment | 8 + ..._3e6aad22e8020b12ff7ef914b75281d1._comment | 8 + ..._3ea529e16502071fc0980c6d5c60a036._comment | 8 + ...ateSymbolicLink:_already_exists__34__.mdwn | 1 + ...it-annex_branch_but_not_master_branch.mdwn | 1 + ..._9a909e3d89061adacbd8ed370520250c._comment | 9 + ..._0dd489b264374b7b1065b89e1ff7561b._comment | 8 + ...equest:_Multiple_concurrent_transfers.mdwn | 19 + ...nnex_copy_--auto_does_the_right_thing.mdwn | 5 + ..._bbac7d0810a79eb1f42a01e1b31d5c4c._comment | 12 + ...pp_support_for_centralized_bare_repos.mdwn | 1 + ...t_at_an_OSX_launcher___40__.app__41__.mdwn | 3 + ..._97c261b9080c5ecc5424683066bbe05b._comment | 14 + ..._ae45f9703b635c235409682cf252d36c._comment | 8 + ..._066ca31a2e5dfe55a58092ba85231c7c._comment | 10 + ..._a0a9f7f44cadb8036fcddfc21bb0781f._comment | 10 + ..._92240b3f8629f1f2bbe1829700082a79._comment | 10 + doc/forum/Fixing_up_corrupt_annexes.mdwn | 10 + ..._cea21f96bcfb56aaab7ea03c1c804d2d._comment | 7 + ..._5cdd2fcfa61b3f6255e5ad63a3ab00ce._comment | 8 + doc/forum/Getting_started_with_Amazon_S3.mdwn | 28 + ..._f50883133d5d4903cc95c0dcaa52d052._comment | 10 + ..._e90aa3259d9a12cd67daa27d42d69ab5._comment | 8 + ..._c3adce7c0f29e71ed9dd07103ede2c1a._comment | 8 + doc/forum/Git_Annex_Transfer_Protocols.mdwn | 9 + ..._a870ec991078c95a6bb683d6962ab56e._comment | 8 + ..._71419376ef50a679ea8f0f9e16991c17._comment | 8 + ..._fea43664a500111ca99f4043e0dadb14._comment | 8 + ..._56fb2dab1d4030c9820be32b495afdf0._comment | 8 + ..._a6ec9c5a4a3c0bac1df87f1df9be140b._comment | 8 + ..._1678452fb7114aeabcf0cc3d5f6c69b0._comment | 8 + .../Git_annex_assistant_in_command_line.mdwn | 2 + ..._ce05226307ade8db90ada2dbf290bd58._comment | 10 + ...ex_syncing_speed__44___possible__63__.mdwn | 21 + ..._8aa224b3016dc38e4cea8ee1865a3ab6._comment | 12 + doc/forum/Git_repos_in_git_annex__63__.mdwn | 7 + ..._8aaa0d83e8fcd5997f6b0097f3b21622._comment | 14 + .../Git_repositories_in_the_annex__63__.mdwn | 5 + ...ial_remote_when_content_changes__63__.mdwn | 19 + ..._05ee6a1b1943ef3c90634e52233bde1c._comment | 12 + ..._48d82e391812d8ec0d4e6562d0607fe7._comment | 10 + .../Help_with_syncing_file_contents.mdwn | 68 + ..._7ec34de3140983739080115c82966bf5._comment | 18 + ..._7dba58d3c62d6f64a270298e4e4329a4._comment | 10 + ..._b26cfa20dc81517d93e760f4809bdc24._comment | 12 + ..._Android_git-annex_installation__63__.mdwn | 1 + ...dropunused_with_an_rsync_remote__63__.mdwn | 3 + ..._8db3cb5348b845eb99c2c829957db9ea._comment | 8 + ..._6cc909d9d74bc1ccb8a7b0d7d234c7cd._comment | 10 + ..._f24d678e4192a70322aa164ed9b71fc8._comment | 8 + ..._9233decd0aaf9211447f36e0d9346445._comment | 15 + ..._e1deb110f752e5495d5c77ec444abac5._comment | 8 + ...now_when_something_fails_a_fsck__63__.mdwn | 4 + ..._1c14981916dd55376d5e9f95023556cb._comment | 32 + ...ex_assistant__39__s_web_browser__63__.mdwn | 7 + ..._f4402eabda2327da3a0bbc64ed3baf9a._comment | 12 + ..._cdb41f2c7b6bc5bf40d88582dcbf45aa._comment | 10 + ..._ca75e928c245eb23a02b5f40ec69cbb1._comment | 8 + ..._1635f136909711295b9b70d1255e0378._comment | 11 + ..._ee0cbe9498c518de98480a2ad229f685._comment | 10 + ...th_renamed_files_in_direct_mode__63__.mdwn | 3 + ..._fe38fedbbc9e4a9e13bf19950e63c7ac._comment | 10 + ...url_for_a_git_remote_repository__63__.mdwn | 7 + ..._52918b5ec25e55837215439fe1bb1a14._comment | 8 + ..._3a1567c9f484b5e12e5560cdcc2cfddd._comment | 8 + ..._48c3a80c14a85f27d742482b2ccbe628._comment | 8 + doc/forum/How_to_delete_a_remote__63__.mdwn | 1 + ..._8cba186bb67079ff41bf6d0b04613f4a._comment | 10 + ...tes_with_git_annex_assistant_and_ext4.mdwn | 28 + ..._42ca6cfbbb79fe63514805b8119ac16b._comment | 8 + ..._c94ce6a9767c624e2445a7d9eea40396._comment | 29 + ..._bcda51053b62bbb20ce71a59469e1b26._comment | 10 + ..._48e5b9eae920e5f13812de8d6f6bc640._comment | 8 + ..._787c0bfdc1d309db1486c3a37723a957._comment | 13 + ..._8894beb06443f234e9200b03b5f3badf._comment | 8 + ..._457f62ee3e58f68a55f66c5bde6002fd._comment | 10 + ..._bd2b412116a66107bc0ff0efd7e39a58._comment | 10 + ..._of_files_that_have_been_edited__63__.mdwn | 7 + ..._dccf4dc4483d08e5e2936b2cadeafeaf._comment | 8 + ..._5710294c1c8652c12b6df2233255a45e._comment | 8 + ..._to_handle_the_git-annex_branch__63__.mdwn | 5 + ..._800bd55b322e72f229882d7fd3888b14._comment | 8 + ...n_releases_work_with_git_annex___63__.mdwn | 5 + ..._9298aa55771b68873de02e6a7964bbdc._comment | 8 + ...stant_from_downloading_all_data__63__.mdwn | 9 + ..._fd8b287758ad77b3527ae71017cffabf._comment | 8 + ..._e8e75b4451aaf55461edf2f3d68797ed._comment | 8 + doc/forum/How_to_rename_a_remote__63__.mdwn | 1 + ..._a9bfbd82f7bb47661f0d9e0e0d904332._comment | 28 + doc/forum/How_to_restore_symlinks.mdwn | 1 + ..._c67e752cf7d5431096fab4b3304790a7._comment | 11 + ..._f9ec6096595e2c149c48924e3b54542f._comment | 14 + ..._4ff80729787a2a4e2baf05dd1db37da3._comment | 12 + ...ly_annex_a_file_already_in_a_git_repo.mdwn | 5 + ..._one_shared_transfer_repository__63__.mdwn | 15 + ..._bedaf308cfc70b9e751914a400ebcbc2._comment | 10 + ..._d665b1514253c8aa487ebf8b2728e3b1._comment | 10 + ..._aef42387a3673ab6710fb23e878d7e17._comment | 10 + ..._bfbcc041db472f4808979e6b3d7c4be2._comment | 10 + .../Howto_remove_a_repository__63__.mdwn | 4 + ..._b55fa4e92bb457ecaa5ca8f5cee7be1d._comment | 8 + doc/forum/Howto_remove_unused_files.mdwn | 31 + ..._f2a7948268ce3cb3967a9fdd8ccc570a._comment | 16 + ..._9b4d198c2d8a52adef3d166a8196fc0d._comment | 8 + ..._441d10901d5c055ac3ed2a6cb61c075c._comment | 8 + ...de_of_the_object_directory_safe__63__.mdwn | 9 + ..._c25900b9d2d62cc0b8c77150bcfebadf._comment | 13 + ...state_before_the_initial_commit__63__.mdwn | 18 + ..._f9decde3955f10148febc4646fba5a68._comment | 12 + ..._ed32a48edce4f150bedf24cfe91de254._comment | 8 + ..._ef9618850e5e688bac3c646983f00ed8._comment | 10 + ...e_git-sync_not_nullify_symlinks__63__.mdwn | 23 + ..._d6f2d2cdc5f4ffde9eee9f3a8c215a06._comment | 10 + ...ebapp_on_Trisquel__47__Ubuntu_Precise.mdwn | 7 + ..._6bd27bd31833336c1df783253378ccae._comment | 10 + .../Let_watch_selectively_annex_files.mdwn | 27 + ..._8379de87d16502d9aadf252da01e4d9a._comment | 10 + ..._2219ff6b4dc927eb2a299cd1af90aed8._comment | 8 + .../Local_and_remote_in_direct_mode.mdwn | 7 + ..._45f89ebcb6092d1b2582feebc8a5e9d7._comment | 15 + doc/forum/Looking_at_the_webapp_on_OSX.mdwn | 18 + ..._68820f2f469356633c1abb18a47e0c59._comment | 12 + ..._4ce86546d8a135df9cfab46b4612fa0b._comment | 23 + ..._6d398a2cceff14a1b774b85ee1725073._comment | 12 + ..._5e503787a4b1d3534c5e20da5480b763._comment | 8 + ..._c735841bc230efc61594ea013fc2902b._comment | 8 + ..._0e489fbfc89d282e9eb47f1b814ff70c._comment | 8 + .../Make_whereis_output_more_compact.mdwn | 13 + ...git-annex_a_self-funded_project__63__.mdwn | 10 + ..._4a1ba95b7231ba973ddb672d2419e28c._comment | 8 + ..._7c476ae92e63c991f229708678874ca2._comment | 8 + .../Making_git-annex_less_necessary.mdwn | 5 + ..._03faaa3866778d24cd03887b85dc9954._comment | 12 + ..._2db02a94dffd525885c9d7fc6c5fa464._comment | 12 + ..._429ec656e0ac02f98843f8d7f3c02d6a._comment | 11 + ..._384813dd022dfd9c1ef14e0f1479a123._comment | 18 + ...ead-only_medium___40__E.G._DVDs__41__.mdwn | 118 + ..._a061d300b718ad943c940e122cc57220._comment | 23 + ..._76529080054407570611b4357ce4f3ed._comment | 8 + ..._9acf5ce41a023f3848a51891cceeb51b._comment | 21 + ..._25e65ee3949e7d918376298cf11585f2._comment | 10 + ..._8a71ca048f9de29a198a6afb17d5315e._comment | 11 + ..._e3d1d3a3d3d831432ec940a8ab6f31e9._comment | 14 + ..._26a33eae98b4faaf6baf6635e3d28a8f._comment | 27 + ..._49ac298d39c824b0e52a239961463e09._comment | 14 + ..._55a4a3616ea59654da1c2f9902561e3b._comment | 13 + ..._92a2af3e0e328bb48bcc67a69187ee57._comment | 13 + ..._f6e39e71882d55cdc061166aea3e2bd3._comment | 26 + ..._6c45a6264d69e22800c329a0f8a2d470._comment | 8 + ...multiple_annexes_with_assistant__63__.mdwn | 13 + ..._ba8c70e4a46441b48ad910625636eee5._comment | 8 + ..._4b4f0a7d84a51ae92536e2c190256069._comment | 10 + ..._86daadc565f96db5db13b6dbcbc66db3._comment | 8 + ..._e43d71ddfdfdb7bcb13bfb894de6a5ec._comment | 8 + ..._e94d33be83b45918d1a39d6e16fba4b4._comment | 8 + ...tiple_repositories_concurrently__63__.mdwn | 5 + ..._ebec1ddad71e961cdc9b21cbddfbcdaf._comment | 10 + doc/forum/Manual_Setup_of_a_Central_Repo.mdwn | 1 + ...ithin_the_repo_without_copying___63__.mdwn | 19 + ..._9e3290138133d5a23a80f72342f47ec4._comment | 8 + ..._232b77894dda51d02cbc34bd25d3213b._comment | 13 + ..._d35ac1bdb3fa6e303ad92348ba174158._comment | 11 + ..._4b443ec6b47eaabe214d0c2222083e4a._comment | 8 + ...s_file_content_without_doing_checkout.mdwn | 4 + ..._f114b75b29123453758b493fae7f5167._comment | 8 + ..._e377b7614c2961b460a10e285f3db274._comment | 10 + ..._d251958795ab0867c65cf182e54a6ffe._comment | 8 + ...r_some_weeks_with_git-annex_assistant.mdwn | 57 + ..._9d4019a54fb508e286a5d6d2660361d9._comment | 26 + ..._build_instructions_for_Debian_stable.mdwn | 5 + ..._8c1eea6dfec8b7e1c7a371b6e9c26118._comment | 8 + ..._f6ff8306c946219dbe39bb8938a349ab._comment | 21 + ..._bcda70cbfc7c1a14fa82da70f9f876e2._comment | 8 + .../Need_some_help_to_fix_my_repository.mdwn | 31 + ..._f0d279c530b796b2c93d793f85d147e8._comment | 13 + ..._a3fcfa1f8eadec5fa8a9efacca174048._comment | 10 + ..._7878f9b76ddfa3392c9ec6a1810cb745._comment | 10 + ...nnex_integration_mode_for_Emacs_users.mdwn | 3 + doc/forum/New_user_misunderstandings.mdwn | 24 + ..._c1785924109b5d5cde9aa3d3460cf955._comment | 10 + ...to_connect_to_the_Jabber_server__34__.mdwn | 5 + ..._59158afcedac18a7285d57491b2a468a._comment | 8 + ..._2a70ac08bb95774415b09dab7d7f8605._comment | 8 + doc/forum/No_SSL_traffic_for_S3__63__.mdwn | 8 + ..._f509bf273896180e6df8c771438dd093._comment | 11 + ..._358635d19c82202c63014ca84de7fc3b._comment | 8 + ...Not_sure_how_to_get_my_s3_remote_back.mdwn | 31 + ..._ed35a6ec605e8f79ec107856af6d1a46._comment | 18 + ..._e48b6efa42159dc83e1be11bfb54abcd._comment | 14 + ..._b58232d0e3fa4649565c0c7d4ce2e82e._comment | 31 + ..._85368b60091dc3ce2efb58013ffe9f83._comment | 10 + ..._e65281bef23e0076936c508728a87897._comment | 25 + ..._fffb59ad5a197d2980dd0ec35cf4aafa._comment | 10 + ..._0cfcc2075bff556b9fde5acc3dc1d599._comment | 8 + ..._6fe2ff1282fb14a4ce26ef8dc775d07e._comment | 8 + ..._64338d2d77dcbabd16b55eb145f40dc6._comment | 12 + ..._dd66c9ea0c83388f6826751944330d10._comment | 16 + ..._dc0c5e395e4c443b7227afdb157194e5._comment | 10 + ..._3c0ea4c76cdd889707f7308576e3efa0._comment | 65 + ..._36519ee4499a19f0864e4fcd264e9933._comment | 20 + ..._85b23f375e53469fb09b24b945b3aba9._comment | 17 + ..._sshd_behaviour_has_limited_paths_set.mdwn | 12 + ...kell-platform_statically_links_things.mdwn | 17 + doc/forum/OpenOffice___47___Libre_Office.mdwn | 5 + ..._98ed542fedd820d47bf8deb7d3232725._comment | 8 + ..._f313fdaa23863c2ae99cfbfe9ec2e1e0._comment | 8 + ...___whereis__44___find_and_status_cmds.mdwn | 5 + .../Overwriting_data_without_getting_it.mdwn | 3 + ..._f1c0199ee9bffcc84287370b89361294._comment | 26 + ..._6a1d08dbca206129ef6cf8aa97daeee1._comment | 8 + ..._52958e76e506fdbb6b533681ab619b3b._comment | 8 + ...Please_fix_compatibility_with_ghc_7.0.mdwn | 1 + ..._d1d10217ebd0151e947b3a6cd37399ce._comment | 8 + doc/forum/Podcast_syncing_use-case.mdwn | 34 + ..._ace6f9d3a950348a3ac0ff592b62e786._comment | 10 + ..._930a6620b4d516e69ed952f9da5371bb._comment | 8 + doc/forum/Poor_man__39__s_IMAP.mdwn | 6 + ..._258ff23c462dc88b88ced405c4f5040f._comment | 11 + ..._c88d1abdda4cb526a6ee45a710c75bc4._comment | 10 + ..._3847e371db1c2788c075e7dca1fbd33e._comment | 8 + ..._cf6cc21f2cf2aa5c949844e24a7b4075._comment | 8 + ..._d861fa69475ce526841b3195be8ee356._comment | 10 + doc/forum/Post-Kickstarter.mdwn | 5 + ...in_directory_tree_below_objects__47__.mdwn | 77 + ..._5dd978f9b5a0771f44ab9e086bf5a07f._comment | 14 + ..._9f51947b35ee04e473655e20d56c740a._comment | 16 + .../Problem_compiling_current_master.mdwn | 12 + ..._135df61ec850c06e3b48ccfef7b5b031._comment | 8 + ..._fb3e27b6014e84bd919a7a4a95e39ef9._comment | 20 + ..._b737b3945103c5e2aa798b4e65fbce06._comment | 8 + ..._28c1b335ae388d4e1f22b711ac1c001f._comment | 8 + .../Problem_with_bup:_cannot_lock_refs.mdwn | 5 + doc/forum/Problems_syncing_with_box.com.mdwn | 26 + ..._8db642849da4d42cd9a43142e2b7cb70._comment | 12 + ..._cd18f33647aebc04af5469e4ce1fbcd2._comment | 11 + ...using_submodules_with_git-annex__63__.mdwn | 1 + ..._c7a927736d419d3c31c912001ff16ee4._comment | 7 + .../Problems_with_large_numbers_of_files.mdwn | 8 + ..._08791cb78b982087c2a07316fe3ed46c._comment | 22 + ..._0392a11219463e40c53bae73c8188b69._comment | 25 + ..._537e9884c1488a7a4bcf131ea63b71f7._comment | 8 + ..._7cb65d013e72bd2b7e90452079d42ac9._comment | 29 + ..._86a42ee3173a5d38f803e64b79496ab3._comment | 14 + ..._4551274288383c9cc27cbf85b122d307._comment | 11 + ..._d18cf944352f8303799c86f2c0354e8e._comment | 8 + .../Push__47__Pull_with_the_Assistant.mdwn | 1 + ..._f7b63d379c2d21794adf8658f546f8a7._comment | 10 + ..._aec8cc20576e7ffd5a8be4348d1a0073._comment | 21 + ..._git_repo_to_AWS_S3_from_behind_proxy.mdwn | 9 + ...Reappearing_repos_in_webapp_and_vicfg.mdwn | 43 + ..._bd977e864ae89816fa7f4ff69879b15f._comment | 8 + ..._05749f9e75689d0111339b7126c12300._comment | 15 + ..._b1531994eea0fbbf4cb097e604378a53._comment | 12 + ..._f1eba3e8aa4116e3c20747ec1d6e24e5._comment | 12 + .../Recommended_number_of_repositories.mdwn | 4 + ..._3ef256230756be8a9679b107cdbfd018._comment | 15 + doc/forum/Relocating_annex_directory.mdwn | 1 + ...g_files_not_found_by_git_annex_unused.mdwn | 29 + ..._420c6230e68de0a0ac7d7da91ac60801._comment | 8 + ...__dumb__34___client_without_git-annex.mdwn | 11 + ..._077c492fd37d335f74a5c886ff0d524f._comment | 32 + ..._00e6576e3e60d2650461eeb0f918e6e5._comment | 8 + ..._c36a9562c53ac683b62fc4471405aa2a._comment | 15 + ...-annex-shell_to_a_specific_repository.mdwn | 25 + ..._66544520bff71181e4a03ca583b0b458._comment | 12 + ..._2a210255e8535712c71fa183e56ab600._comment | 13 + ..._52cd4bd9694b2100b0e0dd2eafa9e828._comment | 8 + ...n_a_server___40__no_X_available__41__.mdwn | 2 + ..._dd75d78ef63f2689199a302ed1846017._comment | 8 + ..._df654df60c5fa6a84d786d248928a352._comment | 11 + .../Running_assistant_steps_manually.mdwn | 20 + ..._e14e0a1d55d01cb4f67a94bbe349b872._comment | 20 + ..._Jabber_account_for_different_annexes.mdwn | 1 + ..._90c3954fe11980eef42b5f5d34f83488._comment | 8 + ..._802600b3568e5f94d0550092b22975db._comment | 8 + doc/forum/Securing_a_shared_ssh_server.mdwn | 3 + ..._ea971b57d94db5b8d487f728faa5e9a8._comment | 10 + ..._421a19f6e1fb40db6ee205daf8e3f867._comment | 15 + ..._acdbf92f646dbbf691621f08b3d94c26._comment | 12 + ..._67533d08e1b8706b844262e9c483d982._comment | 15 + ..._bf193e02b388b4358632a169d2425b5c._comment | 10 + ..._50d391992cd444080ebc70db30b215c5._comment | 9 + ...ial_remote_with_non-standard_ssh_port.mdwn | 13 + ..._1eb6990e93ec92cb6fd7dbee59f31072._comment | 13 + ..._c85d5167e7ccce1ecf1de396e72ce7bc._comment | 8 + .../Sharing_annex_with_local_clones.mdwn | 1 + ..._2b60e13e5f7b8cee56cf2ddc6c47f64d._comment | 12 + ..._24ff2c1eb643077daa37c01644cebcd2._comment | 8 + ..._5359b8eada24d27be83214ac0ae62f23._comment | 8 + ...Simple_check_out_with_assistant__63__.mdwn | 2 + ..._ade8a0743ef1ec933c8a40ed64eeac2d._comment | 8 + doc/forum/Special_remote_without_chmod.mdwn | 12 + ..._4f5f9506cae72a1f321296fc5a5f339a._comment | 8 + ...toring_uncontrolled_files_in_an_annex.mdwn | 3 + ..._175645a90be0c79221c129308adf643e._comment | 27 + ..._d29f214eadfe3bfd098bbc3bcf07129a._comment | 8 + ..._286b502e7906cca50e9e747db735bc88._comment | 10 + .../Stupid_mistake:_recoverable__63__.mdwn | 31 + ..._00ceb3a5e37825c4bbc806f532893706._comment | 23 + ..._cbedc29678d9b6af3b3c0bb1915d2391._comment | 12 + ..._86aa4d92a1330811862da1ba568b3037._comment | 42 + ..._6d15bf8a3c3c27cc92957070161675a9._comment | 12 + ..._f836b9b1d03d94c49e3798961790b2ba._comment | 21 + doc/forum/Sync_without_jabber_account.mdwn | 9 + ..._3e95ac2e67451f953cf0538094109f8b._comment | 10 + ...ize_large_files___40__VM_images__41__.mdwn | 10 + ..._619f6ed2d7da5832ab253d61b6dd8044._comment | 10 + ...yncing_machines_on_different_networks.mdwn | 9 + ..._1c3523c722c178a96b096a68b9be4165._comment | 8 + ..._d7b14ffee65072329cfe9ab08a0dba50._comment | 8 + ..._65d1dae9b76fccb5f2b8fd8c69b60075._comment | 15 + ..._2ec67428af69d6c0ea051c6a67d58905._comment | 10 + ..._5ce093f82a2aad3fd8d7ccd5fdcab94f._comment | 8 + ..._a55982c28d7b90e0b70ec2bb5e594e08._comment | 8 + ..._c519d546e1a2a4e834609f3de3a605b0._comment | 8 + ..._84a822238ddbaf211cce5f527c3559d3._comment | 8 + ...nisation_between_3_repositories__63__.mdwn | 11 + ..._ca5192a26950627a1c2efcb55d6d2fa3._comment | 10 + ..._while_committing_it_repeatedly__63__.mdwn | 3 + ..._3cbe520b184d323219cb402ff046c3b4._comment | 29 + ..._6afe7f593e955db2eefe87d9fa01882b._comment | 8 + ..._209399487fc4f76b29f03ad82dbc2d6f._comment | 8 + ..._f33fd6f72cb9ad7dd20a04c82199413b._comment | 26 + doc/forum/Transfer_remotes.mdwn | 3 + ..._c08cf3bda00d7f20a3ca3d0fdba19c9c._comment | 8 + ..._98930629d398329f1161135464a966a5._comment | 12 + ...stalling_from_cabal_on_debian-testing.mdwn | 15 + ..._0d3e9d7cffafc34bc212557e8bbb987d._comment | 12 + .../Truly_purging_dead_repositories.mdwn | 1 + ..._a4c75d49714b3543a9f1617a15d4a2d1._comment | 8 + ..._3da60a02e7323a204c5c5dd02ba04d6c._comment | 8 + ..._2576e45436008ff5a7ae5a38cade658e._comment | 8 + doc/forum/USB_backup_with_files_visible.mdwn | 7 + ..._2832f8ae24dfb0f101e06f7c18283028._comment | 10 + ..._6163e01aa441f8435091f026cc6da337._comment | 8 + ..._ee92ff320eb5d9a031bdd1896aee0d86._comment | 11 + ..._437c8342c0b65e3a89129800313eb73c._comment | 10 + ..._5e10cffe8465ea4ecaa71c03a4c29ea4._comment | 8 + ...ansfer_group_keeps_growing_-_assistant.txt | 22 + doc/forum/Ubuntu_PPA.mdwn | 3 + ..._b55535258b1b4bcfc802235f0cba075d._comment | 8 + ..._adc4d644fed058d1811acf0b35db9c18._comment | 8 + ..._fc9cd51558c47718f243437202a11803._comment | 8 + ..._3a8bbd0a7450a7f5323cd13144824aea._comment | 12 + ..._2e1beaeebda0201c635db8b276cedf20._comment | 12 + ..._bd99fb70399fc58d98781a89c6d38428._comment | 8 + ..._c3f7ec8573934c59d70a48e36e321c13._comment | 12 + ...ndo_Git_Annex_Changes_To_Linked_Files.mdwn | 7 + ..._568dde820c2608d86d05b07444146a26._comment | 13 + ..._a8cf71cdf1217d9c8596cd9006eb83f5._comment | 8 + doc/forum/Unknown_remote_type_S3.mdwn | 5 + ..._2aea2cd51286c809427d16519606cd37._comment | 8 + ..._06f775062cd30767979fe56bcb3cf7bf._comment | 8 + ...files_when_assistant_is_running__63__.mdwn | 13 + ..._3f4aadf0c856c81e15c6f5ae7f1992b4._comment | 10 + ..._a76797ee9e05e43af7947508cadd7bed._comment | 9 + ...tead_of_re-downloading_from_S3_remote.mdwn | 3 + ..._cfb6021a36eee087705967a69967f327._comment | 10 + ..._7268b194ba72331858bc3274996b780e._comment | 11 + ...s_on_BTRFS_instead_of_symlinks___63__.mdwn | 1 + ...__41___to_manage_photos_with_Shotwell.mdwn | 13 + ..._5e8d54daf6b7ff357619ac65fe39a2d7._comment | 12 + doc/forum/Using_Linux_static_builds.mdwn | 25 + ..._22fd266cbe68af3e754a10f1f1295e9b._comment | 13 + ..._36f69f30117ff8696425a754ab19a08b._comment | 8 + ..._64506833dad0202626239e00d1eb6490._comment | 23 + ...sync__34___to_sink_all_branches__63__.mdwn | 9 + ..._ef3d5c5e2600ffa36dd933c8a42cdf96._comment | 16 + ..._424b0c6fdfe87ca08f5d408b7684ab08._comment | 10 + ..._adaf9114c69f1268330adcebd8018fa0._comment | 10 + doc/forum/Using_for_Music_repo.mdwn | 13 + ..._3488ed85ad98f14cb17f229225ece26e._comment | 10 + ..._c794648878cfc77558f8db862271f997._comment | 25 + ..._8c5e820f5ff7d717d64b1fd66927941b._comment | 8 + doc/forum/Using_git-annex_as_a_library.mdwn | 1 + ..._1f8e74c5856f21c53d5a91892cbef0c6._comment | 8 + ..._11a243fa7d8ac947aa9a798228dbd191._comment | 12 + ...__assistant__47__webapp_documentation.mdwn | 12 + ..._adb377589dbae7fc91001df235c6b48e._comment | 14 + doc/forum/Webapp_on_ARM.mdwn | 6 + ..._82ac40cef5b59070136527b8d81a5ce2._comment | 10 + ...vior_with_OS_X_Finder_and_Preview.app.mdwn | 12 + ..._8c8d86790a9d31518f9bb96a2d2dafee._comment | 18 + ..._b538dc2c6f122b9ce5f7569de1b03f3e._comment | 8 + ..._16e6724fa184392d4decbe0c4eb6efe6._comment | 14 + ..._e514fe2d4d0ad6a10e281939e6ab4266._comment | 15 + ..._e0eec765f72f7bf6f5a2a92c9b5dacad._comment | 10 + .../What_can_be_done_in_case_of_conflict.mdwn | 7 + ..._5ca86b099dfa08a50f656ea03bf1dcd9._comment | 12 + ..._69ee17959a92bb8359c0fd7b2a9d8dfb._comment | 10 + ..._017f4bac57a040c496e0c9d068dcfd9e._comment | 41 + ...hat_happened_to_the_walkthrough__63__.mdwn | 1 + ..._70db0e3cfb1318e95671c23726e5541d._comment | 8 + ..._f9305dd19b9b5f35e66d915b8c30374b._comment | 7 + ...o___34__git_annex_mv__34___file__63__.mdwn | 1 + ..._02d305f307b4d2ff7acd98cb36508a2f._comment | 10 + ...r__34___and___34__remote_server__34__.mdwn | 3 + ..._68734a118b7dc0c88ba67eca20953a55._comment | 10 + ...an_encrypted_ssh_remote_is_left__63__.mdwn | 1 + ..._67ee446ca6d66e2c259ea771c2c9a2b2._comment | 12 + ..._6d3cce3c8048e4aea8f0ed76473f6af1._comment | 8 + ..._bd506e1ca7307660b3b9769eb97beddb._comment | 8 + ..._cloud_providers_are_supported__63___.mdwn | 3 + ..._1f9398840144e0452a2fed9336046547._comment | 10 + ...be_enabled_for_removable_drives__63__.mdwn | 7 + ..._4341898d5ae4f09a5b06d24f5fe6192d._comment | 8 + ...up_remote_use___126____47__.bup__63__.mdwn | 5 + ..._da9c7c0e93aefc2da7409de5b138d86f._comment | 8 + ...Will_git-annex_solve_my_problem__63__.mdwn | 7 + ..._35acbdd1a7727df204d776c2e8f02b53._comment | 8 + ..._230256c19ac139dea207d89c06f70782._comment | 8 + ...x_work_on_a_FAT32_formatted_key__63__.mdwn | 3 + ..._426482e6eb3a27687a48f24f6ef2332f._comment | 8 + ..._af4f8b52526d8bea2904c95406fd2796._comment | 8 + doc/forum/Windows_support.mdwn | 6 + ..._23fa9aa3b00940a1c1b3876c35eef019._comment | 9 + doc/forum/Windows_usage_instructions.mdwn | 25 + ..._d43dbd9406da3b9747b147715eca94ac._comment | 8 + .../Wishlist:_Bittorrent-like_transfers.mdwn | 5 + ..._13544d54fb0418af4ca9200cdb045d91._comment | 15 + ..._9a7dad35bf80c684ad97892420d7370c._comment | 16 + ..._e5de748bc5da12a4a01e08cde2407dd1._comment | 14 + ..._e51530178f1e034c0fdd5c9aa9945567._comment | 8 + ..._81ea9c129d8c02097f09ef8c68f1bb11._comment | 27 + ..._3b5798414f89686526da3dfa72c0c4f2._comment | 10 + ...hlist:_Don__39__t_make_files_readonly.mdwn | 3 + ..._7148527961e2d27793810966588c8d35._comment | 10 + ...s_without_copying_the_file_data__63__.mdwn | 6 + ..._1cf4ab29dfa2cff59b86305fc0018251._comment | 10 + ..._f5ebb7f43dcef861ecc13373fb1e263f._comment | 15 + ...cting_files_based_on_meta-information.mdwn | 15 + ..._818f38aa988177d3a9415055e084f0fb._comment | 15 + ..._97e2ed48bd552d02918c4f98f963e6e1._comment | 9 + doc/forum/Wishlist:_automatic_reinject.mdwn | 14 + ...g_the_disk_used_by_a_subtree_of_files.mdwn | 10 + ..._7abb1155081a23ce4829ee69b2064541._comment | 9 + ..._b4c6ebada7526263e04c70eac312fda9._comment | 18 + ..._ded71b270b94617a8ebb3a713d46a274._comment | 19 + ..._daemon___40__for_the_assistant__41__.mdwn | 1 + ..._42aa2b61b880f4048d874210212aa63b._comment | 8 + ..._3e201039fa0e611554171ee30e69a414._comment | 8 + ..._d1074724c44f3296cb438b2d526d8728._comment | 8 + doc/forum/Wishlist:_mark_remotes_offline.mdwn | 12 + ..._9e3901f0123abb66034cce95cc5a941a._comment | 14 + ..._d10e3d90cf421ae425e64ab266ea811b._comment | 12 + ...ptions_for_syncing_meta-data_and_data.mdwn | 13 + ...ecial_characters_if_filesystem_is_FAT.mdwn | 5 + ..._5d33bcbd862537f53edd91dcff2b8977._comment | 13 + .../XBMC__44___NFS___38___git-annex_.txt | 27 + ..._86480f31d410e903766f82e6ecf83e1c._comment | 12 + ..._d8ed4dd51d3050db691a8abdec24cd42._comment | 10 + ..._42b80ee51ce25775bf4532f53a8ecfe3._comment | 11 + ..._01767f3f864954cf8080274e206da9d4._comment | 8 + doc/forum/XMPP_authentication_failure.mdwn | 15 + ..._19c7c3aa79d209d613d2e061e3129690._comment | 8 + ..._870059fed451e8377e5d382464ecc34b._comment | 8 + ..._1a7ff955e9173f13d10b75f203792384._comment | 10 + ..._d59031ebc0dd3abc1f4c96878328362c._comment | 8 + ..._c37ef477bef7efdb79dd05dce90dfde6._comment | 10 + ..._48cabea4c2caf5b3bd854df3aaa17d3d._comment | 8 + ..._14cd9b67806db93c3af055d88c9a910a._comment | 10 + ..._151d3fd7d3cceb30fd20a8f3bd54036c._comment | 24 + ..._fbb9eba65fbb72201f08511945fbcf8c._comment | 9 + ...provide_some_kind_of_debug_data__63__.mdwn | 9 + ..._1ba0735141fc6a21ac15913f4cacefae._comment | 8 + ..._16994dc86b87592fc62799e2d206d172._comment | 10 + ..._6afd424edc4095b8f71b136de2a9e64d._comment | 10 + ..._1381b6a927410642c6a93aa8354be791._comment | 17 + ..._c5b33c7a8aa8e6d0f9349510dac2366d._comment | 12 + ..._9913d2983ba2744ed24911f74988e4c7._comment | 11 + ..._ad6f385a2b95803eb9d81dfe76359551._comment | 10 + ...du__34___equivalent_on_an_annex__63__.mdwn | 5 + ..._a41bd02361aa961e5285aeaf1ea062be._comment | 10 + ..._28ba62a546f5cc8f416491423d743d8a._comment | 10 + ..._8d97f40c1d14b7230f3656a00a99cf80._comment | 8 + ..._baa8fbbdd5c449a0dc2bb622cb4a47ce._comment | 18 + ..._2ee6cbbfe54a2e7b6e8eb539c18e663d._comment | 10 + ..._48f6a2761a34b7f991325f1d24e2c5ff._comment | 8 + ..._d632baff41b8582f1a79bc5018c68545._comment | 8 + ...__39__t_send_it_to_working_directory..mdwn | 11 + ..._0c0a5999a92bf5880f2113177dc67cc2._comment | 11 + ..._c18083d9054f66f0bd51d63452af07eb._comment | 8 + ...nex_lock__34___very_slow_for_big_repo.mdwn | 7 + ..._044f1c5e5f7a939315c28087495a8ba8._comment | 16 + ..._e854b93415d5ab80eda8e3be3b145ec2._comment | 13 + ..._95c110500bc54013bc1969c1a9c8f842._comment | 8 + ...I_would_like_to_have_file_x_y_z__34__.mdwn | 5 + ..._bfeb1446dee4d2f52ef25fabfb8cc8f6._comment | 11 + ..._e60f2bbc1c058993472fd920edbc75fc._comment | 8 + ...n_denied__34___in_fsck_on_shared_repo.mdwn | 17 + ..._3a5202ef2116ebb5559b6f4d920755fc._comment | 10 + ..._86663eeb75b0477f53c45f26c8e4b051._comment | 8 + ..._c336b2b07cd006d378e5be9639ff17ec._comment | 10 + ..._1339cd27ca2955f30b01ecf4da7d6fe8._comment | 10 + ..._refs__47__heads__47__git-annex__34__.mdwn | 34 + ..._e50188896df347f1d92e20a52053aa14._comment | 10 + ..._d67793f7c969f64943d1fd54a1208c2b._comment | 15 + ..._3523884833b5fd458a35f898797bf897._comment | 10 + ..._02c32c2521ba1a1eaa19eaca7281f2a6._comment | 10 + ....3.2_requires_syb___61____61__0.1.0.2.mdwn | 16 + ..._fae6e88115d175239fc55cef4c33fb2c._comment | 13 + ..._4c7a75638e8717132ccde949018d6008._comment | 10 + .../advantages_of_SHA__42___over_WORM.mdwn | 5 + ..._96c354cac4b5ce5cf6664943bc84db1d._comment | 8 + doc/forum/android_binary-only_download.mdwn | 9 + ..._aab206e0bf0bb5ff47c7cc9795f12f92._comment | 8 + ...y_for_web_remote_with_SHA256E_backend.mdwn | 12 + ..._d1605a6e3b4d6863f4089218994ce564._comment | 29 + ..._d249ff27fa3d9ac3ca32485cdef49930._comment | 8 + doc/forum/archaeology_of_deleted_files.mdwn | 36 + ..._48f27df03ec18d2c27cf6b70dcf71dc5._comment | 10 + ..._c698cd10c8038bac45bd1049506a27c3._comment | 8 + doc/forum/archival_and_multiple_users.mdwn | 8 + ..._fc4ee256f03a7c189d687caf4a34e21e._comment | 9 + ..._a96d57d4bb567ac9b0b9167d5b1be011._comment | 12 + ..._bd44634b04732ffb91154c61ef9cf828._comment | 14 + ..._b89a56a5f1cd641f87925c7a5f74bcec._comment | 13 + ..._81293bf5dc8ad4552712c2083fd589c9._comment | 19 + ...zealously_moving_stuff_to_other_repos.mdwn | 5 + ..._6bd240edf1868615024ff11c24c3d52c._comment | 13 + ..._37c5e9a7669b5b94fbadb8792a765316._comment | 8 + ..._87aa4c5942929be81ddc1e2795d56f0e._comment | 9 + doc/forum/assistant_without_watch__63__.mdwn | 13 + ..._be1f7c038426e53209a85ae1119269d5._comment | 15 + ...ders_for_git-annex_to_aid_development.mdwn | 34 + ..._7e88f815e8d9652ef18ea6d54b118962._comment | 8 + ..._fef17a10226af5671495c2929653c337._comment | 8 + ...nstorming:_git_annex_push___38___pull.mdwn | 28 + ..._3a0bf74b51586354b7a91f8b43472376._comment | 11 + ..._b02ca09914e788393c01196686f95831._comment | 14 + ...batch_check_on_remote_when_using_copy.mdwn | 34 + .../benefit_of_splitting_a_repository.mdwn | 10 + ..._93a86cb03b66e7ab5dd7146e7b86c9e8._comment | 15 + ..._4e2fed247298d620fee7be883a1e86a6._comment | 15 + .../can_git-annex_replace_ddm__63__.mdwn | 13 + ..._aa05008dfe800474ff76678a400099e1._comment | 12 + ..._008554306dd082d7f543baf283510e92._comment | 19 + ..._4c69097fe2ee81359655e59a03a9bb8d._comment | 12 + ...ncrypted__41___rsync_remote_on_MacOSX.mdwn | 18 + ..._21f0101447623f5a0cf9e72c3ff463bb._comment | 8 + ..._6234ca64bd03a0e15efbe8f5c204338a._comment | 8 + ..._5ac2b520a907e232984eb513ce088054._comment | 8 + ..._183dd1c29f66539193e7c0b73f329430._comment | 9 + ..._c920d04ffe332caed9d223fa0ac42746._comment | 7 + ..._7a3cf0853a8ec7b996e19b5e80145d21._comment | 8 + doc/forum/central_non-bare_and_git_push.txt | 9 + ..._76d0c73c8985e860eb86333c63be6340._comment | 8 + doc/forum/clear_box.com_repository.mdwn | 1 + ..._2e839d8f974269c80a9fca183712350f._comment | 8 + ..._8f9c7248a148a24ae2aba39c4a79a6d1._comment | 8 + ..._f64ad21e5abfbf4e1f925b3d651bdba3._comment | 10 + ..._f8c06ac9b23b51cf18d362c260fc47a9._comment | 13 + ..._61d401b29322802cb25896503f3e6514._comment | 8 + doc/forum/cloud_services_to_support.mdwn | 16 + doc/forum/cloudcmd.mdwn | 6 + ..._current_workdir_state_in_direct_mode.mdwn | 5 + ..._748481ff00374f570284bd4571584874._comment | 10 + .../confusion_with_remotes__44___map.mdwn | 113 + ..._a38ded23b7f288292a843abcb1a56f38._comment | 10 + ..._cd1c98b1276444e859a22c3dbd6f2a79._comment | 8 + ..._18531754089c991b6caefc57a5c17fe9._comment | 24 + ..._3b89b6d1518267fcbc050c9de038b9ca._comment | 11 + ..._27801584325d259fa490f67273f2ff71._comment | 16 + ..._496b0d9b86869bbac3a1356d53a3dda4._comment | 8 + ..._9a456f61f956a3d5e81e723d5a90794c._comment | 27 + doc/forum/dot_git_slash_annex_slash_tmp.mdwn | 5 + ..._14b74438bb1e3e02cff7926d774ba09a._comment | 8 + ..._1a35ef8cb89e0cd392f6e9fcee1fb92c._comment | 8 + ..._f4cc36c493d7c20fbaf949edd38e1252._comment | 15 + ..._69268f8aa29e807a56248f1fac86aa41._comment | 10 + ..._0ffb0c803c232a1587f956f16113aeb7._comment | 27 + ..._c303e28825241733d69fca74f2015fc6._comment | 13 + ..._3f0b376e37bd092b8d46c46bb1940e35._comment | 14 + ..._615641b3dd176d4b3a5bbfb521098e38._comment | 9 + ..._4600fa9234a787004ea0e0dbb36184b9._comment | 8 + ..._4f5cd0d0d4db0479c1ad86ffdc5ae434._comment | 8 + doc/forum/endless_password_prompt_loop.mdwn | 8 + ..._cceba12ed25cd671c7cee5a28631163e._comment | 10 + ..._f0cb86b45eb289f35197c43f83660a8f._comment | 8 + ...error_in_installation_of_base-4.5.0.0.mdwn | 14 + ..._0b2f79c014e0dd9badd52b8b6aa47e0c._comment | 19 + ..._3badd64e48fbb174cd7de1ac9589bedf._comment | 31 + ..._d8190061ac1c683a7b699cf42e9db694._comment | 8 + ..._49a4fcd2dc4f97d4055b5051feea5e3b._comment | 8 + ...e_of_massively_disconnected_operation.mdwn | 33 + doc/forum/exclude_files_from_annex.mdwn | 10 + ..._82e7de5e631bae3b347815586274a936._comment | 25 + ..._03d4599fdceb3dff184eed82824674bc._comment | 12 + ...xpire_files__44___move_to_other_hosts.mdwn | 19 + ..._ddcc2a00be1ae96a352d75a443458bcf._comment | 14 + ..._7a4c3858c5eae409d04de3f9da43b57e._comment | 17 + doc/forum/exporting_annexed_files.mdwn | 4 + ..._e08e4c79588e17fb2f1cdf53d9fab7ea._comment | 16 + ..._15dc3024417b5b2ff3544a08beacab34._comment | 8 + ..._86f0e0f767a84a0f583e121d36cb7d48._comment | 8 + ...oes_not_exist__40__v_3.20111231__41__.mdwn | 26 + ..._990197bf01351dc1ccbe1940d5084adb._comment | 7 + ..._3bb1d21b7f0d0bd6d59190ae9d246d46._comment | 12 + ..._692f268218690437138ae0540c879425._comment | 16 + doc/forum/first-time_setup_git-annex.mdwn | 7 + ..._a58d83ee3a7c2251d9a775847223f8ca._comment | 13 + .../flickrannex_--_not_sure_I_get_it.mdwn | 7 + ..._57ea9f26760f970a70f09934d31a79b5._comment | 8 + ..._ba93563b4ce1f6497a9f1d5e6eb0d1bb._comment | 8 + ..._74f143965f48c89a3583acf1b6a7635a._comment | 9 + ..._493bb86dedfa91ccc0c9be4045953ee4._comment | 8 + ..._2c410aa478b21c0e6eb0e4d54bc8c362._comment | 8 + doc/forum/fsck_gives_false_positives.mdwn | 6 + ..._b91070218b9d5fb687eeee1f244237ad._comment | 15 + ..._f51c53f3f6e6ee1ad463992657db5828._comment | 10 + ..._692d6d4cd2f75a497e7d314041a768d2._comment | 10 + ..._7ceb395bf8a2e6a041ccd8de63b1b6eb._comment | 10 + ..._86484a504c3bbcecd5876982b9c95688._comment | 13 + ..._1d4fbbd212fa92967abda346323031f4._comment | 8 + doc/forum/gadu_-_git-annex_disk_usage.mdwn | 7 + ..._f632a62c4dbbf01b29f146893d7725f9._comment | 15 + ..._73461da2d55d040cb43e0db286975821._comment | 21 + ..._6c4fb123091bde435c18ac3dfd5a9b77._comment | 8 + ..._067d0ffe8900751bd2d2743254ac4d77._comment | 11 + ..._ec8b57426e4d82c3392eb7dd683f2ddc._comment | 17 + ..._38296fef5a2dc5794c2dc09df676b8c1._comment | 18 + ..._1bcc94f9982c6cfd0888f3dba0f9221e._comment | 8 + ..._4365cd3031456fac1b563ee72984638e._comment | 18 + ..._2b03d7b857497cb811e992f85700cdcc._comment | 12 + ..._03a4dfaf3bd73d41c6f3c3fab0a6a922._comment | 8 + ..._fc6ddb4dc075ee42368863c1b026dbf7._comment | 13 + ..._f03254e518cbdda73e4b88e72476275d._comment | 8 + .../get_and_copy_with_bare_repositories.mdwn | 7 + ..._a6e4628c0770e3f5e81348a6f29dd845._comment | 10 + ..._652fa1bae5c2bb63dcffcbda97a567c4._comment | 8 + ..._annex_to_do_a_force_copy_to_a_remote.mdwn | 11 + ..._3deb2c31cad37a49896f00d600253ee3._comment | 14 + ..._627f54d158d3ca4b72e45b4da70ff5cd._comment | 12 + ..._3f49dab11aae5df0c4eb5e4b8d741379._comment | 8 + .../git-annex_across_two_filesystems.mdwn | 30 + ..._53167648b8b70b41d19ca662a5f3687e._comment | 26 + ..._39adeebc1af9c437f1fc2e00c07509bf._comment | 11 + ..._f4e3f28db005301adeef7ccd2c9998fb._comment | 18 + ..._53fa7ac6f80e3281768a7bfd3d438b34._comment | 10 + ..._2e1be54c01970ef3456e8af4aaf00cbf._comment | 10 + doc/forum/git-annex_and_tagfs.mdwn | 14 + ..._887c74cb61d30198322ef74ebc80f950._comment | 8 + ...Meego_Harmattan__41___and_Sailfish_OS.mdwn | 7 + ..._301a51c48c3d54f9d37feace26a772f8._comment | 8 + .../git-annex_communication_channels.mdwn | 10 + ..._198325d2e9337c90f026396de89eec0e._comment | 17 + ..._c7aeefa6ef9a2e75d8667b479ade1b7f._comment | 8 + ..._1ff08a3e0e63fa0e560cbc9602245caa._comment | 8 + ..._1ba6ddf54843c17c7d19a9996f2ab712._comment | 8 + ..._404b723a681eb93fee015cea8024b6bc._comment | 8 + ..._0d87d0e26461494b1d7f8a701a924729._comment | 8 + ..._2c87c7a0648fe87c2bf6b4391f1cc468._comment | 10 + doc/forum/git-annex_on_OSX.mdwn | 1 + doc/forum/git-annex_on_Samba_share.mdwn | 9 + ..._3e9cfdf2c088e48c967ad08f79966742._comment | 12 + ..._9d3df393b7b727653598453d94dd33db._comment | 12 + doc/forum/git-annex_teams___47___groups.mdwn | 5 + ..._0450673ab74f184a47ac7bab568d26dc._comment | 8 + doc/forum/git-assistant_clarification.mdwn | 11 + ..._8f553e59da12f798b854a457b96b5778._comment | 14 + ..._06cf62b599edea6ad8396776f0081494._comment | 10 + ..._36f0bd6e7a824e6ef40a309850bb087b._comment | 15 + doc/forum/git-remote-gcrypt.mdwn | 1 + ..._175c8c35d9bbb470fcc17697eb8cc6b8._comment | 12 + ..._fdcaf507e14c995636dd93a41e488df3._comment | 8 + ..._f4e830f961dbe1c60ddd277b9d888133._comment | 8 + doc/forum/git-subtree_support__63__.mdwn | 9 + ..._4f333cb71ed1ff259bbfd86704806aa6._comment | 10 + ..._73d2a015b1ac79ec99e071a8b1e29034._comment | 8 + ..._c533400e22c306c033fcd56e64761b0b._comment | 8 + ..._75b0e072e668aa46ff0a8d62a6620306._comment | 8 + ..._f5ec9649d9f1dc122e715de5533bc674._comment | 8 + ..._85df530f7b6d76b74ac8017c6034f95e._comment | 8 + ...nex_add_crash_and_subsequent_recovery.mdwn | 25 + ..._062d0153a379c1ba1df8585b90220d3d._comment | 18 + ..._6fc6be43c488c468a4811cd0a1360225._comment | 19 + ..._45efaaf27d9b580c4c75cbcdc4f65b64._comment | 10 + ..._c560eae40867512b0af2cbef161fc8ac._comment | 8 + doc/forum/git_annex_alternative.mdwn | 10 + ...istant__44___share_with_other_devices.mdwn | 3 + ...-to_blah_much_slower_than_--from_blah.mdwn | 15 + ..._5b6e0b749b01a97a6b52a2c1cca6e35a._comment | 12 + ..._8f2567f4c4f6db2078211a87689757d3._comment | 17 + ..._ab98121076b88f351fc8cd9197e6bf64._comment | 8 + ..._cb13328add1b7a812efd817ad3dd1a4f._comment | 8 + ...glish_filenames.__Rsync_problem__63__.mdwn | 22 + ..._292ee7c8b37cbd13f03eb67d0359b99e._comment | 10 + ..._f6341119fcfde5d8160c8f603b1a6fea._comment | 10 + ..._8ad3a1d1fe5995d61e5e137280bc76c3._comment | 8 + ..._86b61b0484f3f4ecff657e46333b3d4f._comment | 8 + ..._5ffac00d08d26acaba8c3513b24c4d65._comment | 10 + .../git_annex_get_creates_a_new_uuid.mdwn | 6 + ..._004c87183968c326058bd3159a5baa0b._comment | 14 + ...___47___metadata_in_git_annex_whereis.mdwn | 1 + ..._7fba10b85f4d9289c7782eccef46949e._comment | 8 + ..._7dcec124ea7d0291ed40d80e2ffd5c7e._comment | 8 + doc/forum/git_pull_remote_git-annex.mdwn | 11 + ..._9c245db3518d8b889ecdf5115ad9e053._comment | 36 + ..._0f7f4a311b0ec1d89613e80847e69b42._comment | 14 + ..._1aa89725b5196e40a16edeeb5ccfa371._comment | 14 + ..._646f2077edcabc000a7d9cb75a93cf55._comment | 37 + ..._4f2a05ef6551806dd0ec65372f183ca4._comment | 10 + ..._3925d1aa56bce9380f712e238d63080f._comment | 8 + ..._24c45ee981b18bc78325c768242e635d._comment | 8 + ..._7e76ee9b6520cbffaf484c9299a63ad3._comment | 12 + doc/forum/git_tag_missing_for_3.20111011.mdwn | 1 + ..._7a53bf273f3078ab3351369ef2b5f2a6._comment | 8 + doc/forum/git_unannex_speed.mdwn | 1 + ..._10cf326248f4e89e1f75bf97d7574763._comment | 7 + ...ls_and_daily_free_retrieval_allowance.mdwn | 6 + doc/forum/hashing_objects_directories.mdwn | 27 + ..._c55c56076be4f54251b0b7f79f28a607._comment | 12 + ..._504c96959c779176f991f4125ea22009._comment | 14 + ..._9134bde0a13aac0b6a4e5ebabd7f22e8._comment | 12 + ..._0de9170e429cbfea66f5afa8980d45ac._comment | 12 + ..._ef6cfd49d24c180c2d0a062e5bd3a0be._comment | 12 + ...ing_git-annex_on_top_of_existing_repo.mdwn | 12 + ..._4cb38d71c943657c5ba0896cd70d2e64._comment | 8 + ..._b5e94c10ebbed9125c7e2332f75709ca._comment | 13 + ..._2b3b93bbc60fbc24d436231954d6822a._comment | 10 + ..._2dfda33ffa39b92b16c8bd9005e1cefe._comment | 21 + ..._96b1eb1e8e9f315c646f4686870f9b52._comment | 10 + ..._e85c3fa1d17f1d6ec625b9c4f9b698c3._comment | 47 + ...e_from_encrypted_special_remote__63__.mdwn | 14 + ..._d4dc451892e7a6e230bf32adb7f3f9fa._comment | 8 + ..._79340bf3c0691073a9808c5ac2da0a3d._comment | 14 + ..._6302fb6e5bb7cbddf2cfe74d98d32897._comment | 12 + ..._e3d95bc09c9fb21e8e9bbacc642aa60f._comment | 8 + ..._f2f0a1c2fb0c6323707b11e2b06aa2db._comment | 8 + ..._66fe80e634a8f13cce18fe68974ec67a._comment | 8 + ...nks_but_keep_historical_file_contents.mdwn | 7 + ..._cba76311e146dabb8ffc789bf4c8b714._comment | 14 + ..._8d99c50fc1347367ccc0714e8d1af385._comment | 40 + ..._a7a9c55c2ad448179dff5d5b69976c7d._comment | 10 + doc/forum/incompatible_versions__63__.mdwn | 1 + ..._629f28258746d413e452cbd42a1a43f4._comment | 8 + doc/forum/linux_standalone_tarballs.mdwn | 1 + ..._5c3ceb845a45e50784f7098bfbf94df1._comment | 13 + doc/forum/location_tracking_cleanup.mdwn | 24 + ..._7d6319e8c94dfe998af9cfcbf170efb2._comment | 10 + ..._e7395cb6e01f42da72adf71ea3ebcde4._comment | 15 + ..._c15428cec90e969284a5e690fb4b2fde._comment | 10 + ...use_of_my_shiny_new_rsync.net_account.mdwn | 20 + ..._0ebe509b768d46081db2100f5b712ef7._comment | 10 + ..._ef63d893531d93d2eb09f48f8baff4dd._comment | 13 + ...n_pages_in_the_prebuilt_linux_tarball.mdwn | 1 + ..._a7bc2e84e6d7c0e2de5900685207af78._comment | 9 + doc/forum/managing_multiple_repositories.mdwn | 3 + ..._existing_git_repository_to_git-annex.mdwn | 66 + ..._4181bf34c71e2e8845e6e5fb55d53381._comment | 10 + ..._5f08da5e21c0b3b5a8d1e4408c0d6405._comment | 60 + ..._f483038c006cf7dcccf1014fa771744f._comment | 12 + .../migration_to_git-annex_and_rsync.mdwn | 33 + ...__files__42___into_an_annex.__bummer..mdwn | 3 + ..._752db25abb647804a1cc12c5b247378a._comment | 10 + ..._db6f4959c35732f72e7a90bd9f4c665c._comment | 8 + .../multiple_routes_to_same_repository.mdwn | 2 + ..._26c1734d41d5374f18fc688d862d6b8e._comment | 8 + ..._d119ab485fb2d5512c15372efdb2327d._comment | 8 + ...___40__for_tagging_photos__41____63__.mdwn | 11 + ..._96beb9ea895c80285748adb940b4f57d._comment | 9 + ..._985065c1feed9300631dac7a2701f669._comment | 8 + .../multiple_urls_for_the_same_UUID.mdwn | 26 + ..._de7410d8824a864c4d106c9f1afaec3f._comment | 8 + ..._309a86cf7e08448be64357a30d8b56ae._comment | 13 + ..._fa97a45fc1392935fd5e0714db999bc2._comment | 14 + ..._139178b1ba45b62eec0c89a660c0c81e._comment | 8 + doc/forum/new_microfeatures.mdwn | 59 + ..._058bd517c6fffaf3446b1f5d5be63623._comment | 8 + ..._41ad904c68e89c85e1fc49c9e9106969._comment | 8 + ..._a1a9347b5bc517f2a89a8b292c3f8517._comment | 15 + ..._5a6786dc52382fff5cc42fdb05770196._comment | 18 + ..._3c627d275586ff499d928a8f8136babf._comment | 8 + ..._31ea08c008500560c0b96c6601bc6362._comment | 8 + ..._94045b9078b1fff877933b012d1b49e2._comment | 10 + ...o_results_in_errors_on_drop__47__move.mdwn | 4 + .../nntp__47__usenet_special_remote.mdwn | 18 + ..._171a0b95b1f95cfd82073e88bdefaab9._comment | 10 + doc/forum/non-bare_repo_on_cloud_remote.mdwn | 6 + ..._da0c023af7c78f1ef1cfe1143a900a9f._comment | 10 + ..._71baea93f6caaf7b81a9ac00bee91e5e._comment | 67 + doc/forum/not_getting_file_contents.mdwn | 1 + ..._4a0f7f4de9c9bc4d13db033cb75d20af._comment | 10 + ..._dc7403e1b551552f9fd00da6a1453570._comment | 8 + .../one_annex_versus_many_annexes__63__.mdwn | 10 + doc/forum/one_or_many_annexes__63__.mdwn | 7 + ..._656d96011801d67a45b0b3bb3d70fa63._comment | 8 + ...nce_and_multiple_replication_problems.mdwn | 17 + ..._a2cdf1a4840f099f6bc941fd8de966c7._comment | 16 + ..._e65b360706c66ede6e0e841b2ebbbfbc._comment | 8 + ...it_on_ssd__44___annex_on_spindle_disk.mdwn | 12 + ..._b3f22f9be02bc4f2d5a121db3d753ff5._comment | 8 + ..._f94abce32ef818176b42a3cc860691ae._comment | 20 + ..._0c8e77fe248e00bd990d568623e5a5c9._comment | 10 + ..._4b7e8f9521d61900d9ad418e74808ffb._comment | 8 + doc/forum/post-copy__47__sync_hook.mdwn | 14 + ..._c8322d4b9bbf5eac80b48c312a42fbcf._comment | 11 + ...ontent_settings_for_multiple_symlinks.mdwn | 7 + ..._70da012d96ab576151fe3e081ef905d1._comment | 10 + ..._ccea74d8b5a4de1f3cd1f6da6694ae0e._comment | 8 + ..._fab70c642d5aaf26de05270860281030._comment | 10 + ..._3cbd06de53b6a13e2741124a8e7b5b5b._comment | 10 + ..._963558ab261d8a6315402d371e8348f9._comment | 10 + doc/forum/public-web-frontend.mdwn | 16 + ..._c73bd2dfe020c25eaad1c0707dd2db01._comment | 9 + ..._0026d7be6b17e50d86b3b54985882f80._comment | 14 + doc/forum/pulling_from_encrypted_remote.mdwn | 12 + ..._e9d6a9a6e01d01edb41a11b0da11d74d._comment | 10 + ..._8d0db2ff65ce935c6e68044a3e0721a8._comment | 16 + doc/forum/pure_git-annex_only_workflow.mdwn | 46 + ..._683768c9826b0bf0f267e8734b9eb872._comment | 8 + ..._6b541ed834ef45606f3b98779a25a148._comment | 30 + ..._ca8ca35d6cd4a9f94568536736c12adc._comment | 10 + ..._00c82d320c7b4bb51078beba17e14dc8._comment | 8 + ..._b63568b327215ef8f646a39d760fdfc0._comment | 32 + ..._cb7c856d8141b2de3cc95874753f1ee5._comment | 12 + ..._a32f7efd18d174845099a4ed59e6feae._comment | 32 + ..._66dc9b65523a9912411db03c039ba848._comment | 15 + ..._9b7d89da52f7ebb7801f9ec8545c3aba._comment | 12 + ..._dc8a3f75533906ad3756fcc47f7e96bb._comment | 20 + ..._afe5035a6b35ed2c7e193fb69cc182e2._comment | 24 + ..._3660d45c5656f68924acbd23790024ee._comment | 12 + ..._33db51096f568c65b22b4be0b5538c0d._comment | 15 + ..._6e5b42fdb7801daadc0b3046cbc3d51e._comment | 12 + ..._ace319652f9c7546883b5152ddc82591._comment | 14 + ...out_assistant_and___47__archive__47__.mdwn | 22 + ..._97890e26072af9277144651e3fdcada0._comment | 10 + ..._542bf265e35a976ac76767762d67d617._comment | 104 + ..._bafe99159df2adcd5fecc0d67bbf05a5._comment | 8 + ..._e77fa2992d9302a49a05f514c81612ca._comment | 10 + doc/forum/recover_deleted_files___63__.mdwn | 66 + ..._d7abb7c45c6ec2723a04f153ed215453._comment | 8 + ..._8ea2acaa30d3ee7e9f75310f4ec859b2._comment | 8 + ..._376de81c70799bf409be189a48234815._comment | 12 + .../recovering_from_repo_corruption.mdwn | 11 + ..._01fc85037e24fc70e5c5329898cf6781._comment | 15 + ..._3bd1c0bf25a0e892e711a60f53cd5298._comment | 8 + ..._679dde8ca0081fc6854d6d2e8a42abdb._comment | 8 + ...ity__47__completeness_of_XMPP_updates.mdwn | 7 + ..._e0f7aa48d54fc0564f41c3a569c723b7._comment | 12 + ..._4e74039a673c16c0163f2cfb406dc4c3._comment | 8 + ..._41ade4fe72804b2f06cd4dbf405c1746._comment | 8 + doc/forum/relying_on_git_for_numcopies.mdwn | 47 + ..._8ad3cccd7f66f6423341d71241ba89fc._comment | 36 + ..._be6acbc26008a9cb54e7b8f498f2c2a2._comment | 18 + ..._43d8e1513eb9947f8a503f094c03f307._comment | 8 + ...ent_repositories_are_bare__33____63__.mdwn | 17 + ..._234241460f6c75a8376b303b8dd4e882._comment | 11 + ..._42dfc382d07af2a4f29c76016084f87c._comment | 12 + ..._space_with_directory_special_remotes.mdwn | 2 + ..._cd17b624704d93b51931023f69573323._comment | 8 + ..._877ca1be23d1484a8a30cdaeb6630053._comment | 15 + ..._65910eeaf3c6fcfd03f22c2957293232._comment | 8 + doc/forum/retrieving_previous_versions.mdwn | 7 + ..._a4e83f688d4ec9177e7bf520f12ed26d._comment | 11 + doc/forum/rsync_over_ssh__63__.mdwn | 2 + ..._ee21f32e90303e20339e0a568321bbbe._comment | 8 + ..._aa690da6ecfb2b30fc5080ad76dc77b1._comment | 8 + ..._2e340c5a6473f165dc06cc35db38e5c0._comment | 8 + .../safely_dropping_git-annex_history.mdwn | 20 + ..._a4b93a3fbc98d9b86e942f95e0039862._comment | 8 + ..._383882fafd32f25ed22b5eb2fb3691b9._comment | 18 + ..._4fd76d10a93fe01588fce7a621f9254d._comment | 12 + ..._10ecf3220ffcbbe94ba09da225458f18._comment | 12 + ..._e3beb8acb075faaeef6c052aecbf0a41._comment | 50 + ..._61a5fe2e7e47c60a8b237ea69404a37f._comment | 8 + ..._426d02e2f2a2ae4ec7eae02dfe4519b3._comment | 9 + ..._410a7296c2cee16d3d5bb618a5a41c1d._comment | 10 + ..._42cf492fc98a9eba8176387749ef12e0._comment | 8 + ..._c0327ada073d8b69535f71b4dc6aa57e._comment | 21 + ..._f83d6090aea2b7d5d54c876df940cbad._comment | 40 + ...o_build_fine_on_haskell_platform_2011.mdwn | 1 + doc/forum/shared_cipher_tries_to_use_gpg.mdwn | 10 + ..._760961eaaa7d5c254dd71c5792437c9e._comment | 12 + ..._f3260aea3a5bb9b95a9bdf1d0dfce090._comment | 8 + ..._really_good_happened_with_3.20130124.mdwn | 5 + ..._1712bddd2f483a353f6313aa626445f1._comment | 8 + .../sparse_git_checkouts_with_annex.mdwn | 31 + ..._c7dc199c5740a0e7ba606dfb5e3e579a._comment | 12 + ..._e357db3ccc4079f07a291843975535eb._comment | 8 + ..._fcfafca994194d57dccf5319c7c9e646._comment | 8 + ..._04dc14880f31eee2b6d767d4d4258c5a._comment | 20 + doc/forum/special_remote_for_IMAP.mdwn | 44 + ..._7c7d4b57a1b6508fff1a6b0508c861f8._comment | 10 + ..._9c46fe8a857aa7a5ce797288144386bd._comment | 8 + ..._27e3b644df6942ce4c103236d0d5cb1b._comment | 23 + doc/forum/special_remote_for_iPods.mdwn | 5 + ..._37cc3dc740341cc663074fd3bfb85947._comment | 8 + doc/forum/ssh_password.mdwn | 3 + ..._a3e5a41e1d4da683d577976b134b11ee._comment | 10 + ..._fa261676a99d49d4b237b0d43048d76d._comment | 10 + doc/forum/switching_backends.mdwn | 12 + ..._ecf4109c1148dafde3519243ae3c5a03._comment | 10 + ..._21f465a18f40b95dafd307fce0de659a._comment | 8 + ..._4c13d22c1695195e6b101bd20ef6bb42._comment | 35 + ..._e1d4a48baac23fd3f67b20eba4eee8af._comment | 8 + ...irect_mode_while_assistant_is_running.mdwn | 2 + ..._7832243a36613c48d0077b438dbf8b4a._comment | 10 + doc/forum/syncing_home_directories.mdwn | 7 + ..._220a6e0ffe0ea610921a63c0a6e3beab._comment | 16 + .../syncing_non-git_trees_with_git-annex.mdwn | 46 + ..._7f9593bdfd95e4a8814e6cc5c44619e6._comment | 24 + ..._49f15478781a0ad5e46e75319070335c._comment | 16 + ..._6d8f399f0549eddd1d1f5c9c9a10c654._comment | 13 + doc/forum/taskwarrior.mdwn | 11 + ..._1c3a29e7d292cb602d9d349f8009b51e._comment | 10 + ...ll_us_how_you__39__re_using_git-annex.mdwn | 6 + ..._4884803ddee7f642a3ac995a19967a6a._comment | 17 + ..._61f5054918e7b36c191454365bc7f3b7._comment | 10 + ..._db07e8703be606c998c831e91d300d69._comment | 10 + ..._a58595969cdd42ed20210e9615b42e42._comment | 22 + ...95__remotes__47__hook_with_tahoe-lafs.mdwn | 22 + ..._76bb33ce45ce6a91b86454147463193b._comment | 10 + ..._4d9b9d47d01d606a475678f630797bf9._comment | 10 + ..._8a812b11fcc2dc3b6fcf01cdbbb8459d._comment | 12 + ..._fc98c819bc5eb4d7c9e74d87fb4f6f3b._comment | 39 + ..._c459fb479fe7b13eaea2377cfc1923a6._comment | 8 + ..._2e9da5a919bbbc27b32de3b243867d4f._comment | 23 + ..._d636c868524b2055ee85832527437f90._comment | 20 + ..._39dc449cc60a787c3bfbfaaac6f9be0c._comment | 10 + doc/forum/ui.mdwn | 11 + ..._f3e3446b05d6b573e29e6cad300fb635._comment | 10 + doc/forum/unannex_alternatives.mdwn | 9 + ..._dcd4cd41280b41512bbdffafaf307993._comment | 46 + ..._58a72a9fe0f58c7af0b4d7927a2dd21d._comment | 36 + ..._b1687fc8f9e7744327bbeb6f0635d1cd._comment | 16 + .../unknown_response_from_git_cat-file.mdwn | 8 + ..._f26ba569e715fe69b6de3093930362ee._comment | 8 + .../unlock__47__lock_always_gets_me.mdwn | 11 + ..._dee73a7ea3e1a5154601adb59782831f._comment | 12 + ...ting_the___34__number_of_copies__34__.mdwn | 14 + ..._327bdb0d9c190c60c7147b3acf07af09._comment | 10 + ..._7e11c839637e0894332e413cde02cee9._comment | 8 + ..._8b7a70fb3bb41e4eda412302834730bb._comment | 8 + doc/forum/use_existing_ssh_keys__63__.mdwn | 5 + ..._c420c53f022bbd1b28494bc44d076feb._comment | 8 + ..._e4cae848e5701852073ced307832872b._comment | 12 + ..._a97c20b6df74c49e5f57c7caf962f1e2._comment | 10 + ...2_directories___40__like_unison__41__.mdwn | 7 + ..._5c3ee8a8aaa6d0918c0cc9683ce177ae._comment | 10 + ..._648946353c6d90c57351cce4010f1301._comment | 10 + doc/forum/version_3_upgrade.mdwn | 9 + ..._05fc9c9cad26c520bebb98c852c71e35._comment | 13 + doc/forum/vlc_and_git-annex.mdwn | 11 + ..._9c9ab8ce463cf74418aa2f385955f165._comment | 10 + ..._037f94c1deeac873dbdb36cd4c927e45._comment | 8 + doc/forum/webapp_and_manual_mode.mdwn | 7 + ..._5b5df5ffeb6ee15779972f13fdc11729._comment | 10 + ..._a1f06b50d1317c78a301b47eb05d2617._comment | 19 + .../webapp_listen_port_with_autostart.mdwn | 3 + ..._65dbcf3d8f6c16568f5a326242eab9c5._comment | 20 + doc/forum/windows_port__63__.mdwn | 2 + ..._23fa9aa3b00940a1c1b3876c35eef019._comment | 9 + ...et__47__drop_via_webapp_file_explorer.mdwn | 1 + ..._c818a6d44dc13a56460b1865f70eb97c._comment | 8 + ...ake_copy_stop_on_exhausted_disk_space.mdwn | 4 + ..._467e5e3db3e836030bc4b4f15846951f._comment | 12 + ..._e3ca3db9bea11d3e085ee9c3c56b33fe._comment | 8 + ..._0ef8c37350fc192d9b784fbab1d9f318._comment | 8 + .../working_without_git-annex_commits.mdwn | 20 + doc/future_proofing.mdwn | 38 + doc/git-annex-shell.mdwn | 116 + doc/git-annex.mdwn | 1182 +++ doc/git-union-merge.mdwn | 38 + doc/how_it_works.mdwn | 41 + ..._b3bdd6a06d5764db521ae54878131f5f._comment | 14 + doc/index.mdwn | 45 + doc/install.mdwn | 27 + doc/install/Android.mdwn | 31 + ..._f9ced494a530e6ae3e76cfbaddb89f5d._comment | 8 + ..._74cccae04ea23a8600069c7e658143aa._comment | 8 + ..._82c7cb31d19d4e18ca5548da5ca19a79._comment | 8 + ..._cebaa8ee5bbed27d9b2d032ca7bdec6e._comment | 8 + ..._40cb6cb72c4ad4aa19a4a40f41a6a757._comment | 13 + ..._b0f723538e7328d5070c563f070858bd._comment | 8 + ..._c6dc23d0e6f4138c4bf8e3452755676f._comment | 8 + doc/install/ArchLinux.mdwn | 19 + ..._da5919c986d2ae187bc2f73de9633978._comment | 8 + doc/install/Debian.mdwn | 16 + ..._d5da996e106d2e4d8a822aa9bcc78596._comment | 12 + ..._84283676da247c401bc9b4bb12c2b453._comment | 8 + ..._0aca83b055d0a9dd8589c50250a8bbea._comment | 13 + ..._167a091764e5e99ec0f35a65e95a22de._comment | 8 + ..._029486088d098c2d4f1099f2f0e701a9._comment | 9 + ..._648e3467e260cdf233acdb0b53313ce0._comment | 8 + ..._4d922e11249627634ecc35bba4044d9e._comment | 8 + ..._2a93ab18b05ccb90e7acc5885866fca2._comment | 9 + ..._38e6399083e10a6a274f35bddc15d4ac._comment | 18 + ..._2e7bbdbaabbfb9d89de22e913066e822._comment | 8 + ..._1bccc7bf7a4ef61a9b30024b9b22ba7d._comment | 12 + ..._5b5a3b0e8abe8831a6a15a4e258d14fd._comment | 10 + ..._97eaed998ffd1ed79585075ed5cff06e._comment | 8 + doc/install/Fedora.mdwn | 39 + ..._c4db84e672ad4b45b522db735706b00f._comment | 16 + ..._f98c488c09bef86e2b0414589ce9e141._comment | 25 + ..._d872acf8865fe7c99a9b712db5b38ea4._comment | 8 + doc/install/FreeBSD.mdwn | 2 + doc/install/Gentoo.mdwn | 3 + doc/install/Linux_standalone.mdwn | 33 + doc/install/NixOS.mdwn | 6 + doc/install/OSX.mdwn | 73 + ..._cd2120552ef894a37933b328136fa4cc._comment | 8 + ..._740fa80e2e54e6fb570f820ff1f56440._comment | 8 + ..._a84028080578a8b60115b6c4ef823627._comment | 8 + ..._d6f1db401858ffea23c123db49f5b296._comment | 8 + ..._035f856923276b0edad879e196e94097._comment | 9 + ..._336e0acb00e84943715e69917643a69e._comment | 35 + ..._1befafa862b7d07b1f6e57c0182497cf._comment | 36 + ..._19c08b2c6c2c5cd88bf96d2bcbbd9055._comment | 10 + ..._537fad5d8854e765499d47602d1ab398._comment | 8 + ..._18d4377f4ded5604d395d73783ba82c9._comment | 8 + ..._3e6a3c00444badf2cf7a9ee3d54af11e._comment | 8 + ..._987f1302f56107c926b6daf83e124654._comment | 11 + ..._6b5f44a98f9d37a1c6ecfe19a60fe6c5._comment | 12 + ..._25552ff2942048fafe97d653757f1ad6._comment | 8 + ..._47a77a03040fe628109bd54f82f9ad7a._comment | 17 + ..._25cac8bcd84a5210fc0a5243260b8cc7._comment | 18 + ..._bbe99673033e4c48c8bb3db24ee419f9._comment | 8 + ..._39b4b748b4586bf32b37edfefef84bba._comment | 8 + ..._1a9c91ef43edc4148947f202ff604114._comment | 8 + ..._892f7e65f95f43697164267c4b71c0d5._comment | 8 + ..._38d9c2eea1090674de2361274eab5b0e._comment | 29 + ..._35bf3812db6f3ef25da9b3bc84f147c5._comment | 8 + doc/install/OSX/old_comments.mdwn | 1 + ..._4d15bfc4fc26e7249953bebfbb09e0aa._comment | 11 + ..._798000aab19af2944b6e44dbc550c6fe._comment | 10 + ..._707a1a27a15b2de8dfc8d1a30420ab4c._comment | 10 + ..._60d13f2c8e008af1041bea565a392c83._comment | 8 + ..._a6f48c87c2d6eabe379d6e10a6cac453._comment | 8 + ..._6ef2ddb7b11ce6ad54578ae118ed346e._comment | 9 + ..._6fd1fad5b6d9f36620e5a0e99edd2f89._comment | 9 + ..._af6fe3540032cdf4400478de87771058._comment | 30 + ..._8d3a0596db67108041728b20f2790f31._comment | 7 + ..._0a1760bf0db1f1ba89bdb4c62032f631._comment | 13 + ..._0327c64b15249596add635d26f4ce67f._comment | 19 + ..._7683740a98182de06cb329792e0c0a25._comment | 25 + ..._47c682a779812dda77601c24a619923c._comment | 8 + ..._733147cebe501c60f2141b711f1d7f24._comment | 16 + ..._b090f40fe5a32e00b472a5ab2b850b4a._comment | 8 + ..._fc092412e99cf4c5f095b0ef710bc4de._comment | 8 + ..._d513e21512a9b207983d38abf348d00f._comment | 16 + ..._d68c36432c7be3f4a76f4f0d7300bac9._comment | 20 + ..._e6109a964064a2a799768a370e57801d._comment | 30 + ..._50777853f808d57b957f8ce9a0f84b3d._comment | 10 + ..._626a4b4bf302d4ae750174f860402f70._comment | 8 + ..._18a8df794aa0ddd294dbf17d3d4c7fe2._comment | 7 + ..._2ce7acab15403d3f993cec94ec7f3bc6._comment | 14 + ..._a93ad4b67c5df4243268bcf32562f6be._comment | 39 + ..._ae3ed5345bc84f57e44251d2e6c39342._comment | 14 + ..._c6b1b31d16f2144ad08abd8c767b6ab9._comment | 23 + doc/install/ScientificLinux5.mdwn | 62 + doc/install/Ubuntu.mdwn | 29 + ..._d1c511153fe94bf33e19a1281f1c92f2._comment | 8 + ..._ad13886c1c1f76d1cd995ea7b7d8471c._comment | 8 + ..._a08817322739b03cf0fec97283b16f1a._comment | 8 + ..._fe0997e56136bd30749f0995cbf19b56._comment | 12 + ..._fbb5306a162db1a1ee9efa3523aac952._comment | 11 + ..._a97e7f0e62ac685c3ded423bddeaa67f._comment | 14 + ..._921a223fd7e679b9ced3d8ba5ce688e0._comment | 8 + ..._1f943cb084fa8e21bc6ee5fc3118f02f._comment | 8 + doc/install/Windows.mdwn | 33 + doc/install/cabal.mdwn | 42 + ..._7ebe353b05d4df29897dc9a4f45c8a91._comment | 8 + ..._0d06702e6e0ae3cd331cf748a9f6f273._comment | 44 + ..._b93ca271dffca3f948645d3e1326c1d9._comment | 12 + ..._3dac019cda71bf99878c0a1d9382323b._comment | 8 + ..._f04df6bcd50d1d01eb34868bb00ac35c._comment | 18 + ..._a69d17c55e56a707ec6606d5cdddee25._comment | 17 + ..._55bed050bdb768543dbe1b86edec057d._comment | 10 + ..._2ff7f8a3b03bea7e860248829d595bd1._comment | 14 + ..._8789fc27466714faa5a3a7a6b8ec6e5d._comment | 24 + ..._5afb2d081e8b603bc338cd460ad9317d._comment | 21 + ..._129c4f2e404c874e5adfa52902a81104._comment | 22 + ..._738c108f131e3aab0d720bc4fd6a81fd._comment | 8 + ..._5ddbba419d96a7411f7edddaa4d7b739._comment | 12 + doc/install/fromscratch.mdwn | 68 + doc/install/openSUSE.mdwn | 3 + doc/internals.mdwn | 132 + doc/internals/hashing.mdwn | 30 + doc/internals/key_format.mdwn | 20 + doc/license.mdwn | 14 + doc/license/AGPL | 661 ++ doc/license/GPL | 674 ++ doc/license/LGPL | 502 + doc/links/key_concepts.mdwn | 7 + doc/links/other_stuff.mdwn | 7 + doc/links/the_details.mdwn | 8 + doc/location_tracking.mdwn | 30 + doc/logo-old-bw.svg | 60 + doc/logo-old.png | Bin 0 -> 9092 bytes doc/logo-old.svg | 77 + doc/logo-old_small.png | Bin 0 -> 4713 bytes doc/logo.mdwn | 13 + doc/logo.svg | 92 + doc/logo_small.png | Bin 0 -> 4749 bytes doc/meta.mdwn | 5 + doc/news.mdwn | 11 + doc/news/LWN_article.mdwn | 2 + doc/news/Presentation_at_FOSDEM.mdwn | 4 + ...rebox_a_FUSE_filesystem_for_git-annex.mdwn | 19 + ..._e238d1734238e37bb55ff952b32e06b8._comment | 9 + doc/news/version_4.20130621.mdwn | 40 + doc/news/version_4.20130627.mdwn | 17 + doc/news/version_4.20130709.mdwn | 35 + doc/news/version_4.20130723.mdwn | 36 + doc/news/version_4.20130802.mdwn | 44 + doc/not.mdwn | 55 + ..._ab41bec1ccc884e71780cb9458439170._comment | 8 + ..._0e19ff7deb5ed65f2bc685d4c516d816._comment | 8 + ..._bab9584c41a25dda934ad230e3eb732d._comment | 8 + ..._b2a0d5a45ab8ddd66c29dde9412d7a12._comment | 51 + ..._f2829ecbe80a61aa9a8411d2403de69e._comment | 14 + ..._547fc59b19ad66d7280c53a7f923ea08._comment | 13 + ..._581e23cca0219711f8a4500a8d5d20fc._comment | 16 + ..._5c61457f117de38ef487e5cc2780d554._comment | 24 + doc/preferred_content.mdwn | 197 + ..._7d45e21dfb016e9ffa4715346dd0c1a6._comment | 19 + ..._1ccd90b009245667ad59f4d29d2a3a37._comment | 8 + ..._384025b5fa23a3f175985a081438149f._comment | 8 + ..._6a9bc657bc7415f0e118357d8c6664c6._comment | 18 + ..._f0a957e67297c4bb5a8778c11b3c9fd4._comment | 9 + ..._b434c0e2aaa132020fd4a01551285376._comment | 12 + ..._c4acaa237bf1a8512c5e8ea4cdbd11b9._comment | 8 + ..._ff2a2dc9c566ebd9f570bdfcd7bfc030._comment | 27 + doc/privacy.mdwn | 47 + doc/related_software.mdwn | 11 + doc/repomap.png | Bin 0 -> 67316 bytes doc/scalability.mdwn | 44 + doc/sidebar.mdwn | 15 + doc/sitemap.mdwn | 4 + doc/special_remotes.mdwn | 53 + doc/special_remotes/S3.mdwn | 56 + ..._c366f020c9b97a365e21878a33360079._comment | 10 + ..._c1da387e082d91feec13dde91ccb111a._comment | 12 + ..._59c3ecab7dbc8be53258460473cac21c._comment | 8 + ..._0789a21d980825188bb09f7fc8bba8be._comment | 33 + ..._29574a51d5831c51e2e765eb2c06e567._comment | 12 + ..._4a1f7a230dad6caa84831685b236fd73._comment | 8 + ..._5b22d67de946f4d34a4a3c7449d32988._comment | 8 + ..._bcab2bd0f168954243aa9bcc9671bd94._comment | 8 + ..._38c0b062997fde1ad28facc05d973e83._comment | 12 + ..._409bc2b56382417cf26bb222fb783ba7._comment | 8 + ..._78da9e233882ec0908962882ea8c4056._comment | 10 + ..._6af9781004d982d8e6b20a83ad29eead._comment | 8 + ..._0fa68d584ee7f6b5c9058fba7e911a11._comment | 12 + ..._7ad757b3865b04967c79af0a263bb3b0._comment | 10 + doc/special_remotes/bup.mdwn | 43 + ..._f78c1ed97d2e4c6ebffaa7482cfe0c9b._comment | 23 + ..._b53bceb0058acf4d1ab12ea4853ee443._comment | 24 + ..._65d923226cf6120349d807c5c60f640c._comment | 8 + ..._96179a003da4444f6fc08867872cda0a._comment | 43 + ..._612b038c15206f9f3c2e23c7104ca627._comment | 12 + ..._1186def82741ddab1ade256fb2e59e6f._comment | 17 + ..._7d22a805dd2914971e7ca628ceea69be._comment | 10 + ..._5942333cde09fd98e26c4f1d389cb76f._comment | 10 + ..._cb1a0d3076e9d06e7a24204478f6fa98._comment | 10 + ..._4cbc67e5911748d13cee3c483d7ece8a._comment | 12 + ..._ca7096a759961af375e6bd49663b45b3._comment | 10 + ..._e9881290486a1770bd260f8650ada9c6._comment | 8 + ..._e01b5cc5a0d81b071e93e27e7b91fe2a._comment | 8 + ..._13237170ef5b6646e0e25d3421af3fe5._comment | 10 + ..._1a36a0483a9db04d36e0234a192ebad8._comment | 12 + ..._a8419963dc024b1d9eb73807596012dc._comment | 8 + ..._95ccfdd22a2391daa99e0beb04adedd6._comment | 11 + ..._b9d238fb15ad7628e33c90b071e07bb0._comment | 12 + ..._cc21b81a8f809f6efa5f5b6332513fc3._comment | 12 + ..._3fe750118ff1edbe91a110b86fb5b662._comment | 10 + ..._6794eb52bd87c28ef1df3172aa7d5780._comment | 9 + ..._961276c18e9353ca8e25cad53e7ec51f._comment | 8 + ..._97543acfa7434e332ebea5672e446317._comment | 8 + ..._9229776623c234204c8b164edff95da0._comment | 8 + ..._3bbda479d13f6bf393dcd59ed94ddeaa._comment | 10 + ..._f7000975d38077828ab11a99095b39eb._comment | 8 + ..._5d2bd7c1e1493d3c3784708a9b0bc001._comment | 8 + ..._af01ee5ce31b1490af565cb087d65277._comment | 10 + ..._3d4ffec566d68d601eafe8758a616756._comment | 13 + ..._26af468952f0403171370b56e127830a._comment | 8 + doc/special_remotes/directory.mdwn | 36 + ..._86f8c1b09cbd82bcd76378dfa1b3ca07._comment | 49 + .../directory/comment_12._comment | 16 + ..._311cd013fd8db47856d84161119e059d._comment | 12 + ..._e8a53592adb13f7d7f212a2eb5a18a31._comment | 12 + ..._d949edad6a330079f9e15f703f9091e3._comment | 10 + ..._49009f4e9e335c9a9d0422aa59c9a432._comment | 8 + ..._f5e9b0b477c4e521f8633fd274757fa3._comment | 8 + ..._e790718423c41f5ea8047ea5225bfacd._comment | 11 + ..._325aac80b86588912c4fd61339ccbd0b._comment | 10 + ..._4206db69d68d9917623ce02500387021._comment | 12 + ..._acd9023511fe43817718bc89430f96c3._comment | 8 + ..._d330eb808a990bb71034613c297a265e._comment | 8 + doc/special_remotes/glacier.mdwn | 50 + ..._fcd856b99dc6b3f9141b65fe639ef76b._comment | 10 + ..._38fcca87074f6ea31a12569a822aa8c9._comment | 12 + ..._cea5bcb162e4288847ba5f25464a0406._comment | 28 + ..._0c92cc82c7ac513130f862391a02d329._comment | 8 + ..._8d1dcb4bf48386314bfb248ea6eeeb68._comment | 8 + doc/special_remotes/hook.mdwn | 106 + ..._6a74a25891974a28a8cb42b87cb53c26._comment | 32 + ..._ee7c43b93c5b787216334f019643f6a0._comment | 17 + ..._2593291795e732994862d08bf2ed467b._comment | 17 + ..._35d79b5ffa5a19056efcdc805070bc4b._comment | 18 + ..._6fbf1e963fa3ea4b2eb8ca5a3819762d._comment | 10 + ..._e0ab48d5333e5de85f016b097e6fdac1._comment | 12 + ..._cc2b1243c2c36e63241513bcaddfea67._comment | 10 + ..._bbae315233bda48eb04662dfd48cf1ae._comment | 30 + ..._037523d1994c702239ca96791156fe65._comment | 10 + doc/special_remotes/rsync.mdwn | 60 + ..._9e180c397486989beab21699b8e8f103._comment | 16 + ..._25545dc0b53f09ae73b29899c8884b02._comment | 8 + ..._960a89b1ae7e3888ffba06baa963dc21._comment | 20 + ..._db84816c31239953dd21f23a8c557b43._comment | 15 + ..._ccaffa4aded9dab88c76a856b96ea5b9._comment | 9 + ..._e687b9482b177e1351c8c65ea617d3fa._comment | 8 + doc/special_remotes/web.mdwn | 11 + doc/special_remotes/webdav.mdwn | 45 + ..._6b523eea78eae1d19fe2a9950ee33e3a._comment | 26 + ..._83fc4e7d9ba7a05c8500da659f561b8f._comment | 10 + ..._239367ad639c61ecdf87a89f7ac53efe._comment | 10 + ..._ffa52f7776cdc8caa28667b5eadae123._comment | 8 + ..._5b8cbdb5e9a1b90d748a5074997e1cd5._comment | 12 + ..._d3be3e588c3a2abb2025ceb82c18b0ef._comment | 10 + ..._6fa7e11331db5a943015bd5367eb3d73._comment | 12 + ..._2627b41f80c7511b27464e2040b128a8._comment | 8 + doc/special_remotes/xmpp.mdwn | 36 + ..._568247938929a2934e8198fca80b7184._comment | 11 + ..._9fc3f512020b7eb2591d6b7b2e8de2d7._comment | 10 + doc/summary.mdwn | 11 + doc/sync.mdwn | 38 + ..._59681be5568f568f5c54eb0445163dd2._comment | 8 + ..._9301ff5e81d37475f594e74fbe32f24e._comment | 11 + ..._49560003da47490e4fabd4ab0089f2d7._comment | 8 + ..._cf29326408e62575085d1f980087c923._comment | 8 + doc/templates/bare.tmpl | 1 + doc/templates/bugtemplate.mdwn | 18 + doc/templates/walkthrough.tmpl | 2 + doc/testimonials.mdwn | 34 + doc/tips.mdwn | 4 + ..._37____38____34____35___Haskell__33__.mdwn | 111 + ..._835a3608df3e9d044cabe822d0f3e7e4._comment | 27 + ..._080b30cba72a718e73ea715e259e1cfb._comment | 8 + ...tralized_repository_behind_a_Firewall.mdwn | 59 + ..._78b9035234a690ca5a7c9f3cc78fa092._comment | 8 + .../Delay_Assistant_Startup_on_Login.mdwn | 13 + ..._c63917150527efab4b1106183b3aa7ef._comment | 8 + doc/tips/Git_annex_and_Calibre.mdwn | 118 + ...ly_annex_a_file_already_in_a_git_repo.mdwn | 19 + ..._7eaf73fb3355bd706ab18a43790b3c10._comment | 8 + ..._dac1a171204f30d7c906e878eb6bd461._comment | 45 + ..._b62ec0b848d2487d68d7032682622193._comment | 36 + ..._2423904e41a86cd1c6bc155d7b733642._comment | 9 + doc/tips/Internet_Archive_via_S3.mdwn | 58 + ...Git-annex_as_a_web_browsing_assistant.mdwn | 46 + ..._74167f9fff400f148916003468c77de4._comment | 8 + doc/tips/assume-unstaged.mdwn | 31 + ..._44abd811ef79a85e557418e17a3927be._comment | 10 + ...tomatically_getting_files_on_checkout.mdwn | 15 + ...n_doing_fsck_on_large_special_remotes.mdwn | 2 + ..._e7c5c46112a2406b873d08bbf53c40d8._comment | 8 + ..._daf45ce29fed986fa9aa8b173760d0b7._comment | 14 + ...sed_repository:_starting_from_nothing.mdwn | 75 + ..._b0d22822017646775869ce1292e676f4._comment | 8 + .../centralized_git_repository_tutorial.mdwn | 140 + doc/tips/downloading_podcasts.mdwn | 63 + ..._4d4f6c22070b58918ee8d34c5e7290ad._comment | 8 + ..._d8d77048c7e2524968c188e1ad517873._comment | 24 + ..._0859317471b43c88744dd3df95c879f7._comment | 10 + ..._e8c3c97282d17e2a1d47fb9d5e2b2f7b._comment | 18 + ..._05a3694052de36848fbbad6eeeada895._comment | 8 + ..._21028bed8858c2dae1ac9c2d014fd2a1._comment | 8 + ..._4869fb5c9f896acc477c44de06c36ca7._comment | 8 + ..._2e278ff200c1c15efd27c46a3e0aed40._comment | 9 + ..._f04bc32a34baeeffcd691e9f7cce0230._comment | 13 + ..._a9a98cad7358d16792853a2ee413fe6c._comment | 8 + ..._5a8068a5cb0fd864581157a3aa5d1113._comment | 10 + ..._e7072a9da30b4c4b4c526013144238d4._comment | 10 + ..._79b3f8d678ac9f67df4c0cd649657283._comment | 8 + ..._35106fee5458bdd5c21868fbc49d3616._comment | 10 + ..._ceb16498b7aadbf04a27acd5d6561d46._comment | 8 + ..._147397603f0b3fdb42ca387d1da7c5ef._comment | 8 + ..._6a26a6cc7683d38fae0f23c5a52d1e23._comment | 24 + doc/tips/dropboxannex.mdwn | 28 + doc/tips/emacs_integration.mdwn | 20 + doc/tips/finding_duplicate_files.mdwn | 21 + ..._ddb477ca242ffeb21e0df394d8fdf5d2._comment | 8 + ..._900eafe0a781018ff44b35ac232e3ad3._comment | 8 + .../comment_3._comment | 39 + ..._1494143a74cc1e9fbe4720c14b73d42b._comment | 8 + ..._1a35ca360468bcb84a67ad8d62a2ef7d._comment | 8 + ..._a6e88c93b31f67c933523725ff61b287._comment | 16 + ..._347b0186755a809594bd42feda6363e2._comment | 10 + doc/tips/flickrannex.mdwn | 49 + ..._50707f259abe5829ce075dfbecd5a4ba._comment | 13 + ..._ab5bcb025381b3da4d7c6dfd0c7310dd._comment | 46 + ..._90a331275d888221bc695003c8acbe46._comment | 58 + ..._cf9dad91ee7d334c720adb3310aa0003._comment | 130 + ..._d74c4fc7edf8e47f7482564ce0ef4d12._comment | 10 + ..._f53d0d5520e2835e9705bea4e75556f0._comment | 30 + ..._9ebba4d61140f6c2071e988c9328cf7e._comment | 14 + ..._4470dae270613dd8712623474bc80ab0._comment | 24 + ..._d395cdcf815cb430e374ff05c1a63ff4._comment | 17 + ..._8cf730097001ffe106f2c743edce9d0a._comment | 12 + ..._a80c8087c4e1562a4c98a24edc182e5a._comment | 12 + ..._94f84254c32cf0f7dd1441b7da5d2bc6._comment | 8 + ..._5299b4cab4a4cb8e8fd4d2b39f0ea59c._comment | 9 + doc/tips/googledriveannex.mdwn | 28 + doc/tips/megaannex.mdwn | 41 + doc/tips/migrating_data_to_a_new_backend.mdwn | 16 + doc/tips/owncloudannex.mdwn | 28 + ..._129652308c3c499462828dcaf8e747a4._comment | 40 + ..._38604990368666f654d41891ba99ac61._comment | 15 + ..._1bfd290d00d6536da7d31818db46f8ec._comment | 87 + ..._492b6922a7c5bb5464fedb46b0c5303b._comment | 17 + ..._1d48ac08714fadcb06d874570d745bd8._comment | 16 + ..._65959f49a2f56bffd6fe48670c0c8d5a._comment | 8 + ..._7482002991672ef67836bae43b8d0be8._comment | 8 + doc/tips/powerful_file_matching.mdwn | 36 + doc/tips/recover_data_from_lost+found.mdwn | 19 + ...e_or_dvcs-autosync_with_the_assistant.mdwn | 41 + ..._907e4032ca4a39adb846cf16dbf447dc._comment | 8 + ..._902d001ba86657ef0f8cca5b175f99ca._comment | 8 + ..._a1cf93f9b29658f0f26e9e0ae6057ee3._comment | 60 + ..._e10671908b58c554375787d0f76e2366._comment | 13 + ..._4114380f66b6376c851e93f6876d590b._comment | 8 + ...tup_a_public_repository_on_a_web_site.mdwn | 31 + ..._1d0fa6da33e401df1d7ff31979247fec._comment | 10 + ..._b98b761dee9d923153e3c288c1d987ee._comment | 11 + doc/tips/skydriveannex.mdwn | 29 + doc/tips/untrusted_repositories.mdwn | 28 + doc/tips/using_Amazon_Glacier.mdwn | 75 + doc/tips/using_Amazon_S3.mdwn | 37 + ..._666a26f95024760c99c627eed37b1966._comment | 8 + ..._f5a0883be7dbb421b584c6dc0165f1ef._comment | 8 + doc/tips/using_Google_Cloud_Storage.mdwn | 9 + ..._c576182f39563ae68767391c4227a177._comment | 18 + .../using_box.com_as_a_special_remote.mdwn | 71 + ..._no_fixed_hostname_and_optimising_ssh.mdwn | 59 + ..._c0b7682a2b6f3078457b85683c825baf._comment | 10 + doc/tips/using_gitolite_with_git-annex.mdwn | 89 + ..._8767bc8014b459a3cd76f275fd4fa8d6._comment | 8 + ..._00715e0b47f09130e0e536e29f7b9258._comment | 31 + ..._7027ce60265b8f24c8ab54553e544068._comment | 8 + ..._75218b7409c0e281cb01c9b2791e8cdf._comment | 20 + ..._9a2a2a8eac9af97e0c984ad105763a73._comment | 15 + ..._d8efea4ab9576555fadbb47666ecefa9._comment | 8 + ..._807035f38509ccb9f93f1929ecd37417._comment | 8 + ..._eb81f824aadc97f098379c5f7e4fba4c._comment | 33 + ..._f688309532d2993630e9e72e87fb9c46._comment | 20 + ..._3e203e010a4df5bf03899f867718adc5._comment | 25 + ..._f8fd08b6ab47378ad88c87348057220d._comment | 10 + ..._8249772c142117f88e37975d058aa936._comment | 29 + ..._28418635a6ed7231b89e02211cd3c236._comment | 8 + doc/tips/using_the_SHA1_backend.mdwn | 11 + .../using_the_web_as_a_special_remote.mdwn | 57 + ..._321a41d611c6fe45e047af9c96c5176c._comment | 26 + ..._dfe9c8c49aadff80d2020288584e0390._comment | 10 + ..._ed8dd3bbd9b9ae7f2309b72b94f61eb1._comment | 18 + ..._c1133a524989a940f1b5db588707157a._comment | 10 + .../visualizing_repositories_with_gource.mdwn | 22 + .../screenshot.jpg | Bin 0 -> 78509 bytes ..._to_do_when_a_repository_is_corrupted.mdwn | 22 + ...what_to_do_when_you_lose_a_repository.mdwn | 19 + ..._cf19b8dc304dc37c26717174c4a98aa4._comment | 11 + ..._fa9ca81668f5faebf2f61b10f82c97d2._comment | 8 + ...nother_simple_disk_usage_like_utility.mdwn | 9 + ..._41b212bde8bc88d2a5dea93bd0dc75f1._comment | 9 + doc/todo.mdwn | 4 + ...y_to_pair_devices_like_bittorent_sync.mdwn | 7 + ..._d828bc374e50a49101c0b863f9b33080._comment | 8 + ..._a4badfc248be428e6426a936212cc896._comment | 8 + ..._0b04089d3d33fdb48eeb46bf168e9a3c._comment | 8 + ..._2bcab1b7998b4df08fca41b8d810f115._comment | 10 + doc/todo/Bittorrent-like_features.mdwn | 31 + doc/todo/Build_for_Synology_DSM.mdwn | 1 + ..._e351084d9a83db3fd6d9d983227a6410._comment | 8 + ..._cc67a584f5c460a6fb63cf099c20e573._comment | 9 + ..._94023593d294b9cf69090fcfd6ca0e5a._comment | 9 + ..._314255fd503d125b5aeae2f62acfd592._comment | 8 + ..._4059016fa8da6af7a3eba8966821e8eb._comment | 10 + ..._8900c2985ab68b3b566c9f5d326471d6._comment | 8 + ..._f2b77368473d42b7f21e9d51d6415b58._comment | 10 + ..._a55fea734044c270ceb10adf9c8d9a76._comment | 8 + ..._59865ada057c640ac29855c65cf45dd9._comment | 23 + ..._6d860b1ad8816077b5fa596a71b12d5c._comment | 8 + ..._19ef2d293ba3bc7ece443d7278371c3f._comment | 8 + ..._609b7ad87dfbba49ec1f8c6fc2739ccd._comment | 12 + ..._d94a73b9a07c5cadf191005f817fd59a._comment | 29 + ...___47__ssh__47__git-annex__47__config.mdwn | 7 + ..._284c806e83a32af81b02aea7c7bc285a._comment | 10 + ..._1f55ad6b39906458779b2d604b003ffe._comment | 10 + ..._b00dce2374aac6968317d05d23bcfaf7._comment | 8 + ..._743d0b077110c5cac1e2f47187b75333._comment | 10 + ..._build_if___34__make_test__34___fails.mdwn | 7 + ...e_add_support_for_monad-control_0.3.x.mdwn | 9 + doc/todo/S3.mdwn | 24 + ...ow_transfer_for_a_lot_of_small_files..mdwn | 20 + ...Use_MediaScannerConnection_on_Android.mdwn | 7 + ...g_site_for_files_with_obfuscated_URLs.mdwn | 7 + ..._1a1f34f4f389267d67e79409c0ca8b1d._comment | 9 + ...irectory_and_also_in_the_target_annex.mdwn | 26 + ..._0cc16eb17151309113cec6d1cccf203d._comment | 20 + ...dd_--exclude_option_to_git_annex_find.mdwn | 4 + doc/todo/add_-all_option.mdwn | 22 + doc/todo/add_a_git_backend.mdwn | 18 + .../add_an_icon_for_the_.desktop_file.mdwn | 1 + doc/todo/add_metadata_to_annexed_files.mdwn | 5 + ...epo_via_an_ssh_alias_or_an_ip_address.mdwn | 48 + doc/todo/assistant_git_sync_laddering.mdwn | 10 + .../assistant_parallel_file_transfers.txt | 15 + ...nt_smarter_archive_directory_handling.mdwn | 31 + doc/todo/assistant_threaded_runtime.mdwn | 40 + doc/todo/auto_remotes.mdwn | 29 + doc/todo/auto_remotes/discussion.mdwn | 7 + .../automatic_bookkeeping_watch_command.mdwn | 15 + ...nches_upon___34__git_annex_sync__34__.mdwn | 16 + doc/todo/avoid_unnecessary_union_merges.mdwn | 20 + doc/todo/backendSHA1.mdwn | 7 + doc/todo/branching.mdwn | 159 + doc/todo/cache_key_info.mdwn | 37 + ..._578df1b3b2cbfdc4aa1805378f35dc48._comment | 11 + doc/todo/checkout.mdwn | 23 + doc/todo/direct_mode_guard.mdwn | 22 + ..._431b6e1577bbd30b07dce9002a8fe1a2._comment | 10 + doc/todo/done.mdwn | 4 + doc/todo/exclude_files_on_a_given_remote.mdwn | 18 + doc/todo/faster_gnupg_cipher.mdwn | 1 + ..._8f61f7c724a8224e61c015be68f43db7._comment | 14 + ..._36e1f227a320527653500b445f7c001c._comment | 12 + doc/todo/faster_rsync_remotes.mdwn | 1 + ..._0bc3ee0ae563357675eeccf42461e59a._comment | 8 + ..._ccf6f75450c89ca498c8130054f8d32d._comment | 24 + ..._2f6a9d23cb8351fbf0f60ed93752e76e._comment | 14 + ..._3a2f45defebae3dde336ee5f40c26d7e._comment | 8 + doc/todo/file_copy_progress_bar.mdwn | 5 + ...ce_checking_for_local_special_remotes.mdwn | 4 + ..._47c254cec58cbbb3ea84c93ef8282f01._comment | 8 + doc/todo/fsck.mdwn | 11 + doc/todo/fsck_special_remotes.mdwn | 13 + doc/todo/git-annex-shell.mdwn | 15 + doc/todo/git-annex_unused_eats_memory.mdwn | 32 + ...on_and__47__or_UUID_in_commit_message.mdwn | 13 + doc/todo/gitolite_and_gitosis_support.mdwn | 39 + doc/todo/gitrm.mdwn | 5 + doc/todo/hidden_files.mdwn | 30 + doc/todo/http_headers.mdwn | 8 + doc/todo/immutable_annexed_files.mdwn | 8 + doc/todo/incremental_fsck.mdwn | 24 + ..._609b21141dd5686b2c0eaef2b8d63229._comment | 14 + doc/todo/keep_annexed_files_for_a_while.mdwn | 8 + .../link_file_to_remote_repo_feature.mdwn | 52 + doc/todo/network_remotes.mdwn | 5 + doc/todo/object_dir_reorg_v2.mdwn | 25 + ..._ba03333dc76ff49eccaba375e68cb525._comment | 8 + ..._81276ac309959dc741bc90101c213ab7._comment | 8 + ..._79bdf9c51dec9f52372ce95b53233bb2._comment | 12 + ..._93aada9b1680fed56cc6f0f7c3aca5e5._comment | 12 + ..._821c382987f105da72a50e0a5ce61fdc._comment | 12 + ..._8834c3a3f1258c4349d23aff8549bf35._comment | 10 + ..._42501404c82ca07147e2cce0cff59474._comment | 12 + doc/todo/optimise_git-annex_merge.mdwn | 23 + ...optinally_transfer_file_unencryptedly.mdwn | 6 + ..._4be47e7ac85d0f4e7029a96b615545a7._comment | 8 + doc/todo/parallel_possibilities.mdwn | 13 + ..._d8e34fc2bc4e5cf761574608f970d496._comment | 8 + ..._adb76f06a7997abe4559d3169a3181c3._comment | 12 + ..._145fb974f45da99b7d4b117a3699cccf._comment | 12 + doc/todo/pushpull.mdwn | 4 + doc/todo/redundancy_stats_in_status.mdwn | 23 + ..._9f1c10f8cea4fa60a99cbcc8306dd5de._comment | 10 + doc/todo/resuming_encrypted_uploads.mdwn | 22 + ..._1832a6fb78e8ad7c838582f46731ac3b._comment | 8 + ..._2ecc8e782f49e90ed1549e9179eb1a1e._comment | 8 + doc/todo/rsync.mdwn | 4 + doc/todo/smudge.mdwn | 162 + ..._4ea616bcdbc9e9a6fae9f2e2795c31c9._comment | 8 + ..._e04b32caa0d2b4c577cdaf382a3ff7f6._comment | 12 + .../special_remote_for_amazon_glacier.mdwn | 30 + ..._68f129441eefcbfebf7a9db680f52759._comment | 8 + ..._c5eeaf8ceee414fa0379831ca52e290c._comment | 7 + doc/todo/speed_up_fsck.mdwn | 40 + doc/todo/stream_feature__63__.mdwn | 23 + doc/todo/support-non-utf8-locales.mdwn | 26 + doc/todo/support_S3_multipart_uploads.mdwn | 14 + doc/todo/support_for_lossy_remotes.mdwn | 5 + ..._f5cd9f9deab13ab2d2290ad763906dd3._comment | 8 + ..._for_writing_external_special_remotes.mdwn | 25 + doc/todo/support_fsck_in_bare_repos.mdwn | 17 + doc/todo/symlink_farming_commit_hook.mdwn | 14 + ...my_local_git-annex_from_a_dump_remote.mdwn | 6 + ..._81d63854f89f00855cda5ace0fc8262a._comment | 14 + ..._66822b72b1450e79e8edd0c6c21d5aa6._comment | 14 + doc/todo/tahoe_lfs_for_reals.mdwn | 21 + ..._0a4793ce6a867638f6e510e71dd4bb44._comment | 10 + ..._80b9e848edfdc7be21baab7d0cef0e3a._comment | 13 + doc/todo/union_mounting.mdwn | 10 + ..._cb08435812dd7766de26199c73f38e8b._comment | 8 + ..._240b1736f6bd4fbf87c372d3a46e661b._comment | 9 + doc/todo/untracked_remotes.mdwn | 9 + doc/todo/use_cp_reflink.mdwn | 7 + doc/todo/using_url_backend.mdwn | 11 + doc/todo/windows_support.mdwn | 15 + ..._394127e34e07ab3dc0e7b94ee6898866._comment | 8 + ..._3cc26ad8101a22e95a8c60cf0c4dedcc._comment | 10 + ..._8acae818ce468967499050bbe3c532ea._comment | 12 + ..._bd0a12f4c9b884ab8a06082842381a01._comment | 8 + ..._ad06b98b2ddac866ffee334e41fee6a8._comment | 8 + ..._444fc7251f57db241b6e80abae41851c._comment | 10 + ..._34f1f60b570c389bb1e741b990064a7e._comment | 8 + ..._a5ca56c487257434650420acfa60e39f._comment | 8 + ..._61214de7d967740d42905f3823ce2f65._comment | 12 + ..._259a0b1a6f4d8d1944173380adc5e7c8._comment | 8 + ...Add_to_Android_version_to_Google_Play.mdwn | 9 + ...Advanced_settings_for_xmpp_and_webdav.mdwn | 7 + ..._11c7444ab4988c60732af505b52bde3c._comment | 20 + ...hlist:_An_--all_option_for_dropunused.mdwn | 4 + ..._d8726d108b3b40116b4ec3c9935f2dff._comment | 8 + ..._578248f7686ba2d80d7dc8b17c0cdf52._comment | 16 + .../wishlist:_An_option_like_--git-dir.mdwn | 3 + ..._5d877d90b8bdf21d4b8649744d229efd._comment | 8 + ..._462264821cbc48a433330cbf7ec6044d._comment | 8 + ..._0c3709b07a0a1091ceeee73b69e0f7ac._comment | 8 + doc/todo/wishlist:_GnuPG_options.mdwn | 16 + ..._6662e8a71ce20acc62147ef41ecffa50._comment | 12 + ..._a_preview_of_download_or_upload_size.mdwn | 10 + ..._019a2457e07377510feaa089a93bd76c._comment | 8 + ..._29a154699339bf040af0ee8aa24034f1._comment | 15 + ..._8f7e1c4a5c714cbd719ee170354d79fa._comment | 12 + ..._c7335f757e5546aa841cab38fffe7605._comment | 19 + ..._d2a845354f23d07880612740cf99ddd4._comment | 8 + ...:_Option_to_specify_max_transfer_rate.mdwn | 3 + ..._4fd870e14b5b70c8a6ade41406294387._comment | 10 + ..._dd854f297ad6a94b54be9f3edfd0f766._comment | 8 + ..._a8b7e90a473d5937807cc7eb456efe33._comment | 10 + ...ated_password_prompts_for_one_command.mdwn | 45 + ..._3f9c0d08932c2ede61c802a91261a1f7._comment | 14 + ...4___command_that_will_skip_duplicates.mdwn | 28 + ..._d78d79fb2f3713aa69f45d2691cf8dfe._comment | 68 + ..._4316d9d74312112dc4c823077af7febe._comment | 8 + ..._ed6d07f16a11c6eee7e3d5005e8e6fa3._comment | 8 + ..._fd213310ee548d8726791d2b02237fde._comment | 29 + ..._4394bde1c6fd44acae649baffe802775._comment | 18 + ..._076cb22057583957d5179d8ba9004605._comment | 18 + ..._f120d1e83c1a447f2ecce302fc69cf74._comment | 35 + ..._5c30294b3c59fdebb1eef0ae5da4cd4f._comment | 10 + ..._f24541ada1c86d755acba7e9fa7cff24._comment | 16 + ..._c39f1bb7c61a89b238c61bee1c049767._comment | 54 + ..._221ed2e53420278072a6d879c6f251d1._comment | 8 + ..._aecfa896c97b9448f235bce18a40621d._comment | 14 + ...st:_Restore_s3_files_moved_to_Glacier.mdwn | 7 + ...not__41___to_annex_via_.gitattributes.mdwn | 9 + ...it_annex_add__34___multiple_processes.mdwn | 10 + ..._85b14478411a33e6186a64bd41f0910d._comment | 10 + ..._82e857f463cfdf73c70f6c0a9f9a31d6._comment | 8 + ..._8af85eba7472d9025c6fae4f03e3ad75._comment | 8 + ...___annex_get_for_centralized_use_case.mdwn | 9 + ..._5c8812973cf91b046e7fc44d7e86c78e._comment | 14 + ..._f36b6a5b128423211aac91a252ecf85f._comment | 18 + ..._ad1569b2405acacd2e37f42b82f24c88._comment | 10 + ..._8aba90150fe178ce9712ad951628f3d6._comment | 8 + ..._6f42d240e0021f4dfa37146bea3f5d7e._comment | 16 + ..._5fda455febf728b079f26fe42bf7bcab._comment | 16 + ..._f1052ab997f1a2cccbabfd1533fc0a59._comment | 8 + ..._07804647b6023436878756bd97a23f32._comment | 8 + ...__whereis__39___support_in_the_webapp.mdwn | 4 + ...ecksums_but_disregard_annex.numcopies.mdwn | 12 + ..._6bcf067e4860bdfeb1d7b9fd1702a43a._comment | 8 + ...An_easy_way_to_get_data_into_an_annex.mdwn | 13 + ..._b9fd1bfaf9a3d238fdb7bc9c2d75fe5f._comment | 22 + ..._56f6972413c6f0d9f414245b6f4d27b9._comment | 62 + ..._2c094bef802a2182de4fcd0def1ad29b._comment | 12 + ..._14915c43001f7f72c9fe5119a104ef5c._comment | 10 + ...shlist:___96__git_annex_sync_-m__96__.mdwn | 10 + ...ed___40__e.g.__44___with_WebDAV__41__.mdwn | 25 + ..._f46b0c9b49607e9f4f7a27f5a331ce83._comment | 8 + ..._1b34e1dd72011c65e881dec2543a0373._comment | 12 + doc/todo/wishlist:_addurl_https:.mdwn | 11 + ..._4e8f5e1fc52c3000eb2a1dad0624906e._comment | 14 + ...onfiguration_of_downloader_for_addurl.mdwn | 3 + ...o_be_accissable_via_different_methods.mdwn | 3 + ..._abb6263f3807160222bba1122475c89c._comment | 8 + ...tracking_the_sources_of_the_downloads.mdwn | 28 + ..._36ae3c75053d5ec278b5e6eb2084d57a._comment | 8 + ..._be8eb800523db8cf7a2c68a28fbf5ea5._comment | 8 + ..._d9f725de41a8572c85e4c6d9b4bcc927._comment | 8 + ..._f52492e4cc6f965515800bd1c0e05c90._comment | 10 + ..._5b36656fc5fa124e763f06711d9da32b._comment | 10 + ..._285215a4466806baf85b8606f680494a._comment | 12 + ..._15bf62e46db4b84ed3156f550f03de42._comment | 12 + ...nnex.largefiles_support_for_mimetypes.mdwn | 1 + .../wishlist:_command_options_changes.mdwn | 17 + ..._bfba72a696789bf21b2435dea15f967a._comment | 17 + ..._f6a637c78c989382e3c22d41b7fb4cc2._comment | 19 + ..._bf1114533d2895804e531e76eb6b8095._comment | 8 + ...fine_remotes_that_must_have_all_files.mdwn | 18 + ..._cceccc1a1730ac688d712b81a44e31c3._comment | 10 + ..._eec848fcf3979c03cbff2b7407c75a7a._comment | 16 + .../wishlist:_disable_automatic_commits.mdwn | 36 + ...t:_do_round_robin_downloading_of_data.mdwn | 5 + ..._460335b0e59ad03871c524f1fe812357._comment | 8 + doc/todo/wishlist:_git-annex_replicate.mdwn | 12 + ..._9926132ec6052760cdf28518a24e2358._comment | 10 + ..._c43932f4194aba8fb2470b18e0817599._comment | 12 + ..._c13f4f9c3d5884fc6255fd04feadc2b1._comment | 10 + ..._63f24abf086d644dced8b01e1a9948c9._comment | 8 + doc/todo/wishlist:_git_annex_diff.mdwn | 9 + ...--_same_as_get__44___but_for_defaults.mdwn | 17 + ..._d5413c8acce308505e4e2bec82fb1261._comment | 10 + ..._0aa227c85d34dfff4e94febca44abea8._comment | 12 + ..._2082f4d708a584a1403cc1d4d005fb56._comment | 10 + doc/todo/wishlist:_git_annex_status.mdwn | 21 + ..._994bfd12c5d82e08040d6116915c5090._comment | 8 + ..._c2b0ce025805b774dc77ce264a222824._comment | 13 + ..._d1fd70c67243971c96d59e1ffb7ef6e7._comment | 23 + ..._9aeeb83d202dc8fb33ff364b0705ad94._comment | 8 + .../wishlist:_git_backend_for_git-annex.mdwn | 9 + ..._04319051fedc583e6c326bb21fcce5a5._comment | 10 + ..._7f529f19a47e10b571f65ab382e97fd5._comment | 14 + ..._a077bbad3e4b07cce019eb55a45330e7._comment | 10 + ..._ecca429e12d734b509c671166a676c9d._comment | 8 + ..._3459f0b41d818c23c8fb33edb89df634._comment | 8 + doc/todo/wishlist:_history_of_operations.mdwn | 8 + ..._f9a77ce83c6f39b6272d5c577ffbb9f9._comment | 8 + ...rtial_files_available_during_transfer.mdwn | 18 + ..._1304a721da6f5133fdfa1dac507f1ecb._comment | 10 + ...rd_commit_message_of___96__sync__96__.mdwn | 3 + ...o_the_end_of_the_queue_when_one_fails.mdwn | 7 + ..._82ee9de610a0ac55cd1c27c211079e5b._comment | 10 + ..._bea55156bd32cf9e6dd9b946ba1bb8c1._comment | 10 + ...n_to_disable_url_checking_with_addurl.mdwn | 9 + ..._868a380faa1e55faa3c2d314e3258e86._comment | 10 + ...t_locations_for_files_in_rsync_remote.mdwn | 6 + ...9__s_repo__44___not_your_personal_one.mdwn | 3 + ..._3480b0ec629ef29a151408d869186bf8._comment | 8 + ...ve_directory_remote_setup__47__addurl.mdwn | 7 + ..._b79976afc2242791523e63831f30af71._comment | 12 + ..._1741d2392006a9af9cfd1f3b847600b9._comment | 9 + doc/todo/wishlist:_simpler_gpg_usage.mdwn | 12 + ..._6923fa6ebc0bbe7d93edb1d01d7c46c5._comment | 19 + ..._6fc874b6c391df242bd2592c4a65eae8._comment | 10 + ..._012f340c8c572fe598fc860c1046dabd._comment | 8 + ..._e0c2a13217b795964f3b630c001661ef._comment | 10 + ..._9668b58eb71901e1db8da7db38e068ca._comment | 8 + ...ores___40__gnunet__44___freenet__41__.mdwn | 26 + ..._e2c2047e7401cb95a82ffb686a732859._comment | 8 + ..._472b576afdb169b233edd01adcb2123d._comment | 8 + ...of_Youtube_URLs_in_Web_special_remote.mdwn | 22 + ..._1a383c30df4fb1767f13d8c670b0c0b5._comment | 10 + .../wishlist:_special_remote_Ubuntu_One.mdwn | 1 + ...ist:_special_remote_for_sftp_or_rsync.mdwn | 28 + ..._6f07d9cc92cf8b4927b3a7d1820c9140._comment | 10 + ..._84e4414c88ae91c048564a2cdc2d3250._comment | 8 + ..._79de7ac44e3c0f0f5691a56d3fb88897._comment | 8 + .../wishlist:_special_remote_mega.co.nz.mdwn | 3 + ..._6ca08ef808d4336fc42d0f279d6627b5._comment | 44 + ...upport_copy_--from__61__x_--to__61__y.mdwn | 29 + ..._cf8e0f16b723516374c95a93e4da42fc._comment | 12 + ..._d35359c9dd4dd4365d9a7caf695ff833._comment | 16 + .../wishlist:_support_for_more_ssh_urls_.mdwn | 22 + doc/todo/wishlist:_swift_backend.mdwn | 5 + ..._e6efbb35f61ee521b473a92674036788._comment | 8 + ..._5d8c83b0485112e98367b7abaab3f4e3._comment | 8 + ..._bf8625b909c3a7321cae40e6f145e874._comment | 8 + ...ist:_traffic_accounting_for_git-annex.mdwn | 3 + ...list:_vicfg_possible_repo_group_names.mdwn | 16 + doc/todo/wishlist:alias_system.mdwn | 1 + doc/transferring_data.mdwn | 19 + doc/trust.mdwn | 59 + doc/upgrades.mdwn | 98 + doc/upgrades/SHA_size.mdwn | 20 + ..._20f9b7b75786075de666b2146dc13a60._comment | 12 + doc/use_case/Alice.mdwn | 24 + doc/use_case/Bob.mdwn | 25 + doc/users.mdwn | 9 + doc/users/chrysn.mdwn | 5 + doc/users/fmarier.mdwn | 6 + doc/users/gebi.mdwn | 1 + doc/users/joey.mdwn | 2 + doc/videos.mdwn | 8 + doc/videos/FOSDEM2012.mdwn | 7 + doc/videos/LCA2013.mdwn | 8 + doc/videos/git-annex_assistant_archiving.mdwn | 5 + .../git-annex_assistant_introduction.mdwn | 5 + ..._f42ad4183c2c28319d3705a82fceb82f._comment | 15 + ..._b62f4eeeac1138570f7cb8c98d41c2cb._comment | 12 + .../git-annex_assistant_remote_sharing.mdwn | 6 + doc/videos/git-annex_assistant_sync_demo.mdwn | 8 + doc/videos/git-annex_watch_demo.mdwn | 7 + doc/videos/git-annex_weppapp_demo.mdwn | 8 + doc/walkthrough.mdwn | 25 + doc/walkthrough/adding_a_remote.mdwn | 19 + ..._0a59355bd33a796aec97173607e6adc9._comment | 8 + ..._f8cd79ef1593a8181a7f1086a87713e8._comment | 9 + ..._60691af4400521b5a8c8d75efe3b44cb._comment | 9 + ..._6f7cf5c330272c96b3abeb6612075c9d._comment | 10 + doc/walkthrough/adding_files.mdwn | 11 + .../automatically_managing_content.mdwn | 45 + doc/walkthrough/backups.mdwn | 25 + doc/walkthrough/creating_a_repository.mdwn | 6 + .../fsck:_verifying_your_data.mdwn | 16 + .../fsck:_when_things_go_wrong.mdwn | 13 + doc/walkthrough/getting_file_content.mdwn | 12 + doc/walkthrough/modifying_annexed_files.mdwn | 44 + ..._624b4a0b521b553d68ab6049f7dbaf8c._comment | 14 + ..._b000622304535d32b69db17d51156b21._comment | 10 + doc/walkthrough/more.mdwn | 3 + ...ing_file_content_between_repositories.mdwn | 13 + ..._4c30ade91fc7113a95960aa3bd1d5427._comment | 19 + ..._7d90e1e150e7524ba31687108fcc38d6._comment | 10 + ..._558d80384434207b9cfc033763863de3._comment | 12 + ..._a2f343eceed9e9fba1670f21e0fc6af4._comment | 8 + doc/walkthrough/removing_files.mdwn | 17 + ..._cb65e7c510b75be1c51f655b058667c6._comment | 8 + ..._64709ea4558915edd5c8ca4486965b07._comment | 8 + .../removing_files:_When_things_go_wrong.mdwn | 24 + doc/walkthrough/renaming_files.mdwn | 13 + doc/walkthrough/syncing.mdwn | 29 + ...nsferring_files:_When_things_go_wrong.mdwn | 17 + doc/walkthrough/unused_data.mdwn | 35 + ..._684b7b652d3a8ec04f32129c5528f1ab._comment | 22 + doc/walkthrough/using_bup.mdwn | 37 + doc/walkthrough/using_ssh_remotes.mdwn | 33 + ..._98e97c4d7fbbcd449eddf683967a71d6._comment | 8 + ..._f2775a151ed50caba27057bd9c38bae2._comment | 13 + ..._a8bc6110128431ca2a8624ddc75ea364._comment | 10 + ..._365db5820d96d5daa62c19fd76fcdf1e._comment | 13 + ..._451fd0c6a25ee61ef137e8e5be0c286b._comment | 16 + ..._b2f15a46620385da26d5fe8f11ebfc1a._comment | 15 + ..._433ccc87fbb0a13e32d59d77f0b4e56c._comment | 8 + ..._a9805c7965da0b88a1c9f7f207c450a1._comment | 18 + ..._9d5c12c056892b706cf100ea01866685._comment | 12 + ..._725e7dbb2d0a74a035127cb01ee0442c._comment | 16 + ..._8448e55026d2c2b50d8da41707686bea._comment | 16 + ..._61833299a9878f23ac57598fa6da8839._comment | 23 + doc/walkthrough/using_tags_and_branches.mdwn | 14 + ghci | 4 + git-annex.cabal | 166 + git-annex.hs | 34 + git-union-merge.hs | 48 + standalone/android/Makefile | 153 + standalone/android/busybox_config | 997 ++ standalone/android/dropbear.patch | 55 + standalone/android/evilsplicer-headers.hs | 27 + .../DAV_0.3-0001-build-without-TH.patch | 306 + ...TP_4000.2.8-0001-build-with-base-4.8.patch | 31 + ..._0001-fix-build-not-Android-specific.patch | 34 + .../aeson_0.6.1.0_0001-disable-TH.patch | 24 + ...1-allow-building-with-unreleased-ghc.patch | 25 + ...1-allow-building-with-unreleased-ghc.patch | 27 + ....3.7-0001-support-Android-cert-store.patch | 37 + ...ipher-aes_0.1.7-0001-fix-cross-build.patch | 34 + ...utive_0.3-0001-fixes-for-cross-build.patch | 39 + ...6-0001-use-getprop-to-get-dns-server.patch | 73 + ...-TH-and-export-one-symbol-used-by-TH.patch | 193 + ...1.4-0001-statically-link-with-gnutls.patch | 35 + .../gsasl_0.3.5-0001-link-with-libgsasl.patch | 25 + .../hS3_0.5.7_0001-fix-build.patch | 23 + .../hamlet_1.1.6.1_0001-remove-TH.patch | 294 + ...1.2.11_0001-build-without-IPv6-stuff.patch | 47 + .../lens_3.8.5-0001-build-without-TH.patch | 293 + ..._0.7.3-0001-static-link-with-libxml2.patch | 27 + ...se_0.2.0.2_0001-hacked-for-newer-ghc.patch | 163 + ...ol_0.3.1.4_0001-build-with-newer-ghc.patch | 25 + ...0.2.3.2_0001-remove-TH-logging-stuff.patch | 124 + ...001-NoDelay-does-not-work-on-Android.patch | 43 + ...l-xmpp_0.4.4-0001-avoid-using-gnuidn.patch | 60 + ...work_2.4.1.0_0001-android-port-fixes.patch | 1960 ++++ ....BSD-symbols-not-available-in-bionic.patch | 157 + ....0_0003-configure-misdetects-accept4.patch | 34 + ...-getprotobyname-hack-for-tcp-and-udp.patch | 28 + .../persistent_1.1.5.1_0001-disable-TH.patch | 71 + ...opt-stuff-to-allow-cross-compilation.patch | 24 + ...profunctors_3.3-0001-fix-cross-build.patch | 26 + ...th-hacked-up-lifted-base-which-is-cu.patch | 44 + ...shakespeare-css_1.0.2_0001-remove-TH.patch | 274 + ...1.0.2_0002-expose-modules-used-by-TH.patch | 26 + ...kespeare-i18n_1.0.0.2_0001-remove-TH.patch | 162 + .../shakespeare-js_1.1.2_0001-remove-TH.patch | 308 + ...001-export-symbol-used-by-TH-splices.patch | 139 + .../shakespeare_1.0.3_0001-remove-TH.patch | 208 + .../socks_0.4.2_0001-remove-IPv6-stuff.patch | 107 + ...-modify-to-build-with-unreleased-ghc.patch | 25 + ..._0.3.7_0001-hack-for-cross-compiling.patch | 25 + ...ix-time_0.1.4_0001-hacks-for-android.patch | 81 + ...emove-stuff-not-available-on-Android.patch | 91 + ...tion-that-breaks-when-cross-compilin.patch | 25 + .../wai-app-static_1.3.1-remove-TH.patch | 36 + ...xtra_1.3.2.1_0001-disable-CGI-module.patch | 26 + ...l-hamlet_0.4.0.3-0001-remove-TH-code.patch | 108 + .../yesod-core_1.1.8_0001-remove-TH.patch | 476 + ...2-replaced-TH-in-Yesod.Internal.Core.patch | 267 + ...re_1.1.8_0003-exports-for-TH-splices.patch | 26 + ...yesod-default_1.1.3.2_0001-remove-TH.patch | 102 + ....2.1.1-0001-prepare-for-Evil-Splicer.patch | 83 + .../yesod-form_1.2.1.1-0002-expand-TH.patch | 1606 +++ ...sod-persistent_1.1.0.1_0001-avoid-TH.patch | 41 + ...and-export-module-used-by-TH-splices.patch | 674 ++ .../yesod-static_1.1.2-remove-TH.patch | 174 + ...8_0001-hacked-up-to-build-on-Android.patch | 157 + ....5.4.0_0001-hack-to-build-on-Android.patch | 48 + .../icons/drawable-hdpi/ic_launcher.png | Bin 0 -> 2612 bytes .../ic_stat_service_notification_icon.png | Bin 0 -> 1310 bytes .../icons/drawable-ldpi/ic_launcher.png | Bin 0 -> 1279 bytes .../ic_stat_service_notification_icon.png | Bin 0 -> 682 bytes .../icons/drawable-mdpi/ic_launcher.png | Bin 0 -> 1768 bytes .../ic_stat_service_notification_icon.png | Bin 0 -> 946 bytes .../icons/drawable-xhdpi/ic_launcher.png | Bin 0 -> 3396 bytes .../ic_stat_service_notification_icon.png | Bin 0 -> 1837 bytes .../android/icons/drawable/ic_launcher.png | 1 + .../ic_stat_service_notification_icon.png | 1 + standalone/android/install-haskell-packages | 209 + standalone/android/openssh.config.h | 249 + standalone/android/openssh.patch | 205 + standalone/android/rsync.patch | 40 + standalone/android/runshell | 132 + standalone/android/start | Bin 0 -> 6874 bytes standalone/android/start.c | 64 + standalone/android/term.patch | 598 ++ standalone/licences.gz | Bin 0 -> 60510 bytes standalone/linux/README | 25 + standalone/linux/git-annex | 25 + standalone/linux/git-annex-shell | 25 + standalone/linux/git-annex-webapp | 25 + standalone/linux/glibc-libs | 43 + standalone/linux/runshell | 76 + .../osx/git-annex.app/Contents/Info.plist | 41 + .../osx/git-annex.app/Contents/MacOS/README | 9 + .../git-annex.app/Contents/MacOS/git-annex | 25 + .../Contents/MacOS/git-annex-shell | 25 + .../Contents/MacOS/git-annex-webapp | 26 + .../osx/git-annex.app/Contents/MacOS/runshell | 71 + .../Contents/Resources/git-annex.icns | Bin 0 -> 77548 bytes standalone/windows/build.sh | 63 + ...967426a14eb7e8978277ed4fa937f8e0c514.patch | 75 + static/activityicon.gif | Bin 0 -> 529 bytes static/css/bootstrap-responsive.css | 815 ++ static/css/bootstrap.css | 4983 +++++++++ static/favicon.ico | Bin 0 -> 405 bytes static/img/glyphicons-halflings-white.png | Bin 0 -> 4352 bytes static/img/glyphicons-halflings.png | Bin 0 -> 4352 bytes static/jquery.full.js | 9404 +++++++++++++++++ static/jquery.ui.core.js | 324 + static/jquery.ui.mouse.js | 169 + static/jquery.ui.sortable.js | 1250 +++ static/jquery.ui.widget.js | 521 + static/js/bootstrap-collapse.js | 138 + static/js/bootstrap-dropdown.js | 92 + static/js/bootstrap-modal.js | 210 + static/longpolling.js | 28 + static/syncicon.gif | Bin 0 -> 847 bytes templates/README | 7 + templates/actionbutton.hamlet | 2 + templates/bootstrap.hamlet | 14 + templates/configurators/addbox.com.hamlet | 24 + templates/configurators/adddrive.hamlet | 32 + .../configurators/adddrive/clonemodal.hamlet | 8 + .../configurators/adddrive/confirm.hamlet | 19 + templates/configurators/addglacier.hamlet | 32 + templates/configurators/addia.hamlet | 32 + templates/configurators/addrepository.hamlet | 19 + .../addrepository/archive.hamlet | 11 + .../configurators/addrepository/cloud.hamlet | 26 + .../configurators/addrepository/misc.hamlet | 33 + templates/configurators/addrsync.net.hamlet | 40 + templates/configurators/adds3.hamlet | 28 + .../configurators/checkunfinished.hamlet | 16 + .../delete/currentrepository.hamlet | 34 + .../configurators/delete/finished.hamlet | 14 + templates/configurators/delete/start.hamlet | 11 + templates/configurators/editrepository.hamlet | 35 + templates/configurators/enableaws.hamlet | 30 + .../configurators/enabledirectory.hamlet | 10 + templates/configurators/enableia.hamlet | 22 + templates/configurators/enablewebdav.hamlet | 22 + templates/configurators/main.hamlet | 30 + templates/configurators/needglaciercli.hamlet | 10 + templates/configurators/newrepository.hamlet | 7 + .../newrepository/combine.hamlet | 17 + .../configurators/newrepository/first.hamlet | 19 + .../configurators/newrepository/form.hamlet | 11 + .../configurators/pairing/disabled.hamlet | 5 + .../pairing/local/inprogress.hamlet | 18 + .../configurators/pairing/local/prompt.hamlet | 50 + .../configurators/pairing/xmpp/end.hamlet | 32 + .../pairing/xmpp/friend/confirm.hamlet | 11 + .../pairing/xmpp/friend/prompt.hamlet | 12 + .../pairing/xmpp/self/prompt.hamlet | 20 + .../pairing/xmpp/self/retry.hamlet | 11 + templates/configurators/preferences.hamlet | 13 + templates/configurators/ssh/add.hamlet | 23 + templates/configurators/ssh/confirm.hamlet | 56 + templates/configurators/ssh/enable.hamlet | 30 + templates/configurators/ssh/error.hamlet | 12 + templates/configurators/ssh/testmodal.hamlet | 9 + templates/configurators/xmpp.hamlet | 34 + templates/configurators/xmpp/buddylist.hamlet | 40 + templates/configurators/xmpp/disabled.hamlet | 5 + .../configurators/xmpp/needcloudrepo.hamlet | 17 + templates/control/log.hamlet | 6 + templates/control/repositoryswitcher.hamlet | 14 + templates/control/restarting.hamlet | 2 + templates/control/shutdown.hamlet | 8 + templates/control/shutdownconfirmed.hamlet | 2 + templates/controlmenu.hamlet | 16 + templates/dashboard/main.hamlet | 12 + templates/dashboard/metarefresh.hamlet | 2 + templates/dashboard/transfers.cassius | 2 + templates/dashboard/transfers.hamlet | 45 + templates/documentation/about.hamlet | 33 + templates/documentation/license.hamlet | 2 + templates/documentation/repogroup.hamlet | 58 + templates/error.cassius | 3 + templates/error.hamlet | 26 + templates/notifications/longpolling.julius | 11 + templates/page.cassius | 5 + templates/page.hamlet | 23 + templates/page.julius | 17 + templates/repolist.hamlet | 72 + templates/repolist.julius | 28 + templates/sidebar/alert.hamlet | 25 + templates/sidebar/main.hamlet | 3 + test | Bin 0 -> 9836182 bytes 5105 files changed, 170755 insertions(+) create mode 100644 .ghci create mode 100644 Annex.hs create mode 100644 Annex/Branch.hs create mode 100644 Annex/BranchState.hs create mode 100644 Annex/CatFile.hs create mode 100644 Annex/CheckAttr.hs create mode 100644 Annex/CheckIgnore.hs create mode 100644 Annex/Content.hs create mode 100644 Annex/Content/Direct.hs create mode 100644 Annex/Direct.hs create mode 100644 Annex/Environment.hs create mode 100644 Annex/Exception.hs create mode 100644 Annex/FileMatcher.hs create mode 100644 Annex/Journal.hs create mode 100644 Annex/Link.hs create mode 100644 Annex/LockPool.hs create mode 100644 Annex/Perms.hs create mode 100644 Annex/Queue.hs create mode 100644 Annex/ReplaceFile.hs create mode 100644 Annex/Ssh.hs create mode 100644 Annex/TaggedPush.hs create mode 100644 Annex/UUID.hs create mode 100644 Annex/Version.hs create mode 100644 Annex/Wanted.hs create mode 100644 Assistant.hs create mode 100644 Assistant/Alert.hs create mode 100644 Assistant/Alert/Utility.hs create mode 100644 Assistant/BranchChange.hs create mode 100644 Assistant/Changes.hs create mode 100644 Assistant/Commits.hs create mode 100644 Assistant/Common.hs create mode 100644 Assistant/DaemonStatus.hs create mode 100644 Assistant/DeleteRemote.hs create mode 100644 Assistant/Drop.hs create mode 100644 Assistant/Install.hs create mode 100644 Assistant/Install/AutoStart.hs create mode 100644 Assistant/Install/Menu.hs create mode 100644 Assistant/MakeRemote.hs create mode 100644 Assistant/Monad.hs create mode 100644 Assistant/NamedThread.hs create mode 100644 Assistant/NetMessager.hs create mode 100644 Assistant/Pairing.hs create mode 100644 Assistant/Pairing/MakeRemote.hs create mode 100644 Assistant/Pairing/Network.hs create mode 100644 Assistant/Pushes.hs create mode 100644 Assistant/ScanRemotes.hs create mode 100644 Assistant/Ssh.hs create mode 100644 Assistant/Sync.hs create mode 100644 Assistant/Threads/Committer.hs create mode 100644 Assistant/Threads/ConfigMonitor.hs create mode 100644 Assistant/Threads/DaemonStatus.hs create mode 100644 Assistant/Threads/Glacier.hs create mode 100644 Assistant/Threads/Merger.hs create mode 100644 Assistant/Threads/MountWatcher.hs create mode 100644 Assistant/Threads/NetWatcher.hs create mode 100644 Assistant/Threads/PairListener.hs create mode 100644 Assistant/Threads/Pusher.hs create mode 100644 Assistant/Threads/SanityChecker.hs create mode 100644 Assistant/Threads/TransferPoller.hs create mode 100644 Assistant/Threads/TransferScanner.hs create mode 100644 Assistant/Threads/TransferWatcher.hs create mode 100644 Assistant/Threads/Transferrer.hs create mode 100644 Assistant/Threads/Watcher.hs create mode 100644 Assistant/Threads/WebApp.hs create mode 100644 Assistant/Threads/XMPPClient.hs create mode 100644 Assistant/Threads/XMPPPusher.hs create mode 100644 Assistant/TransferQueue.hs create mode 100644 Assistant/TransferSlots.hs create mode 100644 Assistant/TransferrerPool.hs create mode 100644 Assistant/Types/Alert.hs create mode 100644 Assistant/Types/BranchChange.hs create mode 100644 Assistant/Types/Buddies.hs create mode 100644 Assistant/Types/Changes.hs create mode 100644 Assistant/Types/Commits.hs create mode 100644 Assistant/Types/DaemonStatus.hs create mode 100644 Assistant/Types/NamedThread.hs create mode 100644 Assistant/Types/NetMessager.hs create mode 100644 Assistant/Types/Pushes.hs create mode 100644 Assistant/Types/ScanRemotes.hs create mode 100644 Assistant/Types/ThreadName.hs create mode 100644 Assistant/Types/ThreadedMonad.hs create mode 100644 Assistant/Types/TransferQueue.hs create mode 100644 Assistant/Types/TransferSlots.hs create mode 100644 Assistant/Types/TransferrerPool.hs create mode 100644 Assistant/Types/UrlRenderer.hs create mode 100644 Assistant/WebApp.hs create mode 100644 Assistant/WebApp/Common.hs create mode 100644 Assistant/WebApp/Configurators.hs create mode 100644 Assistant/WebApp/Configurators/AWS.hs create mode 100644 Assistant/WebApp/Configurators/Delete.hs create mode 100644 Assistant/WebApp/Configurators/Edit.hs create mode 100644 Assistant/WebApp/Configurators/IA.hs create mode 100644 Assistant/WebApp/Configurators/Local.hs create mode 100644 Assistant/WebApp/Configurators/Pairing.hs create mode 100644 Assistant/WebApp/Configurators/Preferences.hs create mode 100644 Assistant/WebApp/Configurators/Ssh.hs create mode 100644 Assistant/WebApp/Configurators/WebDAV.hs create mode 100644 Assistant/WebApp/Configurators/XMPP.hs create mode 100644 Assistant/WebApp/Control.hs create mode 100644 Assistant/WebApp/DashBoard.hs create mode 100644 Assistant/WebApp/Documentation.hs create mode 100644 Assistant/WebApp/Form.hs create mode 100644 Assistant/WebApp/Notifications.hs create mode 100644 Assistant/WebApp/OtherRepos.hs create mode 100644 Assistant/WebApp/Page.hs create mode 100644 Assistant/WebApp/RepoList.hs create mode 100644 Assistant/WebApp/SideBar.hs create mode 100644 Assistant/WebApp/Types.hs create mode 100644 Assistant/WebApp/Utility.hs create mode 100644 Assistant/WebApp/routes create mode 100644 Assistant/XMPP.hs create mode 100644 Assistant/XMPP/Buddies.hs create mode 100644 Assistant/XMPP/Client.hs create mode 100644 Assistant/XMPP/Git.hs create mode 100644 Backend.hs create mode 100644 Backend/SHA.hs create mode 100644 Backend/URL.hs create mode 100644 Backend/WORM.hs create mode 100644 Build/BundledPrograms.hs create mode 100644 Build/Configure.hs create mode 100644 Build/DesktopFile.hs create mode 100644 Build/EvilSplicer.hs create mode 100644 Build/InstallDesktopFile.hs create mode 100644 Build/NullSoftInstaller.hs create mode 100644 Build/OSXMkLibs.hs create mode 100644 Build/Standalone.hs create mode 100644 Build/TestConfig.hs create mode 100755 Build/make-sdist.sh create mode 100755 Build/mdwn2man create mode 100644 BuildFlags.hs create mode 120000 CHANGELOG create mode 120000 COPYRIGHT create mode 100644 Checks.hs create mode 100644 CmdLine.hs create mode 100644 Command.hs create mode 100644 Command/Add.hs create mode 100644 Command/AddUnused.hs create mode 100644 Command/AddUrl.hs create mode 100644 Command/Assistant.hs create mode 100644 Command/Commit.hs create mode 100644 Command/ConfigList.hs create mode 100644 Command/Content.hs create mode 100644 Command/Copy.hs create mode 100644 Command/Dead.hs create mode 100644 Command/Describe.hs create mode 100644 Command/Direct.hs create mode 100644 Command/Drop.hs create mode 100644 Command/DropKey.hs create mode 100644 Command/DropUnused.hs create mode 100644 Command/EnableRemote.hs create mode 100644 Command/Find.hs create mode 100644 Command/Fix.hs create mode 100644 Command/FromKey.hs create mode 100644 Command/Fsck.hs create mode 100644 Command/FuzzTest.hs create mode 100644 Command/Get.hs create mode 100644 Command/Group.hs create mode 100644 Command/Help.hs create mode 100644 Command/Import.hs create mode 100644 Command/ImportFeed.hs create mode 100644 Command/InAnnex.hs create mode 100644 Command/Indirect.hs create mode 100644 Command/Init.hs create mode 100644 Command/InitRemote.hs create mode 100644 Command/Lock.hs create mode 100644 Command/Log.hs create mode 100644 Command/Map.hs create mode 100644 Command/Merge.hs create mode 100644 Command/Migrate.hs create mode 100644 Command/Move.hs create mode 100644 Command/PreCommit.hs create mode 100644 Command/ReKey.hs create mode 100644 Command/RecvKey.hs create mode 100644 Command/Reinject.hs create mode 100644 Command/RmUrl.hs create mode 100644 Command/Semitrust.hs create mode 100644 Command/SendKey.hs create mode 100644 Command/Status.hs create mode 100644 Command/Sync.hs create mode 100644 Command/Test.hs create mode 100644 Command/TransferInfo.hs create mode 100644 Command/TransferKey.hs create mode 100644 Command/TransferKeys.hs create mode 100644 Command/Trust.hs create mode 100644 Command/Unannex.hs create mode 100644 Command/Ungroup.hs create mode 100644 Command/Uninit.hs create mode 100644 Command/Unlock.hs create mode 100644 Command/Untrust.hs create mode 100644 Command/Unused.hs create mode 100644 Command/Upgrade.hs create mode 100644 Command/Version.hs create mode 100644 Command/Vicfg.hs create mode 100644 Command/Watch.hs create mode 100644 Command/WebApp.hs create mode 100644 Command/Whereis.hs create mode 100644 Command/XMPPGit.hs create mode 100644 Common.hs create mode 100644 Common/Annex.hs create mode 100644 Config.hs create mode 100644 Config/Cost.hs create mode 100644 Config/Files.hs create mode 100644 Creds.hs create mode 100644 Crypto.hs create mode 100644 Fields.hs create mode 100644 Git.hs create mode 100644 Git/AutoCorrect.hs create mode 100644 Git/Branch.hs create mode 100644 Git/BuildVersion.hs create mode 100644 Git/CatFile.hs create mode 100644 Git/CheckAttr.hs create mode 100644 Git/CheckIgnore.hs create mode 100644 Git/Command.hs create mode 100644 Git/Config.hs create mode 100644 Git/Construct.hs create mode 100644 Git/CurrentRepo.hs create mode 100644 Git/DiffTree.hs create mode 100644 Git/FilePath.hs create mode 100644 Git/Filename.hs create mode 100644 Git/HashObject.hs create mode 100644 Git/Index.hs create mode 100644 Git/LsFiles.hs create mode 100644 Git/LsTree.hs create mode 100644 Git/Merge.hs create mode 100644 Git/Queue.hs create mode 100644 Git/Ref.hs create mode 100644 Git/Remote.hs create mode 100644 Git/Sha.hs create mode 100644 Git/SharedRepository.hs create mode 100644 Git/Types.hs create mode 100644 Git/UnionMerge.hs create mode 100644 Git/UpdateIndex.hs create mode 100644 Git/Url.hs create mode 100644 Git/Version.hs create mode 100644 GitAnnex.hs create mode 100644 GitAnnex/Options.hs create mode 100644 GitAnnexShell.hs create mode 120000 INSTALL create mode 100644 Init.hs create mode 100644 Limit.hs create mode 100644 Locations.hs create mode 100644 Logs/Group.hs create mode 100644 Logs/Location.hs create mode 100644 Logs/PreferredContent.hs create mode 100644 Logs/Presence.hs create mode 100644 Logs/Remote.hs create mode 100644 Logs/Transfer.hs create mode 100644 Logs/Trust.hs create mode 100644 Logs/UUID.hs create mode 100644 Logs/UUIDBased.hs create mode 100644 Logs/Unused.hs create mode 100644 Logs/Web.hs create mode 100644 Makefile create mode 100644 Messages.hs create mode 100644 Messages/JSON.hs create mode 120000 NEWS create mode 100644 Option.hs create mode 100644 README create mode 100644 Remote.hs create mode 100644 Remote/Bup.hs create mode 100644 Remote/Directory.hs create mode 100644 Remote/Git.hs create mode 100644 Remote/Glacier.hs create mode 100644 Remote/Helper/AWS.hs create mode 100644 Remote/Helper/Chunked.hs create mode 100644 Remote/Helper/Encryptable.hs create mode 100644 Remote/Helper/Hooks.hs create mode 100644 Remote/Helper/Special.hs create mode 100644 Remote/Helper/Ssh.hs create mode 100644 Remote/Hook.hs create mode 100644 Remote/List.hs create mode 100644 Remote/Rsync.hs create mode 100644 Remote/S3.hs create mode 100644 Remote/Web.hs create mode 100644 Remote/WebDAV.hs create mode 100644 Seek.hs create mode 100644 Setup.hs create mode 100644 Test.hs create mode 100644 Types.hs create mode 100644 Types/Backend.hs create mode 100644 Types/BranchState.hs create mode 100644 Types/Command.hs create mode 100644 Types/Crypto.hs create mode 100644 Types/FileMatcher.hs create mode 100644 Types/GitConfig.hs create mode 100644 Types/Group.hs create mode 100644 Types/Key.hs create mode 100644 Types/KeySource.hs create mode 100644 Types/Messages.hs create mode 100644 Types/Option.hs create mode 100644 Types/Remote.hs create mode 100644 Types/StandardGroups.hs create mode 100644 Types/TrustLevel.hs create mode 100644 Types/UUID.hs create mode 100644 Upgrade.hs create mode 100644 Upgrade/V0.hs create mode 100644 Upgrade/V1.hs create mode 100644 Upgrade/V2.hs create mode 100644 Usage.hs create mode 100644 Utility/Applicative.hs create mode 100644 Utility/Base64.hs create mode 100644 Utility/Batch.hs create mode 100644 Utility/CoProcess.hs create mode 100644 Utility/CopyFile.hs create mode 100644 Utility/DBus.hs create mode 100644 Utility/Daemon.hs create mode 100644 Utility/DataUnits.hs create mode 100644 Utility/DirWatcher.hs create mode 100644 Utility/DirWatcher/Types.hs create mode 100644 Utility/Directory.hs create mode 100644 Utility/DiskFree.hs create mode 100644 Utility/Dot.hs create mode 100644 Utility/Env.hs create mode 100644 Utility/Exception.hs create mode 100644 Utility/ExternalSHA.hs create mode 100644 Utility/FSEvents.hs create mode 100644 Utility/FileMode.hs create mode 100644 Utility/FileSystemEncoding.hs create mode 100644 Utility/Format.hs create mode 100644 Utility/FreeDesktop.hs create mode 100644 Utility/Gpg.hs create mode 100644 Utility/Gpg/Types.hs create mode 100644 Utility/HumanNumber.hs create mode 100644 Utility/HumanTime.hs create mode 100644 Utility/INotify.hs create mode 100644 Utility/InodeCache.hs create mode 100644 Utility/JSONStream.hs create mode 100644 Utility/Kqueue.hs create mode 100644 Utility/LogFile.hs create mode 100644 Utility/Lsof.hs create mode 100644 Utility/Matcher.hs create mode 100644 Utility/Metered.hs create mode 100644 Utility/Misc.hs create mode 100644 Utility/Monad.hs create mode 100644 Utility/Mounts.hsc create mode 100644 Utility/Network.hs create mode 100644 Utility/NotificationBroadcaster.hs create mode 100644 Utility/OSX.hs create mode 100644 Utility/Parallel.hs create mode 100644 Utility/PartialPrelude.hs create mode 100644 Utility/Path.hs create mode 100644 Utility/Percentage.hs create mode 100644 Utility/Process.hs create mode 100644 Utility/QuickCheck.hs create mode 100644 Utility/Rsync.hs create mode 100644 Utility/SRV.hs create mode 100644 Utility/SafeCommand.hs create mode 100644 Utility/Shell.hs create mode 100644 Utility/TList.hs create mode 100644 Utility/Tense.hs create mode 100644 Utility/ThreadLock.hs create mode 100644 Utility/ThreadScheduler.hs create mode 100644 Utility/Tmp.hs create mode 100644 Utility/Touch.hsc create mode 100644 Utility/Url.hs create mode 100644 Utility/UserInfo.hs create mode 100644 Utility/Verifiable.hs create mode 100644 Utility/WebApp.hs create mode 100644 Utility/Yesod.hs create mode 100644 Utility/libdiskfree.c create mode 100644 Utility/libdiskfree.h create mode 100644 Utility/libkqueue.c create mode 100644 Utility/libkqueue.h create mode 100644 Utility/libmounts.c create mode 100644 Utility/libmounts.h create mode 100644 configure.hs create mode 100644 debian/NEWS create mode 100644 debian/changelog create mode 100644 debian/compat create mode 100644 debian/control create mode 100644 debian/copyright create mode 100644 debian/doc-base create mode 100644 debian/menu create mode 100755 debian/rules create mode 100644 doc/Android.mdwn create mode 100644 doc/Android/comment_15_77bafc01b47d4cf8f96bde2b6704ed71._comment create mode 100644 doc/Android/comment_19_dc7b428f525a082834cb87221fc627ff._comment create mode 100644 doc/Android/comment_20_81940ea56ace3dcd5fa84dfccd88ad96._comment create mode 100644 doc/Android/comment_29_37aa87a451d4390ed367402eec740855._comment create mode 100644 doc/Android/oldcomments.mdwn create mode 100644 doc/Android/oldcomments/comment_10_20e3d513b8b97496d76aca4619026cd6._comment create mode 100644 doc/Android/oldcomments/comment_11_c96b8f1cc1583a74eb2483f48357f023._comment create mode 100644 doc/Android/oldcomments/comment_12_6551f5fa081494b079c10a33c9b0d8ad._comment create mode 100644 doc/Android/oldcomments/comment_13_7c633d245651ec08f63194fe1fc194ae._comment create mode 100644 doc/Android/oldcomments/comment_14_60c2403140085f9caf48a33b59a36ab4._comment create mode 100644 doc/Android/oldcomments/comment_16_9af73451be09f03cfff81fdf9481ffc4._comment create mode 100644 doc/Android/oldcomments/comment_17_f76561a654b534df3a807b1c045710b2._comment create mode 100644 doc/Android/oldcomments/comment_18_1b46cdf154ddadfe17e4b6e4054dc619._comment create mode 100644 doc/Android/oldcomments/comment_1_cc9caa5dd22dd67e5c1d22d697096dd2._comment create mode 100644 doc/Android/oldcomments/comment_21_5903f6a4a81a6534fa8cfafb3b6c37bb._comment create mode 100644 doc/Android/oldcomments/comment_22_36afd354f9669a154d7b6b2c4d43ded9._comment create mode 100644 doc/Android/oldcomments/comment_23_de98154792e8611a134429f06d82bcb1._comment create mode 100644 doc/Android/oldcomments/comment_24_7ab509c25243009bfbffd796ec64e77b._comment create mode 100644 doc/Android/oldcomments/comment_25_026d1a01d5753d71ac3dfc002f2a5eec._comment create mode 100644 doc/Android/oldcomments/comment_26_f0a044fb649d43e32c96b08edbc336c3._comment create mode 100644 doc/Android/oldcomments/comment_27_6b9ae35b1ceeba14cd7a74e142870705._comment create mode 100644 doc/Android/oldcomments/comment_28_c91db1215f529aa68bfb0576c3c5eddc._comment create mode 100644 doc/Android/oldcomments/comment_2_c2422b7dd9d526b3616e49f48cf178c2._comment create mode 100644 doc/Android/oldcomments/comment_3_0e4980c27b13dbc28477c02a82898248._comment create mode 100644 doc/Android/oldcomments/comment_4_86f7b5444e2eaea7f8f7b9160f671a1d._comment create mode 100644 doc/Android/oldcomments/comment_5_9d78009435736a178d5a3f5a9bc0ed6a._comment create mode 100644 doc/Android/oldcomments/comment_6_7b9523ddb20dc4a929e556c3ed0c7406._comment create mode 100644 doc/Android/oldcomments/comment_7_a56628a622da752806c42c5b8b54ceef._comment create mode 100644 doc/Android/oldcomments/comment_8_19656ec99b8f6aa64c1d01a3c9ae9bd0._comment create mode 100644 doc/Android/oldcomments/comment_9_55e703ae105d0c0ee9ac50df8cc59dfb._comment create mode 100644 doc/android/DCIM.png create mode 100644 doc/android/appinstalled.png create mode 100644 doc/android/apps.png create mode 100644 doc/android/install.png create mode 100644 doc/android/newwindow.png create mode 100644 doc/android/terminal.png create mode 100644 doc/android/webapp.png create mode 100644 doc/assistant.mdwn create mode 100644 doc/assistant/addsshserver.png create mode 100644 doc/assistant/archival_walkthrough.mdwn create mode 100644 doc/assistant/buddylist.png create mode 100644 doc/assistant/cloudnudge.png create mode 100644 doc/assistant/combinerepos.png create mode 100644 doc/assistant/comment_1_f2c4857b7b000e005f0c19279db14eaf._comment create mode 100644 doc/assistant/comment_2_befa1f48e5a43a7965060491430a6bc4._comment create mode 100644 doc/assistant/controlmenu.png create mode 100644 doc/assistant/crashrecovery.png create mode 100644 doc/assistant/dashboard.png create mode 100644 doc/assistant/deleterepository.png create mode 100644 doc/assistant/example.png create mode 100644 doc/assistant/iaitem.png create mode 100644 doc/assistant/inotify_max_limit_alert.png create mode 100644 doc/assistant/local_pairing_walkthrough.mdwn create mode 100644 doc/assistant/local_pairing_walkthrough/addrepository.png create mode 100644 doc/assistant/local_pairing_walkthrough/comment_1_b33deed054d3aa8cfa6c9e3958643f16._comment create mode 100644 doc/assistant/local_pairing_walkthrough/comment_2_39f1162b4d43b61e957e7497df4b9e2b._comment create mode 100644 doc/assistant/local_pairing_walkthrough/comment_3_588869692b290483f58f3a7aa2bfb55f._comment create mode 100644 doc/assistant/local_pairing_walkthrough/comment_4_f6bf82c263fefe38701709d9dbd974cc._comment create mode 100644 doc/assistant/local_pairing_walkthrough/comment_5_bada601ea4b7104f162a3e00def4be2b._comment create mode 100644 doc/assistant/local_pairing_walkthrough/comment_6_01ba0f9bfa0ed066c4b73d2d6028eecc._comment create mode 100644 doc/assistant/local_pairing_walkthrough/comment_7_17d44229e4fa46c50815672b96a9735a._comment create mode 100644 doc/assistant/local_pairing_walkthrough/comment_8_b9d4c29cf2cca0427808df6af08fb789._comment create mode 100644 doc/assistant/local_pairing_walkthrough/pairing.png create mode 100644 doc/assistant/local_pairing_walkthrough/pairrequest.png create mode 100644 doc/assistant/local_pairing_walkthrough/secret.png create mode 100644 doc/assistant/local_pairing_walkthrough/secretempty.png create mode 100644 doc/assistant/logs.png create mode 100644 doc/assistant/makerepo.png create mode 100644 doc/assistant/menu.png create mode 100644 doc/assistant/osx-app.png create mode 100644 doc/assistant/preferences.png create mode 100644 doc/assistant/quickstart.mdwn create mode 100644 doc/assistant/release_notes.mdwn create mode 100644 doc/assistant/release_notes/comment_1_bd8f376c9d0c1d5ed07fb013907a60ee._comment create mode 100644 doc/assistant/release_notes/comment_2_75e0774ad042717fbd059a8a9ec2db1e._comment create mode 100644 doc/assistant/release_notes/comment_3_b3bfd8e547e20c51f7c32c6c9424e936._comment create mode 100644 doc/assistant/release_notes/comment_4_c6caa2b521b456bb4ce594d64919cffe._comment create mode 100644 doc/assistant/remote_sharing_walkthrough.mdwn create mode 100644 doc/assistant/remote_sharing_walkthrough/comment_1_e0187b0a926904b363065ab0f850f0b2._comment create mode 100644 doc/assistant/remote_sharing_walkthrough/comment_2_dabcbc9aaf0bdb82716f5a5d55807a21._comment create mode 100644 doc/assistant/repogroups.png create mode 100644 doc/assistant/repositories.png create mode 100644 doc/assistant/rsync.net.png create mode 100644 doc/assistant/running.png create mode 100644 doc/assistant/share_with_a_friend_walkthrough.mdwn create mode 100644 doc/assistant/share_with_a_friend_walkthrough/buddylist.png create mode 100644 doc/assistant/share_with_a_friend_walkthrough/pairing.png create mode 100644 doc/assistant/share_with_a_friend_walkthrough/repolist.png create mode 100644 doc/assistant/share_with_a_friend_walkthrough/xmppalert.png create mode 100644 doc/assistant/thanks.mdwn create mode 100644 doc/assistant/thumbnail.png create mode 100644 doc/assistant/xmpp.png create mode 100644 doc/assistant/xmppnudge.png create mode 100644 doc/assistant/xmpppairingend.png create mode 100644 doc/automatic_conflict_resolution.mdwn create mode 100644 doc/backends.mdwn create mode 100644 doc/backends/comment_1_375bb1fb5973e8fa67b763f2dd6e404b._comment create mode 100644 doc/backends/comment_2_1f2626eca9004b31a0b7fc1a0df8027b._comment create mode 100644 doc/backends/comment_3_fdcbf8727fdefb9942a54689234b9698._comment create mode 100644 doc/bare_repositories.mdwn create mode 100644 doc/bare_repositories/comment_1_148e1da70d37d311634a0309a4ff8dcd._comment create mode 100644 doc/bugs.mdwn create mode 100644 doc/bugs/3.20121112:_build_error_in_assistant.mdwn create mode 100644 doc/bugs/3.20121112:_build_error_in_assistant/comment_1_b42f40ffd83321ab5cc0ef24ced15e98._comment create mode 100644 doc/bugs/3.20121112:_build_error_in_assistant/comment_2_b1d2aa10ea84c5c370b3e76507fc8761._comment create mode 100644 doc/bugs/3.20121112:_build_error_in_assistant/comment_3_b38e40d36bba95b16afbce68e7f25a80._comment create mode 100644 doc/bugs/3.20121112_build_fails_on_Ubuntu_12.04.mdwn create mode 100644 doc/bugs/3.20121112_build_fails_on_Ubuntu_12.04/comment_1_ce2efd2196e7682f4cdbabdb0616d449._comment create mode 100644 doc/bugs/3.20121112_build_fails_on_Ubuntu_12.04/comment_2_2a6faf662ebb85a8f1c89adcdfb9adb6._comment create mode 100644 doc/bugs/3.20121112_build_fails_on_Ubuntu_12.04/comment_3_37f34baa34068def1adf794d0942e462._comment create mode 100644 doc/bugs/3.20121112_build_fails_on_Ubuntu_12.04/comment_4_2f8a859fef9edc8eb93bf1cc74296702._comment create mode 100644 doc/bugs/3.20121113_build_error___39__not_in_scope_getAddBoxComR__39__.mdwn create mode 100644 doc/bugs/4.20130227_won__39__t_build_on_OS_X_Lion__44___because_testpack_won__39__t_build.mdwn create mode 100644 doc/bugs/4.20130227_won__39__t_build_on_OS_X_Lion__44___because_testpack_won__39__t_build/comment_1_b7140e2bf1ea9c73ecc9e214095968e7._comment create mode 100644 doc/bugs/4.20130227_won__39__t_build_on_OS_X_Lion__44___because_testpack_won__39__t_build/comment_2_6be87b2fb2ed828e7b4bf785729e910e._comment create mode 100644 doc/bugs/4.20130601_xmpp_sync_error.mdwn create mode 100644 doc/bugs/4.20130601_xmpp_sync_error/comment_1_5b50d97e44cbd5b31ff24537ec3f8603._comment create mode 100644 doc/bugs/Add_another_repository_on_USB_drive_causes_sync_loop.mdwn create mode 100644 doc/bugs/Add_another_repository_on_USB_drive_causes_sync_loop/comment_1_81839a6de7450734ee75b51e47a0898e._comment create mode 100644 doc/bugs/Add_another_repository_on_USB_drive_causes_sync_loop/comment_2_907ce31a31df94984c2bd7aaafe5b10b._comment create mode 100644 doc/bugs/Add_another_repository_on_USB_drive_causes_sync_loop/comment_3_d8a86ae0ae5fa1f91e0b40b8b2ba0406._comment create mode 100644 doc/bugs/Add_another_repository_on_USB_drive_causes_sync_loop/comment_4_1f08fd5dd4f5d8723c2b5391cc3b60f9._comment create mode 100644 doc/bugs/Adding_a_repository_as_a___34__remote_server__34___creates_a_bare_repository_next_to_the_existing_one.mdwn create mode 100644 doc/bugs/Adding_a_repository_as_a___34__remote_server__34___creates_a_bare_repository_next_to_the_existing_one/comment_1_cb781d34889d583663e855c4074f8e0e._comment create mode 100644 doc/bugs/Adding_a_repository_as_a___34__remote_server__34___creates_a_bare_repository_next_to_the_existing_one/comment_2_c0c87957d7c7a09664e60571a2ca0e8c._comment create mode 100644 doc/bugs/Adding_box.com_remote_on_Android_fails_for_me.mdwn create mode 100644 doc/bugs/Adding_box.com_remote_on_Android_fails_for_me/comment_1_0303ce880415d7e043533551c2b24694._comment create mode 100644 doc/bugs/Adding_git_ssh_remote_fails.mdwn create mode 100644 doc/bugs/Adding_git_ssh_remote_fails/comment_1_05c0bd9ac7c6f0045217fd72fc1f0a1b._comment create mode 100644 doc/bugs/Adding_git_ssh_remote_fails/comment_2_df05456cafdd89e8ceea830199f42d45._comment create mode 100644 doc/bugs/Adding_second_remote_repository_over_ssh_fails.mdwn create mode 100644 doc/bugs/Adding_second_remote_repository_over_ssh_fails/comment_1_308d5f517bf00c8edc53db438de52355._comment create mode 100644 doc/bugs/Addurl_downloads_but_does_not_checkout_files.mdwn create mode 100644 doc/bugs/Allow_syncing_to_a_specific_directory_on_a_USB_remote.mdwn create mode 100644 doc/bugs/Allow_syncing_to_a_specific_directory_on_a_USB_remote/comment_1_13ecedfbb34c3564af3a790b8bf0f591._comment create mode 100644 doc/bugs/Android_app_permission_denial_on_startup.mdwn create mode 100644 doc/bugs/Android_app_permission_denial_on_startup/comment_10_dc06737997c8883ef0a12dbecd9ac30f._comment create mode 100644 doc/bugs/Android_app_permission_denial_on_startup/comment_11_b444cd6717658116533745c51481dd3d._comment create mode 100644 doc/bugs/Android_app_permission_denial_on_startup/comment_12_66181f34ed7496d1f6601b39e5ae3c65._comment create mode 100644 doc/bugs/Android_app_permission_denial_on_startup/comment_1_ddf5761bf14de30ac97030ad338601ae._comment create mode 100644 doc/bugs/Android_app_permission_denial_on_startup/comment_2_8b9fafa73ebf5f803c7da9531cfb5b34._comment create mode 100644 doc/bugs/Android_app_permission_denial_on_startup/comment_3_58501bb043b4c5836d7472ffd6baa72c._comment create mode 100644 doc/bugs/Android_app_permission_denial_on_startup/comment_4_d3a04dc7bbc1816cccc8d85c73ffb689._comment create mode 100644 doc/bugs/Android_app_permission_denial_on_startup/comment_5_eeabbc0cc434ed84c36a3f4e03fcef36._comment create mode 100644 doc/bugs/Android_app_permission_denial_on_startup/comment_6_4203b496bee1bdd424466ed63b5d31cf._comment create mode 100644 doc/bugs/Android_app_permission_denial_on_startup/comment_7_74373eb2cc46b76659e3c463d6682d15._comment create mode 100644 doc/bugs/Android_app_permission_denial_on_startup/comment_8_0923d2a09df01d152ec4784c92689c96._comment create mode 100644 doc/bugs/Android_app_permission_denial_on_startup/comment_9_b60928e54a5b620899cf29820b9b8e70._comment create mode 100644 doc/bugs/Android_daily_build_missing_webapp.mdwn create mode 100644 doc/bugs/Annex_thinks_file_exists_afer_being_dropped.mdwn create mode 100644 doc/bugs/Annex_thinks_file_exists_afer_being_dropped/comment_1_1d100441fd1ef529eb854b350fece9ee._comment create mode 100644 doc/bugs/Annex_thinks_file_exists_afer_being_dropped/comment_2_166c459c2b27859cf457e17da685fe75._comment create mode 100644 doc/bugs/Annex_thinks_file_exists_afer_being_dropped/comment_3_9d985b6e7973bfaaf8b4f5349d8c13ee._comment create mode 100644 doc/bugs/Annex_thinks_file_exists_afer_being_dropped/comment_4_3e084cff454b95c7170c0225a53f0c30._comment create mode 100644 doc/bugs/Assistant_does_not_actually_check_newly_annex-added_files_into_git_until_daily_sanity_check.mdwn create mode 100644 doc/bugs/Assistant_doesn__39__t_actually_sync_file_contents_by_default.mdwn create mode 100644 doc/bugs/Assistant_doesn__39__t_actually_sync_file_contents_by_default/comment_1_8577fdaa4d49e6241c4372b159694c9c._comment create mode 100644 doc/bugs/Assistant_doesn__39__t_actually_sync_file_contents_by_default/comment_2_027521e48283c68b39315bb8213f6e45._comment create mode 100644 doc/bugs/Assistant_doesn__39__t_actually_sync_file_contents_by_default/comment_3_fd8f6938596aace60b04fb35c4069e37._comment create mode 100644 doc/bugs/Assistant_doesn__39__t_actually_sync_file_contents_by_default/comment_4_ca908021ab5a2a50fd0d4a7e8d12498f._comment create mode 100644 doc/bugs/Assistant_doesn__39__t_actually_sync_file_contents_by_default/comment_5_73532556cfc354ad5f37a3f3a048fb32._comment create mode 100644 doc/bugs/Assistant_doesn__39__t_actually_sync_file_contents_by_default/comment_6_ced397b9e6119a0798a282ee07e885df._comment create mode 100644 doc/bugs/Assistant_doesn__39__t_actually_sync_file_contents_by_default/comment_7_8acb66850e5db8337cf3f2b2dd236ccc._comment create mode 100644 doc/bugs/Assistant_doesn__39__t_actually_sync_file_contents_by_default/comment_8_7eb530851ae6fa1a69813725c4e8fcec._comment create mode 100644 doc/bugs/Assistant_doesn__39__t_actually_sync_file_contents_by_default/comment_9_c7d51a26e1febc3894d02546940d64e5._comment create mode 100644 doc/bugs/Assistant_dropping_files_it_has_just_transferred_elsewhere_again.mdwn create mode 100644 doc/bugs/Assistant_dropping_from_backup_repo.mdwn create mode 100644 doc/bugs/Assistant_dropping_from_backup_repo/comment_1_c13d86fb2541676ee4ca1446b99e0e68._comment create mode 100644 doc/bugs/Assistant_enters_eternal_loop_and_eats_up_all_of_RAM_after_X_restart.mdwn create mode 100644 doc/bugs/Assistant_uses_obsolete_GDU_volume_monitor.mdwn create mode 100644 doc/bugs/Assistant_uses_obsolete_GDU_volume_monitor/comment_10_0e1db417a5815ea903c1f7ccd07308c4._comment create mode 100644 doc/bugs/Assistant_uses_obsolete_GDU_volume_monitor/comment_1_28b0cfcba8902c9c16dbe6c4b07984c4._comment create mode 100644 doc/bugs/Assistant_uses_obsolete_GDU_volume_monitor/comment_2_952b3f78da756ff5f89235db94bec67f._comment create mode 100644 doc/bugs/Assistant_uses_obsolete_GDU_volume_monitor/comment_3_d86aba42d014c4b4f708dcb5fe86e055._comment create mode 100644 doc/bugs/Assistant_uses_obsolete_GDU_volume_monitor/comment_4_9aaf296ef53da317d6dc6728705d5c56._comment create mode 100644 doc/bugs/Assistant_uses_obsolete_GDU_volume_monitor/comment_5_0d5f8a05a1505660f7ff1bc4ac6ff271._comment create mode 100644 doc/bugs/Assistant_uses_obsolete_GDU_volume_monitor/comment_6_3dfdfd49597c85575cb689adb70d2de6._comment create mode 100644 doc/bugs/Assistant_uses_obsolete_GDU_volume_monitor/comment_7_943a446c60ed9d7d4f240ba7f00fe925._comment create mode 100644 doc/bugs/Assistant_uses_obsolete_GDU_volume_monitor/comment_8_9563859850fb40b1cc2c20c516c12960._comment create mode 100644 doc/bugs/Assistant_uses_obsolete_GDU_volume_monitor/comment_9_cf6221c585ee3dbf039bdaea71842d9b._comment create mode 100644 doc/bugs/Browser_fails_to_launch_on_Android___39__git_annex_webapp__39__.mdwn create mode 100644 doc/bugs/Browser_fails_to_launch_on_Android___39__git_annex_webapp__39__/comment_1_173393b0b3d2d8c622c0d8a2eaace421._comment create mode 100644 doc/bugs/Build-depends_needs___39__hxt__39___added_-_3.20121127.mdwn create mode 100644 doc/bugs/Build_error_on_Mac_OSX_10.6.mdwn create mode 100644 doc/bugs/Build_failure_at_commit_1efe4f3.mdwn create mode 100644 doc/bugs/Building_fails:_Could_not_find_module___96__Text.Blaze__39__.mdwn create mode 100644 doc/bugs/Building_fails:_Not_in_scope:___96__myHomeDir__39___.mdwn create mode 100644 doc/bugs/Building_fails:__Could_not_find_module___96__Data.XML.Types__39__.mdwn create mode 100644 doc/bugs/Building_fails:___Not_in_scope:_type_constructor_or_class___96__Html__39__.mdwn create mode 100644 doc/bugs/Building_in_cabal_using_--bindir___126____47__bin_breaks_the_desktop_link.mdwn create mode 100644 doc/bugs/Building_in_cabal_using_--bindir___126____47__bin_breaks_the_desktop_link/comment_1_c0f0a2878070ed86900815c6b6a5fa5e._comment create mode 100644 doc/bugs/Building_in_cabal_using_--bindir___126____47__bin_breaks_the_desktop_link/comment_2_53f2de3d3993821d8502fd08a0fcce12._comment create mode 100644 doc/bugs/Cabal_cannot_solve_dependencies.mdwn create mode 100644 doc/bugs/Cabal_cannot_solve_dependencies/comment_1_1d41ac79867226dcb71f1c7b38da062d._comment create mode 100644 doc/bugs/Cabal_cannot_solve_dependencies/comment_2_50e72633a4462f6f6eb33d57b137fdcc._comment create mode 100644 doc/bugs/Cabal_cannot_solve_dependencies/comment_3_886f2d1f7c47a3973b8dc7d7c412289a._comment create mode 100644 doc/bugs/Cabal_dependency_monadIO_missing.mdwn create mode 100644 doc/bugs/Cabal_dependency_monadIO_missing/comment_1_14be660aa57fadec0d81b32a8b52c66f._comment create mode 100644 doc/bugs/Cabal_dependency_monadIO_missing/comment_2_4f4d8e1e00a2a4f7e8a8ab082e16adac._comment create mode 100644 doc/bugs/Calls_to_rsync_don__39__t_always_use__annex-rsync-options.mdwn create mode 100644 doc/bugs/Can__39__t___34__git-annex_get__34___with_3.20111203.mdwn create mode 100644 doc/bugs/Can__39__t_access_files_from___39__Removable_drive__39___repo_even_if_set_as_client.mdwn create mode 100644 doc/bugs/Can__39__t_access_files_from___39__Removable_drive__39___repo_even_if_set_as_client/comment_1_25eb2d7d0a9cdd1c55df0cec68472723._comment create mode 100644 doc/bugs/Can__39__t_access_files_from___39__Removable_drive__39___repo_even_if_set_as_client/comment_2_9e9b96e5113a50533251e946c2560d81._comment create mode 100644 doc/bugs/Can__39__t_access_files_from___39__Removable_drive__39___repo_even_if_set_as_client/comment_3_6b091198ddd6ed709b076df1296aeb77._comment create mode 100644 doc/bugs/Can__39__t_access_files_from___39__Removable_drive__39___repo_even_if_set_as_client/comment_4_118b588685b535cca4c02eb6ef297c67._comment create mode 100644 doc/bugs/Can__39__t_access_files_from___39__Removable_drive__39___repo_even_if_set_as_client/comment_5_5cead277493e1c020e16be6f9245fe33._comment create mode 100644 doc/bugs/Can__39__t_access_files_from___39__Removable_drive__39___repo_even_if_set_as_client/comment_6_0f135f97c2808dce094628dc6608e617._comment create mode 100644 doc/bugs/Can__39__t_access_files_from___39__Removable_drive__39___repo_even_if_set_as_client/comment_7_1d6f47f9e6cf935f19d68af6d5aa92fa._comment create mode 100644 doc/bugs/Can__39__t_access_files_from___39__Removable_drive__39___repo_even_if_set_as_client/comment_8_c5758fdb32348b9cd804ff17d27864e1._comment create mode 100644 doc/bugs/Can__39__t_add_a_git_repo_to_git_annex:___34__Invalid_path_repo__47__.git__47__X__34___for_many_X.mdwn create mode 100644 doc/bugs/Can__39__t_add_a_git_repo_to_git_annex:___34__Invalid_path_repo__47__.git__47__X__34___for_many_X/comment_1_7f54e24c8e721d69bdb1e5a4181641b8._comment create mode 100644 doc/bugs/Can__39__t_add_a_git_repo_to_git_annex:___34__Invalid_path_repo__47__.git__47__X__34___for_many_X/comment_2_6e91bc254f79ccf80d385ba7d35ffa9c._comment create mode 100644 doc/bugs/Can__39__t_add_a_git_repo_to_git_annex:___34__Invalid_path_repo__47__.git__47__X__34___for_many_X/comment_3_4cf34da6050dd96f94ffc3652aa39715._comment create mode 100644 doc/bugs/Can__39__t_add_a_git_repo_to_git_annex:___34__Invalid_path_repo__47__.git__47__X__34___for_many_X/comment_4_cafcc24e98a89f10adaed5e09f75b659._comment create mode 100644 doc/bugs/Can__39__t_clone_on_Windows_because_some_filenames_have_a_colon_in_them.mdwn create mode 100644 doc/bugs/Can__39__t_clone_on_Windows_because_some_filenames_have_a_colon_in_them/comment_1_5fc1347f4bcc13c9f8dbc5ecd4847fc7._comment create mode 100644 doc/bugs/Can__39__t_clone_on_Windows_because_some_filenames_have_a_colon_in_them/comment_2_38696178e658d1d32deec37dbea66a3d._comment create mode 100644 doc/bugs/Can__39__t_rename___34__here__34___repository.mdwn create mode 100644 doc/bugs/Can__39__t_set_repositories_directory.mdwn create mode 100644 doc/bugs/Can__39__t_set_repositories_directory/comment_1_beb5d5b66a8d0fab12be44a7d877e9b0._comment create mode 100644 doc/bugs/Can__39__t_set_repositories_directory/comment_2_366aa798a5e55350d32b63b31c19112b._comment create mode 100644 doc/bugs/Can__39__t_set_repositories_directory/comment_3_812554d58ad9274a50b2a33d5f4d2ec3._comment create mode 100644 doc/bugs/Can__39__t_set_repositories_directory/comment_4_bec5f147441ad18c97845b44c90c728b._comment create mode 100644 doc/bugs/Can__39__t_transfer_files_to_rsync_remote_with_encryption__61__shared.mdwn create mode 100644 doc/bugs/Can__39__t_transfer_files_to_rsync_remote_with_encryption__61__shared/comment_1_ca7ec2041bbec330476fb040b1e66a92._comment create mode 100644 doc/bugs/Can__39__t_transfer_files_to_rsync_remote_with_encryption__61__shared/comment_2_c476847665a5320214721497d8fad15b._comment create mode 100644 doc/bugs/Cannot_build_the_latest_with_GHC_7.6.1.mdwn create mode 100644 doc/bugs/Cannot_build_the_latest_with_GHC_7.6.1/comment_1_b25859c159d62f2e92b92f505535131b._comment create mode 100644 doc/bugs/Cannot_build_the_latest_with_GHC_7.6.1/comment_2_4c9eab9120718457fdc1ae9051e44bca._comment create mode 100644 doc/bugs/Cannot_build_the_latest_with_GHC_7.6.1/comment_3_61aec9801e1f76db4a286536ffacc3ed._comment create mode 100644 doc/bugs/Cannot_build_the_latest_with_GHC_7.6.1/comment_4_6381ff0ea419831d9bbed27511cad1e9._comment create mode 100644 doc/bugs/Cannot_clone_an_annex.mdwn create mode 100644 doc/bugs/Cannot_clone_an_annex/comment_1_b40a2652361a79c6c6eab0fc21be8e46._comment create mode 100644 doc/bugs/Cannot_copy_to_a_git-annex_remote.mdwn create mode 100644 doc/bugs/Cannot_copy_to_a_git-annex_remote/comment_10_258a376cff4c62bc4be919322bb1bd88._comment create mode 100644 doc/bugs/Cannot_copy_to_a_git-annex_remote/comment_10_d9b830a1fdea8760cb7da1d36b3cd34d._comment create mode 100644 doc/bugs/Cannot_copy_to_a_git-annex_remote/comment_1_09d76e5f9480b9a35644a8f08790cd97._comment create mode 100644 doc/bugs/Cannot_copy_to_a_git-annex_remote/comment_2_7b586c705a937d09a1b44bd6af2d4686._comment create mode 100644 doc/bugs/Cannot_copy_to_a_git-annex_remote/comment_3_07dbd8f64982f1921077e23f468122cf._comment create mode 100644 doc/bugs/Cannot_copy_to_a_git-annex_remote/comment_4_926fd494f0b27103a99083cd5d0702d5._comment create mode 100644 doc/bugs/Cannot_copy_to_a_git-annex_remote/comment_5_80444a509cc340f5eb3cd08b193fd389._comment create mode 100644 doc/bugs/Cannot_copy_to_a_git-annex_remote/comment_6_4c6b99cd67b4aa742da5101fb1b379f7._comment create mode 100644 doc/bugs/Cannot_copy_to_a_git-annex_remote/comment_8_f45cdd2b6acc5f458b67539fced0e529._comment create mode 100644 doc/bugs/Cannot_copy_to_a_git-annex_remote/comment_9_5a455dd14fb9d3ff408bb3f81e366c38._comment create mode 100644 doc/bugs/Cannot_sync_repos_setup_using_webapp:___34__git-annex-shell:_Only_allowed_to_access___126____47__foo_not___126____47__bar__47____34__.mdwn create mode 100644 doc/bugs/Cannot_sync_repos_setup_using_webapp:___34__git-annex-shell:_Only_allowed_to_access___126____47__foo_not___126____47__bar__47____34__/comment_1_6f7b5c164ff64f00b8814b2ee334709f._comment create mode 100644 doc/bugs/Cannot_sync_repos_setup_using_webapp:___34__git-annex-shell:_Only_allowed_to_access___126____47__foo_not___126____47__bar__47____34__/comment_2_807ef1250237bf4426e3a24c1f9ba357._comment create mode 100644 doc/bugs/Committer_crashed.mdwn create mode 100644 doc/bugs/Compile_needs_more_than_1.5gb_of_memory.mdwn create mode 100644 doc/bugs/Compile_needs_more_than_1.5gb_of_memory/comment_1_0806b5132c55d7a5a17fbdad7e3f2291._comment create mode 100644 doc/bugs/Complete_failure_trying_to_unannex_a_large_annex.mdwn create mode 100644 doc/bugs/Complete_failure_trying_to_unannex_a_large_annex/comment_1_1c202695ab7fe62cdc8770e1fb428d0c._comment create mode 100644 doc/bugs/ControlPath_too_long_for_Unix_domain_socket.mdwn create mode 100644 doc/bugs/ControlPath_too_long_for_Unix_domain_socket/comment_1_60f58e205604eebe668b1e05dcfbf9a7._comment create mode 100644 doc/bugs/Could_not_find_module_Data.Default.mdwn create mode 100644 doc/bugs/Could_not_resolve_dependencies.mdwn create mode 100644 doc/bugs/Crash_trying_to_sync_with_a_repo_over_ssh.mdwn create mode 100644 doc/bugs/Crash_trying_to_sync_with_a_repo_over_ssh/comment_1_9705f295ad8101f3f0ede18e590b56ef._comment create mode 100644 doc/bugs/Crash_trying_to_sync_with_a_repo_over_ssh/comment_2_0d751d81ac618f8d7e3f1dd20c830542._comment create mode 100644 doc/bugs/Crash_when_adding_jabber_account_.mdwn create mode 100644 doc/bugs/Crash_when_adding_jabber_account_/comment_1_2dc61ebcfa8919fb839656999c155c52._comment create mode 100644 doc/bugs/Crash_when_adding_jabber_account_/comment_2_e49af3b8a937d82eda1509b6f67b21d4._comment create mode 100644 doc/bugs/Crash_when_adding_jabber_account_/comment_3_e59f8813bf1a7c4e3c8c120fe82348b9._comment create mode 100644 doc/bugs/Crash_when_adding_jabber_account_/comment_4_716ac138cb69eecd0fb586699b4aeb2a._comment create mode 100644 doc/bugs/Creating_an_S3_repository_with_an_invalid_name_throws_an_exception.mdwn create mode 100644 doc/bugs/Creating_an_encrypted_S3_does_not_check_for_presence_of_GPG.mdwn create mode 100644 doc/bugs/Creating_second_repository_leads_to_wrong_ip___40__using_git-annex_webapp_--listen__41__.mdwn create mode 100644 doc/bugs/DS__95__Store_not_gitignored.mdwn create mode 100644 doc/bugs/DS__95__Store_not_gitignored/comment_1_b93ac0ea3be82c361ceb4352e742ba39._comment create mode 100644 doc/bugs/DS__95__Store_not_gitignored/comment_2_4136e1f4aba7aa7562dafcf6a213e10c._comment create mode 100644 doc/bugs/Deasn__39__t_clean_up_ssh_keys_after_removing_remote_repo.mdwn create mode 100644 doc/bugs/Deasn__39__t_clean_up_ssh_keys_after_removing_remote_repo/comment_1_88fbf70eae48484988dbb433a437c717._comment create mode 100644 doc/bugs/Detection_assumes_that_shell_is_bash.mdwn create mode 100644 doc/bugs/Difficult_to_troubleshoot_XMPP_login_failures.mdwn create mode 100644 doc/bugs/Difficult_to_troubleshoot_XMPP_login_failures/comment_1_4205bccf515169031e4a9ed8e905262c._comment create mode 100644 doc/bugs/Direct_mode_keeps_re-checksuming_duplicated_files.mdwn create mode 100644 doc/bugs/Direct_mode_keeps_re-checksuming_duplicated_files/comment_1_cb10385a4f046bfe676720ded3409379._comment create mode 100644 doc/bugs/Direct_mode_keeps_re-checksuming_duplicated_files/comment_2_4bcf1a897181e40c9c8969d597a844f0._comment create mode 100644 doc/bugs/Direct_mode_keeps_re-checksuming_duplicated_files/comment_3_6a6d22d218f036c9977072973ed99aa8._comment create mode 100644 doc/bugs/Direct_mode_keeps_re-checksuming_duplicated_files/comment_4_eaa7ffb3a1d9ffd6d89de301bd2cd5b2._comment create mode 100644 doc/bugs/Direct_mode_repositories_end_up_with_unstaged_changes.mdwn create mode 100644 doc/bugs/Direct_mode_repositories_end_up_with_unstaged_changes/comment_1_300a2b246182be3079db20a7e3322261._comment create mode 100644 doc/bugs/Direct_mode_repositories_still_use_symlinks_sometimes.mdwn create mode 100644 doc/bugs/Disconcerting_warning_from_git-annex.mdwn create mode 100644 doc/bugs/Disconcerting_warning_from_git-annex/comment_1_58cebd377bfdf247b6c4fee27a3ba461._comment create mode 100644 doc/bugs/Disconcerting_warning_from_git-annex/comment_2_dc7407044d4c739d05248300c58d8ef2._comment create mode 100644 doc/bugs/Displayed_copy_speed_is_wrong.mdwn create mode 100644 doc/bugs/Displayed_copy_speed_is_wrong/comment_1_74de3091e8bfd7acd6795e61f39f07c6._comment create mode 100644 doc/bugs/Displayed_copy_speed_is_wrong/comment_2_8b240de1d5ae9229fa2d77d1cc15a552._comment create mode 100644 doc/bugs/Enable__47__paus_syncing_to_remote_ssh_server_with_multiple_directories.mdwn create mode 100644 doc/bugs/Enable__47__paus_syncing_to_remote_ssh_server_with_multiple_directories/comment_1_e8affeca873c2ef73255f8f77e0ac16f._comment create mode 100644 doc/bugs/Error___39__get__39__ting_files_from_rsync_remote__44___versions_3.20120315_and_3.20120430.mdwn create mode 100644 doc/bugs/Error_creating_remote_repository_using_ssh_on_OSX.mdwn create mode 100644 doc/bugs/Error_creating_remote_repository_using_ssh_on_OSX/comment_1_559555934d79ae6be383063abcaae22e._comment create mode 100644 doc/bugs/Error_creating_remote_repository_using_ssh_on_OSX/comment_2_a9f4f9db042ab6f6c15d6954651971b2._comment create mode 100644 doc/bugs/Error_creating_remote_repository_using_ssh_on_OSX/comment_3_55a496d0a0be80ba723b17bf9faa3bc0._comment create mode 100644 doc/bugs/Error_when_dropping___34__hGetLine:_end_of_file__34__.mdwn create mode 100644 doc/bugs/Error_when_dropping___34__hGetLine:_end_of_file__34__/comment_10_8742f7ac27b5f4ad6261d04a174a691c._comment create mode 100644 doc/bugs/Error_when_dropping___34__hGetLine:_end_of_file__34__/comment_11_b8e720340000537de6713c49b7733b2f._comment create mode 100644 doc/bugs/Error_when_dropping___34__hGetLine:_end_of_file__34__/comment_1_489fa3a717519cd5d8b4c1a9d143d8c6._comment create mode 100644 doc/bugs/Error_when_dropping___34__hGetLine:_end_of_file__34__/comment_2_b0796d3b1913e1b6f7b34d75a591be42._comment create mode 100644 doc/bugs/Error_when_dropping___34__hGetLine:_end_of_file__34__/comment_3_d8ca17ccaa5ee48d590736af8e77d88a._comment create mode 100644 doc/bugs/Error_when_dropping___34__hGetLine:_end_of_file__34__/comment_4_aa7a690aaf75d21f52051a31d7fce70e._comment create mode 100644 doc/bugs/Error_when_dropping___34__hGetLine:_end_of_file__34__/comment_5_dc235dc2d024b7f340721bb578630e00._comment create mode 100644 doc/bugs/Error_when_dropping___34__hGetLine:_end_of_file__34__/comment_6_5d1e6ea5b5725c773acc6e288add812c._comment create mode 100644 doc/bugs/Error_when_dropping___34__hGetLine:_end_of_file__34__/comment_7_6389b4f03ebc916358bc6674398d70c4._comment create mode 100644 doc/bugs/Error_when_dropping___34__hGetLine:_end_of_file__34__/comment_8_bcacc9fb3751042968118ebe33802e27._comment create mode 100644 doc/bugs/Error_when_dropping___34__hGetLine:_end_of_file__34__/comment_9_6d4c9f0e133ebd94fc11346df446402e._comment create mode 100644 doc/bugs/Error_when_moving_annexed_file_to_a_.gitignored_location.mdwn create mode 100644 doc/bugs/Error_while_adding_a_file___34__createSymbolicLink:_already_exists__34__.mdwn create mode 100644 doc/bugs/Every_new_file_gets_symlinked_to_a_git_object.mdwn create mode 100644 doc/bugs/Every_new_file_gets_symlinked_to_a_git_object/comment_1_d4e7ed56b16494a95e6c904c746cc91f._comment create mode 100644 doc/bugs/Every_new_file_gets_symlinked_to_a_git_object/comment_2_656b2a2cc44e9102c86bdd57045549d5._comment create mode 100644 doc/bugs/Failed_to_make_repository___40__calling_nonexistant_shell__41__.mdwn create mode 100644 doc/bugs/Failed_to_make_repository___40__calling_nonexistant_shell__41__/comment_1_fb8a379ed7f4b88bd55245ce5b18042c._comment create mode 100644 doc/bugs/Fails_to_create_remote_repo_if_no_global_email_set.mdwn create mode 100644 doc/bugs/Files_disappear_from_locally_paired_annexes_when_edited.mdwn create mode 100644 doc/bugs/Files_disappear_from_locally_paired_annexes_when_edited/comment_1_bdc97db9dc9954331e4c400baf9e5541._comment create mode 100644 doc/bugs/Fix_for_opening_a_browser_on_a_mac___40__or_xdg-open_on_linux__47__bsd__63____41__.mdwn create mode 100644 doc/bugs/GIT_DIR_support_incomplete.mdwn create mode 100644 doc/bugs/GPG_passphrase_repeated_prompt.mdwn create mode 100644 doc/bugs/GPG_passphrase_repeated_prompt/comment_1_6ef1c9725befc84ad57bce196ef630ef._comment create mode 100644 doc/bugs/GPG_problem_on_Mac.mdwn create mode 100644 doc/bugs/GPG_problem_on_Mac/comment_1_9ccfa12e7a9569a7ae9a3b819917c275._comment create mode 100644 doc/bugs/GPG_problem_on_Mac/comment_2_a5e07131e2bc1a646c8439fc2506128b._comment create mode 100644 doc/bugs/GPG_problem_on_Mac/comment_3_388238360f2423f84881e904443efb86._comment create mode 100644 doc/bugs/Git_annexed_files_symlink_are_wrong_when_submodule_is_not_in_the_same_path.mdwn create mode 100644 doc/bugs/Glacier_remote_uploads_duplicates.mdwn create mode 100644 doc/bugs/Glacier_remote_uploads_duplicates/comment_1_8aef582a0f0d0c7f764b425fc45de3b4._comment create mode 100644 doc/bugs/Glacier_remote_uploads_duplicates/comment_2_150ce8b7c4424a83c4b1760da5a89d27._comment create mode 100644 doc/bugs/Glacier_remote_uploads_duplicates/comment_3_718af5048c5f894eee134547a2e0a644._comment create mode 100644 doc/bugs/Glacier_remote_uploads_duplicates/comment_4_184ad0f8c2847309632f8c18b918cd42._comment create mode 100644 doc/bugs/Glacier_remote_uploads_duplicates/comment_5_6980a912d3582c2f2511e4827e9e76b3._comment create mode 100644 doc/bugs/Glacier_remote_uploads_duplicates/comment_6_feea067d6856af2840604782b29af86a._comment create mode 100644 doc/bugs/Glacier_remote_uploads_duplicates/comment_7_e96187bad3dae2f5f95118f6df87a1ec._comment create mode 100644 doc/bugs/Handling_of_files_inside_and_outside_archive_directory_at_the_same_time.mdwn create mode 100644 doc/bugs/Handling_of_files_inside_and_outside_archive_directory_at_the_same_time/comment_1_e8bb3d6a2318402b985caed08282d473._comment create mode 100644 doc/bugs/Handling_of_files_inside_and_outside_archive_directory_at_the_same_time/comment_2_ead9fa75a12ef36be9a92637b144e74f._comment create mode 100644 doc/bugs/Hanging_on_install_on_Mountain_lion.mdwn create mode 100644 doc/bugs/Hanging_on_install_on_Mountain_lion/comment_10_f57ff027b19ca16e2ecf1fc6aee9ef4a._comment create mode 100644 doc/bugs/Hanging_on_install_on_Mountain_lion/comment_11_2ff78d2090d0fd3418ab50b27c6028ce._comment create mode 100644 doc/bugs/Hanging_on_install_on_Mountain_lion/comment_1_523d3c0c71f80536850a001b90fd0e9e._comment create mode 100644 doc/bugs/Hanging_on_install_on_Mountain_lion/comment_2_6c360c64093b016c2150206dc3ad1709._comment create mode 100644 doc/bugs/Hanging_on_install_on_Mountain_lion/comment_3_7b77fd9b7dc236c345f2f6149c8138ee._comment create mode 100644 doc/bugs/Hanging_on_install_on_Mountain_lion/comment_5_08289596445d7588e43d35490fbfe5f4._comment create mode 100644 doc/bugs/Hanging_on_install_on_Mountain_lion/comment_5_2a336fe7b8aed07cbdaa868bd34078f9._comment create mode 100644 doc/bugs/Hanging_on_install_on_Mountain_lion/comment_6_ea7a40c3b6748738421aed00a6f7ca10._comment create mode 100644 doc/bugs/Hanging_on_install_on_Mountain_lion/comment_7_00962da9288976f8a48d0cbc08e1d9e2._comment create mode 100644 doc/bugs/Hanging_on_install_on_Mountain_lion/comment_8_5d53d23e529f33f6e7deb10643831613._comment create mode 100644 doc/bugs/Hanging_on_install_on_Mountain_lion/comment_9_f00c8761e3184975b6645c0c3e241365._comment create mode 100644 doc/bugs/Hangs_on_creating_repository_when_using_--listen.mdwn create mode 100644 doc/bugs/Hangs_on_creating_repository_when_using_--listen/comment_1_8cbe786de8cf8b407418149b9c811aab._comment create mode 100644 doc/bugs/Hard_links_not_synced_in_direct_mode.mdwn create mode 100644 doc/bugs/Hard_links_not_synced_in_direct_mode/comment_1_aaa781664ae0c62c4f6530cb075ed367._comment create mode 100644 doc/bugs/Hard_links_not_synced_in_direct_mode/comment_2_213aa10909d1fd0f20ed078a7ed93e79._comment create mode 100644 doc/bugs/Hard_links_not_synced_in_direct_mode/comment_3_e6b783d9aaae20c0d35e9888d878716a._comment create mode 100644 doc/bugs/Hard_links_not_synced_in_direct_mode/comment_4_b008ae7b1cf8685d92c9a87a7609de1e._comment create mode 100644 doc/bugs/Hard_links_not_synced_in_direct_mode/comment_5_949c891209713a2c0a5e66af11ed4c79._comment create mode 100644 doc/bugs/Huge_annex_out_of_memory_on_switch_to_indirect_mode_and_status.mdwn create mode 100644 doc/bugs/Huge_annex_out_of_memory_on_switch_to_indirect_mode_and_status/comment_1_94c678e1348280a96f11d7456c240d3a._comment create mode 100644 doc/bugs/Huge_annex_out_of_memory_on_switch_to_indirect_mode_and_status/comment_2_09450d58df2373174a1f0d90b08e9eb3._comment create mode 100644 doc/bugs/Incorrect_version_on_64_Standalone_Build.mdwn create mode 100644 doc/bugs/Incorrect_version_on_64_Standalone_Build/comment_1_1964e4cad33a9f98b2eedbf095e899ff._comment create mode 100644 doc/bugs/Install_of_git-annex-3.20121112_fails.mdwn create mode 100644 doc/bugs/Install_of_git-annex-3.20121112_fails/comment_1_80fc80151d4390bd8a4332f30723962e._comment create mode 100644 doc/bugs/Install_of_git-annex-3.20121112_fails/comment_2_2613320a41a74dc757a3277c8c328bd0._comment create mode 100644 doc/bugs/Install_of_git-annex-3.20121112_fails/comment_3_c364764d0c56e8dc3cac276905d99841._comment create mode 100644 doc/bugs/Install_of_git-annex-3.20121112_fails/comment_4_f1057340dfa978071d3bbc9e2af1e612._comment create mode 100644 doc/bugs/Install_of_git-annex-3.20121112_fails/comment_5_9007b1a3abd647945604968db19cb841._comment create mode 100644 doc/bugs/Install_of_git-annex-3.20121112_fails/comment_6_0bb3ac5375f29ce9d3d0be93879267e3._comment create mode 100644 doc/bugs/Install_of_git-annex-3.20121112_fails/comment_7_ae4443b8cd069080d1f77fca16aa8b04._comment create mode 100644 doc/bugs/Internal_Server_Error:_Unknown_UUID.mdwn create mode 100644 doc/bugs/Internal_Server_Error:_Unknown_UUID/comment_1_f42f703a5d267557abf5e932f0890d4a._comment create mode 100644 doc/bugs/Internal_Server_Error:_Unknown_UUID/comment_2_eb1999f99c5babf3fcb1ff5d72ea6db6._comment create mode 100644 doc/bugs/Internal_Server_Error:_Unknown_UUID/comment_3_bda72b0d615843d18d6ef21f833432a8._comment create mode 100644 doc/bugs/Internal_Server_Error:_Unknown_UUID/comment_4_651440cda405ad40a04479f5d87d581e._comment create mode 100644 doc/bugs/Internal_Server_Error:_Unknown_UUID/comment_5_21fa189b631c246ac5df16a49c3c0178._comment create mode 100644 doc/bugs/Internal_Server_Error:_Unknown_UUID/comment_6_1f712693d2ded5abceb869fdb7f47ef3._comment create mode 100644 doc/bugs/Internal_Server_Error:_Unknown_UUID/comment_7_7a5ead0ce5c9429d4723ccce4f6a6d6c._comment create mode 100644 doc/bugs/Internal_Server_Error:_Unknown_UUID/comment_8_a4683fd73ae452a9cd7f61d9930f6266._comment create mode 100644 doc/bugs/Internal_Server_Error_unknown_UUID__59___cannot_modify.mdwn create mode 100644 doc/bugs/Internal_Server_Error_when_adding_an_uncrypted_box.com_repo_after_deleted_an_encrypted_one..mdwn create mode 100644 doc/bugs/Internal_server_error_adding_USB_drive_on_OS_X.mdwn create mode 100644 doc/bugs/Internal_server_error_adding_USB_drive_on_OS_X/comment_1_b2ef077d87a9da624f20649c21401b5b._comment create mode 100644 doc/bugs/Internal_server_error_adding_USB_drive_on_OS_X/comment_2_ef849e25b0264808bff800d9d3836119._comment create mode 100644 doc/bugs/Internal_server_error_adding_USB_drive_on_OS_X/comment_3_ae3cbd0eb69cbeb9b349e0060d056d43._comment create mode 100644 doc/bugs/Internal_server_error_adding_USB_drive_on_OS_X/comment_4_0ff2897805928b14829b7b369a3aed91._comment create mode 100644 doc/bugs/Internal_server_error_adding_USB_drive_on_OS_X/comment_5_414a45573aeb5201f4d80433955669d5._comment create mode 100644 doc/bugs/Interrupted_switch_to_direct_mode_can_cause_all_following_switches_to_fail.mdwn create mode 100644 doc/bugs/Is_there_any_way_to_rate_limit_uploads_to_an_S3_backend__63__.mdwn create mode 100644 doc/bugs/Is_there_any_way_to_rate_limit_uploads_to_an_S3_backend__63__/comment_1_ef97e735ce308f7bcc03f5d9fda588bf._comment create mode 100644 doc/bugs/Is_there_any_way_to_rate_limit_uploads_to_an_S3_backend__63__/comment_2_539b89de8743e435386b86119d1e982f._comment create mode 100644 doc/bugs/Issue_on_OSX_with_some_system_limits.mdwn create mode 100644 doc/bugs/Issue_on_OSX_with_some_system_limits/comment_1_5fc1eedb5231edc37c87a2d9b91313b9._comment create mode 100644 doc/bugs/Issue_on_OSX_with_some_system_limits/comment_2_b14e697c211843163285aaa8de5bf4c6._comment create mode 100644 doc/bugs/Issue_on_OSX_with_some_system_limits/comment_3_18ddf8b5934dd6fb1676cd6adc7d103b._comment create mode 100644 doc/bugs/Issue_on_OSX_with_some_system_limits/comment_4_c25a8eb369e546f65e1a72d89f43066f._comment create mode 100644 doc/bugs/Issue_on_OSX_with_some_system_limits/comment_5_6407a3e7aa0316cba2994bfef0e3c633._comment create mode 100644 doc/bugs/Issue_on_OSX_with_some_system_limits/comment_6_f01887695e8b8386e125464c6d401565._comment create mode 100644 doc/bugs/Issue_on_OSX_with_some_system_limits/comment_7_c7776d5b2d073e0d2ae36515185c25aa._comment create mode 100644 doc/bugs/It_is_very_easy_to_turn_git-annex_into_a_zombie.mdwn create mode 100644 doc/bugs/It_is_very_easy_to_turn_git-annex_into_a_zombie/comment_1_d5fba6c061fb21795021ea83070dbfa2._comment create mode 100644 doc/bugs/It_is_very_easy_to_turn_git-annex_into_a_zombie/comment_2_12cba707239018989e8d5b6f456fa754._comment create mode 100644 doc/bugs/JSON_output_broken_with___34__git_annex_sync__34__.mdwn create mode 100644 doc/bugs/JSON_output_broken_with___34__git_annex_sync__34__/comment_1_380a49b3c132f9f529729a1cb5a69621._comment create mode 100644 doc/bugs/JSON_output_broken_with___34__git_annex_sync__34__/comment_2_282f5f89fb4a46e1fad0980e0b2994a0._comment create mode 100644 doc/bugs/JSON_output_broken_with___34__git_annex_sync__34__/comment_3_7ff98958146b7f6396226bdd878ec86e._comment create mode 100644 doc/bugs/JSON_output_broken_with___34__git_annex_sync__34__/comment_4_f9e460a09e7e5f53c16c20ded2649201._comment create mode 100644 doc/bugs/Killing_the_assistant_daemon_leaves_ssh_mux_sessions_behind.mdwn create mode 100644 doc/bugs/Killing_the_assistant_daemon_leaves_ssh_mux_sessions_behind/comment_1_17879b98a5e79ace03b543064751e46e._comment create mode 100644 doc/bugs/Killing_the_assistant_daemon_leaves_ssh_mux_sessions_behind/comment_2_2dc877e281750004b16619ea7b931160._comment create mode 100644 doc/bugs/Large_unannex_operations_result_in_stale_symlinks_and_data_loss.mdwn create mode 100644 doc/bugs/Large_unannex_operations_result_in_stale_symlinks_and_data_loss/comment_1_fbb410a54bb0bd82d0953ef58a88600e._comment create mode 100644 doc/bugs/Large_unannex_operations_result_in_stale_symlinks_and_data_loss/comment_2_8007c9ba42a951a4426255ec3c37d961._comment create mode 100644 doc/bugs/Large_unannex_operations_result_in_stale_symlinks_and_data_loss/comment_3_73ecd4cb8ee58a8dfe7cab0e893dbe5b._comment create mode 100644 doc/bugs/Large_unannex_operations_result_in_stale_symlinks_and_data_loss/comment_4_e8a10886a564f35414c30a04335d9d32._comment create mode 100644 doc/bugs/Large_unannex_operations_result_in_stale_symlinks_and_data_loss/comment_5_6a318edfe45c80343d017dc7b4837acb._comment create mode 100644 doc/bugs/Large_unannex_operations_result_in_stale_symlinks_and_data_loss/comment_6_f7a1d9f9d40aff531d873a95d2196edd._comment create mode 100644 doc/bugs/Large_unannex_operations_result_in_stale_symlinks_and_data_loss/comment_7_1724ffdf986301bf37ef7a6d16b6ea8a._comment create mode 100644 doc/bugs/Large_unannex_operations_result_in_stale_symlinks_and_data_loss/comment_8_5470e2f50e6506139ecb1b342371c509._comment create mode 100644 doc/bugs/Last_two_versions_didn__39__t_show_up_on_hackage.mdwn create mode 100644 doc/bugs/Last_two_versions_didn__39__t_show_up_on_hackage/comment_1_74b56dea2100450e322e726bb55bb310._comment create mode 100644 doc/bugs/Linux_stand_alone_build_20130723_breaks_support_for_glibc_2.13_debian_stable.txt create mode 100644 doc/bugs/Linux_stand_alone_build_20130723_breaks_support_for_glibc_2.13_debian_stable/comment_1_dc7f726a0b60f64392cbbd1b4317bab5._comment create mode 100644 doc/bugs/Linux_stand_alone_build_20130723_breaks_support_for_glibc_2.13_debian_stable/comment_2_4a0198d714bd3b52ba9baa68dc45f535._comment create mode 100644 doc/bugs/Local_files_not_found.mdwn create mode 100644 doc/bugs/Local_files_not_found/comment_1_5e1fcc0597594fa493ffa28aa32e1df8._comment create mode 100644 doc/bugs/Local_network___40__ssh__41___fails_to_pair__47__sync.mdwn create mode 100644 doc/bugs/Local_network___40__ssh__41___fails_to_pair__47__sync/comment_1_bab9cd5bdcffec3c48b9e8657cd9bbf7._comment create mode 100644 doc/bugs/Local_network___40__ssh__41___fails_to_pair__47__sync/comment_2_104898dce3c67c082a9f2b36e2f45ff8._comment create mode 100644 doc/bugs/Local_pairing_fails:_PairListener_crashed.mdwn create mode 100644 doc/bugs/Local_pairing_fails:_PairListener_crashed/comment_1_d9c5d2147cf6d8d8477eb13b72081d46._comment create mode 100644 doc/bugs/Local_pairing_fails:_PairListener_crashed/comment_2_60a21105145ac228f486bc4beb2ea54d._comment create mode 100644 doc/bugs/Lost_S3_Remote.mdwn create mode 100644 doc/bugs/Lost_S3_Remote/comment_1_6e80e6db6671581d471fc9a54181c04c._comment create mode 100644 doc/bugs/Lost_S3_Remote/comment_2_c99c65882a3924f4890e500f9492b442._comment create mode 100644 doc/bugs/Lost_S3_Remote/comment_3_1e434d5a20a692cd9dc7f6f8f20f30dd._comment create mode 100644 doc/bugs/Makefile_is_missing_dependancies.mdwn create mode 100644 doc/bugs/Makefile_is_missing_dependancies/comment_1_5a3da5f79c8563c7a450aa29728abe7c._comment create mode 100644 doc/bugs/Makefile_is_missing_dependancies/comment_2_416f12dbd0c2b841fac8164645b81df5._comment create mode 100644 doc/bugs/Makefile_is_missing_dependancies/comment_3_c38b6f4abc9b9ad413c3b83ca04386c3._comment create mode 100644 doc/bugs/Makefile_is_missing_dependancies/comment_4_cc13873175edf191047282700315beee._comment create mode 100644 doc/bugs/Makefile_is_missing_dependancies/comment_5_0a1c52e2c96d19b9c3eb7e99b8c2434f._comment create mode 100644 doc/bugs/Makefile_is_missing_dependancies/comment_6_24119fc5d5963ce9dd669f7dcf006859._comment create mode 100644 doc/bugs/Makefile_is_missing_dependancies/comment_7_96fd4725df4b54e670077a18d3ac4943._comment create mode 100644 doc/bugs/Makefile_is_missing_dependancies/comment_8_a3555e3286cdc2bfeb9cde0ff727ba74._comment create mode 100644 doc/bugs/Manual_content_mode_isn__39__t_manual.mdwn create mode 100644 doc/bugs/Manual_mode_weirdness.mdwn create mode 100644 doc/bugs/Manual_mode_weirdness/comment_1_f8ab3bac9e9a6768e5fd5a052f0d920f._comment create mode 100644 doc/bugs/Manual_mode_weirdness/comment_2_e810daa488fad32ca8bdaae620051da8._comment create mode 100644 doc/bugs/Missing_dependancy_in_commit_6cecc26206c4a539999b04664136c6f785211a41.mdwn create mode 100644 doc/bugs/Missing_repo_uuid_after_local_pairing_with_older_annex.mdwn create mode 100644 doc/bugs/Missing_repo_uuid_after_local_pairing_with_older_annex/comment_1_8229df64a872bee7590f75eb78f78c4a._comment create mode 100644 doc/bugs/Missing_repo_uuid_after_local_pairing_with_older_annex/comment_2_f37be896396915b1c85cff8811dceb4a._comment create mode 100644 doc/bugs/Missing_repo_uuid_after_local_pairing_with_older_annex/comment_3_df7fc1078059538a76f384a40541e91f._comment create mode 100644 doc/bugs/Missing_repo_uuid_after_local_pairing_with_older_annex/comment_4_70c444c61f41df2f59294c10f94f0c09._comment create mode 100644 doc/bugs/More_sync__39__ing_weirdness_with_the_assistant_branch_on_OSX.mdwn create mode 100644 doc/bugs/More_sync__39__ing_weirdness_with_the_assistant_branch_on_OSX/comment_1_377525e70640751e1ead445aeed15efa._comment create mode 100644 doc/bugs/Most_recent_git-annex_will_not_build_on_OpenIndiana.mdwn create mode 100644 doc/bugs/Most_recent_git-annex_will_not_build_on_OpenIndiana/comment_1_f3c336ecfee51e074ea3a9fc95301de5._comment create mode 100644 doc/bugs/Most_recent_git-annex_will_not_build_on_OpenIndiana/comment_2_102c0e998934e84deca92fd1c90145fa._comment create mode 100644 doc/bugs/Most_recent_git-annex_will_not_build_on_OpenIndiana/comment_3_1449dd796ce9f2209f085d4b017a5f33._comment create mode 100644 doc/bugs/Most_recent_git-annex_will_not_build_on_OpenIndiana/comment_4_c4aa8a4379b2c056ca9b7afcff412bbc._comment create mode 100644 doc/bugs/Most_recent_git-annex_will_not_build_on_OpenIndiana/comment_5_6ca4dd2ad51182edf7198f38b336b9b6._comment create mode 100644 doc/bugs/Name_scheme_does_not_follow_git__39__s_rules.mdwn create mode 100644 doc/bugs/Need_to_manually_install_c2hs_-_3.20121127_and_previous.mdwn create mode 100644 doc/bugs/No_easy_way_to_re-inject_a_file_into_an_annex.mdwn create mode 100644 doc/bugs/No_easy_way_to_re-inject_a_file_into_an_annex/comment_1_c871605e187f539f3bfe7478433e7fb5._comment create mode 100644 doc/bugs/No_easy_way_to_re-inject_a_file_into_an_annex/comment_2_e6f1e9eee8b8dfb60ca10c8cfd807ac9._comment create mode 100644 doc/bugs/No_easy_way_to_re-inject_a_file_into_an_annex/comment_3_be62be5fe819acc0cb8b878802decd46._comment create mode 100644 doc/bugs/No_easy_way_to_re-inject_a_file_into_an_annex/comment_4_480a4f72445a636eab1b1c0f816d365c._comment create mode 100644 doc/bugs/No_progress_bars_with_S3.mdwn create mode 100644 doc/bugs/No_progress_bars_with_S3/comment_1_33a601201a9fdd2357f1c03e32fa6b9c._comment create mode 100644 doc/bugs/No_progress_bars_with_S3/comment_2_52361805ced99c22d663b3b1e8a5b221._comment create mode 100644 doc/bugs/No_progress_bars_with_S3/comment_3_5903c1c40c4562f4fbaccd1640fedb18._comment create mode 100644 doc/bugs/No_progress_bars_with_S3/comment_4_80799c33e513384894b390fe34ab312a._comment create mode 100644 doc/bugs/No_version_information_from_cli.mdwn create mode 100644 doc/bugs/OSX_alias_permissions_and_versions_problem.mdwn create mode 100644 doc/bugs/OSX_alias_permissions_and_versions_problem/comment_1_4fabe32e7e626e6ca23aa0b6f449c4c6._comment create mode 100644 doc/bugs/OSX_alias_permissions_and_versions_problem/comment_2_064d60fcc8366a70958540bc145e611a._comment create mode 100644 doc/bugs/OSX_alias_permissions_and_versions_problem/comment_3_6c72d4f40ea0a9566a1185901beff5ba._comment create mode 100644 doc/bugs/OSX_alias_permissions_and_versions_problem/comment_4_8a11f404bb72a1aeb2290744cce2d00d._comment create mode 100644 doc/bugs/OSX_alias_permissions_and_versions_problem/comment_5_30888607199d6a48b76d0c48f5aa4f64._comment create mode 100644 doc/bugs/OSX_app_issues.mdwn create mode 100644 doc/bugs/OSX_app_issues/comment_10_54d8f3e429df9a9958370635c890abf0._comment create mode 100644 doc/bugs/OSX_app_issues/comment_10_6d23232fbb15d0ee3ab532a4884f81ed._comment create mode 100644 doc/bugs/OSX_app_issues/comment_11_5db2baa771fd01a284eac8a16c1c8c67._comment create mode 100644 doc/bugs/OSX_app_issues/comment_11_bb2ceb95a844449795addee6986d0763._comment create mode 100644 doc/bugs/OSX_app_issues/comment_12_62170597c7f441d84d48986857998858._comment create mode 100644 doc/bugs/OSX_app_issues/comment_12_f3bc5a4e4895ac9351786f0bdd8005ba._comment create mode 100644 doc/bugs/OSX_app_issues/comment_2_fd560811c57df5cbc3976639642b8b19._comment create mode 100644 doc/bugs/OSX_app_issues/comment_7_93e0bb53ac2d7daef53426fbdc5f92d9._comment create mode 100644 doc/bugs/OSX_app_issues/comment_8_141eac2f3fb25fe18b4268786f00ad6a._comment create mode 100644 doc/bugs/OSX_app_issues/comment_8_f4d5b2645d7f29b80925159efb94a998._comment create mode 100644 doc/bugs/OSX_app_issues/comment_9_2e6dfca0fd8df04066769653724eae28._comment create mode 100644 doc/bugs/OSX_app_issues/comment_9_e1bbe83a1b9a7385ed6d443d0cc22bc7._comment create mode 100644 doc/bugs/OSX_app_issues/old.mdwn create mode 100644 doc/bugs/OSX_app_issues/old/comment_10_bb823dc3cd6dc914ed14c176afa0b2f3._comment create mode 100644 doc/bugs/OSX_app_issues/old/comment_11_a30e69fed14b0809184ffe05358ab871._comment create mode 100644 doc/bugs/OSX_app_issues/old/comment_12_23d47b3696e537d60df1d383f33f19e4._comment create mode 100644 doc/bugs/OSX_app_issues/old/comment_13_be5738b42b13ec8cd828c5fa66f030e8._comment create mode 100644 doc/bugs/OSX_app_issues/old/comment_14_5783a4716cd104e1f1c276aa0b9cb153._comment create mode 100644 doc/bugs/OSX_app_issues/old/comment_14_e126d87a263f3aa6261f72ee7ff086fc._comment create mode 100644 doc/bugs/OSX_app_issues/old/comment_15_56c7fcafc7dca8be28ebf9e37a8f6b71._comment create mode 100644 doc/bugs/OSX_app_issues/old/comment_15_e58bd3d66f0f43c159d2b37172f152de._comment create mode 100644 doc/bugs/OSX_app_issues/old/comment_16_01f2c968bad66b0ff0c09eb468325deb._comment create mode 100644 doc/bugs/OSX_app_issues/old/comment_16_0b7cd3d5952c5abf36a89a68a4afc1e7._comment create mode 100644 doc/bugs/OSX_app_issues/old/comment_17_82d9963e1fbf17644ce697e5a43943f5._comment create mode 100644 doc/bugs/OSX_app_issues/old/comment_17_c2de94a48e7958b9efffd89dda9144ff._comment create mode 100644 doc/bugs/OSX_app_issues/old/comment_18_29af9df9ea295d114574e76e15b8e737._comment create mode 100644 doc/bugs/OSX_app_issues/old/comment_18_88ddc846eb4e4a2d54028a3412ba28d6._comment create mode 100644 doc/bugs/OSX_app_issues/old/comment_19_6d6341b05123cd317c4eac96353c8662._comment create mode 100644 doc/bugs/OSX_app_issues/old/comment_19_aff4ab761c4d196732baa046af45fe24._comment create mode 100644 doc/bugs/OSX_app_issues/old/comment_20_43bd5985d8a3a5e7f826a34e5dd9216e._comment create mode 100644 doc/bugs/OSX_app_issues/old/comment_3_08613b2e2318680508483d204a43da76._comment create mode 100644 doc/bugs/OSX_app_issues/old/comment_4_4cda124b57ddc87645d5822f14ed5c59._comment create mode 100644 doc/bugs/OSX_app_issues/old/comment_5_0d1df34f83a8dac9c438d93806236818._comment create mode 100644 doc/bugs/OSX_app_issues/old/comment_6_12bd83e7e2327c992448e87bdb85d17e._comment create mode 100644 doc/bugs/OSX_app_issues/old/comment_6_bc44d5aea5f77e331a32913ada293730._comment create mode 100644 doc/bugs/OSX_app_issues/old/comment_6_cea97dbbfb566a9fe463365ca4511119._comment create mode 100644 doc/bugs/OSX_app_issues/old/comment_7_911f187d46890093a54859032ada2442._comment create mode 100644 doc/bugs/OSX_app_issues/old/comment_7_acd73cc5c4caa88099e2d2f19947aadf._comment create mode 100644 doc/bugs/OSX_app_issues/old/comment_8_08b091a58106ca6050ac669579ed9ff4._comment create mode 100644 doc/bugs/OSX_app_issues/old/comment_9_8464c839cb169a4c6e72bebdc2065e9a._comment create mode 100644 doc/bugs/OSX_git-annex.app_error:__LSOpenURLsWithRole__40____41__.mdwn create mode 100644 doc/bugs/OSX_git-annex.app_error:__LSOpenURLsWithRole__40____41__/comment_1_0dfa839f1ba689b23f811787515b8cff._comment create mode 100644 doc/bugs/OSX_git-annex.app_error:__LSOpenURLsWithRole__40____41__/comment_2_612b947eb5474f6d792a833e33105665._comment create mode 100644 doc/bugs/OSX_git-annex.app_error:__LSOpenURLsWithRole__40____41__/comment_3_549b8bcae6f1f8b21932b734e32fbdd1._comment create mode 100644 doc/bugs/OSX_git-annex.app_error:__LSOpenURLsWithRole__40____41__/comment_4_23078dfea127fa3ef20696eb10ce964c._comment create mode 100644 doc/bugs/OSX_git-annex.app_error:__LSOpenURLsWithRole__40____41__/comment_5_7da5ef8325b8787bbf1c6e2c17b1142e._comment create mode 100644 doc/bugs/OS_X_10.8:_Can__39__t_reopen_webapp.mdwn create mode 100644 doc/bugs/OS_X_10.8:_Can__39__t_reopen_webapp/comment_1_2653fe701a1bb20254f3d6b90f10a43b._comment create mode 100644 doc/bugs/OS_X_10.8:_Can__39__t_reopen_webapp/comment_2_d9ce701d077e40f39b142ce2cc570a3b._comment create mode 100644 doc/bugs/OS_X_10.8:_Can__39__t_reopen_webapp/comment_3_14964ab68253dc1a8903d14a821b8b40._comment create mode 100644 doc/bugs/OS_X_10.8:_Can__39__t_reopen_webapp/comment_4_4a579e9a13305ab4157f4b3eba46b92d._comment create mode 100644 doc/bugs/OS_X_10.8:_Can__39__t_reopen_webapp/comment_5_2a710960dc3a177ce62ef92f8546c496._comment create mode 100644 doc/bugs/OS_X_10.8:_Can__39__t_reopen_webapp/comment_6_a4ad73530cd0f6621bcc6394d5f39af7._comment create mode 100644 doc/bugs/Old_repository_stuck.mdwn create mode 100644 doc/bugs/Older_version_of_git_causes_Internal_Server_Error_when_push.default___61___simple.mdwn create mode 100644 doc/bugs/Older_version_of_git_causes_Internal_Server_Error_when_push.default___61___simple/comment_1_971224d2c0c0ce8d4530b1991508f849._comment create mode 100644 doc/bugs/Older_version_of_git_causes_Internal_Server_Error_when_push.default___61___simple/comment_2_6866f96277dbe83a8aadcdeb426b6750._comment create mode 100644 doc/bugs/On_Windows__44___annex_get_fails_with_HTTP_Remote__44___but_believes_it_has_succeeded..mdwn create mode 100644 doc/bugs/On_Windows__44___annex_get_over_HTTP_sends_URLs_with_incorrect_separator.mdwn create mode 100644 doc/bugs/On_Windows__44___can__39__t_use_a_USB_disk_annex_created_on_Linux.mdwn create mode 100644 doc/bugs/On_Windows__44___can__39__t_use_a_USB_disk_annex_created_on_Linux/comment_1_f224f4155d857a59595658357f97dac1._comment create mode 100644 doc/bugs/On_Windows__44___can__39__t_use_repository_that_has_a_unix-style_local_remote_configured.mdwn create mode 100644 doc/bugs/On_Windows__44___wget_is_not_used__44___even_if_available.mdwn create mode 100644 doc/bugs/Open_webapp_ask_to_create_new_repo___40__on_first_start__41___even_if_repo_exists_on_Android.mdwn create mode 100644 doc/bugs/Open_webapp_ask_to_create_new_repo___40__on_first_start__41___even_if_repo_exists_on_Android/comment_1_9f10bf273b15e93f1eea029f091f26cb._comment create mode 100644 doc/bugs/Out_of_memory_error_in_fsck_whereis_find_and_status_cmds.mdwn create mode 100644 doc/bugs/Out_of_memory_error_in_fsck_whereis_find_and_status_cmds/comment_1_3aef6ca929fad198f2dda0868f2d49cb._comment create mode 100644 doc/bugs/Out_of_memory_error_in_fsck_whereis_find_and_status_cmds/comment_2_f2c1aa84a0d04e840cb34ae15eb1cb03._comment create mode 100644 doc/bugs/Out_of_memory_error_in_fsck_whereis_find_and_status_cmds/comment_3_480c39648e3ca6fc58c30377bdb25a8c._comment create mode 100644 doc/bugs/Out_of_memory_error_in_fsck_whereis_find_and_status_cmds/comment_4_b31496b37046a9886f632ba4f11c56e3._comment create mode 100644 doc/bugs/Out_of_memory_error_in_fsck_whereis_find_and_status_cmds/comment_5_d25ff424dda1f6021c1ba20f79d71ffc._comment create mode 100644 doc/bugs/Pairing_locally_shows:___34__bad_comment_in_ssh_public_key_ssh-rsa__34__.mdwn create mode 100644 doc/bugs/Partial_direct__47__indirect_repo.mdwn create mode 100644 doc/bugs/Partial_direct__47__indirect_repo/comment_1_42344fce051d759f95215c985e9d1135._comment create mode 100644 doc/bugs/Partial_direct__47__indirect_repo/comment_2_8ba64f2750d0ef4adf595674c723bc65._comment create mode 100644 doc/bugs/Partial_direct__47__indirect_repo/comment_3_bd4985864b7dcd70a609ca7bc2617e4a._comment create mode 100644 doc/bugs/Possible_data_loss_-_git_status___39__typechange__39___and_direct_mode.mdwn create mode 100644 doc/bugs/Possible_data_loss_-_git_status___39__typechange__39___and_direct_mode/comment_1_84cb8c651584ec2887f6e1b7dc107190._comment create mode 100644 doc/bugs/Possible_issues_with_git_1.7.10_and_newer___40__merge_command_now_asks_for_a_commit_message__34__.mdwn create mode 100644 doc/bugs/Prevent_accidental_merges.mdwn create mode 100644 doc/bugs/Prevent_accidental_merges/comment_1_4c46a193915eab8f308a04175cb2e40a._comment create mode 100644 doc/bugs/Problem_with_bup:_cannot_lock_refs.mdwn create mode 100644 doc/bugs/Problems_building_on_Mac_OS_X.mdwn create mode 100644 doc/bugs/Problems_building_on_Mac_OS_X/comment_1_1c199b826fdd84b5184b1466ad03a9a4._comment create mode 100644 doc/bugs/Problems_running_make_on_osx.mdwn create mode 100644 doc/bugs/Problems_running_make_on_osx/comment_10_94e4ac430140042a2d0fb5a16d86b4e5._comment create mode 100644 doc/bugs/Problems_running_make_on_osx/comment_11_56f1143fa191361d63b441741699e17f._comment create mode 100644 doc/bugs/Problems_running_make_on_osx/comment_12_ec5131624d0d2285d3b6880e47033f97._comment create mode 100644 doc/bugs/Problems_running_make_on_osx/comment_13_88ed095a448096bf8a69015a04e64df1._comment create mode 100644 doc/bugs/Problems_running_make_on_osx/comment_14_89a960b6706ed703b390a81a8bc4e311._comment create mode 100644 doc/bugs/Problems_running_make_on_osx/comment_15_6b8867b8e48bf807c955779c9f8f0909._comment create mode 100644 doc/bugs/Problems_running_make_on_osx/comment_16_5c2dd6002aadaab30841b77a5f5aed34._comment create mode 100644 doc/bugs/Problems_running_make_on_osx/comment_17_62fccb04b0e4b695312f7a3f32fb96ee._comment create mode 100644 doc/bugs/Problems_running_make_on_osx/comment_18_64fab50d95de619eb2e8f08f90237de1._comment create mode 100644 doc/bugs/Problems_running_make_on_osx/comment_19_4253988ed178054c8b6400beeed68a29._comment create mode 100644 doc/bugs/Problems_running_make_on_osx/comment_1_34120e82331ace01a6a4960862d38f2d._comment create mode 100644 doc/bugs/Problems_running_make_on_osx/comment_20_7db27d1a22666c831848bc6c06d66a84._comment create mode 100644 doc/bugs/Problems_running_make_on_osx/comment_2_cc53d1681d576186dbc868dd9801d551._comment create mode 100644 doc/bugs/Problems_running_make_on_osx/comment_3_68f0f8ae953589ae26d57310b40c878d._comment create mode 100644 doc/bugs/Problems_running_make_on_osx/comment_4_c52be386f79f14c8570a8f1397c68581._comment create mode 100644 doc/bugs/Problems_running_make_on_osx/comment_5_7f1330a1e541b0f3e2192e596d7f7bee._comment create mode 100644 doc/bugs/Problems_running_make_on_osx/comment_6_0c46f5165ceb5a7b9ea9689c33b3a4f8._comment create mode 100644 doc/bugs/Problems_running_make_on_osx/comment_7_237a137cce58a28abcc736cbf2c420b0._comment create mode 100644 doc/bugs/Problems_running_make_on_osx/comment_8_efafa203addf8fa79e33e21a87fb5a2b._comment create mode 100644 doc/bugs/Problems_running_make_on_osx/comment_9_cc283b485b3c95ba7eebc8f0c96969b3._comment create mode 100644 doc/bugs/Problems_with_syncing_gnucash.mdwn create mode 100644 doc/bugs/Problems_with_syncing_gnucash/comment_1_ca195af3ba4a286eb5ab687634192fa4._comment create mode 100644 doc/bugs/Problems_with_syncing_gnucash/comment_2_754fb430381ad88e6248ecb902b32118._comment create mode 100644 doc/bugs/Problems_with_syncing_gnucash/comment_4_25881998c6f149c70b1358f37b7c66ba._comment create mode 100644 doc/bugs/Provide_64-bit_standalone_build.mdwn create mode 100644 doc/bugs/Provide_64-bit_standalone_build/comment_1_1850bb3eb464f1d3c122cfeb4ccaf265._comment create mode 100644 doc/bugs/Proxy_support.mdwn create mode 100644 doc/bugs/Remote_repo_and_set_operation_with_find.mdwn create mode 100644 doc/bugs/Remote_repositories_have_to_be_setup_encrypted.mdwn create mode 100644 doc/bugs/Remote_repositories_have_to_be_setup_encrypted/comment_1_95f73315657bc35a8d3ff9b4ba207af0._comment create mode 100644 doc/bugs/Remotes_only_start_showing_changes_after_both_sides_have_written_to_the_repository.mdwn create mode 100644 doc/bugs/Remotes_only_start_showing_changes_after_both_sides_have_written_to_the_repository/comment_1_92211091daf9827a4ec7e5b5a6769d59._comment create mode 100644 doc/bugs/Remotes_only_start_showing_changes_after_both_sides_have_written_to_the_repository/comment_2_f0fa97a9eba1c624f6f8720ba8a160b7._comment create mode 100644 doc/bugs/Remotes_only_start_showing_changes_after_both_sides_have_written_to_the_repository/comment_3_e3d677ea4170c07cd31efe6dc85fa5f3._comment create mode 100644 doc/bugs/Renamed_special_remote_cannot_be_reactivated_by_the_webapp.mdwn create mode 100644 doc/bugs/Repository_deletion_error.mdwn create mode 100644 doc/bugs/Repository_deletion_error/comment_1_31673d0300986b6098d1af2cc4b180c6._comment create mode 100644 doc/bugs/Resource_exhausted.mdwn create mode 100644 doc/bugs/Resource_exhausted/comment_1_a5ef7a62d4ed9365f9448520bb17e3b5._comment create mode 100644 doc/bugs/Resource_exhausted/comment_2_cdba2015e603f3c21f3e1697dd6fafcd._comment create mode 100644 doc/bugs/Resource_exhausted/comment_3_747d16d050fdcf69dd3d2bc5ca469a2e._comment create mode 100644 doc/bugs/Resource_exhausted/comment_4_1e9b74e60da57c3d5f08c1eb3801c1d2._comment create mode 100644 doc/bugs/Resource_exhausted/comment_5_f55d933bce77fd2185ebd0cc46fe57ec._comment create mode 100644 doc/bugs/Resource_exhausted/comment_6_26c98fca45b029a527f9684873db4be5._comment create mode 100644 doc/bugs/Resource_exhausted/comment_7_8bab413b472f900e04977db2bc3951b6._comment create mode 100644 doc/bugs/Resource_exhausted/comment_8_e9bec0b80179b1229b6af0979a21c727._comment create mode 100644 doc/bugs/Resource_leak_somewhere_in_the___39__get__39___code.mdwn create mode 100644 doc/bugs/Resource_leak_somewhere_in_the___39__get__39___code/comment_1_66b21720cd1b2a4f66ef24252d3e6305._comment create mode 100644 doc/bugs/Resource_leak_somewhere_in_the___39__get__39___code/comment_2_18c9f55c5af1f4f690a7727df71ab561._comment create mode 100644 doc/bugs/Rsync_encrypted_remote_asks_for_ssh_key_password_for_each_file.mdwn create mode 100644 doc/bugs/Rsync_encrypted_remote_asks_for_ssh_key_password_for_each_file/comment_1_fd95e0bb61e80a72b4ac1304ef6c2e77._comment create mode 100644 doc/bugs/Rsync_remote_created_via_webapp_remains_empty.mdwn create mode 100644 doc/bugs/Rsync_remote_created_via_webapp_remains_empty/comment_1_cccf9d58c0ebb8d31cacdd029ea8e23a._comment create mode 100644 doc/bugs/S3_bucket_uses_the_same_key_for_encryption_and_hashing.mdwn create mode 100644 doc/bugs/S3_bucket_uses_the_same_key_for_encryption_and_hashing/comment_1_dc5ae7af499203cfd903e866595b8fea._comment create mode 100644 doc/bugs/S3_bucket_uses_the_same_key_for_encryption_and_hashing/comment_2_c62daf5b3bfcd2f684262c96ef6628c1._comment create mode 100644 doc/bugs/S3_bucket_uses_the_same_key_for_encryption_and_hashing/comment_3_e1f39c4af5bdb0daabf000da80858cd9._comment create mode 100644 doc/bugs/S3_bucket_uses_the_same_key_for_encryption_and_hashing/comment_4_bb6b814ab961818d514f6553455d2bf3._comment create mode 100644 doc/bugs/S3_bucket_uses_the_same_key_for_encryption_and_hashing/comment_5_5bb128f6d2ca4b5e4d881fae297fa1f8._comment create mode 100644 doc/bugs/S3_bucket_uses_the_same_key_for_encryption_and_hashing/comment_6_63fb74da342751fc35e1850409c506f6._comment create mode 100644 doc/bugs/S3_memory_leaks.mdwn create mode 100644 doc/bugs/S3_upload_not_using_multipart.mdwn create mode 100644 doc/bugs/SSH:_command-line:_line_0:_Bad_configuration_option:_ControlPersist___40__SSH_too_old_on_OS_X_10.6.8__63____41__.mdwn create mode 100644 doc/bugs/SSH:_command-line:_line_0:_Bad_configuration_option:_ControlPersist___40__SSH_too_old_on_OS_X_10.6.8__63____41__/comment_1_0c57a2196d35eb1ecfb0c51273bba05c._comment create mode 100644 doc/bugs/Segfaults_on_Fedora_18_with_SELinux_enabled.mdwn create mode 100644 doc/bugs/Segfaults_on_Fedora_18_with_SELinux_enabled/comment_1_f708d87aa65cd38c20087859d3ab2dc7._comment create mode 100644 doc/bugs/Segfaults_on_Fedora_18_with_SELinux_enabled/comment_2_fb7188db031147992f3c906783ebbee0._comment create mode 100644 doc/bugs/Selfsigned_certificates_with_jabber_fail_miserably..mdwn create mode 100644 doc/bugs/Selfsigned_certificates_with_jabber_fail_miserably./comment_1_13d27ba41d9ef78c8db534b6bc26314e._comment create mode 100644 doc/bugs/Selfsigned_certificates_with_jabber_fail_miserably./comment_2_018eed99e71680be9e7c0844020419bb._comment create mode 100644 doc/bugs/Should_ignore_.thumbnails__47___on_android.mdwn create mode 100644 doc/bugs/Should_try_again_when_network_fails___40__esp._DNS__41__.mdwn create mode 100644 doc/bugs/Should_try_again_when_network_fails___40__esp._DNS__41__/comment_1_dd792bd98a48554a65150c06401ed3e5._comment create mode 100644 doc/bugs/Small_archive_behaving_like_archive.mdwn create mode 100644 doc/bugs/Small_archive_behaving_like_archive/comment_1_718dc246cbbbeae04436fa033011ab12._comment create mode 100644 doc/bugs/Specifying_a_filename_starting_with___34__-c__34___instead_applies_it_to_all_files.mdwn create mode 100644 doc/bugs/Specifying_a_filename_starting_with___34__-c__34___instead_applies_it_to_all_files/comment_1_2fe6d735bc075275a6b8890fac48ee58._comment create mode 100644 doc/bugs/Stale_lock_files_on_Android.mdwn create mode 100644 doc/bugs/Stress_test.mdwn create mode 100644 doc/bugs/Stress_test/comment_10_1694e990eab6592159309c231c6dcc16._comment create mode 100644 doc/bugs/Stress_test/comment_11_ab4cb6eefd279e6c1f229e089f703581._comment create mode 100644 doc/bugs/Stress_test/comment_1_c4c764488ac082f5c48d3a6b4b5fba42._comment create mode 100644 doc/bugs/Stress_test/comment_2_42125bba09a0ea9821cda7183e458100._comment create mode 100644 doc/bugs/Stress_test/comment_3_8240e61106b494d3600ad91f16eb5b1c._comment create mode 100644 doc/bugs/Stress_test/comment_4_c38d84e0dcc834931804c44bce7f7b7a._comment create mode 100644 doc/bugs/Stress_test/comment_5_60ce20ee255451c4ea809ba475561adb._comment create mode 100644 doc/bugs/Stress_test/comment_6_1371562e201393986cd41597f6f288cb._comment create mode 100644 doc/bugs/Stress_test/comment_7_a14be7699da224a8f6c9b34f1b911219._comment create mode 100644 doc/bugs/Stress_test/comment_8_a01995bdca7ade7dde9842b53fbc4e0c._comment create mode 100644 doc/bugs/Stress_test/comment_9_9f7efe81b7e40aaa04a865394c53e20f._comment create mode 100644 doc/bugs/Switching_between_direct_and_indirect_stomps_on___39__regular__39___git_files.mdwn create mode 100644 doc/bugs/Switching_between_direct_and_indirect_stomps_on___39__regular__39___git_files/comment_1_0d2cb3b8509cd0eba50aafa14afefc02._comment create mode 100644 doc/bugs/Switching_from_indirect_mode_to_direct_mode_breaks_duplicates.mdwn create mode 100644 doc/bugs/Switching_repositories_in_webapp_on_a_remote_server_is_not_honoring_--listen_parameter.mdwn create mode 100644 doc/bugs/Switching_repositories_in_webapp_on_a_remote_server_is_not_honoring_--listen_parameter/comment_1_4dd773372979dd95538bfba6516a11eb._comment create mode 100644 doc/bugs/Syncing_creates_broken_links_instead_of_proper_files.mdwn create mode 100644 doc/bugs/Syncing_creates_broken_links_instead_of_proper_files/comment_1_a2bedb2e77451b02fc66fc9ef5c4405c._comment create mode 100644 doc/bugs/Test_failure_on_debian_dropunused.mdwn create mode 100644 doc/bugs/The_assistant_hangs_forever.mdwn create mode 100644 doc/bugs/The_assistant_hangs_forever/comment_1_b0291e32860e0da0b66837d14ed5aab6._comment create mode 100644 doc/bugs/The_assistant_hangs_forever/comment_2_a2950cf91b8a4e4f2951f5522ef0e9c4._comment create mode 100644 doc/bugs/The_assistant_hangs_forever/comment_3_db95f78519d5ffbad793906028730dab._comment create mode 100644 doc/bugs/The_assistant_hangs_forever/comment_4_28b13fd3165b38a2fbc9e1a461c38921._comment create mode 100644 doc/bugs/The_assistant_hangs_forever/comment_5_81a79c8840ff26307a9c6edad5b850f9._comment create mode 100644 doc/bugs/The_assistant_hangs_forever/comment_6_b739719b14705f4d7e1d412b3cab090c._comment create mode 100644 doc/bugs/The_assistant_hangs_forever/comment_7_2b300d960697c5b967c1f109dfd6dfbf._comment create mode 100644 doc/bugs/The_assistant_hangs_forever/comment_8_8623220d08b1a72ed8b669a2d9cc0f75._comment create mode 100644 doc/bugs/The_restricted_ssh_key_pair_makes_password_login___40__nearly__41___impossible.mdwn create mode 100644 doc/bugs/The_restricted_ssh_key_pair_makes_password_login___40__nearly__41___impossible/comment_10_8305becdc6e70abdaf17e42f263173fc._comment create mode 100644 doc/bugs/The_restricted_ssh_key_pair_makes_password_login___40__nearly__41___impossible/comment_11_d75896a6e204d1abdda04923aa668d04._comment create mode 100644 doc/bugs/The_restricted_ssh_key_pair_makes_password_login___40__nearly__41___impossible/comment_12_a36a4a64a04c01c2db467b09300e6ebd._comment create mode 100644 doc/bugs/The_restricted_ssh_key_pair_makes_password_login___40__nearly__41___impossible/comment_13_c9d6631c304acb289e485fb901e1f274._comment create mode 100644 doc/bugs/The_restricted_ssh_key_pair_makes_password_login___40__nearly__41___impossible/comment_14_10282c4352075c8d148b8674973b7b16._comment create mode 100644 doc/bugs/The_restricted_ssh_key_pair_makes_password_login___40__nearly__41___impossible/comment_15_ceb68da01d9e2fe9a70fab6244116da0._comment create mode 100644 doc/bugs/The_restricted_ssh_key_pair_makes_password_login___40__nearly__41___impossible/comment_16_cca4abde86a8be5e2919c4738f5bdd0c._comment create mode 100644 doc/bugs/The_restricted_ssh_key_pair_makes_password_login___40__nearly__41___impossible/comment_17_2fa5d7d9110c91b0a3a833cb3d9f53fd._comment create mode 100644 doc/bugs/The_restricted_ssh_key_pair_makes_password_login___40__nearly__41___impossible/comment_18_bf21d28142e4c304aa0bc740955ddea0._comment create mode 100644 doc/bugs/The_restricted_ssh_key_pair_makes_password_login___40__nearly__41___impossible/comment_19_45537758fa937f16fc82120bf8b234e8._comment create mode 100644 doc/bugs/The_restricted_ssh_key_pair_makes_password_login___40__nearly__41___impossible/comment_1_a38497772834a4b12137390b461ce70b._comment create mode 100644 doc/bugs/The_restricted_ssh_key_pair_makes_password_login___40__nearly__41___impossible/comment_20_b685050ee6fbb1a685e33f9656a10e84._comment create mode 100644 doc/bugs/The_restricted_ssh_key_pair_makes_password_login___40__nearly__41___impossible/comment_3_17bc0220c20553c848875475c5fd4ae6._comment create mode 100644 doc/bugs/The_restricted_ssh_key_pair_makes_password_login___40__nearly__41___impossible/comment_4_76472bc58bb790f773c46ec2c39fcf88._comment create mode 100644 doc/bugs/The_restricted_ssh_key_pair_makes_password_login___40__nearly__41___impossible/comment_4_dcd9286e314779c25764484beff40561._comment create mode 100644 doc/bugs/The_restricted_ssh_key_pair_makes_password_login___40__nearly__41___impossible/comment_5_2146eec77b87b615100d0d003e8dce75._comment create mode 100644 doc/bugs/The_restricted_ssh_key_pair_makes_password_login___40__nearly__41___impossible/comment_6_2bd6f4e04903ee251d43d0a97bd40b6e._comment create mode 100644 doc/bugs/The_restricted_ssh_key_pair_makes_password_login___40__nearly__41___impossible/comment_7_7db8ed002eb6313b07f09bd1a34019e3._comment create mode 100644 doc/bugs/The_restricted_ssh_key_pair_makes_password_login___40__nearly__41___impossible/comment_8_1bcb2a238006044bc78849e56cb21a01._comment create mode 100644 doc/bugs/The_restricted_ssh_key_pair_makes_password_login___40__nearly__41___impossible/comment_9_26c6937cf78e7141e0e3b20f25ed8f7a._comment create mode 100644 doc/bugs/The_tests_are_failing_to_build_now_on_commit_e0fdfb2e706da2cb1451193c658dc676b0530968.mdwn create mode 100644 doc/bugs/The_webapp_doesn__39__t_allow_deleting_repositories.mdwn create mode 100644 doc/bugs/The_webapp_doesn__39__t_allow_deleting_repositories/comment_1_1b80f9cfedd25e34997fa07e08d15012._comment create mode 100644 doc/bugs/The_webapp_doesn__39__t_allow_deleting_repositories/comment_2_53499da1185c56d8fd25f86ba41d96ce._comment create mode 100644 doc/bugs/The_webapp_doesn__39__t_allow_deleting_repositories/comment_3_3e07b8386d2c7afce2a78d24b9c260b9._comment create mode 100644 doc/bugs/TransferScanner_crash_on_Android.mdwn create mode 100644 doc/bugs/TransferScanner_crash_on_Android/comment_1_6c3584ade1ee6cccddddeaa8e1697945._comment create mode 100644 doc/bugs/TransferScanner_crash_on_Android/comment_2_06574e05149a677d666a722061586658._comment create mode 100644 doc/bugs/TransferScanner_crash_on_Android/comment_3_54ae097d30bb7a49fe151f38c9bac033._comment create mode 100644 doc/bugs/Tries_to_upload_to_remote_although_remote_is_dead.mdwn create mode 100644 doc/bugs/Tries_to_upload_to_remote_although_remote_is_dead/comment_1_108b3984891f82429430b503cddfb3c1._comment create mode 100644 doc/bugs/Tries_to_upload_to_remote_although_remote_is_dead/comment_2_fa5b1bc26ed3e5bfe48441490c94fe3a._comment create mode 100644 doc/bugs/Tries_to_upload_to_remote_although_remote_is_dead/comment_3_0a785b5dfbf4eef30854d6bedb12b7d1._comment create mode 100644 doc/bugs/Trouble_initializing_git_annex_on_NFS.mdwn create mode 100644 doc/bugs/Trouble_initializing_git_annex_on_NFS/comment_1_e26952373150d63b8a5d3643a2762de1._comment create mode 100644 doc/bugs/Trouble_initializing_git_annex_on_NFS/comment_2_f80b10ed395738e50e345fc22c708ae5._comment create mode 100644 doc/bugs/Trouble_initializing_git_annex_on_NFS/comment_3_f99e0f05950fc2fc80fdecd35e17012c._comment create mode 100644 doc/bugs/Trouble_initializing_git_annex_on_NFS/comment_4_e42146d2dcc4052266dd61d204aeb551._comment create mode 100644 doc/bugs/True_backup_support.mdwn create mode 100644 doc/bugs/True_backup_support/comment_1_50aa0bc1e2502622585682cb703e0b85._comment create mode 100644 doc/bugs/True_backup_support/comment_2_d6030c6c49b227e022f05d590746d4ca._comment create mode 100644 doc/bugs/Truncated_file_transferred_via_S3.mdwn create mode 100644 doc/bugs/Truncated_file_transferred_via_S3/comment_1_5962358e6067448f633cc0eaf42f9ca7._comment create mode 100644 doc/bugs/Truncated_file_transferred_via_S3/comment_2_75a2c272c36fc4fe8f9a79a3fd3ac4e5._comment create mode 100644 doc/bugs/Truncated_file_transferred_via_S3/comment_3_3dae1914c8c90fdad0c21e1fc795f2ca._comment create mode 100644 doc/bugs/Truncated_file_transferred_via_S3/comment_4_3c5fe109f2196cfc196c30da3b62bafd._comment create mode 100644 doc/bugs/Truncated_file_transferred_via_S3/comment_5_f86f83c89300f255e730ddd23f876f61._comment create mode 100644 doc/bugs/Unable_to_add_files_on_Android_due_to_weird_rename_error.mdwn create mode 100644 doc/bugs/Unable_to_add_files_on_Android_due_to_weird_rename_error/comment_1_928289956111d1b22f9d55f15b54f72f._comment create mode 100644 doc/bugs/Unable_to_add_files_on_Android_due_to_weird_rename_error/comment_2_6a0cb836b93ba4cb1e07b11d5d2a7094._comment create mode 100644 doc/bugs/Unable_to_switch_back_to_direct_mode.mdwn create mode 100644 doc/bugs/Unable_to_switch_back_to_direct_mode/comment_1_4585b251f011a153c62f377c324cf963._comment create mode 100644 doc/bugs/Unable_to_switch_back_to_direct_mode/comment_2_5848ebbab38d1244347f7e7351b3a30d._comment create mode 100644 doc/bugs/Unable_to_switch_back_to_direct_mode/comment_3_1c5c7b0c7bc336e00f43e257b87a6208._comment create mode 100644 doc/bugs/Unable_to_switch_back_to_direct_mode/comment_4_b0bfd68998bc3e11d8e089646b8292a6._comment create mode 100644 doc/bugs/Unable_to_sync_a_second_machine_through_Box.mdwn create mode 100644 doc/bugs/Unable_to_sync_a_second_machine_through_Box/comment_1_cb43a2bc976e3eb1cfc3ee9d4d34e78e._comment create mode 100644 doc/bugs/Unable_to_sync_a_second_machine_through_Box/comment_2_3375e9bfab3fed271413bd9bb5fa0121._comment create mode 100644 doc/bugs/Unable_to_sync_a_second_machine_through_Box/comment_3_c4420e1a3db321b4135b1626d3582adb._comment create mode 100644 doc/bugs/Unable_to_sync_a_second_machine_through_Box/comment_4_f4b2c88bb5938dacdd04dfe9a68560de._comment create mode 100644 doc/bugs/Unable_to_sync_a_second_machine_through_Box/comment_5_6dcc95ffb3fc7bbbedd6be5df0111c85._comment create mode 100644 doc/bugs/Unable_to_use_remotes_with_space_in_the_path.mdwn create mode 100644 doc/bugs/Unfortunate_interaction_with_Calibre.mdwn create mode 100644 doc/bugs/Unfortunate_interaction_with_Calibre/comment_1_7cb5561f11dfc7726a537ddde2477489._comment create mode 100644 doc/bugs/Unfortunate_interaction_with_Calibre/comment_2_b8ae4bc589c787dacc08ab2ee5491d6e._comment create mode 100644 doc/bugs/Unfortunate_interaction_with_Calibre/comment_3_977c5f6b82f9e18cdd81d57005bb8b89._comment create mode 100644 doc/bugs/Unfortunate_interaction_with_Calibre/comment_4_ff7d2e9a39dfe12b975d04650ac57cc4._comment create mode 100644 doc/bugs/Unfortunate_interaction_with_Calibre/comment_5_fc4d5301797589e92cc9a24697b2155d._comment create mode 100644 doc/bugs/Unknown_remote_type_webdav.mdwn create mode 100644 doc/bugs/Update_dependency_on_certificate___62____61___1.3.3.mdwn create mode 100644 doc/bugs/Use_a_git_repository_on_the_server_don__39__t_work.mdwn create mode 100644 doc/bugs/Use_a_git_repository_on_the_server_don__39__t_work/comment_1_2143f0540fdcd7efeb25b5a3b54fe0fd._comment create mode 100644 doc/bugs/Use_a_git_repository_on_the_server_don__39__t_work/comment_2_bca95245b457631d08b47591da6163ad._comment create mode 100644 doc/bugs/Use_a_git_repository_on_the_server_don__39__t_work/comment_3_f54bb003096752dae0442660267a1e37._comment create mode 100644 doc/bugs/Use_a_git_repository_on_the_server_don__39__t_work/comment_4_38bb916ed5b90b92ffa91a452ff052a9._comment create mode 100644 doc/bugs/Use_a_git_repository_on_the_server_don__39__t_work/comment_5_5b6ef464ab1ad061f27122db40191e26._comment create mode 100644 doc/bugs/Use_a_git_repository_on_the_server_don__39__t_work/comment_6_3727bda5082cb1f2b1f746f9f80ced7d._comment create mode 100644 doc/bugs/Use_a_git_repository_on_the_server_don__39__t_work/comment_7_a7139f19f0b73c024cd9218eb01e6104._comment create mode 100644 doc/bugs/Using_Github_as_remote_throws_proxy_errors.mdwn create mode 100644 doc/bugs/Using_Github_as_remote_throws_proxy_errors/comment_1_10616b17c3fb8286fdc64c841023f8a1._comment create mode 100644 doc/bugs/Using_Github_as_remote_throws_proxy_errors/comment_2_8a72887d33e492a041f8246d93d0c778._comment create mode 100644 doc/bugs/Using_a_revoked_GPG_key.mdwn create mode 100644 doc/bugs/Using_a_revoked_GPG_key/comment_1_7bb01d081282e5b02b7720b2953fe5be._comment create mode 100644 doc/bugs/Using_a_revoked_GPG_key/comment_2_9c0c40360f0058a4bd346c1362e302b6._comment create mode 100644 doc/bugs/Using_a_revoked_GPG_key/comment_3_8f69f58107246595f5603f35c4aa7395._comment create mode 100644 doc/bugs/WEBDAV_443.mdwn create mode 100644 doc/bugs/WEBDAV_443/comment_10_9ee2c5ed44295455af890caee7b06f1a._comment create mode 100644 doc/bugs/WEBDAV_443/comment_11_863a7d315212c9a8ab8f6fafa5d1b7f5._comment create mode 100644 doc/bugs/WEBDAV_443/comment_12_c17a4e23011e0a917dbe0ecf7e9f0cb5._comment create mode 100644 doc/bugs/WEBDAV_443/comment_13_3414416ff455d2fd1a7c7e7c4554b54d._comment create mode 100644 doc/bugs/WEBDAV_443/comment_14_e1da141eefb0445c217e5f5c119356da._comment create mode 100644 doc/bugs/WEBDAV_443/comment_15_41c3134bcc222b97bf183559723713d9._comment create mode 100644 doc/bugs/WEBDAV_443/comment_16_89621b526065b5bef753ce75db1af7b5._comment create mode 100644 doc/bugs/WEBDAV_443/comment_17_131a1b65c8008cf9f02c93d4fb75720b._comment create mode 100644 doc/bugs/WEBDAV_443/comment_18_b4f894a0b9ebb84ab73f6ffcf0778090._comment create mode 100644 doc/bugs/WEBDAV_443/comment_1_c6572ca1eaaf89b01c0ed99a4058412f._comment create mode 100644 doc/bugs/WEBDAV_443/comment_2_a357969cde382a91e13920ee1e9f711c._comment create mode 100644 doc/bugs/WEBDAV_443/comment_3_213815d6b827d467c60f3e8af925813b._comment create mode 100644 doc/bugs/WEBDAV_443/comment_4_b775be4b722fc7124d9fbe2d5d01cc9f._comment create mode 100644 doc/bugs/WEBDAV_443/comment_5_c4ea745da437e56b2426d1c2c00dfcec._comment create mode 100644 doc/bugs/WEBDAV_443/comment_6_ef05c0ae88fee9c626922c6064ffdf1e._comment create mode 100644 doc/bugs/WEBDAV_443/comment_7_eecabe8d5ed564cb540450770ca7d0b6._comment create mode 100644 doc/bugs/WEBDAV_443/comment_8_7f77ba8ebd90186d3b3949ae529ba393._comment create mode 100644 doc/bugs/WEBDAV_443/comment_9_87ebdc92b48d672964fb3f248c53600f._comment create mode 100644 doc/bugs/WORM:_Handle_long_filenames_correctly.mdwn create mode 100644 doc/bugs/WORM:_Handle_long_filenames_correctly/comment_1_77aa9cafbe20367a41377f3edccc9ddb._comment create mode 100644 doc/bugs/WORM:_Handle_long_filenames_correctly/comment_2_fe735d728878d889ccd34ec12b3a7dea._comment create mode 100644 doc/bugs/WORM:_Handle_long_filenames_correctly/comment_3_2bf0f02d27190578e8f4a32ddb195a0a._comment create mode 100644 doc/bugs/WORM:_Handle_long_filenames_correctly/comment_4_8f7ba9372463863dda5aae13205861bf._comment create mode 100644 doc/bugs/Watch_command_as_of_commit_6cecc26206c4a539999b04664136c6f785211a41_segfaults.mdwn create mode 100644 doc/bugs/Watch_command_as_of_commit_6cecc26206c4a539999b04664136c6f785211a41_segfaults/comment_10_6c872dff4fcc63c16bf69d1e96891c89._comment create mode 100644 doc/bugs/Watch_command_as_of_commit_6cecc26206c4a539999b04664136c6f785211a41_segfaults/comment_1_5cad24007f819e4be193123dab0d511a._comment create mode 100644 doc/bugs/Watch_command_as_of_commit_6cecc26206c4a539999b04664136c6f785211a41_segfaults/comment_2_d449bf656a59d424833f9ab5a7fb4e82._comment create mode 100644 doc/bugs/Watch_command_as_of_commit_6cecc26206c4a539999b04664136c6f785211a41_segfaults/comment_3_ffb1ce41477ad60840abd7a89a133067._comment create mode 100644 doc/bugs/Watch_command_as_of_commit_6cecc26206c4a539999b04664136c6f785211a41_segfaults/comment_4_cebbc138c6861c086bb7937b54f5adbc._comment create mode 100644 doc/bugs/Watch_command_as_of_commit_6cecc26206c4a539999b04664136c6f785211a41_segfaults/comment_5_5e27737a5bb0e9e46c98708700318e67._comment create mode 100644 doc/bugs/Watch_command_as_of_commit_6cecc26206c4a539999b04664136c6f785211a41_segfaults/comment_6_1f92da712232d050e085a4f39063d7a6._comment create mode 100644 doc/bugs/Watch_command_as_of_commit_6cecc26206c4a539999b04664136c6f785211a41_segfaults/comment_7_4153dc8029c545f8e86584a38bd536fb._comment create mode 100644 doc/bugs/Watch_command_as_of_commit_6cecc26206c4a539999b04664136c6f785211a41_segfaults/comment_8_f85b6eb5bfd28ffc6973fb4ab0fe4337._comment create mode 100644 doc/bugs/Watch_command_as_of_commit_6cecc26206c4a539999b04664136c6f785211a41_segfaults/comment_9_c747c488461c98cd285b51d3afc2c3eb._comment create mode 100644 doc/bugs/Watcher_crashed:_addWatch:_does_not_exist.mdwn create mode 100644 doc/bugs/Watcher_crashed:_addWatch:_does_not_exist/comment_1_24f511a8103727894c6e96798a559870._comment create mode 100644 doc/bugs/Watcher_crashed:_addWatch:_does_not_exist/comment_2_e14eddbc09cadbf1e4dbbb0c07e0e5b0._comment create mode 100644 doc/bugs/Watcher_crashed:_addWatch:_does_not_exist/comment_3_513fae4d379008f954a307be8df34976._comment create mode 100644 doc/bugs/Watcher_crashed:_addWatch:_does_not_exist/comment_4_172eaeb3bb8b502379695aba35f96120._comment create mode 100644 doc/bugs/Watcher_crashed:_addWatch:_does_not_exist/comment_5_8adb9de82cc8581422734acc66dd094c._comment create mode 100644 doc/bugs/Watcher_crashed:_addWatch:_does_not_exist/comment_6_02f0beef1188bfa336bf4220eb5c6286._comment create mode 100644 doc/bugs/Watcher_crashed_in_Android_on___47__storage__47__sdcard1_-_bug__63__.mdwn create mode 100644 doc/bugs/Watcher_crashed_in_Android_on___47__storage__47__sdcard1_-_bug__63__/comment_1_71b052be40fbdaca09ca3ede8c59ac7a._comment create mode 100644 doc/bugs/Watcher_crashed_in_Android_on___47__storage__47__sdcard1_-_bug__63__/comment_3_0f7cc02e0193c969c9b6ceb27e71af8a._comment create mode 100644 doc/bugs/WebDAV_HandshakeFailed_.mdwn create mode 100644 doc/bugs/WebDAV_HandshakeFailed_/comment_1_40499110ea43bc99ad9dd9f642da434c._comment create mode 100644 doc/bugs/WebDAV_HandshakeFailed_/comment_2_506712e8cc5b47b9bd69edf67ae54da7._comment create mode 100644 doc/bugs/WebDAV_HandshakeFailed_/comment_3_5641481d9e9ed2b711b1516f1abc5c30._comment create mode 100644 doc/bugs/WebDAV_HandshakeFailed_/comment_4_1d609de93fa66ce9dc802e67b5922243._comment create mode 100644 doc/bugs/WebDAV_HandshakeFailed_/comment_5_62761882d30c1b02930c938cb8e30ed4._comment create mode 100644 doc/bugs/WebDAV_HandshakeFailed_/comment_6_acda8fae848ec486ce2a0b3dff3bd0a5._comment create mode 100644 doc/bugs/WebDAV_HandshakeFailed_/comment_7_6c51b6c7dd477d8911dd9a7a5c41ea2e._comment create mode 100644 doc/bugs/WebDAV_HandshakeFailed_/comment_8_e834f791d3000669fab25732a7c72ab3._comment create mode 100644 doc/bugs/Webapp_fails_to_resolve_ipv6_hostname.mdwn create mode 100644 doc/bugs/Weird_behaviour_of_direct_and_indirect_annexes.mdwn create mode 100644 doc/bugs/Weird_behaviour_of_direct_and_indirect_annexes/comment_1_56474a69c2f174d83be9137d3c045a47._comment create mode 100644 doc/bugs/When_syncing_two_repositories__44___git_annex_uses_9x_times_diskspace.mdwn create mode 100644 doc/bugs/When_syncing_two_repositories__44___git_annex_uses_9x_times_diskspace/comment_0._comment create mode 100644 doc/bugs/When_syncing_two_repositories__44___git_annex_uses_9x_times_diskspace/comment_10_037a6dd6e15ef5f789a1f364f7507b53._comment create mode 100644 doc/bugs/When_syncing_two_repositories__44___git_annex_uses_9x_times_diskspace/comment_11_614e4110188fc6474e7da50fc4281e13._comment create mode 100644 doc/bugs/When_syncing_two_repositories__44___git_annex_uses_9x_times_diskspace/comment_12_dcb74fb91e1c2f0db4efd68c8bcbc96c._comment create mode 100644 doc/bugs/When_syncing_two_repositories__44___git_annex_uses_9x_times_diskspace/comment_14_38671ba8d302f4d32460d1478abd2111._comment create mode 100644 doc/bugs/When_syncing_two_repositories__44___git_annex_uses_9x_times_diskspace/comment_14_483244b1ed5744308022465f45c091fd._comment create mode 100644 doc/bugs/When_syncing_two_repositories__44___git_annex_uses_9x_times_diskspace/comment_1_d2c63723fa4bf828873770a42ffaab20._comment create mode 100644 doc/bugs/When_syncing_two_repositories__44___git_annex_uses_9x_times_diskspace/comment_3_52f0db73dc38c3e3a73f6c7a420bf016._comment create mode 100644 doc/bugs/When_syncing_two_repositories__44___git_annex_uses_9x_times_diskspace/comment_4_93596b4d5a48ffcf4bc11ba9c83cf7ca._comment create mode 100644 doc/bugs/When_syncing_two_repositories__44___git_annex_uses_9x_times_diskspace/comment_5_de94e80dde6d12485140bb079d74d775._comment create mode 100644 doc/bugs/When_syncing_two_repositories__44___git_annex_uses_9x_times_diskspace/comment_6_5f34c3d449247b4bce4665b3ea4d054c._comment create mode 100644 doc/bugs/When_syncing_two_repositories__44___git_annex_uses_9x_times_diskspace/comment_7_b43ae8aec23ba3acaf70edc0de058710._comment create mode 100644 doc/bugs/When_syncing_two_repositories__44___git_annex_uses_9x_times_diskspace/comment_8_13b8e0a62f6b6d02960687e206a8b016._comment create mode 100644 doc/bugs/When_syncing_two_repositories__44___git_annex_uses_9x_times_diskspace/comment_9_818b94a74b01a210d1106dd35bc932d8._comment create mode 100644 doc/bugs/Windows_build_test_failures.mdwn create mode 100644 doc/bugs/Windows_installer_includes_curl_and_wget__44___but_not_required_DLLs.mdwn create mode 100644 doc/bugs/Windows_installer_includes_curl_and_wget__44___but_not_required_DLLs/comment_1_a7bf0f027f2209e5632e292afd7214d0._comment create mode 100644 doc/bugs/Windows_to_Linux_clone_-_Windows_drive_letters_cause_git_annex_get_to_fail.mdwn create mode 100644 doc/bugs/Windows_to_Linux_clone_-_Windows_drive_letters_cause_git_annex_get_to_fail/comment_1_c87bae87b7902db60a3fef41e1fca85d._comment create mode 100644 doc/bugs/With_S3__44___GPG_ask_for_a_new_passphrase.mdwn create mode 100644 doc/bugs/With_S3__44___GPG_ask_for_a_new_passphrase/comment_1_a4fc30bf7d39cae337286e9e815e6cba._comment create mode 100644 doc/bugs/With_S3__44___GPG_ask_for_a_new_passphrase/comment_2_e5d42b623017acedf6a3890ce15680a3._comment create mode 100644 doc/bugs/With_S3__44___GPG_ask_for_a_new_passphrase/comment_3_e5150b65b514896e14b9ad3d951963f7._comment create mode 100644 doc/bugs/With_S3__44___GPG_ask_for_a_new_passphrase/comment_4_47c2fc167b0c396edc40468fb7c7bfee._comment create mode 100644 doc/bugs/Won__39__t_drop_files__44___even_though_remote_annexes_have_at_least_numcopies.mdwn create mode 100644 doc/bugs/Wrong_port_while_configuring_ssh_remote.mdwn create mode 100644 doc/bugs/__34__Adding_4923_files__34___is_really_slow.mdwn create mode 100644 doc/bugs/__34__Adding_4923_files__34___is_really_slow/comment_2_5f3b9f00bc31ce71d695c008971ed7fd._comment create mode 100644 doc/bugs/__34__Adding_4923_files__34___is_really_slow/comment_2_708b02dd06a1eed6b5ded9eb7aa9e7a8._comment create mode 100644 doc/bugs/__34__Adding_4923_files__34___is_really_slow/comment_3_6a735b7875d2a0c92df6786dd649985d._comment create mode 100644 doc/bugs/__34__Adding_4923_files__34___is_really_slow/comment_4_7e768908ba6983ea13af27635c4a947f._comment create mode 100644 doc/bugs/__34__annex_sync__34___gets_confused_when_operating_in_a_symlink__39__ed_directory.mdwn create mode 100644 doc/bugs/__34__drop__34___deletes_all_files_with_identical_content.mdwn create mode 100644 doc/bugs/__34__drop__34___deletes_all_files_with_identical_content/comment_1_2eb20b65582fa7f271b1d0bb5560d08c._comment create mode 100644 doc/bugs/__34__drop__34___deletes_all_files_with_identical_content/comment_2_b14e1d31dd6a8fb930fcc0bec798e194._comment create mode 100644 doc/bugs/__34__drop__34___deletes_all_files_with_identical_content/comment_3_1892bcfbe3c462aa74552a241d65cad9._comment create mode 100644 doc/bugs/__34__drop__34___deletes_all_files_with_identical_content/comment_4_dfa0e31996eaa14e2945c1d11670c4d9._comment create mode 100644 doc/bugs/__34__drop__34___deletes_all_files_with_identical_content/comment_5_e2a9336cf1080c158765d4adfe72f26b._comment create mode 100644 doc/bugs/__34__fatal:_bad_config_file__34__.mdwn create mode 100644 doc/bugs/__34__git_annex_watch__34___adds_map.dot.mdwn create mode 100644 doc/bugs/__34__make_test__34___fails_silently.mdwn create mode 100644 doc/bugs/__34__make_test__34___fails_silently/comment_1_f868e34f41d828d4571968d1ab07820a._comment create mode 100644 doc/bugs/__34__make_test__34___fails_silently/comment_2_fb9e8e2716b0dea15b0d4807ae7cd114._comment create mode 100644 doc/bugs/__39__annex_add__39___fails_to___39__git_add__39___for_parent_relative_path.mdwn create mode 100644 doc/bugs/__39__client__39___repo_starts_pulling_in___39__archive__39___content.mdwn create mode 100644 doc/bugs/__39__client__39___repo_starts_pulling_in___39__archive__39___content/comment_1_56f9cd5cc2e089b32cb076dc2e2a8ca5._comment create mode 100644 doc/bugs/__39__client__39___repo_starts_pulling_in___39__archive__39___content/comment_2_21c0f7f328cb51080fbd97e086c47a30._comment create mode 100644 doc/bugs/__39__client__39___repo_starts_pulling_in___39__archive__39___content/comment_3_3287b2f25f3b5ae4c27f4748694563ee._comment create mode 100644 doc/bugs/__39__client__39___repo_starts_pulling_in___39__archive__39___content/comment_4_e515eca68a70d40c522805d7e0d7c0e6._comment create mode 100644 doc/bugs/__39__client__39___repo_starts_pulling_in___39__archive__39___content/comment_5_b27f4c103dda050b6e9cf03ea3157abc._comment create mode 100644 doc/bugs/__39__client__39___repo_starts_pulling_in___39__archive__39___content/comment_6_2cc7083dab944705bf91fc00319b75e6._comment create mode 100644 doc/bugs/__39__client__39___repo_starts_pulling_in___39__archive__39___content/comment_7_1175f9be789d4c1907f0be98e435bd2f._comment create mode 100644 doc/bugs/__39__client__39___repo_starts_pulling_in___39__archive__39___content/comment_8_78e6164ef67a9560a3a9ead1f7a72473._comment create mode 100644 doc/bugs/__39__client__39___repo_starts_pulling_in___39__archive__39___content/comment_9_1d578fd13022dcd6382b415a7f6e097a._comment create mode 100644 doc/bugs/__91__Installation__93___There_is_no_available_version_of_quickcheck_that_satisfies___62____61__2.1.mdwn create mode 100644 doc/bugs/__91__webapp__93___pause_syncing_with_specific_repository.mdwn create mode 100644 doc/bugs/__96__git_annex_add__96___changes_mtime_if_symlinks_are_fixed_in_the_background.mdwn create mode 100644 doc/bugs/__96__git_annex_fix__96___run_on_non-annexed_files_is_no-op.mdwn create mode 100644 doc/bugs/__96__git_annex_fix__96___run_on_non-annexed_files_is_no-op/comment_1_9b671e583eec5adf870dccd1e97b5dbc._comment create mode 100644 doc/bugs/__96__git_annex_fix__96___run_on_non-annexed_files_is_no-op/comment_2_d11744202213d6f897f4234bc4c70c18._comment create mode 100644 doc/bugs/__96__git_annex_fix__96___run_on_non-annexed_files_is_no-op/comment_3_a729deb465ff44f5a9b87c963cd6235a._comment create mode 100644 doc/bugs/__96__git_annex_fix__96___run_on_non-annexed_files_is_no-op/comment_4_3f735503df9a08472d42fabd219c2ec5._comment create mode 100644 doc/bugs/__96__git_annex_fix__96___run_on_non-annexed_files_is_no-op/comment_5_2c61eabbba7fd2a52ba02d59a0a76a42._comment create mode 100644 doc/bugs/__96__git_annex_import__96___clobbers_mtime.mdwn create mode 100644 doc/bugs/__96__git_annex_import__96___clobbers_mtime/comment_1_d173f2903faf4bff115a0be02c146ce9._comment create mode 100644 doc/bugs/__96__git_annex_import__96___clobbers_mtime/comment_2_3563d9eeb9806f8ca1b9b340925837f5._comment create mode 100644 doc/bugs/__96__git_annex_import__96___clobbers_mtime/comment_3_d5c7488db16b71c4f337662c897278ca._comment create mode 100644 doc/bugs/__96__git_annex_sync__96___ignores_remotes.mdwn create mode 100644 doc/bugs/__96__git_annex_sync__96___ignores_remotes/comment_1_39421e6935233cd8f45949ebdef369fe._comment create mode 100644 doc/bugs/__96__git_annex_sync__96___ignores_remotes/comment_2_53fb15d6fbf96d43564ff7c866239d18._comment create mode 100644 doc/bugs/_impossible_to_switch_repositories_on_android__in_webapp.mdwn create mode 100644 doc/bugs/_impossible_to_switch_repositories_on_android__in_webapp/comment_1_d488d71a72eb54d7711d2a867db6172f._comment create mode 100644 doc/bugs/_impossible_to_switch_repositories_on_android__in_webapp/comment_2_85b31db6d0fb2d20018db3d8c8258bf4._comment create mode 100644 doc/bugs/acl_not_honoured_in_rsync_remote.mdwn create mode 100644 doc/bugs/acl_not_honoured_in_rsync_remote/comment_1_aa6fe1d7b029eae7ee71c97e0f0937a6._comment create mode 100644 doc/bugs/acl_not_honoured_in_rsync_remote/comment_2_ffb9424e966ee10a4fe2d446b3042cb2._comment create mode 100644 doc/bugs/add_range_argument_to___34__git_annex_dropunused__34___.mdwn create mode 100644 doc/bugs/add_script-friendly_output_options.mdwn create mode 100644 doc/bugs/added_branches_makes___39__git_annex_unused__39___slow.mdwn create mode 100644 doc/bugs/added_branches_makes___39__git_annex_unused__39___slow/comment_1_d350c39c67031c500e3224e92c0029ea._comment create mode 100644 doc/bugs/added_branches_makes___39__git_annex_unused__39___slow/comment_2_b2d2b1caa51ffec3d87c36b373cb8d4a._comment create mode 100644 doc/bugs/added_branches_makes___39__git_annex_unused__39___slow/comment_3_12b20cbbc2b4cd1ab8af7e3eec9589b4._comment create mode 100644 doc/bugs/added_branches_makes___39__git_annex_unused__39___slow/comment_4_a50b43c15d2650df90f0fa1ced47f532._comment create mode 100644 doc/bugs/added_branches_makes___39__git_annex_unused__39___slow/comment_5_7328bc51bd001f2b732a92a2ae175839._comment create mode 100644 doc/bugs/added_branches_makes___39__git_annex_unused__39___slow/comment_6_880ef2ee797221332dbb629b2d55522f._comment create mode 100644 doc/bugs/added_branches_makes___39__git_annex_unused__39___slow/comment_7_826fd82cdf9b1c79c9b555ca26c2c176._comment create mode 100644 doc/bugs/adding_an_rsync.net_repo_give_an_gpg_error.mdwn create mode 100644 doc/bugs/adding_an_rsync.net_repo_give_an_gpg_error/comment_1_f55cfc133be72ac10cae93c877c487df._comment create mode 100644 doc/bugs/adding_an_rsync.net_repo_give_an_gpg_error/comment_2_24dd024ac4b21a82a781343b8fe3891e._comment create mode 100644 doc/bugs/addurl:___34__rename:_does_not_exist__34___and_reports_fail__44___but_succeeds.mdwn create mode 100644 doc/bugs/addurl:___34__rename:_does_not_exist__34___and_reports_fail__44___but_succeeds/comment_1_1f5e0bc93631baf0f8c1bec2e68493c5._comment create mode 100644 doc/bugs/addurl_--relaxed_with_--file_doesn__39__t_actually_relax.mdwn create mode 100644 doc/bugs/addurl_file_doesn__39__t_work_with_spaces_in_filenames_and_--fast.mdwn create mode 100644 doc/bugs/addurl_file_doesn__39__t_work_with_spaces_in_filenames_and_--fast/comment_1_eea9477ea1157cb88c8a07d8da5f0dba._comment create mode 100644 doc/bugs/allows_repository_with_the_same_name_twice.mdwn create mode 100644 doc/bugs/allows_repository_with_the_same_name_twice/comment_1_ba7801403e7138684704a3471c8bc4a6._comment create mode 100644 doc/bugs/allows_repository_with_the_same_name_twice/comment_2_8c19a4ddedbe7ddb8bdcf84acac68cc8._comment create mode 100644 doc/bugs/android_4.2.1__44___galaxy_nexus_java.lang.SecurityException.mdwn create mode 100644 doc/bugs/annex-rsync-options_shell-split_carelessly.mdwn create mode 100644 doc/bugs/annex-rsync-options_shell-split_carelessly/comment_1_2636e0d224317f2e6db94658d8a094c4._comment create mode 100644 doc/bugs/annex.numcopies_not_overriden_by_--numcopies_option.mdwn create mode 100644 doc/bugs/annex_add_in_annex.mdwn create mode 100644 doc/bugs/annex_get_fails:___34__No_such_file_or_directory__34__.mdwn create mode 100644 doc/bugs/annex_get_over_SSH_is_very_slow.mdwn create mode 100644 doc/bugs/annex_unannex__47__uninit_should_handle_copies.mdwn create mode 100644 doc/bugs/annex_unannex__47__uninit_should_handle_copies/comment_1_c896ff6589f62178b60e606771e4f2bf._comment create mode 100644 doc/bugs/annex_unannex__47__uninit_should_handle_copies/comment_2_9249609f83f8e9c7521cd2f007c1a39e._comment create mode 100644 doc/bugs/another_build_error_in_assistant.mdwn create mode 100644 doc/bugs/archiving_git_repositories.mdwn create mode 100644 doc/bugs/archiving_git_repositories/comment_1_51f546a571303118446a9e0b3e6482c9._comment create mode 100644 doc/bugs/assistant_-_GTalk_collision.mdwn create mode 100644 doc/bugs/assistant_-_GTalk_collision/comment_1_ab2c1f36113d40f27e1893d32f214296._comment create mode 100644 doc/bugs/assistant_-_GTalk_collision/comment_2_91dff34c629a3b3a97a2313ff077e4ae._comment create mode 100644 doc/bugs/assistant_-_GTalk_collision/comment_3_fefb73f6e570f96b4d82779d6622f690._comment create mode 100644 doc/bugs/assistant___40__OS_X_Lion__41___-___34__too_many_open_files__34___error.mdwn create mode 100644 doc/bugs/assistant___40__OS_X_Lion__41___-___34__too_many_open_files__34___error/comment_1_9904c30a4c24a699d71e90ce5e9b89cf._comment create mode 100644 doc/bugs/assistant_cannot_set_up_remote_repo_via_an_ssh_alias_or_an_ip_address/comment_1_1650539846521ae11837e4ac73348af6._comment create mode 100644 doc/bugs/assistant_cannot_set_up_remote_repo_via_an_ssh_alias_or_an_ip_address/comment_2_b91415e4ee74eb12bc6e6faddd00af6e._comment create mode 100644 doc/bugs/assistant_does_not_always_use_repo_cost_info_when_queueing_downloads.mdwn create mode 100644 doc/bugs/assistant_does_not_list_remote___39__origin__39__.mdwn create mode 100644 doc/bugs/assistant_does_not_list_remote___39__origin__39__/comment_1_ffa008240c61b50396aa92f467731db6._comment create mode 100644 doc/bugs/assistant_does_not_list_remote___39__origin__39__/comment_2_a53f80090bc2a0f32b8d8307cb24b563._comment create mode 100644 doc/bugs/assistant_does_not_warn_on_files_it_failed_to_add.mdwn create mode 100644 doc/bugs/assistant_does_not_warn_on_files_it_failed_to_add/comment_1_13b2f93b7d09c8fd6c22829a0dc6428b._comment create mode 100644 doc/bugs/assistant_does_not_warn_on_files_it_failed_to_add/comment_2_94e46bc0044b8a91a9fd51058825aa8f._comment create mode 100644 doc/bugs/assistant_does_not_warn_on_files_it_failed_to_add/comment_3_10a38bdbf31dd4071e4bc4ac746d9c56._comment create mode 100644 doc/bugs/assistant_does_not_warn_on_files_it_failed_to_add/comment_4_b8fdf502c7e80aece5a9544a2078c85c._comment create mode 100644 doc/bugs/assistant_does_not_warn_on_files_it_failed_to_add/comment_5_a2ff7668f2a0d549b362d7de97fac8a1._comment create mode 100644 doc/bugs/assistant_does_not_warn_on_files_it_failed_to_add/comment_6_60d72f34a6cfd1c081f74aa610f4305a._comment create mode 100644 doc/bugs/assistant_does_not_warn_on_files_it_failed_to_add/comment_7_53a73e662c9356b759fbfa1e5a3bd927._comment create mode 100644 doc/bugs/assistant_does_not_warn_on_files_it_failed_to_add/comment_8_10b65168b6a54d960427966d7e3d05f5._comment create mode 100644 doc/bugs/assistant_does_not_warn_on_files_it_failed_to_add/comment_9_b640e8fa6aafb041d66bbf8857a8fa3d._comment create mode 100644 doc/bugs/assistant_doesn__39__t_sync_empty_directories.mdwn create mode 100644 doc/bugs/assistant_doesn__39__t_sync_empty_directories/comment_1_78a3bde607f43c0f518bd2d3d7196022._comment create mode 100644 doc/bugs/assistant_doesn__39__t_sync_empty_directories/comment_2_83777384b72732b1d0a19b32686d3d1f._comment create mode 100644 doc/bugs/assistant_doesn__39__t_sync_file_permissions.mdwn create mode 100644 doc/bugs/assistant_doesn__39__t_sync_file_permissions/comment_1_fc8d3ea209a2ab39c1aeff52452d4c58._comment create mode 100644 doc/bugs/assistant_doesn__39__t_sync_file_permissions/comment_2_1a364c422e0dd7418f74e1cc3d543a3c._comment create mode 100644 doc/bugs/assistant_hangs_during_commit.mdwn create mode 100644 doc/bugs/assistant_hangs_during_commit/comment_1_aacc15c589d2795254387e427b3afe0c._comment create mode 100644 doc/bugs/assistant_hangs_during_commit/comment_2_b9f1bf9fa919603dca28182c80d39a11._comment create mode 100644 doc/bugs/assistant_hangs_during_commit/comment_3_fb5be10fcf5e7c89da5c34f48539612f._comment create mode 100644 doc/bugs/assistant_hangs_during_commit/comment_4_9ba7efe9112578729d02ac4e6557b3cc._comment create mode 100644 doc/bugs/assistant_hangs_during_commit/comment_5_73b24c901c73d41e0e0abe91267d4920._comment create mode 100644 doc/bugs/assistant_hangs_during_commit/comment_6_1a30b8c82e58222f1366aa368c23e6d3._comment create mode 100644 doc/bugs/assistant_hangs_during_commit/comment_7_56868b2a504ad0a60e8a8c1928330175._comment create mode 100644 doc/bugs/assistant_ignore_.gitignore.mdwn create mode 100644 doc/bugs/assistant_ignore_.gitignore/comment_1_3458b1342cb2e3ccc01eeedc7f0e48fc._comment create mode 100644 doc/bugs/assistant_ignores___34__manual__34___group__44___tries_to_transfer_files.mdwn create mode 100644 doc/bugs/assistant_ignores___34__manual__34___group__44___tries_to_transfer_files/comment_1_e3f545d9adc27a4e7340bf16177c4fe0._comment create mode 100644 doc/bugs/assistant_ignores___34__manual__34___group__44___tries_to_transfer_files/comment_2_1403076dbc47733607f0c8b2856e2381._comment create mode 100644 doc/bugs/assistant_ignores___34__manual__34___group__44___tries_to_transfer_files/comment_3_af83717bfb260bea6d52ff71c6b34743._comment create mode 100644 doc/bugs/assistant_ignores___34__manual__34___group__44___tries_to_transfer_files/comment_4_b4f811611d14e7392009c539fa6b8574._comment create mode 100644 doc/bugs/assistant_listens_on_127.0.0.1_not_::1_which_breaks_IPv6_enabled_hosts.mdwn create mode 100644 doc/bugs/assistant_listens_on_127.0.0.1_not_::1_which_breaks_IPv6_enabled_hosts/comment_1_91a62a2ce14a1027d2ac8b8e88df5f0c._comment create mode 100644 doc/bugs/assistant_listens_on_127.0.0.1_not_::1_which_breaks_IPv6_enabled_hosts/comment_2_4982cd373eaaeee180be03c6e9fda7b1._comment create mode 100644 doc/bugs/assistant_listens_on_127.0.0.1_not_::1_which_breaks_IPv6_enabled_hosts/comment_3_85d264e311acaa91dac0597ee8deda82._comment create mode 100644 doc/bugs/assistant_not_noticing_file_renames__44___not_fixing_files.mdwn create mode 100644 doc/bugs/assistant_not_noticing_file_renames__44___not_fixing_files/comment_1_e0dafc410ffd617d445bb9403c7bfafe._comment create mode 100644 doc/bugs/assistant_not_noticing_file_renames__44___not_fixing_files/comment_2_2af247c8a1fcbde10795a990ef3303e9._comment create mode 100644 doc/bugs/assistant_syncs_with_remotes_even_when_all_remotes_disabled.mdwn create mode 100644 doc/bugs/assistant_wants_my_gpg_passphrase_when_it_has_nothing_to_drop_or_copy_to_that_remote.mdwn create mode 100644 doc/bugs/assistant_wants_my_gpg_passphrase_when_it_has_nothing_to_drop_or_copy_to_that_remote/comment_1_10a9570a5d762ba2da271b38dc63edb6._comment create mode 100644 doc/bugs/assistant_wants_my_gpg_passphrase_when_it_has_nothing_to_drop_or_copy_to_that_remote/comment_2_57d50955b038c2e2405068536c7e83f3._comment create mode 100644 doc/bugs/assistant_wants_my_gpg_passphrase_when_it_has_nothing_to_drop_or_copy_to_that_remote/comment_3_a66f34daaba421c87eb404ef933e5191._comment create mode 100644 doc/bugs/assistant_wants_my_gpg_passphrase_when_it_has_nothing_to_drop_or_copy_to_that_remote/comment_4_094a3272eca1c6d2b4d264911ffe96e5._comment create mode 100644 doc/bugs/assistant_wants_my_gpg_passphrase_when_it_has_nothing_to_drop_or_copy_to_that_remote/comment_5_0161410d042a3421addd4a1fc7c1cd01._comment create mode 100644 doc/bugs/authentication_to_rsync.net_fails.mdwn create mode 100644 doc/bugs/backend_version_upgrade_leaves_repo_unusable.mdwn create mode 100644 doc/bugs/bad_behaviour_with_file_names_with_newline_in_them.mdwn create mode 100644 doc/bugs/bad_behaviour_with_file_names_with_newline_in_them/comment_1_92dfe6e9089c79eb64e2177fb135ef55._comment create mode 100644 doc/bugs/bad_comment_in_ssh_public_key_ssh-rsa.mdwn create mode 100644 doc/bugs/bad_comment_in_ssh_public_key_ssh-rsa/comment_1_15cce6e6f455e83f4362a38c561bc973._comment create mode 100644 doc/bugs/bad_comment_in_ssh_public_key_ssh-rsa/comment_2_e9e1f38880a32610b3fbce475bffc3e4._comment create mode 100644 doc/bugs/bare_git_repos.mdwn create mode 100644 doc/bugs/bug_in_download_prebuilt_linux_tarball__44___and_constraints_issues_with_3.20121112.mdwn create mode 100644 doc/bugs/build_failure_with_kqueue_code__44___first_commit_that_breaks_is_3dce75fb23fca94ad86c3f0ee816bb0ad2ecb27c.mdwn create mode 100644 doc/bugs/build_is_broken_at_commit_cc0e5b7.mdwn create mode 100644 doc/bugs/build_issue_with_8baff14054e65ecbe801eb66786a55fa5245cb30.mdwn create mode 100644 doc/bugs/build_issue_with_latest_release_0.20110522-1-gde817ba.mdwn create mode 100644 doc/bugs/build_problem_on_OSX.mdwn create mode 100644 doc/bugs/building_on_lenny.mdwn create mode 100644 doc/bugs/bup_initremote_failed_with_localhost_+_username.mdwn create mode 100644 doc/bugs/bup_initremote_failed_with_localhost_+_username/comment_1_0e669c3039b089fa8a815d3ec11465d2._comment create mode 100644 doc/bugs/cabal_build:___34__Could_not_find_module___96__Data.AssocList__39____34__.mdwn create mode 100644 doc/bugs/cabal_build:___34__Could_not_find_module___96__Data.AssocList__39____34__/comment_1_0da9fd67c3cc01b316f95a1df4eb62ae._comment create mode 100644 doc/bugs/cabal_configure_is_broken_on_OSX_builds.mdwn create mode 100644 doc/bugs/cabal_install_on_Ubuntu_12.04_fails_with_complaint_about_regex-base.mdwn create mode 100644 doc/bugs/call_to_git-annex-shell_when_on_centralised___40__non-git-annex__41___repository.mdwn create mode 100644 doc/bugs/call_to_git-annex-shell_when_on_centralised___40__non-git-annex__41___repository/comment_1_dd202a7764d9df998868d595a86ffb21._comment create mode 100644 doc/bugs/call_to_git-annex-shell_when_on_centralised___40__non-git-annex__41___repository/comment_2_ca065c82ac8e3215b581660f3e44f459._comment create mode 100644 doc/bugs/call_to_git-annex-shell_when_on_centralised___40__non-git-annex__41___repository/comment_3_927a01f9961c71bedb42c519a31b5fe5._comment create mode 100644 doc/bugs/can__39__t_annex_get_from_annex_in_direct_mode.mdwn create mode 100644 doc/bugs/can__39__t_annex_get_from_annex_in_direct_mode/comment_1_20c31a844d8351a99cf69e05d2836e0e._comment create mode 100644 doc/bugs/can__39__t_annex_get_from_annex_in_direct_mode/comment_2_f26e0f763f9027d9dfc08cd840ced153._comment create mode 100644 doc/bugs/can__39__t_run_the_assistant_from_the_command_line_anymore__63__.mdwn create mode 100644 doc/bugs/can_not_add_ssh_remote_to_assistant_with___34__host:port__34___syntax.mdwn create mode 100644 doc/bugs/can_not_add_ssh_remote_to_assistant_with___34__host:port__34___syntax/comment_1_397eb359c3f8ef30460a9556b6f55848._comment create mode 100644 doc/bugs/cannot_add_file__44___get___34__user_error__34__.mdwn create mode 100644 doc/bugs/cannot_add_file__44___get___34__user_error__34__/comment_1_14aa717c1befcbbf526f25ca2f0af825._comment create mode 100644 doc/bugs/cannot_add_file__44___get___34__user_error__34__/comment_2_7f7ac59e7f3dce9d7a7d0c3379c2edcf._comment create mode 100644 doc/bugs/cannot_add_file__44___get___34__user_error__34__/comment_3_5ebf03120b12edb3fbb8954546e7603e._comment create mode 100644 doc/bugs/cannot_add_file__44___get___34__user_error__34__/comment_4_1ba6d2614778949520b47896fd98b598._comment create mode 100644 doc/bugs/cannot_add_file__44___get___34__user_error__34__/comment_5_4a6e55861a63b350a02edb888b4da99b._comment create mode 100644 doc/bugs/cannot_connect_to_xmpp_server.mdwn create mode 100644 doc/bugs/cannot_connect_to_xmpp_server/comment_10_5072de8fcca9fe70bc235ea8c8ee2877._comment create mode 100644 doc/bugs/cannot_connect_to_xmpp_server/comment_11_dabd74bba1f38b326a2d0c86d3027cd9._comment create mode 100644 doc/bugs/cannot_connect_to_xmpp_server/comment_12_0245b426cc0ab64f8c167b8806b03f5d._comment create mode 100644 doc/bugs/cannot_connect_to_xmpp_server/comment_1_307df11b5bcf289d7999e1e7f7c461c9._comment create mode 100644 doc/bugs/cannot_connect_to_xmpp_server/comment_2_f24378cf30a7d32594da90749fabec3c._comment create mode 100644 doc/bugs/cannot_connect_to_xmpp_server/comment_3_4b07093be844ac62b611cee1dfde5aa7._comment create mode 100644 doc/bugs/cannot_connect_to_xmpp_server/comment_4_fe1ed152a485c4aebfa9b9f300101835._comment create mode 100644 doc/bugs/cannot_connect_to_xmpp_server/comment_5_2d311f520aee04287df6bddfd8535734._comment create mode 100644 doc/bugs/cannot_connect_to_xmpp_server/comment_6_d9f916f012184738446c5996ee9d2270._comment create mode 100644 doc/bugs/cannot_connect_to_xmpp_server/comment_7_0b5f9350e2367301241c7668a15815ef._comment create mode 100644 doc/bugs/cannot_connect_to_xmpp_server/comment_8_f00b6ae154004e405f0bd23b7359962e._comment create mode 100644 doc/bugs/cannot_connect_to_xmpp_server/comment_9_41b86468013da15f46be29da520afa10._comment create mode 100644 doc/bugs/case-insensitive.mdwn create mode 100644 doc/bugs/case_sensitivity_on_FAT.mdwn create mode 100644 doc/bugs/check_for_curl_in_configure.hs.mdwn create mode 100644 doc/bugs/com.branchable.git-annex.assistant.plist_is_invalid.mdwn create mode 100644 doc/bugs/commitBuffer:_invalid_argument___40__invalid_character__41__.mdwn create mode 100644 doc/bugs/commit_f20a40f_breaks_on_OSX_as_mntent.h_doesn__39__t_exist.mdwn create mode 100644 doc/bugs/concurrent_git-annex_processes_can_lead_to_locking_issues.mdwn create mode 100644 doc/bugs/configurable_path_to_git-annex-shell.mdwn create mode 100644 doc/bugs/configurable_path_to_git-annex-shell/comment_1_fb6771f902b57f2b690e7cc46fdac47e._comment create mode 100644 doc/bugs/configurable_path_to_git-annex-shell/comment_2_2b856f4f0b65c2331be7d565f0e4e8a8._comment create mode 100644 doc/bugs/configurable_path_to_git-annex-shell/comment_3_aea42acc039a82efc6bb3a8f173a632e._comment create mode 100644 doc/bugs/configure_mistakes_hashalot_bins_for_sha__63____63____63__sum_and_builds_a_broken_git-annex_executable.mdwn create mode 100644 doc/bugs/configure_script_should_detect_uuidgen_instead_of_just_uuid.mdwn create mode 100644 doc/bugs/conflicting_haskell_packages.mdwn create mode 100644 doc/bugs/conflicting_haskell_packages/comment_1_e552a6cc6d7d1882e14130edfc2d6b3b._comment create mode 100644 doc/bugs/conq:_invalid_command_syntax.mdwn create mode 100644 doc/bugs/conq:_invalid_command_syntax/comment_1_f33b83025ce974e496f83f248275a66a._comment create mode 100644 doc/bugs/conq:_invalid_command_syntax/comment_2_195106ca8dedad5f4d755f625e38e8af._comment create mode 100644 doc/bugs/conq:_invalid_command_syntax/comment_3_55af43e2f43a4c373f7a0a33678d0b1c._comment create mode 100644 doc/bugs/copy_doesn__39__t_scale.mdwn create mode 100644 doc/bugs/copy_doesn__39__t_scale/comment_1_7c12499c9ac28a9883c029f8c659eb57._comment create mode 100644 doc/bugs/copy_doesn__39__t_scale/comment_2_f85d8023cdbc203bb439644cf7245d4e._comment create mode 100644 doc/bugs/copy_doesn__39__t_scale/comment_3_4592765c3d77bb5664b8d16867e9d79c._comment create mode 100644 doc/bugs/copy_fast_confusing_with_broken_locationlog.mdwn create mode 100644 doc/bugs/copy_fast_confusing_with_broken_locationlog/comment_10_435f87d54052f264096a8f23e99eae06._comment create mode 100644 doc/bugs/copy_fast_confusing_with_broken_locationlog/comment_11_9be0aef403a002c1706d17deee45763c._comment create mode 100644 doc/bugs/copy_fast_confusing_with_broken_locationlog/comment_12_26d60661196f63fd01ee4fbb6e2340e7._comment create mode 100644 doc/bugs/copy_fast_confusing_with_broken_locationlog/comment_13_ead55b915d3b92a62549b2957ad211c8._comment create mode 100644 doc/bugs/copy_fast_confusing_with_broken_locationlog/comment_14_191de89d3988083d9cf001799818ff4a._comment create mode 100644 doc/bugs/copy_fast_confusing_with_broken_locationlog/comment_15_b3e3b338ccfa0a32510c78ba1b1bb617._comment create mode 100644 doc/bugs/copy_fast_confusing_with_broken_locationlog/comment_16_04a9f4468c3246c8eff3dbe21dd90101._comment create mode 100644 doc/bugs/copy_fast_confusing_with_broken_locationlog/comment_1_6a41bf7e2db83db3a01722b516fb6886._comment create mode 100644 doc/bugs/copy_fast_confusing_with_broken_locationlog/comment_2_9f5f1dbffb2dd24f4fcf8c2027bf0384._comment create mode 100644 doc/bugs/copy_fast_confusing_with_broken_locationlog/comment_3_b596b5cfd3377e58dbbb5d509d026b90._comment create mode 100644 doc/bugs/copy_fast_confusing_with_broken_locationlog/comment_4_d7112c315fb016a8a399e24e9b6461d8._comment create mode 100644 doc/bugs/copy_fast_confusing_with_broken_locationlog/comment_5_4ea29a6f8152eddf806c536de33ef162._comment create mode 100644 doc/bugs/copy_fast_confusing_with_broken_locationlog/comment_6_0d85f114a103bd6532a3b3b24466012e._comment create mode 100644 doc/bugs/copy_fast_confusing_with_broken_locationlog/comment_7_d38d5bee6d360b0ea852f39e3a7b1bc6._comment create mode 100644 doc/bugs/copy_fast_confusing_with_broken_locationlog/comment_8_29c3de4bf5fbd990b230c443c0303cbe._comment create mode 100644 doc/bugs/copy_fast_confusing_with_broken_locationlog/comment_9_2cee4f6bd6db7518fd61453c595162c6._comment create mode 100644 doc/bugs/creating_a_plain_directory_where_a_mountpoint_should_have_been.mdwn create mode 100644 doc/bugs/creating_a_plain_directory_where_a_mountpoint_should_have_been/comment_1_41cfd5e48426a6ef52bef70a06a6f46a._comment create mode 100644 doc/bugs/creating_a_plain_directory_where_a_mountpoint_should_have_been/comment_2_bd584ccbe128427fca99e61d66d301c9._comment create mode 100644 doc/bugs/creating_a_plain_directory_where_a_mountpoint_should_have_been/comment_3_5bb0347215b321444643646f25a35759._comment create mode 100644 doc/bugs/creating_a_plain_directory_where_a_mountpoint_should_have_been/comment_4_73848a9c783ecf3d9fccdd41b20fbe36._comment create mode 100644 doc/bugs/creating_a_remote_server_repository.mdwn create mode 100644 doc/bugs/creating_a_remote_server_repository/comment_1_de1a370347428245bcfca60eaca96779._comment create mode 100644 doc/bugs/creds_directory_not_automatically_created.mdwn create mode 100644 doc/bugs/cyclic_drop.mdwn create mode 100644 doc/bugs/data_loss_with___34__git_annex_add__34___on_android_in_direct_mode_.mdwn create mode 100644 doc/bugs/data_loss_with___34__git_annex_add__34___on_android_in_direct_mode_/comment_1_eb6db7f6a156a065e2724c2de5fc4366._comment create mode 100644 doc/bugs/data_loss_with___34__git_annex_add__34___on_android_in_direct_mode_/comment_2_59a96cade9e4881767562a139fc7fb4b._comment create mode 100644 doc/bugs/data_loss_with___34__git_annex_add__34___on_android_in_direct_mode_/comment_3_bf9d2562d66f0f6a9478ac178606cf4e._comment create mode 100644 doc/bugs/data_loss_with___34__git_annex_add__34___on_android_in_direct_mode_/comment_4_ad0dbdc448fff2e126ffec9aac6d7463._comment create mode 100644 doc/bugs/data_loss_with___34__git_annex_add__34___on_android_in_direct_mode_/comment_5_e828585e56b10710598143483ce362b6._comment create mode 100644 doc/bugs/direct_mode_assistant_in_subdir_confusion.mdwn create mode 100644 doc/bugs/direct_mode_assistant_in_subdir_confusion/comment_1_351143deec29e712f8718a373ad650d7._comment create mode 100644 doc/bugs/direct_mode_renames.mdwn create mode 100644 doc/bugs/direct_mode_renames/comment_1_f18c335e0d6f4259d2470935ef391cb8._comment create mode 100644 doc/bugs/directory_remote_and_case_sensitivity_on_FAT/comment_1_bcac9fd7b3f4a2ac28bee59bae674fa0._comment create mode 100644 doc/bugs/directory_remote_and_case_sensitivity_on_FAT/comment_2_c9088060fb9133b66951f1a3075981e8._comment create mode 100644 doc/bugs/directory_remote_and_case_sensitivity_on_FAT/comment_3_5bf34466187cfc9b34bd3ca8c89a07c6._comment create mode 100644 doc/bugs/directory_remote_and_case_sensitivity_on_FAT/comment_4_d6201f2d86d5b44051a7fd7a8c9de583._comment create mode 100644 doc/bugs/directory_remote_and_case_sensitivity_on_FAT/comment_5_61c5f0889f30a68ac3b57c4ea564ee0e._comment create mode 100644 doc/bugs/done.mdwn create mode 100644 doc/bugs/dotdot_problem.mdwn create mode 100644 doc/bugs/drop_fails_to_see_copies_that_whereis_sees.mdwn create mode 100644 doc/bugs/drop_fails_to_see_copies_that_whereis_sees/comment_1_f5a9d99d90daf5eba4773d361fa1807a._comment create mode 100644 doc/bugs/drop_fails_to_see_copies_that_whereis_sees/comment_2_040aa454cd8acd2857ef36884465576f._comment create mode 100644 doc/bugs/drop_fails_to_see_copies_that_whereis_sees/comment_3_f5d8faab325ee26800ecad5aba49b54b._comment create mode 100644 doc/bugs/dropping_and_re-adding_from_web_remotes_doesn__39__t_work.mdwn create mode 100644 doc/bugs/dropping_files_with_a_URL_backend_fails.mdwn create mode 100644 doc/bugs/dropunused_doesn__39__t_handle_double_spaces_in_filename.mdwn create mode 100644 doc/bugs/dropunused_doesn__39__t_work_in_my_case__63__.mdwn create mode 100644 doc/bugs/encfs_accused_of_being_crippled.mdwn create mode 100644 doc/bugs/encfs_accused_of_being_crippled/comment_1_5c5be012e1171ef108f38825d72791b6._comment create mode 100644 doc/bugs/encrypted_S3_stalls.mdwn create mode 100644 doc/bugs/encryption_given_a_gpg_keyid_still_uses_symmetric_encryption.mdwn create mode 100644 doc/bugs/encryption_given_a_gpg_keyid_still_uses_symmetric_encryption/comment_1_2f4ec4b7b92a0f0a0c4c0758da4a05a5._comment create mode 100644 doc/bugs/encryption_given_a_gpg_keyid_still_uses_symmetric_encryption/comment_2_7c0aeae6b1b2b0338735b0231c5db7d4._comment create mode 100644 doc/bugs/encryption_key_is_surprising.mdwn create mode 100644 doc/bugs/encryption_key_is_surprising/comment_1_5b172830ac31d51a1687bc8b1db489f9._comment create mode 100644 doc/bugs/encryption_key_is_surprising/comment_2_5b7e6bb36c3333dfd71808e8b4544746._comment create mode 100644 doc/bugs/encryption_key_is_surprising/comment_4_8ec86b8c35bce15337a143e275961cd5._comment create mode 100644 doc/bugs/encryption_key_is_surprising/comment_4_c5e49b3a0eceabe6d14f5226d7ba4c7a._comment create mode 100644 doc/bugs/encryption_key_is_surprising/comment_5_cd7cbf0c0ee9cafec344dfbf1acd9590._comment create mode 100644 doc/bugs/encryption_key_is_surprising/comment_6_01381524114d885961704acc3f172536._comment create mode 100644 doc/bugs/encryption_key_is_surprising/comment_7_c1eb59e1c5f583dcef7cea17623a2435._comment create mode 100644 doc/bugs/error_building_git-annex_3.20120624_using_cabal.mdwn create mode 100644 doc/bugs/error_on_only_repository_copy_deletion.mdwn create mode 100644 doc/bugs/error_on_only_repository_copy_deletion/comment_1_af394ac0956ab33a77256bcb02ef2a0f._comment create mode 100644 doc/bugs/error_propigation.mdwn create mode 100644 doc/bugs/error_when_using_repositories_with_non-ASCII_characters.mdwn create mode 100644 doc/bugs/error_when_using_repositories_with_non-ASCII_characters/comment_1_38cc2d2ed907649df085de8ad83cb9dd._comment create mode 100644 doc/bugs/error_with_file_names_starting_with_dash.mdwn create mode 100644 doc/bugs/extraneous_shell_escaping_for_rsync_remotes.mdwn create mode 100644 doc/bugs/fails_to_handle_lot_of_files.mdwn create mode 100644 doc/bugs/fails_to_handle_lot_of_files/comment_1_09d8e4e66d8273fab611bd29e82dc7fc._comment create mode 100644 doc/bugs/fails_to_handle_lot_of_files/comment_2_fd2ec05f4b5a7a6ae6bd9f5dbc3156de._comment create mode 100644 doc/bugs/failure_to_find_file_that___34__should__34___exist_in_remote_is_silent.mdwn create mode 100644 doc/bugs/failure_to_find_file_that___34__should__34___exist_in_remote_is_silent/comment_1_c686df2824d3f588c0bfb339c99168b7._comment create mode 100644 doc/bugs/failure_to_find_file_that___34__should__34___exist_in_remote_is_silent/comment_2_22edfac4ce25cd9f4e4c85e0a8a52bc1._comment create mode 100644 doc/bugs/failure_to_find_file_that___34__should__34___exist_in_remote_is_silent/comment_3_74fc0e41a6bd5c4d8c4b2f15e5ed8d2f._comment create mode 100644 doc/bugs/failure_to_find_file_that___34__should__34___exist_in_remote_is_silent/comment_4_7d642fc65040a7b583cdece33db01826._comment create mode 100644 doc/bugs/failure_to_find_file_that___34__should__34___exist_in_remote_is_silent/comment_5_49be366b6af6db595fa538373a61e650._comment create mode 100644 doc/bugs/failure_to_return_to_indirect_mode_on_usb.mdwn create mode 100644 doc/bugs/failure_to_return_to_indirect_mode_on_usb/comment_1_d7822b90c68bf845572b0a04a378d0bb._comment create mode 100644 doc/bugs/fat_support.mdwn create mode 100644 doc/bugs/fat_support/comment_1_04bcc4795d431e8cb32293aab29bbfe2._comment create mode 100644 doc/bugs/fat_support/comment_2_bb4a97ebadb5c53809fc78431eabd7c8._comment create mode 100644 doc/bugs/fat_support/comment_3_df3b943bc1081a8f3f7434ae0c8e061e._comment create mode 100644 doc/bugs/fat_support/comment_4_90a8a15bedd94480945a374f9d706b86._comment create mode 100644 doc/bugs/fat_support/comment_5_64bbf89de0836673224b83fdefa0407b._comment create mode 100644 doc/bugs/fat_support/comment_6_a3b6000330c9c376611c228d746a1d55._comment create mode 100644 doc/bugs/fat_support/comment_7_a0ac7f2c44efc8116940c7b94b35e9d0._comment create mode 100644 doc/bugs/fat_support/comment_8_acc947643a635eb10a1bff92083a3506._comment create mode 100644 doc/bugs/fatal:_empty_ident_name.mdwn create mode 100644 doc/bugs/fatal:_empty_ident_name/comment_1_ceae87308fb75a1f79c7c8d63ec47226._comment create mode 100644 doc/bugs/fatal:_empty_ident_name/comment_2_68832ee3e0e7244ce62bccabe2e52630._comment create mode 100644 doc/bugs/fatal:_empty_ident_name/comment_3_ed31ad316747343d7730e4c2d7dacd24._comment create mode 100644 doc/bugs/fatal:_empty_ident_name/comment_4_b812d6f30e8a866bce7260a9ee3218e3._comment create mode 100644 doc/bugs/file_access__47__locking_issues_with_the_assitant.mdwn create mode 100644 doc/bugs/file_access__47__locking_issues_with_the_assitant/comment_10_fadf06f5ab34e36ab130536ec55afc8e._comment create mode 100644 doc/bugs/file_access__47__locking_issues_with_the_assitant/comment_11_4a337f7b1140c45e5dd660b40202f696._comment create mode 100644 doc/bugs/file_access__47__locking_issues_with_the_assitant/comment_1_05e1398e78218ced9c2da6a2510949e8._comment create mode 100644 doc/bugs/file_access__47__locking_issues_with_the_assitant/comment_2_9226f0adf091154c0d8a08b340b71869._comment create mode 100644 doc/bugs/file_access__47__locking_issues_with_the_assitant/comment_3_44d3e2096b7d45a1062222bee83a346d._comment create mode 100644 doc/bugs/file_access__47__locking_issues_with_the_assitant/comment_4_f2e1d188b7b2d2daf0d832c59a68583e._comment create mode 100644 doc/bugs/file_access__47__locking_issues_with_the_assitant/comment_5_998fe58994ecf855310e4b8e6cce9e18._comment create mode 100644 doc/bugs/file_access__47__locking_issues_with_the_assitant/comment_6_4ce243cb0ea8ff810a4949a5320e4afc._comment create mode 100644 doc/bugs/file_access__47__locking_issues_with_the_assitant/comment_7_c713f6316d889c8fc52326f21375c1c4._comment create mode 100644 doc/bugs/file_access__47__locking_issues_with_the_assitant/comment_8_6dd23bab7983b8b1f938dd4f21a16f5a._comment create mode 100644 doc/bugs/file_access__47__locking_issues_with_the_assitant/comment_9_961c8f968eff0b39a85b607ee3f7630d._comment create mode 100644 doc/bugs/fix_for_makefile_to_check_if_OS_is_linux_or_not___40__relates_to_the_new_inotify_flag__41__.mdwn create mode 100644 doc/bugs/free_space_checking.mdwn create mode 100644 doc/bugs/free_space_checking/comment_1_a868e805be43c5a7c19c41f1af8e41e6._comment create mode 100644 doc/bugs/free_space_checking/comment_2_8a65f6d3dcf5baa3f7f2dbe1346e2615._comment create mode 100644 doc/bugs/free_space_checking/comment_3_0fc6ff79a357b1619d13018ccacc7c10._comment create mode 100644 doc/bugs/fsck__47__fix_should_check__47__fix_the_permissions_of_.git__47__annex.mdwn create mode 100644 doc/bugs/fsck_claims_failed_checksum_when_less_copies_than_required_are_found.mdwn create mode 100644 doc/bugs/fsck_output.mdwn create mode 100644 doc/bugs/fsck_should_double-check_when_a_content-check_fails.mdwn create mode 100644 doc/bugs/fsck_should_double-check_when_a_content-check_fails/comment_1_03af24b70adbcd9f4b94d009f6b71d0a._comment create mode 100644 doc/bugs/fsck_should_double-check_when_a_content-check_fails/comment_2_41214a7d18c66b694645248d6ebeadbf._comment create mode 100644 doc/bugs/fsck_should_double-check_when_a_content-check_fails/comment_3_e7ddd77ea35994f2051f840e9b4c7e0c._comment create mode 100644 doc/bugs/fsck_should_double-check_when_a_content-check_fails/comment_4_36a70d5a378983a76fcdbb7fba044044._comment create mode 100644 doc/bugs/fsck_should_double-check_when_a_content-check_fails/comment_5_899c4afbc988d81984c5c3397285bb01._comment create mode 100644 doc/bugs/fsck_should_double-check_when_a_content-check_fails/comment_6_dbff51d00c5645eb1832aa4644889c5e._comment create mode 100644 doc/bugs/fsck_thinks_file_content_is_bad_when_it_isn__39__t.mdwn create mode 100644 doc/bugs/fsck_thinks_file_content_is_bad_when_it_isn__39__t/comment_1_cafb58eca97a0a66110ac39b169d8de3._comment create mode 100644 doc/bugs/get_failed__44___but_remote_has_the_file.mdwn create mode 100644 doc/bugs/get_failed__44___but_remote_has_the_file/comment_1_55c8b73ce05dfca11a393bb296b99b9a._comment create mode 100644 doc/bugs/get_failed__44___but_remote_has_the_file/comment_2_474c67a421dca4c245e7bfe495d3f6d3._comment create mode 100644 doc/bugs/get_failed__44___but_remote_has_the_file/comment_3_845e8a23d63fb0b071c63ee736697d26._comment create mode 100644 doc/bugs/get_failed__44___but_remote_has_the_file/comment_4_7dec21cb67e7f4dbdb49da97f2443e8f._comment create mode 100644 doc/bugs/get_fails_for_file:__47____47___web_remotes_if_the_file_is_empty.mdwn create mode 100644 doc/bugs/git-annex-shell:_internal_error:_evacuate__40__static__41__:_strange_closure_type_30799.mdwn create mode 100644 doc/bugs/git-annex-shell:_internal_error:_evacuate__40__static__41__:_strange_closure_type_30799/comment_1_1c19e716069911f17bbebd196d9e4b61._comment create mode 100644 doc/bugs/git-annex-shell:_internal_error:_evacuate__40__static__41__:_strange_closure_type_30799/comment_2_a4d66f29d257044e548313e014ca3dc3._comment create mode 100644 doc/bugs/git-annex-shell:_internal_error:_evacuate__40__static__41__:_strange_closure_type_30799/comment_3_f5f1081eb18143383b2fb1f57d8640f5._comment create mode 100644 doc/bugs/git-annex-shell:_internal_error:_evacuate__40__static__41__:_strange_closure_type_30799/comment_4_b1f818b85c3540591c48e7ba8560d070._comment create mode 100644 doc/bugs/git-annex-shell:_internal_error:_evacuate__40__static__41__:_strange_closure_type_30799/comment_5_67406dd8d9bd4944202353508468c907._comment create mode 100644 doc/bugs/git-annex-shell:_user_error___40__unrecognized_option___96__--uuid__39__.mdwn create mode 100644 doc/bugs/git-annex-shell:_user_error___40__unrecognized_option___96__--uuid__39__/comment_1_13510e954e36484e196e7395a3a9bf1f._comment create mode 100644 doc/bugs/git-annex-shell:_user_error___40__unrecognized_option___96__--uuid__39__/comment_2_7edc478a76983a3b3c68d01f24dce613._comment create mode 100644 doc/bugs/git-annex-shell_doesn__39__t_honour_Rsync__39__s_bwlimit_option.mdwn create mode 100644 doc/bugs/git-annex-shell_doesn__39__t_honour_Rsync__39__s_bwlimit_option/comment_1_8cda861c11ef2fff3442e5a0df741939._comment create mode 100644 doc/bugs/git-annex-shell_doesn__39__t_honour_Rsync__39__s_bwlimit_option/comment_2_15e06f6db9a14a8217dea25e24ddc23a._comment create mode 100644 doc/bugs/git-annex-shell_doesn__39__t_honour_Rsync__39__s_bwlimit_option/comment_3_d36045e2b466882108c5bf09580755fa._comment create mode 100644 doc/bugs/git-annex:_Cannot_decode_byte___39____92__xfc__39__.mdwn create mode 100644 doc/bugs/git-annex:_Cannot_decode_byte___39____92__xfc__39__/comment_1_f1a7352b04f395e06e0094c1f51b6fff._comment create mode 100644 doc/bugs/git-annex:_Cannot_decode_byte___39____92__xfc__39__/comment_2_c1890067079cd99667f31cbb4d2e4545._comment create mode 100644 doc/bugs/git-annex:_Cannot_decode_byte___39____92__xfc__39__/comment_3_213c96085c60c8e52cd803df07240158._comment create mode 100644 doc/bugs/git-annex:_Not_in_a_git_repository._.mdwn create mode 100644 doc/bugs/git-annex:_Not_in_a_git_repository._/comment_1_e10363a912953a646b87c824d1c6e5d4._comment create mode 100644 doc/bugs/git-annex:_Not_in_a_git_repository._/comment_2_9e96063a664b2be8a36d7940e7632d3f._comment create mode 100644 doc/bugs/git-annex:_Not_in_a_git_repository._/comment_3_8c9bd76b0e1200723ec13fbef943a2cc._comment create mode 100644 doc/bugs/git-annex:_Not_in_a_git_repository._/comment_4_8c49979b8a815f0d6f9de39ee9a88730._comment create mode 100644 doc/bugs/git-annex:___60__socket:_16__62__:_hPutBuf:_resource_vanished___40__Broken_pipe__41__.mdwn create mode 100644 doc/bugs/git-annex:___60__socket:_16__62__:_hPutBuf:_resource_vanished___40__Broken_pipe__41__/comment_1_e962317a939bf76097ae1a3b53b146e6._comment create mode 100644 doc/bugs/git-annex:___60__socket:_16__62__:_hPutBuf:_resource_vanished___40__Broken_pipe__41__/comment_2_b32472b4c9b61e7a33dca802ecafb05b._comment create mode 100644 doc/bugs/git-annex:___60__socket:_16__62__:_hPutBuf:_resource_vanished___40__Broken_pipe__41__/comment_3_fcfea3216831df9afbd855fbd842c27e._comment create mode 100644 doc/bugs/git-annex:___60__socket:_16__62__:_hPutBuf:_resource_vanished___40__Broken_pipe__41__/comment_4_30d0b40efa59eeecb8a4be6d1baa1520._comment create mode 100644 doc/bugs/git-annex:___60__socket:_16__62__:_hPutBuf:_resource_vanished___40__Broken_pipe__41__/comment_5_4af107f3184bc2abd2c9693167018628._comment create mode 100644 doc/bugs/git-annex:___60__socket:_16__62__:_hPutBuf:_resource_vanished___40__Broken_pipe__41__/comment_6_f96027f1e3c405809fae42ce8533c6d1._comment create mode 100644 doc/bugs/git-annex:___60__socket:_16__62__:_hPutBuf:_resource_vanished___40__Broken_pipe__41__/comment_7_b6fe89deb468a7e4f63f7faab147e3fb._comment create mode 100644 doc/bugs/git-annex:___60__socket:_16__62__:_hPutBuf:_resource_vanished___40__Broken_pipe__41__/comment_8_ebec5d9266604f03959dc16d933ff4a4._comment create mode 100644 doc/bugs/git-annex:_fd:14:_hGetLine:_end_of_file.mdwn create mode 100644 doc/bugs/git-annex:_fd:14:_hGetLine:_end_of_file/comment_1_36756f5d9d591cc52113c5cc0c1eae91._comment create mode 100644 doc/bugs/git-annex:_getUserEntryForID:_failed___40__Success__41__.mdwn create mode 100644 doc/bugs/git-annex:_getUserEntryForID:_failed___40__Success__41__/comment_1_11a1615962325327466895d03e3d2379._comment create mode 100644 doc/bugs/git-annex:_getUserEntryForID:_failed___40__Success__41__/comment_2_eac51c3299e9fc04025675360969d537._comment create mode 100644 doc/bugs/git-annex:_getUserEntryForID:_failed___40__Success__41__/comment_3_c23dc02c7487d63b0905f1b7f3ca59f5._comment create mode 100644 doc/bugs/git-annex:_getUserEntryForID:_failed___40__Success__41__/comment_4_0e8b28de5c173bc60ecc0126fb2209ca._comment create mode 100644 doc/bugs/git-annex_3.20130216.1_tests_are_broken.mdwn create mode 100644 doc/bugs/git-annex___38___rsync_can__39__t_copy_files_with___39__:__39___in_their_names.mdwn create mode 100644 doc/bugs/git-annex_add_should_repack_as_it_goes.mdwn create mode 100644 doc/bugs/git-annex_add_should_repack_as_it_goes/comment_1_dbcaa0be4cd764128fb7263a95f73a32._comment create mode 100644 doc/bugs/git-annex_add_should_repack_as_it_goes/comment_2_6a27551c4fb7f62ed9f627134c755d01._comment create mode 100644 doc/bugs/git-annex_add_should_repack_as_it_goes/comment_3_ff8b589fbcf25c98abd1c58830074650._comment create mode 100644 doc/bugs/git-annex_branch_corruption.mdwn create mode 100644 doc/bugs/git-annex_branch_push_race.mdwn create mode 100644 doc/bugs/git-annex_broken_on_Android_4.3.mdwn create mode 100644 doc/bugs/git-annex_broken_on_Android_4.3/comment_1_0ffb3833ce2c2e0320468dc9a09866d7._comment create mode 100644 doc/bugs/git-annex_direct_fails_on_repositories_with_a_partial_set_of_files.mdwn create mode 100644 doc/bugs/git-annex_directory_hashing_problems_on_osx.mdwn create mode 100644 doc/bugs/git-annex_directory_hashing_problems_on_osx/comment_10_f3594de3ba2ab17771a4b116031511bb._comment create mode 100644 doc/bugs/git-annex_directory_hashing_problems_on_osx/comment_11_97de7252bf5d2a4f1381f4b2b4e24ef8._comment create mode 100644 doc/bugs/git-annex_directory_hashing_problems_on_osx/comment_12_f1c53c3058a587185e7a78d84987539d._comment create mode 100644 doc/bugs/git-annex_directory_hashing_problems_on_osx/comment_13_4f56aea35effe5c10ef37d7ad7adb48c._comment create mode 100644 doc/bugs/git-annex_directory_hashing_problems_on_osx/comment_14_cc2a53c31332fe4b828ef1e72c2a4d49._comment create mode 100644 doc/bugs/git-annex_directory_hashing_problems_on_osx/comment_15_37f1d669c1fa53ee371f781c7bb820ae._comment create mode 100644 doc/bugs/git-annex_directory_hashing_problems_on_osx/comment_16_8a4ab1af59098f4950726cf53636c2b3._comment create mode 100644 doc/bugs/git-annex_directory_hashing_problems_on_osx/comment_17_515d5c5fbf5bd0c188a4f1e936d913e2._comment create mode 100644 doc/bugs/git-annex_directory_hashing_problems_on_osx/comment_18_db64c91dd1322a0ab168190686db494f._comment create mode 100644 doc/bugs/git-annex_directory_hashing_problems_on_osx/comment_19_ff555c271637af065203ca99c9eeaf89._comment create mode 100644 doc/bugs/git-annex_directory_hashing_problems_on_osx/comment_1_9a7b09de132097100c1a68ea7b846727._comment create mode 100644 doc/bugs/git-annex_directory_hashing_problems_on_osx/comment_20_7e328b970169fffb8bce373d1522743b._comment create mode 100644 doc/bugs/git-annex_directory_hashing_problems_on_osx/comment_21_98f632652b0db9131b0173d3572f4d62._comment create mode 100644 doc/bugs/git-annex_directory_hashing_problems_on_osx/comment_22_52d41afd7fd0b71a4c8e84ab1b4df5bd._comment create mode 100644 doc/bugs/git-annex_directory_hashing_problems_on_osx/comment_23_c2cd8a69c37539c0511bae02016180ca._comment create mode 100644 doc/bugs/git-annex_directory_hashing_problems_on_osx/comment_2_174952fc3e3be12912e5fcfe78f2dd13._comment create mode 100644 doc/bugs/git-annex_directory_hashing_problems_on_osx/comment_3_a18ada7ac74c63be5753fdb2fe68dae5._comment create mode 100644 doc/bugs/git-annex_directory_hashing_problems_on_osx/comment_4_039e945617a6c1852c96974a402db29c._comment create mode 100644 doc/bugs/git-annex_directory_hashing_problems_on_osx/comment_5_eacd0b18475c05ab9feed8cf7290b79a._comment create mode 100644 doc/bugs/git-annex_directory_hashing_problems_on_osx/comment_6_e55117cb628dc532e468519252571474._comment create mode 100644 doc/bugs/git-annex_directory_hashing_problems_on_osx/comment_7_0f4f471102e394ebb01da40e4d0fd9f6._comment create mode 100644 doc/bugs/git-annex_directory_hashing_problems_on_osx/comment_8_68e2d6ccdb9622b879e4bc7005804623._comment create mode 100644 doc/bugs/git-annex_directory_hashing_problems_on_osx/comment_9_45b11ddd200261115b653c7a14d28aa9._comment create mode 100644 doc/bugs/git-annex_doesn__39__t_list_files_containing_ISO8859-15_characters.mdwn create mode 100644 doc/bugs/git-annex_doesn__39__t_list_files_containing_ISO8859-15_characters/comment_1_b84e831298c03b12471fb75da597e365._comment create mode 100644 doc/bugs/git-annex_dropunused_has_no_effect.mdwn create mode 100644 doc/bugs/git-annex_dropunused_has_no_effect/comment_1_66b581eb7111a9e98c6406ec75b899cf._comment create mode 100644 doc/bugs/git-annex_dropunused_has_no_effect/comment_2_11c46cd2087511c3d22b7ce7c149b3e9._comment create mode 100644 doc/bugs/git-annex_dropunused_has_no_effect/comment_3_b1c3d8c6ec4b20727aaa9c4b746531b0._comment create mode 100644 doc/bugs/git-annex_dropunused_has_no_effect/comment_4_f05a9a3760858c5ee5c98dd8ab059c28._comment create mode 100644 doc/bugs/git-annex_fix_not_noticing_file_renames.mdwn create mode 100644 doc/bugs/git-annex_fix_not_noticing_file_renames/comment_1_4edd95200d59ec5a5426167b8da8e3f9._comment create mode 100644 doc/bugs/git-annex_fix_not_noticing_file_renames/comment_2_a9a44debefb3bdd4b8ed2d1cf53f2338._comment create mode 100644 doc/bugs/git-annex_fix_not_noticing_file_renames/comment_3_0efb11f35b872b75a3fbc4ebb71ac827._comment create mode 100644 doc/bugs/git-annex_get:_requested_key_is_not_present.mdwn create mode 100644 doc/bugs/git-annex_get:_requested_key_is_not_present/comment_1_d4baa6607a61d0e6a7cea1325a5ddf95._comment create mode 100644 doc/bugs/git-annex_get:_requested_key_is_not_present/comment_2_b49725488c3db5e00ede7b65ed9d62fa._comment create mode 100644 doc/bugs/git-annex_get:_requested_key_is_not_present/comment_3_c17a7138579b93c6f14e3444c11664ac._comment create mode 100644 doc/bugs/git-annex_has_issues_with_git_when_staging__47__commiting_logs.mdwn create mode 100644 doc/bugs/git-annex_immediately_re-gets_dropped_files.mdwn create mode 100644 doc/bugs/git-annex_immediately_re-gets_dropped_files/comment_1_09e616a4866e726a48be4febe6375cc8._comment create mode 100644 doc/bugs/git-annex_incorrectly_parses_bare_IPv6_addresses.mdwn create mode 100644 doc/bugs/git-annex_losing_rsync_remotes_with_encryption_enabled.mdwn create mode 100644 doc/bugs/git-annex_on_crippled_filesystem_can_still_failed_due_to_case_.mdwn create mode 100644 doc/bugs/git-annex_on_crippled_filesystem_can_still_failed_due_to_case_/comment_1_850695231926dfe94f11342d3af7f63c._comment create mode 100644 doc/bugs/git-annex_on_crippled_filesystem_can_still_failed_due_to_case_/comment_2_c2a2f801a3e18ad597ff0acf2f104557._comment create mode 100644 doc/bugs/git-annex_opens_too_many_files.mdwn create mode 100644 doc/bugs/git-annex_opens_too_many_files/comment_1_37f6f5838c41c533df4be1f927b9b03d._comment create mode 100644 doc/bugs/git-annex_opens_too_many_files/comment_2_347ef233b9845b84d7c4d49ed166e797._comment create mode 100644 doc/bugs/git-annex_opens_too_many_files/comment_3_d5f644d97cd2db471deb5dcd728cae60._comment create mode 100644 doc/bugs/git-annex_sync_broken_on_squeeze_backports.mdwn create mode 100644 doc/bugs/git-annex_thinks_files_are_in_repositories_they_are_not.mdwn create mode 100644 doc/bugs/git-annex_thinks_files_are_in_repositories_they_are_not/comment_1_6722fd627ec4add9f2b16546bd8ef341._comment create mode 100644 doc/bugs/git-annex_thinks_files_are_in_repositories_they_are_not/comment_2_508e475f764e1cb453b756eb50bc3a15._comment create mode 100644 doc/bugs/git-annex_thinks_files_are_in_repositories_they_are_not/comment_3_1656ba18c519a262c57ef626a3449e77._comment create mode 100644 doc/bugs/git-annex_thinks_files_are_in_repositories_they_are_not/comment_4_347dc3b6e5bc6c4195ec09d54bc1398e._comment create mode 100644 doc/bugs/git-annex_thinks_files_are_in_repositories_they_are_not/comment_5_a9c93bfc3278ef8b1117eac2af859bc3._comment create mode 100644 doc/bugs/git-annex_thinks_files_are_in_repositories_they_are_not/comment_6_804dd62beef64f7d4e203bdb28cbe660._comment create mode 100644 doc/bugs/git-annex_thinks_files_are_in_repositories_they_are_not/comment_7_4ef107d70647780eb5347cae6f467fed._comment create mode 100644 doc/bugs/git-annex_webapp_command_not_found.mdwn create mode 100644 doc/bugs/git-annex_webapp_command_not_found/comment_1_6fa63ae1a7affb2351eda57ab3b4eda1._comment create mode 100644 doc/bugs/git-annex_webapp_command_not_found/comment_2_d25232bb5eaff725281869d7681e81ad._comment create mode 100644 doc/bugs/git_annex_add_..._adds_too_much.mdwn create mode 100644 doc/bugs/git_annex_add_eats_files_when_filename_is_too_long.mdwn create mode 100644 doc/bugs/git_annex_add_eats_files_when_filename_is_too_long/comment_1_9650284913bec2a00cf551b90ab5d8ff._comment create mode 100644 doc/bugs/git_annex_add_eats_files_when_filename_is_too_long/comment_2_c6c8d2a1f444d85c582bc5396b08e148._comment create mode 100644 doc/bugs/git_annex_add_eats_files_when_filename_is_too_long/comment_3_5776864d78d56849001dd12e3adb9cbe._comment create mode 100644 doc/bugs/git_annex_add_eats_files_when_filename_is_too_long/comment_4_371ec7b4ae73280ede31edfe90b42a95._comment create mode 100644 doc/bugs/git_annex_add_eats_files_when_filename_is_too_long/comment_5_4fb04f646de591640f8504c0caf61acd._comment create mode 100644 doc/bugs/git_annex_add_eats_files_when_filename_is_too_long/comment_6_b4055409fe48da95bb3101c0242ef0bc._comment create mode 100644 doc/bugs/git_annex_add_error_with_Andrew_File_System.mdwn create mode 100644 doc/bugs/git_annex_add_error_with_Andrew_File_System/comment_1_bc783e551fc0e8da87bc95bff5b8f73a._comment create mode 100644 doc/bugs/git_annex_add_memory_leak.mdwn create mode 100644 doc/bugs/git_annex_add_removes_file_with_no_data_left.mdwn create mode 100644 doc/bugs/git_annex_add_removes_file_with_no_data_left/comment_10_9cc749a6efd4359a99316036f5bc867f._comment create mode 100644 doc/bugs/git_annex_add_removes_file_with_no_data_left/comment_11_1fed5be9db29866e4dc3d3bb12907bf3._comment create mode 100644 doc/bugs/git_annex_add_removes_file_with_no_data_left/comment_12_06d517ac4ef8def4629a40d7c3549bac._comment create mode 100644 doc/bugs/git_annex_add_removes_file_with_no_data_left/comment_1_8f081aeba7065d143a453dc128543f59._comment create mode 100644 doc/bugs/git_annex_add_removes_file_with_no_data_left/comment_2_54a4b10723fd8a80dd486377ff15ce0d._comment create mode 100644 doc/bugs/git_annex_add_removes_file_with_no_data_left/comment_3_f1964e4e07991a251c2795da0361a4e2._comment create mode 100644 doc/bugs/git_annex_add_removes_file_with_no_data_left/comment_4_73c38d843c30f00f6fd8883db8e55f62._comment create mode 100644 doc/bugs/git_annex_add_removes_file_with_no_data_left/comment_5_7ede5ee312f3abdf78979c0d52a7871a._comment create mode 100644 doc/bugs/git_annex_add_removes_file_with_no_data_left/comment_6_e37cf18708f09619442c3a9532d12ed9._comment create mode 100644 doc/bugs/git_annex_add_removes_file_with_no_data_left/comment_7_a744ef7dd3a224a911ebb24858bc2fd6._comment create mode 100644 doc/bugs/git_annex_add_removes_file_with_no_data_left/comment_8_f97141b255073b90120895148220c2d7._comment create mode 100644 doc/bugs/git_annex_add_removes_file_with_no_data_left/comment_9_dd2be11dfd190129d491f5f891e7cd1a._comment create mode 100644 doc/bugs/git_annex_assistant_--autostart_failed.mdwn create mode 100644 doc/bugs/git_annex_assistant_--autostart_failed/comment_1_746545273b53849c42ff6272324e5155._comment create mode 100644 doc/bugs/git_annex_assistant_--autostart_failed/comment_2_5bdf6f94da12e551ae12e7f550a84d62._comment create mode 100644 doc/bugs/git_annex_assistant_--autostart_failed/comment_3_bfd646f69946a5fe926b270cf94f87cb._comment create mode 100644 doc/bugs/git_annex_content_fails_with_a_parse_error.txt create mode 100644 doc/bugs/git_annex_content_fails_with_a_parse_error/comment_1_2b60b6ae0115de13ecf837b34dadcd1d._comment create mode 100644 doc/bugs/git_annex_copy_--fast_does_not_copy_files.mdwn create mode 100644 doc/bugs/git_annex_copy_-f_REMOTE_._doesn__39__t_work_as_expected.mdwn create mode 100644 doc/bugs/git_annex_copy_trying_to_connect_to_remotes_uninvolved.mdwn create mode 100644 doc/bugs/git_annex_copy_trying_to_connect_to_remotes_uninvolved/comment_1_f1330935a07460c9c8bc82ee8d4709c5._comment create mode 100644 doc/bugs/git_annex_does_nothing_useful.mdwn create mode 100644 doc/bugs/git_annex_does_nothing_useful/comment_10_457354dc0018333002dc5049935c0feb._comment create mode 100644 doc/bugs/git_annex_does_nothing_useful/comment_11_8a6d244165dd238ddf9dd629795de2f6._comment create mode 100644 doc/bugs/git_annex_does_nothing_useful/comment_12_30d06bc0f1c37d988a1a31962b57533c._comment create mode 100644 doc/bugs/git_annex_does_nothing_useful/comment_1_fc4f51ddcbc69631e2835b86c3489c8e._comment create mode 100644 doc/bugs/git_annex_does_nothing_useful/comment_2_9bb1647e6c59f1ed7b13b81ecc33f920._comment create mode 100644 doc/bugs/git_annex_does_nothing_useful/comment_3_d434f5c614a27b75d73530b5b918b851._comment create mode 100644 doc/bugs/git_annex_does_nothing_useful/comment_4_998e33219d29ea41b0b2a5d2955a9862._comment create mode 100644 doc/bugs/git_annex_does_nothing_useful/comment_5_c72e2571e5b8c06bbfa2276a7ad1e8a6._comment create mode 100644 doc/bugs/git_annex_does_nothing_useful/comment_6_bc8b42432ba25de8f972c192bc3cdff6._comment create mode 100644 doc/bugs/git_annex_does_nothing_useful/comment_7_e7469a4c5e45078ade775f5cbdd17cfc._comment create mode 100644 doc/bugs/git_annex_does_nothing_useful/comment_8_bc9e6fd284440a59ffe4e4ed1f73f7d7._comment create mode 100644 doc/bugs/git_annex_does_nothing_useful/comment_9_38a2dbeee3750d79ca9a943a02fceb29._comment create mode 100644 doc/bugs/git_annex_doesn__39__t_work_in_Max_OS_X_10.9.mdwn create mode 100644 doc/bugs/git_annex_doesn__39__t_work_in_Max_OS_X_10.9/comment_1_4fb9d3de245dddab65fb1a53a67a095c._comment create mode 100644 doc/bugs/git_annex_doesn__39__t_work_in_Max_OS_X_10.9/comment_2_f513259a2641e00b049203014ab940c8._comment create mode 100644 doc/bugs/git_annex_doesn__39__t_work_in_Max_OS_X_10.9/comment_3_54ee7b90467fee8b0457e9c447747500._comment create mode 100644 doc/bugs/git_annex_doesn__39__t_work_in_Max_OS_X_10.9/comment_4_7e6223c2dae3346e17276c7bbb01d53e._comment create mode 100644 doc/bugs/git_annex_doesn__39__t_work_in_Max_OS_X_10.9/comment_5_13b6e595d595da7f036e81258a65541e._comment create mode 100644 doc/bugs/git_annex_fork_bombs_on_gpg_file.mdwn create mode 100644 doc/bugs/git_annex_fork_bombs_on_gpg_file/comment_10_6e29b60cd77f3288e33ad270f95f410e._comment create mode 100644 doc/bugs/git_annex_fork_bombs_on_gpg_file/comment_11_ad13e3221ae06086e86800316912d951._comment create mode 100644 doc/bugs/git_annex_fork_bombs_on_gpg_file/comment_12_41746b731eae7f280bb668c776022bcb._comment create mode 100644 doc/bugs/git_annex_fork_bombs_on_gpg_file/comment_13_56ca8590110abffeed6d826c54ca1136._comment create mode 100644 doc/bugs/git_annex_fork_bombs_on_gpg_file/comment_1_73ae438a37e4c5f56fe291448e1c64dd._comment create mode 100644 doc/bugs/git_annex_fork_bombs_on_gpg_file/comment_2_aa237adebe7674b8cdb9a967bb5f96a8._comment create mode 100644 doc/bugs/git_annex_fork_bombs_on_gpg_file/comment_3_ab403d7abbbbabd498b954b0b9742755._comment create mode 100644 doc/bugs/git_annex_fork_bombs_on_gpg_file/comment_4_a35d04440b1220faf9088107c3f17762._comment create mode 100644 doc/bugs/git_annex_fork_bombs_on_gpg_file/comment_5_8345331b9b313769ba401da2ffd89332._comment create mode 100644 doc/bugs/git_annex_fork_bombs_on_gpg_file/comment_6_7eb535ca38b3e84d44d0f8cbf5e61b8b._comment create mode 100644 doc/bugs/git_annex_fork_bombs_on_gpg_file/comment_7_a3aa4231a82917c56cbdf52b65db7133._comment create mode 100644 doc/bugs/git_annex_fork_bombs_on_gpg_file/comment_8_178fd4e4d6abbca192fcd6d592615fca._comment create mode 100644 doc/bugs/git_annex_fork_bombs_on_gpg_file/comment_9_7d80f131f43312bb061df2be7fa956ef._comment create mode 100644 doc/bugs/git_annex_fsck_in_direct_mode_does_not_checksum_files.mdwn create mode 100644 doc/bugs/git_annex_fsck_in_direct_mode_does_not_checksum_files/comment_1_a6cde4aa495512344fa7f50e10749c68._comment create mode 100644 doc/bugs/git_annex_fsck_in_direct_mode_does_not_checksum_files/comment_2_4ac3b87ec0bc0514c4eff9f5a75b9f5d._comment create mode 100644 doc/bugs/git_annex_fsck_in_direct_mode_does_not_checksum_files/comment_3_d18b1fdc866edf2786d2c6b7ec55119f._comment create mode 100644 doc/bugs/git_annex_fsck_in_direct_mode_does_not_checksum_files/comment_4_31e4fcbf63c11cc374a849daf3ce1dbc._comment create mode 100644 doc/bugs/git_annex_fsck_is_a_no-op_in_bare_repos.mdwn create mode 100644 doc/bugs/git_annex_fsck_is_a_no-op_in_bare_repos/comment_1_fc59fbd1cdf8ca97b0a4471d9914aaa1._comment create mode 100644 doc/bugs/git_annex_fsck_is_a_no-op_in_bare_repos/comment_2_273a45e6977d40d39e0d9ab924a83240._comment create mode 100644 doc/bugs/git_annex_get_choke_when_remote_is_an_ssh_url_with_a_port.mdwn create mode 100644 doc/bugs/git_annex_gets_confused_about_remotes_with_dots_in_their_names.mdwn create mode 100644 doc/bugs/git_annex_initremote_needs_some___34__error_checking__34__.mdwn create mode 100644 doc/bugs/git_annex_initremote_walks_.git-annex.mdwn create mode 100644 doc/bugs/git_annex_map_has_problems_with_urls_containing___126__.mdwn create mode 100644 doc/bugs/git_annex_migrate_leaves_old_backend_versions_around.mdwn create mode 100644 doc/bugs/git_annex_migrate_leaves_old_backend_versions_around/comment_1_f3e418144e5a5a9b3eda459546fc2bb0._comment create mode 100644 doc/bugs/git_annex_should_use___39__git_add_-f__39___internally.mdwn create mode 100644 doc/bugs/git_annex_should_use___39__git_add_-f__39___internally/comment_1_7683bf02cf9e97830fb4690314501568._comment create mode 100644 doc/bugs/git_annex_sync_in_direct_mode_does_not_honor_skip-worktree.mdwn create mode 100644 doc/bugs/git_annex_sync_in_direct_mode_does_not_honor_skip-worktree/comment_1_69baeb997086c885f34fd1dc385cf5d6._comment create mode 100644 doc/bugs/git_annex_sync_in_direct_mode_does_not_honor_skip-worktree/comment_2_fb8c0bebb9aaa75ee7eaf6999b1db49e._comment create mode 100644 doc/bugs/git_annex_sync_in_direct_mode_does_not_honor_skip-worktree/comment_3_6bfd4e9a7853af93e72b717249de9439._comment create mode 100644 doc/bugs/git_annex_uninit_loses_content_when_interrupted.mdwn create mode 100644 doc/bugs/git_annex_uninit_loses_content_when_interrupted/comment_1_fd9d2abbc90fb4f470b2212bc1f4a2dd._comment create mode 100644 doc/bugs/git_annex_uninit_loses_content_when_interrupted/comment_2_0e99f6ef4f8b342ef0ebc64dbf8e2ce6._comment create mode 100644 doc/bugs/git_annex_uninit_removes_files_not_previously_added_to_annex.mdwn create mode 100644 doc/bugs/git_annex_uninit_removes_files_not_previously_added_to_annex/comment_1_ce4e3b1bf0d53119d049cf7dd621c5c4._comment create mode 100644 doc/bugs/git_annex_uninit_removes_files_not_previously_added_to_annex/comment_2_3aa125635609fce41ab0c98cefb81f98._comment create mode 100644 doc/bugs/git_annex_unlock_is_not_atomic.mdwn create mode 100644 doc/bugs/git_annex_unused_aborts_due_to_filename_encoding_problems.mdwn create mode 100644 doc/bugs/git_annex_unused_aborts_due_to_filename_encoding_problems/comment_1_8ba4fdb9f2d3bd44db5e910526cb9124._comment create mode 100644 doc/bugs/git_annex_unused_aborts_due_to_filename_encoding_problems/comment_2_2a4a2b3e287a0444a1c8e8d98768a206._comment create mode 100644 doc/bugs/git_annex_unused_aborts_due_to_filename_encoding_problems/comment_3_dacfdb8322045fc4ceefc9128bf7c505._comment create mode 100644 doc/bugs/git_annex_unused_aborts_due_to_filename_encoding_problems/comment_4_7889a3ff5ce80c6322448aa674df8525._comment create mode 100644 doc/bugs/git_annex_unused_aborts_due_to_filename_encoding_problems/comment_5_6d28c2537ce24eeb3496ca349823defd._comment create mode 100644 doc/bugs/git_annex_unused_aborts_due_to_filename_encoding_problems/comment_6_4bf14ecef622988e80976c0fb55c24b9._comment create mode 100644 doc/bugs/git_annex_unused_aborts_due_to_filename_encoding_problems/comment_7_d2e5382fe0f38fb9dd9ee69901c68151._comment create mode 100644 doc/bugs/git_annex_unused_aborts_due_to_filename_encoding_problems/comment_8_b282757537cda863d3dc6d0bbfd6b656._comment create mode 100644 doc/bugs/git_annex_unused_considers_remote_branches_which_makes_it_inconsistent.mdwn create mode 100644 doc/bugs/git_annex_unused_considers_remote_branches_which_makes_it_inconsistent/comment_1_a636ffe55b11c46a0afcc0b9a3a88cd4._comment create mode 100644 doc/bugs/git_annex_unused_considers_remote_branches_which_makes_it_inconsistent/comment_2_5e1ad57420efd16ae09c9e5cad55b5f2._comment create mode 100644 doc/bugs/git_annex_unused_failes_on_empty_repository.mdwn create mode 100644 doc/bugs/git_annex_unused_seems_to_check_for_current_path.mdwn create mode 100644 doc/bugs/git_annex_upgrade_loses_track_of_files_with___34____38____34___character___40__and_probably_others__41__.mdwn create mode 100644 doc/bugs/git_annex_upgrade_loses_track_of_files_with___34____38____34___character___40__and_probably_others__41__/comment_1_861506e40e0d04d2be98bbfe9188be89._comment create mode 100644 doc/bugs/git_annex_upgrade_output_is_inconsistent_and_spammy.mdwn create mode 100644 doc/bugs/git_annex_upgrade_output_is_inconsistent_and_spammy/comment_1_3a01c81efba321b0e46d1bc0426ad8d1._comment create mode 100644 doc/bugs/git_annex_version_should_without_being_in_a_repo_.mdwn create mode 100644 doc/bugs/git_annex_version_should_without_being_in_a_repo_/comment_1_e7b26eeb1a765fd83280ef907c0deef2._comment create mode 100644 doc/bugs/git_annex_webapp_--listen_on_a_remote_linux_server.mdwn create mode 100644 doc/bugs/git_annex_webapp_--listen_on_a_remote_linux_server/comment_1_db99c00830d3f15ebe790c4dc8b60bd7._comment create mode 100644 doc/bugs/git_annex_webapp_runs_on_wine.mdwn create mode 100644 doc/bugs/git_annex_webapp_runs_on_wine/comment_1_c71dfa42780c0fc78f88ce054e5f3ee3._comment create mode 100644 doc/bugs/git_annex_webapp_runs_on_wine/comment_2_f28441b18b0be90c1e58348455ce09d9._comment create mode 100644 doc/bugs/git_annex_won__39__t_copy_files_to_my_usb_drive.mdwn create mode 100644 doc/bugs/git_annex_won__39__t_copy_files_to_my_usb_drive/comment_1_7707017fbf3d92ee21d600fe0aefce4f._comment create mode 100644 doc/bugs/git_annex_won__39__t_copy_files_to_my_usb_drive/comment_2_f3392ec3ca7392823cbad2cc9b77f54e._comment create mode 100644 doc/bugs/git_annex_won__39__t_copy_files_to_my_usb_drive/comment_3_b3d016a487b12748fe2c4d14300eb158._comment create mode 100644 doc/bugs/git_annex_won__39__t_copy_files_to_my_usb_drive/comment_4_61f600511a3172f0707e5809fc444d0c._comment create mode 100644 doc/bugs/git_annex_won__39__t_copy_files_to_my_usb_drive/comment_5_8cf029ac7bf3c19dcb0b613eed3b52ac._comment create mode 100644 doc/bugs/git_annex_won__39__t_copy_files_to_my_usb_drive/comment_6_e40d88eba7d8aec1530ce1d32d1c85f2._comment create mode 100644 doc/bugs/git_annex_won__39__t_copy_files_to_my_usb_drive/comment_7_b101fab9e690d1b335a1a29abab68d6c._comment create mode 100644 doc/bugs/git_annex_won__39__t_copy_files_to_my_usb_drive/comment_8_b30d32086314a7e357f3dd6608828ee5._comment create mode 100644 doc/bugs/git_annix_breaks_git_commit_after_uninstall.mdwn create mode 100644 doc/bugs/git_annix_breaks_git_commit_after_uninstall/comment_1_c8b1bab40d3bb2468a5bba7b116e854e._comment create mode 100644 doc/bugs/git_annix_breaks_git_commit_after_uninstall/comment_2_4173770375fca51dcaf9b974296d041a._comment create mode 100644 doc/bugs/git_command_line_constructed_by_unannex_command_has_tons_of_redundant_-a_paramters.mdwn create mode 100644 doc/bugs/git_defunct_processes___40__child_of_git-annex_assistant__41__.mdwn create mode 100644 doc/bugs/git_defunct_processes___40__child_of_git-annex_assistant__41__/comment_1_5e3f4b63db5cd32b63fb3e6a78f9b093._comment create mode 100644 doc/bugs/git_rename_detection_on_file_move.mdwn create mode 100644 doc/bugs/git_rename_detection_on_file_move/comment_10_5ec2f965c80cc5dd31ee3c4edb695664._comment create mode 100644 doc/bugs/git_rename_detection_on_file_move/comment_1_0531dcfa833b0321a7009526efe3df33._comment create mode 100644 doc/bugs/git_rename_detection_on_file_move/comment_2_7101d07400ad5935f880dc00d89bf90e._comment create mode 100644 doc/bugs/git_rename_detection_on_file_move/comment_3_57010bcaca42089b451ad8659a1e018e._comment create mode 100644 doc/bugs/git_rename_detection_on_file_move/comment_4_79d96599f757757f34d7b784e6c0e81c._comment create mode 100644 doc/bugs/git_rename_detection_on_file_move/comment_5_d61f5693d947b9736b29fca1dbc7ad76._comment create mode 100644 doc/bugs/git_rename_detection_on_file_move/comment_6_f63de6fe2f7189c8c2908cc41c4bc963._comment create mode 100644 doc/bugs/git_rename_detection_on_file_move/comment_7_7f20d0b2f6ed1c34021a135438037306._comment create mode 100644 doc/bugs/git_rename_detection_on_file_move/comment_8_6a00500b24ba53248c78e1ffc8d1a591._comment create mode 100644 doc/bugs/git_rename_detection_on_file_move/comment_9_75e0973f6d573df615e01005ebcea87d._comment create mode 100644 doc/bugs/gix-annex_help_is_homicidal.mdwn create mode 100644 doc/bugs/glacier_from_multiple_repos.mdwn create mode 100644 doc/bugs/googlemail.mdwn create mode 100644 doc/bugs/gpg_bundled_with_OSX_build_fails.mdwn create mode 100644 doc/bugs/gpg_bundled_with_OSX_build_fails/comment_1_ec911f920db6c354ba998ffbb5886606._comment create mode 100644 doc/bugs/gpg_bundled_with_OSX_build_fails/comment_2_bf2a3ab1bbe258bd501ec4b776882adf._comment create mode 100644 doc/bugs/gpg_bundled_with_OSX_build_fails/comment_3_c0142427400323c00bd8294415ae32c5._comment create mode 100644 doc/bugs/gpg_bundled_with_OSX_build_fails/comment_4_b56db4b5afc276f88a2b980e22fda8a0._comment create mode 100644 doc/bugs/gpg_bundled_with_OSX_build_fails/comment_5_a4eda81e5f927c463593bc48fbe84077._comment create mode 100644 doc/bugs/gpg_bundled_with_OSX_build_fails/comment_6_2f0b9331d16a208883bac586258a7b50._comment create mode 100644 doc/bugs/gpg_bundled_with_OSX_build_fails/comment_7_c05c484a6134f93796cff08de0f63e80._comment create mode 100644 doc/bugs/gpg_bundled_with_OSX_build_fails/comment_8_f2cb5467ebe80cf67e1155b771b73978._comment create mode 100644 doc/bugs/gpg_bundled_with_OSX_build_fails/comment_9_27bbda7e31f55b29e1473555ee17e613._comment create mode 100644 doc/bugs/gpg_error_on_android.mdwn create mode 100644 doc/bugs/gpg_error_on_android/comment_1_870583fd1b7a33b688b9a228077d1333._comment create mode 100644 doc/bugs/gpg_error_on_android/comment_2_9ce5511a109bde50d8cf87bad0268b4a._comment create mode 100644 doc/bugs/gpg_error_on_android/comment_3_b345e80f38d38f82cfcfce3102138fb8._comment create mode 100644 doc/bugs/gpg_error_on_android/comment_4_032f42235b7f26854e725041ca33384b._comment create mode 100644 doc/bugs/gpg_goes_to_100__37___cpu_on_bad_input_data.mdwn create mode 100644 doc/bugs/gpg_goes_to_100__37___cpu_on_bad_input_data/comment_1_889218fb7c0115b03d9bad0c07296097._comment create mode 100644 doc/bugs/gpg_hangs_on_glacier_remote_creation.mdwn create mode 100644 doc/bugs/gpg_hangs_on_glacier_remote_creation/comment_1_41ca74a4e4aaf4f6b012a92677037651._comment create mode 100644 doc/bugs/gpg_hangs_on_glacier_remote_creation/comment_2_dd11fd25c8bb1f2d7e1292c07abf553e._comment create mode 100644 doc/bugs/gpg_hangs_on_glacier_remote_creation/comment_3_543d8a13756c1355a5752867bdcbefd3._comment create mode 100644 doc/bugs/gpg_hangs_on_glacier_remote_creation/comment_4_6441cf25e6bd62c96d7e766da9bdd7fb._comment create mode 100644 doc/bugs/gpg_hangs_on_glacier_remote_creation/comment_5_72e152294e36bc5f2d78e8e2ebed6a23._comment create mode 100644 doc/bugs/gpg_hangs_on_glacier_remote_creation/comment_6_890e85df05903795e01efbd7879f9c87._comment create mode 100644 doc/bugs/gpg_hangs_on_glacier_remote_creation/comment_7_042047f9fcc45abbfa47c3973d79f08e._comment create mode 100644 doc/bugs/gpg_needs_--use-agent.mdwn create mode 100644 doc/bugs/hGetContents:_user_error.mdwn create mode 100644 doc/bugs/hGetContents:_user_error/comment_1_30178f151f8c60d2ff856ca543dc506c._comment create mode 100644 doc/bugs/hGetContents:_user_error/comment_2_f74eeed4a007058a22183fd678ecd6c6._comment create mode 100644 doc/bugs/hGetContents:_user_error/comment_3_515e562228a89a13d6d857a874f4a468._comment create mode 100644 doc/bugs/hGetContents:_user_error/comment_4_8c6ed5e459c5c66b77db446c6317114c._comment create mode 100644 doc/bugs/hGetContents:_user_error/comment_5_f80bce48c3f96b0cd6892af43ee88a96._comment create mode 100644 doc/bugs/hGetContents:_user_error/comment_6_69dc09e4ae726856dafbeec34170671c._comment create mode 100644 doc/bugs/hGetContents:_user_error/comment_7_3f66b03f773341fad94ec16b4f55edaa._comment create mode 100644 doc/bugs/hGetContents:_user_error/comment_8_a697e2d36abfc999e65c9f587c0de56e._comment create mode 100644 doc/bugs/hGetContents:_user_error/comment_9_da7c5905a64bb6779970f9394155e629._comment create mode 100644 doc/bugs/haskell-dbus_problems_on_OSX___40__or_this_a_general_problem__41__.mdwn create mode 100644 doc/bugs/host_with_rysnc_installed__44___not_recognized.mdwn create mode 100644 doc/bugs/host_with_rysnc_installed__44___not_recognized/comment_1_3ff000eb3efde41426c7b086ae627dcf._comment create mode 100644 doc/bugs/host_with_rysnc_installed__44___not_recognized/comment_2_34e592ab057df2df54e13d3f5cae64f0._comment create mode 100644 doc/bugs/host_with_rysnc_installed__44___not_recognized/comment_3_05ffbae13d8f9b08315f40bb9b206f46._comment create mode 100644 doc/bugs/host_with_rysnc_installed__44___not_recognized/comment_4_99d1f151263ca3433dd4afa8a928b1fe._comment create mode 100644 doc/bugs/host_with_rysnc_installed__44___not_recognized/comment_5_6ef1a377b0b4d3efeffdf9693d0b496b._comment create mode 100644 doc/bugs/host_with_rysnc_installed__44___not_recognized/comment_6_d9e36828ad55f3181a1c650010f23d6b._comment create mode 100644 doc/bugs/immediately_drops_files.mdwn create mode 100644 doc/bugs/importfeed_uses___34____95__foo__34___as_extension.mdwn create mode 100644 doc/bugs/internal_server_error_creating_repo_on_ssh_server.mdwn create mode 100644 doc/bugs/internal_server_error_creating_repo_on_ssh_server/comment_1_4a2c9338d5c779496049d78e29cf5cbd._comment create mode 100644 doc/bugs/internal_server_error_when_choosing_encrypted_rsync_repo_option.mdwn create mode 100644 doc/bugs/internal_server_error_when_choosing_encrypted_rsync_repo_option/comment_1_14a2f775f43a86129ce3649a06f8ba0b._comment create mode 100644 doc/bugs/internal_server_error_when_choosing_encrypted_rsync_repo_option/comment_2_7b277320fcffd8d03e0d3d31398eb571._comment create mode 100644 doc/bugs/internal_server_error_when_choosing_encrypted_rsync_repo_option/comment_3_ba9dd8f2cc46640383d4339a3661571f._comment create mode 100644 doc/bugs/internal_server_error_when_choosing_encrypted_rsync_repo_option/comment_4_274ae39d55545bde0be931d7a6c42c94._comment create mode 100644 doc/bugs/internal_server_error_when_choosing_encrypted_rsync_repo_option/comment_5_242291d46acc61bdfc112e3316de528b._comment create mode 100644 doc/bugs/internal_server_error_when_choosing_encrypted_rsync_repo_option/comment_6_76b936263e82ca6c415a16ed57e770b4._comment create mode 100644 doc/bugs/internal_server_error_when_choosing_encrypted_rsync_repo_option/comment_7_9ccd3749fd9f32b0906c0b9428cc514f._comment create mode 100644 doc/bugs/internal_server_error_when_choosing_encrypted_rsync_repo_option/comment_8_4e8982668b5044b2286d55c90adb9da3._comment create mode 100644 doc/bugs/internal_server_error_when_choosing_encrypted_rsync_repo_option/comment_9_aaf0ee250972d737a2ca57de5b5f1c0a._comment create mode 100644 doc/bugs/interrupting_migration_causes_problems.mdwn create mode 100644 doc/bugs/javascript_functions_qouting_issue.mdwn create mode 100644 doc/bugs/journal_commit_error_when_using_annex.mdwn create mode 100644 doc/bugs/journal_commit_error_when_using_annex/comment_1_38f60ca3503ea1530c4bd2cde5c9182f._comment create mode 100644 doc/bugs/journal_commit_error_when_using_annex/comment_2_6de455a67f37d9ee0a307a78123781bf._comment create mode 100644 doc/bugs/long_running_assistant_causes_resource_starvation_on_OSX.mdwn create mode 100644 doc/bugs/long_running_assistant_causes_resource_starvation_on_OSX/comment_1_91c911c29fd126ddc365c561591f627e._comment create mode 100644 doc/bugs/long_running_assistant_causes_resource_starvation_on_OSX/comment_2_c316aead931a6a2377a4515bbb34ac5b._comment create mode 100644 doc/bugs/lsof__47__committer_thread_loops_occassionally.mdwn create mode 100644 doc/bugs/lsof__47__committer_thread_loops_occassionally/comment_1_f8d1720aa26c719609720acf0772606e._comment create mode 100644 doc/bugs/lsof__47__committer_thread_loops_occassionally/comment_2_0527569ea2924721d19dadcf4fe0ec5a._comment create mode 100644 doc/bugs/lsof__47__committer_thread_loops_occassionally/comment_3_5b67ff08a897ea3d2266ccc910ab4278._comment create mode 100644 doc/bugs/make_SHA512E_the_default.mdwn create mode 100644 doc/bugs/make_install_can__39__t_be_used_with_sudo.mdwn create mode 100644 doc/bugs/make_install_doesn__39__t_create_git-annex-shell.mdwn create mode 100644 doc/bugs/make_install_doesn__39__t_create_git-annex-shell/comment_1_8c20edd8c6483500f807528d616c6dfd._comment create mode 100644 doc/bugs/make_install_doesn__39__t_create_git-annex-shell/comment_2_8b2cf0fe7219e0bc83fd326adbf26c8a._comment create mode 100644 doc/bugs/make_install_doesn__39__t_create_git-annex-shell/comment_3_25fe06eb127e59a4a07aeb52a5cfeabe._comment create mode 100644 doc/bugs/make_install_doesn__39__t_create_git-annex-shell/comment_4_ec78032ba62d6918baa2c0b07ead5b50._comment create mode 100644 doc/bugs/making_annex-merge_try_a_fast-forward.mdwn create mode 100644 doc/bugs/map_not_respecting_annex_ssh_options__63__.mdwn create mode 100644 doc/bugs/map_not_respecting_annex_ssh_options__63__/comment_1_c63a1ed5909d53f116f06e60aba74dc6._comment create mode 100644 doc/bugs/migrated_files_not_showing_up_in_unused_list.mdwn create mode 100644 doc/bugs/migrated_files_not_showing_up_in_unused_list/comment_1_2cfbf6693b051c758fe5efa5ee885829._comment create mode 100644 doc/bugs/migrated_files_not_showing_up_in_unused_list/comment_2_acb1abeb32c3aba8ba65151afbea753c._comment create mode 100644 doc/bugs/minor_bug:_errors_are_not_verbose_enough.mdwn create mode 100644 doc/bugs/missing_dependency_in_git-annex-3.20130216.mdwn create mode 100644 doc/bugs/missing_kde__47__gnome_menu_item..mdwn create mode 100644 doc/bugs/moreinfo.mdwn create mode 100644 doc/bugs/network___62____61___2.4.0.1_is_not_in_Haskell_Platform_2012.4.0.0.mdwn create mode 100644 doc/bugs/network___62____61___2.4.0.1_is_not_in_Haskell_Platform_2012.4.0.0/comment_1_2c4b3757bb8de563edca65aeabcbbc5a._comment create mode 100644 doc/bugs/nfs_mounted_repo_results_in_errors_on_drop_move.mdwn create mode 100644 doc/bugs/non-annexed_file_changed_to_annexed_on_typechange.mdwn create mode 100644 doc/bugs/non-annexed_file_changed_to_annexed_on_typechange/comment_1_6ac691645edb483797bee05043fd83b3._comment create mode 100644 doc/bugs/non-annexed_file_changed_to_annexed_on_typechange/comment_2_5d67e3a60b7cc30c2b1857f50895d363._comment create mode 100644 doc/bugs/non-annexed_file_changed_to_annexed_on_typechange/comment_3_78f1e081b92f418c20893d86a8715501._comment create mode 100644 doc/bugs/non-annexed_file_changed_to_annexed_on_typechange/comment_4_1e2a59e0eec89ef1a57d1488ff40dcf0._comment create mode 100644 doc/bugs/non-annexed_file_changed_to_annexed_on_typechange/comment_5_5e74431048b07631e0dbeca90fdb365b._comment create mode 100644 doc/bugs/non-annexed_file_changed_to_annexed_on_typechange/comment_6_3724e1c1a5fc6d3589452478249792ec._comment create mode 100644 doc/bugs/non-annexed_file_changed_to_annexed_on_typechange/comment_7_7f841ea7bf7d44f3d810ca097ac9eb47._comment create mode 100644 doc/bugs/non-repos_in_repositories_list___40__+_other_weird_output__41___from_git_annex_status.mdwn create mode 100644 doc/bugs/non-repos_in_repositories_list___40__+_other_weird_output__41___from_git_annex_status/comment_1_fcd230cbb2ac363c469b98021042c011._comment create mode 100644 doc/bugs/non-repos_in_repositories_list___40__+_other_weird_output__41___from_git_annex_status/comment_2_23207ecabd4b41d9551d0491fa71e96b._comment create mode 100644 doc/bugs/non-repos_in_repositories_list___40__+_other_weird_output__41___from_git_annex_status/comment_3_6ea92adfe955b6a5cd2a39fea78b3bf6._comment create mode 100644 doc/bugs/non-repos_in_repositories_list___40__+_other_weird_output__41___from_git_annex_status/comment_4_d0e55585f1612148163039d157253258._comment create mode 100644 doc/bugs/non-repos_in_repositories_list___40__+_other_weird_output__41___from_git_annex_status/comment_6_5506dc1b08516677886da4aa97263864._comment create mode 100644 doc/bugs/non-repos_in_repositories_list___40__+_other_weird_output__41___from_git_annex_status/comment_7_073449cc2cb73efd2b2d3d778a5573de._comment create mode 100644 doc/bugs/non-repos_in_repositories_list___40__+_other_weird_output__41___from_git_annex_status/comment_7_3516e71ba3b07427a10cbb4965712aa6._comment create mode 100644 doc/bugs/non-repos_in_repositories_list___40__+_other_weird_output__41___from_git_annex_status/comment_8_ea2e4704adb2f304f9c11c61eb62e919._comment create mode 100644 doc/bugs/non-repos_in_repositories_list___40__+_other_weird_output__41___from_git_annex_status/comment_9_4d17fedead7977541371a3f2c192e030._comment create mode 100644 doc/bugs/not_possible_to_have_annex_on_a_separate_filesystem.mdwn create mode 100644 doc/bugs/old_data_isn__39__t_unused_after_migration.mdwn create mode 100644 doc/bugs/on--git-dir_and_--work-tree_options.mdwn create mode 100644 doc/bugs/optinally_transfer_file_unencryptedly/comment_1_13a7653d96ddf91f4492a9f3555a69aa._comment create mode 100644 doc/bugs/optinally_transfer_file_unencryptedly/comment_2_31f154011ec26a463de7b1e307e49cb6._comment create mode 100644 doc/bugs/optinally_transfer_file_unencryptedly/comment_3_33433bcfb1946b52f1f41b9158ab452d._comment create mode 100644 doc/bugs/ordering.mdwn create mode 100644 doc/bugs/pasting_into_annex_on_OSX.mdwn create mode 100644 doc/bugs/pasting_into_annex_on_OSX/comment_1_4eab52bb6eda92e39bdaa8eee8f31a7f._comment create mode 100644 doc/bugs/pasting_into_annex_on_OSX/comment_2_f1b58adfec179b75c1fc2bf578a3b5c4._comment create mode 100644 doc/bugs/pasting_into_annex_on_OSX/comment_3_270aa7680c3b899a92ce6543eaba666a._comment create mode 100644 doc/bugs/pasting_into_annex_on_OSX/comment_4_ec11a80d5b0f78c7a927f8aa71a6c57a._comment create mode 100644 doc/bugs/pasting_into_annex_on_OSX/comment_5_1928bd25e5e6874a3b83c2f2adc776f5._comment create mode 100644 doc/bugs/pasting_into_annex_on_OSX/comment_6_0fe288f54b781a0c51395cb32f0e2f9d._comment create mode 100644 doc/bugs/problem_commit_normal_links.mdwn create mode 100644 doc/bugs/problem_with_upgrade_v2_-__62___v3.mdwn create mode 100644 doc/bugs/problem_with_upgrade_v2_-__62___v3/comment_1_5f60006c9bb095167d817f234a14d20b._comment create mode 100644 doc/bugs/problem_with_upgrade_v2_-__62___v3/comment_2_cd0123392b16d89db41b45464165c247._comment create mode 100644 doc/bugs/problem_with_upgrade_v2_-__62___v3/comment_3_86d9e7244ae492bcbe62720b8c4fc4a9._comment create mode 100644 doc/bugs/problem_with_upgrade_v2_-__62___v3/comment_4_91439d4dbbf1461e281b276eb0003691._comment create mode 100644 doc/bugs/problem_with_upgrade_v2_-__62___v3/comment_5_ca33a9ca0df33f7c1b58353d7ffb943d._comment create mode 100644 doc/bugs/problem_with_upgrade_v2_-__62___v3/comment_6_f360f0006bc9115bc5a3e2eb9fe58abd._comment create mode 100644 doc/bugs/problems_with_utf8_names.mdwn create mode 100644 doc/bugs/problems_with_utf8_names/comment_1_3c7e3f021c2c94277eecf9c8af6cec5f._comment create mode 100644 doc/bugs/problems_with_utf8_names/comment_2_bad4c4c5f54358d1bc0ab2adc713782a._comment create mode 100644 doc/bugs/problems_with_utf8_names/comment_3_4f936a5d3f9c7df64c8a87e62b7fbfdc._comment create mode 100644 doc/bugs/problems_with_utf8_names/comment_4_93bee35f5fa7744834994bc7a253a6f9._comment create mode 100644 doc/bugs/problems_with_utf8_names/comment_5_519cda534c7aea7f5ad5acd3f76e21fa._comment create mode 100644 doc/bugs/problems_with_utf8_names/comment_6_52e0bfff2b177b6f92e226b25d2f3ff1._comment create mode 100644 doc/bugs/problems_with_utf8_names/comment_7_0cc588f787d6eecfa19a8f6cee4b07b5._comment create mode 100644 doc/bugs/problems_with_utf8_names/comment_8_ff5c6da9eadfee20c18c86b648a62c47._comment create mode 100644 doc/bugs/random_files_vanishing_when_assistant_gets_restarted.mdwn create mode 100644 doc/bugs/random_files_vanishing_when_assistant_gets_restarted/comment_1_53b4f388c47c1b3f6ffa4fc2155b30fc._comment create mode 100644 doc/bugs/random_files_vanishing_when_assistant_gets_restarted/comment_2_e66532b23b089c9ea61122d6664cddb9._comment create mode 100644 doc/bugs/random_files_vanishing_when_assistant_gets_restarted/comment_3_c9d692c867acc076f64f1213ea03ca11._comment create mode 100644 doc/bugs/regression_in_direct_mode_on_windows_:_weird___96__git_annex_sync__96___behavior.mdwn create mode 100644 doc/bugs/regression_in_direct_mode_on_windows_:_weird___96__git_annex_sync__96___behavior/comment_1_1a0b964f93c753838d6ccbdc8f79b39e._comment create mode 100644 doc/bugs/regression_in_direct_mode_on_windows_:_weird___96__git_annex_sync__96___behavior/comment_2_d22dcd7f95c5dc1c381c3c746781efce._comment create mode 100644 doc/bugs/regression_in_direct_mode_on_windows_:_weird___96__git_annex_sync__96___behavior/comment_3_a25140eb90f6b24c1a3ca39c901694e2._comment create mode 100644 doc/bugs/regression_in_direct_mode_on_windows_:_weird___96__git_annex_sync__96___behavior/comment_4_825e15183008ff7d97a81cacc3f55fb4._comment create mode 100644 doc/bugs/regression_in_direct_mode_on_windows_:_weird___96__git_annex_sync__96___behavior/comment_5_e858fc7c729cd39740354fb12627d556._comment create mode 100644 doc/bugs/regression_in_direct_mode_on_windows_:_weird___96__git_annex_sync__96___behavior/comment_6_9881b0f2dfb0907a60c0da296bc3da3f._comment create mode 100644 doc/bugs/regression_in_direct_mode_on_windows_:_weird___96__git_annex_sync__96___behavior/comment_7_ca017b9d3bafea4cb31448c802f3834e._comment create mode 100644 doc/bugs/reinject_should_leave_file_in_place_on_checksum_mismatch.mdwn create mode 100644 doc/bugs/remote_files_appear_but_are_unreadable_because_their_symlink_targets_don__39__t_exist.mdwn create mode 100644 doc/bugs/remote_files_appear_but_are_unreadable_because_their_symlink_targets_don__39__t_exist/comment_10_8a1d16b2aaba224e94be3d9dcc036d91._comment create mode 100644 doc/bugs/remote_files_appear_but_are_unreadable_because_their_symlink_targets_don__39__t_exist/comment_11_434ed328a22a6657dba3b2929a56e499._comment create mode 100644 doc/bugs/remote_files_appear_but_are_unreadable_because_their_symlink_targets_don__39__t_exist/comment_12_1837b70ace42882db3ab82e001680934._comment create mode 100644 doc/bugs/remote_files_appear_but_are_unreadable_because_their_symlink_targets_don__39__t_exist/comment_13_ca9c87a10f29e41572540edeb99652f2._comment create mode 100644 doc/bugs/remote_files_appear_but_are_unreadable_because_their_symlink_targets_don__39__t_exist/comment_1_69eafc4201e3014ef1b5d74fe319e462._comment create mode 100644 doc/bugs/remote_files_appear_but_are_unreadable_because_their_symlink_targets_don__39__t_exist/comment_2_b7a64db9abe006af8c30169ad849efe9._comment create mode 100644 doc/bugs/remote_files_appear_but_are_unreadable_because_their_symlink_targets_don__39__t_exist/comment_3_197ac6070f256131c6e18a07aa3834fa._comment create mode 100644 doc/bugs/remote_files_appear_but_are_unreadable_because_their_symlink_targets_don__39__t_exist/comment_4_fe07832333b536c71b7dcb46a4a44bd0._comment create mode 100644 doc/bugs/remote_files_appear_but_are_unreadable_because_their_symlink_targets_don__39__t_exist/comment_5_540bca4e6fdfc10eeab875ecc0f2b3f3._comment create mode 100644 doc/bugs/remote_files_appear_but_are_unreadable_because_their_symlink_targets_don__39__t_exist/comment_6_3f236b35e9820cd88bb77fcd57d6975e._comment create mode 100644 doc/bugs/remote_files_appear_but_are_unreadable_because_their_symlink_targets_don__39__t_exist/comment_7_3cc5dae0351201522711a7caeecd60d5._comment create mode 100644 doc/bugs/remote_files_appear_but_are_unreadable_because_their_symlink_targets_don__39__t_exist/comment_8_3c3883cb66d02a15d5de84d22aa113da._comment create mode 100644 doc/bugs/remote_files_appear_but_are_unreadable_because_their_symlink_targets_don__39__t_exist/comment_9_c8cece9559bd2dc6154cd28772369e48._comment create mode 100644 doc/bugs/removable_device_configurator_chokes_on_spaces.mdwn create mode 100644 doc/bugs/renaming_a_file_makes_annex_get_the_file__39__s_content_from_remote.mdwn create mode 100644 doc/bugs/renaming_a_file_makes_annex_get_the_file__39__s_content_from_remote/comment_1_d6aad1831674586fe4cdf61dd2a4bbb9._comment create mode 100644 doc/bugs/renaming_a_file_makes_annex_get_the_file__39__s_content_from_remote/comment_2_8591e174c1a8cddfae9371407a58ff1c._comment create mode 100644 doc/bugs/restart_daemon_required.mdwn create mode 100644 doc/bugs/restart_daemon_required/comment_1_f79ac16cc9f1e3b08cd121bf5efb29c3._comment create mode 100644 doc/bugs/restart_daemon_required/comment_2_50c1b268a3cc4514681059eabca674e3._comment create mode 100644 doc/bugs/restart_daemon_required/comment_3_1716e0f3c7c44dc77ebf7f00fdd8f9e3._comment create mode 100644 doc/bugs/restart_daemon_required/comment_4_3ce776786eca83fcb8ff94c8f6ff3eb9._comment create mode 100644 doc/bugs/rsync_remote_shows_no_progress.mdwn create mode 100644 doc/bugs/rsync_remote_shows_no_progress/comment_1_a7f5d646a924c462b987561cf6fc4318._comment create mode 100644 doc/bugs/rsync_special_remote_fails_to___96__get__96___files_which_have_names_containing_spaces.mdwn create mode 100644 doc/bugs/scp_interrupt_to_background.mdwn create mode 100644 doc/bugs/show_version_without_having_to_be_in_a_git_repo.mdwn create mode 100644 doc/bugs/signal_weirdness.mdwn create mode 100644 doc/bugs/smarter_flood_filling.mdwn create mode 100644 doc/bugs/softlink_mtime.mdwn create mode 100644 doc/bugs/ssh_connection_caching_broken_on_NTFS.mdwn create mode 100644 doc/bugs/ssh_connection_caching_broken_on_NTFS/comment_1_54e7e12514f4c109fd57a4eb744b731a._comment create mode 100644 doc/bugs/submodule_path_problem.mdwn create mode 100644 doc/bugs/submodule_path_problem/comment_1_69aec9207d2e9da4bc042d3f4963d80e._comment create mode 100644 doc/bugs/submodule_path_problem/comment_2_53d9eb28cb70b51637470175a80ddf35._comment create mode 100644 doc/bugs/submodule_path_problem/comment_3_aa5e0f99000a5b4988bccbb2ca28353b._comment create mode 100644 doc/bugs/submodule_path_problem/comment_4_ab1508a5a04e2106aad5e7985775a6fa._comment create mode 100644 doc/bugs/submodule_path_problem/comment_5_8c7539d1c11b81f5d46aa8e1c61745ae._comment create mode 100644 doc/bugs/submodule_path_problem/comment_6_cacc91afcb1739dfca3a60590bb70356._comment create mode 100644 doc/bugs/subtle_build_issue_on_OSX_10.7_and_Haskell_Platform___40__if_you_have_the_32bit_version_installed__41__.mdwn create mode 100644 doc/bugs/subtle_build_issue_on_OSX_10.7_and_Haskell_Platform___40__if_you_have_the_32bit_version_installed__41__/comment_1_6208e70a21a048d5423926d16e32d421._comment create mode 100644 doc/bugs/subtle_build_issue_on_OSX_10.7_and_Haskell_Platform___40__if_you_have_the_32bit_version_installed__41__/comment_2_8765b6190e79251637bb59ba28f245c1._comment create mode 100644 doc/bugs/support_bare_git_repo__44___with_the_annex_directory_exposed_to_http.mdwn create mode 100644 doc/bugs/test_suite_failure_on_samba_mount.mdwn create mode 100644 doc/bugs/test_suite_shouldn__39__t_fail_silently.mdwn create mode 100644 doc/bugs/tests_fail_when_there_is_no_global_.gitconfig_for_the_user.mdwn create mode 100644 doc/bugs/tests_failed_to_build_-_after_an_update_of_haskell_platform.mdwn create mode 100644 doc/bugs/tests_failed_to_build_-_after_an_update_of_haskell_platform/comment_1_20a6fe046111e9ae56fd4d9c9f41f536._comment create mode 100644 doc/bugs/tests_failed_to_build_-_after_an_update_of_haskell_platform/comment_2_6fdc5f8b07908c6eda8a97690408f44e._comment create mode 100644 doc/bugs/tests_failed_to_build_-_after_an_update_of_haskell_platform/comment_3_014474a133c7ff0131029d8721afc710._comment create mode 100644 doc/bugs/tests_failed_to_build_-_after_an_update_of_haskell_platform/comment_4_9c537e251dc99667fe87870804d802c2._comment create mode 100644 doc/bugs/the_tip_at_commit_6cecc26206c4a539999b04664136c6f785211a41_disables_the_watch_command_on_OSX.mdwn create mode 100644 doc/bugs/three_character_directories_created.mdwn create mode 100644 doc/bugs/three_character_directories_created/comment_1_dd91de24dab4f2eaded1f7d659869d4d._comment create mode 100644 doc/bugs/three_character_directories_created/comment_2_f6375964a6c8bb1e6c5b7238effca66d._comment create mode 100644 doc/bugs/three_character_directories_created/comment_3_776e0a9b938d8b260a5111594b442536._comment create mode 100644 doc/bugs/three_character_directories_created/comment_4_e288bacdb336c4886adb6eeb4dca1e92._comment create mode 100644 doc/bugs/three_character_directories_created/comment_5_359b80948ac92a0f1eb695599456486c._comment create mode 100644 doc/bugs/three_way_sync_via_S3_and_Jabber.mdwn create mode 100644 doc/bugs/three_way_sync_via_S3_and_Jabber/comment_10_fc5ec5505f141bb9135e772d1094bc4d._comment create mode 100644 doc/bugs/three_way_sync_via_S3_and_Jabber/comment_11_0df2210c30dec6d88d7858d93eec19a3._comment create mode 100644 doc/bugs/three_way_sync_via_S3_and_Jabber/comment_1_41682b2e72e657e0f23af244f8345e85._comment create mode 100644 doc/bugs/three_way_sync_via_S3_and_Jabber/comment_2_c7b4ea9aea6839763eb8b89e8d6a5ad5._comment create mode 100644 doc/bugs/three_way_sync_via_S3_and_Jabber/comment_3_063f5e5e554ad6710f16394906d87616._comment create mode 100644 doc/bugs/three_way_sync_via_S3_and_Jabber/comment_4_197ad39b4a46936afeeb04eb26cf1ef3._comment create mode 100644 doc/bugs/three_way_sync_via_S3_and_Jabber/comment_5_0b0d829ccd255be0177ae9d8f6b10e63._comment create mode 100644 doc/bugs/three_way_sync_via_S3_and_Jabber/comment_6_37a8e19440c764317589bc4248cbccdf._comment create mode 100644 doc/bugs/three_way_sync_via_S3_and_Jabber/comment_7_12eb333327d31ca2bfee3f3c5e26d641._comment create mode 100644 doc/bugs/three_way_sync_via_S3_and_Jabber/comment_8_e6b1084b2f18d8e536c8692e165754a3._comment create mode 100644 doc/bugs/three_way_sync_via_S3_and_Jabber/comment_9_2120a1c3e5f490a55f68bb1bef5efd0d._comment create mode 100644 doc/bugs/tmp_file_handling.mdwn create mode 100644 doc/bugs/tmp_file_handling/comment_1_0300c11ee3f94a9e7c832671e16f5511._comment create mode 100644 doc/bugs/tmp_file_handling/comment_2_cc14c7a79a544e47654e4cd8abc85edd._comment create mode 100644 doc/bugs/touch.hsc_has_problems_on_non-linux_based_systems.mdwn create mode 100644 doc/bugs/touch.hsc_has_problems_on_non-linux_based_systems/comment_1_1d38283c9ea87174f3bbef9a58f5cb88._comment create mode 100644 doc/bugs/touch.hsc_has_problems_on_non-linux_based_systems/comment_2_bf112edd075fbebe4fc959a387946eb9._comment create mode 100644 doc/bugs/touch.hsc_has_problems_on_non-linux_based_systems/comment_3_a46080fbe82adf0986c5dc045e382501._comment create mode 100644 doc/bugs/touch.hsc_has_problems_on_non-linux_based_systems/comment_4_760437bf3ba972a775bb190fb4b38202._comment create mode 100644 doc/bugs/touch.hsc_has_problems_on_non-linux_based_systems/comment_5_060ba5ea88dcab2f4a0c199f13ef4f67._comment create mode 100644 doc/bugs/touch.hsc_has_problems_on_non-linux_based_systems/comment_6_548303d6ffb21a9370b6904f41ff49c1._comment create mode 100644 doc/bugs/touch.hsc_has_problems_on_non-linux_based_systems/comment_7_7ca00527ab5db058aadec4fe813e51fd._comment create mode 100644 doc/bugs/touch.hsc_has_problems_on_non-linux_based_systems/comment_8_881aecb9ae671689453f6d5d780d844b._comment create mode 100644 doc/bugs/transferkey_fails_due_to_gpg.mdwn create mode 100644 doc/bugs/transferkey_fails_due_to_gpg/comment_1_f6434400d528a0fa59c056995ff2e6f3._comment create mode 100644 doc/bugs/transferkey_fails_due_to_gpg/comment_2_c540b05b62a3186a87efcb180ea2a52d._comment create mode 100644 doc/bugs/transferkey_fails_due_to_gpg/comment_3_9ad2ef73169dbd2866da2f4259ab0f00._comment create mode 100644 doc/bugs/transferkey_fails_due_to_gpg/comment_4_7631b8842efba6a4aad87386ce9443a7._comment create mode 100644 doc/bugs/typo_in___34__ready_to_add_remote_server__34___message.mdwn create mode 100644 doc/bugs/unable_to_change_repository_group_of___34__here__34__.mdwn create mode 100644 doc/bugs/unannex_and_uninit_do_not_work_when_git_index_is_broken.mdwn create mode 100644 doc/bugs/unannex_and_uninit_do_not_work_when_git_index_is_broken/comment_1_1931e733f0698af5603a8b92267203d4._comment create mode 100644 doc/bugs/unannex_and_uninit_do_not_work_when_git_index_is_broken/comment_2_40920b88537b7715395808d8aa94bf03._comment create mode 100644 doc/bugs/unannex_command_doesn__39__t_all_files.mdwn create mode 100644 doc/bugs/unannex_removes_object_even_if_referred_to_by_others.mdwn create mode 100644 doc/bugs/unannex_removes_object_even_if_referred_to_by_others/comment_1_0ce72d0f67082f202cfa58b7c00f2fd3._comment create mode 100644 doc/bugs/unannex_removes_object_even_if_referred_to_by_others/comment_2_647f49ffcaa348660659f9954a59b3ae._comment create mode 100644 doc/bugs/unannex_removes_object_even_if_referred_to_by_others/comment_3_3f7f4b55b7ec2641a70109788e0b5672._comment create mode 100644 doc/bugs/unannex_removes_object_even_if_referred_to_by_others/comment_4_313d393c416495aa0f8573113e41c2f7._comment create mode 100644 doc/bugs/unannex_removes_object_even_if_referred_to_by_others/comment_5_c0e7742672db2629bd906cebefe74f72._comment create mode 100644 doc/bugs/unannex_removes_object_even_if_referred_to_by_others/comment_6_c56171665db3ed14109a09097d49ac5d._comment create mode 100644 doc/bugs/unannex_vs_unlock_hook_confusion.mdwn create mode 100644 doc/bugs/undefined.mdwn create mode 100644 doc/bugs/unfinished_repos_in_webapp.mdwn create mode 100644 doc/bugs/unfinished_repos_in_webapp/comment_1_9628b100e39489be9f28ef75276a7341._comment create mode 100644 doc/bugs/unfinished_repos_in_webapp/comment_2_ba0fbff536b1d067c4098db401dc49f2._comment create mode 100644 doc/bugs/unfinished_repos_in_webapp/comment_3_fd554aa7d93117177784a29270ccf790._comment create mode 100644 doc/bugs/unhappy_without_UTF8_locale.mdwn create mode 100644 doc/bugs/uninit_and_indirect_don__39__t_work_on_android.mdwn create mode 100644 doc/bugs/uninit_and_indirect_don__39__t_work_on_android/comment_1_fec69c4c41987b9469eaa8f745c0a124._comment create mode 100644 doc/bugs/uninit_and_indirect_don__39__t_work_on_android/comment_2_54c3fa77a069b36d03c41aad08fee9af._comment create mode 100644 doc/bugs/uninit_does_not_abort_when_hard_link_creation_fails.mdwn create mode 100644 doc/bugs/uninit_does_not_work_in_old_repos.mdwn create mode 100644 doc/bugs/uninit_does_not_work_in_old_repos/comment_1_bc0619c6e17139df74639448aa6a0f72._comment create mode 100644 doc/bugs/uninit_loses_data_if_git-annex_add_didn__39__t_complete.mdwn create mode 100644 doc/bugs/uninit_should_not_run_when_branch_git-annex_is_checked_out.mdwn create mode 100644 doc/bugs/unlock_fails_silently_with_directory_symlinks.mdwn create mode 100644 doc/bugs/unlock_not_working_on_os_x_10.6_-_cp:_illegal_option_--_-_.mdwn create mode 100644 doc/bugs/unlock_not_working_on_os_x_10.6_-_cp:_illegal_option_--_-_/comment_1_a634a9f1c023bf836183de64abab1224._comment create mode 100644 doc/bugs/unlock_not_working_on_os_x_10.6_-_cp:_illegal_option_--_-_/comment_2_d9ae61a7c3f1eb243ca650945b40f21d._comment create mode 100644 doc/bugs/unlock_not_working_on_os_x_10.6_-_cp:_illegal_option_--_-_/comment_3_fe229c03c14e8eb2b57389e0e193ed99._comment create mode 100644 doc/bugs/unlock_not_working_on_os_x_10.6_-_cp:_illegal_option_--_-_/comment_4_fa12afe295de63c4aa7eb043b715325a._comment create mode 100644 doc/bugs/unlock_then_lock_of_uncommitted_file_loses_it.mdwn create mode 100644 doc/bugs/upgrade_left_untracked_.git-annex__47____42___directories.mdwn create mode 100644 doc/bugs/upgrade_left_untracked_.git-annex__47____42___directories/comment_1_9ca2da52f3c8add0276b72d6099516a6._comment create mode 100644 doc/bugs/upgrade_left_untracked_.git-annex__47____42___directories/comment_2_e14e84b770305893f2fc6e4938359f47._comment create mode 100644 doc/bugs/upgrade_left_untracked_.git-annex__47____42___directories/comment_3_ec04e306c96fd20ab912aea54a8340aa._comment create mode 100644 doc/bugs/upgraded_annex__44___suddenly_trying_to_grab_archive_content_onto_client_again.mdwn create mode 100644 doc/bugs/upgraded_annex__44___suddenly_trying_to_grab_archive_content_onto_client_again/comment_10_51097a6b84edcc607abc0e6e21ca21f2._comment create mode 100644 doc/bugs/upgraded_annex__44___suddenly_trying_to_grab_archive_content_onto_client_again/comment_1_c34a4009213c410bba3c147ae0552029._comment create mode 100644 doc/bugs/upgraded_annex__44___suddenly_trying_to_grab_archive_content_onto_client_again/comment_2_634542867fd28962c47b7bc3ea022175._comment create mode 100644 doc/bugs/upgraded_annex__44___suddenly_trying_to_grab_archive_content_onto_client_again/comment_3_301f3ff2d203ac4c58a037e553b2c14d._comment create mode 100644 doc/bugs/upgraded_annex__44___suddenly_trying_to_grab_archive_content_onto_client_again/comment_4_82ecdc88ccc1f87386b128adc4ff9af4._comment create mode 100644 doc/bugs/upgraded_annex__44___suddenly_trying_to_grab_archive_content_onto_client_again/comment_6_158b2ba3da815910505899606177d415._comment create mode 100644 doc/bugs/upgraded_annex__44___suddenly_trying_to_grab_archive_content_onto_client_again/comment_6_b068924802f3917e3e005350cb0cc2a2._comment create mode 100644 doc/bugs/upgraded_annex__44___suddenly_trying_to_grab_archive_content_onto_client_again/comment_7_f4772858c927d4a62edc3caf59b5da10._comment create mode 100644 doc/bugs/upgraded_annex__44___suddenly_trying_to_grab_archive_content_onto_client_again/comment_8_d0923d2950357f4444c5ef94ff196ba3._comment create mode 100644 doc/bugs/upgraded_annex__44___suddenly_trying_to_grab_archive_content_onto_client_again/comment_9_7fb30cb80aecc60e48c64846aa185206._comment create mode 100644 doc/bugs/uploads_queued_to_annex-ignore_remotes.mdwn create mode 100644 doc/bugs/uploads_queued_to_annex-ignore_remotes/comment_1_fa1c98f38253db8c2be3604c72eb3726._comment create mode 100644 doc/bugs/using_old_remote_format_generates_irritating_output.mdwn create mode 100644 doc/bugs/using_old_remote_format_generates_irritating_output/comment_1_fceba878f1097e27f056580e8d6d5b31._comment create mode 100644 doc/bugs/using_old_remote_format_generates_irritating_output/comment_2_416992874813f120721a56d88b2bef65._comment create mode 100644 doc/bugs/using_old_remote_format_generates_irritating_output/comment_3_a20f470c5226ac5693eb15146a02b3f5._comment create mode 100644 doc/bugs/using_old_remote_format_generates_irritating_output/comment_4_a81f06191bc03a7aad5929af99f0634e._comment create mode 100644 doc/bugs/using_old_remote_format_generates_irritating_output/comment_5_7438caecf78b4fb5d21f9f31dff95cf2._comment create mode 100644 doc/bugs/utf8.mdwn create mode 100644 doc/bugs/utf8/comment_10_f298b8b480d3ab2dd9c279589afcd0ea._comment create mode 100644 doc/bugs/utf8/comment_11_a8864a46f8154680beeea27449ac6f09._comment create mode 100644 doc/bugs/utf8/comment_12_2202c3479d19d306f31aac5a47b55e7d._comment create mode 100644 doc/bugs/utf8/comment_13_7044d2c5bb1c91ee37eb9868963a1ff2._comment create mode 100644 doc/bugs/utf8/comment_14_656b3caa16ae93b092fb5804fa575a3b._comment create mode 100644 doc/bugs/utf8/comment_15_25b3d4c47c45b72129b17b171a45c5f9._comment create mode 100644 doc/bugs/utf8/comment_16_2aaab9253bbc75012292c7b5a7d55696._comment create mode 100644 doc/bugs/utf8/comment_1_416ad6fb5f7379732129dc5283a7e550._comment create mode 100644 doc/bugs/utf8/comment_2_cd55f6bbeb145fd554f331dcff64f5e1._comment create mode 100644 doc/bugs/utf8/comment_3_bb583a419d6fa4e33e5364c4468b35c6._comment create mode 100644 doc/bugs/utf8/comment_4_cd8a22cfb70d9d21f0a5339ccc52ee93._comment create mode 100644 doc/bugs/utf8/comment_5_14eefd4bee283802e9c462fa20b7835c._comment create mode 100644 doc/bugs/utf8/comment_6_58d8b5bdb9f11e8c344e86a675a075dd._comment create mode 100644 doc/bugs/utf8/comment_7_00fa9672ce55b6bfa885b8a13287ac25._comment create mode 100644 doc/bugs/utf8/comment_8_a01e26fa0fafbc291020f53dbfdf6443._comment create mode 100644 doc/bugs/utf8/comment_9_b7c084be01ce985be51e48503fcba468._comment create mode 100644 doc/bugs/uuid.log_trust.log_and_remote.log_merge_wackiness.mdwn create mode 100644 doc/bugs/version_doesn__39__t_show___34__Feeds__34___but_podcasting_feature_working.mdwn create mode 100644 doc/bugs/view_logs_fails:_Internal_Server_Error__internal_liftAnnex.mdwn create mode 100644 doc/bugs/view_logs_fails:_Internal_Server_Error__internal_liftAnnex/comment_1_57547f9a480df2c3f7b3997b0fb7039a._comment create mode 100644 doc/bugs/view_logs_fails:_Internal_Server_Error__internal_liftAnnex/comment_2_99f12da3ef01141dc7a9105fcf966793._comment create mode 100644 doc/bugs/warning_-_WebApp_crashed:___60__file_descriptor_15__62__:_hPutStr:_illegal_operation___40__handle_is_closed__41___on_Android.mdwn create mode 100644 doc/bugs/warning_-_WebApp_crashed:___60__file_descriptor_15__62__:_hPutStr:_illegal_operation___40__handle_is_closed__41___on_Android/comment_1_e8866dc15f8fc049229e7451addad1d5._comment create mode 100644 doc/bugs/warning_-_WebApp_crashed:___60__file_descriptor_15__62__:_hPutStr:_illegal_operation___40__handle_is_closed__41___on_Android/comment_2_ee616b0251ffaace9844cfd7af896c35._comment create mode 100644 doc/bugs/warning_-_WebApp_crashed:___60__file_descriptor_15__62__:_hPutStr:_illegal_operation___40__handle_is_closed__41___on_Android/comment_3_6b8bd314b647ea3a485f5faf4856f9a9._comment create mode 100644 doc/bugs/warning_-_WebApp_crashed:___60__file_descriptor_15__62__:_hPutStr:_illegal_operation___40__handle_is_closed__41___on_Android/comment_4_7009b6727ba40bc9bd1b1f939e75d093._comment create mode 100644 doc/bugs/warning_-_WebApp_crashed:___60__file_descriptor_15__62__:_hPutStr:_illegal_operation___40__handle_is_closed__41___on_Android/comment_5_00ddf7ade6cca758afa838be0b9588cb._comment create mode 100644 doc/bugs/warning_-_WebApp_crashed:___60__file_descriptor_15__62__:_hPutStr:_illegal_operation___40__handle_is_closed__41___on_Android/comment_6_6137ef0ad01d5600dab6fccbeed9a88b._comment create mode 100644 doc/bugs/warning_-_WebApp_crashed:___60__file_descriptor_15__62__:_hPutStr:_illegal_operation___40__handle_is_closed__41___on_Android/comment_7_4b79d7ea338d9f70eb80b8cc2c5a21e4._comment create mode 100644 doc/bugs/watch_command_on_OSX_--_hangs_with_a_small_repo.mdwn create mode 100644 doc/bugs/watch_command_on_OSX_--_hangs_with_a_small_repo/comment_1_63f04694610839db0c2381005b15da35._comment create mode 100644 doc/bugs/watch_command_on_OSX_--_hangs_with_a_small_repo/comment_2_8afe4720e301cf7ccf11ff0a23261936._comment create mode 100644 doc/bugs/watch_command_on_OSX_10.7.mdwn create mode 100644 doc/bugs/watcher_commits_unlocked_files.mdwn create mode 100644 doc/bugs/watcher_commits_unlocked_files/comment_1_f70e1912fde0eee59e208307df06b503._comment create mode 100644 doc/bugs/webapp_hang.mdwn create mode 100644 doc/bugs/webapp_hang/comment_1_08aa908a64d0fe2d50438d01545c3f01._comment create mode 100644 doc/bugs/webapp_hang/comment_2_2a21ac5657128a454f9deb77c4d18057._comment create mode 100644 doc/bugs/webapp_raise_an_internal_server_error_upon_creating_the_initial_repo.mdwn create mode 100644 doc/bugs/webapp_raise_an_internal_server_error_upon_creating_the_initial_repo/comment_1_1bcf0f565eacac851bd21cd428c8e0a5._comment create mode 100644 doc/bugs/webapp_raise_an_internal_server_error_upon_creating_the_initial_repo/comment_2_7dd2483b5b07df8f3b37a34651c05962._comment create mode 100644 doc/bugs/webapp_requires_reload_for_notification_bubbles.mdwn create mode 100644 doc/bugs/webapp_requires_reload_for_notification_bubbles/comment_1_b15480e5dec1ffbebb8cde1ca8d7c9d5._comment create mode 100644 doc/bugs/webapp_requires_reload_for_notification_bubbles/comment_2_8dad57a852e1db804aa38f90f3bb398b._comment create mode 100644 doc/bugs/webapp_shows___34__Added_x_files__34___a_bit_ugly.mdwn create mode 100644 doc/bugs/weird_local_clone_confuses.mdwn create mode 100644 doc/bugs/whereis_outputs_no_informaiton_for_unlocked_files.mdwn create mode 100644 doc/bugs/windows_install_failure.mdwn create mode 100644 doc/bugs/windows_install_failure/comment_1_f339574c7cfa35c1f0dfd515fde457f5._comment create mode 100644 doc/bugs/windows_install_failure/comment_2_1d3364d8f5c4963f3a7e473298ec6ed1._comment create mode 100644 doc/bugs/windows_port_-_can__39__t_directly_access_files.mdwn create mode 100644 doc/bugs/windows_port_-_can__39__t_directly_access_files/comment_1_03ef9d33839173044dcc4f2b37f575d2._comment create mode 100644 doc/bugs/windows_port_-_can__39__t_directly_access_files/comment_2_c65e5491c82908af46fe2c97e048d210._comment create mode 100644 doc/bugs/windows_port_-_git_annex_add_hangs_when_adding_17_files_at_once_or_more_.mdwn create mode 100644 doc/bugs/windows_port_-_repo_can__39__t_pull_newly_added_files_.mdwn create mode 100644 doc/bugs/windows_port_-_repo_can__39__t_pull_newly_added_files_/comment_10_b4f5e2d6a0d690f6b0089fa80a3c920b._comment create mode 100644 doc/bugs/windows_port_-_repo_can__39__t_pull_newly_added_files_/comment_1_c2092add1430667108a3fdc5e1c9b5f5._comment create mode 100644 doc/bugs/windows_port_-_repo_can__39__t_pull_newly_added_files_/comment_2_f0ea453951daf84dbddc653ac64822b6._comment create mode 100644 doc/bugs/windows_port_-_repo_can__39__t_pull_newly_added_files_/comment_3_35a8be5ecc9d1b72c38f8ddb47678160._comment create mode 100644 doc/bugs/windows_port_-_repo_can__39__t_pull_newly_added_files_/comment_4_29e72997b88f91f84639587b4cede34c._comment create mode 100644 doc/bugs/windows_port_-_repo_can__39__t_pull_newly_added_files_/comment_5_2de7f6532de4cbc21737ce53a89d6525._comment create mode 100644 doc/bugs/windows_port_-_repo_can__39__t_pull_newly_added_files_/comment_6_80d130b5af829763be77c61a9c5ca306._comment create mode 100644 doc/bugs/windows_port_-_repo_can__39__t_pull_newly_added_files_/comment_7_ec199db851952b40e8b18922da574ea4._comment create mode 100644 doc/bugs/windows_port_-_repo_can__39__t_pull_newly_added_files_/comment_8_d269fcadea9d5a668e3c6d6cf019f56a._comment create mode 100644 doc/bugs/windows_port_-_repo_can__39__t_pull_newly_added_files_/comment_9_908d1b981d56107f29d8972bf11aefc8._comment create mode 100644 doc/bugs/wishlist:_allow_users_to_provide_UUID_when_running___96__git_annex_init__96__.mdwn create mode 100644 doc/bugs/wishlist:_generic_annex.cost-command.mdwn create mode 100644 doc/bugs/wishlist:_make_git_annex_reinject_work_in_direct_mode.mdwn create mode 100644 doc/bugs/wishlist:_more_descriptive_commit_messages_in_git-annex_branch.mdwn create mode 100644 doc/bugs/wishlist:_option_to_print_more_info_with___39__unused__39__.mdwn create mode 100644 doc/bugs/wishlist:_query_things_like_description__44___trust_level.mdwn create mode 100644 doc/bugs/wishlist:_query_things_like_description__44___trust_level/comment_1_14311384788312b96e550749ab7de9ea._comment create mode 100644 doc/bugs/wishlist:_query_things_like_description__44___trust_level/comment_2_342d1ac07573c7ef4e27f003a692e261._comment create mode 100644 doc/bugs/wishlist:_simple_url_for_webapp.mdwn create mode 100644 doc/bugs/wishlist:_simple_url_for_webapp/comment_1_552aad504fbb68d1f85abfde8c535e69._comment create mode 100644 doc/bugs/wishlist:_support_drop__44___find_on_special_remotes.mdwn create mode 100644 doc/bugs/wishlist:_support_drop__44___find_on_special_remotes/comment_1_f11ed642a83d965076778a162f701e84._comment create mode 100644 doc/bugs/wrong_program_path_in___126____47__.config__47__git-annex__47__program.mdwn create mode 100644 doc/bugs/wrong_program_path_in___126____47__.config__47__git-annex__47__program/comment_1_44c11918d00ead38d40556aade98c0af._comment create mode 100644 doc/bugs/xdg-user-dir_error.mdwn create mode 100644 doc/bugs/xmpp_needs_one_account_per_distinct_repository.mdwn create mode 100644 doc/bugs/xmpp_needs_one_account_per_distinct_repository/comment_1_820732c4dcb15186b4f635c50fdb0805._comment create mode 100644 doc/bugs/yesod-default_is_needed_as_a_dependancy.mdwn create mode 100644 doc/bugs/yesod-form_missing.mdwn create mode 100644 doc/coding_style.mdwn create mode 100644 doc/comments.mdwn create mode 100644 doc/contact.mdwn create mode 100644 doc/copies.mdwn create mode 100644 doc/design.mdwn create mode 100644 doc/design/assistant.mdwn create mode 100644 doc/design/assistant/OSX.mdwn create mode 100644 doc/design/assistant/OSX/comment_1_9290f6e6f265e906b08631224392b7bf._comment create mode 100644 doc/design/assistant/android.mdwn create mode 100644 doc/design/assistant/android/comment_10_316bde8d22628e5e9d4f8dabce1d2ad4._comment create mode 100644 doc/design/assistant/android/comment_1_8be9a74e5fc4641c2bf2e1bb7673dd59._comment create mode 100644 doc/design/assistant/android/comment_2_3dd386ac1b757c73d14f14377b9eedd4._comment create mode 100644 doc/design/assistant/android/comment_3_5dca47a4599d6e88d19193701c5a571b._comment create mode 100644 doc/design/assistant/android/comment_4_054f06311e2b51d73be569f181eb004f._comment create mode 100644 doc/design/assistant/android/comment_5_bb3d36e9d29f2fa77bee6d47ef9917fe._comment create mode 100644 doc/design/assistant/android/comment_6_fee32a831eeb5736fe1dce52e30320c8._comment create mode 100644 doc/design/assistant/android/comment_7_d8e9b0a5287fc96b19dc2cb9da3586ce._comment create mode 100644 doc/design/assistant/android/comment_8_79a7b5bb5f4aaeea4a4e8ced0561701a._comment create mode 100644 doc/design/assistant/android/comment_9_55ea70a6929523d26248ff6409b04a6e._comment create mode 100644 doc/design/assistant/blog.mdwn create mode 100644 doc/design/assistant/blog/day_100__cursed_clouds.mdwn create mode 100644 doc/design/assistant/blog/day_102__very_high_level_programming.mdwn create mode 100644 doc/design/assistant/blog/day_102__very_high_level_programming/comment_1_c028b403261dd66bcf83e6ffd134b80b._comment create mode 100644 doc/design/assistant/blog/day_103__bugfix_day.mdwn create mode 100644 doc/design/assistant/blog/day_104__misc.mdwn create mode 100644 doc/design/assistant/blog/day_104__misc/comment_1_13d7fad2d3f8eab10314784c035e2a16._comment create mode 100644 doc/design/assistant/blog/day_105__lazy_Sunday.mdwn create mode 100644 doc/design/assistant/blog/day_106__lazy_Monday.mdwn create mode 100644 doc/design/assistant/blog/day_107__memory_leak.mdwn create mode 100644 doc/design/assistant/blog/day_108__another_zombie_outbreak.mdwn create mode 100644 doc/design/assistant/blog/day_108__another_zombie_outbreak/comment_1_194c48d65993462f809a2cfaa774a3e2._comment create mode 100644 doc/design/assistant/blog/day_108__another_zombie_outbreak/comment_2_ef5ee5933fcadcb81cc81b816db14bda._comment create mode 100644 doc/design/assistant/blog/day_109__dropping.mdwn create mode 100644 doc/design/assistant/blog/day_10__lsof.mdwn create mode 100644 doc/design/assistant/blog/day_10__lsof/comment_1_9b8c28c85c979f32e5c295b6a03c048e._comment create mode 100644 doc/design/assistant/blog/day_110__more_dropping.mdwn create mode 100644 doc/design/assistant/blog/day_111__config_monitor.mdwn create mode 100644 doc/design/assistant/blog/day_112__and_now_for_something_completely_different.mdwn create mode 100644 doc/design/assistant/blog/day_112__and_now_for_something_completely_different/comment_1_5e4fe1538d9ae1c450b0a6602fc6d29b._comment create mode 100644 doc/design/assistant/blog/day_112__and_now_for_something_completely_different/comment_2_c5a734f611ecc95729904e645583ee43._comment create mode 100644 doc/design/assistant/blog/day_112__and_now_for_something_completely_different/comment_3_46b16dcd0fce07036cd8ed6ed9d2b055._comment create mode 100644 doc/design/assistant/blog/day_112__and_now_for_something_completely_different/comment_4_1fe036e4c65fb4211aa2c394f535344a._comment create mode 100644 doc/design/assistant/blog/day_112__and_now_for_something_completely_different/comment_5_e4ba3568c4efd98f212dd47427a1cf47._comment create mode 100644 doc/design/assistant/blog/day_113__notifier_work.mdwn create mode 100644 doc/design/assistant/blog/day_114__xmpp.mdwn create mode 100644 doc/design/assistant/blog/day_114__xmpp/comment_1_c2b0617a2fc3dc4f19a6be6947913842._comment create mode 100644 doc/design/assistant/blog/day_114__xmpp/comment_2_d14375dfb5791615802dab3c5438f8e2._comment create mode 100644 doc/design/assistant/blog/day_114__xmpp/comment_3_6d72ea32c111e605be30ad2153fc71c9._comment create mode 100644 doc/design/assistant/blog/day_114__xmpp/comment_4_e51d6f854db5f9e74a1aa58bd8923795._comment create mode 100644 doc/design/assistant/blog/day_115__my_new_form.mdwn create mode 100644 doc/design/assistant/blog/day_116__the_segfault.mdwn create mode 100644 doc/design/assistant/blog/day_117__new_topologies.mdwn create mode 100644 doc/design/assistant/blog/day_118__monadic_discontinuity.mdwn create mode 100644 doc/design/assistant/blog/day_119__time_for_testing.mdwn create mode 100644 doc/design/assistant/blog/day_11__freebsd.mdwn create mode 100644 doc/design/assistant/blog/day_120__test_day.mdwn create mode 100644 doc/design/assistant/blog/day_121__buddy_list.mdwn create mode 100644 doc/design/assistant/blog/day_122__xmpp_pairing.mdwn create mode 100644 doc/design/assistant/blog/day_122__xmpp_pairing/comment_1_e95efb23eb2e67e3f11a5c7de56424a7._comment create mode 100644 doc/design/assistant/blog/day_122__xmpp_pairing/comment_2_30e251e73146512bde8b2f69eddeef2e._comment create mode 100644 doc/design/assistant/blog/day_123__xmpp_insanity.mdwn create mode 100644 doc/design/assistant/blog/day_124__git_push_over_xmpp_groundwork.mdwn create mode 100644 doc/design/assistant/blog/day_125__xmpp_push_continues.mdwn create mode 100644 doc/design/assistant/blog/day_126__mr_watson_come_here.mdwn create mode 100644 doc/design/assistant/blog/day_126__mr_watson_come_here/comment_1_ee1361e6b235f4e1c00596ba516b519a._comment create mode 100644 doc/design/assistant/blog/day_126__mr_watson_come_here/comment_2_8eb366ae7efb347bd3bbd9a98e0821b3._comment create mode 100644 doc/design/assistant/blog/day_127__xmpp_syncs.mdwn create mode 100644 doc/design/assistant/blog/day_128__last_xmpp_day.mdwn create mode 100644 doc/design/assistant/blog/day_128__last_xmpp_day/comment_1_fd8c1d6358cb50f4dad8ba11d33d861f._comment create mode 100644 doc/design/assistant/blog/day_128__last_xmpp_day/comment_2_43664b73c71c41d71bc95e665f128106._comment create mode 100644 doc/design/assistant/blog/day_128__last_xmpp_day/comment_3_d369b04f686009a9dbb57b999107a55e._comment create mode 100644 doc/design/assistant/blog/day_128__last_xmpp_day/comment_4_095855d301e7ccd3689ffe507cfb63ee._comment create mode 100644 doc/design/assistant/blog/day_128__last_xmpp_day/comment_5_da7b0586b0b28e1e0fe4126f6543a7bc._comment create mode 100644 doc/design/assistant/blog/day_128__last_xmpp_day/comment_6_2f9ba367e19d77bf52f372b6f0f5938a._comment create mode 100644 doc/design/assistant/blog/day_129__release.mdwn create mode 100644 doc/design/assistant/blog/day_12__freebsd_redux.mdwn create mode 100644 doc/design/assistant/blog/day_12__freebsd_redux/comment_1_5da32cf53f1de27bfe6cec2d294db3e1._comment create mode 100644 doc/design/assistant/blog/day_12__freebsd_redux/comment_2_696d6e22034acf5bb60d80124b72ef2f._comment create mode 100644 doc/design/assistant/blog/day_130__what_now.mdwn create mode 100644 doc/design/assistant/blog/day_130__what_now/comment_1_402f00cc034351d8253a797dd4de55bf._comment create mode 100644 doc/design/assistant/blog/day_131__webdav_groundwork.mdwn create mode 100644 doc/design/assistant/blog/day_132__webdav_continued.mdwn create mode 100644 doc/design/assistant/blog/day_133__webdav_working.mdwn create mode 100644 doc/design/assistant/blog/day_134__box.com_configurator.mdwn create mode 100644 doc/design/assistant/blog/day_135__progress_revisited.mdwn create mode 100644 doc/design/assistant/blog/day_136__misc.mdwn create mode 100644 doc/design/assistant/blog/day_137__Glacier.mdwn create mode 100644 doc/design/assistant/blog/day_138__back.mdwn create mode 100644 doc/design/assistant/blog/day_138__back/comment_1_65a8499b284bed38d2bde1886a54a311._comment create mode 100644 doc/design/assistant/blog/day_139__catch_up.mdwn create mode 100644 doc/design/assistant/blog/day_13__kqueue_continued.mdwn create mode 100644 doc/design/assistant/blog/day_140__release_monday.mdwn create mode 100644 doc/design/assistant/blog/day_141__release_tuesday.mdwn create mode 100644 doc/design/assistant/blog/day_141__release_tuesday/comment_1_a5adea7a726df12f9121c744a036f08d._comment create mode 100644 doc/design/assistant/blog/day_142__filling_in.mdwn create mode 100644 doc/design/assistant/blog/day_143__what_next.mdwn create mode 100644 doc/design/assistant/blog/day_143__what_next/comment_1_40cf25a2ebdd43d8974a28e180e100e5._comment create mode 100644 doc/design/assistant/blog/day_143__what_next/comment_2_af9ccbbc5131e6333c029415141bdb51._comment create mode 100644 doc/design/assistant/blog/day_144__webapp_work.mdwn create mode 100644 doc/design/assistant/blog/day_145__more_webapp_work.mdwn create mode 100644 doc/design/assistant/blog/day_146__meanwhile.mdwn create mode 100644 doc/design/assistant/blog/day_147__direct_mode.mdwn create mode 100644 doc/design/assistant/blog/day_147__direct_mode/comment_1_0bd69532afce9dc04e3d88bfd0aed4b2._comment create mode 100644 doc/design/assistant/blog/day_147__direct_mode/comment_2_3b26f0d081c3bf1037bb872d529ce825._comment create mode 100644 doc/design/assistant/blog/day_148__direct_mode.mdwn create mode 100644 doc/design/assistant/blog/day_149__rainy_day.mdwn create mode 100644 doc/design/assistant/blog/day_14__kqueue_kqueue_kqueue.mdwn create mode 100644 doc/design/assistant/blog/day_14__thinking_about_syncing.mdwn create mode 100644 doc/design/assistant/blog/day_150__12:12.mdwn create mode 100644 doc/design/assistant/blog/day_151__direct_mode_toggle.mdwn create mode 100644 doc/design/assistant/blog/day_152__bugfixes.mdwn create mode 100644 doc/design/assistant/blog/day_152__bugfixes/comment_1_46863a875f9daa6f2c9248b66ff91929._comment create mode 100644 doc/design/assistant/blog/day_152__bugfixes/comment_2_a586e617bc024c8a9ff60f1b8345d74d._comment create mode 100644 doc/design/assistant/blog/day_153__hibernation.mdwn create mode 100644 doc/design/assistant/blog/day_154__direct_mode_merging.mdwn create mode 100644 doc/design/assistant/blog/day_155__bugfixes.mdwn create mode 100644 doc/design/assistant/blog/day_156_and_157__direct_mode_assistant.mdwn create mode 100644 doc/design/assistant/blog/day_158__fsevents.mdwn create mode 100644 doc/design/assistant/blog/day_158__fsevents/comment_1_b278372ac6399f64d5fa9da178278a6d._comment create mode 100644 doc/design/assistant/blog/day_158__fsevents/comment_2_2d5ce9b2807068c3517e185945662bd2._comment create mode 100644 doc/design/assistant/blog/day_159__fsevents_and_assistant.mdwn create mode 100644 doc/design/assistant/blog/day_159__fsevents_and_assistant/comment_1_b85f446c3fa8d703a2a8882825c6f33f._comment create mode 100644 doc/design/assistant/blog/day_159__fsevents_and_assistant/comment_2_a150b404e0c39e0bb2f7dd00cda63cdc._comment create mode 100644 doc/design/assistant/blog/day_159__fsevents_and_assistant/comment_3_37abc41bae23a1d7de0d19d952aec492._comment create mode 100644 doc/design/assistant/blog/day_15__its_aliiive.mdwn create mode 100644 doc/design/assistant/blog/day_160__finishing_up_direct_mode.mdwn create mode 100644 doc/design/assistant/blog/day_161__release_day.mdwn create mode 100644 doc/design/assistant/blog/day_161__release_day/comment_1_e82c67f3ce216618149537bba1e0b850._comment create mode 100644 doc/design/assistant/blog/day_161__release_day/comment_2_b1fe96fd818935c0497b78bb8ad32ffa._comment create mode 100644 doc/design/assistant/blog/day_161__release_day/comment_3_40bac0e1756aa77bb966c4654857141c._comment create mode 100644 doc/design/assistant/blog/day_161__release_day/comment_4_af65656b0d1179636937595868bb97b0._comment create mode 100644 doc/design/assistant/blog/day_161__release_day/comment_5_0c05caaaf9588e124585041bf5f45d75._comment create mode 100644 doc/design/assistant/blog/day_161__release_day/comment_6_5dfb5f428633d6062925f61af2b8829b._comment create mode 100644 doc/design/assistant/blog/day_161__release_day/comment_7_ac4effb381b08d94d4a2d2482e92c89a._comment create mode 100644 doc/design/assistant/blog/day_161__release_day/comment_8_32600e89e3098e52a1280895e03b3f86._comment create mode 100644 doc/design/assistant/blog/day_161__release_day/comment_9_07e5d0c3cad0ce2bd4943e53b61f1767._comment create mode 100644 doc/design/assistant/blog/day_162__UI.mdwn create mode 100644 doc/design/assistant/blog/day_163__free_features.mdwn create mode 100644 doc/design/assistant/blog/day_164__bugfixes.mdwn create mode 100644 doc/design/assistant/blog/day_165__release_day.mdwn create mode 100644 doc/design/assistant/blog/day_166__a_short_long_day.mdwn create mode 100644 doc/design/assistant/blog/day_167__safe_direct_mode_transfers.mdwn create mode 100644 doc/design/assistant/blog/day_167__safe_direct_mode_transfers/comment_1_f1aa64fe803d8c14b250a4e98b88142a._comment create mode 100644 doc/design/assistant/blog/day_167__safe_direct_mode_transfers/comment_2_5ce1db84c9ead713f1272c4975645b93._comment create mode 100644 doc/design/assistant/blog/day_168__back_to_theme.mdwn create mode 100644 doc/design/assistant/blog/day_168__back_to_theme/comment_1_f248780bfcbd0384d9d72c2633a4ea46._comment create mode 100644 doc/design/assistant/blog/day_168__back_to_theme/comment_2_5beba073373b8e75a32d1fcfdc1a0782._comment create mode 100644 doc/design/assistant/blog/day_169__direct_mode_is_safe.mdwn create mode 100644 doc/design/assistant/blog/day_169__direct_mode_is_safe/comment_1_65f87656c4e6bc7cdb614f53961341c9._comment create mode 100644 doc/design/assistant/blog/day_169__direct_mode_is_safe/comment_2_a116a402a126c62be54c06afd82439ab._comment create mode 100644 doc/design/assistant/blog/day_16__more_robust_syncing.mdwn create mode 100644 doc/design/assistant/blog/day_16__more_robust_syncing/comment_1_23e7a90429e4431f90787cd016ebe188._comment create mode 100644 doc/design/assistant/blog/day_16__more_robust_syncing/comment_2_8e7e7cd27791bb47625e60a284e9c802._comment create mode 100644 doc/design/assistant/blog/day_170__bugfixes_and_release.mdwn create mode 100644 doc/design/assistant/blog/day_171__logs.mdwn create mode 100644 doc/design/assistant/blog/day_172__short_day.mdwn create mode 100644 doc/design/assistant/blog/day_172__short_day/comment_1_b75e26b77a23a45da1c4c3bca1399246._comment create mode 100644 doc/design/assistant/blog/day_173__snow_day.mdwn create mode 100644 doc/design/assistant/blog/day_174__last_weekend_before_AU.mdwn create mode 100644 doc/design/assistant/blog/day_174__last_weekend_before_AU/comment_1_05a8fd47f54373331741cc869a53b0c3._comment create mode 100644 doc/design/assistant/blog/day_174__last_weekend_before_AU/comment_2_fc8e65eef954c4caa8321c2fe8b711b7._comment create mode 100644 doc/design/assistant/blog/day_174__last_weekend_before_AU/comment_3_399534f540d85cac067fbb7be9d373b4._comment create mode 100644 doc/design/assistant/blog/day_175__pacific_features.mdwn create mode 100644 doc/design/assistant/blog/day_175__pacific_features/comment_1_c3ee4386f872b7c76aaecfa638b368cb._comment create mode 100644 doc/design/assistant/blog/day_176__thread_management.mdwn create mode 100644 doc/design/assistant/blog/day_178__bus_hacking.mdwn create mode 100644 doc/design/assistant/blog/day_179__brief_updates.mdwn create mode 100644 doc/design/assistant/blog/day_179__brief_updates/comment_1_920a84457d40358507a3eb817a4568d9._comment create mode 100644 doc/design/assistant/blog/day_17__push_queue_prune.mdwn create mode 100644 doc/design/assistant/blog/day_180__back.mdwn create mode 100644 doc/design/assistant/blog/day_181__triage.mdwn create mode 100644 doc/design/assistant/blog/day_182__it_begins.mdwn create mode 100644 doc/design/assistant/blog/day_183__plan_b.mdwn create mode 100644 doc/design/assistant/blog/day_184__just_wanna_run_something.mdwn create mode 100644 doc/design/assistant/blog/day_184__just_wanna_run_something/comment_1_689adac7e26cb0b0a4e7ecc787cfd716._comment create mode 100644 doc/design/assistant/blog/day_185__android_liftoff.mdwn create mode 100644 doc/design/assistant/blog/day_185__android_liftoff/comment_1_b7d28010a72619a7e9a5ad4f2a0d6c07._comment create mode 100644 doc/design/assistant/blog/day_185__android_liftoff/comment_2_ddeb24e86fafb7dae93142cc02767aad._comment create mode 100644 doc/design/assistant/blog/day_186__Android_success.mdwn create mode 100644 doc/design/assistant/blog/day_186__Android_success/comment_1_1629da240ca7db5f8a32059f561fd435._comment create mode 100644 doc/design/assistant/blog/day_187__porting_utilities.mdwn create mode 100644 doc/design/assistant/blog/day_187__porting_utilities/comment_1_0e6a3f4fe8e09f247fa04156bc60f8c7._comment create mode 100644 doc/design/assistant/blog/day_188__crippled_filesystem_support.mdwn create mode 100644 doc/design/assistant/blog/day_188__crippled_filesystem_support/comment_1_32a296fce23ae4b1e18bd5a9964bf619._comment create mode 100644 doc/design/assistant/blog/day_189__more_crippling.mdwn create mode 100644 doc/design/assistant/blog/day_18__merging.mdwn create mode 100644 doc/design/assistant/blog/day_190-191__weekend.mdwn create mode 100644 doc/design/assistant/blog/day_190-191__weekend/comment_1_dbd692d12c14d08acd7d73a655b34e8b._comment create mode 100644 doc/design/assistant/blog/day_190-191__weekend/comment_2_c813830e53471a9732e010a748d574fc._comment create mode 100644 doc/design/assistant/blog/day_192_193__more_porting.mdwn create mode 100644 doc/design/assistant/blog/day_194__nice_moment.mdwn create mode 100644 doc/design/assistant/blog/day_195__real_android_app.mdwn create mode 100644 doc/design/assistant/blog/day_195__real_android_app/comment_10_0112007552b30cd9bfeac614a1e399c4._comment create mode 100644 doc/design/assistant/blog/day_195__real_android_app/comment_11_230d3c169c713f613b9d607d84ce5092._comment create mode 100644 doc/design/assistant/blog/day_195__real_android_app/comment_12_8d74ad2a61c02272758d157282ad56ec._comment create mode 100644 doc/design/assistant/blog/day_195__real_android_app/comment_13_4f6bc0680f2debd638933968a26975e0._comment create mode 100644 doc/design/assistant/blog/day_195__real_android_app/comment_14_71539c62608866464e8faa76bc522a55._comment create mode 100644 doc/design/assistant/blog/day_195__real_android_app/comment_15_e1b205289721ae79ac7fbed2f44018b2._comment create mode 100644 doc/design/assistant/blog/day_195__real_android_app/comment_1_4bc0aeae4fa1116944644c64feaf9697._comment create mode 100644 doc/design/assistant/blog/day_195__real_android_app/comment_2_17bb6e7565d4c757f6c1e3514c22f47d._comment create mode 100644 doc/design/assistant/blog/day_195__real_android_app/comment_3_cd8a6bec0f7c6843dd11d3266f25f864._comment create mode 100644 doc/design/assistant/blog/day_195__real_android_app/comment_4_2d2eee4bcbbd1d069a80bff5edc90c3c._comment create mode 100644 doc/design/assistant/blog/day_195__real_android_app/comment_6_3d96568c469a8c53a982f304eae5e7d4._comment create mode 100644 doc/design/assistant/blog/day_195__real_android_app/comment_6_e8667c47d07fc842cf0fe2ebbfbc1c58._comment create mode 100644 doc/design/assistant/blog/day_195__real_android_app/comment_7_cf8da7720ddc20b05955ee671ca4acd5._comment create mode 100644 doc/design/assistant/blog/day_195__real_android_app/comment_8_f4709bdbc739182819b648fd6aa00531._comment create mode 100644 doc/design/assistant/blog/day_195__real_android_app/comment_9_e66af12c7eca0d457b8406e9fb4b69be._comment create mode 100644 doc/design/assistant/blog/day_196__android_bugfixes.mdwn create mode 100644 doc/design/assistant/blog/day_197__template_haskell.mdwn create mode 100644 doc/design/assistant/blog/day_197__template_haskell/comment_1_82d9f9508929d84abf7b718c59436ae8._comment create mode 100644 doc/design/assistant/blog/day_198__bugfixes.mdwn create mode 100644 doc/design/assistant/blog/day_198__bugfixes/comment_1_5a15b5bad0f9ba2423d2aebe440ac0ea._comment create mode 100644 doc/design/assistant/blog/day_198__bugfixes/comment_2_36d94b838e5e65c85e7afaabe8a578f1._comment create mode 100644 doc/design/assistant/blog/day_198__bugfixes/comment_3_ae9b74341a3bc6e1e84d2c0ca4c5f612._comment create mode 100644 doc/design/assistant/blog/day_198__bugfixes/comment_4_5a4827227c03bcff3b1e4c44b531f816._comment create mode 100644 doc/design/assistant/blog/day_198__bugfixes/comment_5_9c5f4c85217e898be4c57c615e53c36f._comment create mode 100644 doc/design/assistant/blog/day_198__bugfixes/comment_6_bccf1abfb7f56d97673158f3ccfce511._comment create mode 100644 doc/design/assistant/blog/day_198__bugfixes/comment_7_6f1b51b002cc5d2b505d80e3e04bf6f3._comment create mode 100644 doc/design/assistant/blog/day_198__bugfixes/comment_8_8a3542437663028b17442818eba3f7c5._comment create mode 100644 doc/design/assistant/blog/day_199__wrapping_up_Android_for_now.mdwn create mode 100644 doc/design/assistant/blog/day_199__wrapping_up_Android_for_now/comment_1_ec57358afc7e78d2860aa4237793832d._comment create mode 100644 doc/design/assistant/blog/day_19__random_improvements.mdwn create mode 100644 doc/design/assistant/blog/day_1__inotify.mdwn create mode 100644 doc/design/assistant/blog/day_200__release_day.mdwn create mode 100644 doc/design/assistant/blog/day_200__release_day/comment_10_40cfe9bfd9e611fd734dbb5aad348aa3._comment create mode 100644 doc/design/assistant/blog/day_200__release_day/comment_11_b26890fdae575d42170988073fb2e45d._comment create mode 100644 doc/design/assistant/blog/day_200__release_day/comment_13_710a30c5d31bf549833ecfe9a0997c94._comment create mode 100644 doc/design/assistant/blog/day_200__release_day/comment_13_b6f62ab7e810ba6d3a43f0ead370c79a._comment create mode 100644 doc/design/assistant/blog/day_200__release_day/comment_1_a68e1ed7829b49086c567d97ddc09912._comment create mode 100644 doc/design/assistant/blog/day_200__release_day/comment_2_39d3ad0a029fe56e96f97d28d17fbbd2._comment create mode 100644 doc/design/assistant/blog/day_200__release_day/comment_3_5b752d6a8d74e61190f09384b6108206._comment create mode 100644 doc/design/assistant/blog/day_200__release_day/comment_4_881274ae0d6230bb4cafa4151ad72b49._comment create mode 100644 doc/design/assistant/blog/day_200__release_day/comment_5_e220059be77cf0ef396f37a4f9ccf9b5._comment create mode 100644 doc/design/assistant/blog/day_200__release_day/comment_6_ec2152151188dd252cdb61c68cfc12e4._comment create mode 100644 doc/design/assistant/blog/day_200__release_day/comment_7_42572411617c287368482bb9dcf94324._comment create mode 100644 doc/design/assistant/blog/day_200__release_day/comment_8_6b69aa81a9ba4e07e547ed1869946d51._comment create mode 100644 doc/design/assistant/blog/day_200__release_day/comment_9_b070a2e4151d9fbf43d7906efa78515f._comment create mode 100644 doc/design/assistant/blog/day_201__real_Android_wrapup.mdwn create mode 100644 doc/design/assistant/blog/day_201__real_Android_wrapup/comment_1_88b9950c51324f0bb89c5646b3170952._comment create mode 100644 doc/design/assistant/blog/day_201__real_Android_wrapup/fib.png create mode 100644 doc/design/assistant/blog/day_201__working_web_server.mdwn create mode 100644 doc/design/assistant/blog/day_203__procrastination.mdwn create mode 100644 doc/design/assistant/blog/day_204__deprocrastination.mdwn create mode 100644 doc/design/assistant/blog/day_205_206__rainy_day__snow_day.mdwn create mode 100644 doc/design/assistant/blog/day_207__XMPP.mdwn create mode 100644 doc/design/assistant/blog/day_208__bugfixes.mdwn create mode 100644 doc/design/assistant/blog/day_209__The_Bug.mdwn create mode 100644 doc/design/assistant/blog/day_20__data_transfer_design.mdwn create mode 100644 doc/design/assistant/blog/day_210__spring.mdwn create mode 100644 doc/design/assistant/blog/day_211__zooming_along.mdwn create mode 100644 doc/design/assistant/blog/day_212__accidental_all_nighter.mdwn create mode 100644 doc/design/assistant/blog/day_212__accidental_all_nighter/comment_1_6ee1f8056eedb6eb18013faf8f5ec212._comment create mode 100644 doc/design/assistant/blog/day_212__accidental_all_nighter/comment_2_07c83d75bb105bb77ada07359ed0ea7a._comment create mode 100644 doc/design/assistant/blog/day_212__accidental_all_nighter/comment_3_2c904d33f4f14807fbe718a01e98800a._comment create mode 100644 doc/design/assistant/blog/day_212__accidental_all_nighter/comment_4_59ec5c1cab75df87293800a7a03fe9c6._comment create mode 100644 doc/design/assistant/blog/day_212__accidental_all_nighter/comment_5_13893f106e835dcc52e03c7c6740c35b._comment create mode 100644 doc/design/assistant/blog/day_213__costs.mdwn create mode 100644 doc/design/assistant/blog/day_214__release_day.mdwn create mode 100644 doc/design/assistant/blog/day_215__dashboard_UI_refresh.mdwn create mode 100644 doc/design/assistant/blog/day_216__more_bugfixes.mdwn create mode 100644 doc/design/assistant/blog/day_216__more_bugfixes/comment_1_299462bcdd0e4f6cd7895b5f40ca00ad._comment create mode 100644 doc/design/assistant/blog/day_216__more_bugfixes/comment_2_1913d65dfe4ba08379d82a4a2ca91c40._comment create mode 100644 doc/design/assistant/blog/day_216__more_bugfixes/comment_3_92c774599a78540ad398afcd1d05f7ce._comment create mode 100644 doc/design/assistant/blog/day_217__nothing.mdwn create mode 100644 doc/design/assistant/blog/day_219__bug_triage.mdwn create mode 100644 doc/design/assistant/blog/day_219__bug_triage/comment_1_c6b977a969cacdce62987a439b7686f5._comment create mode 100644 doc/design/assistant/blog/day_21__transfer_tracking.mdwn create mode 100644 doc/design/assistant/blog/day_220__performance.mdwn create mode 100644 doc/design/assistant/blog/day_221__this_and_that.mdwn create mode 100644 doc/design/assistant/blog/day_222__back.mdwn create mode 100644 doc/design/assistant/blog/day_222__back/comment_1_f05b48231a1ee0cffba7d66e112e5551._comment create mode 100644 doc/design/assistant/blog/day_222__back/comment_2_4d5f003ccd81580017ebf0dc31bc9cda._comment create mode 100644 doc/design/assistant/blog/day_223__progress_revisited.mdwn create mode 100644 doc/design/assistant/blog/day_224__annex.largefiles.mdwn create mode 100644 doc/design/assistant/blog/day_224__annex.largefiles/comment_1_408e4021b18f7ff5548d2d19ab558922._comment create mode 100644 doc/design/assistant/blog/day_225__back_from_the_dead.mdwn create mode 100644 doc/design/assistant/blog/day_225__back_from_the_dead/comment_1_9ac37c3b5c4c72ec8a39dce00bcbe420._comment create mode 100644 doc/design/assistant/blog/day_225__back_from_the_dead/comment_2_26125dd9ef2bd10b597d14b2c6180952._comment create mode 100644 doc/design/assistant/blog/day_226__poll_results.mdwn create mode 100644 doc/design/assistant/blog/day_226__poll_results/comment_1_1ed980472214be6d0a8cf55f37797fda._comment create mode 100644 doc/design/assistant/blog/day_226__poll_results/comment_2_6823b0a9a8037f1a5214db4db98fb16e._comment create mode 100644 doc/design/assistant/blog/day_227__bigfixing_all_day_today.mdwn create mode 100644 doc/design/assistant/blog/day_228__more_work_on_repository_removals.mdwn create mode 100644 doc/design/assistant/blog/day_229__rainy_day_bugfixes.mdwn create mode 100644 doc/design/assistant/blog/day_22__horrible_option_parsing_hack.mdwn create mode 100644 doc/design/assistant/blog/day_230__Mom.mdwn create mode 100644 doc/design/assistant/blog/day_230__Mom/comment_1_696bba2246c8a9e6ce4aed3071bcc96c._comment create mode 100644 doc/design/assistant/blog/day_230__Mom/comment_2_2fa295ab6db0828cb725cfcfb6777822._comment create mode 100644 doc/design/assistant/blog/day_230__Mom/comment_3_fafd7abec629290418334ddb015bf62c._comment create mode 100644 doc/design/assistant/blog/day_230__Mom/comment_4_450cac0f2e82c94fd34b527ae05ef1b8._comment create mode 100644 doc/design/assistant/blog/day_231__insert_title.mdwn create mode 100644 doc/design/assistant/blog/day_232__headless_webapp.mdwn create mode 100644 doc/design/assistant/blog/day_232__headless_webapp/comment_1_0fdd77d143ecba6fdb9f75cb6fc37bfb._comment create mode 100644 doc/design/assistant/blog/day_232__headless_webapp/comment_2_0784a2a73c3e2945f3d3f2577b3b9c9c._comment create mode 100644 doc/design/assistant/blog/day_232__headless_webapp/comment_3_ccb9fa22422fb913b6a496ebe65c49fb._comment create mode 100644 doc/design/assistant/blog/day_232__headless_webapp/comment_4_ceba4468760a2525960327698431cee2._comment create mode 100644 doc/design/assistant/blog/day_233__taxes.mdwn create mode 100644 doc/design/assistant/blog/day_233__taxes/comment_1_9473ffdc42595af9c293fbcd5a1cdb54._comment create mode 100644 doc/design/assistant/blog/day_233__taxes/comment_2_5feed8d7053ba03812ccda8c61fd9775._comment create mode 100644 doc/design/assistant/blog/day_234__clean_shutdown.mdwn create mode 100644 doc/design/assistant/blog/day_235__birthday.mdwn create mode 100644 doc/design/assistant/blog/day_235__birthday/comment_1_db558b071067c1e63cde05cca0551094._comment create mode 100644 doc/design/assistant/blog/day_235__birthday/comment_2_d1a2c1124781118267599457ae9e0512._comment create mode 100644 doc/design/assistant/blog/day_235__birthday/comment_3_b853508d1d15234958b9f4a39277e45c._comment create mode 100644 doc/design/assistant/blog/day_235__birthday/comment_5_73aad3398a43bc4d28bca9bf635fa757._comment create mode 100644 doc/design/assistant/blog/day_236__evil_splicer.mdwn create mode 100644 doc/design/assistant/blog/day_237__gnome-keyring_craziness.mdwn create mode 100644 doc/design/assistant/blog/day_237__gnome-keyring_craziness/comment_1_0cb088b732881d1fa92493aa1fd93d43._comment create mode 100644 doc/design/assistant/blog/day_237__gnome-keyring_craziness/comment_2_b855fd710954beebaafe6d2bd03eb368._comment create mode 100644 doc/design/assistant/blog/day_238__back_to_Android.mdwn create mode 100644 doc/design/assistant/blog/day_239__bugfixes_and_frustration.mdwn create mode 100644 doc/design/assistant/blog/day_23__transfer_watching.mdwn create mode 100644 doc/design/assistant/blog/day_240__it_builds.mdwn create mode 100644 doc/design/assistant/blog/day_240__it_builds/comment_1_151840ae0020ea63b2f041488c905386._comment create mode 100644 doc/design/assistant/blog/day_241__cleanup.mdwn create mode 100644 doc/design/assistant/blog/day_241__cleanup/comment_1_0e283cdf66a25b3cc9423fe651084cb9._comment create mode 100644 doc/design/assistant/blog/day_242__more_porting.mdwn create mode 100644 doc/design/assistant/blog/day_243__in_the_field.mdwn create mode 100644 doc/design/assistant/blog/day_244__android_porting.mdwn create mode 100644 doc/design/assistant/blog/day_245__misc.mdwn create mode 100644 doc/design/assistant/blog/day_245__misc/comment_1_3a2976617bb0cdc206fb1397a2ef1177._comment create mode 100644 doc/design/assistant/blog/day_245__misc/comment_2_e0f9704e91fedca8ff26356f354cc1c3._comment create mode 100644 doc/design/assistant/blog/day_245__misc/comment_3_93003a0d0983efbdc046d7459be194b0._comment create mode 100644 doc/design/assistant/blog/day_246__bug_treadmill.mdwn create mode 100644 doc/design/assistant/blog/day_246__bug_treadmill/comment_1_f76f653364fe2b97e85e8356c93b0fce._comment create mode 100644 doc/design/assistant/blog/day_247__performance_tuning.mdwn create mode 100644 doc/design/assistant/blog/day_248__Internet_Archive.mdwn create mode 100644 doc/design/assistant/blog/day_249__quiet_day.mdwn create mode 100644 doc/design/assistant/blog/day_24__airport_digressions.mdwn create mode 100644 doc/design/assistant/blog/day_250__stymied.mdwn create mode 100644 doc/design/assistant/blog/day_250__stymied/comment_1_330a10d447ccc3db03fcbfe571dbb404._comment create mode 100644 doc/design/assistant/blog/day_251__xmpp_improvements.mdwn create mode 100644 doc/design/assistant/blog/day_252__release_day.mdwn create mode 100644 doc/design/assistant/blog/day_253__OMG.mdwn create mode 100644 doc/design/assistant/blog/day_253__OMG/comment_1_bbdc61092771163e65a90a4755a807d8._comment create mode 100644 doc/design/assistant/blog/day_254__Android_app_polishing.mdwn create mode 100644 doc/design/assistant/blog/day_254__Android_app_polishing/comment_1_37f4ff5227566ce4b3fa69fc32568841._comment create mode 100644 doc/design/assistant/blog/day_254__Android_app_polishing/comment_2_58bbb105bdbb72bba85c3622195f43b9._comment create mode 100644 doc/design/assistant/blog/day_255__Debian_release_day.mdwn create mode 100644 doc/design/assistant/blog/day_256__8bit.mdwn create mode 100644 doc/design/assistant/blog/day_256__8bit/comment_1_f9b50263e3997d4c5b9836a2e0a346d7._comment create mode 100644 doc/design/assistant/blog/day_257__rainy_day.mdwn create mode 100644 doc/design/assistant/blog/day_258__beginning_of_the_end.mdwn create mode 100644 doc/design/assistant/blog/day_259__Android_dominos_toppling.mdwn create mode 100644 doc/design/assistant/blog/day_259__Android_dominos_toppling/comment_1_0b4a6e4893b0157e4768b46468dbbb87._comment create mode 100644 doc/design/assistant/blog/day_259__Android_dominos_toppling/comment_2_1ebc5aff5d217e1392cb7c8bb6c5156b._comment create mode 100644 doc/design/assistant/blog/day_259__Android_dominos_toppling/comment_3_eed7792f6142f3fc74d3c384bb16559b._comment create mode 100644 doc/design/assistant/blog/day_25__transfer_queueing.mdwn create mode 100644 doc/design/assistant/blog/day_25__transfer_queueing/comment_1_59fd4f1ffe96c412f613dc86276e7dbd._comment create mode 100644 doc/design/assistant/blog/day_25__transfer_queueing/comment_2_93bf768a67117e873af5732ecf08dc78._comment create mode 100644 doc/design/assistant/blog/day_260__Windows_dev_environment.mdwn create mode 100644 doc/design/assistant/blog/day_261__Windows_first_stage_complete.mdwn create mode 100644 doc/design/assistant/blog/day_262__DOS_path_separators.mdwn create mode 100644 doc/design/assistant/blog/day_262__DOS_path_separators/comment_1_45ecae90b22e31202c21083980d6b567._comment create mode 100644 doc/design/assistant/blog/day_263_catching_up.mdwn create mode 100644 doc/design/assistant/blog/day_263_catching_up/comment_1_9023da0573dfc81644d68128adb331a7._comment create mode 100644 doc/design/assistant/blog/day_264__Windows_second_stage_complete.mdwn create mode 100644 doc/design/assistant/blog/day_264__Windows_second_stage_complete/comment_1_42a7502d6ece75520eb59a76fdb1e2f0._comment create mode 100644 doc/design/assistant/blog/day_264__Windows_second_stage_complete/comment_2_f2b11322ac87e2a36cddc035b2c3c1ea._comment create mode 100644 doc/design/assistant/blog/day_264__Windows_second_stage_complete/comment_3_ea6ee05acb946fc7e8d95e62647cfa2a._comment create mode 100644 doc/design/assistant/blog/day_264__Windows_second_stage_complete/comment_4_9ce106baf28b7f75f7f6febd7bfcea70._comment create mode 100644 doc/design/assistant/blog/day_265__correctness.mdwn create mode 100644 doc/design/assistant/blog/day_265__correctness/comment_1_e8959a6df87eb92310947e66c7471e97._comment create mode 100644 doc/design/assistant/blog/day_265__correctness/comment_2_0cb953fcc085eedb34e65c227309ede7._comment create mode 100644 doc/design/assistant/blog/day_265__correctness/comment_3_df57628a8969af2995732e7ea2a0fae3._comment create mode 100644 doc/design/assistant/blog/day_266__release_day.mdwn create mode 100644 doc/design/assistant/blog/day_266__release_day/comment_1_92c8d1d9216b46b07dfe69bbc77a923e._comment create mode 100644 doc/design/assistant/blog/day_267__windows_autobuilder.mdwn create mode 100644 doc/design/assistant/blog/day_267__windows_autobuilder/comment_1_978b584d86395f2f621b0e1f7c5e70d7._comment create mode 100644 doc/design/assistant/blog/day_267__windows_autobuilder/comment_2_8f978d2811c8fbf11e3d12f245bdb52b._comment create mode 100644 doc/design/assistant/blog/day_268__core_monad_change.mdwn create mode 100644 doc/design/assistant/blog/day_269__bugfixes.mdwn create mode 100644 doc/design/assistant/blog/day_26__dying_drives.mdwn create mode 100644 doc/design/assistant/blog/day_270__release_and_xmpp.mdwn create mode 100644 doc/design/assistant/blog/day_271__more_xmpp.mdwn create mode 100644 doc/design/assistant/blog/day_272__fuzz_tester.mdwn create mode 100644 doc/design/assistant/blog/day_273-274__fun.mdwn create mode 100644 doc/design/assistant/blog/day_275__working_hard_or.mdwn create mode 100644 doc/design/assistant/blog/day_276__fuzzing_continues.mdwn create mode 100644 doc/design/assistant/blog/day_276__fuzzing_continues/comment_1_f5dd0658511a1063c2eb025b0fe98426._comment create mode 100644 doc/design/assistant/blog/day_276__fuzzing_continues/comment_2_a56c4c26a9e7bb8cfe3f598dbeed0813._comment create mode 100644 doc/design/assistant/blog/day_277__private_static_protected_void.mdwn create mode 100644 doc/design/assistant/blog/day_278__winding_down.mdwn create mode 100644 doc/design/assistant/blog/day_279__final_release_prep.mdwn create mode 100644 doc/design/assistant/blog/day_27__robust_transfers.mdwn create mode 100644 doc/design/assistant/blog/day_28-35__threaded_runtime_tarpit.mdwn create mode 100644 doc/design/assistant/blog/day_280__yesod.mdwn create mode 100644 doc/design/assistant/blog/day_280__yesod/comment_1_a42213a8cef71f2b54db18606028136d._comment create mode 100644 doc/design/assistant/blog/day_281__back.mdwn create mode 100644 doc/design/assistant/blog/day_281__back/comment_1_128809c5a2a9f5cc345a10fdbf55be01._comment create mode 100644 doc/design/assistant/blog/day_281__back/comment_2_6d0bbdf6ebaff9da399804570f0e606d._comment create mode 100644 doc/design/assistant/blog/day_282-283__caught_up.mdwn create mode 100644 doc/design/assistant/blog/day_284__porting.mdwn create mode 100644 doc/design/assistant/blog/day_285__fixed_the_archive_directory_loop.mdwn create mode 100644 doc/design/assistant/blog/day_285__fixed_the_archive_directory_loop/comment_1_1065e756dc6d66aefd214eb8ac5ebe1d._comment create mode 100644 doc/design/assistant/blog/day_286__Windows_test_suite.mdwn create mode 100644 doc/design/assistant/blog/day_287__niceness.mdwn create mode 100644 doc/design/assistant/blog/day_288__success_stories.mdwn create mode 100644 doc/design/assistant/blog/day_288__success_stories/comment_10_9ddf57b8ae0241268bb33bec1b169e4c._comment create mode 100644 doc/design/assistant/blog/day_288__success_stories/comment_11_50b8a597bd8677608f2ef176443f23f3._comment create mode 100644 doc/design/assistant/blog/day_288__success_stories/comment_12_f2df427cf3608377e9a52d8bdeadb26f._comment create mode 100644 doc/design/assistant/blog/day_288__success_stories/comment_13_8762efed97f21eeba8f0a7be45bd924a._comment create mode 100644 doc/design/assistant/blog/day_288__success_stories/comment_14_55e1bb15c3a93d582d110f8173ceefc2._comment create mode 100644 doc/design/assistant/blog/day_288__success_stories/comment_15_5749aef8b585b293385b20b75c40f9d8._comment create mode 100644 doc/design/assistant/blog/day_288__success_stories/comment_16_911c6d2764906cad7d6324835441ed34._comment create mode 100644 doc/design/assistant/blog/day_288__success_stories/comment_17_eb6aa8af5aa70877255a11d132d51aba._comment create mode 100644 doc/design/assistant/blog/day_288__success_stories/comment_18_9a57de4cea407a73b2d023d85afdccc6._comment create mode 100644 doc/design/assistant/blog/day_288__success_stories/comment_19_1767c86067bee35941004282b96b8e95._comment create mode 100644 doc/design/assistant/blog/day_288__success_stories/comment_1_22b28ca3d4d3283ad8c21ae052fb9752._comment create mode 100644 doc/design/assistant/blog/day_288__success_stories/comment_20_1d47f3e1b9f0081649cedae4288bac83._comment create mode 100644 doc/design/assistant/blog/day_288__success_stories/comment_21_31d3f58cad83cb1ecc4821a15ca258d8._comment create mode 100644 doc/design/assistant/blog/day_288__success_stories/comment_22_b512bd2bf29dfaab6b36bf204518fdb6._comment create mode 100644 doc/design/assistant/blog/day_288__success_stories/comment_2_343333356de20e170edb8020faa7400d._comment create mode 100644 doc/design/assistant/blog/day_288__success_stories/comment_3_4e4034bec789543b562ac263df3e21dd._comment create mode 100644 doc/design/assistant/blog/day_288__success_stories/comment_4_0c52794c77a9b7afc5112f5edf9cb793._comment create mode 100644 doc/design/assistant/blog/day_288__success_stories/comment_5_7ca419aa3a187857b19268572d5df297._comment create mode 100644 doc/design/assistant/blog/day_288__success_stories/comment_6_3edd56b3b04f19faba8d75cca285a662._comment create mode 100644 doc/design/assistant/blog/day_288__success_stories/comment_7_146331ae2de25a6dc3595dffab9514de._comment create mode 100644 doc/design/assistant/blog/day_288__success_stories/comment_8_72be9307e75eb120451f3d6ab7c8165e._comment create mode 100644 doc/design/assistant/blog/day_288__success_stories/comment_9_c27eb0a4181e85a3eed41130402350bf._comment create mode 100644 doc/design/assistant/blog/day_289__back_in_the_swing.mdwn create mode 100644 doc/design/assistant/blog/day_290__https_release.mdwn create mode 100644 doc/design/assistant/blog/day_291__--all.mdwn create mode 100644 doc/design/assistant/blog/day_291__--all/comment_1_eaa9fef19a035bef9c439e87d47c834b._comment create mode 100644 doc/design/assistant/blog/day_291__--all/comment_2_90bbc26bf92048de7cbaf5fb719c9593._comment create mode 100644 doc/design/assistant/blog/day_291__--all/comment_3_75006e9909425dcbf86415a9f7c90372._comment create mode 100644 doc/design/assistant/blog/day_291__--all/comment_4_5440449bbc5a353f7430f72e19c35e92._comment create mode 100644 doc/design/assistant/blog/day_292__bugfixes.mdwn create mode 100644 doc/design/assistant/blog/day_292__bugfixes/comment_1_bbac3878d80f7540d229183c56664784._comment create mode 100644 doc/design/assistant/blog/day_292__bugfixes/comment_2_8c9e5291ceb257f3a938af0ad967c5d7._comment create mode 100644 doc/design/assistant/blog/day_292__bugfixes/comment_3_02f875e8edd30f47939249f16d92712b._comment create mode 100644 doc/design/assistant/blog/day_293__gpg_builds.mdwn create mode 100644 doc/design/assistant/blog/day_293__gpg_builds/comment_1_4f152de8ea5aca4ec381d439e2a821f7._comment create mode 100644 doc/design/assistant/blog/day_293__gpg_builds/comment_2_42f625638638bc875379f6c604d6f673._comment create mode 100644 doc/design/assistant/blog/day_294__release_day.mdwn create mode 100644 doc/design/assistant/blog/day_295__balls_in_the_air.mdwn create mode 100644 doc/design/assistant/blog/day_296__new_crowdfunding_campaign.mdwn create mode 100644 doc/design/assistant/blog/day_296__new_crowdfunding_campaign/comment_1_cccad1a5103c504d21d0f8e69bb39e1b._comment create mode 100644 doc/design/assistant/blog/day_296__new_crowdfunding_campaign/comment_2_4fef7bd9c8e15cd57df365fadb95717f._comment create mode 100644 doc/design/assistant/blog/day_296__new_crowdfunding_campaign/comment_3_0b9258a1f5079e53c60138f06d0c63b1._comment create mode 100644 doc/design/assistant/blog/day_296__new_crowdfunding_campaign/comment_4_46183b97ca904bc06e46569c30db2edc._comment create mode 100644 doc/design/assistant/blog/day_297__back_to_work.mdwn create mode 100644 doc/design/assistant/blog/day_297__back_to_work/comment_1_e300feb821bfe7b76b2cec4376d16ffa._comment create mode 100644 doc/design/assistant/blog/day_298__exceptional.mdwn create mode 100644 doc/design/assistant/blog/day_299__bugfixing.mdwn create mode 100644 doc/design/assistant/blog/day_2__races.mdwn create mode 100644 doc/design/assistant/blog/day_300__new_logo.mdwn create mode 100644 doc/design/assistant/blog/day_300__new_logo/comment_1_9fc64e33863b9fce00f6a03417a91e36._comment create mode 100644 doc/design/assistant/blog/day_300__new_logo/comment_2_e8aac0298f90004e81492d2c7f85eda0._comment create mode 100644 doc/design/assistant/blog/day_300__new_logo/comment_3_6308c767f6e4bf090102191c91520d04._comment create mode 100644 doc/design/assistant/blog/day_301__direct_unannex.mdwn create mode 100644 doc/design/assistant/blog/day_302_release_day.mdwn create mode 100644 doc/design/assistant/blog/day_302_release_day/comment_1_fe6e572ba706e95188463d9f3e004d03._comment create mode 100644 doc/design/assistant/blog/day_303__oops.mdwn create mode 100644 doc/design/assistant/blog/day_304__dropunused_safety.mdwn create mode 100644 doc/design/assistant/blog/day_304__dropunused_safety/comment_1_1bbcf6c74b6437c44ff8604401fb1432._comment create mode 100644 doc/design/assistant/blog/day_305__interesting_bugs.mdwn create mode 100644 doc/design/assistant/blog/day_306__offtopic.mdwn create mode 100644 doc/design/assistant/blog/day_307__buuuugs.mdwn create mode 100644 doc/design/assistant/blog/day_308__ssh-agent.mdwn create mode 100644 doc/design/assistant/blog/day_308__ssh-agent/comment_1_5f0fc810cf1e1cd9b3ddba3cd19bb19d._comment create mode 100644 doc/design/assistant/blog/day_309__filenames.mdwn create mode 100644 doc/design/assistant/blog/day_310__release_day.mdwn create mode 100644 doc/design/assistant/blog/day_310__release_day/comment_1_1e008583cebd8e373e83729529914db7._comment create mode 100644 doc/design/assistant/blog/day_311__Windows_porting.mdwn create mode 100644 doc/design/assistant/blog/day_312__DebConf_midpoint.mdwn create mode 100644 doc/design/assistant/blog/day_36__minimal_test_case.mdwn create mode 100644 doc/design/assistant/blog/day_37__back.mdwn create mode 100644 doc/design/assistant/blog/day_39__twice_is_enemy_action.mdwn create mode 100644 doc/design/assistant/blog/day_3__more_races.mdwn create mode 100644 doc/design/assistant/blog/day_3__more_races/comment_1_d6015338f602b574a3805de5481fc45e._comment create mode 100644 doc/design/assistant/blog/day_3__more_races/comment_2_4d6b23fc6442e0ee0303523cb69d0fba._comment create mode 100644 doc/design/assistant/blog/day_3__more_races/comment_3_03f5b2344c2a47dea60086f217d60f9b._comment create mode 100644 doc/design/assistant/blog/day_3__more_races/comment_4_860e90e989ec022100001c65e353a91e._comment create mode 100644 doc/design/assistant/blog/day_40__dbus.mdwn create mode 100644 doc/design/assistant/blog/day_40__dbus/comment_1_43ed2a79629868b018ec9f54a32bcacc._comment create mode 100644 doc/design/assistant/blog/day_40__dbus/comment_2_6799f2baf6a6ce14b1fa76a8402840c0._comment create mode 100644 doc/design/assistant/blog/day_40__dbus/comment_3_fa1d7444bdafcb990cacf2ace7ee6ef1._comment create mode 100644 doc/design/assistant/blog/day_40__dbus/comment_4_3399ddad951c1a950281bb6941fc3f6f._comment create mode 100644 doc/design/assistant/blog/day_40__dbus/comment_5_40b6b9d741d3081203f0cc94eb8dc3ea._comment create mode 100644 doc/design/assistant/blog/day_41__foo.mdwn create mode 100644 doc/design/assistant/blog/day_41__foo/comment_1_ace21fa257a4c2fd412b6ff2944a23e8._comment create mode 100644 doc/design/assistant/blog/day_42__the_answer.mdwn create mode 100644 doc/design/assistant/blog/day_43__simple_scanner.mdwn create mode 100644 doc/design/assistant/blog/day_44__webapp_basics.mdwn create mode 100644 doc/design/assistant/blog/day_44__webapp_basics/comment_1_d5fb67f373038e9f583cb2e1992bef67._comment create mode 100644 doc/design/assistant/blog/day_45__long_polling.mdwn create mode 100644 doc/design/assistant/blog/day_45__long_polling/comment_1_994bec0978324e268666073e8ff4f6ae._comment create mode 100644 doc/design/assistant/blog/day_45__long_polling/comment_2_dfa164c86290899139491acccddd8b2b._comment create mode 100644 doc/design/assistant/blog/day_45__long_polling/full.png create mode 100644 doc/design/assistant/blog/day_45__long_polling/phone.png create mode 100644 doc/design/assistant/blog/day_46__notification_pools.mdwn create mode 100644 doc/design/assistant/blog/day_47__alert_messages.mdwn create mode 100644 doc/design/assistant/blog/day_48__intro.mdwn create mode 100644 doc/design/assistant/blog/day_49__first_run_experience.mdwn create mode 100644 doc/design/assistant/blog/day_49__first_run_experience/comment_1_e146cf06c8dd6303dd6a991f152a73fe._comment create mode 100644 doc/design/assistant/blog/day_49__first_run_experience/comment_2_5d6adcf6782c02283bef6189582ee467._comment create mode 100644 doc/design/assistant/blog/day_49__first_run_experience/comment_3_7ac2e34c2a7bc9b57488ca0c91307d32._comment create mode 100644 doc/design/assistant/blog/day_49__first_run_experience/comment_4_549b07bb02c07a5b1b95445b01758db2._comment create mode 100644 doc/design/assistant/blog/day_4__speed.mdwn create mode 100644 doc/design/assistant/blog/day_4__speed/comment_1_bf3c9c33cc0dea5eaeb6f2af110b924b._comment create mode 100644 doc/design/assistant/blog/day_4__speed/comment_2_33aba4c9abaa3e6a05a2c87ab7df9d0e._comment create mode 100644 doc/design/assistant/blog/day_50__directory_name.mdwn create mode 100644 doc/design/assistant/blog/day_50__directory_name/comment_1_782cec95a8558a05b2b38a2d2302214d._comment create mode 100644 doc/design/assistant/blog/day_50__directory_name/comment_2_2b8ceb0a26f25e8ed2711bcbe7225a58._comment create mode 100644 doc/design/assistant/blog/day_51__desktop.mdwn create mode 100644 doc/design/assistant/blog/day_52__file_browser.mdwn create mode 100644 doc/design/assistant/blog/day_52__file_browser/comment_1_cd000c2d56b60cc1f17b221322a32aa7._comment create mode 100644 doc/design/assistant/blog/day_52__file_browser/comment_2_21d1da67cf9105a545583ba2302c10fb._comment create mode 100644 doc/design/assistant/blog/day_54__adding_removable_drives.mdwn create mode 100644 doc/design/assistant/blog/day_54__adding_removable_drives/comment_1_5de4f220a3534f55b1f2208d1d812d63._comment create mode 100644 doc/design/assistant/blog/day_54__adding_removable_drives/comment_2_8dae1ed0a70acf9628b88692dc32ac5f._comment create mode 100644 doc/design/assistant/blog/day_55__alerts.mdwn create mode 100644 doc/design/assistant/blog/day_55__alerts/comment_1_6319045500a8a5e049304fdec5ff4cf4._comment create mode 100644 doc/design/assistant/blog/day_56__transfer_control.mdwn create mode 100644 doc/design/assistant/blog/day_57__afk.mdwn create mode 100644 doc/design/assistant/blog/day_57__afk/comment_1_70e1c9f925f040c1700d3e26bab373d5._comment create mode 100644 doc/design/assistant/blog/day_57__afk/comment_2_c70d3faccfcebf47deb25e270498cb56._comment create mode 100644 doc/design/assistant/blog/day_57__afk/comment_3_89020ebc6d31485339bdea41a872df3c._comment create mode 100644 doc/design/assistant/blog/day_57__afk/comment_4_8b1f65f141ffd9813e7f5a3380f7f520._comment create mode 100644 doc/design/assistant/blog/day_58__more_transfer_control.mdwn create mode 100644 doc/design/assistant/blog/day_59__dinner.mdwn create mode 100644 doc/design/assistant/blog/day_59__dinner/comment_1_0c1e2d69496473e7e4a2956a2814f5dd._comment create mode 100644 doc/design/assistant/blog/day_5__committing.mdwn create mode 100644 doc/design/assistant/blog/day_60__taking_stock.mdwn create mode 100644 doc/design/assistant/blog/day_60__taking_stock/comment_1_6722f81ee084f1ea9e8fe47f34576397._comment create mode 100644 doc/design/assistant/blog/day_61__network_connection_detection.mdwn create mode 100644 doc/design/assistant/blog/day_61__network_connection_detection/comment_1_09b58f41a8d48f218619711ee19511ac._comment create mode 100644 doc/design/assistant/blog/day_62__smarter_syncing.mdwn create mode 100644 doc/design/assistant/blog/day_63__transfer_retries.mdwn create mode 100644 doc/design/assistant/blog/day_63__transfer_retries/comment_1_990d4eb6066c4e2b9ddb3cabef32e4b9._comment create mode 100644 doc/design/assistant/blog/day_64__syncing_robustly.mdwn create mode 100644 doc/design/assistant/blog/day_65__transfer_polish.mdwn create mode 100644 doc/design/assistant/blog/day_66__the_merge.mdwn create mode 100644 doc/design/assistant/blog/day_66__the_merge/comment_10_eeccf4e73cc321542a1fe4780805a81e._comment create mode 100644 doc/design/assistant/blog/day_66__the_merge/comment_1_a34e89316d1662826848f31061c4e46b._comment create mode 100644 doc/design/assistant/blog/day_66__the_merge/comment_2_09e244d23d05052fa2b11a7181888366._comment create mode 100644 doc/design/assistant/blog/day_66__the_merge/comment_3_3961a03e167903959b96b054835613f6._comment create mode 100644 doc/design/assistant/blog/day_66__the_merge/comment_4_12a57af9f580918818b4a9f68396d5c4._comment create mode 100644 doc/design/assistant/blog/day_66__the_merge/comment_5_8ce638960012367c888e018a5f05db19._comment create mode 100644 doc/design/assistant/blog/day_66__the_merge/comment_6_f461b856b940e6914bcd2b681cf9505f._comment create mode 100644 doc/design/assistant/blog/day_66__the_merge/comment_7_6e73aca1fc1747d0e742e054b88b5d78._comment create mode 100644 doc/design/assistant/blog/day_66__the_merge/comment_8_d85f1ce23ae16d5a8eb88d2c3999acb7._comment create mode 100644 doc/design/assistant/blog/day_66__the_merge/comment_9_c06dab4d78122c85beeaf300ffc3e376._comment create mode 100644 doc/design/assistant/blog/day_67__progress_bars.mdwn create mode 100644 doc/design/assistant/blog/day_68__transfers.mdwn create mode 100644 doc/design/assistant/blog/day_68__transfers/comment_1_5282960c0b553fbc0f411345b9745324._comment create mode 100644 doc/design/assistant/blog/day_69__build_fixes.mdwn create mode 100644 doc/design/assistant/blog/day_6__polish.mdwn create mode 100644 doc/design/assistant/blog/day_70__adding_ssh_remotes.mdwn create mode 100644 doc/design/assistant/blog/day_70__adding_ssh_remotes/comment_10_2fac85357ac8feccff82beabd3791439._comment create mode 100644 doc/design/assistant/blog/day_70__adding_ssh_remotes/comment_11_e9e496005fd1bf5a10c9e286b83e51fa._comment create mode 100644 doc/design/assistant/blog/day_70__adding_ssh_remotes/comment_1_913e6ae7c8f7db90b9767ec35fc84205._comment create mode 100644 doc/design/assistant/blog/day_70__adding_ssh_remotes/comment_2_634ca3c236e2062289e7df5f0d77a3c5._comment create mode 100644 doc/design/assistant/blog/day_70__adding_ssh_remotes/comment_3_e365bbcbb7f66ce2b35fcd5b969ab315._comment create mode 100644 doc/design/assistant/blog/day_70__adding_ssh_remotes/comment_4_b15499722a655489f9ea60ff9d4c47c6._comment create mode 100644 doc/design/assistant/blog/day_70__adding_ssh_remotes/comment_5_8ea48276f060e75d9f40617d2a1ccd08._comment create mode 100644 doc/design/assistant/blog/day_70__adding_ssh_remotes/comment_6_9b8bf7e9fa715977fbeb98087deefd1a._comment create mode 100644 doc/design/assistant/blog/day_70__adding_ssh_remotes/comment_7_42e09eacdc10c7cf579bfc6470b5117c._comment create mode 100644 doc/design/assistant/blog/day_70__adding_ssh_remotes/comment_8_6c02f31063b3d399d1b4f823bd6543ce._comment create mode 100644 doc/design/assistant/blog/day_70__adding_ssh_remotes/comment_9_dd0447cb3b39d3a8c1a7cc00f17d8bc2._comment create mode 100644 doc/design/assistant/blog/day_71__ssh_probing.mdwn create mode 100644 doc/design/assistant/blog/day_71__ssh_probing/comment_1_56a0c29f7454cfca5cc30b2849e6e942._comment create mode 100644 doc/design/assistant/blog/day_71__ssh_probing/comment_2_f3bd3e366c92c833c7e217da125481b8._comment create mode 100644 doc/design/assistant/blog/day_72__remote_ssh_server_configurator_finished.mdwn create mode 100644 doc/design/assistant/blog/day_73__rsync.net_configurator.mdwn create mode 100644 doc/design/assistant/blog/day_74__bits_and_peices.mdwn create mode 100644 doc/design/assistant/blog/day_75__zeromq_and_pairing.mdwn create mode 100644 doc/design/assistant/blog/day_76__pairing.mdwn create mode 100644 doc/design/assistant/blog/day_76__pairing/comment_1_09665f269343422cd18051fad1a8c19e._comment create mode 100644 doc/design/assistant/blog/day_76__pairing/comment_2_8e1b2233579bc26bfd758bbf6b3bdc07._comment create mode 100644 doc/design/assistant/blog/day_76__pairing/comment_3_a8b6a8432da20c468c633da8e7cbc2f3._comment create mode 100644 doc/design/assistant/blog/day_76__pairing/comment_4_36a428a2e1803f4391b821d1892f0cd7._comment create mode 100644 doc/design/assistant/blog/day_76__pairing/comment_5_11f332fe2050d8c1416e71f9e85ba280._comment create mode 100644 doc/design/assistant/blog/day_76__pairing/comment_6_973aeb656b78eca97474ea1a3f5b57b7._comment create mode 100644 doc/design/assistant/blog/day_76__pairing/comment_7_03d2b3343f34377a4d6171e06b7609f6._comment create mode 100644 doc/design/assistant/blog/day_77_alert_buttons.mdwn create mode 100644 doc/design/assistant/blog/day_78__pairing_continued.mdwn create mode 100644 doc/design/assistant/blog/day_79__pairing_finished.mdwn create mode 100644 doc/design/assistant/blog/day_7__bugfixes.mdwn create mode 100644 doc/design/assistant/blog/day_7__bugfixes/profile.png create mode 100644 doc/design/assistant/blog/day_7__bugfixes/profile2.png create mode 100644 doc/design/assistant/blog/day_80__default_backend.mdwn create mode 100644 doc/design/assistant/blog/day_81__enabling_pre-existing_special_remotes.mdwn create mode 100644 doc/design/assistant/blog/day_82__git-annex_branch_work.mdwn create mode 100644 doc/design/assistant/blog/day_83__3-way.mdwn create mode 100644 doc/design/assistant/blog/day_84__deferred_downloads.mdwn create mode 100644 doc/design/assistant/blog/day_85__more_foundation_work.mdwn create mode 100644 doc/design/assistant/blog/day_86__towards_the_beta.mdwn create mode 100644 doc/design/assistant/blog/day_87__more_progress_progress.mdwn create mode 100644 doc/design/assistant/blog/day_88__progressbars_still_progressing.mdwn create mode 100644 doc/design/assistant/blog/day_89__final_polish.mdwn create mode 100644 doc/design/assistant/blog/day_8__speed.mdwn create mode 100644 doc/design/assistant/blog/day_8__speed/comment_1_a3dba537b276d5737abc8cb93f1965f4._comment create mode 100644 doc/design/assistant/blog/day_90__beta.mdwn create mode 100644 doc/design/assistant/blog/day_90__beta/comment_1_5f2a3b18ad7558abe04f51534a29ff13._comment create mode 100644 doc/design/assistant/blog/day_90__beta/comment_2_961c4eba97f4eac75174244d6b2b00c0._comment create mode 100644 doc/design/assistant/blog/day_90__beta/comment_3_c76675a4633cbbe347ed42c222918d38._comment create mode 100644 doc/design/assistant/blog/day_90__beta/comment_4_f0b8f77cb691e747fe35bcf2f51b5baa._comment create mode 100644 doc/design/assistant/blog/day_90__beta/comment_5_99fbc9feac62e66a12b0d357cf86ccc1._comment create mode 100644 doc/design/assistant/blog/day_91__break.mdwn create mode 100644 doc/design/assistant/blog/day_92__S3.mdwn create mode 100644 doc/design/assistant/blog/day_92__S3/comment_1_eda656247d11cea7fbed2e33137a39e5._comment create mode 100644 doc/design/assistant/blog/day_92__S3/comment_2_8249d2d9521e44c674da3fda74be077a._comment create mode 100644 doc/design/assistant/blog/day_93__OSX_standalone_app.mdwn create mode 100644 doc/design/assistant/blog/day_93__easy_install.mdwn create mode 100644 doc/design/assistant/blog/day_93__easy_install/comment_1_d4f7de723c98577ef28d89ee6b87fd13._comment create mode 100644 doc/design/assistant/blog/day_93__easy_install/comment_2_6337b341c1cfb2132b59704394e57b36._comment create mode 100644 doc/design/assistant/blog/day_95__repository_groups.mdwn create mode 100644 doc/design/assistant/blog/day_96__revisiting_file_adds.mdwn create mode 100644 doc/design/assistant/blog/day_96__revisiting_file_adds/comment_1_da3ca47041168b6c82aeb2c18acc5017._comment create mode 100644 doc/design/assistant/blog/day_97__stuffing.mdwn create mode 100644 doc/design/assistant/blog/day_98__preferred_content.mdwn create mode 100644 doc/design/assistant/blog/day_98__preferred_content/comment_1_2136618e3515d0ac6369a41f1934ec2a._comment create mode 100644 doc/design/assistant/blog/day_98__preferred_content/comment_2_5f6db00e69628bf2f72b0e6f2981a49b._comment create mode 100644 doc/design/assistant/blog/day_99_shotgun.mdwn create mode 100644 doc/design/assistant/blog/day_99_shotgun/comment_1_12bb8f54bb13ea20ac4187a2301d77ca._comment create mode 100644 doc/design/assistant/blog/day_9__correctness.mdwn create mode 100644 doc/design/assistant/blog/day_9__correctness/comment_1_564a39cb976767e2c0a9c74fabe10be4._comment create mode 100644 doc/design/assistant/blog/day_9__correctness/comment_2_77924e9d50b40f05e792e427a25849a6._comment create mode 100644 doc/design/assistant/blog/day_9__correctness/comment_3_92bd86cd06d579e23800af2e5c66a291._comment create mode 100644 doc/design/assistant/blog/day_9__correctness/comment_4_0d12b51ccdfc2a94d3e59a5628521e0a._comment create mode 100644 doc/design/assistant/blog/day_9__correctness/comment_5_208f9dd3e1d92555b05c29159538a901._comment create mode 100644 doc/design/assistant/blog/day_9__correctness/comment_6_90cc6b60718896fb175919417600fdf9._comment create mode 100644 doc/design/assistant/chunks.mdwn create mode 100644 doc/design/assistant/cloud.mdwn create mode 100644 doc/design/assistant/cloud/comment_1_4997778abc171999499487b71b31c9ba._comment create mode 100644 doc/design/assistant/cloud/comment_2_08da8bc74a4845e354dca99184cffd70._comment create mode 100644 doc/design/assistant/cloud/comment_3_faafd1266301997b1822d215ec8e8d8c._comment create mode 100644 doc/design/assistant/cloud/comment_4_3eb557d5439831f6e0032944d12c02cf._comment create mode 100644 doc/design/assistant/comment_10_f2233fad55c20686cf299bf6788f1f23._comment create mode 100644 doc/design/assistant/comment_11_a38f0f21c2346e65b786d791b6829f9b._comment create mode 100644 doc/design/assistant/comment_12_5e991177d6577384f39a36ae02f5f574._comment create mode 100644 doc/design/assistant/comment_13_f8625c6f43b58847840df338a73b7972._comment create mode 100644 doc/design/assistant/comment_14_c37ef5931b0f5c1f808083e0d636a208._comment create mode 100644 doc/design/assistant/comment_15_68c98a27083567f20c2e6bc2a760991b._comment create mode 100644 doc/design/assistant/comment_16_8e6788c817c60371d2a2f158e1a65f87._comment create mode 100644 doc/design/assistant/comment_17_97bdfacac5ac492281c9454ee4c0228e._comment create mode 100644 doc/design/assistant/comment_18_53137b2df4913496c0afb2d895aa4ee2._comment create mode 100644 doc/design/assistant/comment_19_ff1b0ba57e22ed757ec3fc5400b5e43e._comment create mode 100644 doc/design/assistant/comment_1_a48fcfbf97f0a373ea375cd8f07f0fc8._comment create mode 100644 doc/design/assistant/comment_20_099da245e3276fa84f5e14312d186621._comment create mode 100644 doc/design/assistant/comment_2_6d3552414fdcc2ed3244567e6c67989d._comment create mode 100644 doc/design/assistant/comment_3_05223be50c889b2ed6bc4abf74116450._comment create mode 100644 doc/design/assistant/comment_4_fbbd93b55803ae21e6ba4b6568c2fafd._comment create mode 100644 doc/design/assistant/comment_5_f4e9af3fed6c27e8ff39badb9794064d._comment create mode 100644 doc/design/assistant/comment_6_c7ad07cade1f44f9a8b61f92225bb9c5._comment create mode 100644 doc/design/assistant/comment_7_609d38e993267195a80fecd84c93d1e2._comment create mode 100644 doc/design/assistant/comment_8_22b818e1a2a825efb78139271a14f944._comment create mode 100644 doc/design/assistant/comment_9_d052e2142da8b4838fb1edf791ea23ae._comment create mode 100644 doc/design/assistant/configurators.mdwn create mode 100644 doc/design/assistant/deltas.mdwn create mode 100644 doc/design/assistant/deltas/comment_1_bdb477af913c9782c0e8509e6b294b6e._comment create mode 100644 doc/design/assistant/deltas/comment_2_71889d15ba20ebb0fe13080c68162a5b._comment create mode 100644 doc/design/assistant/desymlink.mdwn create mode 100644 doc/design/assistant/desymlink/comment_1_f1bfe250b7f872359f7075998b6e42e3._comment create mode 100644 doc/design/assistant/desymlink/comment_2_5e876edfe9853645f761b5ed9b5021aa._comment create mode 100644 doc/design/assistant/desymlink/comment_3_538561d74371e53c2f8df7f5ebdf58a8._comment create mode 100644 doc/design/assistant/desymlink/comment_4_586ecaa800e6c162377c937da5e65440._comment create mode 100644 doc/design/assistant/desymlink/comment_5_8fc703de67814cf2aec2a908852298a4._comment create mode 100644 doc/design/assistant/desymlink/comment_6_1b473ad89494afb82250af4b6df5f5c9._comment create mode 100644 doc/design/assistant/disaster_recovery.mdwn create mode 100644 doc/design/assistant/encrypted_git_remotes.mdwn create mode 100644 doc/design/assistant/gpgkeys.mdwn create mode 100644 doc/design/assistant/inotify.mdwn create mode 100644 doc/design/assistant/inotify/comment_1_3d3ff74447452d65c10ccc3dbfc323cd._comment create mode 100644 doc/design/assistant/inotify/comment_2_a3c0fa6d97397c508b4b8aafdcee8f6f._comment create mode 100644 doc/design/assistant/inotify/comment_3_b346e870c1cd80e4b0a313c3a9fed6b3._comment create mode 100644 doc/design/assistant/inotify/comment_4_32be58b4c3b17a4ea539690d2fb45159._comment create mode 100644 doc/design/assistant/inotify/comment_5_0cdd3046d90ad2012025d846ece0731e._comment create mode 100644 doc/design/assistant/inotify/comment_6_e197d5d0d853572ec1f2e5985762e60d._comment create mode 100644 doc/design/assistant/leftovers.mdwn create mode 100644 doc/design/assistant/leftovers/comment_1_b20c88bb3c583a32023c1f6b6dc9486d._comment create mode 100644 doc/design/assistant/more_cloud_providers.mdwn create mode 100644 doc/design/assistant/pairing.mdwn create mode 100644 doc/design/assistant/partial_content.mdwn create mode 100644 doc/design/assistant/partial_content/comment_1_58c4faa321a5bb71adf9fdee079849f4._comment create mode 100644 doc/design/assistant/polls.mdwn create mode 100644 doc/design/assistant/polls/Android.mdwn create mode 100644 doc/design/assistant/polls/Android/comment_1_fa6c409833f28c67da105d25f4a440e0._comment create mode 100644 doc/design/assistant/polls/Android_default_directory.mdwn create mode 100644 doc/design/assistant/polls/Android_default_directory/comment_1_d39655091ac3ed51a9d4325d86b23ad7._comment create mode 100644 doc/design/assistant/polls/Android_default_directory/comment_2_2f1eaae95075db26488517720afd1c63._comment create mode 100644 doc/design/assistant/polls/Android_default_directory/comment_3_b484012f60789be73d7d5b338cff6203._comment create mode 100644 doc/design/assistant/polls/goals_for_April.mdwn create mode 100644 doc/design/assistant/polls/goals_for_April/comment_1_9f81fa96db5970a4be0828c74a6d2d55._comment create mode 100644 doc/design/assistant/polls/goals_for_April/comment_2_d8956d220ccacff3d2f6cbeb15718459._comment create mode 100644 doc/design/assistant/polls/goals_for_April/comment_3_aadad6dfd56d068d2e377606910c006f._comment create mode 100644 doc/design/assistant/polls/prioritizing_special_remotes.mdwn create mode 100644 doc/design/assistant/polls/prioritizing_special_remotes/comment_1_dd9280df27848a7ff132f5809dab0a79._comment create mode 100644 doc/design/assistant/polls/prioritizing_special_remotes/comment_2_370e0b9c43486ee96c825f9155eebde4._comment create mode 100644 doc/design/assistant/polls/prioritizing_special_remotes/comment_3_883a003b9c552b89f191135c582f99aa._comment create mode 100644 doc/design/assistant/polls/prioritizing_special_remotes/comment_4_746006c3fffc7f917c4526fd688051f7._comment create mode 100644 doc/design/assistant/polls/what_is_preventing_me_from_using_git-annex_assistant.mdwn create mode 100644 doc/design/assistant/polls/what_is_preventing_me_from_using_git-annex_assistant/comment_10_10a4839a05be39ced54ffbe880a588bb._comment create mode 100644 doc/design/assistant/polls/what_is_preventing_me_from_using_git-annex_assistant/comment_11_ac91d866f11c66dd8c86e2cd1a368c85._comment create mode 100644 doc/design/assistant/polls/what_is_preventing_me_from_using_git-annex_assistant/comment_12_e244c1bf334b1cc9ad0cc760bf8fe5de._comment create mode 100644 doc/design/assistant/polls/what_is_preventing_me_from_using_git-annex_assistant/comment_13_1a0faf4bdc78741937e8a2f5cb5bbec6._comment create mode 100644 doc/design/assistant/polls/what_is_preventing_me_from_using_git-annex_assistant/comment_14_8d8a11dbfae7a7bc574bdf37f87e0684._comment create mode 100644 doc/design/assistant/polls/what_is_preventing_me_from_using_git-annex_assistant/comment_15_c437adeaccf0b3d134e0f81c64e25b9f._comment create mode 100644 doc/design/assistant/polls/what_is_preventing_me_from_using_git-annex_assistant/comment_16_6e3fce3a32ab346dc3d0fd4b69967536._comment create mode 100644 doc/design/assistant/polls/what_is_preventing_me_from_using_git-annex_assistant/comment_17_1b7233d88593d0d99b26ea3e7af20d9c._comment create mode 100644 doc/design/assistant/polls/what_is_preventing_me_from_using_git-annex_assistant/comment_18_a23d5a0e2718b8e486f036fe8a413b36._comment create mode 100644 doc/design/assistant/polls/what_is_preventing_me_from_using_git-annex_assistant/comment_19_f4c84a9d701d52cf2f2e45f3d764a90c._comment create mode 100644 doc/design/assistant/polls/what_is_preventing_me_from_using_git-annex_assistant/comment_1_00a0de8190d946caaeeca3b44646146f._comment create mode 100644 doc/design/assistant/polls/what_is_preventing_me_from_using_git-annex_assistant/comment_20_199c9807499470771af6cbca6d034cfa._comment create mode 100644 doc/design/assistant/polls/what_is_preventing_me_from_using_git-annex_assistant/comment_2_35f6f121e54260cb960211a6e2e51e8e._comment create mode 100644 doc/design/assistant/polls/what_is_preventing_me_from_using_git-annex_assistant/comment_3_acbe4f63b5d552ac5ae5a12c6f42dc18._comment create mode 100644 doc/design/assistant/polls/what_is_preventing_me_from_using_git-annex_assistant/comment_4_0d988280865caae498a3b693b6342e37._comment create mode 100644 doc/design/assistant/polls/what_is_preventing_me_from_using_git-annex_assistant/comment_5_ac8fe3768c30dd7999c183500f8567bb._comment create mode 100644 doc/design/assistant/polls/what_is_preventing_me_from_using_git-annex_assistant/comment_6_36832de705a2bebf8dc6e65dcd661731._comment create mode 100644 doc/design/assistant/polls/what_is_preventing_me_from_using_git-annex_assistant/comment_7_3618067e473577a112e36970ca71e0ab._comment create mode 100644 doc/design/assistant/polls/what_is_preventing_me_from_using_git-annex_assistant/comment_8_07a13b6f000ddc0ac4472b863d8b50bd._comment create mode 100644 doc/design/assistant/polls/what_is_preventing_me_from_using_git-annex_assistant/comment_9_e15eb407d988fda363296c8b566cc8fb._comment create mode 100644 doc/design/assistant/progressbars.mdwn create mode 100644 doc/design/assistant/progressbars/comment_1_3ea263b1f334e8e38e14f00a96202988._comment create mode 100644 doc/design/assistant/rate_limiting.mdwn create mode 100644 doc/design/assistant/screenshot/firstrun.png create mode 100644 doc/design/assistant/screenshot/intro.png create mode 100644 doc/design/assistant/sshpassword.mdwn create mode 100644 doc/design/assistant/syncing.mdwn create mode 100644 doc/design/assistant/syncing/comment_1_c70156174ff19b503978d623bd2df36f._comment create mode 100644 doc/design/assistant/syncing/comment_2_eb992b5b2c7a5ce23443e2a6007e5ff9._comment create mode 100644 doc/design/assistant/syncing/comment_3_e1b5e8a24556de16d1cacd27ee0c1bd1._comment create mode 100644 doc/design/assistant/thanks/comment_1_8b08b5c30e5aea3fc4599f856fd25df5._comment create mode 100644 doc/design/assistant/todo.mdwn create mode 100644 doc/design/assistant/transfer_control.mdwn create mode 100644 doc/design/assistant/transfer_control/comment_1_d5adaef4712913dc0263d4ebafb79320._comment create mode 100644 doc/design/assistant/webapp.mdwn create mode 100644 doc/design/assistant/webapp/comment_1_bab6f6fa720273c0f9700a3765150189._comment create mode 100644 doc/design/assistant/webapp/comment_2_3cf0cf460c7869d0cc22940fcc84aec4._comment create mode 100644 doc/design/assistant/webapp/comment_3_428e153135f7a64215730719207d82c4._comment create mode 100644 doc/design/assistant/webapp/comment_4_f4068a7abbb77ba6a3297cbcf1e503e9._comment create mode 100644 doc/design/assistant/windows.mdwn create mode 100644 doc/design/assistant/windows/comment_1_f4b829318b182e1cec29f13babb6498e._comment create mode 100644 doc/design/assistant/xmpp.mdwn create mode 100644 doc/design/assistant/xmpp/comment_1_f20650f93d7f0ca39b9ba3ce0380193f._comment create mode 100644 doc/design/assistant/xmpp/comment_2_8c22839a8f5912b4a817415c4a359697._comment create mode 100644 doc/design/assistant/xmpp/comment_4_773102522f21844cffc841e6cde9229e._comment create mode 100644 doc/design/assistant/xmpp_security.mdwn create mode 100644 doc/design/assistant/xmpp_security/comment_1_c714e86553c02600249795efb224be8a._comment create mode 100644 doc/design/encryption.mdwn create mode 100644 doc/design/encryption/comment_1_4715ffafb3c4a9915bc33f2b26aaa9c1._comment create mode 100644 doc/design/encryption/comment_2_a610b3d056a059899178859a3a821ea5._comment create mode 100644 doc/design/encryption/comment_3_cca186a9536cd3f6e86994631b14231c._comment create mode 100644 doc/design/encryption/comment_4_8f3ba3e504b058791fc6e6f9c38154cf._comment create mode 100644 doc/design/encryption/comment_5_520e60aa53217b5ba428d4c05d897dee._comment create mode 100644 doc/design/encryption/comment_6_d677fead0fe0c543f48f07d85f83f592._comment create mode 100644 doc/design/encryption/comment_7_c1c38a09b1276e29adc3ba564dc0fe4e._comment create mode 100644 doc/direct_mode.mdwn create mode 100644 doc/direct_mode/comment_1_93fc31e8dc0ad16248a2593a1482d375._comment create mode 100644 doc/direct_mode/comment_2_7f7086b34ed136851963f145868a1d23._comment create mode 100644 doc/direct_mode/comment_3_8020d74bddf0e38b0a297e5dae7c217b._comment create mode 100644 doc/direct_mode/comment_4_97c26bd82f623a3b2d56bab4afff0126._comment create mode 100644 doc/direct_mode/comment_5_42363bf0367f935b3eee8ad3d2eaf5cf._comment create mode 100644 doc/direct_mode/comment_6_5f03b1686c1fb3f7606a5bc724ac3812._comment create mode 100644 doc/direct_mode/comment_7_5355ac418bfb26e990762b80f4c36b77._comment create mode 100644 doc/direct_mode/comment_8_6cd15e2c5fd0bef48f60c6993322c2fc._comment create mode 100644 doc/distributed_version_control.mdwn create mode 100644 doc/download.mdwn create mode 100644 doc/download/comment_1_ec2578241a966cfcdd43f2a26a5c8709._comment create mode 100644 doc/download/comment_2_ee0d158ac59903737dbc4ef632f11fe3._comment create mode 100644 doc/encryption.mdwn create mode 100644 doc/encryption/comment_1_1afca8d7182075d46db41f6ad3dd5911._comment create mode 100644 doc/favicon.ico create mode 100644 doc/favicon.png create mode 100644 doc/feeds.mdwn create mode 100644 doc/footer/column_a.mdwn create mode 100644 doc/footer/column_b.mdwn create mode 100644 doc/forum.mdwn create mode 100644 doc/forum/--print0_option_as_in___34__find__34__.mdwn create mode 100644 doc/forum/A_really_stupid_question.mdwn create mode 100644 doc/forum/A_really_stupid_question/comment_1_40e02556de0b00b94f245a0196b5a89f._comment create mode 100644 doc/forum/Accessing_files_directly_on__a_USB_device.mdwn create mode 100644 doc/forum/Accessing_files_in_bare_repository.mdwn create mode 100644 doc/forum/Accessing_files_in_bare_repository/comment_10_7eb66e3806f9524e043fae2da9d57d64._comment create mode 100644 doc/forum/Accessing_files_in_bare_repository/comment_11_f0165d66865ad14f7eb5d50e900c1df4._comment create mode 100644 doc/forum/Accessing_files_in_bare_repository/comment_12_0e7ea5161b6da6e9bb9425bdb953de33._comment create mode 100644 doc/forum/Accessing_files_in_bare_repository/comment_13_f804b9bf71f7d04bd23ce32d813dc340._comment create mode 100644 doc/forum/Accessing_files_in_bare_repository/comment_1_6de649d38febd2240eb5b703da77c2d6._comment create mode 100644 doc/forum/Accessing_files_in_bare_repository/comment_2_7e8dd09915ddc3267377e900891cb02c._comment create mode 100644 doc/forum/Accessing_files_in_bare_repository/comment_3_80eae4a73f38d1a7e35f97c33b6401f8._comment create mode 100644 doc/forum/Accessing_files_in_bare_repository/comment_4_5ec13e98d3ecb69426e974d34f712f9b._comment create mode 100644 doc/forum/Accessing_files_in_bare_repository/comment_5_dccbf5793998c6381e23eb8ff6497ebf._comment create mode 100644 doc/forum/Accessing_files_in_bare_repository/comment_6_42d923916232c81f3b8bdbefa34a89d3._comment create mode 100644 doc/forum/Accessing_files_in_bare_repository/comment_7_43a0a7d222faee582aeb3150a59cef87._comment create mode 100644 doc/forum/Accessing_files_in_bare_repository/comment_8_ec1024235c1c74c113483a833df84654._comment create mode 100644 doc/forum/Accessing_files_in_bare_repository/comment_9_c156b8c1ae0f2905566bbdb13b84e577._comment create mode 100644 doc/forum/Add_a___34__local__34___remote.txt create mode 100644 doc/forum/Add_a___34__local__34___remote/comment_1_c68ad724b465c4be5243be687168c0b3._comment create mode 100644 doc/forum/Assistant_not_syncing_to_Rsync.mdwn create mode 100644 doc/forum/Assistant_not_syncing_to_Rsync/comment_1_2178a7fc0d66643e84597b0938ef65f2._comment create mode 100644 doc/forum/Assistant_not_syncing_to_Rsync/comment_2_650651398443e128c2adc6a2a2d320d0._comment create mode 100644 doc/forum/Assistant_not_syncing_to_Rsync/comment_3_e6d0c9620b148acc72342862a8b4cfef._comment create mode 100644 doc/forum/Assistant_not_syncing_to_Rsync/comment_4_b91f9febdb8b69d8b487ba4ea08c119a._comment create mode 100644 doc/forum/Assistant_not_syncing_to_Rsync/comment_5_c5ad7c1546a17d8459c995c9c8c26414._comment create mode 100644 doc/forum/Assistant_not_syncing_to_Rsync/comment_6_4c12587f972eced91c5128d4885800b5._comment create mode 100644 doc/forum/Assistant_not_syncing_to_Rsync/comment_7_6ecaaee9316bcf0c65688676d60fc055._comment create mode 100644 doc/forum/Assistant_not_syncing_to_Rsync/comment_8_daa9a9a6188afa0394833e1b682f7cd4._comment create mode 100644 doc/forum/Auto_archiving.mdwn create mode 100644 doc/forum/Automatic___96__git_annex_get__96___after_invalidation_of_local_files_due_to_external_modification__63__.mdwn create mode 100644 doc/forum/Automatic___96__git_annex_get__96___after_invalidation_of_local_files_due_to_external_modification__63__/comment_1_dab1099ee56541c194de319c593f1268._comment create mode 100644 doc/forum/Automatic___96__git_annex_get__96___after_invalidation_of_local_files_due_to_external_modification__63__/comment_2_b5faccf132fb47e3cda778a6600fd9ef._comment create mode 100644 doc/forum/Automatic_commit_messages_for_git_annex_sync.mdwn create mode 100644 doc/forum/Automatic_commit_messages_for_git_annex_sync/comment_1_ea2ec57bc695da4df8a30a35d433959d._comment create mode 100644 doc/forum/Automatic_commit_messages_for_git_annex_sync/comment_2_af71f53dbbca35d5a5c66ff131887ada._comment create mode 100644 doc/forum/Automatically_syncronise_centralised_repository.mdwn create mode 100644 doc/forum/Automatically_syncronise_centralised_repository/comment_1_6a2047daa9faf4309d2ed27d5cc48b76._comment create mode 100644 doc/forum/Automatically_syncronise_centralised_repository/comment_2_3be7b45bc2284019f17a81375637a576._comment create mode 100644 doc/forum/Behaviour_of_fsck.mdwn create mode 100644 doc/forum/Behaviour_of_fsck/comment_1_0e40f158b3f4ccdcaab1408d858b68b8._comment create mode 100644 doc/forum/Behaviour_of_fsck/comment_2_ead36a23c3e6efa1c41e4555f93e014e._comment create mode 100644 doc/forum/Behaviour_of_fsck/comment_3_97848f9a3db89c0427cfb671ba13300e._comment create mode 100644 doc/forum/Behaviour_of_fsck/comment_4_e4911dc6793f98fb81151daacbe49968._comment create mode 100644 doc/forum/Best_way_to_manage_files_on_removable_media__63__.mdwn create mode 100644 doc/forum/Building_a_Debian_package_of_git-annex.mdwn create mode 100644 doc/forum/Building_a_Debian_package_of_git-annex/comment_1_0848513c46f3efa21bc34784554ae88a._comment create mode 100644 doc/forum/Building_git-annex-3.20121112-19309.mdwn create mode 100644 doc/forum/Building_git-annex-3.20121112-19309/comment_1_b115e28c77fe748ee6643c41f766beb4._comment create mode 100644 doc/forum/Building_git-annex-3.20121112-19309/comment_2_8c6ae1fd74f14da12ccfa77dbd27fc65._comment create mode 100644 doc/forum/Building_git-annex-3.20121112-19309/comment_3_2f30b301c14f3a7fa0f52715d6140353._comment create mode 100644 doc/forum/Building_git-annex-3.20121112-19309/comment_4_1e3c3903a71a2ff7109372aa4dd5742a._comment create mode 100644 doc/forum/Cabal:_Could_not_resolve_dependencies___40__yesod__41__.mdwn create mode 100644 doc/forum/Cabal:_Could_not_resolve_dependencies___40__yesod__41__/comment_1_2eb4f410b54a25fcc895893a3c631c43._comment create mode 100644 doc/forum/Cabal:_Could_not_resolve_dependencies___40__yesod__41__/comment_2_44cd6f6dd674df105d6f0b3f320f3236._comment create mode 100644 doc/forum/Cabal:_Could_not_resolve_dependencies___40__yesod__41__/comment_3_992af6855901df79a2018a07941cb8b6._comment create mode 100644 doc/forum/Calculating_Annex_Cost_by_Ping_Times.mdwn create mode 100644 doc/forum/Calculating_Annex_Cost_by_Ping_Times/comment_1_9b4a6bc8d52ecbbdd537e8cf76757a80._comment create mode 100644 doc/forum/Calculating_Annex_Cost_by_Ping_Times/comment_2_7e04f85c6ba74c18c8dde148aef9bf80._comment create mode 100644 doc/forum/Can_I_store_normal_files_in_the_git-annex_git_repository__63__.mdwn create mode 100644 doc/forum/Can_I_store_normal_files_in_the_git-annex_git_repository__63__/comment_1_c8f9923d8dc76b8bed25dce5ae09b520._comment create mode 100644 doc/forum/Can__39__t_get_git-annex_merge_to_work_from_git_hook.mdwn create mode 100644 doc/forum/Can__39__t_get_git-annex_merge_to_work_from_git_hook/comment_1_8b71cb6772b219c27c17392d5099907a._comment create mode 100644 doc/forum/Can__39__t_get_pairing_to_work.mdwn create mode 100644 doc/forum/Can__39__t_get_pairing_to_work/comment_1_b981977b4fb942fd109c37fcf40f35d7._comment create mode 100644 doc/forum/Can__39__t_get_pairing_to_work/comment_2_341e2ff6c88ace1b1422e16781edf580._comment create mode 100644 doc/forum/Can__39__t_get_pairing_to_work/comment_3_0c8cce48f179f2564ff0844bb7ef6bd1._comment create mode 100644 doc/forum/Can__39__t_get_pairing_to_work/comment_4_169d77b30cea05125068ee1eeb2ef328._comment create mode 100644 doc/forum/Can__39__t_get_pairing_to_work/comment_5_70e6c4f4f01277be1767b38ca8374793._comment create mode 100644 doc/forum/Can__39__t_get_pairing_to_work/comment_6_2cd014a76fac6e08269dfd8146957418._comment create mode 100644 doc/forum/Can__39__t_get_pairing_to_work/comment_7_b9b715084d5a5562998b1724699d49e5._comment create mode 100644 doc/forum/Can__39__t_init_git_annex.mdwn create mode 100644 doc/forum/Can__39__t_init_git_annex/comment_10_c4d2ab1ecf69718a2211c3ea7b27092b._comment create mode 100644 doc/forum/Can__39__t_init_git_annex/comment_12_fca9ed3707e097bee2cd642424681005._comment create mode 100644 doc/forum/Can__39__t_init_git_annex/comment_1_a294b5e7e52aa9f66a708866be16f137._comment create mode 100644 doc/forum/Can__39__t_init_git_annex/comment_2_fcf678d5188821d63b4c9ea5b59474a8._comment create mode 100644 doc/forum/Can__39__t_init_git_annex/comment_3_c83f7dea7d5304e226e52eb3c43ef14a._comment create mode 100644 doc/forum/Can__39__t_init_git_annex/comment_4_06a01dd51ffbfd006c0afb8eab40b530._comment create mode 100644 doc/forum/Can__39__t_init_git_annex/comment_5_53c33484bded57abc60f0449331c7b05._comment create mode 100644 doc/forum/Can__39__t_init_git_annex/comment_6_9e0ff44f6e62581bfc83f9f1da3e0100._comment create mode 100644 doc/forum/Can__39__t_init_git_annex/comment_7_7f96b5ef05e2faf4a3dbe8bfc39b810e._comment create mode 100644 doc/forum/Can__39__t_init_git_annex/comment_8_65ab8463716f4ddd7721a5bcfcd18fa0._comment create mode 100644 doc/forum/Can__39__t_init_git_annex/comment_9_31a45f6a72266190b3ed7a7b02e03d5b._comment create mode 100644 doc/forum/Can__39__t_install:_Mac_OS_10.8.2.mdwn create mode 100644 doc/forum/Can__39__t_install:_Mac_OS_10.8.2/comment_1_c44023d81e9e4f7c9341af0e4271a1e4._comment create mode 100644 doc/forum/Can__39__t_install:_Mac_OS_10.8.2/comment_2_dfbcd39eedff28dc9ed866a8f1411ef3._comment create mode 100644 doc/forum/Can__39__t_install:_Mac_OS_10.8.2/comment_3_b37b2a9906ffb956cca91adb4bb4e521._comment create mode 100644 doc/forum/Can__39__t_install:_Mac_OS_10.8.2/comment_4_afddf16f8faedc78d458835480f10dc3._comment create mode 100644 doc/forum/Can_we_have_remotes_that_aren__39__t_tracked__63___.mdwn create mode 100644 doc/forum/Can_we_have_remotes_that_aren__39__t_tracked__63___/comment_1_35e5a963b9e58ed7773dfcb884f8ecbd._comment create mode 100644 doc/forum/Cannot_find_git-annex_in_server.mdwn create mode 100644 doc/forum/Cannot_find_git-annex_in_server/comment_1_bf7e98e6130698ad0dc92e3a6a63ade3._comment create mode 100644 doc/forum/Cannot_find_git-annex_in_server/comment_2_168dda4aed09f90a510bc453e8a7cda7._comment create mode 100644 doc/forum/Cannot_launch_webapp_on_ubuntu_12.04_using_ppa.mdwn create mode 100644 doc/forum/Cannot_launch_webapp_on_ubuntu_12.04_using_ppa/comment_1_9345551f5772c3a6f1490b00e1edbf69._comment create mode 100644 doc/forum/Cannot_launch_webapp_on_ubuntu_12.04_using_ppa/comment_2_0b688a442b6a911a0353e73097a24cb6._comment create mode 100644 doc/forum/Cannot_launch_webapp_on_ubuntu_12.04_using_ppa/comment_3_7e246caa00005560bb489c927c663046._comment create mode 100644 doc/forum/Cannot_launch_webapp_on_ubuntu_12.04_using_ppa/comment_4_1d8025aabe8bc72711a77f691f67da5f._comment create mode 100644 doc/forum/Cannot_launch_webapp_on_ubuntu_12.04_using_ppa/comment_5_7c2f95da65190016192424e7c622122f._comment create mode 100644 doc/forum/Cannot_launch_webapp_on_ubuntu_12.04_using_ppa/comment_6_9b8465cefe609e7a696e7573b8892e38._comment create mode 100644 doc/forum/Cannot_launch_webapp_on_ubuntu_12.04_using_ppa/comment_7_af6472762a598a454ba52ac0caa059aa._comment create mode 100644 doc/forum/Centralized_repository_with_webapp.mdwn create mode 100644 doc/forum/Centralized_repository_with_webapp/comment_1_dcb9b07fd154f4d4fdef4809cc37ce77._comment create mode 100644 doc/forum/Centralized_repository_with_webapp/comment_2_08c84f2703f89dc12982eba9dd2a06d1._comment create mode 100644 doc/forum/Check_if_remote_is_using_GPG__63__.mdwn create mode 100644 doc/forum/Check_if_remote_is_using_GPG__63__/comment_1_db8ce8ef50fc33a28860ee475988450f._comment create mode 100644 doc/forum/Check_if_remote_is_using_GPG__63__/comment_2_11c7033904c9c7a1df766e915632c386._comment create mode 100644 doc/forum/Check_if_remote_is_using_GPG__63__/comment_3_a7ab70ad87a334c36761ddb3d830d99b._comment create mode 100644 doc/forum/Check_when_your_last_fsck_was__63__.mdwn create mode 100644 doc/forum/Check_when_your_last_fsck_was__63__/comment_1_ee98a1fcd796fe4fd7af6f77d0c1837d._comment create mode 100644 doc/forum/Cleaning_up_after_aborted_sync_in_direct_mode.mdwn create mode 100644 doc/forum/Cleaning_up_after_aborted_sync_in_direct_mode/comment_1_3440b2e1662d3b113c18283afcbf4520._comment create mode 100644 doc/forum/Cleaning_up_after_aborted_sync_in_direct_mode/comment_2_9a61ba8ac4a375f1d69cd09b5a6f8091._comment create mode 100644 doc/forum/Cleaning_up_after_aborted_sync_in_direct_mode/comment_3_6b9d8c48547f3d0a911310622ba91df7._comment create mode 100644 doc/forum/Coming_from_git_world.mdwn create mode 100644 doc/forum/Coming_from_git_world/comment_10_098bef38c2688607e869425a557cc482._comment create mode 100644 doc/forum/Coming_from_git_world/comment_11_98d75a1415e0c3689ab4231855e61233._comment create mode 100644 doc/forum/Coming_from_git_world/comment_12_5e7079e9bf3e4d97191333c66ac00e52._comment create mode 100644 doc/forum/Coming_from_git_world/comment_1_357443dc601ae38784c01cf18552f4d5._comment create mode 100644 doc/forum/Coming_from_git_world/comment_2_ed1847dd3f47a9d013b8dd0455fb80ff._comment create mode 100644 doc/forum/Coming_from_git_world/comment_3_09c6bb83a73d34dff2b8bc185a14a1db._comment create mode 100644 doc/forum/Coming_from_git_world/comment_4_6c731bb9a8d21dd9ab8c09612b23f908._comment create mode 100644 doc/forum/Coming_from_git_world/comment_5_e719d99af5afd90da3d3db692eff28dc._comment create mode 100644 doc/forum/Coming_from_git_world/comment_6_85a42106944dba9995fb3f4bfee3443a._comment create mode 100644 doc/forum/Coming_from_git_world/comment_7_90623294b910ceca3dc8ebd41b50fc9b._comment create mode 100644 doc/forum/Coming_from_git_world/comment_8_28dbee30eb54877418f72eb8935302d8._comment create mode 100644 doc/forum/Coming_from_git_world/comment_9_6edb36ea9535030fa3766937398e5bc7._comment create mode 100644 doc/forum/Compression_in_special_remotes___40__specifically_S3__41____63__.mdwn create mode 100644 doc/forum/Compression_in_special_remotes___40__specifically_S3__41____63__/comment_1_9c6c4ca0c9dc6976ba7cf27e84683bf0._comment create mode 100644 doc/forum/DBus_on_Ubuntu_12.04__63__.mdwn create mode 100644 doc/forum/DBus_on_Ubuntu_12.04__63__/comment_1_dc14a40b64b7eda94d1a3fd766cd39cc._comment create mode 100644 doc/forum/DBus_on_Ubuntu_12.04__63__/comment_2_608a30e274e6a691a39f69503720e320._comment create mode 100644 doc/forum/DBus_on_Ubuntu_12.04__63__/comment_3_791b9978b410c1aff7fd8ef05c38f5f9._comment create mode 100644 doc/forum/DBus_on_Ubuntu_12.04__63__/comment_4_8665c95299916138c4af375626d9ec7d._comment create mode 100644 doc/forum/DS__95__Store_files_are_not_added.mdwn create mode 100644 doc/forum/DS__95__Store_files_are_not_added/comment_1_30687306da9bd35ec02a806193c5e240._comment create mode 100644 doc/forum/Debugging_Git_Annex.mdwn create mode 100644 doc/forum/Debugging_Git_Annex/comment_1_ce63b2ee641a2338f1ad5ded9e6f09a8._comment create mode 100644 doc/forum/Debugging_Git_Annex/comment_2_1d70ff052d00f33c34fd45730ea13040._comment create mode 100644 doc/forum/Default_text__47__html_handler.mdwn create mode 100644 doc/forum/Default_text__47__html_handler/comment_1_4730061916c7e12b7a41906152f847ee._comment create mode 100644 doc/forum/Delete_unused_files__47__metadata.mdwn create mode 100644 doc/forum/Delete_unused_files__47__metadata/comment_1_3efc19895c8dec89b71ae3778b583fea._comment create mode 100644 doc/forum/Delete_unused_files__47__metadata/comment_2_23597d9468347b3d94257f3c02afe1b8._comment create mode 100644 doc/forum/Detached_git_work_tree__63__.mdwn create mode 100644 doc/forum/Detached_git_work_tree__63__/comment_10_656c737772bf92be2c7a2f33bd2bb0f0._comment create mode 100644 doc/forum/Detached_git_work_tree__63__/comment_1_28ac35a325fba250721d9f1b7c994960._comment create mode 100644 doc/forum/Detached_git_work_tree__63__/comment_2_7128c26bbc8efea04a5a317edf0ca9f2._comment create mode 100644 doc/forum/Detached_git_work_tree__63__/comment_3_a3c22f905748ff2c803e8621c74a87a0._comment create mode 100644 doc/forum/Detached_git_work_tree__63__/comment_4_8063921241760458349e7cb0cadf3d4e._comment create mode 100644 doc/forum/Detached_git_work_tree__63__/comment_5_4510a787255cb03e7d0c3e7b830b7d52._comment create mode 100644 doc/forum/Detached_git_work_tree__63__/comment_6_ffd9c67ecc5b46ae98996018573f5591._comment create mode 100644 doc/forum/Detached_git_work_tree__63__/comment_7_36ca007643c983604fc4aed6ec8cb3d2._comment create mode 100644 doc/forum/Detached_git_work_tree__63__/comment_8_b7a2da4fbace7156e11c48a496a19dc9._comment create mode 100644 doc/forum/Detached_git_work_tree__63__/comment_9_f9fa237a693d28178f0451799209f7e2._comment create mode 100644 doc/forum/Difference_between_copy__44___move_and_get__63__.mdwn create mode 100644 doc/forum/Difference_between_copy__44___move_and_get__63__/comment_1_26ee8192af3a62178c1ccf17c6da5ca5._comment create mode 100644 doc/forum/Different_annexes_pointing_to_same_special_remote__63__.mdwn create mode 100644 doc/forum/Different_annexes_pointing_to_same_special_remote__63__/comment_1_359f46805e6508d03aadd90429937546._comment create mode 100644 doc/forum/Direct_special_remotes.mdwn create mode 100644 doc/forum/Direct_special_remotes/comment_1_50357130a1c57ad2fab70f71925faf02._comment create mode 100644 doc/forum/Direct_special_remotes/comment_2_e94a722ca056a068bcc16eb822008602._comment create mode 100644 doc/forum/Direct_special_remotes/comment_4_187036bbfee0508e2914afb51ead3c71._comment create mode 100644 doc/forum/Direct_special_remotes/comment_4_6bfbf60f2061d49b7d34c844e7e1dea2._comment create mode 100644 doc/forum/Direct_special_remotes/comment_5_69c34c655e4b153dfc0d1b8580091124._comment create mode 100644 doc/forum/Direct_special_remotes/comment_6_b054cfc3d3f81873f3faae7eb4f5337c._comment create mode 100644 doc/forum/Does_Jabber_syncing_work_when_the_buddy_is_offline__63__.mdwn create mode 100644 doc/forum/Does_Jabber_syncing_work_when_the_buddy_is_offline__63__/comment_1_f290dd8547176793934f8077374e1c0a._comment create mode 100644 doc/forum/Does_Jabber_syncing_work_when_the_buddy_is_offline__63__/comment_2_c358eb51047f333e582bd824be5e0e65._comment create mode 100644 doc/forum/Does_Jabber_syncing_work_when_the_buddy_is_offline__63__/comment_3_a2332c0e7b29110b9aed2ab69ce9d8c4._comment create mode 100644 doc/forum/Does_git-annex_version_big_files__63__.mdwn create mode 100644 doc/forum/Does_git-annex_version_big_files__63__/comment_1_0b44003c1dc53adb807298ae452f8004._comment create mode 100644 doc/forum/Does_git-annex_version_big_files__63__/comment_2_ca40b67abd7bd36155d16d0396d7472c._comment create mode 100644 doc/forum/Does_git-annex_version_big_files__63__/comment_3_32de3501feedce51b43ed9dcc399c7a9._comment create mode 100644 doc/forum/Does_git-annex_version_big_files__63__/comment_4_8c65a7f8bda3c876971c2801fb6a76a1._comment create mode 100644 doc/forum/Does_migrate_ensure_data_integrity__63__.mdwn create mode 100644 doc/forum/Does_migrate_ensure_data_integrity__63__/comment_1_cef50b32c46f4406c6f918c5866ddc15._comment create mode 100644 doc/forum/Does_migrate_ensure_data_integrity__63__/comment_2_f389b924c8531b35fdf5dedd10fc8000._comment create mode 100644 doc/forum/Don__39__t_understand_how_to_delete__47__recover_files.mdwn create mode 100644 doc/forum/Don__39__t_understand_how_to_delete__47__recover_files/comment_1_b307bfb0b70d649897f411eb753bd50a._comment create mode 100644 doc/forum/Don__39__t_understand_how_to_delete__47__recover_files/comment_2_58a6a1476274b8c4feb3d43ecd998759._comment create mode 100644 doc/forum/Don__39__t_understand_how_to_delete__47__recover_files/comment_3_4b857f481db7b2437ac9f8137a8510e2._comment create mode 100644 doc/forum/Don__39__t_understand_how_to_delete__47__recover_files/comment_4_828db3bf2863d98c0b0fb4074aa7f066._comment create mode 100644 doc/forum/Don__39__t_understand_how_to_delete__47__recover_files/comment_5_cb2063d6a4e08a5c12bf3723d0fa74e0._comment create mode 100644 doc/forum/Don__39__t_understand_how_to_delete__47__recover_files/comment_6_1759bcd5708f591f91b9c410f6dc5c54._comment create mode 100644 doc/forum/Don__39__t_understand_how_to_delete__47__recover_files/comment_7_2a389f01eb5131042ea1e71a73c9787a._comment create mode 100644 doc/forum/Don__39__t_understand_local_vs._known_keys.mdwn create mode 100644 doc/forum/Don__39__t_understand_local_vs._known_keys/comment_1_10749c0d76e824217dd1ff8c8a6e42a5._comment create mode 100644 doc/forum/Don__39__t_understand_local_vs._known_keys/comment_2_db9f1b6d9638c2b0a7e241c2727e8cfb._comment create mode 100644 doc/forum/Drop_with_assistant.mdwn create mode 100644 doc/forum/Drop_with_assistant/comment_1_048f5a31c549afb19b76a65bddd0cd24._comment create mode 100644 doc/forum/Drop_with_assistant/comment_2_527d7b6a8efa85b904111f179912d926._comment create mode 100644 doc/forum/Drop_with_assistant/comment_3_c50857506869bb1cd306b66acf37fba8._comment create mode 100644 doc/forum/Drop_with_assistant/comment_4_1ea37445d5eb96c3efa182e88e07b867._comment create mode 100644 doc/forum/Drop_with_assistant/comment_5_c08908ea5232cbe067c73ecd12d0e218._comment create mode 100644 doc/forum/Drop_with_assistant/comment_6_015134228cb865f97326fbb7193636ea._comment create mode 100644 doc/forum/Drop_with_assistant/comment_7_950759930667588f21659cd6d7065fbb._comment create mode 100644 doc/forum/Drop_with_assistant/comment_8_773e540e46adc43487323e8d38ceb2d9._comment create mode 100644 doc/forum/Drop_with_assistant/comment_9_d85d120d7219ea6c179c2619a17bdae9._comment create mode 100644 doc/forum/Encrypted_ssh_remote__44___synced_folders.mdwn create mode 100644 doc/forum/Encrypted_ssh_remote__44___synced_folders/comment_1_7b9b4ef614c90e0b222d24678d1b9026._comment create mode 100644 doc/forum/Error_adding_ssh_remote_in_assistant.mdwn create mode 100644 doc/forum/Error_adding_ssh_remote_in_assistant/comment_1_eecc0660db4083cc91c5330587f74610._comment create mode 100644 doc/forum/Error_adding_ssh_remote_in_assistant/comment_2_3e6aad22e8020b12ff7ef914b75281d1._comment create mode 100644 doc/forum/Error_adding_ssh_remote_in_assistant/comment_3_3ea529e16502071fc0980c6d5c60a036._comment create mode 100644 doc/forum/Error_while_adding_a_file___34__createSymbolicLink:_already_exists__34__.mdwn create mode 100644 doc/forum/External_drive_syncs_git-annex_branch_but_not_master_branch.mdwn create mode 100644 doc/forum/External_drive_syncs_git-annex_branch_but_not_master_branch/comment_1_9a909e3d89061adacbd8ed370520250c._comment create mode 100644 doc/forum/External_drive_syncs_git-annex_branch_but_not_master_branch/comment_2_0dd489b264374b7b1065b89e1ff7561b._comment create mode 100644 doc/forum/Feature_request:_Multiple_concurrent_transfers.mdwn create mode 100644 doc/forum/Feature_request:_git_annex_copy_--auto_does_the_right_thing.mdwn create mode 100644 doc/forum/Feature_request:_git_annex_copy_--auto_does_the_right_thing/comment_1_bbac7d0810a79eb1f42a01e1b31d5c4c._comment create mode 100644 doc/forum/Feature_request:_webapp_support_for_centralized_bare_repos.mdwn create mode 100644 doc/forum/First_attempt_at_an_OSX_launcher___40__.app__41__.mdwn create mode 100644 doc/forum/First_attempt_at_an_OSX_launcher___40__.app__41__/comment_1_97c261b9080c5ecc5424683066bbe05b._comment create mode 100644 doc/forum/First_attempt_at_an_OSX_launcher___40__.app__41__/comment_2_ae45f9703b635c235409682cf252d36c._comment create mode 100644 doc/forum/First_attempt_at_an_OSX_launcher___40__.app__41__/comment_3_066ca31a2e5dfe55a58092ba85231c7c._comment create mode 100644 doc/forum/First_attempt_at_an_OSX_launcher___40__.app__41__/comment_4_a0a9f7f44cadb8036fcddfc21bb0781f._comment create mode 100644 doc/forum/First_attempt_at_an_OSX_launcher___40__.app__41__/comment_5_92240b3f8629f1f2bbe1829700082a79._comment create mode 100644 doc/forum/Fixing_up_corrupt_annexes.mdwn create mode 100644 doc/forum/Fixing_up_corrupt_annexes/comment_1_cea21f96bcfb56aaab7ea03c1c804d2d._comment create mode 100644 doc/forum/Fixing_up_corrupt_annexes/comment_2_5cdd2fcfa61b3f6255e5ad63a3ab00ce._comment create mode 100644 doc/forum/Getting_started_with_Amazon_S3.mdwn create mode 100644 doc/forum/Getting_started_with_Amazon_S3/comment_1_f50883133d5d4903cc95c0dcaa52d052._comment create mode 100644 doc/forum/Getting_started_with_Amazon_S3/comment_2_e90aa3259d9a12cd67daa27d42d69ab5._comment create mode 100644 doc/forum/Getting_started_with_Amazon_S3/comment_3_c3adce7c0f29e71ed9dd07103ede2c1a._comment create mode 100644 doc/forum/Git_Annex_Transfer_Protocols.mdwn create mode 100644 doc/forum/Git_Annex_Transfer_Protocols/comment_1_a870ec991078c95a6bb683d6962ab56e._comment create mode 100644 doc/forum/Git_Annex_Transfer_Protocols/comment_2_71419376ef50a679ea8f0f9e16991c17._comment create mode 100644 doc/forum/Git_Annex_Transfer_Protocols/comment_3_fea43664a500111ca99f4043e0dadb14._comment create mode 100644 doc/forum/Git_Annex_Transfer_Protocols/comment_4_56fb2dab1d4030c9820be32b495afdf0._comment create mode 100644 doc/forum/Git_Annex_Transfer_Protocols/comment_5_a6ec9c5a4a3c0bac1df87f1df9be140b._comment create mode 100644 doc/forum/Git_Annex_Transfer_Protocols/comment_6_1678452fb7114aeabcf0cc3d5f6c69b0._comment create mode 100644 doc/forum/Git_annex_assistant_in_command_line.mdwn create mode 100644 doc/forum/Git_annex_assistant_in_command_line/comment_1_ce05226307ade8db90ada2dbf290bd58._comment create mode 100644 doc/forum/Git_annex_syncing_speed__44___possible__63__.mdwn create mode 100644 doc/forum/Git_annex_syncing_speed__44___possible__63__/comment_1_8aa224b3016dc38e4cea8ee1865a3ab6._comment create mode 100644 doc/forum/Git_repos_in_git_annex__63__.mdwn create mode 100644 doc/forum/Git_repos_in_git_annex__63__/comment_1_8aaa0d83e8fcd5997f6b0097f3b21622._comment create mode 100644 doc/forum/Git_repositories_in_the_annex__63__.mdwn create mode 100644 doc/forum/Handling_web_special_remote_when_content_changes__63__.mdwn create mode 100644 doc/forum/Handling_web_special_remote_when_content_changes__63__/comment_1_05ee6a1b1943ef3c90634e52233bde1c._comment create mode 100644 doc/forum/Handling_web_special_remote_when_content_changes__63__/comment_2_48d82e391812d8ec0d4e6562d0607fe7._comment create mode 100644 doc/forum/Help_with_syncing_file_contents.mdwn create mode 100644 doc/forum/Help_with_syncing_file_contents/comment_1_7ec34de3140983739080115c82966bf5._comment create mode 100644 doc/forum/Help_with_syncing_file_contents/comment_2_7dba58d3c62d6f64a270298e4e4329a4._comment create mode 100644 doc/forum/Help_with_syncing_file_contents/comment_3_b26cfa20dc81517d93e760f4809bdc24._comment create mode 100644 doc/forum/How_do_I_cleanly_remove_an_Android_git-annex_installation__63__.mdwn create mode 100644 doc/forum/How_do_I_dropunused_with_an_rsync_remote__63__.mdwn create mode 100644 doc/forum/How_do_I_dropunused_with_an_rsync_remote__63__/comment_1_8db3cb5348b845eb99c2c829957db9ea._comment create mode 100644 doc/forum/How_do_I_dropunused_with_an_rsync_remote__63__/comment_2_6cc909d9d74bc1ccb8a7b0d7d234c7cd._comment create mode 100644 doc/forum/How_do_I_dropunused_with_an_rsync_remote__63__/comment_3_f24d678e4192a70322aa164ed9b71fc8._comment create mode 100644 doc/forum/How_do_I_dropunused_with_an_rsync_remote__63__/comment_4_9233decd0aaf9211447f36e0d9346445._comment create mode 100644 doc/forum/How_do_I_dropunused_with_an_rsync_remote__63__/comment_5_e1deb110f752e5495d5c77ec444abac5._comment create mode 100644 doc/forum/How_do_you_know_when_something_fails_a_fsck__63__.mdwn create mode 100644 doc/forum/How_do_you_know_when_something_fails_a_fsck__63__/comment_1_1c14981916dd55376d5e9f95023556cb._comment create mode 100644 doc/forum/How_does_one_change_git-annex_assistant__39__s_web_browser__63__.mdwn create mode 100644 doc/forum/How_does_one_change_git-annex_assistant__39__s_web_browser__63__/comment_1_f4402eabda2327da3a0bbc64ed3baf9a._comment create mode 100644 doc/forum/How_does_one_change_git-annex_assistant__39__s_web_browser__63__/comment_2_cdb41f2c7b6bc5bf40d88582dcbf45aa._comment create mode 100644 doc/forum/How_does_one_change_git-annex_assistant__39__s_web_browser__63__/comment_3_ca75e928c245eb23a02b5f40ec69cbb1._comment create mode 100644 doc/forum/How_does_one_change_git-annex_assistant__39__s_web_browser__63__/comment_4_1635f136909711295b9b70d1255e0378._comment create mode 100644 doc/forum/How_does_one_change_git-annex_assistant__39__s_web_browser__63__/comment_5_ee0cbe9498c518de98480a2ad229f685._comment create mode 100644 doc/forum/How_to_deal_with_renamed_files_in_direct_mode__63__.mdwn create mode 100644 doc/forum/How_to_deal_with_renamed_files_in_direct_mode__63__/comment_1_fe38fedbbc9e4a9e13bf19950e63c7ac._comment create mode 100644 doc/forum/How_to_define_an_alternative_remote_url_for_a_git_remote_repository__63__.mdwn create mode 100644 doc/forum/How_to_define_an_alternative_remote_url_for_a_git_remote_repository__63__/comment_1_52918b5ec25e55837215439fe1bb1a14._comment create mode 100644 doc/forum/How_to_define_an_alternative_remote_url_for_a_git_remote_repository__63__/comment_2_3a1567c9f484b5e12e5560cdcc2cfddd._comment create mode 100644 doc/forum/How_to_define_an_alternative_remote_url_for_a_git_remote_repository__63__/comment_3_48c3a80c14a85f27d742482b2ccbe628._comment create mode 100644 doc/forum/How_to_delete_a_remote__63__.mdwn create mode 100644 doc/forum/How_to_delete_a_remote__63__/comment_1_8cba186bb67079ff41bf6d0b04613f4a._comment create mode 100644 doc/forum/How_to_destroy_a_master_annex_and_all_remotes_with_git_annex_assistant_and_ext4.mdwn create mode 100644 doc/forum/How_to_destroy_a_master_annex_and_all_remotes_with_git_annex_assistant_and_ext4/comment_1_42ca6cfbbb79fe63514805b8119ac16b._comment create mode 100644 doc/forum/How_to_destroy_a_master_annex_and_all_remotes_with_git_annex_assistant_and_ext4/comment_2_c94ce6a9767c624e2445a7d9eea40396._comment create mode 100644 doc/forum/How_to_destroy_a_master_annex_and_all_remotes_with_git_annex_assistant_and_ext4/comment_3_bcda51053b62bbb20ce71a59469e1b26._comment create mode 100644 doc/forum/How_to_destroy_a_master_annex_and_all_remotes_with_git_annex_assistant_and_ext4/comment_4_48e5b9eae920e5f13812de8d6f6bc640._comment create mode 100644 doc/forum/How_to_destroy_a_master_annex_and_all_remotes_with_git_annex_assistant_and_ext4/comment_5_787c0bfdc1d309db1486c3a37723a957._comment create mode 100644 doc/forum/How_to_destroy_a_master_annex_and_all_remotes_with_git_annex_assistant_and_ext4/comment_6_8894beb06443f234e9200b03b5f3badf._comment create mode 100644 doc/forum/How_to_destroy_a_master_annex_and_all_remotes_with_git_annex_assistant_and_ext4/comment_7_457f62ee3e58f68a55f66c5bde6002fd._comment create mode 100644 doc/forum/How_to_destroy_a_master_annex_and_all_remotes_with_git_annex_assistant_and_ext4/comment_8_bd2b412116a66107bc0ff0efd7e39a58._comment create mode 100644 doc/forum/How_to_expire_old_versions_of_files_that_have_been_edited__63__.mdwn create mode 100644 doc/forum/How_to_expire_old_versions_of_files_that_have_been_edited__63__/comment_1_dccf4dc4483d08e5e2936b2cadeafeaf._comment create mode 100644 doc/forum/How_to_expire_old_versions_of_files_that_have_been_edited__63__/comment_2_5710294c1c8652c12b6df2233255a45e._comment create mode 100644 doc/forum/How_to_handle_the_git-annex_branch__63__.mdwn create mode 100644 doc/forum/How_to_handle_the_git-annex_branch__63__/comment_1_800bd55b322e72f229882d7fd3888b14._comment create mode 100644 doc/forum/How_to_make_Maven_releases_work_with_git_annex___63__.mdwn create mode 100644 doc/forum/How_to_make_Maven_releases_work_with_git_annex___63__/comment_1_9298aa55771b68873de02e6a7964bbdc._comment create mode 100644 doc/forum/How_to_prevent_the_assistant_from_downloading_all_data__63__.mdwn create mode 100644 doc/forum/How_to_prevent_the_assistant_from_downloading_all_data__63__/comment_1_fd8b287758ad77b3527ae71017cffabf._comment create mode 100644 doc/forum/How_to_prevent_the_assistant_from_downloading_all_data__63__/comment_2_e8e75b4451aaf55461edf2f3d68797ed._comment create mode 100644 doc/forum/How_to_rename_a_remote__63__.mdwn create mode 100644 doc/forum/How_to_rename_a_remote__63__/comment_1_a9bfbd82f7bb47661f0d9e0e0d904332._comment create mode 100644 doc/forum/How_to_restore_symlinks.mdwn create mode 100644 doc/forum/How_to_restore_symlinks/comment_1_c67e752cf7d5431096fab4b3304790a7._comment create mode 100644 doc/forum/How_to_restore_symlinks/comment_2_f9ec6096595e2c149c48924e3b54542f._comment create mode 100644 doc/forum/How_to_restore_symlinks/comment_3_4ff80729787a2a4e2baf05dd1db37da3._comment create mode 100644 doc/forum/How_to_retroactively_annex_a_file_already_in_a_git_repo.mdwn create mode 100644 doc/forum/How_to_set_up_two_assistants_with_one_shared_transfer_repository__63__.mdwn create mode 100644 doc/forum/How_to_set_up_two_assistants_with_one_shared_transfer_repository__63__/comment_1_bedaf308cfc70b9e751914a400ebcbc2._comment create mode 100644 doc/forum/How_to_set_up_two_assistants_with_one_shared_transfer_repository__63__/comment_2_d665b1514253c8aa487ebf8b2728e3b1._comment create mode 100644 doc/forum/How_to_set_up_two_assistants_with_one_shared_transfer_repository__63__/comment_3_aef42387a3673ab6710fb23e878d7e17._comment create mode 100644 doc/forum/How_to_set_up_two_assistants_with_one_shared_transfer_repository__63__/comment_4_bfbcc041db472f4808979e6b3d7c4be2._comment create mode 100644 doc/forum/Howto_remove_a_repository__63__.mdwn create mode 100644 doc/forum/Howto_remove_a_repository__63__/comment_1_b55fa4e92bb457ecaa5ca8f5cee7be1d._comment create mode 100644 doc/forum/Howto_remove_unused_files.mdwn create mode 100644 doc/forum/Howto_remove_unused_files/comment_1_f2a7948268ce3cb3967a9fdd8ccc570a._comment create mode 100644 doc/forum/Howto_remove_unused_files/comment_2_9b4d198c2d8a52adef3d166a8196fc0d._comment create mode 100644 doc/forum/Howto_remove_unused_files/comment_3_441d10901d5c055ac3ed2a6cb61c075c._comment create mode 100644 doc/forum/Is_an_automagic_upgrade_of_the_object_directory_safe__63__.mdwn create mode 100644 doc/forum/Is_an_automagic_upgrade_of_the_object_directory_safe__63__/comment_1_c25900b9d2d62cc0b8c77150bcfebadf._comment create mode 100644 doc/forum/Is_git-annex_in_a_precarious_state_before_the_initial_commit__63__.mdwn create mode 100644 doc/forum/Is_git-annex_in_a_precarious_state_before_the_initial_commit__63__/comment_1_f9decde3955f10148febc4646fba5a68._comment create mode 100644 doc/forum/Is_git-annex_in_a_precarious_state_before_the_initial_commit__63__/comment_2_ed32a48edce4f150bedf24cfe91de254._comment create mode 100644 doc/forum/Is_git-annex_in_a_precarious_state_before_the_initial_commit__63__/comment_3_ef9618850e5e688bac3c646983f00ed8._comment create mode 100644 doc/forum/Is_it_possible_to_make_git-sync_not_nullify_symlinks__63__.mdwn create mode 100644 doc/forum/Is_it_possible_to_make_git-sync_not_nullify_symlinks__63__/comment_1_d6f2d2cdc5f4ffde9eee9f3a8c215a06._comment create mode 100644 doc/forum/Lacking_webapp_on_Trisquel__47__Ubuntu_Precise.mdwn create mode 100644 doc/forum/Lacking_webapp_on_Trisquel__47__Ubuntu_Precise/comment_1_6bd27bd31833336c1df783253378ccae._comment create mode 100644 doc/forum/Let_watch_selectively_annex_files.mdwn create mode 100644 doc/forum/Let_watch_selectively_annex_files/comment_1_8379de87d16502d9aadf252da01e4d9a._comment create mode 100644 doc/forum/Let_watch_selectively_annex_files/comment_2_2219ff6b4dc927eb2a299cd1af90aed8._comment create mode 100644 doc/forum/Local_and_remote_in_direct_mode.mdwn create mode 100644 doc/forum/Local_and_remote_in_direct_mode/comment_1_45f89ebcb6092d1b2582feebc8a5e9d7._comment create mode 100644 doc/forum/Looking_at_the_webapp_on_OSX.mdwn create mode 100644 doc/forum/Looking_at_the_webapp_on_OSX/comment_1_68820f2f469356633c1abb18a47e0c59._comment create mode 100644 doc/forum/Looking_at_the_webapp_on_OSX/comment_2_4ce86546d8a135df9cfab46b4612fa0b._comment create mode 100644 doc/forum/Looking_at_the_webapp_on_OSX/comment_3_6d398a2cceff14a1b774b85ee1725073._comment create mode 100644 doc/forum/Looking_at_the_webapp_on_OSX/comment_4_5e503787a4b1d3534c5e20da5480b763._comment create mode 100644 doc/forum/Looking_at_the_webapp_on_OSX/comment_5_c735841bc230efc61594ea013fc2902b._comment create mode 100644 doc/forum/Looking_at_the_webapp_on_OSX/comment_6_0e489fbfc89d282e9eb47f1b814ff70c._comment create mode 100644 doc/forum/Make_whereis_output_more_compact.mdwn create mode 100644 doc/forum/Making_git-annex_a_self-funded_project__63__.mdwn create mode 100644 doc/forum/Making_git-annex_a_self-funded_project__63__/comment_1_4a1ba95b7231ba973ddb672d2419e28c._comment create mode 100644 doc/forum/Making_git-annex_a_self-funded_project__63__/comment_2_7c476ae92e63c991f229708678874ca2._comment create mode 100644 doc/forum/Making_git-annex_less_necessary.mdwn create mode 100644 doc/forum/Making_git-annex_less_necessary/comment_1_03faaa3866778d24cd03887b85dc9954._comment create mode 100644 doc/forum/Making_git-annex_less_necessary/comment_2_2db02a94dffd525885c9d7fc6c5fa464._comment create mode 100644 doc/forum/Making_git-annex_less_necessary/comment_3_429ec656e0ac02f98843f8d7f3c02d6a._comment create mode 100644 doc/forum/Making_git-annex_less_necessary/comment_4_384813dd022dfd9c1ef14e0f1479a123._comment create mode 100644 doc/forum/Managing_a_large_number_of_files_archived_on_many_pieces_of_read-only_medium___40__E.G._DVDs__41__.mdwn create mode 100644 doc/forum/Managing_a_large_number_of_files_archived_on_many_pieces_of_read-only_medium___40__E.G._DVDs__41__/comment_10_a061d300b718ad943c940e122cc57220._comment create mode 100644 doc/forum/Managing_a_large_number_of_files_archived_on_many_pieces_of_read-only_medium___40__E.G._DVDs__41__/comment_11_76529080054407570611b4357ce4f3ed._comment create mode 100644 doc/forum/Managing_a_large_number_of_files_archived_on_many_pieces_of_read-only_medium___40__E.G._DVDs__41__/comment_12_9acf5ce41a023f3848a51891cceeb51b._comment create mode 100644 doc/forum/Managing_a_large_number_of_files_archived_on_many_pieces_of_read-only_medium___40__E.G._DVDs__41__/comment_1_25e65ee3949e7d918376298cf11585f2._comment create mode 100644 doc/forum/Managing_a_large_number_of_files_archived_on_many_pieces_of_read-only_medium___40__E.G._DVDs__41__/comment_2_8a71ca048f9de29a198a6afb17d5315e._comment create mode 100644 doc/forum/Managing_a_large_number_of_files_archived_on_many_pieces_of_read-only_medium___40__E.G._DVDs__41__/comment_3_e3d1d3a3d3d831432ec940a8ab6f31e9._comment create mode 100644 doc/forum/Managing_a_large_number_of_files_archived_on_many_pieces_of_read-only_medium___40__E.G._DVDs__41__/comment_4_26a33eae98b4faaf6baf6635e3d28a8f._comment create mode 100644 doc/forum/Managing_a_large_number_of_files_archived_on_many_pieces_of_read-only_medium___40__E.G._DVDs__41__/comment_5_49ac298d39c824b0e52a239961463e09._comment create mode 100644 doc/forum/Managing_a_large_number_of_files_archived_on_many_pieces_of_read-only_medium___40__E.G._DVDs__41__/comment_6_55a4a3616ea59654da1c2f9902561e3b._comment create mode 100644 doc/forum/Managing_a_large_number_of_files_archived_on_many_pieces_of_read-only_medium___40__E.G._DVDs__41__/comment_7_92a2af3e0e328bb48bcc67a69187ee57._comment create mode 100644 doc/forum/Managing_a_large_number_of_files_archived_on_many_pieces_of_read-only_medium___40__E.G._DVDs__41__/comment_8_f6e39e71882d55cdc061166aea3e2bd3._comment create mode 100644 doc/forum/Managing_a_large_number_of_files_archived_on_many_pieces_of_read-only_medium___40__E.G._DVDs__41__/comment_9_6c45a6264d69e22800c329a0f8a2d470._comment create mode 100644 doc/forum/Managing_multiple_annexes_with_assistant__63__.mdwn create mode 100644 doc/forum/Managing_multiple_annexes_with_assistant__63__/comment_1_ba8c70e4a46441b48ad910625636eee5._comment create mode 100644 doc/forum/Managing_multiple_annexes_with_assistant__63__/comment_2_4b4f0a7d84a51ae92536e2c190256069._comment create mode 100644 doc/forum/Managing_multiple_annexes_with_assistant__63__/comment_3_86daadc565f96db5db13b6dbcbc66db3._comment create mode 100644 doc/forum/Managing_multiple_annexes_with_assistant__63__/comment_4_e43d71ddfdfdb7bcb13bfb894de6a5ec._comment create mode 100644 doc/forum/Managing_multiple_annexes_with_assistant__63__/comment_5_e94d33be83b45918d1a39d6e16fba4b4._comment create mode 100644 doc/forum/Managing_multiple_repositories_concurrently__63__.mdwn create mode 100644 doc/forum/Managing_multiple_repositories_concurrently__63__/comment_1_ebec1ddad71e961cdc9b21cbddfbcdaf._comment create mode 100644 doc/forum/Manual_Setup_of_a_Central_Repo.mdwn create mode 100644 doc/forum/Moving_large_files_within_the_repo_without_copying___63__.mdwn create mode 100644 doc/forum/Moving_large_files_within_the_repo_without_copying___63__/comment_1_9e3290138133d5a23a80f72342f47ec4._comment create mode 100644 doc/forum/Moving_large_files_within_the_repo_without_copying___63__/comment_2_232b77894dda51d02cbc34bd25d3213b._comment create mode 100644 doc/forum/Moving_large_files_within_the_repo_without_copying___63__/comment_3_d35ac1bdb3fa6e303ad92348ba174158._comment create mode 100644 doc/forum/Moving_large_files_within_the_repo_without_copying___63__/comment_4_4b443ec6b47eaabe214d0c2222083e4a._comment create mode 100644 doc/forum/Moving_older_version__39__s_file_content_without_doing_checkout.mdwn create mode 100644 doc/forum/Moving_older_version__39__s_file_content_without_doing_checkout/comment_1_f114b75b29123453758b493fae7f5167._comment create mode 100644 doc/forum/Moving_older_version__39__s_file_content_without_doing_checkout/comment_2_e377b7614c2961b460a10e285f3db274._comment create mode 100644 doc/forum/Moving_older_version__39__s_file_content_without_doing_checkout/comment_3_d251958795ab0867c65cf182e54a6ffe._comment create mode 100644 doc/forum/My_first_impressions_after_some_weeks_with_git-annex_assistant.mdwn create mode 100644 doc/forum/My_first_impressions_after_some_weeks_with_git-annex_assistant/comment_1_9d4019a54fb508e286a5d6d2660361d9._comment create mode 100644 doc/forum/Need_new_build_instructions_for_Debian_stable.mdwn create mode 100644 doc/forum/Need_new_build_instructions_for_Debian_stable/comment_1_8c1eea6dfec8b7e1c7a371b6e9c26118._comment create mode 100644 doc/forum/Need_new_build_instructions_for_Debian_stable/comment_2_f6ff8306c946219dbe39bb8938a349ab._comment create mode 100644 doc/forum/Need_new_build_instructions_for_Debian_stable/comment_3_bcda70cbfc7c1a14fa82da70f9f876e2._comment create mode 100644 doc/forum/Need_some_help_to_fix_my_repository.mdwn create mode 100644 doc/forum/Need_some_help_to_fix_my_repository/comment_1_f0d279c530b796b2c93d793f85d147e8._comment create mode 100644 doc/forum/Need_some_help_to_fix_my_repository/comment_2_a3fcfa1f8eadec5fa8a9efacca174048._comment create mode 100644 doc/forum/Need_some_help_to_fix_my_repository/comment_3_7878f9b76ddfa3392c9ec6a1810cb745._comment create mode 100644 doc/forum/New_git-annex_integration_mode_for_Emacs_users.mdwn create mode 100644 doc/forum/New_user_misunderstandings.mdwn create mode 100644 doc/forum/New_user_misunderstandings/comment_1_c1785924109b5d5cde9aa3d3460cf955._comment create mode 100644 doc/forum/Newbie_stuck_at___34__Unable_to_connect_to_the_Jabber_server__34__.mdwn create mode 100644 doc/forum/Newbie_stuck_at___34__Unable_to_connect_to_the_Jabber_server__34__/comment_1_59158afcedac18a7285d57491b2a468a._comment create mode 100644 doc/forum/Newbie_stuck_at___34__Unable_to_connect_to_the_Jabber_server__34__/comment_2_2a70ac08bb95774415b09dab7d7f8605._comment create mode 100644 doc/forum/No_SSL_traffic_for_S3__63__.mdwn create mode 100644 doc/forum/No_SSL_traffic_for_S3__63__/comment_1_f509bf273896180e6df8c771438dd093._comment create mode 100644 doc/forum/No_SSL_traffic_for_S3__63__/comment_2_358635d19c82202c63014ca84de7fc3b._comment create mode 100644 doc/forum/Not_sure_how_to_get_my_s3_remote_back.mdwn create mode 100644 doc/forum/Not_sure_how_to_get_my_s3_remote_back/comment_10_ed35a6ec605e8f79ec107856af6d1a46._comment create mode 100644 doc/forum/Not_sure_how_to_get_my_s3_remote_back/comment_11_e48b6efa42159dc83e1be11bfb54abcd._comment create mode 100644 doc/forum/Not_sure_how_to_get_my_s3_remote_back/comment_12_b58232d0e3fa4649565c0c7d4ce2e82e._comment create mode 100644 doc/forum/Not_sure_how_to_get_my_s3_remote_back/comment_13_85368b60091dc3ce2efb58013ffe9f83._comment create mode 100644 doc/forum/Not_sure_how_to_get_my_s3_remote_back/comment_14_e65281bef23e0076936c508728a87897._comment create mode 100644 doc/forum/Not_sure_how_to_get_my_s3_remote_back/comment_1_fffb59ad5a197d2980dd0ec35cf4aafa._comment create mode 100644 doc/forum/Not_sure_how_to_get_my_s3_remote_back/comment_2_0cfcc2075bff556b9fde5acc3dc1d599._comment create mode 100644 doc/forum/Not_sure_how_to_get_my_s3_remote_back/comment_3_6fe2ff1282fb14a4ce26ef8dc775d07e._comment create mode 100644 doc/forum/Not_sure_how_to_get_my_s3_remote_back/comment_4_64338d2d77dcbabd16b55eb145f40dc6._comment create mode 100644 doc/forum/Not_sure_how_to_get_my_s3_remote_back/comment_5_dd66c9ea0c83388f6826751944330d10._comment create mode 100644 doc/forum/Not_sure_how_to_get_my_s3_remote_back/comment_6_dc0c5e395e4c443b7227afdb157194e5._comment create mode 100644 doc/forum/Not_sure_how_to_get_my_s3_remote_back/comment_7_3c0ea4c76cdd889707f7308576e3efa0._comment create mode 100644 doc/forum/Not_sure_how_to_get_my_s3_remote_back/comment_8_36519ee4499a19f0864e4fcd264e9933._comment create mode 100644 doc/forum/Not_sure_how_to_get_my_s3_remote_back/comment_9_85b23f375e53469fb09b24b945b3aba9._comment create mode 100644 doc/forum/OSX__39__s_default_sshd_behaviour_has_limited_paths_set.mdwn create mode 100644 doc/forum/OSX__39__s_haskell-platform_statically_links_things.mdwn create mode 100644 doc/forum/OpenOffice___47___Libre_Office.mdwn create mode 100644 doc/forum/OpenOffice___47___Libre_Office/comment_1_98ed542fedd820d47bf8deb7d3232725._comment create mode 100644 doc/forum/OpenOffice___47___Libre_Office/comment_2_f313fdaa23863c2ae99cfbfe9ec2e1e0._comment create mode 100644 doc/forum/Out_of_memory_error_in_fsck__44___whereis__44___find_and_status_cmds.mdwn create mode 100644 doc/forum/Overwriting_data_without_getting_it.mdwn create mode 100644 doc/forum/Overwriting_data_without_getting_it/comment_1_f1c0199ee9bffcc84287370b89361294._comment create mode 100644 doc/forum/Overwriting_data_without_getting_it/comment_2_6a1d08dbca206129ef6cf8aa97daeee1._comment create mode 100644 doc/forum/Overwriting_data_without_getting_it/comment_3_52958e76e506fdbb6b533681ab619b3b._comment create mode 100644 doc/forum/Please_fix_compatibility_with_ghc_7.0.mdwn create mode 100644 doc/forum/Please_fix_compatibility_with_ghc_7.0/comment_1_d1d10217ebd0151e947b3a6cd37399ce._comment create mode 100644 doc/forum/Podcast_syncing_use-case.mdwn create mode 100644 doc/forum/Podcast_syncing_use-case/comment_1_ace6f9d3a950348a3ac0ff592b62e786._comment create mode 100644 doc/forum/Podcast_syncing_use-case/comment_2_930a6620b4d516e69ed952f9da5371bb._comment create mode 100644 doc/forum/Poor_man__39__s_IMAP.mdwn create mode 100644 doc/forum/Poor_man__39__s_IMAP/comment_1_258ff23c462dc88b88ced405c4f5040f._comment create mode 100644 doc/forum/Poor_man__39__s_IMAP/comment_2_c88d1abdda4cb526a6ee45a710c75bc4._comment create mode 100644 doc/forum/Poor_man__39__s_IMAP/comment_3_3847e371db1c2788c075e7dca1fbd33e._comment create mode 100644 doc/forum/Poor_man__39__s_IMAP/comment_4_cf6cc21f2cf2aa5c949844e24a7b4075._comment create mode 100644 doc/forum/Poor_man__39__s_IMAP/comment_5_d861fa69475ce526841b3195be8ee356._comment create mode 100644 doc/forum/Post-Kickstarter.mdwn create mode 100644 doc/forum/Preserving_file_access_rights_in_directory_tree_below_objects__47__.mdwn create mode 100644 doc/forum/Preserving_file_access_rights_in_directory_tree_below_objects__47__/comment_1_5dd978f9b5a0771f44ab9e086bf5a07f._comment create mode 100644 doc/forum/Preserving_file_access_rights_in_directory_tree_below_objects__47__/comment_2_9f51947b35ee04e473655e20d56c740a._comment create mode 100644 doc/forum/Problem_compiling_current_master.mdwn create mode 100644 doc/forum/Problem_compiling_current_master/comment_1_135df61ec850c06e3b48ccfef7b5b031._comment create mode 100644 doc/forum/Problem_compiling_current_master/comment_2_fb3e27b6014e84bd919a7a4a95e39ef9._comment create mode 100644 doc/forum/Problem_compiling_current_master/comment_3_b737b3945103c5e2aa798b4e65fbce06._comment create mode 100644 doc/forum/Problem_compiling_current_master/comment_4_28c1b335ae388d4e1f22b711ac1c001f._comment create mode 100644 doc/forum/Problem_with_bup:_cannot_lock_refs.mdwn create mode 100644 doc/forum/Problems_syncing_with_box.com.mdwn create mode 100644 doc/forum/Problems_syncing_with_box.com/comment_1_8db642849da4d42cd9a43142e2b7cb70._comment create mode 100644 doc/forum/Problems_syncing_with_box.com/comment_2_cd18f33647aebc04af5469e4ce1fbcd2._comment create mode 100644 doc/forum/Problems_using_submodules_with_git-annex__63__.mdwn create mode 100644 doc/forum/Problems_using_submodules_with_git-annex__63__/comment_1_c7a927736d419d3c31c912001ff16ee4._comment create mode 100644 doc/forum/Problems_with_large_numbers_of_files.mdwn create mode 100644 doc/forum/Problems_with_large_numbers_of_files/comment_1_08791cb78b982087c2a07316fe3ed46c._comment create mode 100644 doc/forum/Problems_with_large_numbers_of_files/comment_2_0392a11219463e40c53bae73c8188b69._comment create mode 100644 doc/forum/Problems_with_large_numbers_of_files/comment_3_537e9884c1488a7a4bcf131ea63b71f7._comment create mode 100644 doc/forum/Problems_with_large_numbers_of_files/comment_4_7cb65d013e72bd2b7e90452079d42ac9._comment create mode 100644 doc/forum/Problems_with_large_numbers_of_files/comment_5_86a42ee3173a5d38f803e64b79496ab3._comment create mode 100644 doc/forum/Problems_with_large_numbers_of_files/comment_6_4551274288383c9cc27cbf85b122d307._comment create mode 100644 doc/forum/Problems_with_large_numbers_of_files/comment_7_d18cf944352f8303799c86f2c0354e8e._comment create mode 100644 doc/forum/Push__47__Pull_with_the_Assistant.mdwn create mode 100644 doc/forum/Push__47__Pull_with_the_Assistant/comment_1_f7b63d379c2d21794adf8658f546f8a7._comment create mode 100644 doc/forum/Push__47__Pull_with_the_Assistant/comment_2_aec8cc20576e7ffd5a8be4348d1a0073._comment create mode 100644 doc/forum/Pushing_git_repo_to_AWS_S3_from_behind_proxy.mdwn create mode 100644 doc/forum/Reappearing_repos_in_webapp_and_vicfg.mdwn create mode 100644 doc/forum/Reappearing_repos_in_webapp_and_vicfg/comment_1_bd977e864ae89816fa7f4ff69879b15f._comment create mode 100644 doc/forum/Reappearing_repos_in_webapp_and_vicfg/comment_2_05749f9e75689d0111339b7126c12300._comment create mode 100644 doc/forum/Reappearing_repos_in_webapp_and_vicfg/comment_3_b1531994eea0fbbf4cb097e604378a53._comment create mode 100644 doc/forum/Reappearing_repos_in_webapp_and_vicfg/comment_4_f1eba3e8aa4116e3c20747ec1d6e24e5._comment create mode 100644 doc/forum/Recommended_number_of_repositories.mdwn create mode 100644 doc/forum/Recommended_number_of_repositories/comment_1_3ef256230756be8a9679b107cdbfd018._comment create mode 100644 doc/forum/Relocating_annex_directory.mdwn create mode 100644 doc/forum/Removing_files_not_found_by_git_annex_unused.mdwn create mode 100644 doc/forum/Removing_files_not_found_by_git_annex_unused/comment_1_420c6230e68de0a0ac7d7da91ac60801._comment create mode 100644 doc/forum/Repo_accessible_from___34__dumb__34___client_without_git-annex.mdwn create mode 100644 doc/forum/Repo_accessible_from___34__dumb__34___client_without_git-annex/comment_1_077c492fd37d335f74a5c886ff0d524f._comment create mode 100644 doc/forum/Repo_accessible_from___34__dumb__34___client_without_git-annex/comment_2_00e6576e3e60d2650461eeb0f918e6e5._comment create mode 100644 doc/forum/Repo_accessible_from___34__dumb__34___client_without_git-annex/comment_3_c36a9562c53ac683b62fc4471405aa2a._comment create mode 100644 doc/forum/Restricting_git-annex-shell_to_a_specific_repository.mdwn create mode 100644 doc/forum/Restricting_git-annex-shell_to_a_specific_repository/comment_1_66544520bff71181e4a03ca583b0b458._comment create mode 100644 doc/forum/Restricting_git-annex-shell_to_a_specific_repository/comment_2_2a210255e8535712c71fa183e56ab600._comment create mode 100644 doc/forum/Restricting_git-annex-shell_to_a_specific_repository/comment_3_52cd4bd9694b2100b0e0dd2eafa9e828._comment create mode 100644 doc/forum/Running_assistant_on_a_server___40__no_X_available__41__.mdwn create mode 100644 doc/forum/Running_assistant_on_a_server___40__no_X_available__41__/comment_1_dd75d78ef63f2689199a302ed1846017._comment create mode 100644 doc/forum/Running_assistant_on_a_server___40__no_X_available__41__/comment_2_df654df60c5fa6a84d786d248928a352._comment create mode 100644 doc/forum/Running_assistant_steps_manually.mdwn create mode 100644 doc/forum/Running_assistant_steps_manually/comment_1_e14e0a1d55d01cb4f67a94bbe349b872._comment create mode 100644 doc/forum/Same_Jabber_account_for_different_annexes.mdwn create mode 100644 doc/forum/Same_Jabber_account_for_different_annexes/comment_1_90c3954fe11980eef42b5f5d34f83488._comment create mode 100644 doc/forum/Same_Jabber_account_for_different_annexes/comment_2_802600b3568e5f94d0550092b22975db._comment create mode 100644 doc/forum/Securing_a_shared_ssh_server.mdwn create mode 100644 doc/forum/Securing_a_shared_ssh_server/comment_1_ea971b57d94db5b8d487f728faa5e9a8._comment create mode 100644 doc/forum/Securing_a_shared_ssh_server/comment_2_421a19f6e1fb40db6ee205daf8e3f867._comment create mode 100644 doc/forum/Securing_a_shared_ssh_server/comment_3_acdbf92f646dbbf691621f08b3d94c26._comment create mode 100644 doc/forum/Securing_a_shared_ssh_server/comment_4_67533d08e1b8706b844262e9c483d982._comment create mode 100644 doc/forum/Securing_a_shared_ssh_server/comment_5_bf193e02b388b4358632a169d2425b5c._comment create mode 100644 doc/forum/Securing_a_shared_ssh_server/comment_6_50d391992cd444080ebc70db30b215c5._comment create mode 100644 doc/forum/Setup_of_rsync_special_remote_with_non-standard_ssh_port.mdwn create mode 100644 doc/forum/Setup_of_rsync_special_remote_with_non-standard_ssh_port/comment_1_1eb6990e93ec92cb6fd7dbee59f31072._comment create mode 100644 doc/forum/Setup_of_rsync_special_remote_with_non-standard_ssh_port/comment_2_c85d5167e7ccce1ecf1de396e72ce7bc._comment create mode 100644 doc/forum/Sharing_annex_with_local_clones.mdwn create mode 100644 doc/forum/Sharing_annex_with_local_clones/comment_1_2b60e13e5f7b8cee56cf2ddc6c47f64d._comment create mode 100644 doc/forum/Sharing_annex_with_local_clones/comment_2_24ff2c1eb643077daa37c01644cebcd2._comment create mode 100644 doc/forum/Sharing_annex_with_local_clones/comment_3_5359b8eada24d27be83214ac0ae62f23._comment create mode 100644 doc/forum/Simple_check_out_with_assistant__63__.mdwn create mode 100644 doc/forum/Simple_check_out_with_assistant__63__/comment_1_ade8a0743ef1ec933c8a40ed64eeac2d._comment create mode 100644 doc/forum/Special_remote_without_chmod.mdwn create mode 100644 doc/forum/Special_remote_without_chmod/comment_1_4f5f9506cae72a1f321296fc5a5f339a._comment create mode 100644 doc/forum/Storing_uncontrolled_files_in_an_annex.mdwn create mode 100644 doc/forum/Storing_uncontrolled_files_in_an_annex/comment_1_175645a90be0c79221c129308adf643e._comment create mode 100644 doc/forum/Storing_uncontrolled_files_in_an_annex/comment_2_d29f214eadfe3bfd098bbc3bcf07129a._comment create mode 100644 doc/forum/Storing_uncontrolled_files_in_an_annex/comment_3_286b502e7906cca50e9e747db735bc88._comment create mode 100644 doc/forum/Stupid_mistake:_recoverable__63__.mdwn create mode 100644 doc/forum/Stupid_mistake:_recoverable__63__/comment_1_00ceb3a5e37825c4bbc806f532893706._comment create mode 100644 doc/forum/Stupid_mistake:_recoverable__63__/comment_2_cbedc29678d9b6af3b3c0bb1915d2391._comment create mode 100644 doc/forum/Stupid_mistake:_recoverable__63__/comment_3_86aa4d92a1330811862da1ba568b3037._comment create mode 100644 doc/forum/Stupid_mistake:_recoverable__63__/comment_4_6d15bf8a3c3c27cc92957070161675a9._comment create mode 100644 doc/forum/Stupid_mistake:_recoverable__63__/comment_5_f836b9b1d03d94c49e3798961790b2ba._comment create mode 100644 doc/forum/Sync_without_jabber_account.mdwn create mode 100644 doc/forum/Sync_without_jabber_account/comment_1_3e95ac2e67451f953cf0538094109f8b._comment create mode 100644 doc/forum/Synchronize_large_files___40__VM_images__41__.mdwn create mode 100644 doc/forum/Synchronize_large_files___40__VM_images__41__/comment_1_619f6ed2d7da5832ab253d61b6dd8044._comment create mode 100644 doc/forum/Syncing_machines_on_different_networks.mdwn create mode 100644 doc/forum/Syncing_machines_on_different_networks/comment_1_1c3523c722c178a96b096a68b9be4165._comment create mode 100644 doc/forum/Syncing_machines_on_different_networks/comment_2_d7b14ffee65072329cfe9ab08a0dba50._comment create mode 100644 doc/forum/Syncing_machines_on_different_networks/comment_3_65d1dae9b76fccb5f2b8fd8c69b60075._comment create mode 100644 doc/forum/Syncing_machines_on_different_networks/comment_4_2ec67428af69d6c0ea051c6a67d58905._comment create mode 100644 doc/forum/Syncing_machines_on_different_networks/comment_5_5ce093f82a2aad3fd8d7ccd5fdcab94f._comment create mode 100644 doc/forum/Syncing_machines_on_different_networks/comment_6_a55982c28d7b90e0b70ec2bb5e594e08._comment create mode 100644 doc/forum/Syncing_machines_on_different_networks/comment_7_c519d546e1a2a4e834609f3de3a605b0._comment create mode 100644 doc/forum/Syncing_machines_on_different_networks/comment_8_84a822238ddbaf211cce5f527c3559d3._comment create mode 100644 doc/forum/Syncronisation_of_syncronisation_between_3_repositories__63__.mdwn create mode 100644 doc/forum/Syncronisation_of_syncronisation_between_3_repositories__63__/comment_1_ca5192a26950627a1c2efcb55d6d2fa3._comment create mode 100644 doc/forum/The_ability_to_leave_a_file_unlocked_for_a_bit_while_committing_it_repeatedly__63__.mdwn create mode 100644 doc/forum/The_ability_to_leave_a_file_unlocked_for_a_bit_while_committing_it_repeatedly__63__/comment_1_3cbe520b184d323219cb402ff046c3b4._comment create mode 100644 doc/forum/The_ability_to_leave_a_file_unlocked_for_a_bit_while_committing_it_repeatedly__63__/comment_2_6afe7f593e955db2eefe87d9fa01882b._comment create mode 100644 doc/forum/The_ability_to_leave_a_file_unlocked_for_a_bit_while_committing_it_repeatedly__63__/comment_3_209399487fc4f76b29f03ad82dbc2d6f._comment create mode 100644 doc/forum/The_ability_to_leave_a_file_unlocked_for_a_bit_while_committing_it_repeatedly__63__/comment_4_f33fd6f72cb9ad7dd20a04c82199413b._comment create mode 100644 doc/forum/Transfer_remotes.mdwn create mode 100644 doc/forum/Transfer_remotes/comment_1_c08cf3bda00d7f20a3ca3d0fdba19c9c._comment create mode 100644 doc/forum/Transfer_remotes/comment_2_98930629d398329f1161135464a966a5._comment create mode 100644 doc/forum/Trouble_installing_from_cabal_on_debian-testing.mdwn create mode 100644 doc/forum/Trouble_installing_from_cabal_on_debian-testing/comment_1_0d3e9d7cffafc34bc212557e8bbb987d._comment create mode 100644 doc/forum/Truly_purging_dead_repositories.mdwn create mode 100644 doc/forum/Truly_purging_dead_repositories/comment_1_a4c75d49714b3543a9f1617a15d4a2d1._comment create mode 100644 doc/forum/Truly_purging_dead_repositories/comment_2_3da60a02e7323a204c5c5dd02ba04d6c._comment create mode 100644 doc/forum/Truly_purging_dead_repositories/comment_3_2576e45436008ff5a7ae5a38cade658e._comment create mode 100644 doc/forum/USB_backup_with_files_visible.mdwn create mode 100644 doc/forum/USB_backup_with_files_visible/comment_1_2832f8ae24dfb0f101e06f7c18283028._comment create mode 100644 doc/forum/USB_backup_with_files_visible/comment_2_6163e01aa441f8435091f026cc6da337._comment create mode 100644 doc/forum/USB_backup_with_files_visible/comment_3_ee92ff320eb5d9a031bdd1896aee0d86._comment create mode 100644 doc/forum/USB_backup_with_files_visible/comment_4_437c8342c0b65e3a89129800313eb73c._comment create mode 100644 doc/forum/USB_backup_with_files_visible/comment_5_5e10cffe8465ea4ecaa71c03a4c29ea4._comment create mode 100644 doc/forum/USB_drive_in_transfer_group_keeps_growing_-_assistant.txt create mode 100644 doc/forum/Ubuntu_PPA.mdwn create mode 100644 doc/forum/Ubuntu_PPA/comment_1_b55535258b1b4bcfc802235f0cba075d._comment create mode 100644 doc/forum/Ubuntu_PPA/comment_2_adc4d644fed058d1811acf0b35db9c18._comment create mode 100644 doc/forum/Ubuntu_PPA/comment_3_fc9cd51558c47718f243437202a11803._comment create mode 100644 doc/forum/Ubuntu_PPA/comment_4_3a8bbd0a7450a7f5323cd13144824aea._comment create mode 100644 doc/forum/Ubuntu_PPA/comment_5_2e1beaeebda0201c635db8b276cedf20._comment create mode 100644 doc/forum/Ubuntu_PPA/comment_6_bd99fb70399fc58d98781a89c6d38428._comment create mode 100644 doc/forum/Ubuntu_PPA/comment_7_c3f7ec8573934c59d70a48e36e321c13._comment create mode 100644 doc/forum/Undo_Git_Annex_Changes_To_Linked_Files.mdwn create mode 100644 doc/forum/Undo_Git_Annex_Changes_To_Linked_Files/comment_1_568dde820c2608d86d05b07444146a26._comment create mode 100644 doc/forum/Undo_Git_Annex_Changes_To_Linked_Files/comment_2_a8cf71cdf1217d9c8596cd9006eb83f5._comment create mode 100644 doc/forum/Unknown_remote_type_S3.mdwn create mode 100644 doc/forum/Unknown_remote_type_S3/comment_1_2aea2cd51286c809427d16519606cd37._comment create mode 100644 doc/forum/Unknown_remote_type_S3/comment_2_06f775062cd30767979fe56bcb3cf7bf._comment create mode 100644 doc/forum/Unlock_files_when_assistant_is_running__63__.mdwn create mode 100644 doc/forum/Unlock_files_when_assistant_is_running__63__/comment_1_3f4aadf0c856c81e15c6f5ae7f1992b4._comment create mode 100644 doc/forum/Unlock_files_when_assistant_is_running__63__/comment_2_a76797ee9e05e43af7947508cadd7bed._comment create mode 100644 doc/forum/Use_local_files_instead_of_re-downloading_from_S3_remote.mdwn create mode 100644 doc/forum/Use_local_files_instead_of_re-downloading_from_S3_remote/comment_1_cfb6021a36eee087705967a69967f327._comment create mode 100644 doc/forum/Use_local_files_instead_of_re-downloading_from_S3_remote/comment_2_7268b194ba72331858bc3274996b780e._comment create mode 100644 doc/forum/Use_reflinks_on_BTRFS_instead_of_symlinks___63__.mdwn create mode 100644 doc/forum/Using_Git-Annex___40__Assistant__41___to_manage_photos_with_Shotwell.mdwn create mode 100644 doc/forum/Using_Git-Annex___40__Assistant__41___to_manage_photos_with_Shotwell/comment_1_5e8d54daf6b7ff357619ac65fe39a2d7._comment create mode 100644 doc/forum/Using_Linux_static_builds.mdwn create mode 100644 doc/forum/Using_Linux_static_builds/comment_1_22fd266cbe68af3e754a10f1f1295e9b._comment create mode 100644 doc/forum/Using_Linux_static_builds/comment_2_36f69f30117ff8696425a754ab19a08b._comment create mode 100644 doc/forum/Using_Linux_static_builds/comment_3_64506833dad0202626239e00d1eb6490._comment create mode 100644 doc/forum/Using___34__sync__34___to_sink_all_branches__63__.mdwn create mode 100644 doc/forum/Using___34__sync__34___to_sink_all_branches__63__/comment_1_ef3d5c5e2600ffa36dd933c8a42cdf96._comment create mode 100644 doc/forum/Using___34__sync__34___to_sink_all_branches__63__/comment_2_424b0c6fdfe87ca08f5d408b7684ab08._comment create mode 100644 doc/forum/Using___34__sync__34___to_sink_all_branches__63__/comment_3_adaf9114c69f1268330adcebd8018fa0._comment create mode 100644 doc/forum/Using_for_Music_repo.mdwn create mode 100644 doc/forum/Using_for_Music_repo/comment_1_3488ed85ad98f14cb17f229225ece26e._comment create mode 100644 doc/forum/Using_for_Music_repo/comment_2_c794648878cfc77558f8db862271f997._comment create mode 100644 doc/forum/Using_for_Music_repo/comment_3_8c5e820f5ff7d717d64b1fd66927941b._comment create mode 100644 doc/forum/Using_git-annex_as_a_library.mdwn create mode 100644 doc/forum/Using_git-annex_as_a_library/comment_1_1f8e74c5856f21c53d5a91892cbef0c6._comment create mode 100644 doc/forum/Using_git-annex_as_a_library/comment_2_11a243fa7d8ac947aa9a798228dbd191._comment create mode 100644 doc/forum/Watch__47__assistant__47__webapp_documentation.mdwn create mode 100644 doc/forum/Watch__47__assistant__47__webapp_documentation/comment_1_adb377589dbae7fc91001df235c6b48e._comment create mode 100644 doc/forum/Webapp_on_ARM.mdwn create mode 100644 doc/forum/Webapp_on_ARM/comment_1_82ac40cef5b59070136527b8d81a5ce2._comment create mode 100644 doc/forum/Weird_behavior_with_OS_X_Finder_and_Preview.app.mdwn create mode 100644 doc/forum/Weird_behavior_with_OS_X_Finder_and_Preview.app/comment_1_8c8d86790a9d31518f9bb96a2d2dafee._comment create mode 100644 doc/forum/Weird_behavior_with_OS_X_Finder_and_Preview.app/comment_2_b538dc2c6f122b9ce5f7569de1b03f3e._comment create mode 100644 doc/forum/Weird_behavior_with_OS_X_Finder_and_Preview.app/comment_3_16e6724fa184392d4decbe0c4eb6efe6._comment create mode 100644 doc/forum/Weird_behavior_with_OS_X_Finder_and_Preview.app/comment_4_e514fe2d4d0ad6a10e281939e6ab4266._comment create mode 100644 doc/forum/Weird_behavior_with_OS_X_Finder_and_Preview.app/comment_5_e0eec765f72f7bf6f5a2a92c9b5dacad._comment create mode 100644 doc/forum/What_can_be_done_in_case_of_conflict.mdwn create mode 100644 doc/forum/What_can_be_done_in_case_of_conflict/comment_1_5ca86b099dfa08a50f656ea03bf1dcd9._comment create mode 100644 doc/forum/What_can_be_done_in_case_of_conflict/comment_2_69ee17959a92bb8359c0fd7b2a9d8dfb._comment create mode 100644 doc/forum/What_can_be_done_in_case_of_conflict/comment_3_017f4bac57a040c496e0c9d068dcfd9e._comment create mode 100644 doc/forum/What_happened_to_the_walkthrough__63__.mdwn create mode 100644 doc/forum/What_happened_to_the_walkthrough__63__/comment_1_70db0e3cfb1318e95671c23726e5541d._comment create mode 100644 doc/forum/What_happened_to_the_walkthrough__63__/comment_2_f9305dd19b9b5f35e66d915b8c30374b._comment create mode 100644 doc/forum/What_is_the_best_way_to___34__git_annex_mv__34___file__63__.mdwn create mode 100644 doc/forum/What_is_the_best_way_to___34__git_annex_mv__34___file__63__/comment_1_02d305f307b4d2ff7acd98cb36508a2f._comment create mode 100644 doc/forum/What_is_the_difference_between___34__local_computer__34___and___34__remote_server__34__.mdwn create mode 100644 doc/forum/What_is_the_difference_between___34__local_computer__34___and___34__remote_server__34__/comment_1_68734a118b7dc0c88ba67eca20953a55._comment create mode 100644 doc/forum/What_to_do_when_computer_is_lost_and_only_an_encrypted_ssh_remote_is_left__63__.mdwn create mode 100644 doc/forum/What_to_do_when_computer_is_lost_and_only_an_encrypted_ssh_remote_is_left__63__/comment_1_67ee446ca6d66e2c259ea771c2c9a2b2._comment create mode 100644 doc/forum/What_to_do_when_computer_is_lost_and_only_an_encrypted_ssh_remote_is_left__63__/comment_2_6d3cce3c8048e4aea8f0ed76473f6af1._comment create mode 100644 doc/forum/What_to_do_when_computer_is_lost_and_only_an_encrypted_ssh_remote_is_left__63__/comment_3_bd506e1ca7307660b3b9769eb97beddb._comment create mode 100644 doc/forum/Which_cloud_providers_are_supported__63___.mdwn create mode 100644 doc/forum/Which_cloud_providers_are_supported__63___/comment_1_1f9398840144e0452a2fed9336046547._comment create mode 100644 doc/forum/Why_can__39__t_encryption_be_enabled_for_removable_drives__63__.mdwn create mode 100644 doc/forum/Why_can__39__t_encryption_be_enabled_for_removable_drives__63__/comment_1_4341898d5ae4f09a5b06d24f5fe6192d._comment create mode 100644 doc/forum/Why_does_the_bup_remote_use___126____47__.bup__63__.mdwn create mode 100644 doc/forum/Why_does_the_bup_remote_use___126____47__.bup__63__/comment_1_da9c7c0e93aefc2da7409de5b138d86f._comment create mode 100644 doc/forum/Will_git-annex_solve_my_problem__63__.mdwn create mode 100644 doc/forum/Will_git-annex_solve_my_problem__63__/comment_1_35acbdd1a7727df204d776c2e8f02b53._comment create mode 100644 doc/forum/Will_git-annex_solve_my_problem__63__/comment_2_230256c19ac139dea207d89c06f70782._comment create mode 100644 doc/forum/Will_git_annex_work_on_a_FAT32_formatted_key__63__.mdwn create mode 100644 doc/forum/Will_git_annex_work_on_a_FAT32_formatted_key__63__/comment_1_426482e6eb3a27687a48f24f6ef2332f._comment create mode 100644 doc/forum/Will_git_annex_work_on_a_FAT32_formatted_key__63__/comment_2_af4f8b52526d8bea2904c95406fd2796._comment create mode 100644 doc/forum/Windows_support.mdwn create mode 100644 doc/forum/Windows_support/comment_1_23fa9aa3b00940a1c1b3876c35eef019._comment create mode 100644 doc/forum/Windows_usage_instructions.mdwn create mode 100644 doc/forum/Windows_usage_instructions/comment_1_d43dbd9406da3b9747b147715eca94ac._comment create mode 100644 doc/forum/Wishlist:_Bittorrent-like_transfers.mdwn create mode 100644 doc/forum/Wishlist:_Bittorrent-like_transfers/comment_1_13544d54fb0418af4ca9200cdb045d91._comment create mode 100644 doc/forum/Wishlist:_Bittorrent-like_transfers/comment_2_9a7dad35bf80c684ad97892420d7370c._comment create mode 100644 doc/forum/Wishlist:_Bittorrent-like_transfers/comment_3_e5de748bc5da12a4a01e08cde2407dd1._comment create mode 100644 doc/forum/Wishlist:_Bittorrent-like_transfers/comment_4_e51530178f1e034c0fdd5c9aa9945567._comment create mode 100644 doc/forum/Wishlist:_Bittorrent-like_transfers/comment_5_81ea9c129d8c02097f09ef8c68f1bb11._comment create mode 100644 doc/forum/Wishlist:_Bittorrent-like_transfers/comment_6_3b5798414f89686526da3dfa72c0c4f2._comment create mode 100644 doc/forum/Wishlist:_Don__39__t_make_files_readonly.mdwn create mode 100644 doc/forum/Wishlist:_Don__39__t_make_files_readonly/comment_1_7148527961e2d27793810966588c8d35._comment create mode 100644 doc/forum/Wishlist:_Is_it_possible_to___34__unlock__34___files_without_copying_the_file_data__63__.mdwn create mode 100644 doc/forum/Wishlist:_Is_it_possible_to___34__unlock__34___files_without_copying_the_file_data__63__/comment_1_1cf4ab29dfa2cff59b86305fc0018251._comment create mode 100644 doc/forum/Wishlist:_Is_it_possible_to___34__unlock__34___files_without_copying_the_file_data__63__/comment_2_f5ebb7f43dcef861ecc13373fb1e263f._comment create mode 100644 doc/forum/Wishlist:_Ways_of_selecting_files_based_on_meta-information.mdwn create mode 100644 doc/forum/Wishlist:_Ways_of_selecting_files_based_on_meta-information/comment_1_818f38aa988177d3a9415055e084f0fb._comment create mode 100644 doc/forum/Wishlist:_Ways_of_selecting_files_based_on_meta-information/comment_2_97e2ed48bd552d02918c4f98f963e6e1._comment create mode 100644 doc/forum/Wishlist:_automatic_reinject.mdwn create mode 100644 doc/forum/Wishlist:_getting_the_disk_used_by_a_subtree_of_files.mdwn create mode 100644 doc/forum/Wishlist:_getting_the_disk_used_by_a_subtree_of_files/comment_1_7abb1155081a23ce4829ee69b2064541._comment create mode 100644 doc/forum/Wishlist:_getting_the_disk_used_by_a_subtree_of_files/comment_2_b4c6ebada7526263e04c70eac312fda9._comment create mode 100644 doc/forum/Wishlist:_getting_the_disk_used_by_a_subtree_of_files/comment_3_ded71b270b94617a8ebb3a713d46a274._comment create mode 100644 doc/forum/Wishlist:_logging_to_file_when_running_as_a_daemon___40__for_the_assistant__41__.mdwn create mode 100644 doc/forum/Wishlist:_logging_to_file_when_running_as_a_daemon___40__for_the_assistant__41__/comment_1_42aa2b61b880f4048d874210212aa63b._comment create mode 100644 doc/forum/Wishlist:_logging_to_file_when_running_as_a_daemon___40__for_the_assistant__41__/comment_2_3e201039fa0e611554171ee30e69a414._comment create mode 100644 doc/forum/Wishlist:_logging_to_file_when_running_as_a_daemon___40__for_the_assistant__41__/comment_3_d1074724c44f3296cb438b2d526d8728._comment create mode 100644 doc/forum/Wishlist:_mark_remotes_offline.mdwn create mode 100644 doc/forum/Wishlist:_mark_remotes_offline/comment_1_9e3901f0123abb66034cce95cc5a941a._comment create mode 100644 doc/forum/Wishlist:_mark_remotes_offline/comment_2_d10e3d90cf421ae425e64ab266ea811b._comment create mode 100644 doc/forum/Wishlist:_options_for_syncing_meta-data_and_data.mdwn create mode 100644 doc/forum/Wishlist:_rename_files__47__dirs_w__47___special_characters_if_filesystem_is_FAT.mdwn create mode 100644 doc/forum/Wishlist:_rename_files__47__dirs_w__47___special_characters_if_filesystem_is_FAT/comment_1_5d33bcbd862537f53edd91dcff2b8977._comment create mode 100644 doc/forum/XBMC__44___NFS___38___git-annex_.txt create mode 100644 doc/forum/XBMC__44___NFS___38___git-annex_/comment_1_86480f31d410e903766f82e6ecf83e1c._comment create mode 100644 doc/forum/XBMC__44___NFS___38___git-annex_/comment_2_d8ed4dd51d3050db691a8abdec24cd42._comment create mode 100644 doc/forum/XBMC__44___NFS___38___git-annex_/comment_3_42b80ee51ce25775bf4532f53a8ecfe3._comment create mode 100644 doc/forum/XBMC__44___NFS___38___git-annex_/comment_4_01767f3f864954cf8080274e206da9d4._comment create mode 100644 doc/forum/XMPP_authentication_failure.mdwn create mode 100644 doc/forum/XMPP_authentication_failure/comment_1_19c7c3aa79d209d613d2e061e3129690._comment create mode 100644 doc/forum/XMPP_authentication_failure/comment_2_870059fed451e8377e5d382464ecc34b._comment create mode 100644 doc/forum/XMPP_authentication_failure/comment_3_1a7ff955e9173f13d10b75f203792384._comment create mode 100644 doc/forum/XMPP_authentication_failure/comment_4_d59031ebc0dd3abc1f4c96878328362c._comment create mode 100644 doc/forum/XMPP_authentication_failure/comment_5_c37ef477bef7efdb79dd05dce90dfde6._comment create mode 100644 doc/forum/XMPP_authentication_failure/comment_6_48cabea4c2caf5b3bd854df3aaa17d3d._comment create mode 100644 doc/forum/XMPP_authentication_failure/comment_7_14cd9b67806db93c3af055d88c9a910a._comment create mode 100644 doc/forum/XMPP_authentication_failure/comment_8_151d3fd7d3cceb30fd20a8f3bd54036c._comment create mode 100644 doc/forum/XMPP_authentication_failure/comment_9_fbb9eba65fbb72201f08511945fbcf8c._comment create mode 100644 doc/forum/XMPP_email_setup_says_wrong_password_but_it__39__s_correct._Can_I_provide_some_kind_of_debug_data__63__.mdwn create mode 100644 doc/forum/XMPP_email_setup_says_wrong_password_but_it__39__s_correct._Can_I_provide_some_kind_of_debug_data__63__/comment_1_1ba0735141fc6a21ac15913f4cacefae._comment create mode 100644 doc/forum/XMPP_email_setup_says_wrong_password_but_it__39__s_correct._Can_I_provide_some_kind_of_debug_data__63__/comment_2_16994dc86b87592fc62799e2d206d172._comment create mode 100644 doc/forum/XMPP_email_setup_says_wrong_password_but_it__39__s_correct._Can_I_provide_some_kind_of_debug_data__63__/comment_3_6afd424edc4095b8f71b136de2a9e64d._comment create mode 100644 doc/forum/XMPP_email_setup_says_wrong_password_but_it__39__s_correct._Can_I_provide_some_kind_of_debug_data__63__/comment_4_1381b6a927410642c6a93aa8354be791._comment create mode 100644 doc/forum/XMPP_email_setup_says_wrong_password_but_it__39__s_correct._Can_I_provide_some_kind_of_debug_data__63__/comment_5_c5b33c7a8aa8e6d0f9349510dac2366d._comment create mode 100644 doc/forum/XMPP_email_setup_says_wrong_password_but_it__39__s_correct._Can_I_provide_some_kind_of_debug_data__63__/comment_6_9913d2983ba2744ed24911f74988e4c7._comment create mode 100644 doc/forum/XMPP_email_setup_says_wrong_password_but_it__39__s_correct._Can_I_provide_some_kind_of_debug_data__63__/comment_7_ad6f385a2b95803eb9d81dfe76359551._comment create mode 100644 doc/forum/__34__du__34___equivalent_on_an_annex__63__.mdwn create mode 100644 doc/forum/__34__du__34___equivalent_on_an_annex__63__/comment_1_a41bd02361aa961e5285aeaf1ea062be._comment create mode 100644 doc/forum/__34__du__34___equivalent_on_an_annex__63__/comment_2_28ba62a546f5cc8f416491423d743d8a._comment create mode 100644 doc/forum/__34__du__34___equivalent_on_an_annex__63__/comment_3_8d97f40c1d14b7230f3656a00a99cf80._comment create mode 100644 doc/forum/__34__du__34___equivalent_on_an_annex__63__/comment_4_baa8fbbdd5c449a0dc2bb622cb4a47ce._comment create mode 100644 doc/forum/__34__du__34___equivalent_on_an_annex__63__/comment_5_2ee6cbbfe54a2e7b6e8eb539c18e663d._comment create mode 100644 doc/forum/__34__du__34___equivalent_on_an_annex__63__/comment_6_48f6a2761a34b7f991325f1d24e2c5ff._comment create mode 100644 doc/forum/__34__du__34___equivalent_on_an_annex__63__/comment_7_d632baff41b8582f1a79bc5018c68545._comment create mode 100644 doc/forum/__34__git_annex_copy_--to___60__REMOTE__62___.__34___doesn__39__t_send_it_to_working_directory..mdwn create mode 100644 doc/forum/__34__git_annex_copy_--to___60__REMOTE__62___.__34___doesn__39__t_send_it_to_working_directory./comment_1_0c0a5999a92bf5880f2113177dc67cc2._comment create mode 100644 doc/forum/__34__git_annex_copy_--to___60__REMOTE__62___.__34___doesn__39__t_send_it_to_working_directory./comment_2_c18083d9054f66f0bd51d63452af07eb._comment create mode 100644 doc/forum/__34__git_annex_lock__34___very_slow_for_big_repo.mdwn create mode 100644 doc/forum/__34__git_annex_lock__34___very_slow_for_big_repo/comment_1_044f1c5e5f7a939315c28087495a8ba8._comment create mode 100644 doc/forum/__34__git_annex_lock__34___very_slow_for_big_repo/comment_2_e854b93415d5ab80eda8e3be3b145ec2._comment create mode 100644 doc/forum/__34__git_annex_lock__34___very_slow_for_big_repo/comment_3_95c110500bc54013bc1969c1a9c8f842._comment create mode 100644 doc/forum/__34__next_time_Im_online_I_would_like_to_have_file_x_y_z__34__.mdwn create mode 100644 doc/forum/__34__next_time_Im_online_I_would_like_to_have_file_x_y_z__34__/comment_1_bfeb1446dee4d2f52ef25fabfb8cc8f6._comment create mode 100644 doc/forum/__34__next_time_Im_online_I_would_like_to_have_file_x_y_z__34__/comment_2_e60f2bbc1c058993472fd920edbc75fc._comment create mode 100644 doc/forum/__34__permission_denied__34___in_fsck_on_shared_repo.mdwn create mode 100644 doc/forum/__34__permission_denied__34___in_fsck_on_shared_repo/comment_1_3a5202ef2116ebb5559b6f4d920755fc._comment create mode 100644 doc/forum/__34__permission_denied__34___in_fsck_on_shared_repo/comment_2_86663eeb75b0477f53c45f26c8e4b051._comment create mode 100644 doc/forum/__34__permission_denied__34___in_fsck_on_shared_repo/comment_3_c336b2b07cd006d378e5be9639ff17ec._comment create mode 100644 doc/forum/__34__permission_denied__34___in_fsck_on_shared_repo/comment_4_1339cd27ca2955f30b01ecf4da7d6fe8._comment create mode 100644 doc/forum/__34__unable_to_resolve_reference_refs__47__heads__47__git-annex__34__.mdwn create mode 100644 doc/forum/__34__unable_to_resolve_reference_refs__47__heads__47__git-annex__34__/comment_1_e50188896df347f1d92e20a52053aa14._comment create mode 100644 doc/forum/__34__unable_to_resolve_reference_refs__47__heads__47__git-annex__34__/comment_2_d67793f7c969f64943d1fd54a1208c2b._comment create mode 100644 doc/forum/__34__unable_to_resolve_reference_refs__47__heads__47__git-annex__34__/comment_3_3523884833b5fd458a35f898797bf897._comment create mode 100644 doc/forum/__34__unable_to_resolve_reference_refs__47__heads__47__git-annex__34__/comment_4_02c32c2521ba1a1eaa19eaca7281f2a6._comment create mode 100644 doc/forum/__91__Installation__93___base-3.0.3.2_requires_syb___61____61__0.1.0.2.mdwn create mode 100644 doc/forum/__91__Installation__93___base-3.0.3.2_requires_syb___61____61__0.1.0.2/comment_1_fae6e88115d175239fc55cef4c33fb2c._comment create mode 100644 doc/forum/__91__Installation__93___base-3.0.3.2_requires_syb___61____61__0.1.0.2/comment_2_4c7a75638e8717132ccde949018d6008._comment create mode 100644 doc/forum/advantages_of_SHA__42___over_WORM.mdwn create mode 100644 doc/forum/advantages_of_SHA__42___over_WORM/comment_1_96c354cac4b5ce5cf6664943bc84db1d._comment create mode 100644 doc/forum/android_binary-only_download.mdwn create mode 100644 doc/forum/android_binary-only_download/comment_1_aab206e0bf0bb5ff47c7cc9795f12f92._comment create mode 100644 doc/forum/annexed_file_key_for_web_remote_with_SHA256E_backend.mdwn create mode 100644 doc/forum/annexed_file_key_for_web_remote_with_SHA256E_backend/comment_1_d1605a6e3b4d6863f4089218994ce564._comment create mode 100644 doc/forum/annexed_file_key_for_web_remote_with_SHA256E_backend/comment_2_d249ff27fa3d9ac3ca32485cdef49930._comment create mode 100644 doc/forum/archaeology_of_deleted_files.mdwn create mode 100644 doc/forum/archaeology_of_deleted_files/comment_1_48f27df03ec18d2c27cf6b70dcf71dc5._comment create mode 100644 doc/forum/archaeology_of_deleted_files/comment_2_c698cd10c8038bac45bd1049506a27c3._comment create mode 100644 doc/forum/archival_and_multiple_users.mdwn create mode 100644 doc/forum/archival_and_multiple_users/comment_1_fc4ee256f03a7c189d687caf4a34e21e._comment create mode 100644 doc/forum/archival_and_multiple_users/comment_2_a96d57d4bb567ac9b0b9167d5b1be011._comment create mode 100644 doc/forum/archival_and_multiple_users/comment_3_bd44634b04732ffb91154c61ef9cf828._comment create mode 100644 doc/forum/archival_and_multiple_users/comment_4_b89a56a5f1cd641f87925c7a5f74bcec._comment create mode 100644 doc/forum/archival_and_multiple_users/comment_5_81293bf5dc8ad4552712c2083fd589c9._comment create mode 100644 doc/forum/assistant_overzealously_moving_stuff_to_other_repos.mdwn create mode 100644 doc/forum/assistant_overzealously_moving_stuff_to_other_repos/comment_1_6bd240edf1868615024ff11c24c3d52c._comment create mode 100644 doc/forum/assistant_overzealously_moving_stuff_to_other_repos/comment_2_37c5e9a7669b5b94fbadb8792a765316._comment create mode 100644 doc/forum/assistant_overzealously_moving_stuff_to_other_repos/comment_3_87aa4c5942929be81ddc1e2795d56f0e._comment create mode 100644 doc/forum/assistant_without_watch__63__.mdwn create mode 100644 doc/forum/assistant_without_watch__63__/comment_1_be1f7c038426e53209a85ae1119269d5._comment create mode 100644 doc/forum/autobuilders_for_git-annex_to_aid_development.mdwn create mode 100644 doc/forum/autobuilders_for_git-annex_to_aid_development/comment_1_7e88f815e8d9652ef18ea6d54b118962._comment create mode 100644 doc/forum/autobuilders_for_git-annex_to_aid_development/comment_2_fef17a10226af5671495c2929653c337._comment create mode 100644 doc/forum/bainstorming:_git_annex_push___38___pull.mdwn create mode 100644 doc/forum/bainstorming:_git_annex_push___38___pull/comment_1_3a0bf74b51586354b7a91f8b43472376._comment create mode 100644 doc/forum/bainstorming:_git_annex_push___38___pull/comment_2_b02ca09914e788393c01196686f95831._comment create mode 100644 doc/forum/batch_check_on_remote_when_using_copy.mdwn create mode 100644 doc/forum/benefit_of_splitting_a_repository.mdwn create mode 100644 doc/forum/benefit_of_splitting_a_repository/comment_1_93a86cb03b66e7ab5dd7146e7b86c9e8._comment create mode 100644 doc/forum/benefit_of_splitting_a_repository/comment_2_4e2fed247298d620fee7be883a1e86a6._comment create mode 100644 doc/forum/can_git-annex_replace_ddm__63__.mdwn create mode 100644 doc/forum/can_git-annex_replace_ddm__63__/comment_1_aa05008dfe800474ff76678a400099e1._comment create mode 100644 doc/forum/can_git-annex_replace_ddm__63__/comment_2_008554306dd082d7f543baf283510e92._comment create mode 100644 doc/forum/can_git-annex_replace_ddm__63__/comment_3_4c69097fe2ee81359655e59a03a9bb8d._comment create mode 100644 doc/forum/cannot_fetch_from___40__gpg-encrypted__41___rsync_remote_on_MacOSX.mdwn create mode 100644 doc/forum/cannot_fetch_from___40__gpg-encrypted__41___rsync_remote_on_MacOSX/comment_1_21f0101447623f5a0cf9e72c3ff463bb._comment create mode 100644 doc/forum/cannot_fetch_from___40__gpg-encrypted__41___rsync_remote_on_MacOSX/comment_2_6234ca64bd03a0e15efbe8f5c204338a._comment create mode 100644 doc/forum/cannot_fetch_from___40__gpg-encrypted__41___rsync_remote_on_MacOSX/comment_3_5ac2b520a907e232984eb513ce088054._comment create mode 100644 doc/forum/cannot_fetch_from___40__gpg-encrypted__41___rsync_remote_on_MacOSX/comment_4_183dd1c29f66539193e7c0b73f329430._comment create mode 100644 doc/forum/cannot_fetch_from___40__gpg-encrypted__41___rsync_remote_on_MacOSX/comment_5_c920d04ffe332caed9d223fa0ac42746._comment create mode 100644 doc/forum/cannot_fetch_from___40__gpg-encrypted__41___rsync_remote_on_MacOSX/comment_6_7a3cf0853a8ec7b996e19b5e80145d21._comment create mode 100644 doc/forum/central_non-bare_and_git_push.txt create mode 100644 doc/forum/central_non-bare_and_git_push/comment_1_76d0c73c8985e860eb86333c63be6340._comment create mode 100644 doc/forum/clear_box.com_repository.mdwn create mode 100644 doc/forum/clear_box.com_repository/comment_1_2e839d8f974269c80a9fca183712350f._comment create mode 100644 doc/forum/clear_box.com_repository/comment_2_8f9c7248a148a24ae2aba39c4a79a6d1._comment create mode 100644 doc/forum/clear_box.com_repository/comment_3_f64ad21e5abfbf4e1f925b3d651bdba3._comment create mode 100644 doc/forum/clear_box.com_repository/comment_4_f8c06ac9b23b51cf18d362c260fc47a9._comment create mode 100644 doc/forum/clear_box.com_repository/comment_5_61d401b29322802cb25896503f3e6514._comment create mode 100644 doc/forum/cloud_services_to_support.mdwn create mode 100644 doc/forum/cloudcmd.mdwn create mode 100644 doc/forum/commit_current_workdir_state_in_direct_mode.mdwn create mode 100644 doc/forum/commit_current_workdir_state_in_direct_mode/comment_1_748481ff00374f570284bd4571584874._comment create mode 100644 doc/forum/confusion_with_remotes__44___map.mdwn create mode 100644 doc/forum/confusion_with_remotes__44___map/comment_1_a38ded23b7f288292a843abcb1a56f38._comment create mode 100644 doc/forum/confusion_with_remotes__44___map/comment_2_cd1c98b1276444e859a22c3dbd6f2a79._comment create mode 100644 doc/forum/confusion_with_remotes__44___map/comment_3_18531754089c991b6caefc57a5c17fe9._comment create mode 100644 doc/forum/confusion_with_remotes__44___map/comment_4_3b89b6d1518267fcbc050c9de038b9ca._comment create mode 100644 doc/forum/confusion_with_remotes__44___map/comment_5_27801584325d259fa490f67273f2ff71._comment create mode 100644 doc/forum/confusion_with_remotes__44___map/comment_6_496b0d9b86869bbac3a1356d53a3dda4._comment create mode 100644 doc/forum/confusion_with_remotes__44___map/comment_7_9a456f61f956a3d5e81e723d5a90794c._comment create mode 100644 doc/forum/dot_git_slash_annex_slash_tmp.mdwn create mode 100644 doc/forum/dot_git_slash_annex_slash_tmp/comment_10_14b74438bb1e3e02cff7926d774ba09a._comment create mode 100644 doc/forum/dot_git_slash_annex_slash_tmp/comment_1_1a35ef8cb89e0cd392f6e9fcee1fb92c._comment create mode 100644 doc/forum/dot_git_slash_annex_slash_tmp/comment_2_f4cc36c493d7c20fbaf949edd38e1252._comment create mode 100644 doc/forum/dot_git_slash_annex_slash_tmp/comment_3_69268f8aa29e807a56248f1fac86aa41._comment create mode 100644 doc/forum/dot_git_slash_annex_slash_tmp/comment_4_0ffb0c803c232a1587f956f16113aeb7._comment create mode 100644 doc/forum/dot_git_slash_annex_slash_tmp/comment_5_c303e28825241733d69fca74f2015fc6._comment create mode 100644 doc/forum/dot_git_slash_annex_slash_tmp/comment_6_3f0b376e37bd092b8d46c46bb1940e35._comment create mode 100644 doc/forum/dot_git_slash_annex_slash_tmp/comment_7_615641b3dd176d4b3a5bbfb521098e38._comment create mode 100644 doc/forum/dot_git_slash_annex_slash_tmp/comment_8_4600fa9234a787004ea0e0dbb36184b9._comment create mode 100644 doc/forum/dot_git_slash_annex_slash_tmp/comment_9_4f5cd0d0d4db0479c1ad86ffdc5ae434._comment create mode 100644 doc/forum/endless_password_prompt_loop.mdwn create mode 100644 doc/forum/endless_password_prompt_loop/comment_1_cceba12ed25cd671c7cee5a28631163e._comment create mode 100644 doc/forum/endless_password_prompt_loop/comment_2_f0cb86b45eb289f35197c43f83660a8f._comment create mode 100644 doc/forum/error_in_installation_of_base-4.5.0.0.mdwn create mode 100644 doc/forum/error_in_installation_of_base-4.5.0.0/comment_1_0b2f79c014e0dd9badd52b8b6aa47e0c._comment create mode 100644 doc/forum/error_in_installation_of_base-4.5.0.0/comment_2_3badd64e48fbb174cd7de1ac9589bedf._comment create mode 100644 doc/forum/error_in_installation_of_base-4.5.0.0/comment_3_d8190061ac1c683a7b699cf42e9db694._comment create mode 100644 doc/forum/error_in_installation_of_base-4.5.0.0/comment_4_49a4fcd2dc4f97d4055b5051feea5e3b._comment create mode 100644 doc/forum/example_of_massively_disconnected_operation.mdwn create mode 100644 doc/forum/exclude_files_from_annex.mdwn create mode 100644 doc/forum/exclude_files_from_annex/comment_1_82e7de5e631bae3b347815586274a936._comment create mode 100644 doc/forum/exclude_files_from_annex/comment_2_03d4599fdceb3dff184eed82824674bc._comment create mode 100644 doc/forum/expire_files__44___move_to_other_hosts.mdwn create mode 100644 doc/forum/expire_files__44___move_to_other_hosts/comment_1_ddcc2a00be1ae96a352d75a443458bcf._comment create mode 100644 doc/forum/expire_files__44___move_to_other_hosts/comment_2_7a4c3858c5eae409d04de3f9da43b57e._comment create mode 100644 doc/forum/exporting_annexed_files.mdwn create mode 100644 doc/forum/exporting_annexed_files/comment_1_e08e4c79588e17fb2f1cdf53d9fab7ea._comment create mode 100644 doc/forum/exporting_annexed_files/comment_2_15dc3024417b5b2ff3544a08beacab34._comment create mode 100644 doc/forum/exporting_annexed_files/comment_3_86f0e0f767a84a0f583e121d36cb7d48._comment create mode 100644 doc/forum/fail_to_git_annex_add_some_files:_getFileStatus:_does_not_exist__40__v_3.20111231__41__.mdwn create mode 100644 doc/forum/fail_to_git_annex_add_some_files:_getFileStatus:_does_not_exist__40__v_3.20111231__41__/comment_1_990197bf01351dc1ccbe1940d5084adb._comment create mode 100644 doc/forum/fail_to_git_annex_add_some_files:_getFileStatus:_does_not_exist__40__v_3.20111231__41__/comment_2_3bb1d21b7f0d0bd6d59190ae9d246d46._comment create mode 100644 doc/forum/fail_to_git_annex_add_some_files:_getFileStatus:_does_not_exist__40__v_3.20111231__41__/comment_3_692f268218690437138ae0540c879425._comment create mode 100644 doc/forum/first-time_setup_git-annex.mdwn create mode 100644 doc/forum/first-time_setup_git-annex/comment_1_a58d83ee3a7c2251d9a775847223f8ca._comment create mode 100644 doc/forum/flickrannex_--_not_sure_I_get_it.mdwn create mode 100644 doc/forum/flickrannex_--_not_sure_I_get_it/comment_1_57ea9f26760f970a70f09934d31a79b5._comment create mode 100644 doc/forum/flickrannex_--_not_sure_I_get_it/comment_2_ba93563b4ce1f6497a9f1d5e6eb0d1bb._comment create mode 100644 doc/forum/flickrannex_--_not_sure_I_get_it/comment_3_74f143965f48c89a3583acf1b6a7635a._comment create mode 100644 doc/forum/flickrannex_--_not_sure_I_get_it/comment_4_493bb86dedfa91ccc0c9be4045953ee4._comment create mode 100644 doc/forum/flickrannex_--_not_sure_I_get_it/comment_5_2c410aa478b21c0e6eb0e4d54bc8c362._comment create mode 100644 doc/forum/fsck_gives_false_positives.mdwn create mode 100644 doc/forum/fsck_gives_false_positives/comment_1_b91070218b9d5fb687eeee1f244237ad._comment create mode 100644 doc/forum/fsck_gives_false_positives/comment_2_f51c53f3f6e6ee1ad463992657db5828._comment create mode 100644 doc/forum/fsck_gives_false_positives/comment_3_692d6d4cd2f75a497e7d314041a768d2._comment create mode 100644 doc/forum/fsck_gives_false_positives/comment_4_7ceb395bf8a2e6a041ccd8de63b1b6eb._comment create mode 100644 doc/forum/fsck_gives_false_positives/comment_5_86484a504c3bbcecd5876982b9c95688._comment create mode 100644 doc/forum/fsck_gives_false_positives/comment_6_1d4fbbd212fa92967abda346323031f4._comment create mode 100644 doc/forum/gadu_-_git-annex_disk_usage.mdwn create mode 100644 doc/forum/gadu_-_git-annex_disk_usage/comment_10_f632a62c4dbbf01b29f146893d7725f9._comment create mode 100644 doc/forum/gadu_-_git-annex_disk_usage/comment_11_73461da2d55d040cb43e0db286975821._comment create mode 100644 doc/forum/gadu_-_git-annex_disk_usage/comment_12_6c4fb123091bde435c18ac3dfd5a9b77._comment create mode 100644 doc/forum/gadu_-_git-annex_disk_usage/comment_1_067d0ffe8900751bd2d2743254ac4d77._comment create mode 100644 doc/forum/gadu_-_git-annex_disk_usage/comment_2_ec8b57426e4d82c3392eb7dd683f2ddc._comment create mode 100644 doc/forum/gadu_-_git-annex_disk_usage/comment_3_38296fef5a2dc5794c2dc09df676b8c1._comment create mode 100644 doc/forum/gadu_-_git-annex_disk_usage/comment_4_1bcc94f9982c6cfd0888f3dba0f9221e._comment create mode 100644 doc/forum/gadu_-_git-annex_disk_usage/comment_5_4365cd3031456fac1b563ee72984638e._comment create mode 100644 doc/forum/gadu_-_git-annex_disk_usage/comment_6_2b03d7b857497cb811e992f85700cdcc._comment create mode 100644 doc/forum/gadu_-_git-annex_disk_usage/comment_7_03a4dfaf3bd73d41c6f3c3fab0a6a922._comment create mode 100644 doc/forum/gadu_-_git-annex_disk_usage/comment_8_fc6ddb4dc075ee42368863c1b026dbf7._comment create mode 100644 doc/forum/gadu_-_git-annex_disk_usage/comment_9_f03254e518cbdda73e4b88e72476275d._comment create mode 100644 doc/forum/get_and_copy_with_bare_repositories.mdwn create mode 100644 doc/forum/get_and_copy_with_bare_repositories/comment_1_a6e4628c0770e3f5e81348a6f29dd845._comment create mode 100644 doc/forum/get_and_copy_with_bare_repositories/comment_2_652fa1bae5c2bb63dcffcbda97a567c4._comment create mode 100644 doc/forum/getting_git_annex_to_do_a_force_copy_to_a_remote.mdwn create mode 100644 doc/forum/getting_git_annex_to_do_a_force_copy_to_a_remote/comment_1_3deb2c31cad37a49896f00d600253ee3._comment create mode 100644 doc/forum/getting_git_annex_to_do_a_force_copy_to_a_remote/comment_2_627f54d158d3ca4b72e45b4da70ff5cd._comment create mode 100644 doc/forum/getting_git_annex_to_do_a_force_copy_to_a_remote/comment_3_3f49dab11aae5df0c4eb5e4b8d741379._comment create mode 100644 doc/forum/git-annex_across_two_filesystems.mdwn create mode 100644 doc/forum/git-annex_across_two_filesystems/comment_1_53167648b8b70b41d19ca662a5f3687e._comment create mode 100644 doc/forum/git-annex_across_two_filesystems/comment_2_39adeebc1af9c437f1fc2e00c07509bf._comment create mode 100644 doc/forum/git-annex_across_two_filesystems/comment_3_f4e3f28db005301adeef7ccd2c9998fb._comment create mode 100644 doc/forum/git-annex_across_two_filesystems/comment_4_53fa7ac6f80e3281768a7bfd3d438b34._comment create mode 100644 doc/forum/git-annex_across_two_filesystems/comment_5_2e1be54c01970ef3456e8af4aaf00cbf._comment create mode 100644 doc/forum/git-annex_and_tagfs.mdwn create mode 100644 doc/forum/git-annex_and_tagfs/comment_1_887c74cb61d30198322ef74ebc80f950._comment create mode 100644 doc/forum/git-annex_build_for_Nokia_N9___40__Meego_Harmattan__41___and_Sailfish_OS.mdwn create mode 100644 doc/forum/git-annex_build_for_Nokia_N9___40__Meego_Harmattan__41___and_Sailfish_OS/comment_1_301a51c48c3d54f9d37feace26a772f8._comment create mode 100644 doc/forum/git-annex_communication_channels.mdwn create mode 100644 doc/forum/git-annex_communication_channels/comment_1_198325d2e9337c90f026396de89eec0e._comment create mode 100644 doc/forum/git-annex_communication_channels/comment_2_c7aeefa6ef9a2e75d8667b479ade1b7f._comment create mode 100644 doc/forum/git-annex_communication_channels/comment_3_1ff08a3e0e63fa0e560cbc9602245caa._comment create mode 100644 doc/forum/git-annex_communication_channels/comment_4_1ba6ddf54843c17c7d19a9996f2ab712._comment create mode 100644 doc/forum/git-annex_communication_channels/comment_5_404b723a681eb93fee015cea8024b6bc._comment create mode 100644 doc/forum/git-annex_communication_channels/comment_6_0d87d0e26461494b1d7f8a701a924729._comment create mode 100644 doc/forum/git-annex_communication_channels/comment_7_2c87c7a0648fe87c2bf6b4391f1cc468._comment create mode 100644 doc/forum/git-annex_on_OSX.mdwn create mode 100644 doc/forum/git-annex_on_Samba_share.mdwn create mode 100644 doc/forum/git-annex_on_Samba_share/comment_1_3e9cfdf2c088e48c967ad08f79966742._comment create mode 100644 doc/forum/git-annex_on_Samba_share/comment_2_9d3df393b7b727653598453d94dd33db._comment create mode 100644 doc/forum/git-annex_teams___47___groups.mdwn create mode 100644 doc/forum/git-annex_teams___47___groups/comment_1_0450673ab74f184a47ac7bab568d26dc._comment create mode 100644 doc/forum/git-assistant_clarification.mdwn create mode 100644 doc/forum/git-assistant_clarification/comment_1_8f553e59da12f798b854a457b96b5778._comment create mode 100644 doc/forum/git-assistant_clarification/comment_2_06cf62b599edea6ad8396776f0081494._comment create mode 100644 doc/forum/git-assistant_clarification/comment_3_36f0bd6e7a824e6ef40a309850bb087b._comment create mode 100644 doc/forum/git-remote-gcrypt.mdwn create mode 100644 doc/forum/git-remote-gcrypt/comment_1_175c8c35d9bbb470fcc17697eb8cc6b8._comment create mode 100644 doc/forum/git-remote-gcrypt/comment_2_fdcaf507e14c995636dd93a41e488df3._comment create mode 100644 doc/forum/git-remote-gcrypt/comment_3_f4e830f961dbe1c60ddd277b9d888133._comment create mode 100644 doc/forum/git-subtree_support__63__.mdwn create mode 100644 doc/forum/git-subtree_support__63__/comment_1_4f333cb71ed1ff259bbfd86704806aa6._comment create mode 100644 doc/forum/git-subtree_support__63__/comment_2_73d2a015b1ac79ec99e071a8b1e29034._comment create mode 100644 doc/forum/git-subtree_support__63__/comment_3_c533400e22c306c033fcd56e64761b0b._comment create mode 100644 doc/forum/git-subtree_support__63__/comment_4_75b0e072e668aa46ff0a8d62a6620306._comment create mode 100644 doc/forum/git-subtree_support__63__/comment_5_f5ec9649d9f1dc122e715de5533bc674._comment create mode 100644 doc/forum/git-subtree_support__63__/comment_6_85df530f7b6d76b74ac8017c6034f95e._comment create mode 100644 doc/forum/git_annex_add_crash_and_subsequent_recovery.mdwn create mode 100644 doc/forum/git_annex_add_crash_and_subsequent_recovery/comment_1_062d0153a379c1ba1df8585b90220d3d._comment create mode 100644 doc/forum/git_annex_add_crash_and_subsequent_recovery/comment_2_6fc6be43c488c468a4811cd0a1360225._comment create mode 100644 doc/forum/git_annex_add_crash_and_subsequent_recovery/comment_3_45efaaf27d9b580c4c75cbcdc4f65b64._comment create mode 100644 doc/forum/git_annex_add_crash_and_subsequent_recovery/comment_4_c560eae40867512b0af2cbef161fc8ac._comment create mode 100644 doc/forum/git_annex_alternative.mdwn create mode 100644 doc/forum/git_annex_assistant__44___share_with_other_devices.mdwn create mode 100644 doc/forum/git_annex_copy_--fast_--to_blah_much_slower_than_--from_blah.mdwn create mode 100644 doc/forum/git_annex_copy_--fast_--to_blah_much_slower_than_--from_blah/comment_1_5b6e0b749b01a97a6b52a2c1cca6e35a._comment create mode 100644 doc/forum/git_annex_copy_--fast_--to_blah_much_slower_than_--from_blah/comment_2_8f2567f4c4f6db2078211a87689757d3._comment create mode 100644 doc/forum/git_annex_copy_--fast_--to_blah_much_slower_than_--from_blah/comment_3_ab98121076b88f351fc8cd9197e6bf64._comment create mode 100644 doc/forum/git_annex_copy_--fast_--to_blah_much_slower_than_--from_blah/comment_4_cb13328add1b7a812efd817ad3dd1a4f._comment create mode 100644 doc/forum/git_annex_failing_to_get_non-English_filenames.__Rsync_problem__63__.mdwn create mode 100644 doc/forum/git_annex_failing_to_get_non-English_filenames.__Rsync_problem__63__/comment_1_292ee7c8b37cbd13f03eb67d0359b99e._comment create mode 100644 doc/forum/git_annex_failing_to_get_non-English_filenames.__Rsync_problem__63__/comment_2_f6341119fcfde5d8160c8f603b1a6fea._comment create mode 100644 doc/forum/git_annex_failing_to_get_non-English_filenames.__Rsync_problem__63__/comment_3_8ad3a1d1fe5995d61e5e137280bc76c3._comment create mode 100644 doc/forum/git_annex_failing_to_get_non-English_filenames.__Rsync_problem__63__/comment_4_86b61b0484f3f4ecff657e46333b3d4f._comment create mode 100644 doc/forum/git_annex_failing_to_get_non-English_filenames.__Rsync_problem__63__/comment_5_5ffac00d08d26acaba8c3513b24c4d65._comment create mode 100644 doc/forum/git_annex_get_creates_a_new_uuid.mdwn create mode 100644 doc/forum/git_annex_get_creates_a_new_uuid/comment_1_004c87183968c326058bd3159a5baa0b._comment create mode 100644 doc/forum/git_annex_ls___47___metadata_in_git_annex_whereis.mdwn create mode 100644 doc/forum/git_annex_ls___47___metadata_in_git_annex_whereis/comment_1_7fba10b85f4d9289c7782eccef46949e._comment create mode 100644 doc/forum/git_annex_ls___47___metadata_in_git_annex_whereis/comment_2_7dcec124ea7d0291ed40d80e2ffd5c7e._comment create mode 100644 doc/forum/git_pull_remote_git-annex.mdwn create mode 100644 doc/forum/git_pull_remote_git-annex/comment_1_9c245db3518d8b889ecdf5115ad9e053._comment create mode 100644 doc/forum/git_pull_remote_git-annex/comment_2_0f7f4a311b0ec1d89613e80847e69b42._comment create mode 100644 doc/forum/git_pull_remote_git-annex/comment_3_1aa89725b5196e40a16edeeb5ccfa371._comment create mode 100644 doc/forum/git_pull_remote_git-annex/comment_4_646f2077edcabc000a7d9cb75a93cf55._comment create mode 100644 doc/forum/git_pull_remote_git-annex/comment_5_4f2a05ef6551806dd0ec65372f183ca4._comment create mode 100644 doc/forum/git_pull_remote_git-annex/comment_6_3925d1aa56bce9380f712e238d63080f._comment create mode 100644 doc/forum/git_pull_remote_git-annex/comment_7_24c45ee981b18bc78325c768242e635d._comment create mode 100644 doc/forum/git_pull_remote_git-annex/comment_8_7e76ee9b6520cbffaf484c9299a63ad3._comment create mode 100644 doc/forum/git_tag_missing_for_3.20111011.mdwn create mode 100644 doc/forum/git_tag_missing_for_3.20111011/comment_1_7a53bf273f3078ab3351369ef2b5f2a6._comment create mode 100644 doc/forum/git_unannex_speed.mdwn create mode 100644 doc/forum/git_unannex_speed/comment_1_10cf326248f4e89e1f75bf97d7574763._comment create mode 100644 doc/forum/glacier_-_range_retrievals_and_daily_free_retrieval_allowance.mdwn create mode 100644 doc/forum/hashing_objects_directories.mdwn create mode 100644 doc/forum/hashing_objects_directories/comment_1_c55c56076be4f54251b0b7f79f28a607._comment create mode 100644 doc/forum/hashing_objects_directories/comment_2_504c96959c779176f991f4125ea22009._comment create mode 100644 doc/forum/hashing_objects_directories/comment_3_9134bde0a13aac0b6a4e5ebabd7f22e8._comment create mode 100644 doc/forum/hashing_objects_directories/comment_4_0de9170e429cbfea66f5afa8980d45ac._comment create mode 100644 doc/forum/hashing_objects_directories/comment_5_ef6cfd49d24c180c2d0a062e5bd3a0be._comment create mode 100644 doc/forum/help_running_git-annex_on_top_of_existing_repo.mdwn create mode 100644 doc/forum/help_running_git-annex_on_top_of_existing_repo/comment_1_4cb38d71c943657c5ba0896cd70d2e64._comment create mode 100644 doc/forum/help_running_git-annex_on_top_of_existing_repo/comment_2_b5e94c10ebbed9125c7e2332f75709ca._comment create mode 100644 doc/forum/help_running_git-annex_on_top_of_existing_repo/comment_3_2b3b93bbc60fbc24d436231954d6822a._comment create mode 100644 doc/forum/help_running_git-annex_on_top_of_existing_repo/comment_4_2dfda33ffa39b92b16c8bd9005e1cefe._comment create mode 100644 doc/forum/help_running_git-annex_on_top_of_existing_repo/comment_5_96b1eb1e8e9f315c646f4686870f9b52._comment create mode 100644 doc/forum/help_running_git-annex_on_top_of_existing_repo/comment_6_e85c3fa1d17f1d6ec625b9c4f9b698c3._comment create mode 100644 doc/forum/how_to_decrypt_file_from_encrypted_special_remote__63__.mdwn create mode 100644 doc/forum/how_to_decrypt_file_from_encrypted_special_remote__63__/comment_1_d4dc451892e7a6e230bf32adb7f3f9fa._comment create mode 100644 doc/forum/how_to_decrypt_file_from_encrypted_special_remote__63__/comment_2_79340bf3c0691073a9808c5ac2da0a3d._comment create mode 100644 doc/forum/how_to_decrypt_file_from_encrypted_special_remote__63__/comment_3_6302fb6e5bb7cbddf2cfe74d98d32897._comment create mode 100644 doc/forum/how_to_decrypt_file_from_encrypted_special_remote__63__/comment_4_e3d95bc09c9fb21e8e9bbacc642aa60f._comment create mode 100644 doc/forum/how_to_decrypt_file_from_encrypted_special_remote__63__/comment_5_f2f0a1c2fb0c6323707b11e2b06aa2db._comment create mode 100644 doc/forum/how_to_decrypt_file_from_encrypted_special_remote__63__/comment_6_66fe80e634a8f13cce18fe68974ec67a._comment create mode 100644 doc/forum/how_to_remove_files_and_symlinks_but_keep_historical_file_contents.mdwn create mode 100644 doc/forum/how_to_remove_files_and_symlinks_but_keep_historical_file_contents/comment_1_cba76311e146dabb8ffc789bf4c8b714._comment create mode 100644 doc/forum/how_to_remove_files_and_symlinks_but_keep_historical_file_contents/comment_2_8d99c50fc1347367ccc0714e8d1af385._comment create mode 100644 doc/forum/how_to_remove_files_and_symlinks_but_keep_historical_file_contents/comment_3_a7a9c55c2ad448179dff5d5b69976c7d._comment create mode 100644 doc/forum/incompatible_versions__63__.mdwn create mode 100644 doc/forum/incompatible_versions__63__/comment_1_629f28258746d413e452cbd42a1a43f4._comment create mode 100644 doc/forum/linux_standalone_tarballs.mdwn create mode 100644 doc/forum/linux_standalone_tarballs/comment_1_5c3ceb845a45e50784f7098bfbf94df1._comment create mode 100644 doc/forum/location_tracking_cleanup.mdwn create mode 100644 doc/forum/location_tracking_cleanup/comment_1_7d6319e8c94dfe998af9cfcbf170efb2._comment create mode 100644 doc/forum/location_tracking_cleanup/comment_2_e7395cb6e01f42da72adf71ea3ebcde4._comment create mode 100644 doc/forum/location_tracking_cleanup/comment_3_c15428cec90e969284a5e690fb4b2fde._comment create mode 100644 doc/forum/making_good_use_of_my_shiny_new_rsync.net_account.mdwn create mode 100644 doc/forum/making_good_use_of_my_shiny_new_rsync.net_account/comment_1_0ebe509b768d46081db2100f5b712ef7._comment create mode 100644 doc/forum/making_good_use_of_my_shiny_new_rsync.net_account/comment_2_ef63d893531d93d2eb09f48f8baff4dd._comment create mode 100644 doc/forum/man_pages_in_the_prebuilt_linux_tarball.mdwn create mode 100644 doc/forum/man_pages_in_the_prebuilt_linux_tarball/comment_1_a7bc2e84e6d7c0e2de5900685207af78._comment create mode 100644 doc/forum/managing_multiple_repositories.mdwn create mode 100644 doc/forum/migrate_existing_git_repository_to_git-annex.mdwn create mode 100644 doc/forum/migrate_existing_git_repository_to_git-annex/comment_1_4181bf34c71e2e8845e6e5fb55d53381._comment create mode 100644 doc/forum/migrate_existing_git_repository_to_git-annex/comment_2_5f08da5e21c0b3b5a8d1e4408c0d6405._comment create mode 100644 doc/forum/migrate_existing_git_repository_to_git-annex/comment_3_f483038c006cf7dcccf1014fa771744f._comment create mode 100644 doc/forum/migration_to_git-annex_and_rsync.mdwn create mode 100644 doc/forum/mistakenly_checked___42__files__42___into_an_annex.__bummer..mdwn create mode 100644 doc/forum/mistakenly_checked___42__files__42___into_an_annex.__bummer./comment_1_752db25abb647804a1cc12c5b247378a._comment create mode 100644 doc/forum/mistakenly_checked___42__files__42___into_an_annex.__bummer./comment_2_db6f4959c35732f72e7a90bd9f4c665c._comment create mode 100644 doc/forum/multiple_routes_to_same_repository.mdwn create mode 100644 doc/forum/multiple_routes_to_same_repository/comment_1_26c1734d41d5374f18fc688d862d6b8e._comment create mode 100644 doc/forum/multiple_routes_to_same_repository/comment_2_d119ab485fb2d5512c15372efdb2327d._comment create mode 100644 doc/forum/multiple_sym_links___40__for_tagging_photos__41____63__.mdwn create mode 100644 doc/forum/multiple_sym_links___40__for_tagging_photos__41____63__/comment_1_96beb9ea895c80285748adb940b4f57d._comment create mode 100644 doc/forum/multiple_sym_links___40__for_tagging_photos__41____63__/comment_2_985065c1feed9300631dac7a2701f669._comment create mode 100644 doc/forum/multiple_urls_for_the_same_UUID.mdwn create mode 100644 doc/forum/multiple_urls_for_the_same_UUID/comment_1_de7410d8824a864c4d106c9f1afaec3f._comment create mode 100644 doc/forum/multiple_urls_for_the_same_UUID/comment_2_309a86cf7e08448be64357a30d8b56ae._comment create mode 100644 doc/forum/multiple_urls_for_the_same_UUID/comment_3_fa97a45fc1392935fd5e0714db999bc2._comment create mode 100644 doc/forum/multiple_urls_for_the_same_UUID/comment_4_139178b1ba45b62eec0c89a660c0c81e._comment create mode 100644 doc/forum/new_microfeatures.mdwn create mode 100644 doc/forum/new_microfeatures/comment_1_058bd517c6fffaf3446b1f5d5be63623._comment create mode 100644 doc/forum/new_microfeatures/comment_2_41ad904c68e89c85e1fc49c9e9106969._comment create mode 100644 doc/forum/new_microfeatures/comment_3_a1a9347b5bc517f2a89a8b292c3f8517._comment create mode 100644 doc/forum/new_microfeatures/comment_4_5a6786dc52382fff5cc42fdb05770196._comment create mode 100644 doc/forum/new_microfeatures/comment_5_3c627d275586ff499d928a8f8136babf._comment create mode 100644 doc/forum/new_microfeatures/comment_6_31ea08c008500560c0b96c6601bc6362._comment create mode 100644 doc/forum/new_microfeatures/comment_7_94045b9078b1fff877933b012d1b49e2._comment create mode 100644 doc/forum/nfs_mounted_repo_results_in_errors_on_drop__47__move.mdwn create mode 100644 doc/forum/nntp__47__usenet_special_remote.mdwn create mode 100644 doc/forum/nntp__47__usenet_special_remote/comment_1_171a0b95b1f95cfd82073e88bdefaab9._comment create mode 100644 doc/forum/non-bare_repo_on_cloud_remote.mdwn create mode 100644 doc/forum/non-bare_repo_on_cloud_remote/comment_1_da0c023af7c78f1ef1cfe1143a900a9f._comment create mode 100644 doc/forum/non-bare_repo_on_cloud_remote/comment_2_71baea93f6caaf7b81a9ac00bee91e5e._comment create mode 100644 doc/forum/not_getting_file_contents.mdwn create mode 100644 doc/forum/not_getting_file_contents/comment_1_4a0f7f4de9c9bc4d13db033cb75d20af._comment create mode 100644 doc/forum/not_getting_file_contents/comment_2_dc7403e1b551552f9fd00da6a1453570._comment create mode 100644 doc/forum/one_annex_versus_many_annexes__63__.mdwn create mode 100644 doc/forum/one_or_many_annexes__63__.mdwn create mode 100644 doc/forum/one_or_many_annexes__63__/comment_1_656d96011801d67a45b0b3bb3d70fa63._comment create mode 100644 doc/forum/performance_and_multiple_replication_problems.mdwn create mode 100644 doc/forum/performance_and_multiple_replication_problems/comment_1_a2cdf1a4840f099f6bc941fd8de966c7._comment create mode 100644 doc/forum/performance_and_multiple_replication_problems/comment_2_e65b360706c66ede6e0e841b2ebbbfbc._comment create mode 100644 doc/forum/performance_improvement:_git_on_ssd__44___annex_on_spindle_disk.mdwn create mode 100644 doc/forum/performance_improvement:_git_on_ssd__44___annex_on_spindle_disk/comment_1_b3f22f9be02bc4f2d5a121db3d753ff5._comment create mode 100644 doc/forum/performance_improvement:_git_on_ssd__44___annex_on_spindle_disk/comment_2_f94abce32ef818176b42a3cc860691ae._comment create mode 100644 doc/forum/performance_improvement:_git_on_ssd__44___annex_on_spindle_disk/comment_3_0c8e77fe248e00bd990d568623e5a5c9._comment create mode 100644 doc/forum/performance_improvement:_git_on_ssd__44___annex_on_spindle_disk/comment_4_4b7e8f9521d61900d9ad418e74808ffb._comment create mode 100644 doc/forum/post-copy__47__sync_hook.mdwn create mode 100644 doc/forum/post-copy__47__sync_hook/comment_1_c8322d4b9bbf5eac80b48c312a42fbcf._comment create mode 100644 doc/forum/preferred_content_settings_for_multiple_symlinks.mdwn create mode 100644 doc/forum/preferred_content_settings_for_multiple_symlinks/comment_1_70da012d96ab576151fe3e081ef905d1._comment create mode 100644 doc/forum/preferred_content_settings_for_multiple_symlinks/comment_2_ccea74d8b5a4de1f3cd1f6da6694ae0e._comment create mode 100644 doc/forum/preferred_content_settings_for_multiple_symlinks/comment_3_fab70c642d5aaf26de05270860281030._comment create mode 100644 doc/forum/preferred_content_settings_for_multiple_symlinks/comment_4_3cbd06de53b6a13e2741124a8e7b5b5b._comment create mode 100644 doc/forum/preferred_content_settings_for_multiple_symlinks/comment_5_963558ab261d8a6315402d371e8348f9._comment create mode 100644 doc/forum/public-web-frontend.mdwn create mode 100644 doc/forum/public-web-frontend/comment_1_c73bd2dfe020c25eaad1c0707dd2db01._comment create mode 100644 doc/forum/public-web-frontend/comment_2_0026d7be6b17e50d86b3b54985882f80._comment create mode 100644 doc/forum/pulling_from_encrypted_remote.mdwn create mode 100644 doc/forum/pulling_from_encrypted_remote/comment_1_e9d6a9a6e01d01edb41a11b0da11d74d._comment create mode 100644 doc/forum/pulling_from_encrypted_remote/comment_2_8d0db2ff65ce935c6e68044a3e0721a8._comment create mode 100644 doc/forum/pure_git-annex_only_workflow.mdwn create mode 100644 doc/forum/pure_git-annex_only_workflow/comment_10_683768c9826b0bf0f267e8734b9eb872._comment create mode 100644 doc/forum/pure_git-annex_only_workflow/comment_11_6b541ed834ef45606f3b98779a25a148._comment create mode 100644 doc/forum/pure_git-annex_only_workflow/comment_12_ca8ca35d6cd4a9f94568536736c12adc._comment create mode 100644 doc/forum/pure_git-annex_only_workflow/comment_13_00c82d320c7b4bb51078beba17e14dc8._comment create mode 100644 doc/forum/pure_git-annex_only_workflow/comment_14_b63568b327215ef8f646a39d760fdfc0._comment create mode 100644 doc/forum/pure_git-annex_only_workflow/comment_15_cb7c856d8141b2de3cc95874753f1ee5._comment create mode 100644 doc/forum/pure_git-annex_only_workflow/comment_1_a32f7efd18d174845099a4ed59e6feae._comment create mode 100644 doc/forum/pure_git-annex_only_workflow/comment_2_66dc9b65523a9912411db03c039ba848._comment create mode 100644 doc/forum/pure_git-annex_only_workflow/comment_3_9b7d89da52f7ebb7801f9ec8545c3aba._comment create mode 100644 doc/forum/pure_git-annex_only_workflow/comment_4_dc8a3f75533906ad3756fcc47f7e96bb._comment create mode 100644 doc/forum/pure_git-annex_only_workflow/comment_5_afe5035a6b35ed2c7e193fb69cc182e2._comment create mode 100644 doc/forum/pure_git-annex_only_workflow/comment_6_3660d45c5656f68924acbd23790024ee._comment create mode 100644 doc/forum/pure_git-annex_only_workflow/comment_7_33db51096f568c65b22b4be0b5538c0d._comment create mode 100644 doc/forum/pure_git-annex_only_workflow/comment_8_6e5b42fdb7801daadc0b3046cbc3d51e._comment create mode 100644 doc/forum/pure_git-annex_only_workflow/comment_9_ace319652f9c7546883b5152ddc82591._comment create mode 100644 doc/forum/question_about_assistant_and___47__archive__47__.mdwn create mode 100644 doc/forum/question_about_assistant_and___47__archive__47__/comment_1_97890e26072af9277144651e3fdcada0._comment create mode 100644 doc/forum/question_about_assistant_and___47__archive__47__/comment_2_542bf265e35a976ac76767762d67d617._comment create mode 100644 doc/forum/question_about_assistant_and___47__archive__47__/comment_3_bafe99159df2adcd5fecc0d67bbf05a5._comment create mode 100644 doc/forum/question_about_assistant_and___47__archive__47__/comment_4_e77fa2992d9302a49a05f514c81612ca._comment create mode 100644 doc/forum/recover_deleted_files___63__.mdwn create mode 100644 doc/forum/recover_deleted_files___63__/comment_1_d7abb7c45c6ec2723a04f153ed215453._comment create mode 100644 doc/forum/recover_deleted_files___63__/comment_2_8ea2acaa30d3ee7e9f75310f4ec859b2._comment create mode 100644 doc/forum/recover_deleted_files___63__/comment_3_376de81c70799bf409be189a48234815._comment create mode 100644 doc/forum/recovering_from_repo_corruption.mdwn create mode 100644 doc/forum/recovering_from_repo_corruption/comment_1_01fc85037e24fc70e5c5329898cf6781._comment create mode 100644 doc/forum/recovering_from_repo_corruption/comment_2_3bd1c0bf25a0e892e711a60f53cd5298._comment create mode 100644 doc/forum/recovering_from_repo_corruption/comment_3_679dde8ca0081fc6854d6d2e8a42abdb._comment create mode 100644 doc/forum/reliability__47__completeness_of_XMPP_updates.mdwn create mode 100644 doc/forum/reliability__47__completeness_of_XMPP_updates/comment_1_e0f7aa48d54fc0564f41c3a569c723b7._comment create mode 100644 doc/forum/reliability__47__completeness_of_XMPP_updates/comment_2_4e74039a673c16c0163f2cfb406dc4c3._comment create mode 100644 doc/forum/reliability__47__completeness_of_XMPP_updates/comment_3_41ade4fe72804b2f06cd4dbf405c1746._comment create mode 100644 doc/forum/relying_on_git_for_numcopies.mdwn create mode 100644 doc/forum/relying_on_git_for_numcopies/comment_1_8ad3cccd7f66f6423341d71241ba89fc._comment create mode 100644 doc/forum/relying_on_git_for_numcopies/comment_2_be6acbc26008a9cb54e7b8f498f2c2a2._comment create mode 100644 doc/forum/relying_on_git_for_numcopies/comment_3_43d8e1513eb9947f8a503f094c03f307._comment create mode 100644 doc/forum/remote_server_client_repositories_are_bare__33____63__.mdwn create mode 100644 doc/forum/remote_server_client_repositories_are_bare__33____63__/comment_1_234241460f6c75a8376b303b8dd4e882._comment create mode 100644 doc/forum/remote_server_client_repositories_are_bare__33____63__/comment_2_42dfc382d07af2a4f29c76016084f87c._comment create mode 100644 doc/forum/reserving_space_with_directory_special_remotes.mdwn create mode 100644 doc/forum/reserving_space_with_directory_special_remotes/comment_1_cd17b624704d93b51931023f69573323._comment create mode 100644 doc/forum/reserving_space_with_directory_special_remotes/comment_2_877ca1be23d1484a8a30cdaeb6630053._comment create mode 100644 doc/forum/reserving_space_with_directory_special_remotes/comment_3_65910eeaf3c6fcfd03f22c2957293232._comment create mode 100644 doc/forum/retrieving_previous_versions.mdwn create mode 100644 doc/forum/retrieving_previous_versions/comment_1_a4e83f688d4ec9177e7bf520f12ed26d._comment create mode 100644 doc/forum/rsync_over_ssh__63__.mdwn create mode 100644 doc/forum/rsync_over_ssh__63__/comment_1_ee21f32e90303e20339e0a568321bbbe._comment create mode 100644 doc/forum/rsync_over_ssh__63__/comment_2_aa690da6ecfb2b30fc5080ad76dc77b1._comment create mode 100644 doc/forum/rsync_remote_is_not_available_from_a_cloned_repo/comment_1_2e340c5a6473f165dc06cc35db38e5c0._comment create mode 100644 doc/forum/safely_dropping_git-annex_history.mdwn create mode 100644 doc/forum/safely_dropping_git-annex_history/comment_10_a4b93a3fbc98d9b86e942f95e0039862._comment create mode 100644 doc/forum/safely_dropping_git-annex_history/comment_11_383882fafd32f25ed22b5eb2fb3691b9._comment create mode 100644 doc/forum/safely_dropping_git-annex_history/comment_1_4fd76d10a93fe01588fce7a621f9254d._comment create mode 100644 doc/forum/safely_dropping_git-annex_history/comment_2_10ecf3220ffcbbe94ba09da225458f18._comment create mode 100644 doc/forum/safely_dropping_git-annex_history/comment_3_e3beb8acb075faaeef6c052aecbf0a41._comment create mode 100644 doc/forum/safely_dropping_git-annex_history/comment_4_61a5fe2e7e47c60a8b237ea69404a37f._comment create mode 100644 doc/forum/safely_dropping_git-annex_history/comment_5_426d02e2f2a2ae4ec7eae02dfe4519b3._comment create mode 100644 doc/forum/safely_dropping_git-annex_history/comment_6_410a7296c2cee16d3d5bb618a5a41c1d._comment create mode 100644 doc/forum/safely_dropping_git-annex_history/comment_7_42cf492fc98a9eba8176387749ef12e0._comment create mode 100644 doc/forum/safely_dropping_git-annex_history/comment_8_c0327ada073d8b69535f71b4dc6aa57e._comment create mode 100644 doc/forum/safely_dropping_git-annex_history/comment_9_f83d6090aea2b7d5d54c876df940cbad._comment create mode 100644 doc/forum/seems_to_build_fine_on_haskell_platform_2011.mdwn create mode 100644 doc/forum/shared_cipher_tries_to_use_gpg.mdwn create mode 100644 doc/forum/shared_cipher_tries_to_use_gpg/comment_1_760961eaaa7d5c254dd71c5792437c9e._comment create mode 100644 doc/forum/shared_cipher_tries_to_use_gpg/comment_2_f3260aea3a5bb9b95a9bdf1d0dfce090._comment create mode 100644 doc/forum/something_really_good_happened_with_3.20130124.mdwn create mode 100644 doc/forum/something_really_good_happened_with_3.20130124/comment_1_1712bddd2f483a353f6313aa626445f1._comment create mode 100644 doc/forum/sparse_git_checkouts_with_annex.mdwn create mode 100644 doc/forum/sparse_git_checkouts_with_annex/comment_1_c7dc199c5740a0e7ba606dfb5e3e579a._comment create mode 100644 doc/forum/sparse_git_checkouts_with_annex/comment_2_e357db3ccc4079f07a291843975535eb._comment create mode 100644 doc/forum/sparse_git_checkouts_with_annex/comment_3_fcfafca994194d57dccf5319c7c9e646._comment create mode 100644 doc/forum/sparse_git_checkouts_with_annex/comment_4_04dc14880f31eee2b6d767d4d4258c5a._comment create mode 100644 doc/forum/special_remote_for_IMAP.mdwn create mode 100644 doc/forum/special_remote_for_IMAP/comment_1_7c7d4b57a1b6508fff1a6b0508c861f8._comment create mode 100644 doc/forum/special_remote_for_IMAP/comment_2_9c46fe8a857aa7a5ce797288144386bd._comment create mode 100644 doc/forum/special_remote_for_IMAP/comment_3_27e3b644df6942ce4c103236d0d5cb1b._comment create mode 100644 doc/forum/special_remote_for_iPods.mdwn create mode 100644 doc/forum/special_remote_for_iPods/comment_1_37cc3dc740341cc663074fd3bfb85947._comment create mode 100644 doc/forum/ssh_password.mdwn create mode 100644 doc/forum/ssh_password/comment_1_a3e5a41e1d4da683d577976b134b11ee._comment create mode 100644 doc/forum/ssh_password/comment_2_fa261676a99d49d4b237b0d43048d76d._comment create mode 100644 doc/forum/switching_backends.mdwn create mode 100644 doc/forum/switching_backends/comment_1_ecf4109c1148dafde3519243ae3c5a03._comment create mode 100644 doc/forum/switching_backends/comment_2_21f465a18f40b95dafd307fce0de659a._comment create mode 100644 doc/forum/switching_backends/comment_4_4c13d22c1695195e6b101bd20ef6bb42._comment create mode 100644 doc/forum/switching_backends/comment_4_e1d4a48baac23fd3f67b20eba4eee8af._comment create mode 100644 doc/forum/switching_to__47__from_direct_mode_while_assistant_is_running.mdwn create mode 100644 doc/forum/switching_to__47__from_direct_mode_while_assistant_is_running/comment_1_7832243a36613c48d0077b438dbf8b4a._comment create mode 100644 doc/forum/syncing_home_directories.mdwn create mode 100644 doc/forum/syncing_home_directories/comment_1_220a6e0ffe0ea610921a63c0a6e3beab._comment create mode 100644 doc/forum/syncing_non-git_trees_with_git-annex.mdwn create mode 100644 doc/forum/syncing_non-git_trees_with_git-annex/comment_1_7f9593bdfd95e4a8814e6cc5c44619e6._comment create mode 100644 doc/forum/syncing_non-git_trees_with_git-annex/comment_2_49f15478781a0ad5e46e75319070335c._comment create mode 100644 doc/forum/syncing_non-git_trees_with_git-annex/comment_3_6d8f399f0549eddd1d1f5c9c9a10c654._comment create mode 100644 doc/forum/taskwarrior.mdwn create mode 100644 doc/forum/taskwarrior/comment_1_1c3a29e7d292cb602d9d349f8009b51e._comment create mode 100644 doc/forum/tell_us_how_you__39__re_using_git-annex.mdwn create mode 100644 doc/forum/tell_us_how_you__39__re_using_git-annex/comment_1_4884803ddee7f642a3ac995a19967a6a._comment create mode 100644 doc/forum/tell_us_how_you__39__re_using_git-annex/comment_2_61f5054918e7b36c191454365bc7f3b7._comment create mode 100644 doc/forum/tell_us_how_you__39__re_using_git-annex/comment_3_db07e8703be606c998c831e91d300d69._comment create mode 100644 doc/forum/tell_us_how_you__39__re_using_git-annex/comment_4_a58595969cdd42ed20210e9615b42e42._comment create mode 100644 doc/forum/tips:_special__95__remotes__47__hook_with_tahoe-lafs.mdwn create mode 100644 doc/forum/tips:_special__95__remotes__47__hook_with_tahoe-lafs/comment_1_76bb33ce45ce6a91b86454147463193b._comment create mode 100644 doc/forum/tips:_special__95__remotes__47__hook_with_tahoe-lafs/comment_2_4d9b9d47d01d606a475678f630797bf9._comment create mode 100644 doc/forum/tips:_special__95__remotes__47__hook_with_tahoe-lafs/comment_3_8a812b11fcc2dc3b6fcf01cdbbb8459d._comment create mode 100644 doc/forum/tips:_special__95__remotes__47__hook_with_tahoe-lafs/comment_4_fc98c819bc5eb4d7c9e74d87fb4f6f3b._comment create mode 100644 doc/forum/tips:_special__95__remotes__47__hook_with_tahoe-lafs/comment_5_c459fb479fe7b13eaea2377cfc1923a6._comment create mode 100644 doc/forum/tips:_special__95__remotes__47__hook_with_tahoe-lafs/comment_6_2e9da5a919bbbc27b32de3b243867d4f._comment create mode 100644 doc/forum/tips:_special__95__remotes__47__hook_with_tahoe-lafs/comment_7_d636c868524b2055ee85832527437f90._comment create mode 100644 doc/forum/tips:_special__95__remotes__47__hook_with_tahoe-lafs/comment_8_39dc449cc60a787c3bfbfaaac6f9be0c._comment create mode 100644 doc/forum/ui.mdwn create mode 100644 doc/forum/ui/comment_1_f3e3446b05d6b573e29e6cad300fb635._comment create mode 100644 doc/forum/unannex_alternatives.mdwn create mode 100644 doc/forum/unannex_alternatives/comment_1_dcd4cd41280b41512bbdffafaf307993._comment create mode 100644 doc/forum/unannex_alternatives/comment_2_58a72a9fe0f58c7af0b4d7927a2dd21d._comment create mode 100644 doc/forum/unannex_alternatives/comment_3_b1687fc8f9e7744327bbeb6f0635d1cd._comment create mode 100644 doc/forum/unknown_response_from_git_cat-file.mdwn create mode 100644 doc/forum/unknown_response_from_git_cat-file/comment_1_f26ba569e715fe69b6de3093930362ee._comment create mode 100644 doc/forum/unlock__47__lock_always_gets_me.mdwn create mode 100644 doc/forum/unlock__47__lock_always_gets_me/comment_1_dee73a7ea3e1a5154601adb59782831f._comment create mode 100644 doc/forum/updating_the___34__number_of_copies__34__.mdwn create mode 100644 doc/forum/updating_the___34__number_of_copies__34__/comment_1_327bdb0d9c190c60c7147b3acf07af09._comment create mode 100644 doc/forum/updating_the___34__number_of_copies__34__/comment_2_7e11c839637e0894332e413cde02cee9._comment create mode 100644 doc/forum/updating_the___34__number_of_copies__34__/comment_3_8b7a70fb3bb41e4eda412302834730bb._comment create mode 100644 doc/forum/use_existing_ssh_keys__63__.mdwn create mode 100644 doc/forum/use_existing_ssh_keys__63__/comment_1_c420c53f022bbd1b28494bc44d076feb._comment create mode 100644 doc/forum/use_existing_ssh_keys__63__/comment_2_e4cae848e5701852073ced307832872b._comment create mode 100644 doc/forum/use_existing_ssh_keys__63__/comment_3_a97c20b6df74c49e5f57c7caf962f1e2._comment create mode 100644 doc/forum/using_git_annex_to_merge_and_synchronize_2_directories___40__like_unison__41__.mdwn create mode 100644 doc/forum/using_git_annex_to_merge_and_synchronize_2_directories___40__like_unison__41__/comment_1_5c3ee8a8aaa6d0918c0cc9683ce177ae._comment create mode 100644 doc/forum/using_git_annex_to_merge_and_synchronize_2_directories___40__like_unison__41__/comment_2_648946353c6d90c57351cce4010f1301._comment create mode 100644 doc/forum/version_3_upgrade.mdwn create mode 100644 doc/forum/version_3_upgrade/comment_1_05fc9c9cad26c520bebb98c852c71e35._comment create mode 100644 doc/forum/vlc_and_git-annex.mdwn create mode 100644 doc/forum/vlc_and_git-annex/comment_1_9c9ab8ce463cf74418aa2f385955f165._comment create mode 100644 doc/forum/vlc_and_git-annex/comment_2_037f94c1deeac873dbdb36cd4c927e45._comment create mode 100644 doc/forum/webapp_and_manual_mode.mdwn create mode 100644 doc/forum/webapp_and_manual_mode/comment_1_5b5df5ffeb6ee15779972f13fdc11729._comment create mode 100644 doc/forum/webapp_and_manual_mode/comment_2_a1f06b50d1317c78a301b47eb05d2617._comment create mode 100644 doc/forum/webapp_listen_port_with_autostart.mdwn create mode 100644 doc/forum/webapp_listen_port_with_autostart/comment_1_65dbcf3d8f6c16568f5a326242eab9c5._comment create mode 100644 doc/forum/windows_port__63__.mdwn create mode 100644 doc/forum/windows_port__63__/comment_1_23fa9aa3b00940a1c1b3876c35eef019._comment create mode 100644 doc/forum/wishlist:_get__47__drop_via_webapp_file_explorer.mdwn create mode 100644 doc/forum/wishlist:_get__47__drop_via_webapp_file_explorer/comment_1_c818a6d44dc13a56460b1865f70eb97c._comment create mode 100644 doc/forum/wishlist:_make_copy_stop_on_exhausted_disk_space.mdwn create mode 100644 doc/forum/wishlist:_make_copy_stop_on_exhausted_disk_space/comment_1_467e5e3db3e836030bc4b4f15846951f._comment create mode 100644 doc/forum/wishlist:_make_copy_stop_on_exhausted_disk_space/comment_2_e3ca3db9bea11d3e085ee9c3c56b33fe._comment create mode 100644 doc/forum/wishlist:_make_copy_stop_on_exhausted_disk_space/comment_3_0ef8c37350fc192d9b784fbab1d9f318._comment create mode 100644 doc/forum/working_without_git-annex_commits.mdwn create mode 100644 doc/future_proofing.mdwn create mode 100644 doc/git-annex-shell.mdwn create mode 100644 doc/git-annex.mdwn create mode 100644 doc/git-union-merge.mdwn create mode 100644 doc/how_it_works.mdwn create mode 100644 doc/how_it_works/comment_1_b3bdd6a06d5764db521ae54878131f5f._comment create mode 100644 doc/index.mdwn create mode 100644 doc/install.mdwn create mode 100644 doc/install/Android.mdwn create mode 100644 doc/install/Android/comment_1_f9ced494a530e6ae3e76cfbaddb89f5d._comment create mode 100644 doc/install/Android/comment_2_74cccae04ea23a8600069c7e658143aa._comment create mode 100644 doc/install/Android/comment_3_82c7cb31d19d4e18ca5548da5ca19a79._comment create mode 100644 doc/install/Android/comment_4_cebaa8ee5bbed27d9b2d032ca7bdec6e._comment create mode 100644 doc/install/Android/comment_5_40cb6cb72c4ad4aa19a4a40f41a6a757._comment create mode 100644 doc/install/Android/comment_6_b0f723538e7328d5070c563f070858bd._comment create mode 100644 doc/install/Android/comment_7_c6dc23d0e6f4138c4bf8e3452755676f._comment create mode 100644 doc/install/ArchLinux.mdwn create mode 100644 doc/install/ArchLinux/comment_1_da5919c986d2ae187bc2f73de9633978._comment create mode 100644 doc/install/Debian.mdwn create mode 100644 doc/install/Debian/comment_10_d5da996e106d2e4d8a822aa9bcc78596._comment create mode 100644 doc/install/Debian/comment_11_84283676da247c401bc9b4bb12c2b453._comment create mode 100644 doc/install/Debian/comment_12_0aca83b055d0a9dd8589c50250a8bbea._comment create mode 100644 doc/install/Debian/comment_13_167a091764e5e99ec0f35a65e95a22de._comment create mode 100644 doc/install/Debian/comment_1_029486088d098c2d4f1099f2f0e701a9._comment create mode 100644 doc/install/Debian/comment_2_648e3467e260cdf233acdb0b53313ce0._comment create mode 100644 doc/install/Debian/comment_3_4d922e11249627634ecc35bba4044d9e._comment create mode 100644 doc/install/Debian/comment_4_2a93ab18b05ccb90e7acc5885866fca2._comment create mode 100644 doc/install/Debian/comment_5_38e6399083e10a6a274f35bddc15d4ac._comment create mode 100644 doc/install/Debian/comment_6_2e7bbdbaabbfb9d89de22e913066e822._comment create mode 100644 doc/install/Debian/comment_7_1bccc7bf7a4ef61a9b30024b9b22ba7d._comment create mode 100644 doc/install/Debian/comment_8_5b5a3b0e8abe8831a6a15a4e258d14fd._comment create mode 100644 doc/install/Debian/comment_9_97eaed998ffd1ed79585075ed5cff06e._comment create mode 100644 doc/install/Fedora.mdwn create mode 100644 doc/install/Fedora/comment_1_c4db84e672ad4b45b522db735706b00f._comment create mode 100644 doc/install/Fedora/comment_2_f98c488c09bef86e2b0414589ce9e141._comment create mode 100644 doc/install/Fedora/comment_3_d872acf8865fe7c99a9b712db5b38ea4._comment create mode 100644 doc/install/FreeBSD.mdwn create mode 100644 doc/install/Gentoo.mdwn create mode 100644 doc/install/Linux_standalone.mdwn create mode 100644 doc/install/NixOS.mdwn create mode 100644 doc/install/OSX.mdwn create mode 100644 doc/install/OSX/comment_10_cd2120552ef894a37933b328136fa4cc._comment create mode 100644 doc/install/OSX/comment_11_740fa80e2e54e6fb570f820ff1f56440._comment create mode 100644 doc/install/OSX/comment_12_a84028080578a8b60115b6c4ef823627._comment create mode 100644 doc/install/OSX/comment_13_d6f1db401858ffea23c123db49f5b296._comment create mode 100644 doc/install/OSX/comment_14_035f856923276b0edad879e196e94097._comment create mode 100644 doc/install/OSX/comment_15_336e0acb00e84943715e69917643a69e._comment create mode 100644 doc/install/OSX/comment_16_1befafa862b7d07b1f6e57c0182497cf._comment create mode 100644 doc/install/OSX/comment_17_19c08b2c6c2c5cd88bf96d2bcbbd9055._comment create mode 100644 doc/install/OSX/comment_18_537fad5d8854e765499d47602d1ab398._comment create mode 100644 doc/install/OSX/comment_19_18d4377f4ded5604d395d73783ba82c9._comment create mode 100644 doc/install/OSX/comment_20_3e6a3c00444badf2cf7a9ee3d54af11e._comment create mode 100644 doc/install/OSX/comment_21_987f1302f56107c926b6daf83e124654._comment create mode 100644 doc/install/OSX/comment_22_6b5f44a98f9d37a1c6ecfe19a60fe6c5._comment create mode 100644 doc/install/OSX/comment_2_25552ff2942048fafe97d653757f1ad6._comment create mode 100644 doc/install/OSX/comment_3_47a77a03040fe628109bd54f82f9ad7a._comment create mode 100644 doc/install/OSX/comment_4_25cac8bcd84a5210fc0a5243260b8cc7._comment create mode 100644 doc/install/OSX/comment_4_bbe99673033e4c48c8bb3db24ee419f9._comment create mode 100644 doc/install/OSX/comment_5_39b4b748b4586bf32b37edfefef84bba._comment create mode 100644 doc/install/OSX/comment_6_1a9c91ef43edc4148947f202ff604114._comment create mode 100644 doc/install/OSX/comment_7_892f7e65f95f43697164267c4b71c0d5._comment create mode 100644 doc/install/OSX/comment_8_38d9c2eea1090674de2361274eab5b0e._comment create mode 100644 doc/install/OSX/comment_9_35bf3812db6f3ef25da9b3bc84f147c5._comment create mode 100644 doc/install/OSX/old_comments.mdwn create mode 100644 doc/install/OSX/old_comments/comment_10_4d15bfc4fc26e7249953bebfbb09e0aa._comment create mode 100644 doc/install/OSX/old_comments/comment_10_798000aab19af2944b6e44dbc550c6fe._comment create mode 100644 doc/install/OSX/old_comments/comment_11_707a1a27a15b2de8dfc8d1a30420ab4c._comment create mode 100644 doc/install/OSX/old_comments/comment_12_60d13f2c8e008af1041bea565a392c83._comment create mode 100644 doc/install/OSX/old_comments/comment_13_a6f48c87c2d6eabe379d6e10a6cac453._comment create mode 100644 doc/install/OSX/old_comments/comment_14_6ef2ddb7b11ce6ad54578ae118ed346e._comment create mode 100644 doc/install/OSX/old_comments/comment_15_6fd1fad5b6d9f36620e5a0e99edd2f89._comment create mode 100644 doc/install/OSX/old_comments/comment_16_af6fe3540032cdf4400478de87771058._comment create mode 100644 doc/install/OSX/old_comments/comment_17_8d3a0596db67108041728b20f2790f31._comment create mode 100644 doc/install/OSX/old_comments/comment_1_0a1760bf0db1f1ba89bdb4c62032f631._comment create mode 100644 doc/install/OSX/old_comments/comment_2_0327c64b15249596add635d26f4ce67f._comment create mode 100644 doc/install/OSX/old_comments/comment_2_7683740a98182de06cb329792e0c0a25._comment create mode 100644 doc/install/OSX/old_comments/comment_3_47c682a779812dda77601c24a619923c._comment create mode 100644 doc/install/OSX/old_comments/comment_3_733147cebe501c60f2141b711f1d7f24._comment create mode 100644 doc/install/OSX/old_comments/comment_3_b090f40fe5a32e00b472a5ab2b850b4a._comment create mode 100644 doc/install/OSX/old_comments/comment_3_fc092412e99cf4c5f095b0ef710bc4de._comment create mode 100644 doc/install/OSX/old_comments/comment_4_d513e21512a9b207983d38abf348d00f._comment create mode 100644 doc/install/OSX/old_comments/comment_4_d68c36432c7be3f4a76f4f0d7300bac9._comment create mode 100644 doc/install/OSX/old_comments/comment_4_e6109a964064a2a799768a370e57801d._comment create mode 100644 doc/install/OSX/old_comments/comment_5_50777853f808d57b957f8ce9a0f84b3d._comment create mode 100644 doc/install/OSX/old_comments/comment_5_626a4b4bf302d4ae750174f860402f70._comment create mode 100644 doc/install/OSX/old_comments/comment_6_18a8df794aa0ddd294dbf17d3d4c7fe2._comment create mode 100644 doc/install/OSX/old_comments/comment_7_2ce7acab15403d3f993cec94ec7f3bc6._comment create mode 100644 doc/install/OSX/old_comments/comment_8_a93ad4b67c5df4243268bcf32562f6be._comment create mode 100644 doc/install/OSX/old_comments/comment_9_ae3ed5345bc84f57e44251d2e6c39342._comment create mode 100644 doc/install/OSX/old_comments/comment_9_c6b1b31d16f2144ad08abd8c767b6ab9._comment create mode 100644 doc/install/ScientificLinux5.mdwn create mode 100644 doc/install/Ubuntu.mdwn create mode 100644 doc/install/Ubuntu/comment_1_d1c511153fe94bf33e19a1281f1c92f2._comment create mode 100644 doc/install/Ubuntu/comment_2_ad13886c1c1f76d1cd995ea7b7d8471c._comment create mode 100644 doc/install/Ubuntu/comment_3_a08817322739b03cf0fec97283b16f1a._comment create mode 100644 doc/install/Ubuntu/comment_4_fe0997e56136bd30749f0995cbf19b56._comment create mode 100644 doc/install/Ubuntu/comment_5_fbb5306a162db1a1ee9efa3523aac952._comment create mode 100644 doc/install/Ubuntu/comment_6_a97e7f0e62ac685c3ded423bddeaa67f._comment create mode 100644 doc/install/Ubuntu/comment_7_921a223fd7e679b9ced3d8ba5ce688e0._comment create mode 100644 doc/install/Ubuntu/comment_8_1f943cb084fa8e21bc6ee5fc3118f02f._comment create mode 100644 doc/install/Windows.mdwn create mode 100644 doc/install/cabal.mdwn create mode 100644 doc/install/cabal/comment_10_7ebe353b05d4df29897dc9a4f45c8a91._comment create mode 100644 doc/install/cabal/comment_11_0d06702e6e0ae3cd331cf748a9f6f273._comment create mode 100644 doc/install/cabal/comment_12_b93ca271dffca3f948645d3e1326c1d9._comment create mode 100644 doc/install/cabal/comment_13_3dac019cda71bf99878c0a1d9382323b._comment create mode 100644 doc/install/cabal/comment_1_f04df6bcd50d1d01eb34868bb00ac35c._comment create mode 100644 doc/install/cabal/comment_2_a69d17c55e56a707ec6606d5cdddee25._comment create mode 100644 doc/install/cabal/comment_3_55bed050bdb768543dbe1b86edec057d._comment create mode 100644 doc/install/cabal/comment_4_2ff7f8a3b03bea7e860248829d595bd1._comment create mode 100644 doc/install/cabal/comment_5_8789fc27466714faa5a3a7a6b8ec6e5d._comment create mode 100644 doc/install/cabal/comment_6_5afb2d081e8b603bc338cd460ad9317d._comment create mode 100644 doc/install/cabal/comment_7_129c4f2e404c874e5adfa52902a81104._comment create mode 100644 doc/install/cabal/comment_8_738c108f131e3aab0d720bc4fd6a81fd._comment create mode 100644 doc/install/cabal/comment_9_5ddbba419d96a7411f7edddaa4d7b739._comment create mode 100644 doc/install/fromscratch.mdwn create mode 100644 doc/install/openSUSE.mdwn create mode 100644 doc/internals.mdwn create mode 100644 doc/internals/hashing.mdwn create mode 100644 doc/internals/key_format.mdwn create mode 100644 doc/license.mdwn create mode 100644 doc/license/AGPL create mode 100644 doc/license/GPL create mode 100644 doc/license/LGPL create mode 100644 doc/links/key_concepts.mdwn create mode 100644 doc/links/other_stuff.mdwn create mode 100644 doc/links/the_details.mdwn create mode 100644 doc/location_tracking.mdwn create mode 100644 doc/logo-old-bw.svg create mode 100644 doc/logo-old.png create mode 100644 doc/logo-old.svg create mode 100644 doc/logo-old_small.png create mode 100644 doc/logo.mdwn create mode 100644 doc/logo.svg create mode 100644 doc/logo_small.png create mode 100644 doc/meta.mdwn create mode 100644 doc/news.mdwn create mode 100644 doc/news/LWN_article.mdwn create mode 100644 doc/news/Presentation_at_FOSDEM.mdwn create mode 100644 doc/news/sharebox_a_FUSE_filesystem_for_git-annex.mdwn create mode 100644 doc/news/sharebox_a_FUSE_filesystem_for_git-annex/comment_1_e238d1734238e37bb55ff952b32e06b8._comment create mode 100644 doc/news/version_4.20130621.mdwn create mode 100644 doc/news/version_4.20130627.mdwn create mode 100644 doc/news/version_4.20130709.mdwn create mode 100644 doc/news/version_4.20130723.mdwn create mode 100644 doc/news/version_4.20130802.mdwn create mode 100644 doc/not.mdwn create mode 100644 doc/not/comment_1_ab41bec1ccc884e71780cb9458439170._comment create mode 100644 doc/not/comment_2_0e19ff7deb5ed65f2bc685d4c516d816._comment create mode 100644 doc/not/comment_3_bab9584c41a25dda934ad230e3eb732d._comment create mode 100644 doc/not/comment_4_b2a0d5a45ab8ddd66c29dde9412d7a12._comment create mode 100644 doc/not/comment_5_f2829ecbe80a61aa9a8411d2403de69e._comment create mode 100644 doc/not/comment_6_547fc59b19ad66d7280c53a7f923ea08._comment create mode 100644 doc/not/comment_7_581e23cca0219711f8a4500a8d5d20fc._comment create mode 100644 doc/not/comment_8_5c61457f117de38ef487e5cc2780d554._comment create mode 100644 doc/preferred_content.mdwn create mode 100644 doc/preferred_content/comment_1_7d45e21dfb016e9ffa4715346dd0c1a6._comment create mode 100644 doc/preferred_content/comment_2_1ccd90b009245667ad59f4d29d2a3a37._comment create mode 100644 doc/preferred_content/comment_4_384025b5fa23a3f175985a081438149f._comment create mode 100644 doc/preferred_content/comment_4_6a9bc657bc7415f0e118357d8c6664c6._comment create mode 100644 doc/preferred_content/comment_5_f0a957e67297c4bb5a8778c11b3c9fd4._comment create mode 100644 doc/preferred_content/comment_6_b434c0e2aaa132020fd4a01551285376._comment create mode 100644 doc/preferred_content/comment_7_c4acaa237bf1a8512c5e8ea4cdbd11b9._comment create mode 100644 doc/preferred_content/comment_8_ff2a2dc9c566ebd9f570bdfcd7bfc030._comment create mode 100644 doc/privacy.mdwn create mode 100644 doc/related_software.mdwn create mode 100644 doc/repomap.png create mode 100644 doc/scalability.mdwn create mode 100644 doc/sidebar.mdwn create mode 100644 doc/sitemap.mdwn create mode 100644 doc/special_remotes.mdwn create mode 100644 doc/special_remotes/S3.mdwn create mode 100644 doc/special_remotes/S3/comment_10_c366f020c9b97a365e21878a33360079._comment create mode 100644 doc/special_remotes/S3/comment_11_c1da387e082d91feec13dde91ccb111a._comment create mode 100644 doc/special_remotes/S3/comment_12_59c3ecab7dbc8be53258460473cac21c._comment create mode 100644 doc/special_remotes/S3/comment_13_0789a21d980825188bb09f7fc8bba8be._comment create mode 100644 doc/special_remotes/S3/comment_14_29574a51d5831c51e2e765eb2c06e567._comment create mode 100644 doc/special_remotes/S3/comment_1_4a1f7a230dad6caa84831685b236fd73._comment create mode 100644 doc/special_remotes/S3/comment_2_5b22d67de946f4d34a4a3c7449d32988._comment create mode 100644 doc/special_remotes/S3/comment_3_bcab2bd0f168954243aa9bcc9671bd94._comment create mode 100644 doc/special_remotes/S3/comment_4_38c0b062997fde1ad28facc05d973e83._comment create mode 100644 doc/special_remotes/S3/comment_5_409bc2b56382417cf26bb222fb783ba7._comment create mode 100644 doc/special_remotes/S3/comment_6_78da9e233882ec0908962882ea8c4056._comment create mode 100644 doc/special_remotes/S3/comment_7_6af9781004d982d8e6b20a83ad29eead._comment create mode 100644 doc/special_remotes/S3/comment_8_0fa68d584ee7f6b5c9058fba7e911a11._comment create mode 100644 doc/special_remotes/S3/comment_9_7ad757b3865b04967c79af0a263bb3b0._comment create mode 100644 doc/special_remotes/bup.mdwn create mode 100644 doc/special_remotes/bup/comment_10_f78c1ed97d2e4c6ebffaa7482cfe0c9b._comment create mode 100644 doc/special_remotes/bup/comment_11_b53bceb0058acf4d1ab12ea4853ee443._comment create mode 100644 doc/special_remotes/bup/comment_12_65d923226cf6120349d807c5c60f640c._comment create mode 100644 doc/special_remotes/bup/comment_1_96179a003da4444f6fc08867872cda0a._comment create mode 100644 doc/special_remotes/bup/comment_2_612b038c15206f9f3c2e23c7104ca627._comment create mode 100644 doc/special_remotes/bup/comment_3_1186def82741ddab1ade256fb2e59e6f._comment create mode 100644 doc/special_remotes/bup/comment_4_7d22a805dd2914971e7ca628ceea69be._comment create mode 100644 doc/special_remotes/bup/comment_6_5942333cde09fd98e26c4f1d389cb76f._comment create mode 100644 doc/special_remotes/bup/comment_7_cb1a0d3076e9d06e7a24204478f6fa98._comment create mode 100644 doc/special_remotes/bup/comment_8_4cbc67e5911748d13cee3c483d7ece8a._comment create mode 100644 doc/special_remotes/bup/comment_9_ca7096a759961af375e6bd49663b45b3._comment create mode 100644 doc/special_remotes/comment_10_e9881290486a1770bd260f8650ada9c6._comment create mode 100644 doc/special_remotes/comment_11_e01b5cc5a0d81b071e93e27e7b91fe2a._comment create mode 100644 doc/special_remotes/comment_12_13237170ef5b6646e0e25d3421af3fe5._comment create mode 100644 doc/special_remotes/comment_13_1a36a0483a9db04d36e0234a192ebad8._comment create mode 100644 doc/special_remotes/comment_14_a8419963dc024b1d9eb73807596012dc._comment create mode 100644 doc/special_remotes/comment_15_95ccfdd22a2391daa99e0beb04adedd6._comment create mode 100644 doc/special_remotes/comment_16_b9d238fb15ad7628e33c90b071e07bb0._comment create mode 100644 doc/special_remotes/comment_17_cc21b81a8f809f6efa5f5b6332513fc3._comment create mode 100644 doc/special_remotes/comment_18_3fe750118ff1edbe91a110b86fb5b662._comment create mode 100644 doc/special_remotes/comment_19_6794eb52bd87c28ef1df3172aa7d5780._comment create mode 100644 doc/special_remotes/comment_1_961276c18e9353ca8e25cad53e7ec51f._comment create mode 100644 doc/special_remotes/comment_2_97543acfa7434e332ebea5672e446317._comment create mode 100644 doc/special_remotes/comment_3_9229776623c234204c8b164edff95da0._comment create mode 100644 doc/special_remotes/comment_4_3bbda479d13f6bf393dcd59ed94ddeaa._comment create mode 100644 doc/special_remotes/comment_5_f7000975d38077828ab11a99095b39eb._comment create mode 100644 doc/special_remotes/comment_6_5d2bd7c1e1493d3c3784708a9b0bc001._comment create mode 100644 doc/special_remotes/comment_7_af01ee5ce31b1490af565cb087d65277._comment create mode 100644 doc/special_remotes/comment_8_3d4ffec566d68d601eafe8758a616756._comment create mode 100644 doc/special_remotes/comment_9_26af468952f0403171370b56e127830a._comment create mode 100644 doc/special_remotes/directory.mdwn create mode 100644 doc/special_remotes/directory/comment_11_86f8c1b09cbd82bcd76378dfa1b3ca07._comment create mode 100644 doc/special_remotes/directory/comment_12._comment create mode 100644 doc/special_remotes/directory/comment_12_311cd013fd8db47856d84161119e059d._comment create mode 100644 doc/special_remotes/directory/comment_1_e8a53592adb13f7d7f212a2eb5a18a31._comment create mode 100644 doc/special_remotes/directory/comment_2_d949edad6a330079f9e15f703f9091e3._comment create mode 100644 doc/special_remotes/directory/comment_3_49009f4e9e335c9a9d0422aa59c9a432._comment create mode 100644 doc/special_remotes/directory/comment_4_f5e9b0b477c4e521f8633fd274757fa3._comment create mode 100644 doc/special_remotes/directory/comment_5_e790718423c41f5ea8047ea5225bfacd._comment create mode 100644 doc/special_remotes/directory/comment_6_325aac80b86588912c4fd61339ccbd0b._comment create mode 100644 doc/special_remotes/directory/comment_7_4206db69d68d9917623ce02500387021._comment create mode 100644 doc/special_remotes/directory/comment_8_acd9023511fe43817718bc89430f96c3._comment create mode 100644 doc/special_remotes/directory/comment_9_d330eb808a990bb71034613c297a265e._comment create mode 100644 doc/special_remotes/glacier.mdwn create mode 100644 doc/special_remotes/glacier/comment_1_fcd856b99dc6b3f9141b65fe639ef76b._comment create mode 100644 doc/special_remotes/glacier/comment_2_38fcca87074f6ea31a12569a822aa8c9._comment create mode 100644 doc/special_remotes/glacier/comment_3_cea5bcb162e4288847ba5f25464a0406._comment create mode 100644 doc/special_remotes/glacier/comment_4_0c92cc82c7ac513130f862391a02d329._comment create mode 100644 doc/special_remotes/glacier/comment_5_8d1dcb4bf48386314bfb248ea6eeeb68._comment create mode 100644 doc/special_remotes/hook.mdwn create mode 100644 doc/special_remotes/hook/comment_1_6a74a25891974a28a8cb42b87cb53c26._comment create mode 100644 doc/special_remotes/hook/comment_2_ee7c43b93c5b787216334f019643f6a0._comment create mode 100644 doc/special_remotes/hook/comment_3_2593291795e732994862d08bf2ed467b._comment create mode 100644 doc/special_remotes/hook/comment_4_35d79b5ffa5a19056efcdc805070bc4b._comment create mode 100644 doc/special_remotes/hook/comment_5_6fbf1e963fa3ea4b2eb8ca5a3819762d._comment create mode 100644 doc/special_remotes/hook/comment_6_e0ab48d5333e5de85f016b097e6fdac1._comment create mode 100644 doc/special_remotes/hook/comment_7_cc2b1243c2c36e63241513bcaddfea67._comment create mode 100644 doc/special_remotes/hook/comment_8_bbae315233bda48eb04662dfd48cf1ae._comment create mode 100644 doc/special_remotes/hook/comment_9_037523d1994c702239ca96791156fe65._comment create mode 100644 doc/special_remotes/rsync.mdwn create mode 100644 doc/special_remotes/rsync/comment_1_9e180c397486989beab21699b8e8f103._comment create mode 100644 doc/special_remotes/rsync/comment_2_25545dc0b53f09ae73b29899c8884b02._comment create mode 100644 doc/special_remotes/rsync/comment_3_960a89b1ae7e3888ffba06baa963dc21._comment create mode 100644 doc/special_remotes/rsync/comment_4_db84816c31239953dd21f23a8c557b43._comment create mode 100644 doc/special_remotes/rsync/comment_5_ccaffa4aded9dab88c76a856b96ea5b9._comment create mode 100644 doc/special_remotes/rsync/comment_6_e687b9482b177e1351c8c65ea617d3fa._comment create mode 100644 doc/special_remotes/web.mdwn create mode 100644 doc/special_remotes/webdav.mdwn create mode 100644 doc/special_remotes/webdav/comment_1_6b523eea78eae1d19fe2a9950ee33e3a._comment create mode 100644 doc/special_remotes/webdav/comment_2_83fc4e7d9ba7a05c8500da659f561b8f._comment create mode 100644 doc/special_remotes/webdav/comment_3_239367ad639c61ecdf87a89f7ac53efe._comment create mode 100644 doc/special_remotes/webdav/comment_4_ffa52f7776cdc8caa28667b5eadae123._comment create mode 100644 doc/special_remotes/webdav/comment_5_5b8cbdb5e9a1b90d748a5074997e1cd5._comment create mode 100644 doc/special_remotes/webdav/comment_6_d3be3e588c3a2abb2025ceb82c18b0ef._comment create mode 100644 doc/special_remotes/webdav/comment_7_6fa7e11331db5a943015bd5367eb3d73._comment create mode 100644 doc/special_remotes/webdav/comment_8_2627b41f80c7511b27464e2040b128a8._comment create mode 100644 doc/special_remotes/xmpp.mdwn create mode 100644 doc/special_remotes/xmpp/comment_1_568247938929a2934e8198fca80b7184._comment create mode 100644 doc/special_remotes/xmpp/comment_2_9fc3f512020b7eb2591d6b7b2e8de2d7._comment create mode 100644 doc/summary.mdwn create mode 100644 doc/sync.mdwn create mode 100644 doc/sync/comment_1_59681be5568f568f5c54eb0445163dd2._comment create mode 100644 doc/sync/comment_2_9301ff5e81d37475f594e74fbe32f24e._comment create mode 100644 doc/sync/comment_3_49560003da47490e4fabd4ab0089f2d7._comment create mode 100644 doc/sync/comment_4_cf29326408e62575085d1f980087c923._comment create mode 100644 doc/templates/bare.tmpl create mode 100644 doc/templates/bugtemplate.mdwn create mode 100644 doc/templates/walkthrough.tmpl create mode 100644 doc/testimonials.mdwn create mode 100644 doc/tips.mdwn create mode 100644 doc/tips/Building_git-annex_on_Debian_OR___37____164____35____34____164____37____38____34____35___Haskell__33__.mdwn create mode 100644 doc/tips/Building_git-annex_on_Debian_OR___37____164____35____34____164____37____38____34____35___Haskell__33__/comment_1_835a3608df3e9d044cabe822d0f3e7e4._comment create mode 100644 doc/tips/Building_git-annex_on_Debian_OR___37____164____35____34____164____37____38____34____35___Haskell__33__/comment_2_080b30cba72a718e73ea715e259e1cfb._comment create mode 100644 doc/tips/Decentralized_repository_behind_a_Firewall.mdwn create mode 100644 doc/tips/Decentralized_repository_behind_a_Firewall/comment_1_78b9035234a690ca5a7c9f3cc78fa092._comment create mode 100644 doc/tips/Delay_Assistant_Startup_on_Login.mdwn create mode 100644 doc/tips/Delay_Assistant_Startup_on_Login/comment_1_c63917150527efab4b1106183b3aa7ef._comment create mode 100644 doc/tips/Git_annex_and_Calibre.mdwn create mode 100644 doc/tips/How_to_retroactively_annex_a_file_already_in_a_git_repo.mdwn create mode 100644 doc/tips/How_to_retroactively_annex_a_file_already_in_a_git_repo/comment_1_7eaf73fb3355bd706ab18a43790b3c10._comment create mode 100644 doc/tips/How_to_retroactively_annex_a_file_already_in_a_git_repo/comment_2_dac1a171204f30d7c906e878eb6bd461._comment create mode 100644 doc/tips/How_to_retroactively_annex_a_file_already_in_a_git_repo/comment_3_b62ec0b848d2487d68d7032682622193._comment create mode 100644 doc/tips/How_to_retroactively_annex_a_file_already_in_a_git_repo/comment_4_2423904e41a86cd1c6bc155d7b733642._comment create mode 100644 doc/tips/Internet_Archive_via_S3.mdwn create mode 100644 doc/tips/Using_Git-annex_as_a_web_browsing_assistant.mdwn create mode 100644 doc/tips/Using_Git-annex_as_a_web_browsing_assistant/comment_1_74167f9fff400f148916003468c77de4._comment create mode 100644 doc/tips/assume-unstaged.mdwn create mode 100644 doc/tips/assume-unstaged/comment_1_44abd811ef79a85e557418e17a3927be._comment create mode 100644 doc/tips/automatically_getting_files_on_checkout.mdwn create mode 100644 doc/tips/beware_of_SSD_wear_when_doing_fsck_on_large_special_remotes.mdwn create mode 100644 doc/tips/beware_of_SSD_wear_when_doing_fsck_on_large_special_remotes/comment_1_e7c5c46112a2406b873d08bbf53c40d8._comment create mode 100644 doc/tips/beware_of_SSD_wear_when_doing_fsck_on_large_special_remotes/comment_2_daf45ce29fed986fa9aa8b173760d0b7._comment create mode 100644 doc/tips/centralised_repository:_starting_from_nothing.mdwn create mode 100644 doc/tips/centralised_repository:_starting_from_nothing/comment_1_b0d22822017646775869ce1292e676f4._comment create mode 100644 doc/tips/centralized_git_repository_tutorial.mdwn create mode 100644 doc/tips/downloading_podcasts.mdwn create mode 100644 doc/tips/downloading_podcasts/comment_10_4d4f6c22070b58918ee8d34c5e7290ad._comment create mode 100644 doc/tips/downloading_podcasts/comment_11_d8d77048c7e2524968c188e1ad517873._comment create mode 100644 doc/tips/downloading_podcasts/comment_12_0859317471b43c88744dd3df95c879f7._comment create mode 100644 doc/tips/downloading_podcasts/comment_13_e8c3c97282d17e2a1d47fb9d5e2b2f7b._comment create mode 100644 doc/tips/downloading_podcasts/comment_14_05a3694052de36848fbbad6eeeada895._comment create mode 100644 doc/tips/downloading_podcasts/comment_15_21028bed8858c2dae1ac9c2d014fd2a1._comment create mode 100644 doc/tips/downloading_podcasts/comment_16_4869fb5c9f896acc477c44de06c36ca7._comment create mode 100644 doc/tips/downloading_podcasts/comment_17_2e278ff200c1c15efd27c46a3e0aed40._comment create mode 100644 doc/tips/downloading_podcasts/comment_1_f04bc32a34baeeffcd691e9f7cce0230._comment create mode 100644 doc/tips/downloading_podcasts/comment_2_a9a98cad7358d16792853a2ee413fe6c._comment create mode 100644 doc/tips/downloading_podcasts/comment_3_5a8068a5cb0fd864581157a3aa5d1113._comment create mode 100644 doc/tips/downloading_podcasts/comment_4_e7072a9da30b4c4b4c526013144238d4._comment create mode 100644 doc/tips/downloading_podcasts/comment_5_79b3f8d678ac9f67df4c0cd649657283._comment create mode 100644 doc/tips/downloading_podcasts/comment_6_35106fee5458bdd5c21868fbc49d3616._comment create mode 100644 doc/tips/downloading_podcasts/comment_7_ceb16498b7aadbf04a27acd5d6561d46._comment create mode 100644 doc/tips/downloading_podcasts/comment_8_147397603f0b3fdb42ca387d1da7c5ef._comment create mode 100644 doc/tips/downloading_podcasts/comment_9_6a26a6cc7683d38fae0f23c5a52d1e23._comment create mode 100644 doc/tips/dropboxannex.mdwn create mode 100644 doc/tips/emacs_integration.mdwn create mode 100644 doc/tips/finding_duplicate_files.mdwn create mode 100644 doc/tips/finding_duplicate_files/comment_1_ddb477ca242ffeb21e0df394d8fdf5d2._comment create mode 100644 doc/tips/finding_duplicate_files/comment_2_900eafe0a781018ff44b35ac232e3ad3._comment create mode 100644 doc/tips/finding_duplicate_files/comment_3._comment create mode 100644 doc/tips/finding_duplicate_files/comment_4_1494143a74cc1e9fbe4720c14b73d42b._comment create mode 100644 doc/tips/finding_duplicate_files/comment_5_1a35ca360468bcb84a67ad8d62a2ef7d._comment create mode 100644 doc/tips/finding_duplicate_files/comment_6_a6e88c93b31f67c933523725ff61b287._comment create mode 100644 doc/tips/finding_duplicate_files/comment_7_347b0186755a809594bd42feda6363e2._comment create mode 100644 doc/tips/flickrannex.mdwn create mode 100644 doc/tips/flickrannex/comment_10_50707f259abe5829ce075dfbecd5a4ba._comment create mode 100644 doc/tips/flickrannex/comment_11_ab5bcb025381b3da4d7c6dfd0c7310dd._comment create mode 100644 doc/tips/flickrannex/comment_12_90a331275d888221bc695003c8acbe46._comment create mode 100644 doc/tips/flickrannex/comment_13_cf9dad91ee7d334c720adb3310aa0003._comment create mode 100644 doc/tips/flickrannex/comment_2_d74c4fc7edf8e47f7482564ce0ef4d12._comment create mode 100644 doc/tips/flickrannex/comment_2_f53d0d5520e2835e9705bea4e75556f0._comment create mode 100644 doc/tips/flickrannex/comment_4_9ebba4d61140f6c2071e988c9328cf7e._comment create mode 100644 doc/tips/flickrannex/comment_5_4470dae270613dd8712623474bc80ab0._comment create mode 100644 doc/tips/flickrannex/comment_5_d395cdcf815cb430e374ff05c1a63ff4._comment create mode 100644 doc/tips/flickrannex/comment_6_8cf730097001ffe106f2c743edce9d0a._comment create mode 100644 doc/tips/flickrannex/comment_7_a80c8087c4e1562a4c98a24edc182e5a._comment create mode 100644 doc/tips/flickrannex/comment_8_94f84254c32cf0f7dd1441b7da5d2bc6._comment create mode 100644 doc/tips/flickrannex/comment_9_5299b4cab4a4cb8e8fd4d2b39f0ea59c._comment create mode 100644 doc/tips/googledriveannex.mdwn create mode 100644 doc/tips/megaannex.mdwn create mode 100644 doc/tips/migrating_data_to_a_new_backend.mdwn create mode 100644 doc/tips/owncloudannex.mdwn create mode 100644 doc/tips/owncloudannex/comment_1_129652308c3c499462828dcaf8e747a4._comment create mode 100644 doc/tips/owncloudannex/comment_2_38604990368666f654d41891ba99ac61._comment create mode 100644 doc/tips/owncloudannex/comment_3_1bfd290d00d6536da7d31818db46f8ec._comment create mode 100644 doc/tips/owncloudannex/comment_4_492b6922a7c5bb5464fedb46b0c5303b._comment create mode 100644 doc/tips/owncloudannex/comment_5_1d48ac08714fadcb06d874570d745bd8._comment create mode 100644 doc/tips/owncloudannex/comment_6_65959f49a2f56bffd6fe48670c0c8d5a._comment create mode 100644 doc/tips/owncloudannex/comment_7_7482002991672ef67836bae43b8d0be8._comment create mode 100644 doc/tips/powerful_file_matching.mdwn create mode 100644 doc/tips/recover_data_from_lost+found.mdwn create mode 100644 doc/tips/replacing_Sparkleshare_or_dvcs-autosync_with_the_assistant.mdwn create mode 100644 doc/tips/replacing_Sparkleshare_or_dvcs-autosync_with_the_assistant/comment_1_907e4032ca4a39adb846cf16dbf447dc._comment create mode 100644 doc/tips/replacing_Sparkleshare_or_dvcs-autosync_with_the_assistant/comment_2_902d001ba86657ef0f8cca5b175f99ca._comment create mode 100644 doc/tips/replacing_Sparkleshare_or_dvcs-autosync_with_the_assistant/comment_3_a1cf93f9b29658f0f26e9e0ae6057ee3._comment create mode 100644 doc/tips/replacing_Sparkleshare_or_dvcs-autosync_with_the_assistant/comment_4_e10671908b58c554375787d0f76e2366._comment create mode 100644 doc/tips/replacing_Sparkleshare_or_dvcs-autosync_with_the_assistant/comment_5_4114380f66b6376c851e93f6876d590b._comment create mode 100644 doc/tips/setup_a_public_repository_on_a_web_site.mdwn create mode 100644 doc/tips/setup_a_public_repository_on_a_web_site/comment_1_1d0fa6da33e401df1d7ff31979247fec._comment create mode 100644 doc/tips/setup_a_public_repository_on_a_web_site/comment_2_b98b761dee9d923153e3c288c1d987ee._comment create mode 100644 doc/tips/skydriveannex.mdwn create mode 100644 doc/tips/untrusted_repositories.mdwn create mode 100644 doc/tips/using_Amazon_Glacier.mdwn create mode 100644 doc/tips/using_Amazon_S3.mdwn create mode 100644 doc/tips/using_Amazon_S3/comment_1_666a26f95024760c99c627eed37b1966._comment create mode 100644 doc/tips/using_Amazon_S3/comment_2_f5a0883be7dbb421b584c6dc0165f1ef._comment create mode 100644 doc/tips/using_Google_Cloud_Storage.mdwn create mode 100644 doc/tips/using_Google_Cloud_Storage/comment_1_c576182f39563ae68767391c4227a177._comment create mode 100644 doc/tips/using_box.com_as_a_special_remote.mdwn create mode 100644 doc/tips/using_git_annex_with_no_fixed_hostname_and_optimising_ssh.mdwn create mode 100644 doc/tips/using_git_annex_with_no_fixed_hostname_and_optimising_ssh/comment_1_c0b7682a2b6f3078457b85683c825baf._comment create mode 100644 doc/tips/using_gitolite_with_git-annex.mdwn create mode 100644 doc/tips/using_gitolite_with_git-annex/comment_10_8767bc8014b459a3cd76f275fd4fa8d6._comment create mode 100644 doc/tips/using_gitolite_with_git-annex/comment_11_00715e0b47f09130e0e536e29f7b9258._comment create mode 100644 doc/tips/using_gitolite_with_git-annex/comment_12_7027ce60265b8f24c8ab54553e544068._comment create mode 100644 doc/tips/using_gitolite_with_git-annex/comment_13_75218b7409c0e281cb01c9b2791e8cdf._comment create mode 100644 doc/tips/using_gitolite_with_git-annex/comment_1_9a2a2a8eac9af97e0c984ad105763a73._comment create mode 100644 doc/tips/using_gitolite_with_git-annex/comment_2_d8efea4ab9576555fadbb47666ecefa9._comment create mode 100644 doc/tips/using_gitolite_with_git-annex/comment_3_807035f38509ccb9f93f1929ecd37417._comment create mode 100644 doc/tips/using_gitolite_with_git-annex/comment_4_eb81f824aadc97f098379c5f7e4fba4c._comment create mode 100644 doc/tips/using_gitolite_with_git-annex/comment_5_f688309532d2993630e9e72e87fb9c46._comment create mode 100644 doc/tips/using_gitolite_with_git-annex/comment_6_3e203e010a4df5bf03899f867718adc5._comment create mode 100644 doc/tips/using_gitolite_with_git-annex/comment_7_f8fd08b6ab47378ad88c87348057220d._comment create mode 100644 doc/tips/using_gitolite_with_git-annex/comment_8_8249772c142117f88e37975d058aa936._comment create mode 100644 doc/tips/using_gitolite_with_git-annex/comment_9_28418635a6ed7231b89e02211cd3c236._comment create mode 100644 doc/tips/using_the_SHA1_backend.mdwn create mode 100644 doc/tips/using_the_web_as_a_special_remote.mdwn create mode 100644 doc/tips/using_the_web_as_a_special_remote/comment_1_321a41d611c6fe45e047af9c96c5176c._comment create mode 100644 doc/tips/using_the_web_as_a_special_remote/comment_2_dfe9c8c49aadff80d2020288584e0390._comment create mode 100644 doc/tips/using_the_web_as_a_special_remote/comment_3_ed8dd3bbd9b9ae7f2309b72b94f61eb1._comment create mode 100644 doc/tips/using_the_web_as_a_special_remote/comment_4_c1133a524989a940f1b5db588707157a._comment create mode 100644 doc/tips/visualizing_repositories_with_gource.mdwn create mode 100644 doc/tips/visualizing_repositories_with_gource/screenshot.jpg create mode 100644 doc/tips/what_to_do_when_a_repository_is_corrupted.mdwn create mode 100644 doc/tips/what_to_do_when_you_lose_a_repository.mdwn create mode 100644 doc/tips/what_to_do_when_you_lose_a_repository/comment_1_cf19b8dc304dc37c26717174c4a98aa4._comment create mode 100644 doc/tips/what_to_do_when_you_lose_a_repository/comment_3_fa9ca81668f5faebf2f61b10f82c97d2._comment create mode 100644 doc/tips/yet_another_simple_disk_usage_like_utility.mdwn create mode 100644 doc/tips/yet_another_simple_disk_usage_like_utility/comment_1_41b212bde8bc88d2a5dea93bd0dc75f1._comment create mode 100644 doc/todo.mdwn create mode 100644 doc/todo/A_really_simple_way_to_pair_devices_like_bittorent_sync.mdwn create mode 100644 doc/todo/A_really_simple_way_to_pair_devices_like_bittorent_sync/comment_1_d828bc374e50a49101c0b863f9b33080._comment create mode 100644 doc/todo/A_really_simple_way_to_pair_devices_like_bittorent_sync/comment_2_a4badfc248be428e6426a936212cc896._comment create mode 100644 doc/todo/A_really_simple_way_to_pair_devices_like_bittorent_sync/comment_3_0b04089d3d33fdb48eeb46bf168e9a3c._comment create mode 100644 doc/todo/A_really_simple_way_to_pair_devices_like_bittorent_sync/comment_4_2bcab1b7998b4df08fca41b8d810f115._comment create mode 100644 doc/todo/Bittorrent-like_features.mdwn create mode 100644 doc/todo/Build_for_Synology_DSM.mdwn create mode 100644 doc/todo/Build_for_Synology_DSM/comment_10_e351084d9a83db3fd6d9d983227a6410._comment create mode 100644 doc/todo/Build_for_Synology_DSM/comment_11_cc67a584f5c460a6fb63cf099c20e573._comment create mode 100644 doc/todo/Build_for_Synology_DSM/comment_12_94023593d294b9cf69090fcfd6ca0e5a._comment create mode 100644 doc/todo/Build_for_Synology_DSM/comment_13_314255fd503d125b5aeae2f62acfd592._comment create mode 100644 doc/todo/Build_for_Synology_DSM/comment_1_4059016fa8da6af7a3eba8966821e8eb._comment create mode 100644 doc/todo/Build_for_Synology_DSM/comment_2_8900c2985ab68b3b566c9f5d326471d6._comment create mode 100644 doc/todo/Build_for_Synology_DSM/comment_3_f2b77368473d42b7f21e9d51d6415b58._comment create mode 100644 doc/todo/Build_for_Synology_DSM/comment_4_a55fea734044c270ceb10adf9c8d9a76._comment create mode 100644 doc/todo/Build_for_Synology_DSM/comment_5_59865ada057c640ac29855c65cf45dd9._comment create mode 100644 doc/todo/Build_for_Synology_DSM/comment_6_6d860b1ad8816077b5fa596a71b12d5c._comment create mode 100644 doc/todo/Build_for_Synology_DSM/comment_7_19ef2d293ba3bc7ece443d7278371c3f._comment create mode 100644 doc/todo/Build_for_Synology_DSM/comment_8_609b7ad87dfbba49ec1f8c6fc2739ccd._comment create mode 100644 doc/todo/Build_for_Synology_DSM/comment_9_d94a73b9a07c5cadf191005f817fd59a._comment create mode 100644 doc/todo/Move_ssh_config_to___126____47__ssh__47__git-annex__47__config.mdwn create mode 100644 doc/todo/Move_ssh_config_to___126____47__ssh__47__git-annex__47__config/comment_1_284c806e83a32af81b02aea7c7bc285a._comment create mode 100644 doc/todo/Move_ssh_config_to___126____47__ssh__47__git-annex__47__config/comment_2_1f55ad6b39906458779b2d604b003ffe._comment create mode 100644 doc/todo/Move_ssh_config_to___126____47__ssh__47__git-annex__47__config/comment_3_b00dce2374aac6968317d05d23bcfaf7._comment create mode 100644 doc/todo/Move_ssh_config_to___126____47__ssh__47__git-annex__47__config/comment_4_743d0b077110c5cac1e2f47187b75333._comment create mode 100644 doc/todo/Please_abort_build_if___34__make_test__34___fails.mdwn create mode 100644 doc/todo/Please_add_support_for_monad-control_0.3.x.mdwn create mode 100644 doc/todo/S3.mdwn create mode 100644 doc/todo/Slow_transfer_for_a_lot_of_small_files..mdwn create mode 100644 doc/todo/Use_MediaScannerConnection_on_Android.mdwn create mode 100644 doc/todo/Use_a_remote_as_a_sharing_site_for_files_with_obfuscated_URLs.mdwn create mode 100644 doc/todo/Use_a_remote_as_a_sharing_site_for_files_with_obfuscated_URLs/comment_1_1a1f34f4f389267d67e79409c0ca8b1d._comment create mode 100644 doc/todo/__96__git_annex_import_--lazy__96___--_Delete_everything_that__39__s_in_the_source_directory_and_also_in_the_target_annex.mdwn create mode 100644 doc/todo/__96__git_annex_import_--lazy__96___--_Delete_everything_that__39__s_in_the_source_directory_and_also_in_the_target_annex/comment_1_0cc16eb17151309113cec6d1cccf203d._comment create mode 100644 doc/todo/add_--exclude_option_to_git_annex_find.mdwn create mode 100644 doc/todo/add_-all_option.mdwn create mode 100644 doc/todo/add_a_git_backend.mdwn create mode 100644 doc/todo/add_an_icon_for_the_.desktop_file.mdwn create mode 100644 doc/todo/add_metadata_to_annexed_files.mdwn create mode 100644 doc/todo/assistant_cannot_set_up_remote_repo_via_an_ssh_alias_or_an_ip_address.mdwn create mode 100644 doc/todo/assistant_git_sync_laddering.mdwn create mode 100644 doc/todo/assistant_parallel_file_transfers.txt create mode 100644 doc/todo/assistant_smarter_archive_directory_handling.mdwn create mode 100644 doc/todo/assistant_threaded_runtime.mdwn create mode 100644 doc/todo/auto_remotes.mdwn create mode 100644 doc/todo/auto_remotes/discussion.mdwn create mode 100644 doc/todo/automatic_bookkeeping_watch_command.mdwn create mode 100644 doc/todo/automatic_merge_of_synced_branches_upon___34__git_annex_sync__34__.mdwn create mode 100644 doc/todo/avoid_unnecessary_union_merges.mdwn create mode 100644 doc/todo/backendSHA1.mdwn create mode 100644 doc/todo/branching.mdwn create mode 100644 doc/todo/cache_key_info.mdwn create mode 100644 doc/todo/cache_key_info/comment_1_578df1b3b2cbfdc4aa1805378f35dc48._comment create mode 100644 doc/todo/checkout.mdwn create mode 100644 doc/todo/direct_mode_guard.mdwn create mode 100644 doc/todo/direct_mode_guard/comment_1_431b6e1577bbd30b07dce9002a8fe1a2._comment create mode 100644 doc/todo/done.mdwn create mode 100644 doc/todo/exclude_files_on_a_given_remote.mdwn create mode 100644 doc/todo/faster_gnupg_cipher.mdwn create mode 100644 doc/todo/faster_gnupg_cipher/comment_1_8f61f7c724a8224e61c015be68f43db7._comment create mode 100644 doc/todo/faster_gnupg_cipher/comment_2_36e1f227a320527653500b445f7c001c._comment create mode 100644 doc/todo/faster_rsync_remotes.mdwn create mode 100644 doc/todo/faster_rsync_remotes/comment_1_0bc3ee0ae563357675eeccf42461e59a._comment create mode 100644 doc/todo/faster_rsync_remotes/comment_2_ccf6f75450c89ca498c8130054f8d32d._comment create mode 100644 doc/todo/faster_rsync_remotes/comment_3_2f6a9d23cb8351fbf0f60ed93752e76e._comment create mode 100644 doc/todo/faster_rsync_remotes/comment_4_3a2f45defebae3dde336ee5f40c26d7e._comment create mode 100644 doc/todo/file_copy_progress_bar.mdwn create mode 100644 doc/todo/free_space_checking_for_local_special_remotes.mdwn create mode 100644 doc/todo/free_space_checking_for_local_special_remotes/comment_1_47c254cec58cbbb3ea84c93ef8282f01._comment create mode 100644 doc/todo/fsck.mdwn create mode 100644 doc/todo/fsck_special_remotes.mdwn create mode 100644 doc/todo/git-annex-shell.mdwn create mode 100644 doc/todo/git-annex_unused_eats_memory.mdwn create mode 100644 doc/todo/git_annex_init_:_include_repo_description_and__47__or_UUID_in_commit_message.mdwn create mode 100644 doc/todo/gitolite_and_gitosis_support.mdwn create mode 100644 doc/todo/gitrm.mdwn create mode 100644 doc/todo/hidden_files.mdwn create mode 100644 doc/todo/http_headers.mdwn create mode 100644 doc/todo/immutable_annexed_files.mdwn create mode 100644 doc/todo/incremental_fsck.mdwn create mode 100644 doc/todo/incremental_fsck/comment_1_609b21141dd5686b2c0eaef2b8d63229._comment create mode 100644 doc/todo/keep_annexed_files_for_a_while.mdwn create mode 100644 doc/todo/link_file_to_remote_repo_feature.mdwn create mode 100644 doc/todo/network_remotes.mdwn create mode 100644 doc/todo/object_dir_reorg_v2.mdwn create mode 100644 doc/todo/object_dir_reorg_v2/comment_1_ba03333dc76ff49eccaba375e68cb525._comment create mode 100644 doc/todo/object_dir_reorg_v2/comment_2_81276ac309959dc741bc90101c213ab7._comment create mode 100644 doc/todo/object_dir_reorg_v2/comment_3_79bdf9c51dec9f52372ce95b53233bb2._comment create mode 100644 doc/todo/object_dir_reorg_v2/comment_4_93aada9b1680fed56cc6f0f7c3aca5e5._comment create mode 100644 doc/todo/object_dir_reorg_v2/comment_5_821c382987f105da72a50e0a5ce61fdc._comment create mode 100644 doc/todo/object_dir_reorg_v2/comment_6_8834c3a3f1258c4349d23aff8549bf35._comment create mode 100644 doc/todo/object_dir_reorg_v2/comment_7_42501404c82ca07147e2cce0cff59474._comment create mode 100644 doc/todo/optimise_git-annex_merge.mdwn create mode 100644 doc/todo/optinally_transfer_file_unencryptedly.mdwn create mode 100644 doc/todo/optinally_transfer_file_unencryptedly/comment_1_4be47e7ac85d0f4e7029a96b615545a7._comment create mode 100644 doc/todo/parallel_possibilities.mdwn create mode 100644 doc/todo/parallel_possibilities/comment_1_d8e34fc2bc4e5cf761574608f970d496._comment create mode 100644 doc/todo/parallel_possibilities/comment_2_adb76f06a7997abe4559d3169a3181c3._comment create mode 100644 doc/todo/parallel_possibilities/comment_3_145fb974f45da99b7d4b117a3699cccf._comment create mode 100644 doc/todo/pushpull.mdwn create mode 100644 doc/todo/redundancy_stats_in_status.mdwn create mode 100644 doc/todo/redundancy_stats_in_status/comment_1_9f1c10f8cea4fa60a99cbcc8306dd5de._comment create mode 100644 doc/todo/resuming_encrypted_uploads.mdwn create mode 100644 doc/todo/resuming_encrypted_uploads/comment_1_1832a6fb78e8ad7c838582f46731ac3b._comment create mode 100644 doc/todo/resuming_encrypted_uploads/comment_2_2ecc8e782f49e90ed1549e9179eb1a1e._comment create mode 100644 doc/todo/rsync.mdwn create mode 100644 doc/todo/smudge.mdwn create mode 100644 doc/todo/smudge/comment_1_4ea616bcdbc9e9a6fae9f2e2795c31c9._comment create mode 100644 doc/todo/smudge/comment_2_e04b32caa0d2b4c577cdaf382a3ff7f6._comment create mode 100644 doc/todo/special_remote_for_amazon_glacier.mdwn create mode 100644 doc/todo/special_remote_for_amazon_glacier/comment_1_68f129441eefcbfebf7a9db680f52759._comment create mode 100644 doc/todo/special_remote_for_amazon_glacier/comment_2_c5eeaf8ceee414fa0379831ca52e290c._comment create mode 100644 doc/todo/speed_up_fsck.mdwn create mode 100644 doc/todo/stream_feature__63__.mdwn create mode 100644 doc/todo/support-non-utf8-locales.mdwn create mode 100644 doc/todo/support_S3_multipart_uploads.mdwn create mode 100644 doc/todo/support_for_lossy_remotes.mdwn create mode 100644 doc/todo/support_for_lossy_remotes/comment_1_f5cd9f9deab13ab2d2290ad763906dd3._comment create mode 100644 doc/todo/support_for_writing_external_special_remotes.mdwn create mode 100644 doc/todo/support_fsck_in_bare_repos.mdwn create mode 100644 doc/todo/symlink_farming_commit_hook.mdwn create mode 100644 doc/todo/sync_my_local_git-annex_from_a_dump_remote.mdwn create mode 100644 doc/todo/sync_my_local_git-annex_from_a_dump_remote/comment_1_81d63854f89f00855cda5ace0fc8262a._comment create mode 100644 doc/todo/sync_my_local_git-annex_from_a_dump_remote/comment_2_66822b72b1450e79e8edd0c6c21d5aa6._comment create mode 100644 doc/todo/tahoe_lfs_for_reals.mdwn create mode 100644 doc/todo/tahoe_lfs_for_reals/comment_1_0a4793ce6a867638f6e510e71dd4bb44._comment create mode 100644 doc/todo/tahoe_lfs_for_reals/comment_2_80b9e848edfdc7be21baab7d0cef0e3a._comment create mode 100644 doc/todo/union_mounting.mdwn create mode 100644 doc/todo/union_mounting/comment_1_cb08435812dd7766de26199c73f38e8b._comment create mode 100644 doc/todo/union_mounting/comment_2_240b1736f6bd4fbf87c372d3a46e661b._comment create mode 100644 doc/todo/untracked_remotes.mdwn create mode 100644 doc/todo/use_cp_reflink.mdwn create mode 100644 doc/todo/using_url_backend.mdwn create mode 100644 doc/todo/windows_support.mdwn create mode 100644 doc/todo/windows_support/comment_10_394127e34e07ab3dc0e7b94ee6898866._comment create mode 100644 doc/todo/windows_support/comment_1_3cc26ad8101a22e95a8c60cf0c4dedcc._comment create mode 100644 doc/todo/windows_support/comment_2_8acae818ce468967499050bbe3c532ea._comment create mode 100644 doc/todo/windows_support/comment_3_bd0a12f4c9b884ab8a06082842381a01._comment create mode 100644 doc/todo/windows_support/comment_4_ad06b98b2ddac866ffee334e41fee6a8._comment create mode 100644 doc/todo/windows_support/comment_5_444fc7251f57db241b6e80abae41851c._comment create mode 100644 doc/todo/windows_support/comment_6_34f1f60b570c389bb1e741b990064a7e._comment create mode 100644 doc/todo/windows_support/comment_7_a5ca56c487257434650420acfa60e39f._comment create mode 100644 doc/todo/windows_support/comment_8_61214de7d967740d42905f3823ce2f65._comment create mode 100644 doc/todo/windows_support/comment_9_259a0b1a6f4d8d1944173380adc5e7c8._comment create mode 100644 doc/todo/wishlist:_Add_to_Android_version_to_Google_Play.mdwn create mode 100644 doc/todo/wishlist:_Advanced_settings_for_xmpp_and_webdav.mdwn create mode 100644 doc/todo/wishlist:_Advanced_settings_for_xmpp_and_webdav/comment_1_11c7444ab4988c60732af505b52bde3c._comment create mode 100644 doc/todo/wishlist:_An_--all_option_for_dropunused.mdwn create mode 100644 doc/todo/wishlist:_An_--all_option_for_dropunused/comment_1_d8726d108b3b40116b4ec3c9935f2dff._comment create mode 100644 doc/todo/wishlist:_An_--all_option_for_dropunused/comment_2_578248f7686ba2d80d7dc8b17c0cdf52._comment create mode 100644 doc/todo/wishlist:_An_option_like_--git-dir.mdwn create mode 100644 doc/todo/wishlist:_An_option_like_--git-dir/comment_1_5d877d90b8bdf21d4b8649744d229efd._comment create mode 100644 doc/todo/wishlist:_An_option_like_--git-dir/comment_2_462264821cbc48a433330cbf7ec6044d._comment create mode 100644 doc/todo/wishlist:_An_option_like_--git-dir/comment_3_0c3709b07a0a1091ceeee73b69e0f7ac._comment create mode 100644 doc/todo/wishlist:_GnuPG_options.mdwn create mode 100644 doc/todo/wishlist:_GnuPG_options/comment_1_6662e8a71ce20acc62147ef41ecffa50._comment create mode 100644 doc/todo/wishlist:_Have_a_preview_of_download_or_upload_size.mdwn create mode 100644 doc/todo/wishlist:_Have_a_preview_of_download_or_upload_size/comment_1_019a2457e07377510feaa089a93bd76c._comment create mode 100644 doc/todo/wishlist:_Have_a_preview_of_download_or_upload_size/comment_3_29a154699339bf040af0ee8aa24034f1._comment create mode 100644 doc/todo/wishlist:_Have_a_preview_of_download_or_upload_size/comment_3_8f7e1c4a5c714cbd719ee170354d79fa._comment create mode 100644 doc/todo/wishlist:_Have_a_preview_of_download_or_upload_size/comment_4_c7335f757e5546aa841cab38fffe7605._comment create mode 100644 doc/todo/wishlist:_Have_a_preview_of_download_or_upload_size/comment_5_d2a845354f23d07880612740cf99ddd4._comment create mode 100644 doc/todo/wishlist:_Option_to_specify_max_transfer_rate.mdwn create mode 100644 doc/todo/wishlist:_Option_to_specify_max_transfer_rate/comment_1_4fd870e14b5b70c8a6ade41406294387._comment create mode 100644 doc/todo/wishlist:_Option_to_specify_max_transfer_rate/comment_2_dd854f297ad6a94b54be9f3edfd0f766._comment create mode 100644 doc/todo/wishlist:_Option_to_specify_max_transfer_rate/comment_3_a8b7e90a473d5937807cc7eb456efe33._comment create mode 100644 doc/todo/wishlist:_Prevent_repeated_password_prompts_for_one_command.mdwn create mode 100644 doc/todo/wishlist:_Prevent_repeated_password_prompts_for_one_command/comment_1_3f9c0d08932c2ede61c802a91261a1f7._comment create mode 100644 doc/todo/wishlist:_Provide_a___34__git_annex__34___command_that_will_skip_duplicates.mdwn create mode 100644 doc/todo/wishlist:_Provide_a___34__git_annex__34___command_that_will_skip_duplicates/comment_10_d78d79fb2f3713aa69f45d2691cf8dfe._comment create mode 100644 doc/todo/wishlist:_Provide_a___34__git_annex__34___command_that_will_skip_duplicates/comment_11_4316d9d74312112dc4c823077af7febe._comment create mode 100644 doc/todo/wishlist:_Provide_a___34__git_annex__34___command_that_will_skip_duplicates/comment_12_ed6d07f16a11c6eee7e3d5005e8e6fa3._comment create mode 100644 doc/todo/wishlist:_Provide_a___34__git_annex__34___command_that_will_skip_duplicates/comment_1_fd213310ee548d8726791d2b02237fde._comment create mode 100644 doc/todo/wishlist:_Provide_a___34__git_annex__34___command_that_will_skip_duplicates/comment_2_4394bde1c6fd44acae649baffe802775._comment create mode 100644 doc/todo/wishlist:_Provide_a___34__git_annex__34___command_that_will_skip_duplicates/comment_3_076cb22057583957d5179d8ba9004605._comment create mode 100644 doc/todo/wishlist:_Provide_a___34__git_annex__34___command_that_will_skip_duplicates/comment_4_f120d1e83c1a447f2ecce302fc69cf74._comment create mode 100644 doc/todo/wishlist:_Provide_a___34__git_annex__34___command_that_will_skip_duplicates/comment_5_5c30294b3c59fdebb1eef0ae5da4cd4f._comment create mode 100644 doc/todo/wishlist:_Provide_a___34__git_annex__34___command_that_will_skip_duplicates/comment_6_f24541ada1c86d755acba7e9fa7cff24._comment create mode 100644 doc/todo/wishlist:_Provide_a___34__git_annex__34___command_that_will_skip_duplicates/comment_7_c39f1bb7c61a89b238c61bee1c049767._comment create mode 100644 doc/todo/wishlist:_Provide_a___34__git_annex__34___command_that_will_skip_duplicates/comment_8_221ed2e53420278072a6d879c6f251d1._comment create mode 100644 doc/todo/wishlist:_Provide_a___34__git_annex__34___command_that_will_skip_duplicates/comment_9_aecfa896c97b9448f235bce18a40621d._comment create mode 100644 doc/todo/wishlist:_Restore_s3_files_moved_to_Glacier.mdwn create mode 100644 doc/todo/wishlist:_Tell_git_annex___40__assistant__41___which_files___40__not__41___to_annex_via_.gitattributes.mdwn create mode 100644 doc/todo/wishlist:___34__git_annex_add__34___multiple_processes.mdwn create mode 100644 doc/todo/wishlist:___34__git_annex_add__34___multiple_processes/comment_1_85b14478411a33e6186a64bd41f0910d._comment create mode 100644 doc/todo/wishlist:___34__git_annex_add__34___multiple_processes/comment_2_82e857f463cfdf73c70f6c0a9f9a31d6._comment create mode 100644 doc/todo/wishlist:___34__git_annex_add__34___multiple_processes/comment_3_8af85eba7472d9025c6fae4f03e3ad75._comment create mode 100644 doc/todo/wishlist:___34__quiet__34___annex_get_for_centralized_use_case.mdwn create mode 100644 doc/todo/wishlist:___34__quiet__34___annex_get_for_centralized_use_case/comment_1_5c8812973cf91b046e7fc44d7e86c78e._comment create mode 100644 doc/todo/wishlist:___34__quiet__34___annex_get_for_centralized_use_case/comment_2_f36b6a5b128423211aac91a252ecf85f._comment create mode 100644 doc/todo/wishlist:___34__quiet__34___annex_get_for_centralized_use_case/comment_3_ad1569b2405acacd2e37f42b82f24c88._comment create mode 100644 doc/todo/wishlist:___34__quiet__34___annex_get_for_centralized_use_case/comment_4_8aba90150fe178ce9712ad951628f3d6._comment create mode 100644 doc/todo/wishlist:___34__quiet__34___annex_get_for_centralized_use_case/comment_5_6f42d240e0021f4dfa37146bea3f5d7e._comment create mode 100644 doc/todo/wishlist:___34__quiet__34___annex_get_for_centralized_use_case/comment_6_5fda455febf728b079f26fe42bf7bcab._comment create mode 100644 doc/todo/wishlist:___34__quiet__34___annex_get_for_centralized_use_case/comment_7_f1052ab997f1a2cccbabfd1533fc0a59._comment create mode 100644 doc/todo/wishlist:___34__quiet__34___annex_get_for_centralized_use_case/comment_8_07804647b6023436878756bd97a23f32._comment create mode 100644 doc/todo/wishlist:___39__whereis__39___support_in_the_webapp.mdwn create mode 100644 doc/todo/wishlist:___96__git_annex_fsck_--checksums__96___--_verify_checksums_but_disregard_annex.numcopies.mdwn create mode 100644 doc/todo/wishlist:___96__git_annex_fsck_--checksums__96___--_verify_checksums_but_disregard_annex.numcopies/comment_1_6bcf067e4860bdfeb1d7b9fd1702a43a._comment create mode 100644 doc/todo/wishlist:___96__git_annex_import__96___--_An_easy_way_to_get_data_into_an_annex.mdwn create mode 100644 doc/todo/wishlist:___96__git_annex_import__96___--_An_easy_way_to_get_data_into_an_annex/comment_1_b9fd1bfaf9a3d238fdb7bc9c2d75fe5f._comment create mode 100644 doc/todo/wishlist:___96__git_annex_import__96___--_An_easy_way_to_get_data_into_an_annex/comment_2_56f6972413c6f0d9f414245b6f4d27b9._comment create mode 100644 doc/todo/wishlist:___96__git_annex_import__96___--_An_easy_way_to_get_data_into_an_annex/comment_3_2c094bef802a2182de4fcd0def1ad29b._comment create mode 100644 doc/todo/wishlist:___96__git_annex_import__96___--_An_easy_way_to_get_data_into_an_annex/comment_4_14915c43001f7f72c9fe5119a104ef5c._comment create mode 100644 doc/todo/wishlist:___96__git_annex_sync_-m__96__.mdwn create mode 100644 doc/todo/wishlist:_a_spec.remote_for_network_directories_that_would_mount_them_whenever_needed___40__e.g.__44___with_WebDAV__41__.mdwn create mode 100644 doc/todo/wishlist:_a_spec.remote_for_network_directories_that_would_mount_them_whenever_needed___40__e.g.__44___with_WebDAV__41__/comment_1_f46b0c9b49607e9f4f7a27f5a331ce83._comment create mode 100644 doc/todo/wishlist:_a_spec.remote_for_network_directories_that_would_mount_them_whenever_needed___40__e.g.__44___with_WebDAV__41__/comment_2_1b34e1dd72011c65e881dec2543a0373._comment create mode 100644 doc/todo/wishlist:_addurl_https:.mdwn create mode 100644 doc/todo/wishlist:_addurl_https:/comment_1_4e8f5e1fc52c3000eb2a1dad0624906e._comment create mode 100644 doc/todo/wishlist:_allow_configuration_of_downloader_for_addurl.mdwn create mode 100644 doc/todo/wishlist:_allow_the_same_remote_to_be_accissable_via_different_methods.mdwn create mode 100644 doc/todo/wishlist:_allow_the_same_remote_to_be_accissable_via_different_methods/comment_1_abb6263f3807160222bba1122475c89c._comment create mode 100644 doc/todo/wishlist:_an___34__assistant__34___for_web-browsing_--_tracking_the_sources_of_the_downloads.mdwn create mode 100644 doc/todo/wishlist:_an___34__assistant__34___for_web-browsing_--_tracking_the_sources_of_the_downloads/comment_1_36ae3c75053d5ec278b5e6eb2084d57a._comment create mode 100644 doc/todo/wishlist:_an___34__assistant__34___for_web-browsing_--_tracking_the_sources_of_the_downloads/comment_3_be8eb800523db8cf7a2c68a28fbf5ea5._comment create mode 100644 doc/todo/wishlist:_an___34__assistant__34___for_web-browsing_--_tracking_the_sources_of_the_downloads/comment_3_d9f725de41a8572c85e4c6d9b4bcc927._comment create mode 100644 doc/todo/wishlist:_an___34__assistant__34___for_web-browsing_--_tracking_the_sources_of_the_downloads/comment_4_f52492e4cc6f965515800bd1c0e05c90._comment create mode 100644 doc/todo/wishlist:_an___34__assistant__34___for_web-browsing_--_tracking_the_sources_of_the_downloads/comment_5_5b36656fc5fa124e763f06711d9da32b._comment create mode 100644 doc/todo/wishlist:_an___34__assistant__34___for_web-browsing_--_tracking_the_sources_of_the_downloads/comment_6_285215a4466806baf85b8606f680494a._comment create mode 100644 doc/todo/wishlist:_an___34__assistant__34___for_web-browsing_--_tracking_the_sources_of_the_downloads/comment_7_15bf62e46db4b84ed3156f550f03de42._comment create mode 100644 doc/todo/wishlist:_annex.largefiles_support_for_mimetypes.mdwn create mode 100644 doc/todo/wishlist:_command_options_changes.mdwn create mode 100644 doc/todo/wishlist:_command_options_changes/comment_1_bfba72a696789bf21b2435dea15f967a._comment create mode 100644 doc/todo/wishlist:_command_options_changes/comment_2_f6a637c78c989382e3c22d41b7fb4cc2._comment create mode 100644 doc/todo/wishlist:_command_options_changes/comment_3_bf1114533d2895804e531e76eb6b8095._comment create mode 100644 doc/todo/wishlist:_define_remotes_that_must_have_all_files.mdwn create mode 100644 doc/todo/wishlist:_define_remotes_that_must_have_all_files/comment_1_cceccc1a1730ac688d712b81a44e31c3._comment create mode 100644 doc/todo/wishlist:_define_remotes_that_must_have_all_files/comment_2_eec848fcf3979c03cbff2b7407c75a7a._comment create mode 100644 doc/todo/wishlist:_disable_automatic_commits.mdwn create mode 100644 doc/todo/wishlist:_do_round_robin_downloading_of_data.mdwn create mode 100644 doc/todo/wishlist:_do_round_robin_downloading_of_data/comment_1_460335b0e59ad03871c524f1fe812357._comment create mode 100644 doc/todo/wishlist:_git-annex_replicate.mdwn create mode 100644 doc/todo/wishlist:_git-annex_replicate/comment_1_9926132ec6052760cdf28518a24e2358._comment create mode 100644 doc/todo/wishlist:_git-annex_replicate/comment_2_c43932f4194aba8fb2470b18e0817599._comment create mode 100644 doc/todo/wishlist:_git-annex_replicate/comment_3_c13f4f9c3d5884fc6255fd04feadc2b1._comment create mode 100644 doc/todo/wishlist:_git-annex_replicate/comment_4_63f24abf086d644dced8b01e1a9948c9._comment create mode 100644 doc/todo/wishlist:_git_annex_diff.mdwn create mode 100644 doc/todo/wishlist:_git_annex_put_--_same_as_get__44___but_for_defaults.mdwn create mode 100644 doc/todo/wishlist:_git_annex_put_--_same_as_get__44___but_for_defaults/comment_1_d5413c8acce308505e4e2bec82fb1261._comment create mode 100644 doc/todo/wishlist:_git_annex_put_--_same_as_get__44___but_for_defaults/comment_2_0aa227c85d34dfff4e94febca44abea8._comment create mode 100644 doc/todo/wishlist:_git_annex_put_--_same_as_get__44___but_for_defaults/comment_3_2082f4d708a584a1403cc1d4d005fb56._comment create mode 100644 doc/todo/wishlist:_git_annex_status.mdwn create mode 100644 doc/todo/wishlist:_git_annex_status/comment_1_994bfd12c5d82e08040d6116915c5090._comment create mode 100644 doc/todo/wishlist:_git_annex_status/comment_2_c2b0ce025805b774dc77ce264a222824._comment create mode 100644 doc/todo/wishlist:_git_annex_status/comment_3_d1fd70c67243971c96d59e1ffb7ef6e7._comment create mode 100644 doc/todo/wishlist:_git_annex_status/comment_4_9aeeb83d202dc8fb33ff364b0705ad94._comment create mode 100644 doc/todo/wishlist:_git_backend_for_git-annex.mdwn create mode 100644 doc/todo/wishlist:_git_backend_for_git-annex/comment_1_04319051fedc583e6c326bb21fcce5a5._comment create mode 100644 doc/todo/wishlist:_git_backend_for_git-annex/comment_2_7f529f19a47e10b571f65ab382e97fd5._comment create mode 100644 doc/todo/wishlist:_git_backend_for_git-annex/comment_3_a077bbad3e4b07cce019eb55a45330e7._comment create mode 100644 doc/todo/wishlist:_git_backend_for_git-annex/comment_4_ecca429e12d734b509c671166a676c9d._comment create mode 100644 doc/todo/wishlist:_git_backend_for_git-annex/comment_5_3459f0b41d818c23c8fb33edb89df634._comment create mode 100644 doc/todo/wishlist:_history_of_operations.mdwn create mode 100644 doc/todo/wishlist:_history_of_operations/comment_1_f9a77ce83c6f39b6272d5c577ffbb9f9._comment create mode 100644 doc/todo/wishlist:_make_partial_files_available_during_transfer.mdwn create mode 100644 doc/todo/wishlist:_make_partial_files_available_during_transfer/comment_3_1304a721da6f5133fdfa1dac507f1ecb._comment create mode 100644 doc/todo/wishlist:_more_info_in_the_standard_commit_message_of___96__sync__96__.mdwn create mode 100644 doc/todo/wishlist:_move_pending_transfers_for_a_host_to_the_end_of_the_queue_when_one_fails.mdwn create mode 100644 doc/todo/wishlist:_move_pending_transfers_for_a_host_to_the_end_of_the_queue_when_one_fails/comment_1_82ee9de610a0ac55cd1c27c211079e5b._comment create mode 100644 doc/todo/wishlist:_move_pending_transfers_for_a_host_to_the_end_of_the_queue_when_one_fails/comment_2_bea55156bd32cf9e6dd9b946ba1bb8c1._comment create mode 100644 doc/todo/wishlist:_option_to_disable_url_checking_with_addurl.mdwn create mode 100644 doc/todo/wishlist:_option_to_disable_url_checking_with_addurl/comment_1_868a380faa1e55faa3c2d314e3258e86._comment create mode 100644 doc/todo/wishlist:_print_locations_for_files_in_rsync_remote.mdwn create mode 100644 doc/todo/wishlist:_push_to_cia.vc_from_the_website__39__s_repo__44___not_your_personal_one.mdwn create mode 100644 doc/todo/wishlist:_push_to_cia.vc_from_the_website__39__s_repo__44___not_your_personal_one/comment_1_3480b0ec629ef29a151408d869186bf8._comment create mode 100644 doc/todo/wishlist:_recursive_directory_remote_setup__47__addurl.mdwn create mode 100644 doc/todo/wishlist:_recursive_directory_remote_setup__47__addurl/comment_1_b79976afc2242791523e63831f30af71._comment create mode 100644 doc/todo/wishlist:_recursive_directory_remote_setup__47__addurl/comment_2_1741d2392006a9af9cfd1f3b847600b9._comment create mode 100644 doc/todo/wishlist:_simpler_gpg_usage.mdwn create mode 100644 doc/todo/wishlist:_simpler_gpg_usage/comment_1_6923fa6ebc0bbe7d93edb1d01d7c46c5._comment create mode 100644 doc/todo/wishlist:_simpler_gpg_usage/comment_2_6fc874b6c391df242bd2592c4a65eae8._comment create mode 100644 doc/todo/wishlist:_simpler_gpg_usage/comment_3_012f340c8c572fe598fc860c1046dabd._comment create mode 100644 doc/todo/wishlist:_simpler_gpg_usage/comment_4_e0c2a13217b795964f3b630c001661ef._comment create mode 100644 doc/todo/wishlist:_simpler_gpg_usage/comment_5_9668b58eb71901e1db8da7db38e068ca._comment create mode 100644 doc/todo/wishlist:_spec.remotes_for_other_peer_network_data_stores___40__gnunet__44___freenet__41__.mdwn create mode 100644 doc/todo/wishlist:_spec.remotes_for_other_peer_network_data_stores___40__gnunet__44___freenet__41__/comment_1_e2c2047e7401cb95a82ffb686a732859._comment create mode 100644 doc/todo/wishlist:_spec.remotes_for_other_peer_network_data_stores___40__gnunet__44___freenet__41__/comment_2_472b576afdb169b233edd01adcb2123d._comment create mode 100644 doc/todo/wishlist:_special-case_handling_of_Youtube_URLs_in_Web_special_remote.mdwn create mode 100644 doc/todo/wishlist:_special-case_handling_of_Youtube_URLs_in_Web_special_remote/comment_1_1a383c30df4fb1767f13d8c670b0c0b5._comment create mode 100644 doc/todo/wishlist:_special_remote_Ubuntu_One.mdwn create mode 100644 doc/todo/wishlist:_special_remote_for_sftp_or_rsync.mdwn create mode 100644 doc/todo/wishlist:_special_remote_for_sftp_or_rsync/comment_1_6f07d9cc92cf8b4927b3a7d1820c9140._comment create mode 100644 doc/todo/wishlist:_special_remote_for_sftp_or_rsync/comment_2_84e4414c88ae91c048564a2cdc2d3250._comment create mode 100644 doc/todo/wishlist:_special_remote_for_sftp_or_rsync/comment_3_79de7ac44e3c0f0f5691a56d3fb88897._comment create mode 100644 doc/todo/wishlist:_special_remote_mega.co.nz.mdwn create mode 100644 doc/todo/wishlist:_special_remote_mega.co.nz/comment_2_6ca08ef808d4336fc42d0f279d6627b5._comment create mode 100644 doc/todo/wishlist:_support_copy_--from__61__x_--to__61__y.mdwn create mode 100644 doc/todo/wishlist:_support_copy_--from__61__x_--to__61__y/comment_1_cf8e0f16b723516374c95a93e4da42fc._comment create mode 100644 doc/todo/wishlist:_support_copy_--from__61__x_--to__61__y/comment_2_d35359c9dd4dd4365d9a7caf695ff833._comment create mode 100644 doc/todo/wishlist:_support_for_more_ssh_urls_.mdwn create mode 100644 doc/todo/wishlist:_swift_backend.mdwn create mode 100644 doc/todo/wishlist:_swift_backend/comment_1_e6efbb35f61ee521b473a92674036788._comment create mode 100644 doc/todo/wishlist:_swift_backend/comment_2_5d8c83b0485112e98367b7abaab3f4e3._comment create mode 100644 doc/todo/wishlist:_swift_backend/comment_3_bf8625b909c3a7321cae40e6f145e874._comment create mode 100644 doc/todo/wishlist:_traffic_accounting_for_git-annex.mdwn create mode 100644 doc/todo/wishlist:_vicfg_possible_repo_group_names.mdwn create mode 100644 doc/todo/wishlist:alias_system.mdwn create mode 100644 doc/transferring_data.mdwn create mode 100644 doc/trust.mdwn create mode 100644 doc/upgrades.mdwn create mode 100644 doc/upgrades/SHA_size.mdwn create mode 100644 doc/upgrades/SHA_size/comment_1_20f9b7b75786075de666b2146dc13a60._comment create mode 100644 doc/use_case/Alice.mdwn create mode 100644 doc/use_case/Bob.mdwn create mode 100644 doc/users.mdwn create mode 100644 doc/users/chrysn.mdwn create mode 100644 doc/users/fmarier.mdwn create mode 100644 doc/users/gebi.mdwn create mode 100644 doc/users/joey.mdwn create mode 100644 doc/videos.mdwn create mode 100644 doc/videos/FOSDEM2012.mdwn create mode 100644 doc/videos/LCA2013.mdwn create mode 100644 doc/videos/git-annex_assistant_archiving.mdwn create mode 100644 doc/videos/git-annex_assistant_introduction.mdwn create mode 100644 doc/videos/git-annex_assistant_introduction/comment_1_f42ad4183c2c28319d3705a82fceb82f._comment create mode 100644 doc/videos/git-annex_assistant_introduction/comment_2_b62f4eeeac1138570f7cb8c98d41c2cb._comment create mode 100644 doc/videos/git-annex_assistant_remote_sharing.mdwn create mode 100644 doc/videos/git-annex_assistant_sync_demo.mdwn create mode 100644 doc/videos/git-annex_watch_demo.mdwn create mode 100644 doc/videos/git-annex_weppapp_demo.mdwn create mode 100644 doc/walkthrough.mdwn create mode 100644 doc/walkthrough/adding_a_remote.mdwn create mode 100644 doc/walkthrough/adding_a_remote/comment_1_0a59355bd33a796aec97173607e6adc9._comment create mode 100644 doc/walkthrough/adding_a_remote/comment_2_f8cd79ef1593a8181a7f1086a87713e8._comment create mode 100644 doc/walkthrough/adding_a_remote/comment_3_60691af4400521b5a8c8d75efe3b44cb._comment create mode 100644 doc/walkthrough/adding_a_remote/comment_4_6f7cf5c330272c96b3abeb6612075c9d._comment create mode 100644 doc/walkthrough/adding_files.mdwn create mode 100644 doc/walkthrough/automatically_managing_content.mdwn create mode 100644 doc/walkthrough/backups.mdwn create mode 100644 doc/walkthrough/creating_a_repository.mdwn create mode 100644 doc/walkthrough/fsck:_verifying_your_data.mdwn create mode 100644 doc/walkthrough/fsck:_when_things_go_wrong.mdwn create mode 100644 doc/walkthrough/getting_file_content.mdwn create mode 100644 doc/walkthrough/modifying_annexed_files.mdwn create mode 100644 doc/walkthrough/modifying_annexed_files/comment_1_624b4a0b521b553d68ab6049f7dbaf8c._comment create mode 100644 doc/walkthrough/modifying_annexed_files/comment_2_b000622304535d32b69db17d51156b21._comment create mode 100644 doc/walkthrough/more.mdwn create mode 100644 doc/walkthrough/moving_file_content_between_repositories.mdwn create mode 100644 doc/walkthrough/moving_file_content_between_repositories/comment_1_4c30ade91fc7113a95960aa3bd1d5427._comment create mode 100644 doc/walkthrough/moving_file_content_between_repositories/comment_2_7d90e1e150e7524ba31687108fcc38d6._comment create mode 100644 doc/walkthrough/moving_file_content_between_repositories/comment_3_558d80384434207b9cfc033763863de3._comment create mode 100644 doc/walkthrough/moving_file_content_between_repositories/comment_4_a2f343eceed9e9fba1670f21e0fc6af4._comment create mode 100644 doc/walkthrough/removing_files.mdwn create mode 100644 doc/walkthrough/removing_files/comment_1_cb65e7c510b75be1c51f655b058667c6._comment create mode 100644 doc/walkthrough/removing_files/comment_2_64709ea4558915edd5c8ca4486965b07._comment create mode 100644 doc/walkthrough/removing_files:_When_things_go_wrong.mdwn create mode 100644 doc/walkthrough/renaming_files.mdwn create mode 100644 doc/walkthrough/syncing.mdwn create mode 100644 doc/walkthrough/transferring_files:_When_things_go_wrong.mdwn create mode 100644 doc/walkthrough/unused_data.mdwn create mode 100644 doc/walkthrough/unused_data/comment_1_684b7b652d3a8ec04f32129c5528f1ab._comment create mode 100644 doc/walkthrough/using_bup.mdwn create mode 100644 doc/walkthrough/using_ssh_remotes.mdwn create mode 100644 doc/walkthrough/using_ssh_remotes/comment_10_98e97c4d7fbbcd449eddf683967a71d6._comment create mode 100644 doc/walkthrough/using_ssh_remotes/comment_11_f2775a151ed50caba27057bd9c38bae2._comment create mode 100644 doc/walkthrough/using_ssh_remotes/comment_12_a8bc6110128431ca2a8624ddc75ea364._comment create mode 100644 doc/walkthrough/using_ssh_remotes/comment_2_365db5820d96d5daa62c19fd76fcdf1e._comment create mode 100644 doc/walkthrough/using_ssh_remotes/comment_2_451fd0c6a25ee61ef137e8e5be0c286b._comment create mode 100644 doc/walkthrough/using_ssh_remotes/comment_3_b2f15a46620385da26d5fe8f11ebfc1a._comment create mode 100644 doc/walkthrough/using_ssh_remotes/comment_4_433ccc87fbb0a13e32d59d77f0b4e56c._comment create mode 100644 doc/walkthrough/using_ssh_remotes/comment_5_a9805c7965da0b88a1c9f7f207c450a1._comment create mode 100644 doc/walkthrough/using_ssh_remotes/comment_6_9d5c12c056892b706cf100ea01866685._comment create mode 100644 doc/walkthrough/using_ssh_remotes/comment_7_725e7dbb2d0a74a035127cb01ee0442c._comment create mode 100644 doc/walkthrough/using_ssh_remotes/comment_8_8448e55026d2c2b50d8da41707686bea._comment create mode 100644 doc/walkthrough/using_ssh_remotes/comment_9_61833299a9878f23ac57598fa6da8839._comment create mode 100644 doc/walkthrough/using_tags_and_branches.mdwn create mode 100755 ghci create mode 100644 git-annex.cabal create mode 100644 git-annex.hs create mode 100644 git-union-merge.hs create mode 100644 standalone/android/Makefile create mode 100644 standalone/android/busybox_config create mode 100644 standalone/android/dropbear.patch create mode 100644 standalone/android/evilsplicer-headers.hs create mode 100644 standalone/android/haskell-patches/DAV_0.3-0001-build-without-TH.patch create mode 100644 standalone/android/haskell-patches/HTTP_4000.2.8-0001-build-with-base-4.8.patch create mode 100644 standalone/android/haskell-patches/MissingH_1.2.0.0_0001-fix-build-not-Android-specific.patch create mode 100644 standalone/android/haskell-patches/aeson_0.6.1.0_0001-disable-TH.patch create mode 100644 standalone/android/haskell-patches/async_2.0.1.4_0001-allow-building-with-unreleased-ghc.patch create mode 100644 standalone/android/haskell-patches/case-insensitive_0.4.0.1_0001-allow-building-with-unreleased-ghc.patch create mode 100644 standalone/android/haskell-patches/certificate_1.3.7-0001-support-Android-cert-store.patch create mode 100644 standalone/android/haskell-patches/cipher-aes_0.1.7-0001-fix-cross-build.patch create mode 100644 standalone/android/haskell-patches/distributive_0.3-0001-fixes-for-cross-build.patch create mode 100644 standalone/android/haskell-patches/dns_0.3.6-0001-use-getprop-to-get-dns-server.patch create mode 100644 standalone/android/haskell-patches/file-embed_0.0.4.7-0001-remove-TH-and-export-one-symbol-used-by-TH.patch create mode 100644 standalone/android/haskell-patches/gnutls_0.1.4-0001-statically-link-with-gnutls.patch create mode 100644 standalone/android/haskell-patches/gsasl_0.3.5-0001-link-with-libgsasl.patch create mode 100644 standalone/android/haskell-patches/hS3_0.5.7_0001-fix-build.patch create mode 100644 standalone/android/haskell-patches/hamlet_1.1.6.1_0001-remove-TH.patch create mode 100644 standalone/android/haskell-patches/iproute_1.2.11_0001-build-without-IPv6-stuff.patch create mode 100644 standalone/android/haskell-patches/lens_3.8.5-0001-build-without-TH.patch create mode 100644 standalone/android/haskell-patches/libxml-sax_0.7.3-0001-static-link-with-libxml2.patch create mode 100644 standalone/android/haskell-patches/lifted-base_0.2.0.2_0001-hacked-for-newer-ghc.patch create mode 100644 standalone/android/haskell-patches/monad-control_0.3.1.4_0001-build-with-newer-ghc.patch create mode 100644 standalone/android/haskell-patches/monad-logger_0.2.3.2_0001-remove-TH-logging-stuff.patch create mode 100644 standalone/android/haskell-patches/network-conduit_0.6.2.2_0001-NoDelay-does-not-work-on-Android.patch create mode 100644 standalone/android/haskell-patches/network-protocol-xmpp_0.4.4-0001-avoid-using-gnuidn.patch create mode 100644 standalone/android/haskell-patches/network_2.4.1.0_0001-android-port-fixes.patch create mode 100644 standalone/android/haskell-patches/network_2.4.1.0_0002-remove-Network.BSD-symbols-not-available-in-bionic.patch create mode 100644 standalone/android/haskell-patches/network_2.4.1.0_0003-configure-misdetects-accept4.patch create mode 100644 standalone/android/haskell-patches/network_2.4.1.0_0004-getprotobyname-hack-for-tcp-and-udp.patch create mode 100644 standalone/android/haskell-patches/persistent_1.1.5.1_0001-disable-TH.patch create mode 100644 standalone/android/haskell-patches/primitive_0.5.0.1_0001-disable-i386-opt-stuff-to-allow-cross-compilation.patch create mode 100644 standalone/android/haskell-patches/profunctors_3.3-0001-fix-cross-build.patch create mode 100644 standalone/android/haskell-patches/resourcet_0.4.4_0001-hack-to-build-with-hacked-up-lifted-base-which-is-cu.patch create mode 100644 standalone/android/haskell-patches/shakespeare-css_1.0.2_0001-remove-TH.patch create mode 100644 standalone/android/haskell-patches/shakespeare-css_1.0.2_0002-expose-modules-used-by-TH.patch create mode 100644 standalone/android/haskell-patches/shakespeare-i18n_1.0.0.2_0001-remove-TH.patch create mode 100644 standalone/android/haskell-patches/shakespeare-js_1.1.2_0001-remove-TH.patch create mode 100644 standalone/android/haskell-patches/shakespeare_1.0.3_0001-export-symbol-used-by-TH-splices.patch create mode 100644 standalone/android/haskell-patches/shakespeare_1.0.3_0001-remove-TH.patch create mode 100644 standalone/android/haskell-patches/socks_0.4.2_0001-remove-IPv6-stuff.patch create mode 100644 standalone/android/haskell-patches/split_0.2.1.2_0001-modify-to-build-with-unreleased-ghc.patch create mode 100644 standalone/android/haskell-patches/syb_0.3.7_0001-hack-for-cross-compiling.patch create mode 100644 standalone/android/haskell-patches/unix-time_0.1.4_0001-hacks-for-android.patch create mode 100644 standalone/android/haskell-patches/unix_2.6.0.1_0001-remove-stuff-not-available-on-Android.patch create mode 100644 standalone/android/haskell-patches/vector_0.10.0.1_0001-disable-optimisation-that-breaks-when-cross-compilin.patch create mode 100644 standalone/android/haskell-patches/wai-app-static_1.3.1-remove-TH.patch create mode 100644 standalone/android/haskell-patches/wai-extra_1.3.2.1_0001-disable-CGI-module.patch create mode 100644 standalone/android/haskell-patches/xml-hamlet_0.4.0.3-0001-remove-TH-code.patch create mode 100644 standalone/android/haskell-patches/yesod-core_1.1.8_0001-remove-TH.patch create mode 100644 standalone/android/haskell-patches/yesod-core_1.1.8_0002-replaced-TH-in-Yesod.Internal.Core.patch create mode 100644 standalone/android/haskell-patches/yesod-core_1.1.8_0003-exports-for-TH-splices.patch create mode 100644 standalone/android/haskell-patches/yesod-default_1.1.3.2_0001-remove-TH.patch create mode 100644 standalone/android/haskell-patches/yesod-form_1.2.1.1-0001-prepare-for-Evil-Splicer.patch create mode 100644 standalone/android/haskell-patches/yesod-form_1.2.1.1-0002-expand-TH.patch create mode 100644 standalone/android/haskell-patches/yesod-persistent_1.1.0.1_0001-avoid-TH.patch create mode 100644 standalone/android/haskell-patches/yesod-routes_1.1.2_0001-remove-TH-and-export-module-used-by-TH-splices.patch create mode 100644 standalone/android/haskell-patches/yesod-static_1.1.2-remove-TH.patch create mode 100644 standalone/android/haskell-patches/yesod_1.1.8_0001-hacked-up-to-build-on-Android.patch create mode 100644 standalone/android/haskell-patches/zlib_0.5.4.0_0001-hack-to-build-on-Android.patch create mode 100644 standalone/android/icons/drawable-hdpi/ic_launcher.png create mode 100644 standalone/android/icons/drawable-hdpi/ic_stat_service_notification_icon.png create mode 100644 standalone/android/icons/drawable-ldpi/ic_launcher.png create mode 100644 standalone/android/icons/drawable-ldpi/ic_stat_service_notification_icon.png create mode 100644 standalone/android/icons/drawable-mdpi/ic_launcher.png create mode 100644 standalone/android/icons/drawable-mdpi/ic_stat_service_notification_icon.png create mode 100644 standalone/android/icons/drawable-xhdpi/ic_launcher.png create mode 100644 standalone/android/icons/drawable-xhdpi/ic_stat_service_notification_icon.png create mode 120000 standalone/android/icons/drawable/ic_launcher.png create mode 120000 standalone/android/icons/drawable/ic_stat_service_notification_icon.png create mode 100755 standalone/android/install-haskell-packages create mode 100644 standalone/android/openssh.config.h create mode 100644 standalone/android/openssh.patch create mode 100644 standalone/android/rsync.patch create mode 100755 standalone/android/runshell create mode 100755 standalone/android/start create mode 100644 standalone/android/start.c create mode 100644 standalone/android/term.patch create mode 100644 standalone/licences.gz create mode 100644 standalone/linux/README create mode 100755 standalone/linux/git-annex create mode 100755 standalone/linux/git-annex-shell create mode 100755 standalone/linux/git-annex-webapp create mode 100644 standalone/linux/glibc-libs create mode 100755 standalone/linux/runshell create mode 100644 standalone/osx/git-annex.app/Contents/Info.plist create mode 100644 standalone/osx/git-annex.app/Contents/MacOS/README create mode 100755 standalone/osx/git-annex.app/Contents/MacOS/git-annex create mode 100755 standalone/osx/git-annex.app/Contents/MacOS/git-annex-shell create mode 100755 standalone/osx/git-annex.app/Contents/MacOS/git-annex-webapp create mode 100755 standalone/osx/git-annex.app/Contents/MacOS/runshell create mode 100644 standalone/osx/git-annex.app/Contents/Resources/git-annex.icns create mode 100644 standalone/windows/build.sh create mode 100644 standalone/windows/haskell-patches/ccc5967426a14eb7e8978277ed4fa937f8e0c514.patch create mode 100644 static/activityicon.gif create mode 100644 static/css/bootstrap-responsive.css create mode 100644 static/css/bootstrap.css create mode 100644 static/favicon.ico create mode 100644 static/img/glyphicons-halflings-white.png create mode 100644 static/img/glyphicons-halflings.png create mode 100644 static/jquery.full.js create mode 100644 static/jquery.ui.core.js create mode 100644 static/jquery.ui.mouse.js create mode 100644 static/jquery.ui.sortable.js create mode 100644 static/jquery.ui.widget.js create mode 100644 static/js/bootstrap-collapse.js create mode 100644 static/js/bootstrap-dropdown.js create mode 100644 static/js/bootstrap-modal.js create mode 100644 static/longpolling.js create mode 100644 static/syncicon.gif create mode 100644 templates/README create mode 100644 templates/actionbutton.hamlet create mode 100644 templates/bootstrap.hamlet create mode 100644 templates/configurators/addbox.com.hamlet create mode 100644 templates/configurators/adddrive.hamlet create mode 100644 templates/configurators/adddrive/clonemodal.hamlet create mode 100644 templates/configurators/adddrive/confirm.hamlet create mode 100644 templates/configurators/addglacier.hamlet create mode 100644 templates/configurators/addia.hamlet create mode 100644 templates/configurators/addrepository.hamlet create mode 100644 templates/configurators/addrepository/archive.hamlet create mode 100644 templates/configurators/addrepository/cloud.hamlet create mode 100644 templates/configurators/addrepository/misc.hamlet create mode 100644 templates/configurators/addrsync.net.hamlet create mode 100644 templates/configurators/adds3.hamlet create mode 100644 templates/configurators/checkunfinished.hamlet create mode 100644 templates/configurators/delete/currentrepository.hamlet create mode 100644 templates/configurators/delete/finished.hamlet create mode 100644 templates/configurators/delete/start.hamlet create mode 100644 templates/configurators/editrepository.hamlet create mode 100644 templates/configurators/enableaws.hamlet create mode 100644 templates/configurators/enabledirectory.hamlet create mode 100644 templates/configurators/enableia.hamlet create mode 100644 templates/configurators/enablewebdav.hamlet create mode 100644 templates/configurators/main.hamlet create mode 100644 templates/configurators/needglaciercli.hamlet create mode 100644 templates/configurators/newrepository.hamlet create mode 100644 templates/configurators/newrepository/combine.hamlet create mode 100644 templates/configurators/newrepository/first.hamlet create mode 100644 templates/configurators/newrepository/form.hamlet create mode 100644 templates/configurators/pairing/disabled.hamlet create mode 100644 templates/configurators/pairing/local/inprogress.hamlet create mode 100644 templates/configurators/pairing/local/prompt.hamlet create mode 100644 templates/configurators/pairing/xmpp/end.hamlet create mode 100644 templates/configurators/pairing/xmpp/friend/confirm.hamlet create mode 100644 templates/configurators/pairing/xmpp/friend/prompt.hamlet create mode 100644 templates/configurators/pairing/xmpp/self/prompt.hamlet create mode 100644 templates/configurators/pairing/xmpp/self/retry.hamlet create mode 100644 templates/configurators/preferences.hamlet create mode 100644 templates/configurators/ssh/add.hamlet create mode 100644 templates/configurators/ssh/confirm.hamlet create mode 100644 templates/configurators/ssh/enable.hamlet create mode 100644 templates/configurators/ssh/error.hamlet create mode 100644 templates/configurators/ssh/testmodal.hamlet create mode 100644 templates/configurators/xmpp.hamlet create mode 100644 templates/configurators/xmpp/buddylist.hamlet create mode 100644 templates/configurators/xmpp/disabled.hamlet create mode 100644 templates/configurators/xmpp/needcloudrepo.hamlet create mode 100644 templates/control/log.hamlet create mode 100644 templates/control/repositoryswitcher.hamlet create mode 100644 templates/control/restarting.hamlet create mode 100644 templates/control/shutdown.hamlet create mode 100644 templates/control/shutdownconfirmed.hamlet create mode 100644 templates/controlmenu.hamlet create mode 100644 templates/dashboard/main.hamlet create mode 100644 templates/dashboard/metarefresh.hamlet create mode 100644 templates/dashboard/transfers.cassius create mode 100644 templates/dashboard/transfers.hamlet create mode 100644 templates/documentation/about.hamlet create mode 100644 templates/documentation/license.hamlet create mode 100644 templates/documentation/repogroup.hamlet create mode 100644 templates/error.cassius create mode 100644 templates/error.hamlet create mode 100644 templates/notifications/longpolling.julius create mode 100644 templates/page.cassius create mode 100644 templates/page.hamlet create mode 100644 templates/page.julius create mode 100644 templates/repolist.hamlet create mode 100644 templates/repolist.julius create mode 100644 templates/sidebar/alert.hamlet create mode 100644 templates/sidebar/main.hamlet create mode 100755 test diff --git a/.ghci b/.ghci new file mode 100644 index 0000000000..c5550cee6e --- /dev/null +++ b/.ghci @@ -0,0 +1 @@ +:load Common diff --git a/Annex.hs b/Annex.hs new file mode 100644 index 0000000000..7625fa8b60 --- /dev/null +++ b/Annex.hs @@ -0,0 +1,247 @@ +{- git-annex monad + - + - Copyright 2010-2013 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE GeneralizedNewtypeDeriving, TypeFamilies, MultiParamTypeClasses #-} + +module Annex ( + Annex, + AnnexState(..), + PreferredContentMap, + new, + newState, + run, + eval, + getState, + changeState, + setFlag, + setField, + setOutput, + getFlag, + getField, + addCleanup, + gitRepo, + inRepo, + fromRepo, + calcRepo, + getGitConfig, + changeGitConfig, + changeGitRepo, + withCurrentState, +) where + +import "mtl" Control.Monad.Reader +import "MonadCatchIO-transformers" Control.Monad.CatchIO +import System.Posix.Types (Fd) +import Control.Concurrent + +import Common +import qualified Git +import qualified Git.Config +import Git.CatFile +import Git.CheckAttr +import Git.CheckIgnore +import Git.SharedRepository +import qualified Git.Queue +import Types.Backend +import Types.GitConfig +import qualified Types.Remote +import Types.Crypto +import Types.BranchState +import Types.TrustLevel +import Types.Group +import Types.Messages +import Types.UUID +import Types.FileMatcher +import qualified Utility.Matcher +import qualified Data.Map as M +import qualified Data.Set as S + +{- git-annex's monad is a ReaderT around an AnnexState stored in a MVar. + - This allows modifying the state in an exception-safe fashion. + - The MVar is not exposed outside this module. + -} +newtype Annex a = Annex { runAnnex :: ReaderT (MVar AnnexState) IO a } + deriving ( + Monad, + MonadIO, + MonadReader (MVar AnnexState), + MonadCatchIO, + Functor, + Applicative + ) + +type Matcher a = Either [Utility.Matcher.Token a] (Utility.Matcher.Matcher a) +type PreferredContentMap = M.Map UUID (Utility.Matcher.Matcher (S.Set UUID -> FileInfo -> Annex Bool)) + +-- internal state storage +data AnnexState = AnnexState + { repo :: Git.Repo + , gitconfig :: GitConfig + , backends :: [BackendA Annex] + , remotes :: [Types.Remote.RemoteA Annex] + , output :: MessageState + , force :: Bool + , fast :: Bool + , auto :: Bool + , daemon :: Bool + , branchstate :: BranchState + , repoqueue :: Maybe Git.Queue.Queue + , catfilehandles :: M.Map FilePath CatFileHandle + , checkattrhandle :: Maybe CheckAttrHandle + , checkignorehandle :: Maybe (Maybe CheckIgnoreHandle) + , forcebackend :: Maybe String + , forcenumcopies :: Maybe Int + , limit :: Matcher (FileInfo -> Annex Bool) + , uuidmap :: Maybe UUIDMap + , preferredcontentmap :: Maybe PreferredContentMap + , shared :: Maybe SharedRepository + , forcetrust :: TrustMap + , trustmap :: Maybe TrustMap + , groupmap :: Maybe GroupMap + , ciphers :: M.Map StorableCipher Cipher + , lockpool :: M.Map FilePath Fd + , flags :: M.Map String Bool + , fields :: M.Map String String + , cleanup :: M.Map String (Annex ()) + , inodeschanged :: Maybe Bool + } + +newState :: Git.Repo -> AnnexState +newState gitrepo = AnnexState + { repo = gitrepo + , gitconfig = extractGitConfig gitrepo + , backends = [] + , remotes = [] + , output = defaultMessageState + , force = False + , fast = False + , auto = False + , daemon = False + , branchstate = startBranchState + , repoqueue = Nothing + , catfilehandles = M.empty + , checkattrhandle = Nothing + , checkignorehandle = Nothing + , forcebackend = Nothing + , forcenumcopies = Nothing + , limit = Left [] + , uuidmap = Nothing + , preferredcontentmap = Nothing + , shared = Nothing + , forcetrust = M.empty + , trustmap = Nothing + , groupmap = Nothing + , ciphers = M.empty + , lockpool = M.empty + , flags = M.empty + , fields = M.empty + , cleanup = M.empty + , inodeschanged = Nothing + } + +{- Makes an Annex state object for the specified git repo. + - Ensures the config is read, if it was not already. -} +new :: Git.Repo -> IO AnnexState +new = newState <$$> Git.Config.read + +{- Performs an action in the Annex monad from a starting state, + - returning a new state. -} +run :: AnnexState -> Annex a -> IO (a, AnnexState) +run s a = do + mvar <- newMVar s + r <- runReaderT (runAnnex a) mvar + s' <- takeMVar mvar + return (r, s') + +{- Performs an action in the Annex monad from a starting state, + - and throws away the new state. -} +eval :: AnnexState -> Annex a -> IO a +eval s a = do + mvar <- newMVar s + runReaderT (runAnnex a) mvar + +getState :: (AnnexState -> v) -> Annex v +getState selector = do + mvar <- ask + s <- liftIO $ readMVar mvar + return $ selector s + +changeState :: (AnnexState -> AnnexState) -> Annex () +changeState modifier = do + mvar <- ask + liftIO $ modifyMVar_ mvar $ return . modifier + +{- Sets a flag to True -} +setFlag :: String -> Annex () +setFlag flag = changeState $ \s -> + s { flags = M.insertWith' const flag True $ flags s } + +{- Sets a field to a value -} +setField :: String -> String -> Annex () +setField field value = changeState $ \s -> + s { fields = M.insertWith' const field value $ fields s } + +{- Adds a cleanup action to perform. -} +addCleanup :: String -> Annex () -> Annex () +addCleanup uid a = changeState $ \s -> + s { cleanup = M.insertWith' const uid a $ cleanup s } + +{- Sets the type of output to emit. -} +setOutput :: OutputType -> Annex () +setOutput o = changeState $ \s -> + s { output = (output s) { outputType = o } } + +{- Checks if a flag was set. -} +getFlag :: String -> Annex Bool +getFlag flag = fromMaybe False . M.lookup flag <$> getState flags + +{- Gets the value of a field. -} +getField :: String -> Annex (Maybe String) +getField field = M.lookup field <$> getState fields + +{- Returns the annex's git repository. -} +gitRepo :: Annex Git.Repo +gitRepo = getState repo + +{- Runs an IO action in the annex's git repository. -} +inRepo :: (Git.Repo -> IO a) -> Annex a +inRepo a = liftIO . a =<< gitRepo + +{- Extracts a value from the annex's git repisitory. -} +fromRepo :: (Git.Repo -> a) -> Annex a +fromRepo a = a <$> gitRepo + +{- Calculates a value from an annex's git repository and its GitConfig. -} +calcRepo :: (Git.Repo -> GitConfig -> IO a) -> Annex a +calcRepo a = do + s <- getState id + liftIO $ a (repo s) (gitconfig s) + +{- Gets the GitConfig settings. -} +getGitConfig :: Annex GitConfig +getGitConfig = getState gitconfig + +{- Modifies a GitConfig setting. -} +changeGitConfig :: (GitConfig -> GitConfig) -> Annex () +changeGitConfig a = changeState $ \s -> s { gitconfig = a (gitconfig s) } + +{- Changing the git Repo data also involves re-extracting its GitConfig. -} +changeGitRepo :: Git.Repo -> Annex () +changeGitRepo r = changeState $ \s -> s + { 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 new file mode 100644 index 0000000000..bc3736a9a6 --- /dev/null +++ b/Annex/Branch.hs @@ -0,0 +1,363 @@ +{- management of the git-annex branch + - + - Copyright 2011-2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE CPP #-} + +module Annex.Branch ( + fullname, + name, + hasOrigin, + hasSibling, + siblingBranches, + create, + update, + forceUpdate, + updateTo, + get, + change, + commit, + files, + withIndex, +) where + +import qualified Data.ByteString.Lazy.Char8 as L + +import Common.Annex +import Annex.BranchState +import Annex.Journal +import qualified Git +import qualified Git.Command +import qualified Git.Ref +import qualified Git.Branch +import qualified Git.UnionMerge +import qualified Git.UpdateIndex +import Git.HashObject +import Git.Types +import Git.FilePath +import Annex.CatFile +import Annex.Perms +import qualified Annex +import Utility.Env + +{- Name of the branch that is used to store git-annex's information. -} +name :: Git.Ref +name = Git.Ref "git-annex" + +{- Fully qualified name of the branch. -} +fullname :: Git.Ref +fullname = Git.Ref $ "refs/heads/" ++ show name + +{- Branch's name in origin. -} +originname :: Git.Ref +originname = Git.Ref $ "origin/" ++ show name + +{- Does origin/git-annex exist? -} +hasOrigin :: Annex Bool +hasOrigin = inRepo $ Git.Ref.exists originname + +{- Does the git-annex branch or a sibling foo/git-annex branch exist? -} +hasSibling :: Annex Bool +hasSibling = not . null <$> siblingBranches + +{- List of git-annex (refs, branches), including the main one and any + - from remotes. Duplicate refs are filtered out. -} +siblingBranches :: Annex [(Git.Ref, Git.Branch)] +siblingBranches = inRepo $ Git.Ref.matchingUniq [name] + +{- Creates the branch, if it does not already exist. -} +create :: Annex () +create = void getBranch + +{- Returns the ref of the branch, creating it first if necessary. -} +getBranch :: Annex Git.Ref +getBranch = maybe (hasOrigin >>= go >>= use) return =<< branchsha + where + go True = do + inRepo $ Git.Command.run + [Param "branch", Param $ show name, Param $ show originname] + fromMaybe (error $ "failed to create " ++ show name) + <$> branchsha + go False = withIndex' True $ + inRepo $ Git.Branch.commit "branch created" fullname [] + use sha = do + setIndexSha sha + return sha + branchsha = inRepo $ Git.Ref.sha fullname + +{- Ensures that the branch and index are up-to-date; should be + - called before data is read from it. Runs only once per git-annex run. -} +update :: Annex () +update = runUpdateOnce $ void $ updateTo =<< siblingBranches + +{- Forces an update even if one has already been run. -} +forceUpdate :: Annex Bool +forceUpdate = updateTo =<< siblingBranches + +{- Merges the specified Refs into the index, if they have any changes not + - already in it. The Branch names are only used in the commit message; + - it's even possible that the provided Branches have not been updated to + - point to the Refs yet. + - + - The branch is fast-forwarded if possible, otherwise a merge commit is + - made. + - + - Before Refs are merged into the index, it's important to first stage the + - journal into the index. Otherwise, any changes in the journal would + - later get staged, and might overwrite changes made during the merge. + - This is only done if some of the Refs do need to be merged. + - + - Returns True if any refs were merged in, False otherwise. + -} +updateTo :: [(Git.Ref, Git.Branch)] -> Annex Bool +updateTo pairs = do + -- ensure branch exists, and get its current ref + branchref <- getBranch + dirty <- journalDirty + (refs, branches) <- unzip <$> filterM isnewer pairs + if null refs + {- Even when no refs need to be merged, the index + - may still be updated if the branch has gotten ahead + - of the index. -} + then whenM (needUpdateIndex branchref) $ lockJournal $ do + forceUpdateIndex branchref + {- When there are journalled changes + - as well as the branch being updated, + - a commit needs to be done. -} + when dirty $ + go branchref True [] [] + else lockJournal $ go branchref dirty refs branches + return $ not $ null refs + where + isnewer (r, _) = inRepo $ Git.Branch.changed fullname r + go branchref dirty refs branches = withIndex $ do + cleanjournal <- if dirty then stageJournal else return noop + let merge_desc = if null branches + then "update" + else "merging " ++ + unwords (map Git.Ref.describe branches) ++ + " into " ++ show name + unless (null branches) $ do + showSideAction merge_desc + mergeIndex refs + ff <- if dirty + then return False + else inRepo $ Git.Branch.fastForward fullname refs + if ff + then updateIndex branchref + else commitBranch branchref merge_desc + (nub $ fullname:refs) + liftIO cleanjournal + +{- Gets the content of a file, which may be in the journal, or in the index + - (and committed to the branch). + - + - Updates the branch if necessary, to ensure the most up-to-date available + - content is available. + - + - Returns an empty string if the file doesn't exist yet. -} +get :: FilePath -> Annex String +get file = do + update + get' file + +{- Like get, but does not merge the branch, so the info returned may not + - reflect changes in remotes. + - (Changing the value this returns, and then merging is always the + - same as using get, and then changing its value.) -} +getStale :: FilePath -> Annex String +getStale = get' + +get' :: FilePath -> Annex String +get' file = go =<< getJournalFile file + where + go (Just journalcontent) = return journalcontent + go Nothing = withIndex $ L.unpack <$> catFile fullname file + +{- Applies a function to modifiy the content of a file. + - + - Note that this does not cause the branch to be merged, it only + - modifes the current content of the file on the branch. + -} +change :: FilePath -> (String -> String) -> Annex () +change file a = lockJournal $ a <$> getStale file >>= set file + +{- Records new content of a file into the journal -} +set :: FilePath -> String -> Annex () +set = setJournalFile + +{- Stages the journal, and commits staged changes to the branch. -} +commit :: String -> Annex () +commit message = whenM journalDirty $ lockJournal $ do + cleanjournal <- stageJournal + ref <- getBranch + withIndex $ commitBranch ref message [fullname] + liftIO cleanjournal + +{- Commits the staged changes in the index to the branch. + - + - Ensures that the branch's index file is first updated to the state + - of the branch at branchref, before running the commit action. This + - is needed because the branch may have had changes pushed to it, that + - are not yet reflected in the index. + - + - Also safely handles a race that can occur if a change is being pushed + - into the branch at the same time. When the race happens, the commit will + - be made on top of the newly pushed change, but without the index file + - being updated to include it. The result is that the newly pushed + - change is reverted. This race is detected and another commit made + - to fix it. + - + - The branchref value can have been obtained using getBranch at any + - previous point, though getting it a long time ago makes the race + - more likely to occur. + -} +commitBranch :: Git.Ref -> String -> [Git.Ref] -> Annex () +commitBranch branchref message parents = do + showStoringStateAction + commitBranch' branchref message parents +commitBranch' :: Git.Ref -> String -> [Git.Ref] -> Annex () +commitBranch' branchref message parents = do + updateIndex branchref + committedref <- inRepo $ Git.Branch.commit message fullname parents + setIndexSha committedref + parentrefs <- commitparents <$> catObject committedref + when (racedetected branchref parentrefs) $ + fixrace committedref parentrefs + where + -- look for "parent ref" lines and return the refs + commitparents = map (Git.Ref . snd) . filter isparent . + map (toassoc . L.unpack) . L.lines + toassoc = separate (== ' ') + isparent (k,_) = k == "parent" + + {- The race can be detected by checking the commit's + - parent, which will be the newly pushed branch, + - instead of the expected ref that the index was updated to. -} + racedetected expectedref parentrefs + | expectedref `elem` parentrefs = False -- good parent + | otherwise = True -- race! + + {- To recover from the race, union merge the lost refs + - into the index, and recommit on top of the bad commit. -} + fixrace committedref lostrefs = do + mergeIndex lostrefs + commitBranch committedref racemessage [committedref] + + racemessage = message ++ " (recovery from race)" + +{- Lists all files on the branch. There may be duplicates in the list. -} +files :: Annex [FilePath] +files = do + update + withIndex $ do + bfiles <- inRepo $ Git.Command.pipeNullSplitZombie + [ Params "ls-tree --name-only -r -z" + , Param $ show fullname + ] + jfiles <- getJournalledFiles + return $ jfiles ++ bfiles + +{- Populates the branch's index file with the current branch contents. + - + - This is only done when the index doesn't yet exist, and the index + - is used to build up changes to be commited to the branch, and merge + - in changes from other branches. + -} +genIndex :: Git.Repo -> IO () +genIndex g = Git.UpdateIndex.streamUpdateIndex g + [Git.UpdateIndex.lsTree fullname g] + +{- Merges the specified refs into the index. + - Any changes staged in the index will be preserved. -} +mergeIndex :: [Git.Ref] -> Annex () +mergeIndex branches = do + h <- catFileHandle + inRepo $ \g -> Git.UnionMerge.mergeIndex h g branches + +{- Runs an action using the branch's index file. -} +withIndex :: Annex a -> Annex a +withIndex = withIndex' False +withIndex' :: Bool -> Annex a -> Annex a +withIndex' bootstrapping a = do + f <- fromRepo gitAnnexIndex + g <- gitRepo +#ifdef __ANDROID__ + {- This should not be necessary on Android, but there is some + - weird getEnvironment breakage. See + - https://github.com/neurocyte/ghc-android/issues/7 + - Use getEnv to get some key environment variables that + - git expects to have. -} + let keyenv = words "USER PATH GIT_EXEC_PATH HOSTNAME HOME" + let getEnvPair k = maybe Nothing (\v -> Just (k, v)) <$> getEnv k + e <- liftIO $ catMaybes <$> forM keyenv getEnvPair +#else + e <- liftIO getEnvironment +#endif + let g' = g { gitEnv = Just $ ("GIT_INDEX_FILE", f):e } + + Annex.changeState $ \s -> s { Annex.repo = g' } + checkIndexOnce $ unlessM (liftIO $ doesFileExist f) $ do + unless bootstrapping create + liftIO $ createDirectoryIfMissing True $ takeDirectory f + unless bootstrapping $ inRepo genIndex + r <- a + Annex.changeState $ \s -> s { Annex.repo = (Annex.repo s) { gitEnv = gitEnv g} } + + return r + +{- Updates the branch's index to reflect the current contents of the branch. + - Any changes staged in the index will be preserved. + - + - Compares the ref stored in the lock file with the current + - ref of the branch to see if an update is needed. + -} +updateIndex :: Git.Ref -> Annex () +updateIndex branchref = whenM (needUpdateIndex branchref) $ + forceUpdateIndex branchref + +forceUpdateIndex :: Git.Ref -> Annex () +forceUpdateIndex branchref = do + withIndex $ mergeIndex [fullname] + setIndexSha branchref + +{- Checks if the index needs to be updated. -} +needUpdateIndex :: Git.Ref -> Annex Bool +needUpdateIndex branchref = do + lock <- fromRepo gitAnnexIndexLock + lockref <- Git.Ref . firstLine <$> + liftIO (catchDefaultIO "" $ readFileStrict lock) + return (lockref /= branchref) + +{- Record that the branch's index has been updated to correspond to a + - given ref of the branch. -} +setIndexSha :: Git.Ref -> Annex () +setIndexSha ref = do + lock <- fromRepo gitAnnexIndexLock + liftIO $ writeFile lock $ show ref ++ "\n" + setAnnexPerm lock + +{- Stages the journal into the index and returns an action that will + - clean up the staged journal files, which should only be run once + - the index has been committed to the branch. Should be run within + - lockJournal, to prevent others from modifying the journal. -} +stageJournal :: Annex (IO ()) +stageJournal = withIndex $ do + g <- gitRepo + let dir = gitAnnexJournalDir g + fs <- getJournalFiles + liftIO $ do + h <- hashObjectStart g + Git.UpdateIndex.streamUpdateIndex g + [genstream dir h fs] + hashObjectStop h + return $ liftIO $ mapM_ (removeFile . (dir )) fs + where + genstream dir h fs streamer = forM_ fs $ \file -> do + let path = dir file + sha <- hashFile h path + streamer $ Git.UpdateIndex.updateIndexLine + sha FileBlob (asTopFilePath $ fileJournal file) diff --git a/Annex/BranchState.hs b/Annex/BranchState.hs new file mode 100644 index 0000000000..9b2f9a04c5 --- /dev/null +++ b/Annex/BranchState.hs @@ -0,0 +1,43 @@ +{- git-annex branch state management + - + - Runtime state about the git-annex branch. + - + - Copyright 2011-2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Annex.BranchState where + +import Common.Annex +import Types.BranchState +import qualified Annex + +getState :: Annex BranchState +getState = Annex.getState Annex.branchstate + +setState :: BranchState -> Annex () +setState state = Annex.changeState $ \s -> s { Annex.branchstate = state } + +changeState :: (BranchState -> BranchState) -> Annex () +changeState changer = setState =<< changer <$> getState + +{- Runs an action to check that the index file exists, if it's not been + - checked before in this run of git-annex. -} +checkIndexOnce :: Annex () -> Annex () +checkIndexOnce a = unlessM (indexChecked <$> getState) $ do + a + changeState $ \s -> s { indexChecked = True } + +{- Runs an action to update the branch, if it's not been updated before + - in this run of git-annex. -} +runUpdateOnce :: Annex () -> Annex () +runUpdateOnce a = unlessM (branchUpdated <$> getState) $ do + a + disableUpdate + +{- Avoids updating the branch. A useful optimisation when the branch + - is known to have not changed, or git-annex won't be relying on info + - from it. -} +disableUpdate :: Annex () +disableUpdate = changeState $ \s -> s { branchUpdated = True } diff --git a/Annex/CatFile.hs b/Annex/CatFile.hs new file mode 100644 index 0000000000..f90e74509c --- /dev/null +++ b/Annex/CatFile.hs @@ -0,0 +1,92 @@ +{- git cat-file interface, with handle automatically stored in the Annex monad + - + - Copyright 2011-2013 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Annex.CatFile ( + catFile, + catObject, + catObjectDetails, + catFileHandle, + catKey, + catKeyFile, +) where + +import qualified Data.ByteString.Lazy as L +import qualified Data.Map as M + +import Common.Annex +import qualified Git +import qualified Git.CatFile +import qualified Annex +import Git.Types +import Git.FilePath + +catFile :: Git.Branch -> FilePath -> Annex L.ByteString +catFile branch file = do + h <- catFileHandle + liftIO $ Git.CatFile.catFile h branch file + +catObject :: Git.Ref -> Annex L.ByteString +catObject ref = do + h <- catFileHandle + liftIO $ Git.CatFile.catObject h ref + +catObjectDetails :: Git.Ref -> Annex (Maybe (L.ByteString, Sha)) +catObjectDetails ref = do + h <- catFileHandle + liftIO $ Git.CatFile.catObjectDetails h ref + +{- There can be multiple index files, and a different cat-file is needed + - for each. This is selected by setting GIT_INDEX_FILE in the gitEnv. -} +catFileHandle :: Annex Git.CatFile.CatFileHandle +catFileHandle = do + m <- Annex.getState Annex.catfilehandles + indexfile <- fromMaybe "" . maybe Nothing (lookup "GIT_INDEX_FILE") + <$> fromRepo gitEnv + case M.lookup indexfile m of + Just h -> return h + Nothing -> do + h <- inRepo Git.CatFile.catFileStart + let m' = M.insert indexfile h m + Annex.changeState $ \s -> s { Annex.catfilehandles = m' } + return h + +{- From the Sha or Ref of a symlink back to the key. -} +catKey :: Ref -> Annex (Maybe Key) +catKey ref = do + l <- fromInternalGitPath . encodeW8 . L.unpack <$> catObject ref + return $ if isLinkToAnnex l + then fileKey $ takeFileName l + else Nothing + +{- From a file in the repository back to the key. + - + - Prefixing the file with ./ makes this work even if in a subdirectory + - of a repo. + - + - Ideally, this should reflect the key that's staged in the index, + - not the key that's committed to HEAD. Unfortunately, git cat-file + - does not refresh the index file after it's started up, so things + - newly staged in the index won't show up. It does, however, notice + - when branches change. + - + - For command-line git-annex use, that doesn't matter. It's perfectly + - reasonable for things staged in the index after the currently running + - git-annex process to not be noticed by it. + - + - For the assistant, this is much more of a problem, since it commits + - files and then needs to be able to immediately look up their keys. + - OTOH, the assistant doesn't keep changes staged in the index for very + - long at all before committing them -- and it won't look at the keys + - of files until after committing them. + - + - So, this gets info from the index, unless running as a daemon. + -} +catKeyFile :: FilePath -> Annex (Maybe Key) +catKeyFile f = ifM (Annex.getState Annex.daemon) + ( catKey $ Ref $ "HEAD:./" ++ f + , catKey $ Ref $ ":./" ++ f + ) diff --git a/Annex/CheckAttr.hs b/Annex/CheckAttr.hs new file mode 100644 index 0000000000..8eed9e804c --- /dev/null +++ b/Annex/CheckAttr.hs @@ -0,0 +1,35 @@ +{- git check-attr interface, with handle automatically stored in the Annex monad + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Annex.CheckAttr ( + checkAttr, + checkAttrHandle +) where + +import Common.Annex +import qualified Git.CheckAttr as Git +import qualified Annex + +{- All gitattributes used by git-annex. -} +annexAttrs :: [Git.Attr] +annexAttrs = + [ "annex.backend" + , "annex.numcopies" + ] + +checkAttr :: Git.Attr -> FilePath -> Annex String +checkAttr attr file = do + h <- checkAttrHandle + liftIO $ Git.checkAttr h attr file + +checkAttrHandle :: Annex Git.CheckAttrHandle +checkAttrHandle = maybe startup return =<< Annex.getState Annex.checkattrhandle + where + startup = do + h <- inRepo $ Git.checkAttrStart annexAttrs + Annex.changeState $ \s -> s { Annex.checkattrhandle = Just h } + return h diff --git a/Annex/CheckIgnore.hs b/Annex/CheckIgnore.hs new file mode 100644 index 0000000000..e5626557d5 --- /dev/null +++ b/Annex/CheckIgnore.hs @@ -0,0 +1,32 @@ +{- git check-ignore interface, with handle automatically stored in + - the Annex monad + - + - Copyright 2013 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Annex.CheckIgnore ( + checkIgnored, + checkIgnoreHandle +) where + +import Common.Annex +import qualified Git.CheckIgnore as Git +import qualified Annex + +checkIgnored :: FilePath -> Annex Bool +checkIgnored file = go =<< checkIgnoreHandle + where + go Nothing = return False + go (Just h) = liftIO $ Git.checkIgnored h file + +checkIgnoreHandle :: Annex (Maybe Git.CheckIgnoreHandle) +checkIgnoreHandle = maybe startup return =<< Annex.getState Annex.checkignorehandle + where + startup = do + v <- inRepo $ Git.checkIgnoreStart + when (isNothing v) $ + warning "The installed version of git is too old for .gitignores to be honored by git-annex." + Annex.changeState $ \s -> s { Annex.checkignorehandle = Just v } + return v diff --git a/Annex/Content.hs b/Annex/Content.hs new file mode 100644 index 0000000000..01ad6f96fd --- /dev/null +++ b/Annex/Content.hs @@ -0,0 +1,511 @@ +{- git-annex file content managing + - + - Copyright 2010-2013 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE CPP #-} + +module Annex.Content ( + inAnnex, + inAnnexSafe, + inAnnexCheck, + lockContent, + getViaTmp, + getViaTmpChecked, + getViaTmpUnchecked, + withTmp, + checkDiskSpace, + moveAnnex, + sendAnnex, + prepSendAnnex, + removeAnnex, + fromAnnex, + moveBad, + getKeysPresent, + saveState, + downloadUrl, + preseedTmp, + freezeContent, + thawContent, + cleanObjectLoc, +) where + +import System.IO.Unsafe (unsafeInterleaveIO) +import System.PosixCompat.Files + +import Common.Annex +import Logs.Location +import qualified Git +import qualified Annex +import qualified Annex.Queue +import qualified Annex.Branch +import Utility.DiskFree +import Utility.FileMode +import qualified Utility.Url as Url +import Types.Key +import Utility.DataUnits +import Utility.CopyFile +import Config +import Git.SharedRepository +import Annex.Perms +import Annex.Link +import Annex.Content.Direct +import Annex.ReplaceFile +#ifndef mingw32_HOST_OS +import Annex.Exception +#endif + +{- Checks if a given key's content is currently present. -} +inAnnex :: Key -> Annex Bool +inAnnex key = inAnnexCheck key $ liftIO . doesFileExist + +{- Runs an arbitrary check on a key's content. -} +inAnnexCheck :: Key -> (FilePath -> Annex Bool) -> Annex Bool +inAnnexCheck key check = inAnnex' id False check key + +{- Generic inAnnex, handling both indirect and direct mode. + - + - In direct mode, at least one of the associated files must pass the + - check. Additionally, the file must be unmodified. + -} +inAnnex' :: (a -> Bool) -> a -> (FilePath -> Annex a) -> Key -> Annex a +inAnnex' isgood bad check key = withObjectLoc key checkindirect checkdirect + where + checkindirect loc = do + whenM (fromRepo Git.repoIsUrl) $ + error "inAnnex cannot check remote repo" + check loc + checkdirect [] = return bad + checkdirect (loc:locs) = do + r <- check loc + if isgood r + then ifM (goodContent key loc) + ( return r + , checkdirect locs + ) + else checkdirect locs + +{- A safer check; the key's content must not only be present, but + - is not in the process of being removed. -} +inAnnexSafe :: Key -> Annex (Maybe Bool) +inAnnexSafe = inAnnex' (fromMaybe False) (Just False) go + where + go f = liftIO $ openforlock f >>= check +#ifndef mingw32_HOST_OS + openforlock f = catchMaybeIO $ + openFd f ReadOnly Nothing defaultFileFlags +#else + openforlock _ = return $ Just () +#endif + check Nothing = return is_missing +#ifndef mingw32_HOST_OS + check (Just h) = do + v <- getLock h (ReadLock, AbsoluteSeek, 0, 0) + closeFd h + return $ case v of + Just _ -> is_locked + Nothing -> is_unlocked +#else + check (Just _) = return is_unlocked +#endif +#ifndef mingw32_HOST_OS + is_locked = Nothing +#endif + is_unlocked = Just True + is_missing = Just False + +{- Content is exclusively locked while running an action that might remove + - it. (If the content is not present, no locking is done.) -} +lockContent :: Key -> Annex a -> Annex a +#ifndef mingw32_HOST_OS +lockContent key a = do + file <- calcRepo $ gitAnnexLocation key + bracketIO (openforlock file >>= lock) unlock (const a) + where + {- Since files are stored with the write bit disabled, have + - to fiddle with permissions to open for an exclusive lock. -} + openforlock f = catchMaybeIO $ ifM (doesFileExist f) + ( withModifiedFileMode f + (`unionFileModes` ownerWriteMode) + open + , open + ) + where + open = openFd f ReadWrite Nothing defaultFileFlags + lock Nothing = return Nothing + lock (Just fd) = do + v <- tryIO $ setLock fd (WriteLock, AbsoluteSeek, 0, 0) + case v of + Left _ -> error "content is locked" + Right _ -> return $ Just fd + unlock Nothing = noop + unlock (Just l) = closeFd l +#else +lockContent _key a = a -- no locking for Windows! +#endif + +{- Runs an action, passing it a temporary filename to get, + - and if the action succeeds, moves the temp file into + - the annex as a key's content. -} +getViaTmp :: Key -> (FilePath -> Annex Bool) -> Annex Bool +getViaTmp = getViaTmpChecked (return True) + +{- Like getViaTmp, but does not check that there is enough disk space + - for the incoming key. For use when the key content is already on disk + - and not being copied into place. -} +getViaTmpUnchecked :: Key -> (FilePath -> Annex Bool) -> Annex Bool +getViaTmpUnchecked = finishGetViaTmp (return True) + +getViaTmpChecked :: Annex Bool -> Key -> (FilePath -> Annex Bool) -> Annex Bool +getViaTmpChecked check key action = do + tmp <- fromRepo $ gitAnnexTmpLocation key + + -- Check that there is enough free disk space. + -- When the temp file already exists, count the space + -- it is using as free. + e <- liftIO $ doesFileExist tmp + alreadythere <- if e + then fromIntegral . fileSize <$> liftIO (getFileStatus tmp) + else return 0 + ifM (checkDiskSpace Nothing key alreadythere) + ( do + when e $ thawContent tmp + finishGetViaTmp check key action + , return False + ) + +finishGetViaTmp :: Annex Bool -> Key -> (FilePath -> Annex Bool) -> Annex Bool +finishGetViaTmp check key action = do + tmpfile <- prepTmp key + ifM (action tmpfile <&&> check) + ( do + moveAnnex key tmpfile + logStatus key InfoPresent + return True + , do + -- the tmp file is left behind, in case caller wants + -- to resume its transfer + return False + ) + +prepTmp :: Key -> Annex FilePath +prepTmp key = do + tmp <- fromRepo $ gitAnnexTmpLocation key + createAnnexDirectory (parentDir tmp) + return tmp + +{- Creates a temp file, runs an action on it, and cleans up the temp file. -} +withTmp :: Key -> (FilePath -> Annex a) -> Annex a +withTmp key action = do + tmp <- prepTmp key + res <- action tmp + liftIO $ nukeFile tmp + return res + +{- Checks that there is disk space available to store a given key, + - in a destination (or the annex) printing a warning if not. -} +checkDiskSpace :: Maybe FilePath -> Key -> Integer -> Annex Bool +checkDiskSpace destination key alreadythere = do + reserve <- annexDiskReserve <$> Annex.getGitConfig + free <- liftIO . getDiskFree =<< dir + force <- Annex.getState Annex.force + case (free, keySize key) of + (Just have, Just need) -> do + let ok = (need + reserve <= have + alreadythere) || force + unless ok $ + needmorespace (need + reserve - have - alreadythere) + return ok + _ -> return True + where + dir = maybe (fromRepo gitAnnexDir) return destination + needmorespace n = + warning $ "not enough free space, need " ++ + roughSize storageUnits True n ++ + " more" ++ forcemsg + forcemsg = " (use --force to override this check or adjust annex.diskreserve)" + +{- Moves a key's content into .git/annex/objects/ + - + - In direct mode, moves it to the associated file, or files. + - + - What if the key there already has content? This could happen for + - various reasons; perhaps the same content is being annexed again. + - Perhaps there has been a hash collision generating the keys. + - + - The current strategy is to assume that in this case it's safe to delete + - one of the two copies of the content; and the one already in the annex + - is left there, assuming it's the original, canonical copy. + - + - I considered being more paranoid, and checking that both files had + - the same content. Decided against it because A) users explicitly choose + - a backend based on its hashing properties and so if they're dealing + - with colliding files it's their own fault and B) adding such a check + - would not catch all cases of colliding keys. For example, perhaps + - a remote has a key; if it's then added again with different content then + - the overall system now has two different peices of content for that + - key, and one of them will probably get deleted later. So, adding the + - check here would only raise expectations that git-annex cannot truely + - meet. + -} +moveAnnex :: Key -> FilePath -> Annex () +moveAnnex key src = withObjectLoc key storeobject storedirect + where + storeobject dest = ifM (liftIO $ doesFileExist dest) + ( alreadyhave + , do + createContentDir dest + liftIO $ moveFile src dest + freezeContent dest + freezeContentDir dest + ) + storeindirect = storeobject =<< calcRepo (gitAnnexLocation key) + + {- In direct mode, the associated file's content may be locally + - modified. In that case, it's preserved. However, the content + - we're moving into the annex may be the only extant copy, so + - it's important we not lose it. So, when the key's content + - cannot be moved to any associated file, it's stored in indirect + - mode. + -} + storedirect = storedirect' storeindirect + storedirect' fallback [] = fallback + storedirect' fallback (f:fs) = do + thawContentDir =<< calcRepo (gitAnnexLocation key) + thawContent src + v <- isAnnexLink f + if (Just key == v) + then do + updateInodeCache key src + replaceFile f $ liftIO . moveFile src + forM_ fs $ + addContentWhenNotPresent key f + else ifM (goodContent key f) + ( storedirect' alreadyhave fs + , storedirect' fallback fs + ) + + alreadyhave = liftIO $ removeFile src + +{- Runs an action to transfer an object's content. + - + - 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. + -} +sendAnnex :: Key -> Annex () -> (FilePath -> Annex Bool) -> Annex Bool +sendAnnex key rollback sendobject = go =<< prepSendAnnex key + where + go Nothing = return False + go (Just (f, checksuccess)) = do + r <- sendobject f + ifM checksuccess + ( return r + , do + rollback + return False + ) + +{- Returns a file that contains an object's content, + - and an check to run after the transfer is complete. + - + - In direct mode, it's possible for the file to change as it's being sent, + - and the check detects this case and returns False. + - + - Note that the returned check 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. + -} +prepSendAnnex :: Key -> Annex (Maybe (FilePath, Annex Bool)) +prepSendAnnex key = withObjectLoc key indirect direct + where + indirect f = return $ Just (f, return True) + direct [] = return Nothing + direct (f:fs) = do + cache <- recordedInodeCache key + -- check that we have a good file + ifM (sameInodeCache f cache) + ( return $ Just (f, sameInodeCache f cache) + , direct fs + ) + +{- Performs an action, passing it the location to use for a key's content. + - + - In direct mode, the associated files will be passed. But, if there are + - no associated files for a key, the indirect mode action will be + - performed instead. -} +withObjectLoc :: Key -> (FilePath -> Annex a) -> ([FilePath] -> Annex a) -> Annex a +withObjectLoc key indirect direct = ifM isDirect + ( do + fs <- associatedFiles key + if null fs + then goindirect + else direct fs + , goindirect + ) + where + goindirect = indirect =<< calcRepo (gitAnnexLocation key) + +cleanObjectLoc :: Key -> Annex () +cleanObjectLoc key = do + file <- calcRepo $ gitAnnexLocation key + unlessM crippledFileSystem $ + void $ liftIO $ catchMaybeIO $ allowWrite $ parentDir file + liftIO $ removeparents file (3 :: Int) + where + removeparents _ 0 = noop + removeparents file n = do + let dir = parentDir file + maybe noop (const $ removeparents dir (n-1)) + <=< catchMaybeIO $ removeDirectory dir + +{- Removes a key's file from .git/annex/objects/ + - + - In direct mode, deletes the associated files or files, and replaces + - them with symlinks. -} +removeAnnex :: Key -> Annex () +removeAnnex key = withObjectLoc key remove removedirect + where + remove file = do + thawContentDir file + liftIO $ nukeFile file + removeInodeCache key + cleanObjectLoc key + removedirect fs = do + thawContentDir =<< calcRepo (gitAnnexLocation key) + cache <- recordedInodeCache key + removeInodeCache key + mapM_ (resetfile cache) fs + resetfile cache f = whenM (sameInodeCache f cache) $ do + l <- inRepo $ gitAnnexLink f key + top <- fromRepo Git.repoPath + cwd <- liftIO getCurrentDirectory + let top' = fromMaybe top $ absNormPath cwd top + let l' = relPathDirToFile top' (fromMaybe l $ absNormPath top' l) + replaceFile f $ makeAnnexLink l' + +{- Moves a key's file out of .git/annex/objects/ -} +fromAnnex :: Key -> FilePath -> Annex () +fromAnnex key dest = do + file <- calcRepo $ gitAnnexLocation key + thawContentDir file + thawContent file + liftIO $ moveFile file dest + cleanObjectLoc key + +{- Moves a key out of .git/annex/objects/ into .git/annex/bad, and + - returns the file it was moved to. -} +moveBad :: Key -> Annex FilePath +moveBad key = do + src <- calcRepo $ gitAnnexLocation key + bad <- fromRepo gitAnnexBadDir + let dest = bad takeFileName src + createAnnexDirectory (parentDir dest) + thawContentDir src + liftIO $ moveFile src dest + cleanObjectLoc key + logStatus key InfoMissing + return dest + +{- List of keys whose content exists in the annex. -} +getKeysPresent :: Annex [Key] +getKeysPresent = do + direct <- isDirect + dir <- fromRepo gitAnnexObjectDir + liftIO $ traverse direct (2 :: Int) dir + where + traverse direct depth dir = do + contents <- catchDefaultIO [] (dirContents dir) + if depth == 0 + then do + contents' <- filterM (present direct) contents + let keys = mapMaybe (fileKey . takeFileName) contents' + continue keys [] + else do + let deeper = traverse direct (depth - 1) + continue [] (map deeper contents) + continue keys [] = return keys + continue keys (a:as) = do + {- Force lazy traversal with unsafeInterleaveIO. -} + morekeys <- unsafeInterleaveIO a + continue (morekeys++keys) as + + {- In indirect mode, look for the key. In direct mode, + - the inode cache file is only present when a key's content + - is present. -} + present False d = doesFileExist $ contentfile d + present True d = doesFileExist $ contentfile d ++ ".cache" + contentfile d = d takeFileName d + +{- Things to do to record changes to content when shutting down. + - + - It's acceptable to avoid committing changes to the branch, + - especially if performing a short-lived action. + -} +saveState :: Bool -> Annex () +saveState nocommit = doSideAction $ do + Annex.Queue.flush + unless nocommit $ + whenM (annexAlwaysCommit <$> Annex.getGitConfig) $ + Annex.Branch.commit "update" + +{- Downloads content from any of a list of urls. -} +downloadUrl :: [Url.URLString] -> FilePath -> Annex Bool +downloadUrl urls file = go =<< annexWebDownloadCommand <$> Annex.getGitConfig + where + go Nothing = do + opts <- map Param . annexWebOptions <$> Annex.getGitConfig + headers <- getHttpHeaders + liftIO $ anyM (\u -> Url.download u headers opts file) urls + go (Just basecmd) = liftIO $ anyM (downloadcmd basecmd) urls + downloadcmd basecmd url = + boolSystem "sh" [Param "-c", Param $ gencmd url basecmd] + <&&> doesFileExist file + gencmd url = massReplace + [ ("%file", shellEscape file) + , ("%url", shellEscape url) + ] + +{- Copies a key's content, when present, to a temp file. + - This is used to speed up some rsyncs. -} +preseedTmp :: Key -> FilePath -> Annex Bool +preseedTmp key file = go =<< inAnnex key + where + go False = return False + go True = do + ok <- copy + when ok $ thawContent file + return ok + copy = ifM (liftIO $ doesFileExist file) + ( return True + , do + s <- calcRepo $ gitAnnexLocation key + liftIO $ copyFileExternal s file + ) + +{- Blocks writing to an annexed file, and modifies file permissions to + - allow reading it, per core.sharedRepository setting. -} +freezeContent :: FilePath -> Annex () +freezeContent file = unlessM crippledFileSystem $ + liftIO . go =<< fromRepo getSharedRepository + where + go GroupShared = modifyFileMode file $ + removeModes writeModes . + addModes [ownerReadMode, groupReadMode] + go AllShared = modifyFileMode file $ + removeModes writeModes . + addModes readModes + go _ = modifyFileMode file $ + removeModes writeModes . + addModes [ownerReadMode] + +{- Allows writing to an annexed file that freezeContent was called on + - before. -} +thawContent :: FilePath -> Annex () +thawContent file = unlessM crippledFileSystem $ + liftIO . go =<< fromRepo getSharedRepository + where + go GroupShared = groupWriteRead file + go AllShared = groupWriteRead file + go _ = allowWrite file diff --git a/Annex/Content/Direct.hs b/Annex/Content/Direct.hs new file mode 100644 index 0000000000..6da7fab52c --- /dev/null +++ b/Annex/Content/Direct.hs @@ -0,0 +1,251 @@ +{- git-annex file content managing for direct mode + - + - Copyright 2012-2013 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Annex.Content.Direct ( + associatedFiles, + associatedFilesRelative, + removeAssociatedFile, + removeAssociatedFileUnchecked, + addAssociatedFile, + goodContent, + recordedInodeCache, + updateInodeCache, + addInodeCache, + writeInodeCache, + compareInodeCaches, + compareInodeCachesWith, + sameInodeCache, + elemInodeCaches, + sameFileStatus, + removeInodeCache, + toInodeCache, + inodesChanged, + createInodeSentinalFile, + addContentWhenNotPresent, +) where + +import Common.Annex +import qualified Annex +import Annex.Perms +import qualified Git +import Utility.Tmp +import Logs.Location +import Utility.InodeCache +import Utility.CopyFile +import Annex.ReplaceFile +import Annex.Link + +{- Absolute FilePaths of Files in the tree that are associated with a key. -} +associatedFiles :: Key -> Annex [FilePath] +associatedFiles key = do + files <- associatedFilesRelative key + top <- fromRepo Git.repoPath + return $ map (top ) files + +{- List of files in the tree that are associated with a key, relative to + - the top of the repo. -} +associatedFilesRelative :: Key -> Annex [FilePath] +associatedFilesRelative key = do + mapping <- calcRepo $ gitAnnexMapping key + liftIO $ catchDefaultIO [] $ do + h <- openFile mapping ReadMode + fileEncoding h + lines <$> hGetContents h + +{- Changes the associated files information for a key, applying a + - transformation to the list. Returns new associatedFiles value. -} +changeAssociatedFiles :: Key -> ([FilePath] -> [FilePath]) -> Annex [FilePath] +changeAssociatedFiles key transform = do + mapping <- calcRepo $ gitAnnexMapping key + files <- associatedFilesRelative key + let files' = transform files + when (files /= files') $ do + createContentDir mapping + liftIO $ viaTmp write mapping $ unlines files' + top <- fromRepo Git.repoPath + return $ map (top ) files' + where + write file content = do + h <- openFile file WriteMode + fileEncoding h + hPutStr h content + hClose h + +{- Removes an associated file. Returns new associatedFiles value. + - Checks if this was the last copy of the object, and updates location + - log. -} +removeAssociatedFile :: Key -> FilePath -> Annex [FilePath] +removeAssociatedFile key file = do + fs <- removeAssociatedFileUnchecked key file + when (null fs) $ + logStatus key InfoMissing + return fs + +{- Removes an associated file. Returns new associatedFiles value. -} +removeAssociatedFileUnchecked :: Key -> FilePath -> Annex [FilePath] +removeAssociatedFileUnchecked key file = do + file' <- normaliseAssociatedFile file + changeAssociatedFiles key $ filter (/= file') + +{- Adds an associated file. Returns new associatedFiles value. -} +addAssociatedFile :: Key -> FilePath -> Annex [FilePath] +addAssociatedFile key file = do + file' <- normaliseAssociatedFile file + changeAssociatedFiles key $ \files -> + if file' `elem` files + then files + else file':files + +{- Associated files are always stored relative to the top of the repository. + - The input FilePath is relative to the CWD. -} +normaliseAssociatedFile :: FilePath -> Annex FilePath +normaliseAssociatedFile file = do + top <- fromRepo Git.repoPath + liftIO $ relPathDirToFile top <$> absPath file + +{- Checks if a file in the tree, associated with a key, has not been modified. + - + - To avoid needing to fsck the file's content, which can involve an + - expensive checksum, this relies on a cache that contains the file's + - expected mtime and inode. + -} +goodContent :: Key -> FilePath -> Annex Bool +goodContent key file = sameInodeCache file =<< recordedInodeCache key + +{- Gets the recorded inode cache for a key. + - + - A key can be associated with multiple files, so may return more than + - one. -} +recordedInodeCache :: Key -> Annex [InodeCache] +recordedInodeCache key = withInodeCacheFile key $ \f -> + liftIO $ catchDefaultIO [] $ + mapMaybe readInodeCache . lines <$> readFileStrict f + +{- Caches an inode for a file. + - + - Anything else already cached is preserved. + -} +updateInodeCache :: Key -> FilePath -> Annex () +updateInodeCache key file = maybe noop (addInodeCache key) + =<< liftIO (genInodeCache file) + +{- Adds another inode to the cache for a key. -} +addInodeCache :: Key -> InodeCache -> Annex () +addInodeCache key cache = do + oldcaches <- recordedInodeCache key + unlessM (elemInodeCaches cache oldcaches) $ + writeInodeCache key (cache:oldcaches) + +{- Writes inode cache for a key. -} +writeInodeCache :: Key -> [InodeCache] -> Annex () +writeInodeCache key caches = withInodeCacheFile key $ \f -> do + createContentDir f + liftIO $ writeFile f $ + unlines $ map showInodeCache caches + +{- Removes an inode cache. -} +removeInodeCache :: Key -> Annex () +removeInodeCache key = withInodeCacheFile key $ \f -> do + createContentDir f -- also thaws directory + liftIO $ nukeFile f + +withInodeCacheFile :: Key -> (FilePath -> Annex a) -> Annex a +withInodeCacheFile key a = a =<< calcRepo (gitAnnexInodeCache key) + +{- Checks if a InodeCache matches the current version of a file. -} +sameInodeCache :: FilePath -> [InodeCache] -> Annex Bool +sameInodeCache _ [] = return False +sameInodeCache file old = go =<< liftIO (genInodeCache file) + where + go Nothing = return False + go (Just curr) = elemInodeCaches curr old + +{- Checks if a FileStatus matches the recorded InodeCache of a file. -} +sameFileStatus :: Key -> FileStatus -> Annex Bool +sameFileStatus key status = do + old <- recordedInodeCache key + let curr = toInodeCache status + case (old, curr) of + (_, Just c) -> elemInodeCaches c old + ([], Nothing) -> return True + _ -> return False + +{- If the inodes have changed, only the size and mtime are compared. -} +compareInodeCaches :: InodeCache -> InodeCache -> Annex Bool +compareInodeCaches x y + | compareStrong x y = return True + | otherwise = ifM inodesChanged + ( return $ compareWeak x y + , return False + ) + +elemInodeCaches :: InodeCache -> [InodeCache] -> Annex Bool +elemInodeCaches _ [] = return False +elemInodeCaches c (l:ls) = ifM (compareInodeCaches c l) + ( return True + , elemInodeCaches c ls + ) + +compareInodeCachesWith :: Annex InodeComparisonType +compareInodeCachesWith = ifM inodesChanged ( return Weakly, return Strongly ) + +{- Copies the contentfile to the associated file, if the associated + - file has no content. If the associated file does have content, + - even if the content differs, it's left unchanged. -} +addContentWhenNotPresent :: Key -> FilePath -> FilePath -> Annex () +addContentWhenNotPresent key contentfile associatedfile = do + v <- isAnnexLink associatedfile + when (Just key == v) $ do + replaceFile associatedfile $ + liftIO . void . copyFileExternal contentfile + updateInodeCache key associatedfile + +{- 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. + - + - If the sentinal file does not exist, we have to assume that the + - inodes have changed. + -} +inodesChanged :: Annex Bool +inodesChanged = maybe calc return =<< Annex.getState Annex.inodeschanged + where + calc = do + scache <- liftIO . genInodeCache + =<< fromRepo gitAnnexInodeSentinal + scached <- readInodeSentinalFile + let changed = case (scache, scached) of + (Just c1, Just c2) -> not $ compareStrong c1 c2 + _ -> True + Annex.changeState $ \s -> s { Annex.inodeschanged = Just changed } + return changed + +readInodeSentinalFile :: Annex (Maybe InodeCache) +readInodeSentinalFile = do + sentinalcachefile <- fromRepo gitAnnexInodeSentinalCache + liftIO $ catchDefaultIO Nothing $ + readInodeCache <$> readFile sentinalcachefile + +writeInodeSentinalFile :: Annex () +writeInodeSentinalFile = do + sentinalfile <- fromRepo gitAnnexInodeSentinal + createAnnexDirectory (parentDir sentinalfile) + sentinalcachefile <- fromRepo gitAnnexInodeSentinalCache + liftIO $ writeFile sentinalfile "" + liftIO $ maybe noop (writeFile sentinalcachefile . showInodeCache) + =<< genInodeCache sentinalfile + +{- The sentinal file is only created when first initializing a repository. + - If there are any annexed objects in the repository already, creating + - the file would invalidate their inode caches. -} +createInodeSentinalFile :: Annex () +createInodeSentinalFile = + unlessM (alreadyexists <||> hasobjects) + writeInodeSentinalFile + where + alreadyexists = isJust <$> readInodeSentinalFile + hasobjects = liftIO . doesDirectoryExist =<< fromRepo gitAnnexObjectDir diff --git a/Annex/Direct.hs b/Annex/Direct.hs new file mode 100644 index 0000000000..d2e2cdc00a --- /dev/null +++ b/Annex/Direct.hs @@ -0,0 +1,232 @@ +{- git-annex direct mode + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Annex.Direct where + +import Common.Annex +import qualified Git +import qualified Git.LsFiles +import qualified Git.Merge +import qualified Git.DiffTree as DiffTree +import Git.Sha +import Git.Types +import Annex.CatFile +import Utility.FileMode +import qualified Annex.Queue +import Logs.Location +import Backend +import Types.KeySource +import Annex.Content +import Annex.Content.Direct +import Annex.Link +import Utility.InodeCache +import Utility.CopyFile +import Annex.Perms +import Annex.ReplaceFile +import Annex.Exception + +{- Uses git ls-files to find files that need to be committed, and stages + - them into the index. Returns True if some changes were staged. -} +stageDirect :: Annex Bool +stageDirect = do + Annex.Queue.flush + top <- fromRepo Git.repoPath + (l, cleanup) <- inRepo $ Git.LsFiles.stagedOthersDetails [top] + forM_ l go + void $ liftIO cleanup + staged <- Annex.Queue.size + Annex.Queue.flush + return $ staged /= 0 + where + {- Determine what kind of modified or deleted file this is, as + - efficiently as we can, by getting any key that's associated + - with it in git, as well as its stat info. -} + go (file, Just sha) = do + shakey <- catKey sha + mstat <- liftIO $ catchMaybeIO $ getSymbolicLinkStatus file + filekey <- isAnnexLink file + case (shakey, filekey, mstat, toInodeCache =<< mstat) of + (_, Just key, _, _) + | shakey == filekey -> noop + {- A changed symlink. -} + | otherwise -> stageannexlink file key + (Just key, _, _, Just cache) -> do + {- All direct mode files will show as + - modified, so compare the cache to see if + - it really was. -} + oldcache <- recordedInodeCache key + case oldcache of + [] -> modifiedannexed file key cache + _ -> unlessM (elemInodeCaches cache oldcache) $ + modifiedannexed file key cache + (Just key, _, Nothing, _) -> deletedannexed file key + (Nothing, _, Nothing, _) -> deletegit file + (_, _, Just _, _) -> addgit file + go _ = noop + + modifiedannexed file oldkey cache = do + void $ removeAssociatedFile oldkey file + void $ addDirect file cache + + deletedannexed file key = do + void $ removeAssociatedFile key file + deletegit file + + stageannexlink file key = do + l <- inRepo $ gitAnnexLink file key + stageSymlink file =<< hashSymlink l + void $ addAssociatedFile key file + + addgit file = Annex.Queue.addCommand "add" [Param "-f"] [file] + + deletegit file = Annex.Queue.addCommand "rm" [Param "-f"] [file] + +{- Adds a file to the annex in direct mode. Can fail, if the file is + - modified or deleted while it's being added. -} +addDirect :: FilePath -> InodeCache -> Annex Bool +addDirect file cache = do + showStart "add" file + let source = KeySource + { keyFilename = file + , contentLocation = file + , inodeCache = Just cache + } + got =<< genKey source =<< chooseBackend file + where + got Nothing = do + showEndFail + return False + got (Just (key, _)) = ifM (sameInodeCache file [cache]) + ( do + l <- inRepo $ gitAnnexLink file key + stageSymlink file =<< hashSymlink l + addInodeCache key cache + void $ addAssociatedFile key file + logStatus key InfoPresent + showEndOk + return True + , do + showEndFail + return False + ) + +{- In direct mode, git merge would usually refuse to do anything, since it + - sees present direct mode files as type changed files. To avoid this, + - merge is run with the work tree set to a temp directory. + - + - This should only be used once any changes to the real working tree have + - already been committed, because it overwrites files in the working tree. + -} +mergeDirect :: FilePath -> Git.Ref -> Git.Repo -> IO Bool +mergeDirect d branch g = do + createDirectoryIfMissing True d + let g' = g { location = Local { gitdir = Git.localGitDir g, worktree = Just d } } + Git.Merge.mergeNonInteractive branch g' + +{- Cleans up after a direct mode merge. The merge must have been committed, + - and the commit sha passed in, along with the old sha of the tree + - before the merge. Uses git diff-tree to find files that changed between + - the two shas, and applies those changes to the work tree. + -} +mergeDirectCleanup :: FilePath -> Git.Ref -> Git.Ref -> Annex () +mergeDirectCleanup d oldsha newsha = do + (items, cleanup) <- inRepo $ DiffTree.diffTreeRecursive oldsha newsha + forM_ items updated + void $ liftIO cleanup + liftIO $ removeDirectoryRecursive d + where + updated item = do + void $ tryAnnex $ + go DiffTree.srcsha DiffTree.srcmode moveout moveout_raw + void $ tryAnnex $ + go DiffTree.dstsha DiffTree.dstmode movein movein_raw + where + go getsha getmode a araw + | getsha item == nullSha = noop + | isSymLink (getmode item) = + maybe (araw f) (\k -> void $ a k f) + =<< catKey (getsha item) + | otherwise = araw f + f = DiffTree.file item + + moveout = removeDirect + + {- Files deleted by the merge are removed from the work tree. + - Empty work tree directories are removed, per git behavior. -} + moveout_raw f = liftIO $ do + nukeFile f + void $ tryIO $ removeDirectory $ parentDir f + + {- If the file is already present, with the right content for the + - key, it's left alone. Otherwise, create the symlink and then + - if possible, replace it with the content. -} + movein k f = unlessM (goodContent k f) $ do + l <- inRepo $ gitAnnexLink f k + replaceFile f $ makeAnnexLink l + toDirect k f + + {- Any new, modified, or renamed files were written to the temp + - directory by the merge, and are moved to the real work tree. -} + movein_raw f = liftIO $ do + createDirectoryIfMissing True $ parentDir f + void $ tryIO $ rename (d f) f + +{- If possible, converts a symlink in the working tree into a direct + - mode file. If the content is not available, leaves the symlink + - unchanged. -} +toDirect :: Key -> FilePath -> Annex () +toDirect k f = fromMaybe noop =<< toDirectGen k f + +toDirectGen :: Key -> FilePath -> Annex (Maybe (Annex ())) +toDirectGen k f = do + loc <- calcRepo $ gitAnnexLocation k + ifM (liftIO $ doesFileExist loc) + ( return $ Just $ fromindirect loc + , do + {- Copy content from another direct file. -} + absf <- liftIO $ absPath f + dlocs <- filterM (goodContent k) =<< + filterM (\l -> isNothing <$> getAnnexLinkTarget l) =<< + (filter (/= absf) <$> addAssociatedFile k f) + case dlocs of + [] -> return Nothing + (dloc:_) -> return $ Just $ fromdirect dloc + ) + where + fromindirect loc = do + {- Move content from annex to direct file. -} + thawContentDir loc + updateInodeCache k loc + void $ addAssociatedFile k f + thawContent loc + replaceFile f $ liftIO . moveFile loc + fromdirect loc = do + replaceFile f $ + liftIO . void . copyFileExternal loc + updateInodeCache k f + +{- Removes a direct mode file, while retaining its content in the annex + - (unless its content has already been changed). -} +removeDirect :: Key -> FilePath -> Annex () +removeDirect k f = do + void $ removeAssociatedFileUnchecked k f + unlessM (inAnnex k) $ + ifM (goodContent k f) + ( moveAnnex k f + , logStatus k InfoMissing + ) + liftIO $ do + nukeFile f + void $ tryIO $ removeDirectory $ parentDir f + +{- Called when a direct mode file has been changed. Its old content may be + - lost. -} +changedDirect :: Key -> FilePath -> Annex () +changedDirect oldk f = do + locs <- removeAssociatedFile oldk f + whenM (pure (null locs) <&&> not <$> inAnnex oldk) $ + logStatus oldk InfoMissing diff --git a/Annex/Environment.hs b/Annex/Environment.hs new file mode 100644 index 0000000000..ae5a5646fc --- /dev/null +++ b/Annex/Environment.hs @@ -0,0 +1,65 @@ +{- git-annex environment + - + - Copyright 2012, 2013 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE CPP #-} + +module Annex.Environment where + +import Common.Annex +import Utility.UserInfo +import qualified Git.Config +import Config +import Annex.Exception + +#ifndef mingw32_HOST_OS +import Utility.Env +#endif + +{- Checks that the system's environment allows git to function. + - Git requires a GECOS username, or suitable git configuration, or + - environment variables. + - + - Git also requires the system have a hostname containing a dot. + - Otherwise, it tries various methods to find a FQDN, and will fail if it + - does not. To avoid replicating that code here, which would break if its + - methods change, this function does not check the hostname is valid. + - Instead, code that commits can use ensureCommit. + -} +checkEnvironment :: Annex () +checkEnvironment = do + gitusername <- fromRepo $ Git.Config.getMaybe "user.name" + when (gitusername == Nothing || gitusername == Just "") $ + liftIO checkEnvironmentIO + +checkEnvironmentIO :: IO () +checkEnvironmentIO = +#ifdef mingw32_HOST_OS + noop +#else + whenM (null <$> myUserGecos) $ do + username <- myUserName + ensureEnv "GIT_AUTHOR_NAME" username + ensureEnv "GIT_COMMITTER_NAME" username + where +#ifndef __ANDROID__ + -- existing environment is not overwritten + ensureEnv var val = void $ setEnv var val False +#else + -- Environment setting is broken on Android, so this is dealt with + -- in runshell instead. + ensureEnv _ _ = noop +#endif +#endif + +{- Runs an action that commits to the repository, and if it fails, + - sets user.email to a dummy value and tries the action again. -} +ensureCommit :: Annex a -> Annex a +ensureCommit a = either retry return =<< tryAnnex a + where + retry _ = do + setConfig (ConfigKey "user.email") =<< liftIO myUserName + a diff --git a/Annex/Exception.hs b/Annex/Exception.hs new file mode 100644 index 0000000000..99466a8519 --- /dev/null +++ b/Annex/Exception.hs @@ -0,0 +1,39 @@ +{- exception handling in the git-annex monad + - + - Note that when an Annex action fails and the exception is handled + - by these functions, any changes the action has made to the + - AnnexState are retained. This works because the Annex monad + - internally stores the AnnexState in a MVar. + - + - Copyright 2011-2013 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Annex.Exception ( + bracketIO, + tryAnnex, + throwAnnex, + catchAnnex, +) where + +import qualified "MonadCatchIO-transformers" Control.Monad.CatchIO as M +import Control.Exception + +import Common.Annex + +{- Runs an Annex action, with setup and cleanup both in the IO monad. -} +bracketIO :: IO v -> (v -> IO b) -> (v -> Annex a) -> Annex a +bracketIO setup cleanup go = M.bracket (liftIO setup) (liftIO . cleanup) go + +{- try in the Annex monad -} +tryAnnex :: Annex a -> Annex (Either SomeException a) +tryAnnex = M.try + +{- throw in the Annex monad -} +throwAnnex :: Exception e => e -> Annex a +throwAnnex = M.throw + +{- catch in the Annex monad -} +catchAnnex :: Exception e => Annex a -> (e -> Annex a) -> Annex a +catchAnnex = M.catch diff --git a/Annex/FileMatcher.hs b/Annex/FileMatcher.hs new file mode 100644 index 0000000000..3abba10557 --- /dev/null +++ b/Annex/FileMatcher.hs @@ -0,0 +1,101 @@ +{- git-annex file matching + - + - Copyright 2012, 2013 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Annex.FileMatcher where + +import qualified Data.Map as M + +import Common.Annex +import Limit +import Utility.Matcher +import Types.Group +import Logs.Group +import Logs.Remote +import Annex.UUID +import qualified Annex +import Types.FileMatcher +import Git.FilePath +import Types.Remote (RemoteConfig) + +import Data.Either +import qualified Data.Set as S + +type FileMatcher = Matcher MatchFiles + +checkFileMatcher :: FileMatcher -> FilePath -> Annex Bool +checkFileMatcher matcher file = checkFileMatcher' matcher file S.empty True + +checkFileMatcher' :: FileMatcher -> FilePath -> AssumeNotPresent -> Bool -> Annex Bool +checkFileMatcher' matcher file notpresent def + | isEmpty matcher = return def + | otherwise = do + matchfile <- getTopFilePath <$> inRepo (toTopFilePath file) + let fi = FileInfo + { matchFile = matchfile + , relFile = file + } + matchMrun matcher $ \a -> a notpresent fi + +matchAll :: FileMatcher +matchAll = generate [] + +parsedToMatcher :: [Either String (Token MatchFiles)] -> Either String FileMatcher +parsedToMatcher parsed = case partitionEithers parsed of + ([], vs) -> Right $ generate vs + (es, _) -> Left $ unwords $ map ("Parse failure: " ++) es + +exprParser :: GroupMap -> M.Map UUID RemoteConfig -> Maybe UUID -> String -> [Either String (Token MatchFiles)] +exprParser groupmap configmap mu expr = + map parse $ tokenizeMatcher expr + where + parse = parseToken + (limitPresent mu) + (limitInDir preferreddir) + groupmap + preferreddir = fromMaybe "public" $ + M.lookup "preferreddir" =<< (`M.lookup` configmap) =<< mu + +parseToken :: MkLimit -> MkLimit -> GroupMap -> String -> Either String (Token MatchFiles) +parseToken checkpresent checkpreferreddir groupmap t + | t `elem` tokens = Right $ token t + | t == "present" = use checkpresent + | t == "inpreferreddir" = use checkpreferreddir + | otherwise = maybe (Left $ "near " ++ show t) use $ M.lookup k $ + M.fromList + [ ("include", limitInclude) + , ("exclude", limitExclude) + , ("copies", limitCopies) + , ("inbackend", limitInBackend) + , ("largerthan", limitSize (>)) + , ("smallerthan", limitSize (<)) + , ("inallgroup", limitInAllGroup groupmap) + ] + where + (k, v) = separate (== '=') t + use a = Operation <$> a v + +{- This is really dumb tokenization; there's no support for quoted values. + - Open and close parens are always treated as standalone tokens; + - otherwise tokens must be separated by whitespace. -} +tokenizeMatcher :: String -> [String] +tokenizeMatcher = filter (not . null ) . concatMap splitparens . words + where + splitparens = segmentDelim (`elem` "()") + +{- Generates a matcher for files large enough (or meeting other criteria) + - to be added to the annex, rather than directly to git. -} +largeFilesMatcher :: Annex FileMatcher +largeFilesMatcher = go =<< annexLargeFiles <$> Annex.getGitConfig + where + go Nothing = return matchAll + go (Just expr) = do + gm <- groupMap + rc <- readRemoteLog + u <- getUUID + either badexpr return $ + parsedToMatcher $ exprParser gm rc (Just u) expr + badexpr e = error $ "bad annex.largefiles configuration: " ++ e diff --git a/Annex/Journal.hs b/Annex/Journal.hs new file mode 100644 index 0000000000..fff20ccc46 --- /dev/null +++ b/Annex/Journal.hs @@ -0,0 +1,104 @@ +{- management of the git-annex journal + - + - The journal is used to queue up changes before they are committed to the + - git-annex branch. Amoung other things, it ensures that if git-annex is + - interrupted, its recorded data is not lost. + - + - Copyright 2011 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE CPP #-} + +module Annex.Journal where + +import System.IO.Binary + +import Common.Annex +import Annex.Exception +import qualified Git +import Annex.Perms + +{- Records content for a file in the branch to the journal. + - + - Using the journal, rather than immediatly staging content to the index + - avoids git needing to rewrite the index after every change. -} +setJournalFile :: FilePath -> String -> Annex () +setJournalFile file content = do + createAnnexDirectory =<< fromRepo gitAnnexJournalDir + createAnnexDirectory =<< fromRepo gitAnnexTmpDir + -- journal file is written atomically + jfile <- fromRepo $ journalFile file + tmp <- fromRepo gitAnnexTmpDir + let tmpfile = tmp takeFileName jfile + liftIO $ do + writeBinaryFile tmpfile content + moveFile tmpfile jfile + +{- Gets any journalled content for a file in the branch. -} +getJournalFile :: FilePath -> Annex (Maybe String) +getJournalFile file = inRepo $ \g -> catchMaybeIO $ + readFileStrict $ journalFile file g + +{- List of files that have updated content in the journal. -} +getJournalledFiles :: Annex [FilePath] +getJournalledFiles = map fileJournal <$> getJournalFiles + +{- List of existing journal files. -} +getJournalFiles :: Annex [FilePath] +getJournalFiles = do + g <- gitRepo + fs <- liftIO $ catchDefaultIO [] $ + getDirectoryContents $ gitAnnexJournalDir g + return $ filter (`notElem` [".", ".."]) fs + +{- Checks if there are changes in the journal. -} +journalDirty :: Annex Bool +journalDirty = not . null <$> getJournalFiles + +{- Produces a filename to use in the journal for a file on the branch. + - + - The journal typically won't have a lot of files in it, so the hashing + - used in the branch is not necessary, and all the files are put directly + - in the journal directory. + -} +journalFile :: FilePath -> Git.Repo -> FilePath +journalFile file repo = gitAnnexJournalDir repo concatMap mangle file + where + mangle c + | c == pathSeparator = "_" + | c == '_' = "__" + | otherwise = [c] + +{- Converts a journal file (relative to the journal dir) back to the + - filename on the branch. -} +fileJournal :: FilePath -> FilePath +fileJournal = replace [pathSeparator, pathSeparator] "_" . + replace "_" [pathSeparator] + +{- Runs an action that modifies the journal, using locking to avoid + - contention with other git-annex processes. -} +lockJournal :: Annex a -> Annex a +lockJournal a = do + lockfile <- fromRepo gitAnnexJournalLock + createAnnexDirectory $ takeDirectory lockfile + mode <- annexFileMode + bracketIO (lock lockfile mode) unlock (const a) + where +#ifndef mingw32_HOST_OS + lock lockfile mode = do + l <- noUmask mode $ createFile lockfile mode + waitToSetLock l (WriteLock, AbsoluteSeek, 0, 0) + return l +#else + lock lockfile _mode = do + writeFile lockfile "" + return lockfile +#endif +#ifndef mingw32_HOST_OS + unlock = closeFd +#else + unlock = removeFile +#endif + diff --git a/Annex/Link.hs b/Annex/Link.hs new file mode 100644 index 0000000000..becd7e7ece --- /dev/null +++ b/Annex/Link.hs @@ -0,0 +1,105 @@ +{- git-annex links to content + - + - On file systems that support them, symlinks are used. + - + - On other filesystems, git instead stores the symlink target in a regular + - file. + - + - Copyright 2013 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Annex.Link where + +import Common.Annex +import qualified Annex +import qualified Git.HashObject +import qualified Git.UpdateIndex +import qualified Annex.Queue +import Git.Types +import Git.FilePath + +type LinkTarget = String + +{- Checks if a file is a link to a key. -} +isAnnexLink :: FilePath -> Annex (Maybe Key) +isAnnexLink file = maybe Nothing (fileKey . takeFileName) <$> getAnnexLinkTarget file + +{- Gets the link target of a symlink. + - + - On a filesystem that does not support symlinks, fall back to getting the + - link target by looking inside the file. + - + - Returns Nothing if the file is not a symlink, or not a link to annex + - content. + -} +getAnnexLinkTarget :: FilePath -> Annex (Maybe LinkTarget) +getAnnexLinkTarget file = ifM (coreSymlinks <$> Annex.getGitConfig) + ( check readSymbolicLink $ + return Nothing + , check readSymbolicLink $ + check probefilecontent $ + return Nothing + ) + where + check getlinktarget fallback = do + v <- liftIO $ catchMaybeIO $ getlinktarget file + case v of + Just l + | isLinkToAnnex (fromInternalGitPath l) -> return v + | otherwise -> return Nothing + Nothing -> fallback + + probefilecontent f = do + h <- openFile f ReadMode + fileEncoding h + -- The first 8k is more than enough to read; link + -- files are small. + s <- take 8192 <$> hGetContents h + -- If we got the full 8k, the file is too large + if length s == 8192 + then do + hClose h + return "" + else do + hClose h + -- If there are any NUL or newline + -- characters, or whitespace, we + -- certianly don't have a link to a + -- git-annex key. + if any (`elem` s) "\0\n\r \t" + then return "" + else return s + +{- Creates a link on disk. + - + - On a filesystem that does not support symlinks, writes the link target + - to a file. Note that git will only treat the file as a symlink if + - it's staged as such, so use addAnnexLink when adding a new file or + - modified link to git. + -} +makeAnnexLink :: LinkTarget -> FilePath -> Annex () +makeAnnexLink linktarget file = ifM (coreSymlinks <$> Annex.getGitConfig) + ( liftIO $ do + void $ tryIO $ removeFile file + createSymbolicLink linktarget file + , liftIO $ writeFile file linktarget + ) + +{- Creates a link on disk, and additionally stages it in git. -} +addAnnexLink :: LinkTarget -> FilePath -> Annex () +addAnnexLink linktarget file = do + makeAnnexLink linktarget file + stageSymlink file =<< hashSymlink linktarget + +{- Injects a symlink target into git, returning its Sha. -} +hashSymlink :: LinkTarget -> Annex Sha +hashSymlink linktarget = inRepo $ Git.HashObject.hashObject BlobObject $ + toInternalGitPath linktarget + +{- Stages a symlink to the annex, using a Sha of its target. -} +stageSymlink :: FilePath -> Sha -> Annex () +stageSymlink file sha = + Annex.Queue.addUpdateIndex =<< + inRepo (Git.UpdateIndex.stageSymlink file sha) diff --git a/Annex/LockPool.hs b/Annex/LockPool.hs new file mode 100644 index 0000000000..a9a0f31019 --- /dev/null +++ b/Annex/LockPool.hs @@ -0,0 +1,56 @@ +{- git-annex lock pool + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE CPP #-} + +module Annex.LockPool where + +import qualified Data.Map as M +import System.Posix.Types (Fd) + +import Common.Annex +import Annex +#ifndef mingw32_HOST_OS +import Annex.Perms +#endif + +{- Create a specified lock file, and takes a shared lock. -} +lockFile :: FilePath -> Annex () +lockFile file = go =<< fromPool file + where + go (Just _) = noop -- already locked + go Nothing = do +#ifndef mingw32_HOST_OS + mode <- annexFileMode + fd <- liftIO $ noUmask mode $ + openFd file ReadOnly (Just mode) defaultFileFlags + liftIO $ waitToSetLock fd (ReadLock, AbsoluteSeek, 0, 0) +#else + liftIO $ writeFile file "" + let fd = 0 +#endif + changePool $ M.insert file fd + +unlockFile :: FilePath -> Annex () +unlockFile file = maybe noop go =<< fromPool file + where + go fd = do +#ifndef mingw32_HOST_OS + liftIO $ closeFd fd +#endif + changePool $ M.delete file + +getPool :: Annex (M.Map FilePath Fd) +getPool = getState lockpool + +fromPool :: FilePath -> Annex (Maybe Fd) +fromPool file = M.lookup file <$> getPool + +changePool :: (M.Map FilePath Fd -> M.Map FilePath Fd) -> Annex () +changePool a = do + m <- getPool + changeState $ \s -> s { lockpool = a m } diff --git a/Annex/Perms.hs b/Annex/Perms.hs new file mode 100644 index 0000000000..f5925b741a --- /dev/null +++ b/Annex/Perms.hs @@ -0,0 +1,105 @@ +{- git-annex file permissions + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Annex.Perms ( + setAnnexPerm, + annexFileMode, + createAnnexDirectory, + noUmask, + createContentDir, + freezeContentDir, + thawContentDir, +) where + +import Common.Annex +import Utility.FileMode +import Git.SharedRepository +import qualified Annex +import Config + +import System.Posix.Types + +withShared :: (SharedRepository -> Annex a) -> Annex a +withShared a = maybe startup a =<< Annex.getState Annex.shared + where + startup = do + shared <- fromRepo getSharedRepository + Annex.changeState $ \s -> s { Annex.shared = Just shared } + a shared + +{- Sets appropriate file mode for a file or directory in the annex, + - other than the content files and content directory. Normally, + - use the default mode, but with core.sharedRepository set, + - allow the group to write, etc. -} +setAnnexPerm :: FilePath -> Annex () +setAnnexPerm file = unlessM crippledFileSystem $ + withShared $ liftIO . go + where + go GroupShared = groupWriteRead file + go AllShared = modifyFileMode file $ addModes $ + [ ownerWriteMode, groupWriteMode ] ++ readModes + go _ = noop + +{- Gets the appropriate mode to use for creating a file in the annex + - (other than content files, which are locked down more). -} +annexFileMode :: Annex FileMode +annexFileMode = withShared $ return . go + where + go GroupShared = sharedmode + go AllShared = combineModes (sharedmode:readModes) + go _ = stdFileMode + sharedmode = combineModes + [ ownerWriteMode, groupWriteMode + , ownerReadMode, groupReadMode + ] + +{- Creates a directory inside the gitAnnexDir, including any parent + - directories. Makes directories with appropriate permissions. -} +createAnnexDirectory :: FilePath -> Annex () +createAnnexDirectory dir = traverse dir [] =<< top + where + top = parentDir <$> fromRepo gitAnnexDir + traverse d below stop + | d `equalFilePath` stop = done + | otherwise = ifM (liftIO $ doesDirectoryExist d) + ( done + , traverse (parentDir d) (d:below) stop + ) + where + done = forM_ below $ \p -> do + liftIO $ createDirectoryIfMissing True p + setAnnexPerm p + +{- Blocks writing to the directory an annexed file is in, to prevent the + - file accidentially being deleted. However, if core.sharedRepository + - is set, this is not done, since the group must be allowed to delete the + - file. + -} +freezeContentDir :: FilePath -> Annex () +freezeContentDir file = unlessM crippledFileSystem $ + liftIO . go =<< fromRepo getSharedRepository + where + dir = parentDir file + go GroupShared = groupWriteRead dir + go AllShared = groupWriteRead dir + go _ = preventWrite dir + +thawContentDir :: FilePath -> Annex () +thawContentDir file = unlessM crippledFileSystem $ + liftIO $ allowWrite $ parentDir file + +{- Makes the directory tree to store an annexed file's content, + - with appropriate permissions on each level. -} +createContentDir :: FilePath -> Annex () +createContentDir dest = do + unlessM (liftIO $ doesDirectoryExist dir) $ + createAnnexDirectory dir + -- might have already existed with restricted perms + unlessM crippledFileSystem $ + liftIO $ allowWrite dir + where + dir = parentDir dest diff --git a/Annex/Queue.hs b/Annex/Queue.hs new file mode 100644 index 0000000000..a5ef600379 --- /dev/null +++ b/Annex/Queue.hs @@ -0,0 +1,62 @@ +{- git-annex command queue + - + - Copyright 2011, 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Annex.Queue ( + addCommand, + addUpdateIndex, + flush, + flushWhenFull, + size +) where + +import Common.Annex +import Annex hiding (new) +import qualified Git.Queue +import qualified Git.UpdateIndex + +{- Adds a git command to the queue. -} +addCommand :: String -> [CommandParam] -> [FilePath] -> Annex () +addCommand command params files = do + q <- get + store <=< inRepo $ Git.Queue.addCommand command params files q + +{- Adds an update-index stream to the queue. -} +addUpdateIndex :: Git.UpdateIndex.Streamer -> Annex () +addUpdateIndex streamer = do + q <- get + store <=< inRepo $ Git.Queue.addUpdateIndex streamer q + +{- Runs the queue if it is full. Should be called periodically. -} +flushWhenFull :: Annex () +flushWhenFull = do + q <- get + when (Git.Queue.full q) flush + +{- Runs (and empties) the queue. -} +flush :: Annex () +flush = do + q <- get + unless (0 == Git.Queue.size q) $ do + showStoringStateAction + q' <- inRepo $ Git.Queue.flush q + store q' + +{- Gets the size of the queue. -} +size :: Annex Int +size = Git.Queue.size <$> get + +get :: Annex Git.Queue.Queue +get = maybe new return =<< getState repoqueue + +new :: Annex Git.Queue.Queue +new = do + q <- Git.Queue.new . annexQueueSize <$> getGitConfig + store q + return q + +store :: Git.Queue.Queue -> Annex () +store q = changeState $ \s -> s { repoqueue = Just q } diff --git a/Annex/ReplaceFile.hs b/Annex/ReplaceFile.hs new file mode 100644 index 0000000000..dd93b471c8 --- /dev/null +++ b/Annex/ReplaceFile.hs @@ -0,0 +1,39 @@ +{- git-annex file replacing + - + - Copyright 2013 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Annex.ReplaceFile where + +import Common.Annex +import Annex.Perms +import Annex.Exception + +{- Replaces a possibly already existing file with a new version, + - atomically, by running an action. + - + - The action is passed a temp file, which it can write to, and once + - done the temp file is moved into place. + - + - The action can throw an IO exception, in which case the temp file + - will be deleted, and the existing file will be preserved. + - + - Throws an IO exception when it was unable to replace the file. + -} +replaceFile :: FilePath -> (FilePath -> Annex ()) -> Annex () +replaceFile file a = do + tmpdir <- fromRepo gitAnnexTmpDir + void $ createAnnexDirectory tmpdir + bracketIO (setup tmpdir) nukeFile $ \tmpfile -> do + a tmpfile + liftIO $ catchIO (rename tmpfile file) (fallback tmpfile) + where + setup tmpdir = do + (tmpfile, h) <- openTempFileWithDefaultPermissions tmpdir "tmp" + hClose h + return tmpfile + fallback tmpfile _ = do + createDirectoryIfMissing True $ parentDir file + rename tmpfile file diff --git a/Annex/Ssh.hs b/Annex/Ssh.hs new file mode 100644 index 0000000000..6fd2c556cf --- /dev/null +++ b/Annex/Ssh.hs @@ -0,0 +1,177 @@ +{- git-annex ssh interface, with connection caching + - + - Copyright 2012,2013 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE CPP #-} + +module Annex.Ssh ( + sshCachingOptions, + sshCleanup, + sshCacheDir, + sshReadPort, +) where + +import qualified Data.Map as M +import Data.Hash.MD5 + +import Common.Annex +import Annex.LockPool +import qualified Build.SysConfig as SysConfig +import qualified Annex +import Config +import Utility.Env +#ifndef mingw32_HOST_OS +import Annex.Perms +#endif + +{- Generates parameters to ssh to a given host (or user@host) on a given + - port, with connection caching. -} +sshCachingOptions :: (String, Maybe Integer) -> [CommandParam] -> Annex [CommandParam] +sshCachingOptions (host, port) opts = go =<< sshInfo (host, port) + where + go (Nothing, params) = ret params + go (Just socketfile, params) = do + cleanstale + liftIO $ createDirectoryIfMissing True $ parentDir socketfile + lockFile $ socket2lock socketfile + ret params + ret ps = return $ ps ++ opts ++ portParams port ++ [Param "-T"] + -- 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. + cleanstale = whenM (not . any isLock . M.keys <$> getPool) $ + sshCleanup + +{- Returns a filename to use for a ssh connection caching socket, and + - parameters to enable ssh connection caching. -} +sshInfo :: (String, Maybe Integer) -> Annex (Maybe FilePath, [CommandParam]) +sshInfo (host, port) = go =<< sshCacheDir + where + go Nothing = return (Nothing, []) + go (Just dir) = do + let socketfile = dir hostport2socket host port + if valid_unix_socket_path socketfile + then return (Just socketfile, sshConnectionCachingParams socketfile) + else do + socketfile' <- liftIO $ relPathCwdToFile socketfile + if valid_unix_socket_path socketfile' + then return (Just socketfile', sshConnectionCachingParams socketfile') + else return (Nothing, []) + +sshConnectionCachingParams :: FilePath -> [CommandParam] +sshConnectionCachingParams socketfile = + [ Param "-S", Param socketfile + , Params "-o ControlMaster=auto -o ControlPersist=yes" + ] + +{- ssh connection caching creates sockets, so will not work on a + - crippled filesystem. A GIT_ANNEX_TMP_DIR can be provided to use + - a different filesystem. -} +sshCacheDir :: Annex (Maybe FilePath) +sshCacheDir + | SysConfig.sshconnectioncaching = ifM crippledFileSystem + ( maybe (return Nothing) usetmpdir =<< gettmpdir + , ifM (fromMaybe True . annexSshCaching <$> Annex.getGitConfig) + ( Just <$> fromRepo gitAnnexSshDir + , return Nothing + ) + ) + | otherwise = return Nothing + where + gettmpdir = liftIO $ getEnv "GIT_ANNEX_TMP_DIR" + usetmpdir tmpdir = liftIO $ catchMaybeIO $ do + createDirectoryIfMissing True tmpdir + return tmpdir + +portParams :: Maybe Integer -> [CommandParam] +portParams Nothing = [] +portParams (Just port) = [Param "-p", Param $ show port] + +{- Stop any unused ssh processes. -} +sshCleanup :: Annex () +sshCleanup = go =<< sshCacheDir + where + go Nothing = noop + go (Just dir) = do + sockets <- filter (not . isLock) <$> + liftIO (catchDefaultIO [] $ dirContents dir) + forM_ sockets cleanup + cleanup socketfile = do +#ifndef mingw32_HOST_OS + -- Drop any shared lock we have, and take an + -- exclusive lock, without blocking. If the lock + -- succeeds, nothing is using this ssh, and it can + -- be stopped. + let lockfile = socket2lock socketfile + unlockFile lockfile + mode <- annexFileMode + fd <- liftIO $ noUmask mode $ + openFd lockfile ReadWrite (Just mode) defaultFileFlags + v <- liftIO $ tryIO $ + setLock fd (WriteLock, AbsoluteSeek, 0, 0) + case v of + Left _ -> noop + Right _ -> stopssh socketfile + liftIO $ closeFd fd +#else + stopssh socketfile +#endif + stopssh socketfile = do + let params = sshConnectionCachingParams socketfile + -- "ssh -O stop" is noisy on stderr even with -q + void $ liftIO $ catchMaybeIO $ + withQuietOutput createProcessSuccess $ + proc "ssh" $ toCommand $ + [ Params "-O stop" + ] ++ params ++ [Param "any"] + -- Cannot remove the lock file; other processes may + -- be waiting on our exclusive lock to use it. + +{- This needs to be as short as possible, due to limitations on the length + - of the path to a socket file. At the same time, it needs to be unique + - for each host. + -} +hostport2socket :: String -> Maybe Integer -> FilePath +hostport2socket host Nothing = hostport2socket' host +hostport2socket host (Just port) = hostport2socket' $ host ++ "!" ++ show port +hostport2socket' :: String -> FilePath +hostport2socket' s + | length s > 32 = md5s (Str s) + | otherwise = s + +socket2lock :: FilePath -> FilePath +socket2lock socket = socket ++ lockExt + +isLock :: FilePath -> Bool +isLock f = lockExt `isSuffixOf` f + +lockExt :: String +lockExt = ".lock" + +{- This is the size of the sun_path component of sockaddr_un, which + - is the limit to the total length of the filename of a unix socket. + - + - On Linux, this is 108. On OSX, 104. TODO: Probe + -} +sizeof_sockaddr_un_sun_path :: Int +sizeof_sockaddr_un_sun_path = 100 + +{- Note that this looks at the true length of the path in bytes, as it will + - appear on disk. -} +valid_unix_socket_path :: FilePath -> Bool +valid_unix_socket_path f = length (decodeW8 f) < sizeof_sockaddr_un_sun_path + +{- Parses the SSH port, and returns the other OpenSSH options. If + - several ports are found, the last one takes precedence. -} +sshReadPort :: [String] -> (Maybe Integer, [String]) +sshReadPort params = (port, reverse args) + where + (port,args) = aux (Nothing, []) params + aux (p,ps) [] = (p,ps) + aux (_,ps) ("-p":p:rest) = aux (readPort p, ps) rest + aux (p,ps) (q:rest) | "-p" `isPrefixOf` q = aux (readPort $ drop 2 q, ps) rest + | otherwise = aux (p,q:ps) rest + readPort p = fmap fst $ listToMaybe $ reads p diff --git a/Annex/TaggedPush.hs b/Annex/TaggedPush.hs new file mode 100644 index 0000000000..5dac345f2b --- /dev/null +++ b/Annex/TaggedPush.hs @@ -0,0 +1,57 @@ +{- git-annex tagged pushes + - + - Copyright 2012 Joey Hess + - + - 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 $ intercalate "/" $ 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/Annex/UUID.hs b/Annex/UUID.hs new file mode 100644 index 0000000000..c36861bbe3 --- /dev/null +++ b/Annex/UUID.hs @@ -0,0 +1,74 @@ +{- git-annex uuids + - + - Each git repository used by git-annex has an annex.uuid setting that + - uniquely identifies that repository. + - + - UUIDs of remotes are cached in git config, using keys named + - remote..annex-uuid + - + - Copyright 2010-2013 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Annex.UUID ( + getUUID, + getRepoUUID, + getUncachedUUID, + prepUUID, + genUUID, + removeRepoUUID, + storeUUID, +) where + +import Common.Annex +import qualified Git +import qualified Git.Config +import Config + +import qualified Data.UUID as U +import System.Random + +configkey :: ConfigKey +configkey = annexConfig "uuid" + +{- Generates a random UUID, that does not include the MAC address. -} +genUUID :: IO UUID +genUUID = UUID . show <$> (randomIO :: IO U.UUID) + +{- Get current repository's UUID. -} +getUUID :: Annex UUID +getUUID = getRepoUUID =<< gitRepo + +{- Looks up a repo's UUID, caching it in .git/config if it's not already. -} +getRepoUUID :: Git.Repo -> Annex UUID +getRepoUUID r = do + c <- toUUID <$> getConfig cachekey "" + let u = getUncachedUUID r + + if c /= u && u /= NoUUID + then do + updatecache u + return u + else return c + where + updatecache u = do + g <- gitRepo + when (g /= r) $ storeUUID cachekey u + cachekey = remoteConfig r "uuid" + +removeRepoUUID :: Annex () +removeRepoUUID = unsetConfig configkey + +getUncachedUUID :: Git.Repo -> UUID +getUncachedUUID = toUUID . Git.Config.get key "" + where + (ConfigKey key) = configkey + +{- Make sure that the repo has an annex.uuid setting. -} +prepUUID :: Annex () +prepUUID = whenM ((==) NoUUID <$> getUUID) $ + storeUUID configkey =<< liftIO genUUID + +storeUUID :: ConfigKey -> UUID -> Annex () +storeUUID configfield = setConfig configfield . fromUUID diff --git a/Annex/Version.hs b/Annex/Version.hs new file mode 100644 index 0000000000..05b3f02273 --- /dev/null +++ b/Annex/Version.hs @@ -0,0 +1,53 @@ +{- git-annex repository versioning + - + - Copyright 2010,2013 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE CPP #-} + +module Annex.Version where + +import Common.Annex +import Config +import qualified Annex + +type Version = String + +defaultVersion :: Version +defaultVersion = "3" + +directModeVersion :: Version +directModeVersion = "4" + +supportedVersions :: [Version] +supportedVersions = [defaultVersion, directModeVersion] + +upgradableVersions :: [Version] +#ifndef mingw32_HOST_OS +upgradableVersions = ["0", "1", "2"] +#else +upgradableVersions = ["2"] +#endif + +versionField :: ConfigKey +versionField = annexConfig "version" + +getVersion :: Annex (Maybe Version) +getVersion = annexVersion <$> Annex.getGitConfig + +setVersion :: Version -> Annex () +setVersion = setConfig versionField + +removeVersion :: Annex () +removeVersion = unsetConfig versionField + +checkVersion :: Version -> Annex () +checkVersion v + | v `elem` supportedVersions = noop + | v `elem` upgradableVersions = err "Upgrade this repository: git-annex upgrade" + | otherwise = err "Upgrade git-annex." + where + err msg = error $ "Repository version " ++ v ++ + " is not supported. " ++ msg diff --git a/Annex/Wanted.hs b/Annex/Wanted.hs new file mode 100644 index 0000000000..b90a1af317 --- /dev/null +++ b/Annex/Wanted.hs @@ -0,0 +1,32 @@ +{- git-annex control over whether content is wanted + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Annex.Wanted where + +import Common.Annex +import Logs.PreferredContent +import Annex.UUID + +import qualified Data.Set as S + +{- Check if a file is preferred content for the local repository. -} +wantGet :: Bool -> AssociatedFile -> Annex Bool +wantGet def Nothing = return def +wantGet def (Just file) = isPreferredContent Nothing S.empty file def + +{- Check if a file is preferred content for a remote. -} +wantSend :: Bool -> AssociatedFile -> UUID -> Annex Bool +wantSend def Nothing _ = return def +wantSend def (Just file) to = isPreferredContent (Just to) S.empty file def + +{- Check if a file can be dropped, maybe from a remote. + - Don't drop files that are preferred content. -} +wantDrop :: Bool -> Maybe UUID -> AssociatedFile -> Annex Bool +wantDrop def _ Nothing = return $ not def +wantDrop def from (Just file) = do + u <- maybe getUUID (return . id) from + not <$> isPreferredContent (Just u) (S.singleton u) file def diff --git a/Assistant.hs b/Assistant.hs new file mode 100644 index 0000000000..c14f1e0df7 --- /dev/null +++ b/Assistant.hs @@ -0,0 +1,147 @@ +{- git-annex assistant daemon + - + - Copyright 2012-2013 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE CPP #-} + +module Assistant where + +import qualified Annex +import Assistant.Common +import Assistant.DaemonStatus +import Assistant.NamedThread +import Assistant.Types.ThreadedMonad +import Assistant.Threads.DaemonStatus +import Assistant.Threads.Watcher +import Assistant.Threads.Committer +import Assistant.Threads.Pusher +import Assistant.Threads.Merger +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 +import Assistant.Threads.ConfigMonitor +import Assistant.Threads.Glacier +#ifdef WITH_WEBAPP +import Assistant.WebApp +import Assistant.Threads.WebApp +#ifdef WITH_PAIRING +import Assistant.Threads.PairListener +#endif +#ifdef WITH_XMPP +import Assistant.Threads.XMPPClient +import Assistant.Threads.XMPPPusher +#endif +#else +#warning Building without the webapp. You probably need to install Yesod.. +import Assistant.Types.UrlRenderer +#endif +import qualified Utility.Daemon +import Utility.LogFile +import Utility.ThreadScheduler +import qualified Build.SysConfig as SysConfig + +import System.Log.Logger +import Network.Socket (HostName) + +stopDaemon :: Annex () +stopDaemon = liftIO . Utility.Daemon.stopDaemon =<< fromRepo gitAnnexPidFile + +{- Starts the daemon. If the daemon is run in the foreground, once it's + - running, can start the browser. + - + - startbrowser is passed the url and html shim file, as well as the original + - stdout and stderr descriptors. -} +startDaemon :: Bool -> Bool -> Maybe HostName -> Maybe (Maybe Handle -> Maybe Handle -> String -> FilePath -> IO ()) -> Annex () +startDaemon assistant foreground listenhost startbrowser = do + Annex.changeState $ \s -> s { Annex.daemon = True } + pidfile <- fromRepo gitAnnexPidFile + logfile <- fromRepo gitAnnexLogFile + logfd <- liftIO $ openLog logfile + if foreground + then do + origout <- liftIO $ catchMaybeIO $ + fdToHandle =<< dup stdOutput + origerr <- liftIO $ catchMaybeIO $ + fdToHandle =<< dup stdError + let undaemonize a = do + debugM desc $ "logging to " ++ logfile + Utility.Daemon.lockPidFile pidfile + Utility.LogFile.redirLog logfd + a + start undaemonize $ + case startbrowser of + Nothing -> Nothing + Just a -> Just $ a origout origerr + else + start (Utility.Daemon.daemonize logfd (Just pidfile) False) Nothing + where + desc + | assistant = "assistant" + | otherwise = "watch" + start daemonize webappwaiter = withThreadState $ \st -> do + checkCanWatch + dstatus <- startDaemonStatus + logfile <- fromRepo gitAnnexLogFile + liftIO $ debugM desc $ "logging to " ++ logfile + liftIO $ daemonize $ + flip runAssistant (go webappwaiter) + =<< newAssistantData st dstatus + + +#ifdef WITH_WEBAPP + go webappwaiter = do + d <- getAssistant id +#else + go _webappwaiter = do +#endif + notice ["starting", desc, "version", SysConfig.packageversion] + urlrenderer <- liftIO newUrlRenderer + mapM_ (startthread urlrenderer) + [ watch $ commitThread +#ifdef WITH_WEBAPP + , assist $ webAppThread d urlrenderer False listenhost Nothing webappwaiter +#ifdef WITH_PAIRING + , assist $ pairListenerThread urlrenderer +#endif +#ifdef WITH_XMPP + , assist $ xmppClientThread urlrenderer + , assist $ xmppSendPackThread urlrenderer + , assist $ xmppReceivePackThread urlrenderer +#endif +#endif + , assist $ pushThread + , assist $ pushRetryThread + , assist $ mergeThread + , assist $ transferWatcherThread + , assist $ transferPollerThread + , assist $ transfererThread + , assist $ daemonStatusThread + , assist $ sanityCheckerDailyThread + , assist $ sanityCheckerHourlyThread +#ifdef WITH_CLIBS + , assist $ mountWatcherThread +#endif + , assist $ netWatcherThread + , assist $ netWatcherFallbackThread + , assist $ transferScannerThread urlrenderer + , assist $ configMonitorThread + , assist $ glacierThread + , watch $ watchThread + ] + + liftIO waitForTermination + + watch a = (True, a) + assist a = (False, a) + startthread urlrenderer (watcher, t) + | watcher || assistant = startNamedThread urlrenderer t + | otherwise = noop diff --git a/Assistant/Alert.hs b/Assistant/Alert.hs new file mode 100644 index 0000000000..df5ee29107 --- /dev/null +++ b/Assistant/Alert.hs @@ -0,0 +1,311 @@ +{- git-annex assistant alerts + - + - Copyright 2012, 2013 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE OverloadedStrings, CPP #-} + +module Assistant.Alert where + +import Common.Annex +import Assistant.Types.Alert +import Assistant.Alert.Utility +import qualified Remote +import Utility.Tense +import Logs.Transfer + +import Data.String +import qualified Data.Text as T + +#ifdef WITH_WEBAPP +import Assistant.Monad +import Assistant.DaemonStatus +import Assistant.WebApp.Types +import Assistant.WebApp +import Yesod +#endif + +{- Makes a button for an alert that opens a Route. The button will + - close the alert it's attached to when clicked. -} +#ifdef WITH_WEBAPP +mkAlertButton :: T.Text -> UrlRenderer -> Route WebApp -> Assistant AlertButton +mkAlertButton label urlrenderer route = do + close <- asIO1 removeAlert + url <- liftIO $ renderUrl urlrenderer route [] + return $ AlertButton + { buttonLabel = label + , buttonUrl = url + , buttonAction = Just close + } +#endif + +renderData :: Alert -> TenseText +renderData = tenseWords . alertData + +baseActivityAlert :: Alert +baseActivityAlert = Alert + { alertClass = Activity + , alertHeader = Nothing + , alertMessageRender = renderData + , alertData = [] + , alertCounter = 0 + , alertBlockDisplay = False + , alertClosable = False + , alertPriority = Medium + , alertIcon = Just ActivityIcon + , alertCombiner = Nothing + , alertName = Nothing + , alertButton = Nothing + } + +warningAlert :: String -> String -> Alert +warningAlert name msg = Alert + { alertClass = Warning + , alertHeader = Just $ tenseWords ["warning"] + , alertMessageRender = renderData + , alertData = [UnTensed $ T.pack msg] + , alertCounter = 0 + , alertBlockDisplay = True + , alertClosable = True + , alertPriority = High + , alertIcon = Just ErrorIcon + , alertCombiner = Just $ dataCombiner $ \_old new -> new + , alertName = Just $ WarningAlert name + , alertButton = Nothing + } + +activityAlert :: Maybe TenseText -> [TenseChunk] -> Alert +activityAlert header dat = baseActivityAlert + { alertHeader = header + , alertData = dat + } + +startupScanAlert :: Alert +startupScanAlert = activityAlert Nothing + [Tensed "Performing" "Performed", "startup scan"] + +{- Displayed when a shutdown is occurring, so will be seen after shutdown + - has happened. -} +shutdownAlert :: Alert +shutdownAlert = warningAlert "shutdown" "git-annex has been shut down" + +commitAlert :: Alert +commitAlert = activityAlert Nothing + [Tensed "Committing" "Committed", "changes to git"] + +showRemotes :: [Remote] -> TenseChunk +showRemotes = UnTensed . T.intercalate ", " . map (T.pack . Remote.name) + +syncAlert :: [Remote] -> Alert +syncAlert rs = baseActivityAlert + { alertName = Just SyncAlert + , alertHeader = Just $ tenseWords + [Tensed "Syncing" "Synced", "with", showRemotes rs] + , alertPriority = Low + , alertIcon = Just SyncIcon + } + +syncResultAlert :: [Remote] -> [Remote] -> Alert +syncResultAlert succeeded failed = makeAlertFiller (not $ null succeeded) $ + baseActivityAlert + { alertName = Just SyncAlert + , alertHeader = Just $ tenseWords msg + } + where + msg + | null succeeded = ["Failed to sync with", showRemotes failed] + | null failed = ["Synced with", showRemotes succeeded] + | otherwise = + [ "Synced with", showRemotes succeeded + , "but not with", showRemotes failed + ] + +sanityCheckAlert :: Alert +sanityCheckAlert = activityAlert + (Just $ tenseWords [Tensed "Running" "Ran", "daily sanity check"]) + ["to make sure everything is ok."] + +sanityCheckFixAlert :: String -> Alert +sanityCheckFixAlert msg = Alert + { alertClass = Warning + , alertHeader = Just $ tenseWords ["Fixed a problem"] + , alertMessageRender = render + , alertData = [UnTensed $ T.pack msg] + , alertCounter = 0 + , alertBlockDisplay = True + , alertPriority = High + , alertClosable = True + , alertIcon = Just ErrorIcon + , alertName = Just SanityCheckFixAlert + , alertCombiner = Just $ dataCombiner (++) + , alertButton = Nothing + } + where + render alert = tenseWords $ alerthead : alertData alert ++ [alertfoot] + alerthead = "The daily sanity check found and fixed a problem:" + alertfoot = "If these problems persist, consider filing a bug report." + +pairingAlert :: AlertButton -> Alert +pairingAlert button = baseActivityAlert + { alertData = [ UnTensed "Pairing in progress" ] + , alertPriority = High + , alertButton = Just button + } + +pairRequestReceivedAlert :: String -> AlertButton -> Alert +pairRequestReceivedAlert who button = Alert + { alertClass = Message + , alertHeader = Nothing + , alertMessageRender = renderData + , alertData = [UnTensed $ T.pack $ who ++ " is sending a pair request."] + , alertCounter = 0 + , alertBlockDisplay = False + , alertPriority = High + , alertClosable = True + , alertIcon = Just InfoIcon + , alertName = Just $ PairAlert who + , alertCombiner = Just $ dataCombiner $ \_old new -> new + , alertButton = Just button + } + +pairRequestAcknowledgedAlert :: String -> Maybe AlertButton -> Alert +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 + } + +xmppNeededAlert :: AlertButton -> Alert +xmppNeededAlert button = Alert + { alertHeader = Just "Share with friends, and keep your devices in sync across the cloud." + , alertIcon = Just TheCloud + , alertPriority = High + , alertButton = Just button + , alertClosable = True + , alertClass = Message + , alertMessageRender = renderData + , alertCounter = 0 + , alertBlockDisplay = True + , alertName = Just $ XMPPNeededAlert + , alertCombiner = Just $ dataCombiner $ \_old new -> new + , alertData = [] + } + +cloudRepoNeededAlert :: Maybe String -> AlertButton -> Alert +cloudRepoNeededAlert friendname button = Alert + { alertHeader = Just $ fromString $ unwords + [ "Unable to download files from" + , (fromMaybe "your other devices" friendname) ++ "." + ] + , alertIcon = Just ErrorIcon + , alertPriority = High + , alertButton = Just button + , alertClosable = True + , alertClass = Message + , alertMessageRender = renderData + , alertCounter = 0 + , alertBlockDisplay = True + , alertName = Just $ CloudRepoNeededAlert + , alertCombiner = Just $ dataCombiner $ \_old new -> new + , alertData = [] + } + +remoteRemovalAlert :: String -> AlertButton -> Alert +remoteRemovalAlert desc button = Alert + { alertHeader = Just $ fromString $ + "The repository \"" ++ desc ++ + "\" has been emptied, and can now be removed." + , alertIcon = Just InfoIcon + , alertPriority = High + , alertButton = Just button + , alertClosable = True + , alertClass = Message + , alertMessageRender = renderData + , alertCounter = 0 + , alertBlockDisplay = True + , alertName = Just $ RemoteRemovalAlert desc + , alertCombiner = Just $ dataCombiner $ \_old new -> new + , alertData = [] + } + +{- Show a message that relates to a list of files. + - + - The most recent several files are shown, and a count of any others. -} +fileAlert :: TenseChunk -> [FilePath] -> Alert +fileAlert msg files = (activityAlert Nothing shortfiles) + { alertName = Just $ FileAlert msg + , alertMessageRender = renderer + , alertCounter = counter + , alertCombiner = Just $ fullCombiner combiner + } + where + maxfilesshown = 10 + + (somefiles, counter) = splitcounter (dedupadjacent files) + shortfiles = map (fromString . shortFile . takeFileName) somefiles + + renderer alert = tenseWords $ msg : alertData alert ++ showcounter + where + showcounter = case alertCounter alert of + 0 -> [] + _ -> [fromString $ "and " ++ show (alertCounter alert) ++ " other files"] + + dedupadjacent (x:y:rest) + | x == y = dedupadjacent (y:rest) + | otherwise = x : dedupadjacent (y:rest) + dedupadjacent (x:[]) = [x] + dedupadjacent [] = [] + + {- Note that this ensures the counter is never 1; no need to say + - "1 file" when the filename could be shown. -} + splitcounter l + | length l <= maxfilesshown = (l, 0) + | otherwise = + let (keep, rest) = splitAt (maxfilesshown - 1) l + in (keep, length rest) + + combiner new old = + let (fs, n) = splitcounter $ + dedupadjacent $ alertData new ++ alertData old + cnt = n + alertCounter new + alertCounter old + in old + { alertData = fs + , alertCounter = cnt + } + +addFileAlert :: [FilePath] -> Alert +addFileAlert = fileAlert (Tensed "Adding" "Added") + +{- This is only used as a success alert after a transfer, not during it. -} +transferFileAlert :: Direction -> Bool -> FilePath -> Alert +transferFileAlert direction True file + | direction == Upload = fileAlert "Uploaded" [file] + | otherwise = fileAlert "Downloaded" [file] +transferFileAlert direction False file + | direction == Upload = fileAlert "Upload failed" [file] + | otherwise = fileAlert "Download failed" [file] + +dataCombiner :: ([TenseChunk] -> [TenseChunk] -> [TenseChunk]) -> AlertCombiner +dataCombiner combiner = fullCombiner $ + \new old -> old { alertData = alertData new `combiner` alertData old } + +fullCombiner :: (Alert -> Alert -> Alert) -> AlertCombiner +fullCombiner combiner new old + | alertClass new /= alertClass old = Nothing + | alertName new == alertName old = + Just $! new `combiner` old + | otherwise = Nothing + +shortFile :: FilePath -> String +shortFile f + | len < maxlen = f + | otherwise = take half f ++ ".." ++ drop (len - half) f + where + len = length f + maxlen = 20 + half = (maxlen - 2) `div` 2 + diff --git a/Assistant/Alert/Utility.hs b/Assistant/Alert/Utility.hs new file mode 100644 index 0000000000..af52a4235d --- /dev/null +++ b/Assistant/Alert/Utility.hs @@ -0,0 +1,130 @@ +{- git-annex assistant alert utilities + - + - Copyright 2012, 2013 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Assistant.Alert.Utility where + +import Common.Annex +import Assistant.Types.Alert +import Utility.Tense + +import qualified Data.Text as T +import Data.Text (Text) +import qualified Data.Map as M + +{- This is as many alerts as it makes sense to display at a time. + - A display might be smaller, or larger, the point is to not overwhelm the + - user with a ton of alerts. -} +displayAlerts :: Int +displayAlerts = 6 + +{- This is not a hard maximum, but there's no point in keeping a great + - many filler alerts in an AlertMap, so when there's more than this many, + - they start being pruned, down toward displayAlerts. -} +maxAlerts :: Int +maxAlerts = displayAlerts * 2 + +type AlertPair = (AlertId, Alert) + +{- The desired order is the reverse of: + - + - - Pinned alerts + - - High priority alerts, newest first + - - Medium priority Activity, newest first (mostly used for Activity) + - - Low priority alerts, newest first + - - Filler priorty alerts, newest first + - - Ties are broken by the AlertClass, with Errors etc coming first. + -} +compareAlertPairs :: AlertPair -> AlertPair -> Ordering +compareAlertPairs + (aid, Alert { alertClass = aclass, alertPriority = aprio }) + (bid, Alert { alertClass = bclass, alertPriority = bprio }) + = compare aprio bprio + `thenOrd` compare aid bid + `thenOrd` compare aclass bclass + +sortAlertPairs :: [AlertPair] -> [AlertPair] +sortAlertPairs = sortBy compareAlertPairs + +{- Renders an alert's header for display, if it has one. -} +renderAlertHeader :: Alert -> Maybe Text +renderAlertHeader alert = renderTense (alertTense alert) <$> alertHeader alert + +{- Renders an alert's message for display. -} +renderAlertMessage :: Alert -> Text +renderAlertMessage alert = renderTense (alertTense alert) $ + (alertMessageRender alert) alert + +showAlert :: Alert -> String +showAlert alert = T.unpack $ T.unwords $ catMaybes + [ renderAlertHeader alert + , Just $ renderAlertMessage alert + ] + +alertTense :: Alert -> Tense +alertTense alert + | alertClass alert == Activity = Present + | otherwise = Past + +{- Checks if two alerts display the same. -} +effectivelySameAlert :: Alert -> Alert -> Bool +effectivelySameAlert x y = all id + [ alertClass x == alertClass y + , alertHeader x == alertHeader y + , alertData x == alertData y + , alertBlockDisplay x == alertBlockDisplay y + , alertClosable x == alertClosable y + , alertPriority x == alertPriority y + ] + +makeAlertFiller :: Bool -> Alert -> Alert +makeAlertFiller success alert + | isFiller alert = alert + | otherwise = alert + { alertClass = if c == Activity then c' else c + , alertPriority = Filler + , alertClosable = True + , alertButton = Nothing + , alertIcon = Just $ if success then SuccessIcon else ErrorIcon + } + where + c = alertClass alert + c' + | success = Success + | otherwise = Error + +isFiller :: Alert -> Bool +isFiller alert = alertPriority alert == Filler + +{- Updates the Alertmap, adding or updating an alert. + - + - Any old filler that looks the same as the alert is removed. + - + - Or, if the alert has an alertCombiner that combines it with + - an old alert, the old alert is replaced with the result, and the + - alert is removed. + - + - Old filler alerts are pruned once maxAlerts is reached. + -} +mergeAlert :: AlertId -> Alert -> AlertMap -> AlertMap +mergeAlert i al m = maybe updatePrune updateCombine (alertCombiner al) + where + pruneSame k al' = k == i || not (effectivelySameAlert al al') + pruneBloat m' + | bloat > 0 = M.fromList $ pruneold $ M.toList m' + | otherwise = m' + where + bloat = M.size m' - maxAlerts + pruneold l = + let (f, rest) = partition (\(_, a) -> isFiller a) l + in drop bloat f ++ rest + updatePrune = pruneBloat $ M.filterWithKey pruneSame $ + M.insertWith' const i al m + updateCombine combiner = + let combined = M.mapMaybe (combiner al) m + in if M.null combined + then updatePrune + else M.delete i $ M.union combined m diff --git a/Assistant/BranchChange.hs b/Assistant/BranchChange.hs new file mode 100644 index 0000000000..c9354544a5 --- /dev/null +++ b/Assistant/BranchChange.hs @@ -0,0 +1,19 @@ +{- git-annex assistant git-annex branch change tracking + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Assistant.BranchChange where + +import Assistant.Common +import Assistant.Types.BranchChange + +import Control.Concurrent.MSampleVar + +branchChanged :: Assistant () +branchChanged = flip writeSV () <<~ (fromBranchChangeHandle . branchChangeHandle) + +waitBranchChange :: Assistant () +waitBranchChange = readSV <<~ (fromBranchChangeHandle . branchChangeHandle) diff --git a/Assistant/Changes.hs b/Assistant/Changes.hs new file mode 100644 index 0000000000..2ecd2036ce --- /dev/null +++ b/Assistant/Changes.hs @@ -0,0 +1,47 @@ +{- git-annex assistant change tracking + - + - Copyright 2012-2013 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Assistant.Changes where + +import Assistant.Common +import Assistant.Types.Changes +import Utility.TList + +import Data.Time.Clock +import Control.Concurrent.STM + +{- Handlers call this when they made a change that needs to get committed. -} +madeChange :: FilePath -> ChangeInfo -> Assistant (Maybe Change) +madeChange f t = Just <$> (Change <$> liftIO getCurrentTime <*> pure f <*> pure t) + +noChange :: Assistant (Maybe Change) +noChange = return Nothing + +{- Indicates an add needs to be done, but has not started yet. -} +pendingAddChange :: FilePath -> Assistant (Maybe Change) +pendingAddChange f = Just <$> (PendingAddChange <$> liftIO getCurrentTime <*> pure f) + +{- Gets all unhandled changes. + - Blocks until at least one change is made. -} +getChanges :: Assistant [Change] +getChanges = (atomically . getTList) <<~ changePool + +{- Gets all unhandled changes, without blocking. -} +getAnyChanges :: Assistant [Change] +getAnyChanges = (atomically . takeTList) <<~ changePool + +{- Puts unhandled changes back into the pool. + - Note: Original order is not preserved. -} +refillChanges :: [Change] -> Assistant () +refillChanges cs = (atomically . flip appendTList cs) <<~ changePool + +{- Records a change to the pool. -} +recordChange :: Change -> Assistant () +recordChange c = (atomically . flip snocTList c) <<~ changePool + +recordChanges :: [Change] -> Assistant () +recordChanges = refillChanges diff --git a/Assistant/Commits.hs b/Assistant/Commits.hs new file mode 100644 index 0000000000..7d1d3780fe --- /dev/null +++ b/Assistant/Commits.hs @@ -0,0 +1,23 @@ +{- git-annex assistant commit tracking + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Assistant.Commits where + +import Assistant.Common +import Assistant.Types.Commits +import Utility.TList + +import Control.Concurrent.STM + +{- Gets all unhandled commits. + - Blocks until at least one commit is made. -} +getCommits :: Assistant [Commit] +getCommits = (atomically . getTList) <<~ commitChan + +{- Records a commit in the channel. -} +recordCommit :: Assistant () +recordCommit = (atomically . flip consTList Commit) <<~ commitChan diff --git a/Assistant/Common.hs b/Assistant/Common.hs new file mode 100644 index 0000000000..f9719422d9 --- /dev/null +++ b/Assistant/Common.hs @@ -0,0 +1,14 @@ +{- Common infrastructure for the git-annex assistant. + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Assistant.Common (module X) where + +import Common.Annex as X +import Assistant.Monad as X +import Assistant.Types.DaemonStatus as X +import Assistant.Types.NamedThread as X +import Assistant.Types.Alert as X diff --git a/Assistant/DaemonStatus.hs b/Assistant/DaemonStatus.hs new file mode 100644 index 0000000000..af072d8aeb --- /dev/null +++ b/Assistant/DaemonStatus.hs @@ -0,0 +1,259 @@ +{- git-annex assistant daemon status + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Assistant.DaemonStatus where + +import Assistant.Common +import Assistant.Alert.Utility +import Utility.Tmp +import Assistant.Types.NetMessager +import Utility.NotificationBroadcaster +import Logs.Transfer +import Logs.Trust +import qualified Remote +import qualified Types.Remote as Remote +import qualified Git + +import Control.Concurrent.STM +import System.Posix.Types +import Data.Time.Clock.POSIX +import Data.Time +import System.Locale +import qualified Data.Map as M +import qualified Data.Text as T + +getDaemonStatus :: Assistant DaemonStatus +getDaemonStatus = (atomically . readTMVar) <<~ daemonStatusHandle + +modifyDaemonStatus_ :: (DaemonStatus -> DaemonStatus) -> Assistant () +modifyDaemonStatus_ a = modifyDaemonStatus $ \s -> (a s, ()) + +modifyDaemonStatus :: (DaemonStatus -> (DaemonStatus, b)) -> Assistant b +modifyDaemonStatus a = do + dstatus <- getAssistant daemonStatusHandle + liftIO $ do + (s, b) <- atomically $ do + r@(s, _) <- a <$> takeTMVar dstatus + putTMVar dstatus s + return r + sendNotification $ changeNotifier s + return b + +{- Returns a function that updates the lists of syncable remotes + - and other associated information. -} +calcSyncRemotes :: Annex (DaemonStatus -> DaemonStatus) +calcSyncRemotes = do + rs <- filter (remoteAnnexSync . Remote.gitconfig) . + concat . Remote.byCost <$> Remote.remoteList + alive <- trustExclude DeadTrusted (map Remote.uuid rs) + let good r = Remote.uuid r `elem` alive + let syncable = filter good rs + let syncdata = filter (not . remoteAnnexIgnore . Remote.gitconfig) $ + filter (not . isXMPPRemote) syncable + + return $ \dstatus -> dstatus + { syncRemotes = syncable + , syncGitRemotes = + filter (not . Remote.specialRemote) syncable + , syncDataRemotes = syncdata + , syncingToCloudRemote = any iscloud syncdata + } + where + iscloud r = not (Remote.readonly r) && Remote.globallyAvailable r + +{- Updates the syncRemotes list from the list of all remotes in Annex state. -} +updateSyncRemotes :: Assistant () +updateSyncRemotes = do + modifyDaemonStatus_ =<< liftAnnex calcSyncRemotes + status <- getDaemonStatus + liftIO $ sendNotification $ syncRemotesNotifier status + + when (syncingToCloudRemote status) $ + updateAlertMap $ + M.filter $ \alert -> + alertName alert /= Just CloudRepoNeededAlert + +{- Load any previous daemon status file, and store it in a MVar for this + - process to use as its DaemonStatus. Also gets current transfer status. -} +startDaemonStatus :: Annex DaemonStatusHandle +startDaemonStatus = do + file <- fromRepo gitAnnexDaemonStatusFile + status <- liftIO $ + flip catchDefaultIO (readDaemonStatusFile file) =<< newDaemonStatus + transfers <- M.fromList <$> getTransfers + addsync <- calcSyncRemotes + liftIO $ atomically $ newTMVar $ addsync $ status + { scanComplete = False + , sanityCheckRunning = False + , currentTransfers = transfers + } + +{- Don't just dump out the structure, because it will change over time, + - and parts of it are not relevant. -} +writeDaemonStatusFile :: FilePath -> DaemonStatus -> IO () +writeDaemonStatusFile file status = + viaTmp writeFile file =<< serialized <$> getPOSIXTime + where + serialized now = unlines + [ "lastRunning:" ++ show now + , "scanComplete:" ++ show (scanComplete status) + , "sanityCheckRunning:" ++ show (sanityCheckRunning status) + , "lastSanityCheck:" ++ maybe "" show (lastSanityCheck status) + ] + +readDaemonStatusFile :: FilePath -> IO DaemonStatus +readDaemonStatusFile file = parse <$> newDaemonStatus <*> readFile file + where + parse status = foldr parseline status . lines + parseline line status + | key == "lastRunning" = parseval readtime $ \v -> + status { lastRunning = Just v } + | key == "scanComplete" = parseval readish $ \v -> + status { scanComplete = v } + | key == "sanityCheckRunning" = parseval readish $ \v -> + status { sanityCheckRunning = v } + | key == "lastSanityCheck" = parseval readtime $ \v -> + status { lastSanityCheck = Just v } + | otherwise = status -- unparsable line + where + (key, value) = separate (== ':') line + parseval parser a = maybe status a (parser value) + readtime s = do + d <- parseTime defaultTimeLocale "%s%Qs" s + Just $ utcTimeToPOSIXSeconds d + +{- Checks if a time stamp was made after the daemon was lastRunning. + - + - Some slop is built in; this really checks if the time stamp was made + - at least ten minutes after the daemon was lastRunning. This is to + - ensure the daemon shut down cleanly, and deal with minor clock skew. + - + - If the daemon has never ran before, this always returns False. + -} +afterLastDaemonRun :: EpochTime -> DaemonStatus -> Bool +afterLastDaemonRun timestamp status = maybe False (< t) (lastRunning status) + where + t = realToFrac (timestamp + slop) :: POSIXTime + slop = fromIntegral tenMinutes + +tenMinutes :: Int +tenMinutes = 10 * 60 + +{- Mutates the transfer map. Runs in STM so that the transfer map can + - be modified in the same transaction that modifies the transfer queue. + - Note that this does not send a notification of the change; that's left + - to the caller. -} +adjustTransfersSTM :: DaemonStatusHandle -> (TransferMap -> TransferMap) -> STM () +adjustTransfersSTM dstatus a = do + s <- takeTMVar dstatus + putTMVar dstatus $ s { currentTransfers = a (currentTransfers s) } + +{- Checks if a transfer is currently running. -} +checkRunningTransferSTM :: DaemonStatusHandle -> Transfer -> STM Bool +checkRunningTransferSTM dstatus t = M.member t . currentTransfers + <$> readTMVar dstatus + +{- Alters a transfer's info, if the transfer is in the map. -} +alterTransferInfo :: Transfer -> (TransferInfo -> TransferInfo) -> Assistant () +alterTransferInfo t a = updateTransferInfo' $ M.adjust a t + +{- Updates a transfer's info. Adds the transfer to the map if necessary, + - or if already present, updates it while preserving the old transferTid, + - transferPaused, and bytesComplete values, which are not written to disk. -} +updateTransferInfo :: Transfer -> TransferInfo -> Assistant () +updateTransferInfo t info = updateTransferInfo' $ M.insertWith' merge t info + where + merge new old = new + { transferTid = maybe (transferTid new) Just (transferTid old) + , transferPaused = transferPaused new || transferPaused old + , bytesComplete = maybe (bytesComplete new) Just (bytesComplete old) + } + +updateTransferInfo' :: (TransferMap -> TransferMap) -> Assistant () +updateTransferInfo' a = notifyTransfer `after` modifyDaemonStatus_ update + where + update s = s { currentTransfers = a (currentTransfers s) } + +{- Removes a transfer from the map, and returns its info. -} +removeTransfer :: Transfer -> Assistant (Maybe TransferInfo) +removeTransfer t = notifyTransfer `after` modifyDaemonStatus remove + where + remove s = + let (info, ts) = M.updateLookupWithKey + (\_k _v -> Nothing) + t (currentTransfers s) + in (s { currentTransfers = ts }, info) + +{- Send a notification when a transfer is changed. -} +notifyTransfer :: Assistant () +notifyTransfer = do + dstatus <- getAssistant daemonStatusHandle + liftIO $ sendNotification + =<< transferNotifier <$> atomically (readTMVar dstatus) + +{- Send a notification when alerts are changed. -} +notifyAlert :: Assistant () +notifyAlert = do + dstatus <- getAssistant daemonStatusHandle + liftIO $ sendNotification + =<< alertNotifier <$> atomically (readTMVar dstatus) + +{- Returns the alert's identifier, which can be used to remove it. -} +addAlert :: Alert -> Assistant AlertId +addAlert alert = do + notice [showAlert alert] + notifyAlert `after` modifyDaemonStatus add + where + add s = (s { lastAlertId = i, alertMap = m }, i) + where + i = nextAlertId $ lastAlertId s + m = mergeAlert i alert (alertMap s) + +removeAlert :: AlertId -> Assistant () +removeAlert i = updateAlert i (const Nothing) + +updateAlert :: AlertId -> (Alert -> Maybe Alert) -> Assistant () +updateAlert i a = updateAlertMap $ \m -> M.update a i m + +updateAlertMap :: (AlertMap -> AlertMap) -> Assistant () +updateAlertMap a = notifyAlert `after` modifyDaemonStatus_ update + where + update s = s { alertMap = a (alertMap s) } + +{- Displays an alert while performing an activity that returns True on + - success. + - + - The alert is left visible afterwards, as filler. + - Old filler is pruned, to prevent the map growing too large. -} +alertWhile :: Alert -> Assistant Bool -> Assistant Bool +alertWhile alert a = alertWhile' alert $ do + r <- a + return (r, r) + +{- Like alertWhile, but allows the activity to return a value too. -} +alertWhile' :: Alert -> Assistant (Bool, a) -> Assistant a +alertWhile' alert a = do + let alert' = alert { alertClass = Activity } + i <- addAlert alert' + (ok, r) <- a + updateAlertMap $ mergeAlert i $ makeAlertFiller ok alert' + return r + +{- Displays an alert while performing an activity, then removes it. -} +alertDuring :: Alert -> Assistant a -> Assistant a +alertDuring alert a = do + i <- addAlert $ alert { alertClass = Activity } + removeAlert i `after` a + +{- Remotes using the XMPP transport have urls like xmpp::user@host -} +isXMPPRemote :: Remote -> Bool +isXMPPRemote remote = Git.repoIsUrl r && "xmpp::" `isPrefixOf` Git.repoLocation r + where + r = Remote.repo remote + +getXMPPClientID :: Remote -> ClientID +getXMPPClientID r = T.pack $ drop (length "xmpp::") (Git.repoLocation (Remote.repo r)) diff --git a/Assistant/DeleteRemote.hs b/Assistant/DeleteRemote.hs new file mode 100644 index 0000000000..2e06d52cd1 --- /dev/null +++ b/Assistant/DeleteRemote.hs @@ -0,0 +1,98 @@ +{- git-annex assistant remote deletion utilities + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE CPP #-} + +module Assistant.DeleteRemote where + +import Assistant.Common +import Assistant.Types.UrlRenderer +import Assistant.TransferQueue +import Logs.Transfer +import Logs.Location +import Assistant.DaemonStatus +import qualified Remote +import Remote.List +import qualified Git.Command +import qualified Git.BuildVersion +import Logs.Trust +import qualified Annex + +#ifdef WITH_WEBAPP +import Assistant.WebApp.Types +import Assistant.Alert +import qualified Data.Text as T +#endif + +{- Removes a remote (but leave the repository as-is), and returns the old + - Remote data. -} +disableRemote :: UUID -> Assistant Remote +disableRemote uuid = do + remote <- fromMaybe (error "unknown remote") + <$> liftAnnex (Remote.remoteFromUUID uuid) + liftAnnex $ do + inRepo $ Git.Command.run + [ Param "remote" + -- name of this subcommand changed + , Param $ + if Git.BuildVersion.older "1.8.0" + then "rm" + else "remove" + , Param (Remote.name remote) + ] + void $ remoteListRefresh + updateSyncRemotes + return remote + +{- Removes a remote, marking it dead .-} +removeRemote :: UUID -> Assistant Remote +removeRemote uuid = do + liftAnnex $ trustSet uuid DeadTrusted + disableRemote uuid + +{- Called when a Remote is probably empty, to remove it. + - + - This does one last check for any objects remaining in the Remote, + - and if there are any, queues Downloads of them, and defers removing + - the remote for later. This is to catch any objects not referred to + - in keys in the current branch. + -} +removableRemote :: UrlRenderer -> UUID -> Assistant () +removableRemote urlrenderer uuid = do + keys <- getkeys + if null keys + then finishRemovingRemote urlrenderer uuid + else do + r <- fromMaybe (error "unknown remote") + <$> liftAnnex (Remote.remoteFromUUID uuid) + mapM_ (queueremaining r) keys + where + queueremaining r k = + queueTransferWhenSmall "remaining object in unwanted remote" + Nothing (Transfer Download uuid k) r + {- Scanning for keys can take a long time; do not tie up + - the Annex monad while doing it, so other threads continue to + - run. -} + getkeys = do + a <- liftAnnex $ Annex.withCurrentState $ loggedKeysFor uuid + liftIO a + +{- With the webapp, this asks the user to click on a button to finish + - removing the remote. + - + - Without the webapp, just do the removal now. + -} +finishRemovingRemote :: UrlRenderer -> UUID -> Assistant () +#ifdef WITH_WEBAPP +finishRemovingRemote urlrenderer uuid = do + desc <- liftAnnex $ Remote.prettyUUID uuid + button <- mkAlertButton (T.pack "Finish deletion process") urlrenderer $ + FinishDeleteRepositoryR uuid + void $ addAlert $ remoteRemovalAlert desc button +#else +finishRemovingRemote _ uuid = void $ removeRemote uuid +#endif diff --git a/Assistant/Drop.hs b/Assistant/Drop.hs new file mode 100644 index 0000000000..d677a69c8b --- /dev/null +++ b/Assistant/Drop.hs @@ -0,0 +1,112 @@ +{- git-annex assistant dropping of unwanted content + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Assistant.Drop where + +import Assistant.Common +import Assistant.DaemonStatus +import Logs.Location +import Logs.Trust +import Types.Remote (uuid) +import qualified Remote +import qualified Command.Drop +import Command +import Annex.Wanted +import Annex.Exception +import Config +import Annex.Content.Direct + +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 :: Reason -> Bool -> Key -> AssociatedFile -> Maybe Remote -> Assistant () +handleDrops _ _ _ Nothing _ = noop +handleDrops reason fromhere key f knownpresentremote = do + syncrs <- syncDataRemotes <$> getDaemonStatus + 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 allowed to drop fromhere, that drop will be tried first. + - + - In direct mode, all associated files are checked, and only if all + - of them are unwanted are they dropped. + -} +handleDropsFrom :: [UUID] -> [Remote] -> Reason -> Bool -> Key -> AssociatedFile -> Maybe Remote -> Assistant () +handleDropsFrom _ _ _ _ _ Nothing _ = noop +handleDropsFrom locs rs reason fromhere key (Just afile) knownpresentremote = do + fs <- liftAnnex $ ifM isDirect + ( do + l <- associatedFilesRelative key + if null l + then return [afile] + else return l + , return [afile] + ) + n <- getcopies fs + if fromhere && checkcopies n Nothing + then go fs rs =<< dropl fs n + else go fs rs n + where + getcopies fs = liftAnnex $ do + (untrusted, have) <- trustPartition UnTrusted locs + numcopies <- maximum <$> mapM (getNumCopies <=< numCopies) fs + return (length have, numcopies, S.fromList untrusted) + + {- Check that we have enough copies still to drop the content. + - When the remote being dropped from is untrusted, it was not + - counted as a copy, so having only numcopies suffices. Otherwise, + - we need more than numcopies to safely drop. -} + checkcopies (have, numcopies, _untrusted) Nothing = have > numcopies + checkcopies (have, numcopies, untrusted) (Just u) + | S.member u untrusted = have >= numcopies + | otherwise = have > numcopies + + decrcopies (have, numcopies, untrusted) Nothing = + (have - 1, numcopies, untrusted) + decrcopies v@(_have, _numcopies, untrusted) (Just u) + | S.member u untrusted = v + | otherwise = decrcopies v Nothing + + go _ [] _ = noop + go fs (r:rest) n + | uuid r `S.notMember` slocs = go fs rest n + | checkcopies n (Just $ Remote.uuid r) = + dropr fs r n >>= go fs rest + | otherwise = noop + + checkdrop fs n@(have, numcopies, _untrusted) u a = + ifM (liftAnnex $ allM (wantDrop True u . Just) fs) + ( ifM (liftAnnex $ safely $ doCommand $ a (Just numcopies)) + ( do + debug + [ "dropped" + , afile + , "(from " ++ maybe "here" show u ++ ")" + , "(copies now " ++ show (have - 1) ++ ")" + , ": " ++ reason + ] + return $ decrcopies n u + , return n + ) + , return n + ) + + dropl fs n = checkdrop fs n Nothing $ \numcopies -> + Command.Drop.startLocal afile numcopies key knownpresentremote + + dropr fs r n = checkdrop fs n (Just $ Remote.uuid r) $ \numcopies -> + Command.Drop.startRemote afile numcopies key r + + safely a = either (const False) id <$> tryAnnex a + + slocs = S.fromList locs diff --git a/Assistant/Install.hs b/Assistant/Install.hs new file mode 100644 index 0000000000..dee1b5be37 --- /dev/null +++ b/Assistant/Install.hs @@ -0,0 +1,101 @@ +{- Assistant installation + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE CPP #-} + +module Assistant.Install where + +import Assistant.Common +import Assistant.Install.AutoStart +import Assistant.Install.Menu +import Assistant.Ssh +import Config.Files +import Utility.FileMode +import Utility.Shell +import Utility.Tmp +import Utility.Env + +#ifdef darwin_HOST_OS +import Utility.OSX +#else +import Utility.FreeDesktop +#endif + +standaloneAppBase :: IO (Maybe FilePath) +standaloneAppBase = getEnv "GIT_ANNEX_APP_BASE" + +{- The standalone app does not have an installation process. + - So when it's run, it needs to set up autostarting of the assistant + - daemon, as well as writing the programFile, and putting a + - git-annex-shell wrapper into ~/.ssh + - + - Note that this is done every time it's started, so if the user moves + - it around, the paths this sets up won't break. + -} +ensureInstalled :: IO () +ensureInstalled = go =<< standaloneAppBase + where + go Nothing = noop + go (Just base) = do + let program = base "git-annex" + programfile <- programFile + createDirectoryIfMissing True (parentDir programfile) + writeFile programfile program + +#ifdef darwin_HOST_OS + autostartfile <- userAutoStart osxAutoStartLabel +#else + menufile <- desktopMenuFilePath "git-annex" <$> userDataDir + icondir <- iconDir <$> userDataDir + installMenu program menufile base icondir + autostartfile <- autoStartPath "git-annex" <$> userConfigDir +#endif + installAutoStart program autostartfile + + {- This shim is only updated if it doesn't + - 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_local + , "set -e" + , "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) + viaTmp writeFile shim content + modifyFileMode shim $ addModes [ownerExecuteMode] + +{- Returns a cleaned up environment that lacks settings used to make the + - standalone builds use their bundled libraries and programs. + - Useful when calling programs not included in the standalone builds. + - + - For a non-standalone build, returns Nothing. + -} +cleanEnvironment :: IO (Maybe [(String, String)]) +cleanEnvironment = clean <$> getEnvironment + where + clean env + | null vars = Nothing + | otherwise = Just $ catMaybes $ map (restoreorig env) env + | otherwise = Nothing + where + vars = words $ fromMaybe "" $ + lookup "GIT_ANNEX_STANDLONE_ENV" env + restoreorig oldenv p@(k, _v) + | k `elem` vars = case lookup ("ORIG_" ++ k) oldenv of + Nothing -> Nothing + (Just v') -> Just (k, v') + | otherwise = Just p diff --git a/Assistant/Install/AutoStart.hs b/Assistant/Install/AutoStart.hs new file mode 100644 index 0000000000..b03d202244 --- /dev/null +++ b/Assistant/Install/AutoStart.hs @@ -0,0 +1,39 @@ +{- Assistant autostart file installation + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE CPP #-} + +module Assistant.Install.AutoStart where + +import Utility.FreeDesktop +#ifdef darwin_HOST_OS +import Utility.OSX +import Utility.Path +import System.Directory +#endif + +installAutoStart :: FilePath -> FilePath -> IO () +installAutoStart command file = do +#ifdef darwin_HOST_OS + createDirectoryIfMissing True (parentDir file) + writeFile file $ genOSXAutoStartFile osxAutoStartLabel command + ["assistant", "--autostart"] +#else + writeDesktopMenuFile (fdoAutostart command) file +#endif + +osxAutoStartLabel :: String +osxAutoStartLabel = "com.branchable.git-annex.assistant" + +fdoAutostart :: FilePath -> DesktopEntry +fdoAutostart command = genDesktopEntry + "Git Annex Assistant" + "Autostart" + False + (command ++ " assistant --autostart") + Nothing + [] diff --git a/Assistant/Install/Menu.hs b/Assistant/Install/Menu.hs new file mode 100644 index 0000000000..41ec855b69 --- /dev/null +++ b/Assistant/Install/Menu.hs @@ -0,0 +1,47 @@ +{- Assistant menu installation. + - + - Copyright 2013 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE CPP #-} + +module Assistant.Install.Menu where + +import Common + +import Utility.FreeDesktop + +installMenu :: FilePath -> FilePath -> FilePath -> FilePath -> IO () +installMenu command menufile iconsrcdir icondir = do +#ifdef darwin_HOST_OS + return () +#else + writeDesktopMenuFile (fdoDesktopMenu command) menufile + installIcon (iconsrcdir "logo.svg") $ + iconFilePath (iconBaseName ++ ".svg") "scalable" icondir + installIcon (iconsrcdir "favicon.png") $ + iconFilePath (iconBaseName ++ ".png") "16x16" icondir +#endif + +{- The command can be either just "git-annex", or the full path to use + - to run it. -} +fdoDesktopMenu :: FilePath -> DesktopEntry +fdoDesktopMenu command = genDesktopEntry + "Git Annex" + "Track and sync the files in your Git Annex" + False + (command ++ " webapp") + (Just iconBaseName) + ["Network", "FileTransfer"] + +installIcon :: FilePath -> FilePath -> IO () +installIcon src dest = do + createDirectoryIfMissing True (parentDir dest) + withBinaryFile src ReadMode $ \hin -> + withBinaryFile dest WriteMode $ \hout -> + hGetContents hin >>= hPutStr hout + +iconBaseName :: String +iconBaseName = "git-annex" diff --git a/Assistant/MakeRemote.hs b/Assistant/MakeRemote.hs new file mode 100644 index 0000000000..e26d6057af --- /dev/null +++ b/Assistant/MakeRemote.hs @@ -0,0 +1,175 @@ +{- git-annex assistant remote creation utilities + - + - Copyright 2012, 2013 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Assistant.MakeRemote where + +import Assistant.Common +import Assistant.Ssh +import Assistant.Sync +import qualified Types.Remote as R +import qualified Remote +import Remote.List +import qualified Remote.Rsync as Rsync +import qualified Git +import qualified Git.Command +import qualified Command.InitRemote +import Logs.UUID +import Logs.Remote +import Git.Remote +import Config +import Config.Cost +import Creds + +import qualified Data.Text as T +import qualified Data.Map as M + +type RemoteName = String + +{- Sets up and begins syncing with a new ssh or rsync remote. -} +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 + syncRemote r + return r + where + rsync = forcersync || rsyncOnly sshdata + maker + | rsync = makeRsyncRemote + | otherwise = makeGitRemote + sshurl = T.unpack $ T.concat $ + if rsync + then [u, h, T.pack ":", sshDirectory sshdata, T.pack "/"] + else [T.pack "ssh://", u, h, d, T.pack "/"] + where + u = maybe (T.pack "") (\v -> T.concat [v, T.pack "@"]) $ sshUserName sshdata + h = sshHostName sshdata + d + | T.pack "/" `T.isPrefixOf` sshDirectory sshdata = sshDirectory sshdata + | T.pack "~/" `T.isPrefixOf` sshDirectory sshdata = T.concat [T.pack "/", sshDirectory sshdata] + | otherwise = T.concat [T.pack "/~/", sshDirectory sshdata] + +{- Runs an action that returns a name of the remote, and finishes adding it. -} +addRemote :: Annex RemoteName -> Annex Remote +addRemote a = do + name <- a + void remoteListRefresh + maybe (error "failed to add remote") return + =<< Remote.byName (Just name) + +{- Inits a rsync special remote, and returns its name. -} +makeRsyncRemote :: RemoteName -> String -> Annex String +makeRsyncRemote name location = makeRemote name location $ const $ void $ + go =<< Command.InitRemote.findExisting name + where + go Nothing = setupSpecialRemote name Rsync.remote config + =<< Command.InitRemote.generateNew name + go (Just v) = setupSpecialRemote name Rsync.remote config v + config = M.fromList + [ ("encryption", "shared") + , ("rsyncurl", location) + , ("type", "rsync") + ] + +type SpecialRemoteMaker = RemoteName -> RemoteType -> R.RemoteConfig -> Annex RemoteName + +{- Inits a new special remote. The name is used as a suggestion, but + - will be changed if there is already a special remote with that name. -} +initSpecialRemote :: SpecialRemoteMaker +initSpecialRemote name remotetype config = go 0 + where + go :: Int -> Annex RemoteName + go n = do + let fullname = if n == 0 then name else name ++ show n + r <- Command.InitRemote.findExisting fullname + case r of + Nothing -> setupSpecialRemote fullname remotetype config + =<< Command.InitRemote.generateNew fullname + Just _ -> go (n + 1) + +{- Enables an existing special remote. -} +enableSpecialRemote :: SpecialRemoteMaker +enableSpecialRemote name remotetype config = do + r <- Command.InitRemote.findExisting name + case r of + Nothing -> error $ "Cannot find a special remote named " ++ name + Just v -> setupSpecialRemote name remotetype config v + +setupSpecialRemote :: RemoteName -> RemoteType -> R.RemoteConfig -> (UUID, R.RemoteConfig) -> Annex RemoteName +setupSpecialRemote name remotetype config (u, c) = do + {- Currently, only 'weak' ciphers can be generated from the + - assistant, because otherwise GnuPG may block once the entropy + - pool is drained, and as of now there's no way to tell the user + - to perform IO actions to refill the pool. -} + c' <- R.setup remotetype u $ + M.insert "highRandomQuality" "false" $ M.union config c + describeUUID u name + configSet u c' + return name + +{- Returns the name of the git remote it created. If there's already a + - remote at the location, returns its name. -} +makeGitRemote :: String -> String -> Annex RemoteName +makeGitRemote basename location = makeRemote basename location $ \name -> + 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. + - + - Returns the name of the remote. -} +makeRemote :: String -> String -> (RemoteName -> Annex ()) -> Annex RemoteName +makeRemote basename location a = do + g <- gitRepo + if not (any samelocation $ Git.remotes g) + then do + + let name = uniqueRemoteName basename 0 g + a name + return name + else return basename + where + samelocation x = Git.repoLocation x == location + +{- Generate an unused name for a remote, adding a number if + - necessary. + - + - Ensures that the returned name is a legal git remote name. -} +uniqueRemoteName :: String -> Int -> Git.Repo -> RemoteName +uniqueRemoteName basename n r + | null namecollision = name + | otherwise = uniqueRemoteName legalbasename (succ n) r + where + namecollision = filter samename (Git.remotes r) + samename x = Git.remoteName x == Just name + name + | n == 0 = legalbasename + | otherwise = legalbasename ++ show n + legalbasename = makeLegalName basename + +{- Finds a CredPair belonging to any Remote that is of a given type + - and matches some other criteria. + - + - This can be used as a default when another repository is being set up + - using the same service. + - + - A function must be provided that returns the CredPairStorage + - to use for a particular Remote's uuid. + -} +previouslyUsedCredPair + :: (UUID -> CredPairStorage) + -> RemoteType + -> (Remote -> Bool) + -> Annex (Maybe CredPair) +previouslyUsedCredPair getstorage remotetype criteria = + getM fromstorage =<< filter criteria . filter sametype <$> remoteList + where + sametype r = R.typename (R.remotetype r) == R.typename remotetype + fromstorage r = do + let storage = getstorage (R.uuid r) + getRemoteCredPair (R.config r) storage diff --git a/Assistant/Monad.hs b/Assistant/Monad.hs new file mode 100644 index 0000000000..4b73061f91 --- /dev/null +++ b/Assistant/Monad.hs @@ -0,0 +1,141 @@ +{- git-annex assistant monad + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE GeneralizedNewtypeDeriving, MultiParamTypeClasses #-} + +module Assistant.Monad ( + Assistant, + AssistantData(..), + newAssistantData, + runAssistant, + getAssistant, + LiftAnnex, + liftAnnex, + (<~>), + (<<~), + asIO, + asIO1, + asIO2, + ThreadName, + debug, + notice +) where + +import "mtl" Control.Monad.Reader +import System.Log.Logger + +import Common.Annex +import Assistant.Types.ThreadedMonad +import Assistant.Types.DaemonStatus +import Assistant.Types.ScanRemotes +import Assistant.Types.TransferQueue +import Assistant.Types.TransferSlots +import Assistant.Types.TransferrerPool +import Assistant.Types.Pushes +import Assistant.Types.BranchChange +import Assistant.Types.Commits +import Assistant.Types.Changes +import Assistant.Types.Buddies +import Assistant.Types.NetMessager +import Assistant.Types.ThreadName + +newtype Assistant a = Assistant { mkAssistant :: ReaderT AssistantData IO a } + deriving ( + Monad, + MonadIO, + MonadReader AssistantData, + Functor, + Applicative + ) + +data AssistantData = AssistantData + { threadName :: ThreadName + , threadState :: ThreadState + , daemonStatusHandle :: DaemonStatusHandle + , scanRemoteMap :: ScanRemoteMap + , transferQueue :: TransferQueue + , transferSlots :: TransferSlots + , transferrerPool :: TransferrerPool + , failedPushMap :: FailedPushMap + , commitChan :: CommitChan + , changePool :: ChangePool + , branchChangeHandle :: BranchChangeHandle + , buddyList :: BuddyList + , netMessager :: NetMessager + } + +newAssistantData :: ThreadState -> DaemonStatusHandle -> IO AssistantData +newAssistantData st dstatus = AssistantData + <$> pure (ThreadName "main") + <*> pure st + <*> pure dstatus + <*> newScanRemoteMap + <*> newTransferQueue + <*> newTransferSlots + <*> newTransferrerPool + <*> newFailedPushMap + <*> newCommitChan + <*> newChangePool + <*> newBranchChangeHandle + <*> newBuddyList + <*> newNetMessager + +runAssistant :: AssistantData -> Assistant a -> IO a +runAssistant d a = runReaderT (mkAssistant a) d + +getAssistant :: (AssistantData -> a) -> Assistant a +getAssistant = reader + +{- Using a type class for lifting into the annex monad allows + - easily lifting to it from multiple different monads. -} +class LiftAnnex m where + liftAnnex :: Annex a -> m a + +{- Runs an action in the git-annex monad. Note that the same monad state + - is shared amoung all assistant threads, so only one of these can run at + - a time. Therefore, long-duration actions should be avoided. -} +instance LiftAnnex Assistant where + liftAnnex a = do + st <- reader threadState + liftIO $ runThreadState st a + +{- Runs an IO action, passing it an IO action that runs an Assistant action. -} +(<~>) :: (IO a -> IO b) -> Assistant a -> Assistant b +io <~> a = do + d <- reader id + liftIO $ io $ runAssistant d a + +{- Creates an IO action that will run an Assistant action when run. -} +asIO :: Assistant a -> Assistant (IO a) +asIO a = do + d <- reader id + return $ runAssistant d a + +asIO1 :: (a -> Assistant b) -> Assistant (a -> IO b) +asIO1 a = do + d <- reader id + return $ \v -> runAssistant d $ a v + +asIO2 :: (a -> b -> Assistant c) -> Assistant (a -> b -> IO c) +asIO2 a = do + d <- reader id + return $ \v1 v2 -> runAssistant d (a v1 v2) + +{- Runs an IO action on a selected field of the AssistantData. -} +(<<~) :: (a -> IO b) -> (AssistantData -> a) -> Assistant b +io <<~ v = reader v >>= liftIO . io + +debug :: [String] -> Assistant () +debug = logaction debugM + +notice :: [String] -> Assistant () +notice = logaction noticeM + +logaction :: (String -> String -> IO ()) -> [String] -> Assistant () +logaction a ws = do + ThreadName name <- getAssistant threadName + liftIO $ a name $ unwords $ (name ++ ":") : ws diff --git a/Assistant/NamedThread.hs b/Assistant/NamedThread.hs new file mode 100644 index 0000000000..edebe830fe --- /dev/null +++ b/Assistant/NamedThread.hs @@ -0,0 +1,91 @@ +{- git-annex assistant named threads. + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE CPP #-} + +module Assistant.NamedThread where + +import Common.Annex +import Assistant.Types.NamedThread +import Assistant.Types.ThreadName +import Assistant.Types.DaemonStatus +import Assistant.Types.UrlRenderer +import Assistant.DaemonStatus +import Assistant.Monad + +import Control.Concurrent +import Control.Concurrent.Async +import qualified Data.Map as M +import qualified Control.Exception as E + +#ifdef WITH_WEBAPP +import Assistant.WebApp.Types +import Assistant.Types.Alert +import Assistant.Alert +import qualified Data.Text as T +#endif + +{- Starts a named thread, if it's not already running. + - + - Named threads are run by a management thread, so if they crash + - an alert is displayed, allowing the thread to be restarted. -} +startNamedThread :: UrlRenderer -> NamedThread -> Assistant () +startNamedThread urlrenderer namedthread@(NamedThread name a) = do + m <- startedThreads <$> getDaemonStatus + case M.lookup name m of + Nothing -> start + Just (aid, _) -> do + r <- liftIO (E.try (poll aid) :: IO (Either E.SomeException (Maybe (Either E.SomeException ())))) + case r of + Right Nothing -> noop + _ -> start + where + start = do + d <- getAssistant id + aid <- liftIO $ runmanaged $ d { threadName = name } + restart <- asIO $ startNamedThread urlrenderer namedthread + modifyDaemonStatus_ $ \s -> s + { startedThreads = M.insertWith' const name (aid, restart) (startedThreads s) } + runmanaged d = do + aid <- async $ runAssistant d a + void $ forkIO $ manager d aid + return aid + manager d aid = do + r <- E.try (wait aid) :: IO (Either E.SomeException ()) + case r of + Right _ -> noop + Left e -> do + let msg = unwords + [ fromThreadName $ threadName d + , "crashed:", show e + ] + hPutStrLn stderr msg +#ifdef WITH_WEBAPP + button <- runAssistant d $ mkAlertButton + (T.pack "Restart Thread") + urlrenderer + (RestartThreadR name) + runAssistant d $ void $ addAlert $ + (warningAlert (fromThreadName name) msg) + { alertButton = Just button } +#endif + +namedThreadId :: NamedThread -> Assistant (Maybe ThreadId) +namedThreadId (NamedThread name _) = do + m <- startedThreads <$> getDaemonStatus + return $ asyncThreadId . fst <$> M.lookup name m + +{- Waits for all named threads that have been started to finish. + - + - Note that if a named thread crashes, it will probably + - cause this to crash as well. Also, named threads that are started + - after this is called will not be waited on. -} +waitNamedThreads :: Assistant () +waitNamedThreads = do + m <- startedThreads <$> getDaemonStatus + liftIO $ mapM_ (wait . fst) $ M.elems m + diff --git a/Assistant/NetMessager.hs b/Assistant/NetMessager.hs new file mode 100644 index 0000000000..329d808fcf --- /dev/null +++ b/Assistant/NetMessager.hs @@ -0,0 +1,176 @@ +{- git-annex assistant out of band network messager interface + - + - Copyright 2012-2013 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE BangPatterns #-} + +module Assistant.NetMessager where + +import Assistant.Common +import Assistant.Types.NetMessager + +import Control.Concurrent.STM +import Control.Concurrent.MSampleVar +import qualified Data.Set as S +import qualified Data.Map as M +import qualified Data.DList as D + +sendNetMessage :: NetMessage -> Assistant () +sendNetMessage m = + (atomically . flip writeTChan m) <<~ (netMessages . netMessager) + +waitNetMessage :: Assistant (NetMessage) +waitNetMessage = (atomically . readTChan) <<~ (netMessages . netMessager) + +notifyNetMessagerRestart :: Assistant () +notifyNetMessagerRestart = + flip writeSV () <<~ (netMessagerRestart . netMessager) + +waitNetMessagerRestart :: Assistant () +waitNetMessagerRestart = readSV <<~ (netMessagerRestart . netMessager) + +{- Store a new important NetMessage for a client, and if an equivilant + - older message is already stored, remove it from both importantNetMessages + - and 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 $ + M.mapWithKey removematching q + putTMVar (sentImportantNetMessages nm) $ + M.mapWithKey removematching sent + removematching someclient s + | matchingclient someclient = S.filter (not . equivilantImportantNetMessages 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) + +{- Queues a push initiation message in the queue for the appropriate + - side of the push but only if there is not already an initiation message + - from the same client in the queue. -} +queuePushInitiation :: NetMessage -> Assistant () +queuePushInitiation msg@(Pushing clientid stage) = do + tv <- getPushInitiationQueue side + liftIO $ atomically $ do + r <- tryTakeTMVar tv + case r of + Nothing -> putTMVar tv [msg] + Just l -> do + let !l' = msg : filter differentclient l + putTMVar tv l' + where + side = pushDestinationSide stage + differentclient (Pushing cid _) = cid /= clientid + differentclient _ = True +queuePushInitiation _ = noop + +{- Waits for a push inititation message to be received, and runs + - function to select a message from the queue. -} +waitPushInitiation :: PushSide -> ([NetMessage] -> (NetMessage, [NetMessage])) -> Assistant NetMessage +waitPushInitiation side selector = do + tv <- getPushInitiationQueue side + liftIO $ atomically $ do + q <- takeTMVar tv + if null q + then retry + else do + let (msg, !q') = selector q + unless (null q') $ + putTMVar tv q' + return msg + +{- Stores messages for a push into the appropriate inbox. + - + - To avoid overflow, only 1000 messages max are stored in any + - inbox, which should be far more than necessary. + - + - TODO: If we have more than 100 inboxes for different clients, + - discard old ones that are not currently being used by any push. + -} +storeInbox :: NetMessage -> Assistant () +storeInbox msg@(Pushing clientid stage) = do + inboxes <- getInboxes side + stored <- liftIO $ atomically $ do + m <- readTVar inboxes + let update = \v -> do + writeTVar inboxes $ + M.insertWith' const clientid v m + return True + case M.lookup clientid m of + Nothing -> update (1, tostore) + Just (sz, l) + | sz > 1000 -> return False + | otherwise -> + let !sz' = sz + 1 + !l' = D.append l tostore + in update (sz', l') + if stored + then netMessagerDebug clientid ["stored", logNetMessage msg, "in", show side, "inbox"] + else netMessagerDebug clientid ["discarded", logNetMessage msg, "; ", show side, "inbox is full"] + where + side = pushDestinationSide stage + tostore = D.singleton msg +storeInbox _ = noop + +{- Gets the new message for a push from its inbox. + - Blocks until a message has been received. -} +waitInbox :: ClientID -> PushSide -> Assistant (NetMessage) +waitInbox clientid side = do + inboxes <- getInboxes side + liftIO $ atomically $ do + m <- readTVar inboxes + case M.lookup clientid m of + Nothing -> retry + Just (sz, dl) + | sz < 1 -> retry + | otherwise -> do + let msg = D.head dl + let dl' = D.tail dl + let !sz' = sz - 1 + writeTVar inboxes $ + M.insertWith' const clientid (sz', dl') m + return msg + +emptyInbox :: ClientID -> PushSide -> Assistant () +emptyInbox clientid side = do + inboxes <- getInboxes side + liftIO $ atomically $ + modifyTVar' inboxes $ + M.delete clientid + +getInboxes :: PushSide -> Assistant Inboxes +getInboxes side = + getSide side . netMessagerInboxes <$> getAssistant netMessager + +getPushInitiationQueue :: PushSide -> Assistant (TMVar [NetMessage]) +getPushInitiationQueue side = + getSide side . netMessagerPushInitiations <$> getAssistant netMessager + +netMessagerDebug :: ClientID -> [String] -> Assistant () +netMessagerDebug clientid l = debug $ + "NetMessager" : l ++ [show $ logClientID clientid] diff --git a/Assistant/Pairing.hs b/Assistant/Pairing.hs new file mode 100644 index 0000000000..4736c4396c --- /dev/null +++ b/Assistant/Pairing.hs @@ -0,0 +1,92 @@ +{- git-annex assistant repo pairing, core data types + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE CPP #-} + +module Assistant.Pairing where + +import Common.Annex +import Utility.Verifiable +import Assistant.Ssh + +import Control.Concurrent +import Network.Socket +import Data.Char +import qualified Data.Text as T + +data PairStage + {- "I'll pair with anybody who shares the secret that can be used + - to verify this request." -} + = PairReq + {- "I've verified your request, and you can verify this to see + - that I know the secret. I set up your ssh key already. + - Here's mine for you to set up." -} + | PairAck + {- "I saw your PairAck; you can stop sending them." -} + | PairDone + deriving (Eq, Read, Show, Ord) + +newtype PairMsg = PairMsg (Verifiable (PairStage, PairData, SomeAddr)) + deriving (Eq, Read, Show) + +verifiedPairMsg :: PairMsg -> PairingInProgress -> Bool +verifiedPairMsg (PairMsg m) pip = verify m $ inProgressSecret pip + +fromPairMsg :: PairMsg -> Verifiable (PairStage, PairData, SomeAddr) +fromPairMsg (PairMsg m) = m + +pairMsgStage :: PairMsg -> PairStage +pairMsgStage (PairMsg (Verifiable (s, _, _) _)) = s + +pairMsgData :: PairMsg -> PairData +pairMsgData (PairMsg (Verifiable (_, d, _) _)) = d + +pairMsgAddr :: PairMsg -> SomeAddr +pairMsgAddr (PairMsg (Verifiable (_, _, a) _)) = a + +data PairData = PairData + -- uname -n output, not a full domain name + { remoteHostName :: Maybe HostName + , remoteUserName :: UserName + , remoteDirectory :: FilePath + , remoteSshPubKey :: SshPubKey + , pairUUID :: UUID + } + deriving (Eq, Read, Show) + +type UserName = String + +{- A pairing that is in progress has a secret, a thread that is + - broadcasting pairing messages, and a SshKeyPair that has not yet been + - set up on disk. -} +data PairingInProgress = PairingInProgress + { inProgressSecret :: Secret + , inProgressThreadId :: Maybe ThreadId + , inProgressSshKeyPair :: SshKeyPair + , inProgressPairData :: PairData + , inProgressPairStage :: PairStage + } + deriving (Show) + +data SomeAddr = IPv4Addr HostAddress +{- My Android build of the Network library does not currently have IPV6 + - support. -} +#ifndef __ANDROID__ + | IPv6Addr HostAddress6 +#endif + deriving (Ord, Eq, Read, Show) + +{- This contains the whole secret, just lightly obfuscated to make it not + - too obvious. It's only displayed in the user's web browser. -} +newtype SecretReminder = SecretReminder [Int] + deriving (Show, Eq, Ord, Read) + +toSecretReminder :: T.Text -> SecretReminder +toSecretReminder = SecretReminder . map ord . T.unpack + +fromSecretReminder :: SecretReminder -> T.Text +fromSecretReminder (SecretReminder s) = T.pack $ map chr s diff --git a/Assistant/Pairing/MakeRemote.hs b/Assistant/Pairing/MakeRemote.hs new file mode 100644 index 0000000000..edd27e35a2 --- /dev/null +++ b/Assistant/Pairing/MakeRemote.hs @@ -0,0 +1,91 @@ +{- git-annex assistant pairing remote creation + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Assistant.Pairing.MakeRemote where + +import Assistant.Common +import Assistant.Ssh +import Assistant.Pairing +import Assistant.Pairing.Network +import Assistant.MakeRemote +import Config.Cost + +import Network.Socket +import qualified Data.Text as T + +{- Authorized keys are set up before pairing is complete, so that the other + - side can immediately begin syncing. -} +setupAuthorizedKeys :: PairMsg -> FilePath -> IO () +setupAuthorizedKeys msg repodir = do + validateSshPubKey pubkey + unlessM (liftIO $ addAuthorizedKeys False repodir pubkey) $ + error "failed setting up ssh authorized keys" + where + pubkey = remoteSshPubKey $ pairMsgData msg + +{- When local pairing is complete, this is used to set up the remote for + - the host we paired with. -} +finishedLocalPairing :: PairMsg -> SshKeyPair -> Assistant () +finishedLocalPairing msg keypair = do + sshdata <- liftIO $ setupSshKeyPair keypair =<< pairMsgToSshData msg + {- Ensure that we know the ssh host key for the host we paired with. + - If we don't, ssh over to get it. -} + liftIO $ unlessM (knownHost $ sshHostName sshdata) $ + void $ sshTranscript + [ sshOpt "StrictHostKeyChecking" "no" + , sshOpt "NumberOfPasswordPrompts" "0" + , "-n" + , genSshHost (sshHostName sshdata) (sshUserName sshdata) + , "git-annex-shell -c configlist " ++ T.unpack (sshDirectory sshdata) + ] + Nothing + void $ makeSshRemote False sshdata (Just semiExpensiveRemoteCost) + +{- Mostly a straightforward conversion. Except: + - * Determine the best hostname to use to contact the host. + - * Strip leading ~/ from the directory name. + -} +pairMsgToSshData :: PairMsg -> IO SshData +pairMsgToSshData msg = do + let d = pairMsgData msg + hostname <- liftIO $ bestHostName msg + let dir = case remoteDirectory d of + ('~':'/':v) -> v + v -> v + return SshData + { sshHostName = T.pack hostname + , sshUserName = Just (T.pack $ remoteUserName d) + , sshDirectory = T.pack dir + , sshRepoName = genSshRepoName hostname dir + , sshPort = 22 + , needsPubKey = True + , rsyncOnly = False + } + +{- Finds the best hostname to use for the host that sent the PairMsg. + - + - If remoteHostName is set, tries to use a .local address based on it. + - That's the most robust, if this system supports .local. + - Otherwise, looks up the hostname in the DNS for the remoteAddress, + - if any. May fall back to remoteAddress if there's no DNS. Ugh. -} +bestHostName :: PairMsg -> IO HostName +bestHostName msg = case remoteHostName $ pairMsgData msg of + Just h -> do + let localname = h ++ ".local" + addrs <- catchDefaultIO [] $ + getAddrInfo Nothing (Just localname) Nothing + maybe fallback (const $ return localname) (headMaybe addrs) + Nothing -> fallback + where + fallback = do + let a = pairMsgAddr msg + let sockaddr = case a of + IPv4Addr addr -> SockAddrInet (PortNum 0) addr + IPv6Addr addr -> SockAddrInet6 (PortNum 0) 0 addr 0 + fromMaybe (showAddr a) + <$> catchDefaultIO Nothing + (fst <$> getNameInfo [] True False sockaddr) diff --git a/Assistant/Pairing/Network.hs b/Assistant/Pairing/Network.hs new file mode 100644 index 0000000000..6c625f8814 --- /dev/null +++ b/Assistant/Pairing/Network.hs @@ -0,0 +1,130 @@ +{- git-annex assistant pairing network code + - + - All network traffic is sent over multicast UDP. For reliability, + - each message is repeated until acknowledged. This is done using a + - thread, that gets stopped before the next message is sent. + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Assistant.Pairing.Network where + +import Assistant.Common +import Assistant.Pairing +import Assistant.DaemonStatus +import Utility.ThreadScheduler +import Utility.Verifiable + +import Network.Multicast +import Network.Info +import Network.Socket +import Control.Exception (bracket) +import qualified Data.Map as M +import Control.Concurrent + +{- This is an arbitrary port in the dynamic port range, that could + - conceivably be used for some other broadcast messages. + - If so, hope they ignore the garbage from us; we'll certianly + - ignore garbage from them. Wild wild west. -} +pairingPort :: PortNumber +pairingPort = 55556 + +{- Goal: Reach all hosts on the same network segment. + - Method: Use same address that avahi uses. Other broadcast addresses seem + - to not be let through some routers. -} +multicastAddress :: SomeAddr -> HostName +multicastAddress (IPv4Addr _) = "224.0.0.251" +multicastAddress (IPv6Addr _) = "ff02::fb" + +{- Multicasts a message repeatedly on all interfaces, with a 2 second + - delay between each transmission. The message is repeated forever + - unless a number of repeats is specified. + - + - The remoteHostAddress is set to the interface's IP address. + - + - Note that new sockets are opened each time. This is hardly efficient, + - but it allows new network interfaces to be used as they come up. + - On the other hand, the expensive DNS lookups are cached. + -} +multicastPairMsg :: Maybe Int -> Secret -> PairData -> PairStage -> IO () +multicastPairMsg repeats secret pairdata stage = go M.empty repeats + where + go _ (Just 0) = noop + go cache n = do + addrs <- activeNetworkAddresses + let cache' = updatecache cache addrs + mapM_ (sendinterface cache') addrs + threadDelaySeconds (Seconds 2) + go cache' $ pred <$> n + {- The multicast library currently chokes on ipv6 addresses. -} + sendinterface _ (IPv6Addr _) = noop + sendinterface cache i = void $ tryIO $ + withSocketsDo $ bracket setup cleanup use + where + setup = multicastSender (multicastAddress i) pairingPort + cleanup (sock, _) = sClose sock -- FIXME does not work + use (sock, addr) = do + setInterface sock (showAddr i) + maybe noop (\s -> void $ sendTo sock s addr) + (M.lookup i cache) + updatecache cache [] = cache + updatecache cache (i:is) + | M.member i cache = updatecache cache is + | otherwise = updatecache (M.insert i (show $ mkmsg i) cache) is + mkmsg addr = PairMsg $ + mkVerifiable (stage, pairdata, addr) secret + +startSending :: PairingInProgress -> PairStage -> (PairStage -> IO ()) -> Assistant () +startSending pip stage sender = do + a <- asIO start + void $ liftIO $ forkIO a + where + start = do + tid <- liftIO myThreadId + let pip' = pip { inProgressPairStage = stage, inProgressThreadId = Just tid } + oldpip <- modifyDaemonStatus $ + \s -> (s { pairingInProgress = Just pip' }, pairingInProgress s) + maybe noop stopold oldpip + liftIO $ sender stage + stopold = maybe noop (liftIO . killThread) . inProgressThreadId + +stopSending :: PairingInProgress -> Assistant () +stopSending pip = do + maybe noop (liftIO . killThread) $ inProgressThreadId pip + modifyDaemonStatus_ $ \s -> s { pairingInProgress = Nothing } + +class ToSomeAddr a where + toSomeAddr :: a -> SomeAddr + +instance ToSomeAddr IPv4 where + toSomeAddr (IPv4 a) = IPv4Addr a + +instance ToSomeAddr IPv6 where + toSomeAddr (IPv6 o1 o2 o3 o4) = IPv6Addr (o1, o2, o3, o4) + +showAddr :: SomeAddr -> HostName +showAddr (IPv4Addr a) = show $ IPv4 a +showAddr (IPv6Addr (o1, o2, o3, o4)) = show $ IPv6 o1 o2 o3 o4 + +activeNetworkAddresses :: IO [SomeAddr] +activeNetworkAddresses = filter (not . all (`elem` "0.:") . showAddr) + . concatMap (\ni -> [toSomeAddr $ ipv4 ni, toSomeAddr $ ipv6 ni]) + <$> getNetworkInterfaces + +{- A human-visible description of the repository being paired with. + - Note that the repository's description is not shown to the user, because + - it could be something like "my repo", which is confusing when pairing + - with someone else's repo. However, this has the same format as the + - default decription of a repo. -} +pairRepo :: PairMsg -> String +pairRepo msg = concat + [ remoteUserName d + , "@" + , fromMaybe (showAddr $ pairMsgAddr msg) (remoteHostName d) + , ":" + , remoteDirectory d + ] + where + d = pairMsgData msg diff --git a/Assistant/Pushes.hs b/Assistant/Pushes.hs new file mode 100644 index 0000000000..54f31a84bc --- /dev/null +++ b/Assistant/Pushes.hs @@ -0,0 +1,40 @@ +{- git-annex assistant push tracking + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Assistant.Pushes where + +import Assistant.Common +import Assistant.Types.Pushes + +import Control.Concurrent.STM +import Data.Time.Clock +import qualified Data.Map as M + +{- Blocks until there are failed pushes. + - Returns Remotes whose pushes failed a given time duration or more ago. + - (This may be an empty list.) -} +getFailedPushesBefore :: NominalDiffTime -> Assistant [Remote] +getFailedPushesBefore duration = do + v <- getAssistant failedPushMap + liftIO $ do + m <- atomically $ readTMVar v + now <- getCurrentTime + return $ M.keys $ M.filter (not . toorecent now) m + where + toorecent now time = now `diffUTCTime` time < duration + +{- Modifies the map. -} +changeFailedPushMap :: (PushMap -> PushMap) -> Assistant () +changeFailedPushMap a = do + v <- getAssistant failedPushMap + liftIO $ atomically $ store v . a . fromMaybe M.empty =<< tryTakeTMVar v + where + {- tryTakeTMVar empties the TMVar; refill it only if + - the modified map is not itself empty -} + store v m + | m == M.empty = noop + | otherwise = putTMVar v $! m diff --git a/Assistant/ScanRemotes.hs b/Assistant/ScanRemotes.hs new file mode 100644 index 0000000000..2743c0f361 --- /dev/null +++ b/Assistant/ScanRemotes.hs @@ -0,0 +1,41 @@ +{- git-annex assistant remotes needing scanning + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Assistant.ScanRemotes where + +import Assistant.Common +import Assistant.Types.ScanRemotes +import qualified Types.Remote as Remote + +import Data.Function +import Control.Concurrent.STM +import qualified Data.Map as M + +{- Blocks until there is a remote or remotes that need to be scanned. + - + - The list has higher priority remotes listed first. -} +getScanRemote :: Assistant [(Remote, ScanInfo)] +getScanRemote = do + v <- getAssistant scanRemoteMap + liftIO $ atomically $ + reverse . sortBy (compare `on` scanPriority . snd) . M.toList + <$> takeTMVar v + +{- Adds new remotes that need scanning. -} +addScanRemotes :: Bool -> [Remote] -> Assistant () +addScanRemotes _ [] = noop +addScanRemotes full rs = do + v <- getAssistant scanRemoteMap + liftIO $ atomically $ do + m <- fromMaybe M.empty <$> tryTakeTMVar v + putTMVar v $ M.unionWith merge (M.fromList $ zip rs (map info rs)) m + where + info r = ScanInfo (-1 * Remote.cost r) full + merge x y = ScanInfo + { scanPriority = max (scanPriority x) (scanPriority y) + , fullScan = fullScan x || fullScan y + } diff --git a/Assistant/Ssh.hs b/Assistant/Ssh.hs new file mode 100644 index 0000000000..a623190964 --- /dev/null +++ b/Assistant/Ssh.hs @@ -0,0 +1,293 @@ +{- git-annex assistant ssh utilities + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Assistant.Ssh where + +import Common.Annex +import Utility.Tmp +import Utility.UserInfo +import Utility.Shell +import Git.Remote + +import Data.Text (Text) +import qualified Data.Text as T +import Data.Char +import Network.URI + +data SshData = SshData + { sshHostName :: Text + , sshUserName :: Maybe Text + , sshDirectory :: Text + , sshRepoName :: String + , sshPort :: Int + , needsPubKey :: Bool + , rsyncOnly :: Bool + } + deriving (Read, Show, Eq) + +data SshKeyPair = SshKeyPair + { sshPubKey :: String + , sshPrivKey :: String + } + +instance Show SshKeyPair where + show = sshPubKey + +type SshPubKey = String + +{- ssh -ofoo=bar command-line option -} +sshOpt :: String -> String -> String +sshOpt k v = concat ["-o", k, "=", v] + +sshDir :: IO FilePath +sshDir = do + home <- myHomeDir + return $ home ".ssh" + +{- user@host or host -} +genSshHost :: Text -> Maybe Text -> String +genSshHost host user = maybe "" (\v -> T.unpack v ++ "@") user ++ T.unpack host + +{- Generates a git remote name, like host_dir or host -} +genSshRepoName :: String -> FilePath -> String +genSshRepoName host dir + | null dir = makeLegalName host + | otherwise = makeLegalName $ host ++ "_" ++ dir + +{- The output of ssh, including both stdout and stderr. -} +sshTranscript :: [String] -> (Maybe String) -> IO (String, Bool) +sshTranscript opts input = processTranscript "ssh" opts input + +{- Ensure that the ssh public key doesn't include any ssh options, like + - command=foo, or other weirdness -} +validateSshPubKey :: SshPubKey -> IO () +validateSshPubKey pubkey + | length (lines pubkey) == 1 = + either error return $ check $ words pubkey + | otherwise = error "too many lines in ssh public key" + where + check [prefix, _key, comment] = do + checkprefix prefix + checkcomment comment + check [prefix, _key] = + checkprefix prefix + check _ = err "wrong number of words in ssh public key" + + ok = Right () + err msg = Left $ unwords [msg, pubkey] + + checkprefix prefix + | ssh == "ssh" && all isAlphaNum keytype = ok + | otherwise = err "bad ssh public key prefix" + where + (ssh, keytype) = separate (== '-') prefix + + checkcomment comment = case filter (not . safeincomment) comment of + [] -> ok + badstuff -> err $ "bad comment in ssh public key (contains: \"" ++ badstuff ++ "\")" + safeincomment c = isAlphaNum c || c == '@' || c == '-' || c == '_' || c == '.' + +addAuthorizedKeys :: Bool -> FilePath -> SshPubKey -> IO Bool +addAuthorizedKeys rsynconly dir pubkey = boolSystem "sh" + [ Param "-c" , Param $ addAuthorizedKeysCommand rsynconly dir pubkey ] + +removeAuthorizedKeys :: Bool -> FilePath -> SshPubKey -> IO () +removeAuthorizedKeys rsynconly dir pubkey = do + let keyline = authorizedKeysLine rsynconly dir pubkey + sshdir <- sshDir + let keyfile = sshdir "authorized_keys" + ls <- lines <$> readFileStrict keyfile + writeFile keyfile $ unlines $ filter (/= keyline) ls + +{- Implemented as a shell command, so it can be run on remote servers over + - ssh. + - + - The ~/.ssh/git-annex-shell wrapper script is created if not already + - present. + -} +addAuthorizedKeysCommand :: Bool -> FilePath -> SshPubKey -> String +addAuthorizedKeysCommand rsynconly dir pubkey = intercalate "&&" + [ "mkdir -p ~/.ssh" + , intercalate "; " + [ "if [ ! -e " ++ wrapper ++ " ]" + , "then (" ++ intercalate ";" (map echoval script) ++ ") > " ++ wrapper + , "fi" + ] + , "chmod 700 " ++ wrapper + , "touch ~/.ssh/authorized_keys" + , "chmod 600 ~/.ssh/authorized_keys" + , unwords + [ "echo" + , shellEscape $ authorizedKeysLine rsynconly dir pubkey + , ">>~/.ssh/authorized_keys" + ] + ] + where + echoval v = "echo " ++ shellEscape v + wrapper = "~/.ssh/git-annex-shell" + script = + [ shebang_portable + , "set -e" + , "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 + {- TODO: Locking down rsync is difficult, requiring a rather + - long perl script. -} + | rsynconly = pubkey + | otherwise = limitcommand ++ pubkey + where + limitcommand = "command=\"GIT_ANNEX_SHELL_DIRECTORY="++shellEscape dir++" ~/.ssh/git-annex-shell\",no-agent-forwarding,no-port-forwarding,no-X11-forwarding " + +{- Generates a ssh key pair. -} +genSshKeyPair :: IO SshKeyPair +genSshKeyPair = withTmpDir "git-annex-keygen" $ \dir -> do + ok <- boolSystem "ssh-keygen" + [ Param "-P", Param "" -- no password + , Param "-f", File $ dir "key" + ] + unless ok $ + error "ssh-keygen failed" + SshKeyPair + <$> readFile (dir "key.pub") + <*> readFile (dir "key") + +{- Installs a ssh key pair, and sets up ssh config with a mangled hostname + - that will enable use of the key. This way we avoid changing the user's + - regular ssh experience at all. Returns a modified SshData containing the + - mangled hostname. + - + - Note that the key files are put in ~/.ssh/git-annex/, rather than directly + - in ssh because of an **INSANE** behavior of gnome-keyring: It loads + - ~/.ssh/ANYTHING.pub, and uses them indiscriminately. But using this key + - for a normal login to the server will force git-annex-shell to run, + - and locks the user out. Luckily, it does not recurse into subdirectories. + - + - Similarly, IdentitiesOnly is set in the ssh config to prevent the + - ssh-agent from forcing use of a different key. + -} +setupSshKeyPair :: SshKeyPair -> SshData -> IO SshData +setupSshKeyPair sshkeypair sshdata = do + sshdir <- sshDir + createDirectoryIfMissing True $ parentDir $ sshdir sshprivkeyfile + + unlessM (doesFileExist $ sshdir sshprivkeyfile) $ do + h <- fdToHandle =<< + createFile (sshdir sshprivkeyfile) + (unionFileModes ownerWriteMode ownerReadMode) + hPutStr h (sshPrivKey sshkeypair) + hClose h + unlessM (doesFileExist $ sshdir sshpubkeyfile) $ + writeFile (sshdir sshpubkeyfile) (sshPubKey sshkeypair) + + setSshConfig sshdata + [ ("IdentityFile", "~/.ssh/" ++ sshprivkeyfile) + , ("IdentitiesOnly", "yes") + ] + where + sshprivkeyfile = "git-annex" "key." ++ mangleSshHostName sshdata + sshpubkeyfile = sshprivkeyfile ++ ".pub" + +{- Fixes git-annex ssh key pairs configured in .ssh/config + - by old versions to set IdentitiesOnly. -} +fixSshKeyPair :: IO () +fixSshKeyPair = do + sshdir <- sshDir + let configfile = sshdir "config" + whenM (doesFileExist configfile) $ do + ls <- lines <$> readFileStrict configfile + let ls' = fixSshKeyPair' ls + when (ls /= ls') $ + viaTmp writeFile configfile $ unlines ls' + +{- Strategy: Search for IdentityFile lines in for files with key.git-annex + - in their names. These are for git-annex ssh key pairs. + - Add the IdentitiesOnly line immediately after them, if not already + - present. -} +fixSshKeyPair' :: [String] -> [String] +fixSshKeyPair' = go [] + where + go c [] = reverse c + go c (l:[]) + | all (`isInfixOf` l) indicators = go (fixedline l:l:c) [] + | otherwise = go (l:c) [] + go c (l:next:rest) + | all (`isInfixOf` l) indicators && not ("IdentitiesOnly" `isInfixOf` next) = + go (fixedline l:l:c) (next:rest) + | otherwise = go (l:c) (next:rest) + indicators = ["IdentityFile", "key.git-annex"] + fixedline tmpl = takeWhile isSpace tmpl ++ "IdentitiesOnly yes" + +{- Setups up a ssh config with a mangled hostname. + - Returns a modified SshData containing the mangled hostname. -} +setSshConfig :: SshData -> [(String, String)] -> IO SshData +setSshConfig sshdata config = do + sshdir <- sshDir + createDirectoryIfMissing True sshdir + let configfile = sshdir "config" + unlessM (catchBoolIO $ isInfixOf mangledhost <$> readFile configfile) $ + appendFile configfile $ unlines $ + [ "" + , "# Added automatically by git-annex" + , "Host " ++ mangledhost + ] ++ map (\(k, v) -> "\t" ++ k ++ " " ++ v) + (settings ++ config) + return $ sshdata { sshHostName = T.pack mangledhost } + where + mangledhost = mangleSshHostName sshdata + settings = + [ ("Hostname", T.unpack $ sshHostName sshdata) + , ("Port", show $ sshPort sshdata) + ] + +{- This hostname is specific to a given repository on the ssh host, + - so it is based on the real hostname, the username, and the directory. + - + - The mangled hostname has the form "git-annex-realhostname-username_dir". + - The only use of "-" is to separate the parts shown; this is necessary + - to allow unMangleSshHostName to work. Any unusual characters in the + - username or directory are url encoded, except using "." rather than "%" + - (the latter has special meaning to ssh). + -} +mangleSshHostName :: SshData -> String +mangleSshHostName sshdata = "git-annex-" ++ T.unpack (sshHostName sshdata) + ++ "-" ++ escape extra + where + extra = intercalate "_" $ map T.unpack $ catMaybes + [ sshUserName sshdata + , Just $ sshDirectory sshdata + ] + safe c + | isAlphaNum c = True + | c == '_' = True + | otherwise = False + escape s = replace "%" "." $ escapeURIString safe s + +{- Extracts the real hostname from a mangled ssh hostname. -} +unMangleSshHostName :: String -> String +unMangleSshHostName h = case split "-" h of + ("git":"annex":rest) -> intercalate "-" (beginning rest) + _ -> h + +{- Does ssh have known_hosts data for a hostname? -} +knownHost :: Text -> IO Bool +knownHost hostname = do + sshdir <- sshDir + ifM (doesFileExist $ sshdir "known_hosts") + ( not . null <$> checkhost + , return False + ) + where + {- ssh-keygen -F can crash on some old known_hosts file -} + checkhost = catchDefaultIO "" $ + readProcess "ssh-keygen" ["-F", T.unpack hostname] diff --git a/Assistant/Sync.hs b/Assistant/Sync.hs new file mode 100644 index 0000000000..fe578ab438 --- /dev/null +++ b/Assistant/Sync.hs @@ -0,0 +1,222 @@ +{- git-annex assistant repo syncing + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Assistant.Sync where + +import Assistant.Common +import Assistant.Pushes +import Assistant.NetMessager +import Assistant.Types.NetMessager +import Assistant.Alert +import Assistant.Alert.Utility +import Assistant.DaemonStatus +import Assistant.ScanRemotes +import qualified Command.Sync +import Utility.Parallel +import qualified Git +import qualified Git.Branch +import qualified Git.Command +import qualified Git.Ref +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. + - + - First gets git in sync, and then prepares any necessary file transfers. + - + - 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 + modifyDaemonStatus_ $ \s -> s + { desynced = S.union (S.fromList $ map Remote.uuid xmppremotes) (desynced s) } + syncAction rs (const go) + where + gitremotes = filter (notspecialremote . Remote.repo) rs + (xmppremotes, nonxmppremotes) = partition isXMPPRemote rs + notspecialremote r + | Git.repoIsUrl r = True + | Git.repoIsLocal r = True + | Git.repoIsLocalUnknown r = True + | otherwise = False + sync (Just branch) = do + (failedpull, diverged) <- manualPull (Just branch) gitremotes + now <- liftIO getCurrentTime + failedpush <- pushToRemotes' now notifypushes gitremotes + return (nub $ failedpull ++ failedpush, diverged) + {- No local branch exists yet, but we can try pulling. -} + sync Nothing = manualPull Nothing gitremotes + go = do + (failed, diverged) <- sync + =<< liftAnnex (inRepo Git.Branch.current) + addScanRemotes diverged $ + filter (not . remoteAnnexIgnore . Remote.gitconfig) + nonxmppremotes + return failed + +{- Updates the local sync branch, then pushes it to all remotes, in + - parallel, along with the git-annex branch. This is the same + - as "git annex sync", except in parallel, and will co-exist with use of + - "git annex sync". + - + - After the pushes to normal git remotes, also signals XMPP clients that + - they can request an XMPP push. + - + - Avoids running possibly long-duration commands in the Annex monad, so + - as not to block other threads. + - + - This can fail, when the remote's sync branch (or git-annex branch) has + - been updated by some other remote pushing into it, or by the remote + - itself. To handle failure, a manual pull and merge is done, and the push + - is retried. + - + - When there's a lot of activity, we may fail more than once. + - On the other hand, we may fail because the remote is not available. + - Rather than retrying indefinitely, after the first retry we enter a + - fallback mode, where our push is guarenteed to succeed if the remote is + - reachable. If the fallback fails, the push is queued to be retried + - later. + - + - Returns any remotes that it failed to push to. + -} +pushToRemotes :: Bool -> [Remote] -> Assistant [Remote] +pushToRemotes notifypushes remotes = do + now <- liftIO getCurrentTime + syncAction remotes (pushToRemotes' now notifypushes) +pushToRemotes' :: UTCTime -> Bool -> [Remote] -> Assistant [Remote] +pushToRemotes' now notifypushes remotes = do + (g, branch, u) <- liftAnnex $ do + Annex.Branch.commit "update" + (,,) + <$> gitRepo + <*> inRepo Git.Branch.current + <*> getUUID + let (xmppremotes, normalremotes) = partition isXMPPRemote remotes + ret <- go True branch g u normalremotes + unless (null xmppremotes) $ do + shas <- liftAnnex $ map fst <$> + inRepo (Git.Ref.matchingWithHEAD + [Annex.Branch.fullname, Git.Ref.headRef]) + forM_ xmppremotes $ \r -> sendNetMessage $ + Pushing (getXMPPClientID r) (CanPush u shas) + return ret + where + go _ Nothing _ _ _ = return [] -- no branch, so nothing to do + go _ _ _ _ [] = return [] -- no remotes, so nothing to do + go shouldretry (Just branch) g u rs = do + debug ["pushing to", show rs] + liftIO $ Command.Sync.updateBranch (Command.Sync.syncBranch branch) g + (succeeded, failed) <- liftIO $ inParallel (push g branch) rs + updatemap succeeded [] + if null failed + then do + when notifypushes $ + sendNetMessage $ NotifyPush $ + map Remote.uuid succeeded + return failed + else if shouldretry + then retry branch g u failed + else fallback branch g u failed + + updatemap succeeded failed = changeFailedPushMap $ \m -> + M.union (makemap failed) $ + M.difference m (makemap succeeded) + makemap l = M.fromList $ zip l (repeat now) + + retry branch g u rs = do + debug ["trying manual pull to resolve failed pushes"] + void $ manualPull (Just branch) rs + go False (Just branch) g u rs + + fallback branch g u rs = do + debug ["fallback pushing to", show rs] + (succeeded, failed) <- liftIO $ + inParallel (\r -> taggedPush u Nothing branch r g) rs + updatemap succeeded failed + when (notifypushes && (not $ null succeeded)) $ + sendNetMessage $ NotifyPush $ + map Remote.uuid succeeded + return failed + + push g branch remote = Command.Sync.pushBranch remote branch g + +{- Displays an alert while running an action that syncs with some remotes, + - and returns any remotes that it failed to sync with. + - + - XMPP remotes are handled specially; since the action can only start + - an async process for them, they are not included in the alert, but are + - still passed to the action. + - + - Readonly remotes are also hidden (to hide the web special remote). + -} +syncAction :: [Remote] -> ([Remote] -> Assistant [Remote]) -> Assistant [Remote] +syncAction rs a + | null visibleremotes = a rs + | otherwise = do + i <- addAlert $ syncAlert visibleremotes + failed <- a rs + let failed' = filter (not . Git.repoIsLocalUnknown . Remote.repo) failed + let succeeded = filter (`notElem` failed) visibleremotes + if null succeeded && null failed' + then removeAlert i + else updateAlertMap $ mergeAlert i $ + syncResultAlert succeeded failed' + return failed + where + visibleremotes = filter (not . Remote.readonly) $ + filter (not . isXMPPRemote) rs + +{- Manually pull from remotes and merge their branches. Returns any + - remotes that it failed to pull from, and a Bool indicating + - whether the git-annex branches of the remotes and local had + - diverged 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 ([Remote], Bool) +manualPull currentbranch remotes = do + g <- liftAnnex gitRepo + let (xmppremotes, normalremotes) = partition isXMPPRemote remotes + failed <- liftIO $ forM normalremotes $ \r -> + ifM (Git.Command.runBool [Param "fetch", Param $ Remote.name r] g) + ( return Nothing + , return $ Just r + ) + haddiverged <- liftAnnex Annex.Branch.forceUpdate + forM_ normalremotes $ \r -> + liftAnnex $ Command.Sync.mergeRemote r currentbranch + u <- liftAnnex getUUID + forM_ xmppremotes $ \r -> + sendNetMessage $ Pushing (getXMPPClientID r) (PushRequest u) + return (catMaybes failed, haddiverged) + +{- Start syncing a remote, using a background thread. -} +syncRemote :: Remote -> Assistant () +syncRemote remote = do + updateSyncRemotes + thread <- asIO $ do + reconnectRemotes False [remote] + addScanRemotes True [remote] + void $ liftIO $ forkIO $ thread diff --git a/Assistant/Threads/Committer.hs b/Assistant/Threads/Committer.hs new file mode 100644 index 0000000000..be3bc3c842 --- /dev/null +++ b/Assistant/Threads/Committer.hs @@ -0,0 +1,492 @@ +{- git-annex assistant commit thread + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE CPP #-} + +module Assistant.Threads.Committer where + +import Assistant.Common +import Assistant.Changes +import Assistant.Types.Changes +import Assistant.Commits +import Assistant.Alert +import Assistant.DaemonStatus +import Assistant.TransferQueue +import Assistant.Drop +import Logs.Transfer +import Logs.Location +import qualified Annex.Queue +import qualified Git.Command +import qualified Git.LsFiles +import qualified Git.BuildVersion +import qualified Command.Add +import Utility.ThreadScheduler +import qualified Utility.Lsof as Lsof +import qualified Utility.DirWatcher as DirWatcher +import Types.KeySource +import Config +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 +commitThread = namedThread "Committer" $ do + delayadd <- liftAnnex $ + maybe delayaddDefault (return . Just . Seconds) + =<< annexDelayAdd <$> Annex.getGitConfig + waitChangeTime $ \(changes, time) -> do + readychanges <- handleAdds delayadd changes + if shouldCommit time (length readychanges) readychanges + then do + debug + [ "committing" + , show (length readychanges) + , "changes" + ] + void $ alertWhile commitAlert $ + liftAnnex commitStaged + recordCommit + let numchanges = length readychanges + mapM_ checkChangeContent readychanges + return numchanges + else do + refill readychanges + return 0 + +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, and then + - runs an action to commit them. If more changes arrive while this is + - going on, they're handled intelligently, batching up changes into + - large commits where possible, doing rename detection, and + - commiting immediately otherwise. -} +waitChangeTime :: (([Change], UTCTime) -> Assistant Int) -> Assistant () +waitChangeTime a = waitchanges 0 + where + waitchanges lastcommitsize = do + -- Wait one one second as a simple rate limiter. + liftIO $ threadDelaySeconds (Seconds 1) + -- Now, wait until at least one change is available for + -- processing. + cs <- getChanges + handlechanges cs lastcommitsize + handlechanges changes lastcommitsize = do + let len = length changes + -- See if now's a good time to commit. + now <- liftIO getCurrentTime + case (lastcommitsize >= maxCommitSize, shouldCommit now len changes, possiblyrename changes) of + (True, True, _) + | len > maxCommitSize -> + waitchanges =<< a (changes, now) + | otherwise -> aftermaxcommit changes + (_, True, False) -> + waitchanges =<< a (changes, now) + (_, True, True) -> do + morechanges <- getrelatedchanges changes + waitchanges =<< a (changes ++ morechanges, now) + _ -> do + refill changes + waitchanges lastcommitsize + + {- 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) + + {- The last commit was maximum size, so it's very likely there + - are more changes and we'd like to ensure we make another commit + - of maximum size if possible. + - + - But, it can take a while for the Watcher to wake back up + - after a commit. It can get blocked by another thread + - that is using the Annex state, such as a git-annex branch + - commit. Especially after such a large commit, this can + - take several seconds. When this happens, it defeats the + - normal commit batching, which sees some old changes the + - Watcher found while the commit was being prepared, and sees + - no recent ones, and wants to commit immediately. + - + - All that we need to do, then, is wait for the Watcher to + - wake up, and queue up one more change. + - + - However, it's also possible that we're at the end of changes for + - now. So to avoid waiting a really long time before committing + - those changes we have, poll for up to 30 seconds, and then + - commit them. + - + - Also, try to run something in Annex, to ensure we block + - longer if the Annex state is indeed blocked. + -} + aftermaxcommit oldchanges = loop (30 :: Int) + where + loop 0 = continue oldchanges + loop n = do + liftAnnex noop -- ensure Annex state is free + liftIO $ threadDelaySeconds (Seconds 1) + changes <- getAnyChanges + if null changes + then loop (n - 1) + else continue (oldchanges ++ changes) + continue cs + | null cs = waitchanges 0 + | otherwise = handlechanges cs 0 + +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 + +maxCommitSize :: Int +maxCommitSize = 5000 + +{- 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 -> Int -> [Change] -> Bool +shouldCommit now len changes + | len == 0 = False + | len >= maxCommitSize = True + | length recentchanges < 10 = True + | otherwise = False -- batch activity + where + 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 + - something else. -} + v <- tryAnnex Annex.Queue.flush + case v of + Left _ -> return False + Right _ -> do + {- 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" : nomessage params) + return True + where + params = + [ Param "--quiet" + {- Avoid running the usual pre-commit hook; + - the Watcher does the same symlink fixing, + - and direct mode bookkeeping updating. -} + , Param "--no-verify" + ] + nomessage ps + | Git.BuildVersion.older "1.7.2" = + Param "-m" : Param "autocommit" : ps + | Git.BuildVersion.older "1.7.8" = + Param "--allow-empty-message" : + Param "-m" : Param "" : ps + | otherwise = + Param "--allow-empty-message" : + Param "--no-edit" : Param "-m" : Param "" : ps + +{- 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 + - it. -} +delayaddDefault :: Annex (Maybe Seconds) +#ifdef darwin_HOST_OS +delayaddDefault = ifM isDirect + ( return Nothing + , return $ Just $ Seconds 1 + ) +#else +delayaddDefault = return Nothing +#endif + +{- If there are PendingAddChanges, or InProcessAddChanges, the files + - have not yet actually been added to the annex, and that has to be done + - now, before committing. + - + - Deferring the adds to this point causes batches to be bundled together, + - which allows faster checking with lsof that the files are not still open + - for write by some other process, and faster checking with git-ls-files + - that the files are not already checked into git. + - + - When a file is added, Inotify will notice the new symlink. So this waits + - for additional Changes to arrive, so that the symlink has hopefully been + - staged before returning, and will be committed immediately. + - + - OTOH, for kqueue, eventsCoalesce, so instead the symlink is directly + - created and staged. + - + - Returns a list of all changes that are ready to be committed. + - Any pending adds that are not ready yet are put back into the ChangeChan, + - where they will be retried later. + -} +handleAdds :: Maybe Seconds -> [Change] -> Assistant [Change] +handleAdds delayadd cs = returnWhen (null incomplete) $ do + let (pending, inprocess) = partition isPendingAddChange incomplete + direct <- liftAnnex isDirect + (pending', cleanup) <- if direct + then return (pending, noop) + else findnew pending + (postponed, toadd) <- partitionEithers <$> safeToAdd delayadd pending' inprocess + cleanup + + unless (null postponed) $ + refillChanges postponed + + returnWhen (null toadd) $ do + added <- addaction toadd $ + catMaybes <$> if direct + then adddirect toadd + else forM toadd add + if DirWatcher.eventsCoalesce || null added || direct + then return $ added ++ otherchanges + else do + r <- handleAdds delayadd =<< getChanges + return $ r ++ added ++ otherchanges + where + (incomplete, otherchanges) = partition (\c -> isPendingAddChange c || isInProcessAddChange c) cs + + findnew [] = return ([], noop) + findnew pending@(exemplar:_) = do + (newfiles, cleanup) <- liftAnnex $ + inRepo (Git.LsFiles.notInRepo False $ map changeFile pending) + -- note: timestamp info is lost here + let ts = changeTime exemplar + return (map (PendingAddChange ts) newfiles, void $ liftIO $ cleanup) + + returnWhen c a + | c = return otherchanges + | otherwise = a + + add :: Change -> Assistant (Maybe Change) + add change@(InProcessAddChange { keySource = ks }) = + catchDefaultIO Nothing <~> do + sanitycheck ks $ do + key <- liftAnnex $ do + showStart "add" $ keyFilename ks + Command.Add.ingest $ Just ks + maybe (failedingest change) (done change $ keyFilename ks) key + add _ = return Nothing + + {- 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 . concat <$> mapM mkpairs (catMaybes mks) + where + mkpairs k = map (\c -> (inodeCacheToKey ct c, k)) <$> + recordedInodeCache k + + failedingest change = do + refill [retryChange change] + liftAnnex showEndFail + return Nothing + + done change file key = liftAnnex $ do + logStatus key InfoPresent + link <- ifM isDirect + ( inRepo $ gitAnnexLink 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. -} + sanitycheck keysource a = do + fs <- liftIO $ getSymbolicLinkStatus $ keyFilename keysource + ks <- liftIO $ getSymbolicLinkStatus $ contentLocation keysource + if deviceID ks == deviceID fs && fileID ks == fileID fs + then a + else do + -- remove the hard link + when (contentLocation keysource /= keyFilename keysource) $ + void $ liftIO $ tryIO $ removeFile $ contentLocation keysource + return Nothing + + {- Shown an alert while performing an action to add a file or + - files. When only a few files are added, their names are shown + - in the alert. When it's a batch add, the number of files added + - is shown. + - + - Add errors tend to be transient and will be + - automatically dealt with, so the alert is always told + - the add succeeded. + -} + addaction [] a = a + addaction toadd a = alertWhile' (addFileAlert $ map changeFile toadd) $ + (,) + <$> pure True + <*> a + +{- Files can Either be Right to be added now, + - or are unsafe, and must be Left for later. + - + - Check by running lsof on the repository. + -} +safeToAdd :: Maybe Seconds -> [Change] -> [Change] -> Assistant [Either Change Change] +safeToAdd _ [] [] = return [] +safeToAdd delayadd pending inprocess = do + maybe noop (liftIO . threadDelaySeconds) delayadd + liftAnnex $ do + keysources <- mapM Command.Add.lockDown (map changeFile pending) + let inprocess' = inprocess ++ catMaybes (map mkinprocess $ zip pending keysources) + openfiles <- S.fromList . map fst3 . filter openwrite <$> + findopenfiles (map keySource inprocess') + let checked = map (check openfiles) inprocess' + + {- If new events are received when files are closed, + - there's no need to retry any changes that cannot + - be done now. -} + if DirWatcher.closingTracked + then do + mapM_ canceladd $ lefts checked + allRight $ rights checked + else return checked + where + check openfiles change@(InProcessAddChange { keySource = ks }) + | S.member (contentLocation ks) openfiles = Left change + check _ change = Right change + + mkinprocess (c, Just ks) = Just $ InProcessAddChange + { changeTime = changeTime c + , keySource = ks + } + mkinprocess (_, Nothing) = Nothing + + canceladd (InProcessAddChange { keySource = ks }) = do + warning $ keyFilename ks + ++ " still has writers, not adding" + -- remove the hard link + when (contentLocation ks /= keyFilename ks) $ + void $ liftIO $ tryIO $ removeFile $ contentLocation ks + canceladd _ = noop + + openwrite (_file, mode, _pid) + | mode == Lsof.OpenWriteOnly = True + | mode == Lsof.OpenReadWrite = True + | mode == Lsof.OpenUnknown = True + | otherwise = False + + allRight = return . map Right + + {- Normally the KeySources are locked down inside the temp directory, + - so can just lsof that, which is quite efficient. + - + - In crippled filesystem mode, there is no lock down, so must run lsof + - on each individual file. + -} + findopenfiles keysources = ifM crippledFileSystem + ( liftIO $ do + let segments = segmentXargs $ map keyFilename keysources + concat <$> forM segments (\fs -> Lsof.query $ "--" : fs) + , do + 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/ConfigMonitor.hs b/Assistant/Threads/ConfigMonitor.hs new file mode 100644 index 0000000000..6a01ff35ee --- /dev/null +++ b/Assistant/Threads/ConfigMonitor.hs @@ -0,0 +1,86 @@ +{- git-annex assistant config monitor thread + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Assistant.Threads.ConfigMonitor where + +import Assistant.Common +import Assistant.BranchChange +import Assistant.DaemonStatus +import Assistant.Commits +import Utility.ThreadScheduler +import Logs.UUID +import Logs.Trust +import Logs.Remote +import Logs.PreferredContent +import Logs.Group +import Remote.List (remoteListRefresh) +import qualified Git.LsTree as LsTree +import qualified Annex.Branch + +import qualified Data.Set as S + +{- This thread detects when configuration changes have been made to the + - git-annex branch and reloads cached configuration. + - + - If the branch is frequently changing, it's checked for configuration + - changes no more often than once every 60 seconds. On the other hand, + - if the branch has not changed in a while, configuration changes will + - be detected immediately. + -} +configMonitorThread :: NamedThread +configMonitorThread = namedThread "ConfigMonitor" $ loop =<< getConfigs + where + loop old = do + waitBranchChange + new <- getConfigs + when (old /= new) $ do + let changedconfigs = new `S.difference` old + debug $ "reloading config" : + map fst (S.toList changedconfigs) + reloadConfigs new + {- Record a commit to get this config + - change pushed out to remotes. -} + recordCommit + liftIO $ threadDelaySeconds (Seconds 60) + loop new + +{- Config files, and their checksums. -} +type Configs = S.Set (FilePath, String) + +{- All git-annex's config files, and actions to run when they change. -} +configFilesActions :: [(FilePath, Annex ())] +configFilesActions = + [ (uuidLog, void $ uuidMapLoad) + , (remoteLog, void remoteListRefresh) + , (trustLog, void trustMapLoad) + , (groupLog, void groupMapLoad) + -- Preferred content settings depend on most of the other configs, + -- so will be reloaded whenever any configs change. + , (preferredContentLog, noop) + ] + +reloadConfigs :: Configs -> Assistant () +reloadConfigs changedconfigs = do + liftAnnex $ do + sequence_ as + void preferredContentMapLoad + {- Changes to the remote log, or the trust log, can affect the + - syncRemotes list. Changes to the uuid log may affect its + - display so are also included. -} + when (any (`elem` fs) [remoteLog, trustLog, uuidLog]) $ + updateSyncRemotes + where + (fs, as) = unzip $ filter (flip S.member changedfiles . fst) + configFilesActions + changedfiles = S.map fst changedconfigs + +getConfigs :: Assistant Configs +getConfigs = S.fromList . map extract + <$> liftAnnex (inRepo $ LsTree.lsTreeFiles Annex.Branch.fullname files) + where + files = map fst configFilesActions + extract treeitem = (LsTree.file treeitem, LsTree.sha treeitem) diff --git a/Assistant/Threads/DaemonStatus.hs b/Assistant/Threads/DaemonStatus.hs new file mode 100644 index 0000000000..5bbb15acbe --- /dev/null +++ b/Assistant/Threads/DaemonStatus.hs @@ -0,0 +1,29 @@ +{- git-annex assistant daemon status thread + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Assistant.Threads.DaemonStatus where + +import Assistant.Common +import Assistant.DaemonStatus +import Utility.ThreadScheduler +import Utility.NotificationBroadcaster + +{- This writes the daemon status to disk, when it changes, but no more + - frequently than once every ten minutes. + -} +daemonStatusThread :: NamedThread +daemonStatusThread = namedThread "DaemonStatus" $ do + notifier <- liftIO . newNotificationHandle False + =<< changeNotifier <$> getDaemonStatus + checkpoint + runEvery (Seconds tenMinutes) <~> do + liftIO $ waitNotification notifier + checkpoint + where + checkpoint = do + file <- liftAnnex $ fromRepo gitAnnexDaemonStatusFile + liftIO . writeDaemonStatusFile file =<< getDaemonStatus diff --git a/Assistant/Threads/Glacier.hs b/Assistant/Threads/Glacier.hs new file mode 100644 index 0000000000..46f64cd561 --- /dev/null +++ b/Assistant/Threads/Glacier.hs @@ -0,0 +1,43 @@ +{- git-annex assistant Amazon Glacier retrieval + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE CPP #-} +{-# LANGUAGE OverloadedStrings #-} + +module Assistant.Threads.Glacier where + +import Assistant.Common +import Utility.ThreadScheduler +import qualified Types.Remote as Remote +import qualified Remote.Glacier as Glacier +import Logs.Transfer +import Assistant.DaemonStatus +import Assistant.TransferQueue + +import qualified Data.Set as S + +{- Wakes up every half hour and checks if any glacier remotes have failed + - downloads. If so, runs glacier-cli to check if the files are now + - available, and queues the downloads. -} +glacierThread :: NamedThread +glacierThread = namedThread "Glacier" $ runEvery (Seconds 3600) <~> go + where + isglacier r = Remote.remotetype r == Glacier.remote + go = do + rs <- filter isglacier . syncDataRemotes <$> getDaemonStatus + forM_ rs $ \r -> + check r =<< (liftAnnex $ getFailedTransfers $ Remote.uuid r) + check _ [] = noop + check r l = do + let keys = map getkey l + (availkeys, failedkeys) <- liftAnnex $ Glacier.jobList r keys + let s = S.fromList (failedkeys ++ availkeys) + let l' = filter (\p -> S.member (getkey p) s) l + forM_ l' $ \(t, info) -> do + liftAnnex $ removeFailedTransfer t + queueTransferWhenSmall "object available from glacier" (associatedFile info) t r + getkey = transferKey . fst diff --git a/Assistant/Threads/Merger.hs b/Assistant/Threads/Merger.hs new file mode 100644 index 0000000000..650293e4b2 --- /dev/null +++ b/Assistant/Threads/Merger.hs @@ -0,0 +1,118 @@ +{- git-annex assistant git merge thread + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Assistant.Threads.Merger where + +import Assistant.Common +import Assistant.TransferQueue +import Assistant.BranchChange +import Assistant.DaemonStatus +import Assistant.ScanRemotes +import Utility.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 +mergeThread = namedThread "Merger" $ do + g <- liftAnnex gitRepo + let dir = Git.localGitDir g "refs" + liftIO $ createDirectoryIfMissing True dir + let hook a = Just <$> asIO2 (runHandler a) + changehook <- hook onChange + errhook <- hook onErr + let hooks = mkWatchHooks + { addHook = changehook + , modifyHook = changehook + , errHook = errhook + } + void $ liftIO $ watchDir dir (const False) hooks id + debug ["watching", dir] + +type Handler = FilePath -> Assistant () + +{- Runs an action handler. + - + - Exceptions are ignored, otherwise a whole thread could be crashed. + -} +runHandler :: Handler -> FilePath -> Maybe FileStatus -> Assistant () +runHandler handler file _filestatus = + either (liftIO . print) (const noop) =<< tryIO <~> handler file + +{- Called when there's an error with inotify. -} +onErr :: Handler +onErr msg = error msg + +{- Called when a new branch ref is written, or a branch ref is modified. + - + - At startup, synthetic add events fire, causing this to run, but that's + - ok; it ensures that any changes pushed since the last time the assistant + - ran are merged in. + -} +onChange :: Handler +onChange file + | ".lock" `isSuffixOf` file = noop + | isAnnexBranch file = do + branchChanged + diverged <- liftAnnex Annex.Branch.forceUpdate + when diverged $ + unlessM handleDesynced $ + queueDeferredDownloads "retrying deferred download" Later + | "/synced/" `isInfixOf` file = + mergecurrent =<< liftAnnex (inRepo Git.Branch.current) + | otherwise = noop + where + changedbranch = fileToBranch file + + mergecurrent (Just current) + | equivBranches changedbranch current = do + debug + [ "merging", show changedbranch + , "into", show current + ] + 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 + where + base = takeFileName . show + +isAnnexBranch :: FilePath -> Bool +isAnnexBranch f = n `isSuffixOf` f + where + n = "/" ++ show Annex.Branch.name + +fileToBranch :: FilePath -> Git.Ref +fileToBranch f = Git.Ref $ "refs" base + where + base = Prelude.last $ split "/refs/" f diff --git a/Assistant/Threads/MountWatcher.hs b/Assistant/Threads/MountWatcher.hs new file mode 100644 index 0000000000..143ae9cee4 --- /dev/null +++ b/Assistant/Threads/MountWatcher.hs @@ -0,0 +1,192 @@ +{- git-annex assistant mount watcher, using either dbus or mtab polling + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE CPP #-} +{-# LANGUAGE OverloadedStrings #-} + +module Assistant.Threads.MountWatcher where + +import Assistant.Common +import Assistant.DaemonStatus +import Assistant.Sync +import qualified Annex +import qualified Git +import Utility.ThreadScheduler +import Utility.Mounts +import Remote.List +import qualified Types.Remote as Remote + +import qualified Data.Set as S + +#if WITH_DBUS +import Utility.DBus +import DBus.Client +import DBus +import Data.Word (Word32) +import Control.Concurrent +import qualified Control.Exception as E +#else +#warning Building without dbus support; will use mtab polling +#endif + +mountWatcherThread :: NamedThread +mountWatcherThread = namedThread "MountWatcher" $ +#if WITH_DBUS + dbusThread +#else + pollingThread +#endif + +#if WITH_DBUS + +dbusThread :: Assistant () +dbusThread = do + runclient <- asIO1 go + r <- liftIO $ E.try $ runClient getSessionAddress runclient + either onerr (const noop) r + where + go client = ifM (checkMountMonitor client) + ( do + {- Store the current mount points in an MVar, to be + - compared later. We could in theory work out the + - mount point from the dbus message, but this is + - easier. -} + mvar <- liftIO $ newMVar =<< currentMountPoints + handleevent <- asIO1 $ \_event -> do + nowmounted <- liftIO $ currentMountPoints + wasmounted <- liftIO $ swapMVar mvar nowmounted + handleMounts wasmounted nowmounted + liftIO $ forM_ mountChanged $ \matcher -> + listen client matcher handleevent + , do + liftAnnex $ + warning "No known volume monitor available through dbus; falling back to mtab polling" + pollingThread + ) + onerr :: E.SomeException -> Assistant () + onerr e = do + {- If the session dbus fails, the user probably + - logged out of their desktop. Even if they log + - back in, we won't have access to the dbus + - session key, so polling is the best that can be + - done in this situation. -} + liftAnnex $ + warning $ "dbus failed; falling back to mtab polling (" ++ show e ++ ")" + pollingThread + +{- Examine the list of services connected to dbus, to see if there + - are any we can use to monitor mounts. If not, will attempt to start one. -} +checkMountMonitor :: Client -> Assistant Bool +checkMountMonitor client = do + running <- filter (`elem` usableservices) + <$> liftIO (listServiceNames client) + case running of + [] -> startOneService client startableservices + (service:_) -> do + debug [ "Using running DBUS service" + , service + , "to monitor mount events." + ] + return True + where + startableservices = [gvfs, gvfsgdu] + usableservices = startableservices ++ [kde] + gvfs = "org.gtk.Private.UDisks2VolumeMonitor" + gvfsgdu = "org.gtk.Private.GduVolumeMonitor" + kde = "org.kde.DeviceNotifications" + +startOneService :: Client -> [ServiceName] -> Assistant Bool +startOneService _ [] = return False +startOneService client (x:xs) = do + _ <- liftIO $ tryNonAsync $ callDBus client "StartServiceByName" + [toVariant x, toVariant (0 :: Word32)] + ifM (liftIO $ elem x <$> listServiceNames client) + ( do + debug + [ "Started DBUS service", x + , "to monitor mount events." + ] + return True + , startOneService client xs + ) + +{- Filter matching events recieved when drives are mounted and unmounted. -} +mountChanged :: [MatchRule] +mountChanged = [gvfs True, gvfs False, kde, kdefallback] + where + {- gvfs reliably generates this event whenever a + - drive is mounted/unmounted, whether automatically, or manually -} + gvfs mount = matchAny + { matchInterface = Just "org.gtk.Private.RemoteVolumeMonitor" + , matchMember = Just $ if mount then "MountAdded" else "MountRemoved" + } + {- This event fires when KDE prompts the user what to do with a drive, + - but maybe not at other times. And it's not received -} + kde = matchAny + { matchInterface = Just "org.kde.Solid.Device" + , matchMember = Just "setupDone" + } + {- This event may not be closely related to mounting a drive, but it's + - observed reliably when a drive gets mounted or unmounted. -} + kdefallback = matchAny + { matchInterface = Just "org.kde.KDirNotify" + , matchMember = Just "enteredDirectory" + } + +#endif + +pollingThread :: Assistant () +pollingThread = go =<< liftIO currentMountPoints + where + go wasmounted = do + liftIO $ threadDelaySeconds (Seconds 10) + nowmounted <- liftIO currentMountPoints + handleMounts wasmounted nowmounted + go nowmounted + +handleMounts :: MountPoints -> MountPoints -> Assistant () +handleMounts wasmounted nowmounted = + mapM_ (handleMount . mnt_dir) $ + S.toList $ newMountPoints wasmounted nowmounted + +handleMount :: FilePath -> Assistant () +handleMount dir = do + debug ["detected mount of", dir] + rs <- filter (Git.repoIsLocal . Remote.repo) <$> remotesUnder dir + reconnectRemotes True rs + +{- Finds remotes located underneath the mount point. + - + - Updates state to include the remotes. + - + - The config of git remotes is re-read, as it may not have been available + - at startup time, or may have changed (it could even be a different + - repository at the same remote location..) + -} +remotesUnder :: FilePath -> Assistant [Remote] +remotesUnder dir = do + repotop <- liftAnnex $ fromRepo Git.repoPath + rs <- liftAnnex remoteList + pairs <- liftAnnex $ mapM (checkremote repotop) rs + let (waschanged, rs') = unzip pairs + when (any id waschanged) $ do + liftAnnex $ Annex.changeState $ \s -> s { Annex.remotes = rs' } + updateSyncRemotes + return $ map snd $ filter fst pairs + where + checkremote repotop r = case Remote.localpath r of + Just p | dirContains dir (absPathFrom repotop p) -> + (,) <$> pure True <*> updateRemote r + _ -> return (False, r) + +type MountPoints = S.Set Mntent + +currentMountPoints :: IO MountPoints +currentMountPoints = S.fromList <$> getMounts + +newMountPoints :: MountPoints -> MountPoints -> MountPoints +newMountPoints old new = S.difference new old diff --git a/Assistant/Threads/NetWatcher.hs b/Assistant/Threads/NetWatcher.hs new file mode 100644 index 0000000000..5974de11d3 --- /dev/null +++ b/Assistant/Threads/NetWatcher.hs @@ -0,0 +1,131 @@ +{- git-annex assistant network connection watcher, using dbus + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE CPP #-} +{-# LANGUAGE OverloadedStrings #-} + +module Assistant.Threads.NetWatcher where + +import Assistant.Common +import Assistant.Sync +import Utility.ThreadScheduler +import qualified Types.Remote as Remote +import Assistant.DaemonStatus + +#if WITH_DBUS +import Utility.DBus +import DBus.Client +import DBus +import Data.Word (Word32) +import Assistant.NetMessager +#else +#warning Building without dbus support; will poll for network connection changes +#endif + +netWatcherThread :: NamedThread +#if WITH_DBUS +netWatcherThread = thread dbusThread +#else +netWatcherThread = thread noop +#endif + where + thread = namedThread "NetWatcher" + +{- This is a fallback for when dbus cannot be used to detect + - network connection changes, but it also ensures that + - any networked remotes that may have not been routable for a + - while (despite the local network staying up), are synced with + - periodically. -} +netWatcherFallbackThread :: NamedThread +netWatcherFallbackThread = namedThread "NetWatcherFallback" $ + runEvery (Seconds 3600) <~> handleConnection + +#if WITH_DBUS + +dbusThread :: Assistant () +dbusThread = do + handleerr <- asIO2 onerr + runclient <- asIO1 go + liftIO $ persistentClient getSystemAddress () handleerr runclient + where + go client = ifM (checkNetMonitor client) + ( do + listenNMConnections client <~> handleconn + listenWicdConnections client <~> handleconn + , do + liftAnnex $ + warning "No known network monitor available through dbus; falling back to polling" + ) + handleconn = do + debug ["detected network connection"] + notifyNetMessagerRestart + handleConnection + onerr e _ = do + liftAnnex $ + warning $ "lost dbus connection; falling back to polling (" ++ show e ++ ")" + {- Wait, in hope that dbus will come back -} + liftIO $ threadDelaySeconds (Seconds 60) + +{- Examine the list of services connected to dbus, to see if there + - are any we can use to monitor network connections. -} +checkNetMonitor :: Client -> Assistant Bool +checkNetMonitor client = do + running <- liftIO $ filter (`elem` [networkmanager, wicd]) + <$> listServiceNames client + case running of + [] -> return False + (service:_) -> do + debug [ "Using running DBUS service" + , service + , "to monitor network connection events." + ] + return True + where + networkmanager = "org.freedesktop.NetworkManager" + wicd = "org.wicd.daemon" + +{- Listens for new NetworkManager connections. -} +listenNMConnections :: Client -> IO () -> IO () +listenNMConnections client callback = + listen client matcher $ \event -> + when (Just True == anyM activeconnection (signalBody event)) $ + callback + where + matcher = matchAny + { matchInterface = Just "org.freedesktop.NetworkManager.Connection.Active" + , matchMember = Just "PropertiesChanged" + } + nm_connection_activated = toVariant (2 :: Word32) + nm_state_key = toVariant ("State" :: String) + activeconnection v = do + m <- fromVariant v + vstate <- lookup nm_state_key $ dictionaryItems m + state <- fromVariant vstate + return $ state == nm_connection_activated + +{- Listens for new Wicd connections. -} +listenWicdConnections :: Client -> IO () -> IO () +listenWicdConnections client callback = + listen client matcher $ \event -> + when (any (== wicd_success) (signalBody event)) $ + callback + where + matcher = matchAny + { matchInterface = Just "org.wicd.daemon" + , matchMember = Just "ConnectResultsSent" + } + wicd_success = toVariant ("success" :: String) + +#endif + +handleConnection :: Assistant () +handleConnection = reconnectRemotes True =<< networkRemotes + +{- Network remotes to sync with. -} +networkRemotes :: Assistant [Remote] +networkRemotes = filter (isNothing . Remote.localpath) . syncRemotes + <$> getDaemonStatus diff --git a/Assistant/Threads/PairListener.hs b/Assistant/Threads/PairListener.hs new file mode 100644 index 0000000000..8fc015c223 --- /dev/null +++ b/Assistant/Threads/PairListener.hs @@ -0,0 +1,148 @@ +{- git-annex assistant thread to listen for incoming pairing traffic + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Assistant.Threads.PairListener where + +import Assistant.Common +import Assistant.Pairing +import Assistant.Pairing.Network +import Assistant.Pairing.MakeRemote +import Assistant.WebApp (UrlRenderer) +import Assistant.WebApp.Types +import Assistant.Alert +import Assistant.DaemonStatus +import Utility.ThreadScheduler +import Git + +import Network.Multicast +import Network.Socket +import qualified Data.Text as T +import Data.Char + +pairListenerThread :: UrlRenderer -> NamedThread +pairListenerThread urlrenderer = namedThread "PairListener" $ do + listener <- asIO1 $ go [] [] + liftIO $ withSocketsDo $ + runEvery (Seconds 1) $ void $ tryIO $ + listener =<< getsock + where + {- Note this can crash if there's no network interface, + - or only one like lo that doesn't support multicast. -} + getsock = multicastReceiver (multicastAddress $ IPv4Addr undefined) pairingPort + + go reqs cache sock = liftIO (getmsg sock []) >>= \msg -> case readish msg of + Nothing -> go reqs cache sock + Just m -> do + debug ["received", show msg] + sane <- checkSane msg + (pip, verified) <- verificationCheck m + =<< (pairingInProgress <$> getDaemonStatus) + let wrongstage = maybe False (\p -> pairMsgStage m <= inProgressPairStage p) pip + case (wrongstage, sane, pairMsgStage m) of + -- ignore our own messages, and + -- out of order messages + (True, _, _) -> go reqs cache sock + (_, False, _) -> go reqs cache sock + (_, _, PairReq) -> if m `elem` reqs + then go reqs (invalidateCache m cache) sock + else do + pairReqReceived verified urlrenderer m + go (m:take 10 reqs) (invalidateCache m cache) sock + (_, _, PairAck) -> do + cache' <- pairAckReceived verified pip m cache + go reqs cache' sock + (_, _, PairDone) -> do + pairDoneReceived verified pip m + go reqs cache sock + + {- As well as verifying the message using the shared secret, + - check its UUID against the UUID we have stored. If + - they're the same, someone is sending bogus messages, + - which could be an attempt to brute force the shared secret. -} + verificationCheck _ Nothing = return (Nothing, False) + verificationCheck m (Just pip) + | not verified && sameuuid = do + liftAnnex $ warning + "detected possible pairing brute force attempt; disabled pairing" + stopSending pip + return (Nothing, False) + |otherwise = return (Just pip, verified && sameuuid) + where + verified = verifiedPairMsg m pip + sameuuid = pairUUID (inProgressPairData pip) == pairUUID (pairMsgData m) + + {- Various sanity checks on the content of the message. -} + checkSane msg + {- Control characters could be used in a + - console poisoning attack. -} + | any isControl msg || any (`elem` "\r\n") msg = do + liftAnnex $ warning + "illegal control characters in pairing message; ignoring" + return False + | otherwise = return True + + {- PairReqs invalidate the cache of recently finished pairings. + - This is so that, if a new pairing is started with the + - same secret used before, a bogus PairDone is not sent. -} + invalidateCache msg = filter (not . verifiedPairMsg msg) + + getmsg sock c = do + (msg, n, _) <- recvFrom sock chunksz + if n < chunksz + then return $ c ++ msg + else getmsg sock $ c ++ msg + where + chunksz = 1024 + +{- Show an alert when a PairReq is seen. -} +pairReqReceived :: Bool -> UrlRenderer -> PairMsg -> Assistant () +pairReqReceived True _ _ = noop -- ignore our own PairReq +pairReqReceived False urlrenderer msg = do + button <- mkAlertButton (T.pack "Respond") urlrenderer (FinishLocalPairR msg) + void $ addAlert $ pairRequestReceivedAlert repo button + where + repo = pairRepo msg + +{- When a verified PairAck is seen, a host is ready to pair with us, and has + - already configured our ssh key. Stop sending PairReqs, finish the pairing, + - and send a single PairDone. -} +pairAckReceived :: Bool -> Maybe PairingInProgress -> PairMsg -> [PairingInProgress] -> Assistant [PairingInProgress] +pairAckReceived True (Just pip) msg cache = do + stopSending pip + repodir <- repoPath <$> liftAnnex gitRepo + liftIO $ setupAuthorizedKeys msg repodir + finishedLocalPairing msg (inProgressSshKeyPair pip) + startSending pip PairDone $ multicastPairMsg + (Just 1) (inProgressSecret pip) (inProgressPairData pip) + return $ pip : take 10 cache +{- A stale PairAck might also be seen, after we've finished pairing. + - Perhaps our PairDone was not received. To handle this, we keep + - a cache of recently finished pairings, and re-send PairDone in + - response to stale PairAcks for them. -} +pairAckReceived _ _ msg cache = do + let pips = filter (verifiedPairMsg msg) cache + unless (null pips) $ + forM_ pips $ \pip -> + startSending pip PairDone $ multicastPairMsg + (Just 1) (inProgressSecret pip) (inProgressPairData pip) + return cache + +{- If we get a verified PairDone, the host has accepted our PairAck, and + - has paired with us. Stop sending PairAcks, and finish pairing with them. + - + - TODO: Should third-party hosts remove their pair request alert when they + - see a PairDone? + - Complication: The user could have already clicked on the alert and be + - entering the secret. Would be better to start a fresh pair request in this + - situation. + -} +pairDoneReceived :: Bool -> Maybe PairingInProgress -> PairMsg -> Assistant () +pairDoneReceived False _ _ = noop -- not verified +pairDoneReceived True Nothing _ = noop -- not in progress +pairDoneReceived True (Just pip) msg = do + stopSending pip + finishedLocalPairing msg (inProgressSshKeyPair pip) diff --git a/Assistant/Threads/Pusher.hs b/Assistant/Threads/Pusher.hs new file mode 100644 index 0000000000..060f26cf51 --- /dev/null +++ b/Assistant/Threads/Pusher.hs @@ -0,0 +1,48 @@ +{- git-annex assistant git pushing thread + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Assistant.Threads.Pusher where + +import Assistant.Common +import Assistant.Commits +import Assistant.Pushes +import Assistant.DaemonStatus +import Assistant.Sync +import Utility.ThreadScheduler +import qualified Types.Remote as Remote + +{- This thread retries pushes that failed before. -} +pushRetryThread :: NamedThread +pushRetryThread = namedThread "PushRetrier" $ runEvery (Seconds halfhour) <~> do + -- We already waited half an hour, now wait until there are failed + -- pushes to retry. + topush <- getFailedPushesBefore (fromIntegral halfhour) + unless (null topush) $ do + debug ["retrying", show (length topush), "failed pushes"] + void $ pushToRemotes True topush + where + halfhour = 1800 + +{- This thread pushes git commits out to remotes soon after they are made. -} +pushThread :: NamedThread +pushThread = namedThread "Pusher" $ runEvery (Seconds 2) <~> do + -- We already waited two seconds as a simple rate limiter. + -- Next, wait until at least one commit has been made + void getCommits + -- Now see if now's a good time to push. + void $ pushToRemotes True =<< pushTargets + +{- 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 diff --git a/Assistant/Threads/SanityChecker.hs b/Assistant/Threads/SanityChecker.hs new file mode 100644 index 0000000000..0c97a9e8f7 --- /dev/null +++ b/Assistant/Threads/SanityChecker.hs @@ -0,0 +1,138 @@ +{- git-annex assistant sanity checker + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Assistant.Threads.SanityChecker ( + 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 Utility.Batch +import Config + +import Data.Time.Clock.POSIX + +{- 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"] + void $ alertWhile sanityCheckAlert go + debug ["sanity check complete"] + where + go = do + modifyDaemonStatus_ $ \s -> s { sanityCheckRunning = True } + + now <- liftIO $ getPOSIXTime -- before check started + r <- either showerr return =<< (tryIO . batch) <~> dailyCheck + + modifyDaemonStatus_ $ \s -> s + { sanityCheckRunning = False + , lastSanityCheck = Just now + } + + return r + + showerr e = do + liftAnnex $ warning $ show e + return False + +{- Only run one check per day, from the time of the last check. -} +waitForNextCheck :: Assistant () +waitForNextCheck = do + v <- lastSanityCheck <$> getDaemonStatus + now <- liftIO getPOSIXTime + liftIO $ threadDelaySeconds $ Seconds $ calcdelay now v + where + calcdelay _ Nothing = oneDay + calcdelay now (Just lastcheck) + | lastcheck < now = max oneDay $ + oneDay - truncate (now - lastcheck) + | otherwise = oneDay + +{- 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. -} +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 + forM_ unstaged $ \file -> do + ms <- liftIO $ catchMaybeIO $ getSymbolicLinkStatus file + case ms of + Just s | toonew (statusChangeTime s) now -> noop + | 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) + slop = fromIntegral tenMinutes + insanity msg = do + liftAnnex $ warning msg + void $ addAlert $ sanityCheckFixAlert msg + addsymlink file s = do + 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/TransferPoller.hs b/Assistant/Threads/TransferPoller.hs new file mode 100644 index 0000000000..68075cac8a --- /dev/null +++ b/Assistant/Threads/TransferPoller.hs @@ -0,0 +1,56 @@ +{- git-annex assistant transfer polling thread + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Assistant.Threads.TransferPoller where + +import Assistant.Common +import Assistant.DaemonStatus +import Logs.Transfer +import Utility.NotificationBroadcaster +import qualified Assistant.Threads.TransferWatcher as TransferWatcher + +import Control.Concurrent +import qualified Data.Map as M + +{- This thread polls the status of ongoing transfers, determining how much + - of each transfer is complete. -} +transferPollerThread :: NamedThread +transferPollerThread = namedThread "TransferPoller" $ do + g <- liftAnnex gitRepo + tn <- liftIO . newNotificationHandle True =<< + transferNotifier <$> getDaemonStatus + forever $ do + liftIO $ threadDelay 500000 -- 0.5 seconds + ts <- currentTransfers <$> getDaemonStatus + if M.null ts + -- block until transfers running + then liftIO $ waitNotification tn + else mapM_ (poll g) $ M.toList ts + where + poll g (t, info) + {- Downloads are polled by checking the size of the + - temp file being used for the transfer. -} + | transferDirection t == Download = do + let f = gitAnnexTmpLocation (transferKey t) g + sz <- liftIO $ catchMaybeIO $ + fromIntegral . fileSize <$> getFileStatus f + newsize t info sz + {- Uploads don't need to be polled for when the TransferWatcher + - thread can track file modifications. -} + | TransferWatcher.watchesTransferSize = noop + {- Otherwise, this code polls the upload progress + - by reading the transfer info file. -} + | otherwise = do + let f = transferFile t g + mi <- liftIO $ catchDefaultIO Nothing $ + readTransferInfoFile Nothing f + maybe noop (newsize t info . bytesComplete) mi + + newsize t info sz + | bytesComplete info /= sz && isJust sz = + alterTransferInfo t $ \i -> i { bytesComplete = sz } + | otherwise = noop diff --git a/Assistant/Threads/TransferScanner.hs b/Assistant/Threads/TransferScanner.hs new file mode 100644 index 0000000000..5a6871fdb7 --- /dev/null +++ b/Assistant/Threads/TransferScanner.hs @@ -0,0 +1,180 @@ +{- git-annex assistant thread to scan remotes to find needed transfers + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Assistant.Threads.TransferScanner where + +import Assistant.Common +import Assistant.Types.ScanRemotes +import Assistant.ScanRemotes +import Assistant.TransferQueue +import Assistant.DaemonStatus +import Assistant.Drop +import Assistant.Sync +import Assistant.DeleteRemote +import Assistant.Types.UrlRenderer +import Logs.Transfer +import Logs.Location +import Logs.Group +import Logs.Web (webUUID) +import qualified Remote +import qualified Types.Remote as Remote +import Utility.ThreadScheduler +import Utility.NotificationBroadcaster +import Utility.Batch +import qualified Git.LsFiles as LsFiles +import qualified Backend +import Annex.Content +import Annex.Wanted + +import qualified Data.Set as S + +{- This thread waits until a remote needs to be scanned, to find transfers + - that need to be made, to keep data in sync. + -} +transferScannerThread :: UrlRenderer -> NamedThread +transferScannerThread urlrenderer = namedThread "TransferScanner" $ do + startupScan + go S.empty + where + go scanned = do + scanrunning False + liftIO $ threadDelaySeconds (Seconds 2) + (rs, infos) <- unzip <$> getScanRemote + scanrunning True + if any fullScan infos || any (`S.notMember` scanned) rs + then do + expensiveScan urlrenderer rs + go $ scanned `S.union` S.fromList rs + else do + mapM_ failedTransferScan rs + go scanned + scanrunning b = do + ds <- modifyDaemonStatus $ \s -> + (s { transferScanRunning = b }, s) + liftIO $ sendNotification $ transferNotifier ds + + {- 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. + - * We may have run before, and had transfers queued, + - and then the system (or us) crashed, and that info was + - lost. + - * A remote may be in the unwanted group, and this is a chance + - to determine if the remote has been emptied. + -} + startupScan = do + reconnectRemotes True =<< syncGitRemotes <$> getDaemonStatus + addScanRemotes True =<< syncDataRemotes <$> getDaemonStatus + +{- This is a cheap scan for failed transfers involving a remote. -} +failedTransferScan :: Remote -> Assistant () +failedTransferScan r = do + failed <- liftAnnex $ getFailedTransfers (Remote.uuid r) + liftAnnex $ mapM_ removeFailedTransfer $ map fst failed + mapM_ retry failed + where + retry (t, info) + | transferDirection t == Download = do + {- Check if the remote still has the key. + - If not, relies on the expensiveScan to + - get it queued from some other remote. -} + whenM (liftAnnex $ remoteHas r $ transferKey t) $ + requeue t info + | otherwise = do + {- The Transferrer checks when uploading + - that the remote doesn't already have the + - key, so it's not redundantly checked here. -} + requeue t info + 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 + - too large. + - + - This also finds files that are present either here or on a remote + - but that are not preferred content, and drops them. Searching for files + - to drop is done concurrently with the scan for transfers. + - + - TODO: It would be better to first drop as much as we can, before + - transferring much, to minimise disk use. + - + - During the scan, we'll also check if any unwanted repositories are empty, + - and can be removed. While unrelated, this is a cheap place to do it, + - since we need to look at the locations of all keys anyway. + -} +expensiveScan :: UrlRenderer -> [Remote] -> Assistant () +expensiveScan urlrenderer rs = unless onlyweb $ batch <~> do + debug ["starting scan of", show visiblers] + + unwantedrs <- liftAnnex $ S.fromList + <$> filterM inUnwantedGroup (map Remote.uuid rs) + + g <- liftAnnex gitRepo + (files, cleanup) <- liftIO $ LsFiles.inRepo [] g + removablers <- scan unwantedrs files + void $ liftIO cleanup + + debug ["finished scan of", show visiblers] + + remove <- asIO1 $ removableRemote urlrenderer + liftIO $ mapM_ (void . tryNonAsync . remove) $ S.toList removablers + where + onlyweb = all (== webUUID) $ map Remote.uuid rs + visiblers = let rs' = filter (not . Remote.readonly) rs + in if null rs' then rs else rs' + + scan unwanted [] = return unwanted + scan unwanted (f:fs) = do + (unwanted', ts) <- maybe + (return (unwanted, [])) + (findtransfers f unwanted) + =<< liftAnnex (Backend.lookupFile f) + mapM_ (enqueue f) ts + scan unwanted' fs + + enqueue f (r, t) = + queueTransferWhenSmall "expensive scan found missing object" + (Just f) t r + findtransfers f unwanted (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 + let slocs = S.fromList locs + let use a = return $ catMaybes $ map (a key slocs) syncrs + ts <- if present + then filterM (wantSend True (Just f) . Remote.uuid . fst) + =<< use (genTransfer Upload False) + else ifM (wantGet True $ Just f) + ( use (genTransfer Download True) , return [] ) + let unwanted' = S.difference unwanted slocs + return (unwanted', ts) + +genTransfer :: Direction -> Bool -> Key -> S.Set UUID -> Remote -> Maybe (Remote, Transfer) +genTransfer direction want key slocs r + | direction == Upload && Remote.readonly r = Nothing + | (S.member (Remote.uuid r) slocs) == want = Just + (r, Transfer direction (Remote.uuid r) key) + | otherwise = Nothing + +remoteHas :: Remote -> Key -> Annex Bool +remoteHas r key = elem + <$> pure (Remote.uuid r) + <*> loggedLocations key diff --git a/Assistant/Threads/TransferWatcher.hs b/Assistant/Threads/TransferWatcher.hs new file mode 100644 index 0000000000..7045e842dc --- /dev/null +++ b/Assistant/Threads/TransferWatcher.hs @@ -0,0 +1,126 @@ +{- git-annex assistant transfer watching thread + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Assistant.Threads.TransferWatcher where + +import Assistant.Common +import Assistant.DaemonStatus +import Assistant.TransferQueue +import Assistant.Drop +import Annex.Content +import Logs.Transfer +import Utility.DirWatcher +import Utility.DirWatcher.Types +import qualified Remote + +import Control.Concurrent + +{- This thread watches for changes to the gitAnnexTransferDir, + - and updates the DaemonStatus's map of ongoing transfers. -} +transferWatcherThread :: NamedThread +transferWatcherThread = namedThread "TransferWatcher" $ do + dir <- liftAnnex $ gitAnnexTransferDir <$> gitRepo + liftIO $ createDirectoryIfMissing True dir + let hook a = Just <$> asIO2 (runHandler a) + addhook <- hook onAdd + delhook <- hook onDel + modifyhook <- hook onModify + errhook <- hook onErr + let hooks = mkWatchHooks + { addHook = addhook + , delHook = delhook + , modifyHook = modifyhook + , errHook = errhook + } + void $ liftIO $ watchDir dir (const False) hooks id + debug ["watching for transfers"] + +type Handler = FilePath -> Assistant () + +{- Runs an action handler. + - + - Exceptions are ignored, otherwise a whole thread could be crashed. + -} +runHandler :: Handler -> FilePath -> Maybe FileStatus -> Assistant () +runHandler handler file _filestatus = + either (liftIO . print) (const noop) =<< tryIO <~> handler file + +{- Called when there's an error with inotify. -} +onErr :: Handler +onErr msg = error msg + +{- Called when a new transfer information file is written. -} +onAdd :: Handler +onAdd file = case parseTransferFile file of + Nothing -> noop + Just t -> go t =<< liftAnnex (checkTransfer t) + where + go _ Nothing = noop -- transfer already finished + go t (Just info) = do + debug [ "transfer starting:", describeTransfer t info ] + r <- liftAnnex $ Remote.remoteFromUUID $ transferUUID t + updateTransferInfo t info { transferRemote = r } + +{- Called when a transfer information file is updated. + - + - The only thing that should change in the transfer info is the + - bytesComplete, so that's the only thing updated in the DaemonStatus. -} +onModify :: Handler +onModify file = do + case parseTransferFile file of + Nothing -> noop + Just t -> go t =<< liftIO (readTransferInfoFile Nothing file) + where + go _ Nothing = noop + go t (Just newinfo) = alterTransferInfo t $ + \i -> i { bytesComplete = bytesComplete newinfo } + +{- This thread can only watch transfer sizes when the DirWatcher supports + - tracking modificatons to files. -} +watchesTransferSize :: Bool +watchesTransferSize = modifyTracked + +{- Called when a transfer information file is removed. -} +onDel :: Handler +onDel file = case parseTransferFile file of + Nothing -> noop + Just t -> do + debug [ "transfer finishing:", show t] + minfo <- removeTransfer t + + finished <- asIO2 finishedTransfer + void $ liftIO $ forkIO $ do + {- XXX race workaround delay. The location + - log needs to be updated before finishedTransfer + - runs. -} + threadDelay 10000000 -- 10 seconds + finished t minfo + +{- Queue uploads of files downloaded to us, spreading them + - out to other reachable remotes. + - + - Downloading a file may have caused a remote to not want it; + - so check for drops from remotes. + - + - Uploading a file may cause the local repo, or some other remote to not + - want it; handle that too. + -} +finishedTransfer :: Transfer -> Maybe TransferInfo -> Assistant () +finishedTransfer t (Just info) + | transferDirection t == Download = + whenM (liftAnnex $ inAnnex $ transferKey t) $ do + 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 new file mode 100644 index 0000000000..67a8c2a7b5 --- /dev/null +++ b/Assistant/Threads/Transferrer.hs @@ -0,0 +1,140 @@ +{- git-annex assistant data transferrer thread + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Assistant.Threads.Transferrer where + +import Assistant.Common +import Assistant.DaemonStatus +import Assistant.TransferQueue +import Assistant.TransferSlots +import Assistant.Alert +import Assistant.Alert.Utility +import Assistant.Commits +import Assistant.Drop +import Assistant.TransferrerPool +import Logs.Transfer +import Logs.Location +import Annex.Content +import qualified Remote +import qualified Types.Remote as Remote +import qualified Git +import Config.Files +import Assistant.Threads.TransferWatcher +import Annex.Wanted + +{- Dispatches transfers from the queue. -} +transfererThread :: NamedThread +transfererThread = namedThread "Transferrer" $ do + program <- liftIO readProgramFile + forever $ inTransferSlot program $ + maybe (return Nothing) (uncurry $ genTransfer) + =<< getNextTransfer notrunning + where + {- Skip transfers that are already running. -} + notrunning = isNothing . startedTime + +{- By the time this is called, the daemonstatus's currentTransfers map should + - already have been updated to include the transfer. -} +genTransfer :: Transfer -> TransferInfo -> TransferGenerator +genTransfer t info = case (transferRemote info, associatedFile info) of + (Just remote, Just file) + | Git.repoIsLocalUnknown (Remote.repo remote) -> do + -- optimisation for removable drives not plugged in + liftAnnex $ recordFailedTransfer t info + void $ removeTransfer t + return Nothing + | otherwise -> ifM (liftAnnex $ shouldTransfer t info) + ( do + debug [ "Transferring:" , describeTransfer t info ] + notifyTransfer + return $ Just (t, info, go remote file) + , do + debug [ "Skipping unnecessary transfer:", + describeTransfer t info ] + void $ removeTransfer t + finishedTransfer t (Just info) + return Nothing + ) + _ -> return Nothing + where + direction = transferDirection t + isdownload = direction == Download + + {- Alerts are only shown for successful transfers. + - Transfers can temporarily fail for many reasons, + - so there's no point in bothering the user about + - those. The assistant should recover. + - + - After a successful upload, handle dropping it from + - here, if desired. In this case, the remote it was + - uploaded to is known to have it. + - + - Also, after a successful transfer, the location + - log has changed. Indicate that a commit has been + - made, in order to queue a push of the git-annex + - branch out to remotes that did not participate + - in the transfer. + - + - If the process failed, it could have crashed, + - so remove the transfer from the list of current + - transfers, just in case it didn't stop + - in a way that lets the TransferWatcher do its + - usual cleanup. However, first check if something else is + - running the transfer, to avoid removing active transfers. + -} + go remote file transferrer = ifM (liftIO $ performTransfer transferrer t $ associatedFile info) + ( do + void $ addAlert $ makeAlertFiller True $ + transferFileAlert direction True file + unless isdownload $ + handleDrops + ("object uploaded to " ++ show remote) + True (transferKey t) + (associatedFile info) + (Just remote) + void $ recordCommit + , whenM (liftAnnex $ isNothing <$> checkTransfer t) $ + void $ removeTransfer t + ) + +{- 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) <&&> 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 new file mode 100644 index 0000000000..ef8bcd41f9 --- /dev/null +++ b/Assistant/Threads/Watcher.hs @@ -0,0 +1,352 @@ +{- git-annex assistant tree watcher + - + - Copyright 2012-2013 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE DeriveDataTypeable, BangPatterns, CPP #-} + +module Assistant.Threads.Watcher ( + watchThread, + WatcherException(..), + checkCanWatch, + needLsof, + onAddSymlink, + runHandler, +) where + +import Assistant.Common +import Assistant.DaemonStatus +import Assistant.Changes +import Assistant.Types.Changes +import Assistant.Alert +import Utility.DirWatcher +import Utility.DirWatcher.Types +import Utility.Lsof +import qualified Annex +import qualified Annex.Queue +import qualified Git +import qualified Git.UpdateIndex +import qualified Git.LsFiles as LsFiles +import qualified Backend +import Annex.Direct +import Annex.Content.Direct +import Annex.CatFile +import Annex.CheckIgnore +import Annex.Link +import Annex.FileMatcher +import Annex.ReplaceFile +import Git.Types +import Config +import Utility.ThreadScheduler + +import Data.Bits.Utils +import Data.Typeable +import qualified Data.ByteString.Lazy as L +import qualified Control.Exception as E +import Data.Time.Clock + +checkCanWatch :: Annex () +checkCanWatch + | canWatch = do + liftIO setupLsof + unlessM (liftIO (inPath "lsof") <||> Annex.getState Annex.force) + needLsof + | otherwise = error "watch mode is not available on this system" + +needLsof :: Annex () +needLsof = error $ unlines + [ "The lsof command is needed for watch mode to be safe, and is not in PATH." + , "To override lsof checks to ensure that files are not open for writing" + , "when added to the annex, you can use --force" + , "Be warned: This can corrupt data in the annex, and make fsck complain." + ] + +{- A special exception that can be thrown to pause or resume the watcher. -} +data WatcherException = PauseWatcher | ResumeWatcher + deriving (Show, Eq, Typeable) + +instance E.Exception WatcherException + +watchThread :: NamedThread +watchThread = namedThread "Watcher" $ + ifM (liftAnnex $ annexAutoCommit <$> Annex.getGitConfig) + ( runWatcher + , waitFor ResumeWatcher runWatcher + ) + +runWatcher :: Assistant () +runWatcher = do + startup <- asIO1 startupScan + matcher <- liftAnnex $ largeFilesMatcher + direct <- liftAnnex isDirect + symlinkssupported <- liftAnnex $ coreSymlinks <$> Annex.getGitConfig + addhook <- hook $ if direct + then onAddDirect symlinkssupported matcher + else onAdd matcher + delhook <- hook onDel + addsymlinkhook <- hook $ onAddSymlink direct + deldirhook <- hook onDelDir + errhook <- hook onErr + let hooks = mkWatchHooks + { addHook = addhook + , delHook = delhook + , addSymlinkHook = addsymlinkhook + , delDirHook = deldirhook + , errHook = errhook + } + handle <- liftIO $ watchDir "." ignored hooks startup + debug [ "watching", "."] + + {- Let the DirWatcher thread run until signalled to pause it, + - then wait for a resume signal, and restart. -} + waitFor PauseWatcher $ do + liftIO $ stopWatchDir handle + waitFor ResumeWatcher runWatcher + where + hook a = Just <$> asIO2 (runHandler a) + +waitFor :: WatcherException -> Assistant () -> Assistant () +waitFor sig next = do + r <- liftIO $ (E.try pause :: IO (Either E.SomeException ())) + case r of + Left e -> case E.fromException e of + Just s + | s == sig -> next + _ -> noop + _ -> noop + where + pause = runEvery (Seconds 86400) noop + +{- Initial scartup scan. The action should return once the scan is complete. -} +startupScan :: IO a -> Assistant a +startupScan scanner = do + liftAnnex $ showAction "scanning" + alertWhile' startupScanAlert $ do + r <- liftIO $ scanner + + -- Notice any files that were deleted before + -- watching was started. + top <- liftAnnex $ fromRepo Git.repoPath + (fs, cleanup) <- liftAnnex $ inRepo $ LsFiles.deleted [top] + forM_ fs $ \f -> do + liftAnnex $ onDel' f + maybe noop recordChange =<< madeChange f RmChange + void $ liftIO $ cleanup + + liftAnnex $ showAction "started" + liftIO $ putStrLn "" + + modifyDaemonStatus_ $ \s -> s { scanComplete = True } + + return (True, r) + +{- Hardcoded ignores, passed to the DirWatcher so it can avoid looking + - at the entire .git directory. Does not include .gitignores. -} +ignored :: FilePath -> Bool +ignored = ig . takeFileName + where + ig ".git" = True + ig ".gitignore" = True + ig ".gitattributes" = True +#ifdef darwin_HOST_OS + ig ".DS_Store" = True +#endif + ig _ = False + +unlessIgnored :: FilePath -> Assistant (Maybe Change) -> Assistant (Maybe Change) +unlessIgnored file a = ifM (liftAnnex $ checkIgnored file) + ( noChange + , a + ) + +type Handler = FilePath -> Maybe FileStatus -> Assistant (Maybe Change) + +{- Runs an action handler, and if there was a change, adds it to the ChangeChan. + - + - Exceptions are ignored, otherwise a whole watcher thread could be crashed. + -} +runHandler :: Handler -> FilePath -> Maybe FileStatus -> Assistant () +runHandler handler file filestatus = void $ do + r <- tryIO <~> handler (normalize file) filestatus + case r of + Left e -> liftIO $ print e + Right Nothing -> noop + Right (Just change) -> do + -- Just in case the commit thread is not + -- flushing the queue fast enough. + liftAnnex $ Annex.Queue.flushWhenFull + recordChange change + where + normalize f + | "./" `isPrefixOf` file = drop 2 f + | otherwise = f + +{- Small files are added to git as-is, while large ones go into the annex. -} +add :: FileMatcher -> FilePath -> Assistant (Maybe Change) +add bigfilematcher file = ifM (liftAnnex $ checkFileMatcher bigfilematcher file) + ( pendingAddChange file + , do + liftAnnex $ Annex.Queue.addCommand "add" + [Params "--force --"] [file] + madeChange file AddFileChange + ) + +onAdd :: FileMatcher -> Handler +onAdd matcher file filestatus + | maybe False isRegularFile filestatus = + unlessIgnored file $ + add matcher file + | otherwise = noChange + +{- In direct mode, add events are received for both new files, and + - modified existing files. + -} +onAddDirect :: Bool -> FileMatcher -> Handler +onAddDirect symlinkssupported matcher file fs = do + v <- liftAnnex $ catKeyFile file + case (v, fs) of + (Just key, Just filestatus) -> + ifM (liftAnnex $ sameFileStatus key filestatus) + {- It's possible to get an add event for + - an existing file that is not + - really modified, but it might have + - just been deleted and been put back, + - so it symlink is restaged to make sure. -} + ( ifM (scanComplete <$> getDaemonStatus) + ( do + link <- liftAnnex $ inRepo $ gitAnnexLink file key + addLink file link (Just key) + , noChange + ) + , guardSymlinkStandin (Just key) $ do + debug ["changed direct", file] + liftAnnex $ changedDirect key file + add matcher file + ) + _ -> unlessIgnored file $ + guardSymlinkStandin Nothing $ do + debug ["add direct", file] + add matcher file + where + {- On a filesystem without symlinks, we'll get changes for regular + - files that git uses to stand-in for symlinks. Detect when + - this happens, and stage the symlink, rather than annexing the + - file. -} + guardSymlinkStandin mk a + | symlinkssupported = a + | otherwise = do + linktarget <- liftAnnex $ getAnnexLinkTarget file + case linktarget of + Nothing -> a + Just lt -> do + case fileKey $ takeFileName lt of + Nothing -> noop + Just key -> void $ liftAnnex $ + addAssociatedFile key file + onAddSymlink' linktarget mk True file fs + +{- A symlink might be an arbitrary symlink, which is just added. + - Or, if it is a git-annex symlink, ensure it points to the content + - before adding it. + -} +onAddSymlink :: Bool -> Handler +onAddSymlink isdirect file filestatus = unlessIgnored file $ do + linktarget <- liftIO (catchMaybeIO $ readSymbolicLink file) + kv <- liftAnnex (Backend.lookupFile file) + onAddSymlink' linktarget (fmap fst kv) isdirect file filestatus + +onAddSymlink' :: Maybe String -> Maybe Key -> Bool -> Handler +onAddSymlink' linktarget mk isdirect file filestatus = go mk + where + go (Just key) = do + when isdirect $ + liftAnnex $ void $ addAssociatedFile key file + link <- liftAnnex $ inRepo $ gitAnnexLink file key + if linktarget == Just link + then ensurestaged (Just link) =<< getDaemonStatus + else do + unless isdirect $ + liftAnnex $ replaceFile file $ + makeAnnexLink link + addLink file link (Just key) + -- other symlink, not git-annex + go Nothing = ensurestaged linktarget =<< getDaemonStatus + + {- This is often called on symlinks that are already + - staged correctly. A symlink may have been deleted + - and being re-added, or added when the watcher was + - not running. So they're normally restaged to make sure. + - + - As an optimisation, during the startup scan, avoid + - restaging everything. Only links that were created since + - the last time the daemon was running are staged. + - (If the daemon has never ran before, avoid staging + - links too.) + -} + ensurestaged (Just link) daemonstatus + | scanComplete daemonstatus = addLink file link mk + | otherwise = case filestatus of + Just s + | not (afterLastDaemonRun (statusChangeTime s) daemonstatus) -> noChange + _ -> addLink file link mk + ensurestaged Nothing _ = noChange + +{- For speed, tries to reuse the existing blob for symlink target. -} +addLink :: FilePath -> FilePath -> Maybe Key -> Assistant (Maybe Change) +addLink file link mk = do + debug ["add symlink", file] + liftAnnex $ do + v <- catObjectDetails $ Ref $ ':':file + case v of + Just (currlink, sha) + | s2w8 link == L.unpack currlink -> + stageSymlink file sha + _ -> stageSymlink file =<< hashSymlink link + madeChange file $ LinkChange mk + +onDel :: Handler +onDel file _ = do + debug ["file deleted", file] + liftAnnex $ onDel' file + madeChange file RmChange + +onDel' :: FilePath -> Annex () +onDel' file = do + whenM isDirect $ do + mkey <- catKeyFile file + case mkey of + Nothing -> noop + Just key -> void $ removeAssociatedFile key file + Annex.Queue.addUpdateIndex =<< + inRepo (Git.UpdateIndex.unstageFile file) + +{- 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. + - + - 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] + (fs, clean) <- liftAnnex $ inRepo $ LsFiles.deleted [dir] + + liftAnnex $ mapM_ onDel' fs + + -- Get the events queued up as fast as possible, so the + -- committer sees them all in one block. + now <- liftIO getCurrentTime + recordChanges $ map (\f -> Change now f RmChange) fs + + void $ liftIO $ clean + liftAnnex $ Annex.Queue.flushWhenFull + noChange + +{- Called when there's an error with inotify or kqueue. -} +onErr :: Handler +onErr msg _ = do + liftAnnex $ warning msg + void $ addAlert $ warningAlert "watcher" msg + noChange diff --git a/Assistant/Threads/WebApp.hs b/Assistant/Threads/WebApp.hs new file mode 100644 index 0000000000..0fb038b6a7 --- /dev/null +++ b/Assistant/Threads/WebApp.hs @@ -0,0 +1,101 @@ +{- git-annex assistant webapp thread + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE TemplateHaskell, MultiParamTypeClasses #-} +{-# LANGUAGE CPP #-} +{-# OPTIONS_GHC -fno-warn-orphans #-} + +module Assistant.Threads.WebApp where + +import Assistant.Common +import Assistant.WebApp +import Assistant.WebApp.Types +import Assistant.WebApp.DashBoard +import Assistant.WebApp.SideBar +import Assistant.WebApp.Notifications +import Assistant.WebApp.RepoList +import Assistant.WebApp.Configurators +import Assistant.WebApp.Configurators.Local +import Assistant.WebApp.Configurators.Ssh +import Assistant.WebApp.Configurators.Pairing +import Assistant.WebApp.Configurators.AWS +import Assistant.WebApp.Configurators.IA +import Assistant.WebApp.Configurators.WebDAV +import Assistant.WebApp.Configurators.XMPP +import Assistant.WebApp.Configurators.Preferences +import Assistant.WebApp.Configurators.Edit +import Assistant.WebApp.Configurators.Delete +import Assistant.WebApp.Documentation +import Assistant.WebApp.Control +import Assistant.WebApp.OtherRepos +import Assistant.Types.ThreadedMonad +import Utility.WebApp +import Utility.Tmp +import Utility.FileMode +import Git + +import Yesod +import Network.Socket (SockAddr, HostName) +import Data.Text (pack, unpack) + +mkYesodDispatch "WebApp" $(parseRoutesFile "Assistant/WebApp/routes") + +type Url = String + +webAppThread + :: AssistantData + -> UrlRenderer + -> Bool + -> Maybe HostName + -> Maybe (IO Url) + -> Maybe (Url -> FilePath -> IO ()) + -> NamedThread +webAppThread assistantdata urlrenderer noannex listenhost postfirstrun onstartup = thread $ liftIO $ do +#ifdef __ANDROID__ + when (isJust listenhost) $ + -- See Utility.WebApp + error "Sorry, --listen is not currently supported on Android" +#endif + webapp <- WebApp + <$> pure assistantdata + <*> (pack <$> genRandomToken) + <*> getreldir + <*> pure staticRoutes + <*> pure postfirstrun + <*> pure noannex + <*> pure listenhost + setUrlRenderer urlrenderer $ yesodRender webapp (pack "") + app <- toWaiAppPlain webapp + app' <- ifM debugEnabled + ( return $ httpDebugLogger app + , return app + ) + runWebApp listenhost app' $ \addr -> if noannex + then withTmpFile "webapp.html" $ \tmpfile _ -> + go addr webapp tmpfile Nothing + else do + let st = threadState assistantdata + htmlshim <- runThreadState st $ fromRepo gitAnnexHtmlShim + urlfile <- runThreadState st $ fromRepo gitAnnexUrlFile + go addr webapp htmlshim (Just urlfile) + where + thread = namedThread "WebApp" + getreldir + | noannex = return Nothing + | otherwise = Just <$> + (relHome =<< absPath + =<< runThreadState (threadState assistantdata) (fromRepo repoPath)) + go addr webapp htmlshim urlfile = do + let url = myUrl webapp addr + maybe noop (`writeFileProtected` url) urlfile + writeHtmlShim "Starting webapp..." url htmlshim + maybe noop (\a -> a url htmlshim) onstartup + +myUrl :: WebApp -> SockAddr -> Url +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 new file mode 100644 index 0000000000..a90ffb820b --- /dev/null +++ b/Assistant/Threads/XMPPClient.hs @@ -0,0 +1,370 @@ +{- git-annex XMPP client + - + - Copyright 2012, 2013 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Assistant.Threads.XMPPClient where + +import Assistant.Common +import Assistant.XMPP +import Assistant.XMPP.Client +import Assistant.NetMessager +import Assistant.Types.NetMessager +import Assistant.Types.Buddies +import Assistant.XMPP.Buddies +import Assistant.Sync +import Assistant.DaemonStatus +import qualified Remote +import Utility.ThreadScheduler +import Assistant.WebApp (UrlRenderer) +import Assistant.WebApp.Types hiding (liftAssistant) +import Assistant.Alert +import Assistant.Pairing +import Assistant.XMPP.Git +import Annex.UUID +import Logs.UUID + +import Network.Protocol.XMPP +import Control.Concurrent +import Control.Concurrent.STM.TMVar +import Control.Concurrent.STM (atomically) +import qualified Data.Text as T +import qualified Data.Set as S +import qualified Data.Map as M +import qualified Git.Branch +import Data.Time.Clock +import Control.Concurrent.Async + +xmppClientThread :: UrlRenderer -> NamedThread +xmppClientThread urlrenderer = namedThread "XMPPClient" $ + restartableClient . xmppClient urlrenderer =<< getAssistant id + +{- Runs the client, handing restart events. -} +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 -> XMPPCreds -> IO () +xmppClient urlrenderer d creds = + retry (runclient creds) =<< getCurrentTime + where + liftAssistant = runAssistant d + inAssistant = liftIO . liftAssistant + + {- When the client exits, it's restarted; + - 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 + void client + liftAssistant $ modifyDaemonStatus_ $ \s -> s + { xmppClientID = Nothing } + now <- getCurrentTime + if diffUTCTime now starttime > 300 + then do + liftAssistant $ debug ["connection lost; reconnecting"] + retry client now + else do + liftAssistant $ debug ["connection failed; will retry"] + threadDelaySeconds (Seconds 300) + retry client =<< getCurrentTime + + runclient c = liftIO $ connectXMPP c $ \jid -> do + selfjid <- bindJID jid + putStanza gitAnnexSignature + + inAssistant $ do + modifyDaemonStatus_ $ \s -> s + { xmppClientID = Just $ xmppJID creds } + debug ["connected", logJid selfjid] + + lasttraffic <- liftIO $ atomically . newTMVar =<< getCurrentTime + + sender <- xmppSession $ sendnotifications selfjid + receiver <- xmppSession $ receivenotifications selfjid lasttraffic + pinger <- xmppSession $ sendpings selfjid lasttraffic + {- Run all 3 threads concurrently, until + - any of them throw an exception. + - Then kill all 3 threads, and rethrow the + - exception. + - + - If this thread gets an exception, the 3 threads + - will also be killed. -} + liftIO $ pinger `concurrently` sender `concurrently` receiver + + sendnotifications selfjid = forever $ do + a <- inAssistant $ relayNetMessage selfjid + a + receivenotifications selfjid lasttraffic = forever $ do + l <- decodeStanza selfjid <$> getStanza + void $ liftIO $ atomically . swapTMVar lasttraffic =<< getCurrentTime + inAssistant $ debug + ["received:", show $ map logXMPPEvent l] + mapM_ (handle selfjid) l + sendpings selfjid lasttraffic = forever $ do + putStanza pingstanza + + startping <- liftIO $ getCurrentTime + liftIO $ threadDelaySeconds (Seconds 120) + t <- liftIO $ atomically $ readTMVar lasttraffic + when (t < startping) $ do + inAssistant $ debug ["ping timeout"] + error "ping timeout" + where + {- XEP-0199 says that the server will respond with either + - a ping response or an error message. Either will + - cause traffic, so good enough. -} + pingstanza = xmppPing selfjid + + 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)) = + maybe noop (inAssistant . pairMsgReceived urlrenderer stage u selfjid) (parseJID c) + handle _ (GotNetMessage m@(Pushing _ pushstage)) + | isPushNotice pushstage = inAssistant $ handlePushNotice m + | isPushInitiation pushstage = inAssistant $ queuePushInitiation m + | otherwise = inAssistant $ storeInbox m + handle _ (Ignorable _) = noop + 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 + let msg' = readdressNetMessage msg c + inAssistant $ debug + [ "sending to new client:" + , logJid jid + , show $ logNetMessage msg' + ] + a <- inAssistant $ convertNetMsg msg' selfjid + a + inAssistant $ sentImportantNetMessage msg c + resendImportantMessages _ _ = noop + +data XMPPEvent + = GotNetMessage NetMessage + | PresenceMessage Presence + | Ignorable ReceivedStanza + | Unknown ReceivedStanza + | ProtocolError ReceivedStanza + deriving Show + +logXMPPEvent :: XMPPEvent -> String +logXMPPEvent (GotNetMessage m) = logNetMessage m +logXMPPEvent (PresenceMessage p) = logPresence p +logXMPPEvent (Ignorable (ReceivedPresence p)) = "Ignorable " ++ logPresence p +logXMPPEvent (Ignorable _) = "Ignorable message" +logXMPPEvent (Unknown _) = "Unknown message" +logXMPPEvent (ProtocolError _) = "Protocol error message" + +logPresence :: Presence -> String +logPresence (p@Presence { presenceFrom = Just jid }) = unwords + [ "Presence from" + , logJid jid + , show $ extractGitAnnexTag p + ] +logPresence _ = "Presence from unknown" + +logJid :: JID -> String +logJid jid = + let name = T.unpack (buddyName jid) + resource = maybe "" (T.unpack . strResource) (jidResource jid) + in take 1 name ++ show (length name) ++ "/" ++ resource + +logClient :: Client -> String +logClient (Client jid) = logJid jid + +{- Decodes an XMPP stanza into one or more events. -} +decodeStanza :: JID -> ReceivedStanza -> [XMPPEvent] +decodeStanza selfjid s@(ReceivedPresence p) + | presenceType p == PresenceError = [ProtocolError s] + | presenceFrom p == Nothing = [Ignorable s] + | presenceFrom p == Just selfjid = [Ignorable s] + | otherwise = maybe [PresenceMessage p] decode (gitAnnexTagInfo p) + where + decode i + | tagAttr i == pushAttr = impliedp $ GotNetMessage $ NotifyPush $ + decodePushNotification (tagValue i) + | tagAttr i == queryAttr = impliedp $ GotNetMessage QueryPresence + | otherwise = [Unknown s] + {- Things sent via presence imply a presence message, + - along with their real meaning. -} + impliedp v = [PresenceMessage p, v] +decodeStanza selfjid s@(ReceivedMessage m) + | messageFrom m == Nothing = [Ignorable s] + | messageFrom m == Just selfjid = [Ignorable s] + | messageType m == MessageError = [ProtocolError s] + | otherwise = [fromMaybe (Unknown s) (GotNetMessage <$> decodeMessage m)] +decodeStanza _ s = [Unknown s] + +{- Waits for a NetMessager message to be sent, and relays it to XMPP. + - + - 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 + - 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 = do + msg <- waitNetMessage + debug ["sending:", logNetMessage msg] + a1 <- handleImportant msg + a2 <- convert msg + return (a1 >> a2) + where + handleImportant msg = case parseJID =<< isImportantNetMessage msg of + Just tojid + | tojid == baseJID tojid -> do + storeImportantNetMessage msg (formatJID tojid) $ + \c -> (baseJID <$> parseJID c) == Just tojid + return $ putStanza presenceQuery + _ -> return noop + convert (Pushing c pushstage) = withOtherClient selfjid c $ \tojid -> do + if tojid == baseJID tojid + then do + clients <- maybe [] (S.toList . buddyAssistants) + <$> getBuddy (genBuddyKey tojid) <<~ buddyList + debug ["exploded undirected message to clients", unwords $ map logClient clients] + return $ forM_ (clients) $ \(Client jid) -> + putStanza $ pushMessage pushstage jid selfjid + else do + debug ["to client:", logJid tojid] + return $ putStanza $ pushMessage pushstage tojid selfjid + convert msg = convertNetMsg msg selfjid + +{- 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 + +{- Returns an IO action that runs a XMPP action in a separate thread, + - using a session to allow it to access the same XMPP client. -} +xmppSession :: XMPP () -> XMPP (IO ()) +xmppSession a = do + s <- getSession + return $ void $ runXMPP s a + +{- We only pull from one remote out of the set listed in the push + - notification, as an optimisation. + - + - Note that it might be possible (though very unlikely) for the push + - notification to take a while to be sent, and multiple pushes happen + - before it is sent, so it includes multiple remotes that were pushed + - to at different times. + - + - It could then be the case that the remote we choose had the earlier + - push sent to it, but then failed to get the later push, and so is not + - fully up-to-date. If that happens, the pushRetryThread will come along + - and retry the push, and we'll get another notification once it succeeds, + - and pull again. -} +pull :: [UUID] -> Assistant () +pull [] = noop +pull us = do + rs <- filter matching . syncGitRemotes <$> getDaemonStatus + debug $ "push notification for" : map (fromUUID . Remote.uuid ) rs + pullone rs =<< liftAnnex (inRepo Git.Branch.current) + where + matching r = Remote.uuid r `S.member` s + s = S.fromList us + + pullone [] _ = noop + pullone (r:rs) branch = + unlessM (null . fst <$> manualPull branch [r]) $ + pullone rs branch + +{- PairReq from another client using our JID is automatically + - accepted. This is so pairing devices all using the same XMPP + - account works without confirmations. + - + - Also, autoaccept PairReq from the same JID of any repo we've + - already paired with, as long as the UUID in the PairReq is + - one we know about. +-} +pairMsgReceived :: UrlRenderer -> PairStage -> UUID -> JID -> JID -> Assistant () +pairMsgReceived urlrenderer PairReq theiruuid selfjid theirjid + | baseJID selfjid == baseJID theirjid = autoaccept + | otherwise = do + knownjids <- catMaybes . map (parseJID . getXMPPClientID) + . filter isXMPPRemote . syncRemotes <$> getDaemonStatus + um <- liftAnnex uuidMap + if any (== baseJID theirjid) knownjids && M.member theiruuid um + then autoaccept + else showalert + + where + autoaccept = do + selfuuid <- liftAnnex getUUID + sendNetMessage $ + PairingNotification PairAck (formatJID theirjid) selfuuid + finishXMPPPairing theirjid theiruuid + -- Show an alert to let the user decide if they want to pair. + showalert = do + button <- mkAlertButton (T.pack "Respond") urlrenderer $ + ConfirmXMPPPairFriendR $ + PairKey theiruuid $ formatJID theirjid + void $ addAlert $ pairRequestReceivedAlert + (T.unpack $ buddyName theirjid) + button + +{- PairAck must come from one of the buddies we are pairing with; + - don't pair with just anyone. -} +pairMsgReceived _ PairAck theiruuid _selfjid theirjid = + whenM (isBuddyPairing theirjid) $ do + changeBuddyPairing theirjid False + selfuuid <- liftAnnex getUUID + sendNetMessage $ + PairingNotification PairDone (formatJID theirjid) selfuuid + finishXMPPPairing theirjid theiruuid + +pairMsgReceived _ PairDone _theiruuid _selfjid theirjid = + changeBuddyPairing theirjid False + +isBuddyPairing :: JID -> Assistant Bool +isBuddyPairing jid = maybe False buddyPairing <$> + getBuddy (genBuddyKey jid) <<~ buddyList + +changeBuddyPairing :: JID -> Bool -> Assistant () +changeBuddyPairing jid ispairing = + updateBuddyList (M.adjust set key) <<~ buddyList + where + key = genBuddyKey jid + set b = b { buddyPairing = ispairing } diff --git a/Assistant/Threads/XMPPPusher.hs b/Assistant/Threads/XMPPPusher.hs new file mode 100644 index 0000000000..30c91c7f09 --- /dev/null +++ b/Assistant/Threads/XMPPPusher.hs @@ -0,0 +1,81 @@ +{- git-annex XMPP pusher threads + - + - This is a pair of threads. One handles git send-pack, + - and the other git receive-pack. Each thread can be running at most + - one such operation at a time. + - + - Why not use a single thread? Consider two clients A and B. + - If both decide to run a receive-pack at the same time to the other, + - they would deadlock with only one thread. For larger numbers of + - clients, the two threads are also sufficient. + - + - Copyright 2013 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Assistant.Threads.XMPPPusher where + +import Assistant.Common +import Assistant.NetMessager +import Assistant.Types.NetMessager +import Assistant.WebApp (UrlRenderer) +import Assistant.WebApp.Configurators.XMPP (checkCloudRepos) +import Assistant.XMPP.Git + +import Control.Exception as E + +xmppSendPackThread :: UrlRenderer -> NamedThread +xmppSendPackThread = pusherThread "XMPPSendPack" SendPack + +xmppReceivePackThread :: UrlRenderer -> NamedThread +xmppReceivePackThread = pusherThread "XMPPReceivePack" ReceivePack + +pusherThread :: String -> PushSide -> UrlRenderer -> NamedThread +pusherThread threadname side urlrenderer = namedThread threadname $ go Nothing + where + go lastpushedto = do + msg <- waitPushInitiation side $ selectNextPush lastpushedto + debug ["started running push", logNetMessage msg] + + runpush <- asIO $ runPush checker msg + r <- liftIO (E.try runpush :: IO (Either SomeException (Maybe ClientID))) + let successful = case r of + Right (Just _) -> True + _ -> False + + {- Empty the inbox, because stuff may have + - been left in it if the push failed. -} + let justpushedto = getclient msg + maybe noop (`emptyInbox` side) justpushedto + + debug ["finished running push", logNetMessage msg, show successful] + go $ if successful then justpushedto else lastpushedto + + checker = checkCloudRepos urlrenderer + + getclient (Pushing cid _) = Just cid + getclient _ = Nothing + +{- Select the next push to run from the queue. + - The queue cannot be empty! + - + - We prefer to select the most recently added push, because its requestor + - is more likely to still be connected. + - + - When passed the ID of a client we just pushed to, we prefer to not + - immediately push again to that same client. This avoids one client + - drowing out others. So pushes from the client we just pushed to are + - relocated to the beginning of the list, to be processed later. + -} +selectNextPush :: Maybe ClientID -> [NetMessage] -> (NetMessage, [NetMessage]) +selectNextPush _ (m:[]) = (m, []) -- common case +selectNextPush _ [] = error "selectNextPush: empty list" +selectNextPush lastpushedto l = go [] l + where + go (r:ejected) [] = (r, ejected) + go rejected (m:ms) = case m of + (Pushing clientid _) + | Just clientid /= lastpushedto -> (m, rejected ++ ms) + _ -> go (m:rejected) ms + go [] [] = undefined diff --git a/Assistant/TransferQueue.hs b/Assistant/TransferQueue.hs new file mode 100644 index 0000000000..f94e73c2b2 --- /dev/null +++ b/Assistant/TransferQueue.hs @@ -0,0 +1,223 @@ +{- git-annex assistant pending transfer queue + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Assistant.TransferQueue ( + TransferQueue, + Schedule(..), + newTransferQueue, + getTransferQueue, + queueTransfers, + queueTransfersMatching, + queueDeferredDownloads, + queueTransfer, + queueTransferAt, + queueTransferWhenSmall, + getNextTransfer, + getMatchingTransfers, + dequeueTransfers, +) where + +import Assistant.Common +import Assistant.DaemonStatus +import Assistant.Types.TransferQueue +import Logs.Transfer +import Types.Remote +import qualified Remote +import qualified Types.Remote as Remote +import Annex.Wanted +import Utility.TList + +import Control.Concurrent.STM +import qualified Data.Map as M +import qualified Data.Set as S + +type Reason = String + +{- Reads the queue's content without blocking or changing it. -} +getTransferQueue :: Assistant [(Transfer, TransferInfo)] +getTransferQueue = (atomically . readTList . queuelist) <<~ transferQueue + +stubInfo :: AssociatedFile -> Remote -> TransferInfo +stubInfo f r = stubTransferInfo + { transferRemote = Just r + , associatedFile = f + } + +{- Adds transfers to queue for some of the known remotes. + - Honors preferred content settings, only transferring wanted files. -} +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) -> 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 . selectremotes + =<< syncDataRemotes <$> getDaemonStatus + let matchingrs = filter (matching . Remote.uuid) rs + if null matchingrs + then defer + else forM_ matchingrs $ \r -> + enqueue reason schedule (gentransfer r) (stubInfo f r) + selectremotes rs + {- Queue downloads from all remotes that + - have the key. The list of remotes is ordered with + - cheapest first. More expensive ones will only be tried + - if downloading from a cheap one fails. -} + | direction == Download = do + s <- locs + return $ filter (inset s) rs + {- Upload to all remotes that want the content and don't + - already have it. -} + | otherwise = do + s <- locs + filterM (wantSend True f . Remote.uuid) $ + filter (\r -> not (inset s r || Remote.readonly r)) rs + where + locs = S.fromList <$> Remote.keyLocations k + inset s r = S.member (Remote.uuid r) s + gentransfer r = Transfer + { transferDirection = direction + , transferKey = k + , transferUUID = Remote.uuid r + } + defer + {- Defer this download, as no known remote has the key. -} + | direction == Download = do + q <- getAssistant transferQueue + void $ liftIO $ atomically $ + consTList (deferreddownloads q) (k, f) + | otherwise = noop + +{- Queues any deferred downloads that can now be accomplished, leaving + - any others in the list to try again later. -} +queueDeferredDownloads :: Reason -> Schedule -> Assistant () +queueDeferredDownloads reason schedule = do + q <- getAssistant transferQueue + l <- liftIO $ atomically $ readTList (deferreddownloads q) + rs <- syncDataRemotes <$> getDaemonStatus + left <- filterM (queue rs) l + unless (null left) $ + liftIO $ atomically $ appendTList (deferreddownloads q) left + where + queue rs (k, f) = do + uuids <- liftAnnex $ Remote.keyLocations k + let sources = filter (\r -> uuid r `elem` uuids) rs + unless (null sources) $ + forM_ sources $ \r -> + enqueue reason schedule + (gentransfer r) (stubInfo f r) + return $ null sources + where + gentransfer r = Transfer + { transferDirection = Download + , transferKey = k + , transferUUID = Remote.uuid r + } + +enqueue :: Reason -> Schedule -> Transfer -> TransferInfo -> Assistant () +enqueue reason schedule t info + | schedule == Next = go consTList + | otherwise = go snocTList + where + go modlist = whenM (add modlist) $ do + debug [ "queued", describeTransfer t info, ": " ++ reason ] + notifyTransfer + add modlist = do + q <- getAssistant transferQueue + dstatus <- getAssistant daemonStatusHandle + liftIO $ atomically $ ifM (checkRunningTransferSTM dstatus t) + ( return False + , do + l <- readTList (queuelist q) + if (t `notElem` map fst l) + then do + void $ modifyTVar' (queuesize q) succ + void $ modlist (queuelist q) (t, info) + return True + else return False + ) + +{- Adds a transfer to the queue. -} +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 -> 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 reason schedule t (stubInfo f remote) + +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 + - 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. -} +getNextTransfer :: (TransferInfo -> Bool) -> Assistant (Maybe (Transfer, TransferInfo)) +getNextTransfer acceptable = do + q <- getAssistant transferQueue + dstatus <- getAssistant daemonStatusHandle + liftIO $ atomically $ do + sz <- readTVar (queuesize q) + if sz < 1 + then retry -- blocks until queuesize changes + else do + (r@(t,info):rest) <- readTList (queuelist q) + void $ modifyTVar' (queuesize q) pred + setTList (queuelist q) rest + if acceptable info + then do + adjustTransfersSTM dstatus $ + M.insertWith' const t info + return $ Just r + else return Nothing + +{- Moves transfers matching a condition from the queue, to the + - currentTransfers map. -} +getMatchingTransfers :: (Transfer -> Bool) -> Assistant [(Transfer, TransferInfo)] +getMatchingTransfers c = do + q <- getAssistant transferQueue + dstatus <- getAssistant daemonStatusHandle + liftIO $ atomically $ do + ts <- dequeueTransfersSTM q c + unless (null ts) $ + adjustTransfersSTM dstatus $ \m -> M.union m $ M.fromList ts + return ts + +{- Removes transfers matching a condition from the queue, and returns the + - removed transfers. -} +dequeueTransfers :: (Transfer -> Bool) -> Assistant [(Transfer, TransferInfo)] +dequeueTransfers c = do + q <- getAssistant transferQueue + removed <- liftIO $ atomically $ dequeueTransfersSTM q c + unless (null removed) $ + notifyTransfer + return removed + +dequeueTransfersSTM :: TransferQueue -> (Transfer -> Bool) -> STM [(Transfer, TransferInfo)] +dequeueTransfersSTM q c = do + (removed, ts) <- partition (c . fst) <$> readTList (queuelist q) + void $ writeTVar (queuesize q) (length ts) + setTList (queuelist q) ts + return removed diff --git a/Assistant/TransferSlots.hs b/Assistant/TransferSlots.hs new file mode 100644 index 0000000000..81a778a0ac --- /dev/null +++ b/Assistant/TransferSlots.hs @@ -0,0 +1,78 @@ +{- git-annex assistant transfer slots + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Assistant.TransferSlots where + +import Assistant.Common +import Utility.ThreadScheduler +import Assistant.Types.TransferSlots +import Assistant.DaemonStatus +import Assistant.TransferrerPool +import Assistant.Types.TransferrerPool +import Logs.Transfer + +import qualified Control.Exception as E +import Control.Concurrent +import qualified Control.Concurrent.MSemN as MSemN + +type TransferGenerator = Assistant (Maybe (Transfer, TransferInfo, Transferrer -> Assistant ())) + +{- Waits until a transfer slot becomes available, then runs a + - TransferGenerator, and then runs the transfer action in its own thread. + -} +inTransferSlot :: FilePath -> TransferGenerator -> Assistant () +inTransferSlot program gen = do + flip MSemN.wait 1 <<~ transferSlots + runTransferThread program =<< gen + +{- Runs a TransferGenerator, and its transfer action, + - without waiting for a slot to become available. -} +inImmediateTransferSlot :: FilePath -> TransferGenerator -> Assistant () +inImmediateTransferSlot program gen = do + flip MSemN.signal (-1) <<~ transferSlots + runTransferThread program =<< gen + +{- Runs a transfer action, in an already allocated transfer slot. + - Once it finishes, frees the transfer slot. + - + - Note that the action is subject to being killed when the transfer + - is canceled or paused. + - + - A PauseTransfer exception is handled by letting the action be killed, + - then pausing the thread until a ResumeTransfer exception is raised, + - then rerunning the action. + -} +runTransferThread :: FilePath -> Maybe (Transfer, TransferInfo, Transferrer -> Assistant ()) -> Assistant () +runTransferThread _ Nothing = flip MSemN.signal 1 <<~ transferSlots +runTransferThread program (Just (t, info, a)) = do + d <- getAssistant id + aio <- asIO1 a + tid <- liftIO $ forkIO $ runTransferThread' program d aio + updateTransferInfo t $ info { transferTid = Just tid } + +runTransferThread' :: FilePath -> AssistantData -> (Transferrer -> IO ()) -> IO () +runTransferThread' program d run = go + where + go = catchPauseResume $ + withTransferrer program (transferrerPool d) + run + pause = catchPauseResume $ + runEvery (Seconds 86400) noop + {- Note: This must use E.try, rather than E.catch. + - When E.catch is used, and has called go in its exception + - handler, Control.Concurrent.throwTo will block sometimes + - when signaling. Using E.try avoids the problem. -} + catchPauseResume a' = do + r <- E.try a' :: IO (Either E.SomeException ()) + case r of + Left e -> case E.fromException e of + Just PauseTransfer -> pause + Just ResumeTransfer -> go + _ -> done + _ -> done + done = runAssistant d $ + flip MSemN.signal 1 <<~ transferSlots diff --git a/Assistant/TransferrerPool.hs b/Assistant/TransferrerPool.hs new file mode 100644 index 0000000000..d9104f74dd --- /dev/null +++ b/Assistant/TransferrerPool.hs @@ -0,0 +1,82 @@ +{- A pool of "git-annex transferkeys" processes + - + - Copyright 2013 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Assistant.TransferrerPool where + +import Assistant.Common +import Assistant.Types.TransferrerPool +import Logs.Transfer +import qualified Command.TransferKeys as T + +import Control.Concurrent.STM +import System.Process (create_group) +import Control.Exception (throw) +import Control.Concurrent + +{- Runs an action with a Transferrer from the pool. -} +withTransferrer :: FilePath -> TransferrerPool -> (Transferrer -> IO a) -> IO a +withTransferrer program pool a = do + t <- maybe (mkTransferrer program) (checkTransferrer program) + =<< atomically (tryReadTChan pool) + v <- tryNonAsync $ a t + unlessM (putback t) $ + void $ forkIO $ stopTransferrer t + either throw return v + where + putback t = atomically $ ifM (isEmptyTChan pool) + ( do + writeTChan pool t + return True + , return False + ) + +{- Requests that a Transferrer perform a Transfer, and waits for it to + - finish. -} +performTransfer :: Transferrer -> Transfer -> AssociatedFile -> IO Bool +performTransfer transferrer t f = catchBoolIO $ do + T.sendRequest t f (transferrerWrite transferrer) + T.readResponse (transferrerRead transferrer) + +{- Starts a new git-annex transferkeys process, setting up a pipe + - that will be used to communicate with it. -} +mkTransferrer :: FilePath -> IO Transferrer +mkTransferrer program = do + (myread, twrite) <- createPipe + (tread, mywrite) <- createPipe + mapM_ (\fd -> setFdOption fd CloseOnExec True) [myread, mywrite] + let params = + [ Param "transferkeys" + , Param "--readfd", Param $ show tread + , Param "--writefd", Param $ show twrite + ] + {- It's put into its own group so that the whole group can be + - killed to stop a transfer. -} + (_, _, _, pid) <- createProcess (proc program $ toCommand params) + { create_group = True } + closeFd twrite + closeFd tread + myreadh <- fdToHandle myread + mywriteh <- fdToHandle mywrite + fileEncoding myreadh + fileEncoding mywriteh + return $ Transferrer + { transferrerRead = myreadh + , transferrerWrite = mywriteh + , transferrerHandle = pid + } + +{- Checks if a Transferrer is still running. If not, makes a new one. -} +checkTransferrer :: FilePath -> Transferrer -> IO Transferrer +checkTransferrer program t = maybe (return t) (const $ mkTransferrer program) + =<< getProcessExitCode (transferrerHandle t) + +{- Closing the fds will stop the transferrer. -} +stopTransferrer :: Transferrer -> IO () +stopTransferrer t = do + hClose $ transferrerRead t + hClose $ transferrerWrite t + void $ waitForProcess $ transferrerHandle t diff --git a/Assistant/Types/Alert.hs b/Assistant/Types/Alert.hs new file mode 100644 index 0000000000..290733b669 --- /dev/null +++ b/Assistant/Types/Alert.hs @@ -0,0 +1,75 @@ +{- git-annex assistant alert types + - + - Copyright 2013 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Assistant.Types.Alert where + +import Utility.Tense + +import Data.Text (Text) +import qualified Data.Map as M + +{- Different classes of alerts are displayed differently. -} +data AlertClass = Success | Message | Activity | Warning | Error + deriving (Eq, Ord) + +data AlertPriority = Filler | Low | Medium | High | Pinned + deriving (Eq, Ord) + +{- An alert can have an name, which is used to combine it with other similar + - alerts. -} +data AlertName + = FileAlert TenseChunk + | SanityCheckFixAlert + | WarningAlert String + | PairAlert String + | XMPPNeededAlert + | RemoteRemovalAlert String + | CloudRepoNeededAlert + | SyncAlert + deriving (Eq) + +{- The first alert is the new alert, the second is an old alert. + - Should return a modified version of the old alert. -} +type AlertCombiner = Alert -> Alert -> Maybe Alert + +data Alert = Alert + { alertClass :: AlertClass + , alertHeader :: Maybe TenseText + , alertMessageRender :: Alert -> TenseText + , alertData :: [TenseChunk] + , alertCounter :: Int + , alertBlockDisplay :: Bool + , alertClosable :: Bool + , alertPriority :: AlertPriority + , alertIcon :: Maybe AlertIcon + , alertCombiner :: Maybe AlertCombiner + , alertName :: Maybe AlertName + , alertButton :: Maybe AlertButton + } + +data AlertIcon = ActivityIcon | SyncIcon | SuccessIcon | ErrorIcon | InfoIcon | TheCloud + +type AlertMap = M.Map AlertId Alert + +{- Higher AlertId indicates a more recent alert. -} +newtype AlertId = AlertId Integer + deriving (Read, Show, Eq, Ord) + +firstAlertId :: AlertId +firstAlertId = AlertId 0 + +nextAlertId :: AlertId -> AlertId +nextAlertId (AlertId i) = AlertId $ succ i + +{- When clicked, a button always redirects to a URL + - It may also run an IO action in the background, which is useful + - to make the button close or otherwise change the alert. -} +data AlertButton = AlertButton + { buttonLabel :: Text + , buttonUrl :: Text + , buttonAction :: Maybe (AlertId -> IO ()) + } diff --git a/Assistant/Types/BranchChange.hs b/Assistant/Types/BranchChange.hs new file mode 100644 index 0000000000..399abee54d --- /dev/null +++ b/Assistant/Types/BranchChange.hs @@ -0,0 +1,19 @@ +{- git-annex assistant git-annex branch change tracking + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Assistant.Types.BranchChange where + +import Control.Concurrent.MSampleVar +import Common.Annex + +newtype BranchChangeHandle = BranchChangeHandle (MSampleVar ()) + +newBranchChangeHandle :: IO BranchChangeHandle +newBranchChangeHandle = BranchChangeHandle <$> newEmptySV + +fromBranchChangeHandle :: BranchChangeHandle -> MSampleVar () +fromBranchChangeHandle (BranchChangeHandle v) = v diff --git a/Assistant/Types/Buddies.hs b/Assistant/Types/Buddies.hs new file mode 100644 index 0000000000..36d8a4fedc --- /dev/null +++ b/Assistant/Types/Buddies.hs @@ -0,0 +1,80 @@ +{- git-annex assistant buddies + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE CPP #-} + +module Assistant.Types.Buddies where + +import Common.Annex + +import qualified Data.Map as M +import Control.Concurrent.STM +import Utility.NotificationBroadcaster +import Data.Text as T + +{- For simplicity, dummy types are defined even when XMPP is disabled. -} +#ifdef WITH_XMPP +import Network.Protocol.XMPP +import Data.Set as S +import Data.Ord + +newtype Client = Client JID + deriving (Eq, Show) + +instance Ord Client where + compare = comparing show + +data Buddy = Buddy + { buddyPresent :: S.Set Client + , buddyAway :: S.Set Client + , buddyAssistants :: S.Set Client + , buddyPairing :: Bool + } +#else +data Buddy = Buddy +#endif + deriving (Eq, Show) + +data BuddyKey = BuddyKey T.Text + deriving (Eq, Ord, Show, Read) + +data PairKey = PairKey UUID T.Text + deriving (Eq, Ord, Show, Read) + +type Buddies = M.Map BuddyKey Buddy + +{- A list of buddies, and a way to notify when it changes. -} +type BuddyList = (TMVar Buddies, NotificationBroadcaster) + +noBuddies :: Buddies +noBuddies = M.empty + +newBuddyList :: IO BuddyList +newBuddyList = (,) + <$> atomically (newTMVar noBuddies) + <*> newNotificationBroadcaster + +getBuddyList :: BuddyList -> IO [Buddy] +getBuddyList (v, _) = M.elems <$> atomically (readTMVar v) + +getBuddy :: BuddyKey -> BuddyList -> IO (Maybe Buddy) +getBuddy k (v, _) = M.lookup k <$> atomically (readTMVar v) + +getBuddyBroadcaster :: BuddyList -> NotificationBroadcaster +getBuddyBroadcaster (_, h) = h + +{- Applies a function to modify the buddy list, and if it's changed, + - sends notifications to any listeners. -} +updateBuddyList :: (Buddies -> Buddies) -> BuddyList -> IO () +updateBuddyList a (v, caster) = do + changed <- atomically $ do + buds <- takeTMVar v + let buds' = a buds + putTMVar v buds' + return $ buds /= buds' + when changed $ + sendNotification caster diff --git a/Assistant/Types/Changes.hs b/Assistant/Types/Changes.hs new file mode 100644 index 0000000000..e8ecc6e48b --- /dev/null +++ b/Assistant/Types/Changes.hs @@ -0,0 +1,77 @@ +{- git-annex assistant change tracking + - + - Copyright 2012-2013 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Assistant.Types.Changes where + +import Types.KeySource +import Types.Key +import Utility.TList + +import Control.Concurrent.STM +import Data.Time.Clock + +{- An un-ordered pool of Changes that have been noticed and should be + - staged and committed. Changes will typically be in order, but ordering + - may be lost. In any case, order should not matter, as any given Change + - may later be reverted by a later Change (ie, a file is added and then + - deleted). Code that processes the changes needs to deal with such + - scenarios. + -} +type ChangePool = TList Change + +newChangePool :: IO ChangePool +newChangePool = atomically newTList + +data Change + = Change + { changeTime :: UTCTime + , _changeFile :: FilePath + , changeInfo :: ChangeInfo + } + | PendingAddChange + { changeTime ::UTCTime + , _changeFile :: FilePath + } + | InProcessAddChange + { changeTime ::UTCTime + , keySource :: KeySource + } + deriving (Show) + +data ChangeInfo = AddKeyChange Key | AddFileChange | LinkChange (Maybe Key) | RmChange + deriving (Show, Eq, Ord) + +changeInfoKey :: ChangeInfo -> Maybe Key +changeInfoKey (AddKeyChange k) = Just k +changeInfoKey (LinkChange (Just k)) = Just k +changeInfoKey _ = Nothing + +changeFile :: Change -> FilePath +changeFile (Change _ f _) = f +changeFile (PendingAddChange _ f) = f +changeFile (InProcessAddChange _ ks) = keyFilename ks + +isPendingAddChange :: Change -> Bool +isPendingAddChange (PendingAddChange {}) = True +isPendingAddChange _ = False + +isInProcessAddChange :: Change -> Bool +isInProcessAddChange (InProcessAddChange {}) = True +isInProcessAddChange _ = False + +retryChange :: Change -> Change +retryChange (InProcessAddChange time ks) = + PendingAddChange time (keyFilename ks) +retryChange c = c + +finishedChange :: Change -> Key -> Change +finishedChange c@(InProcessAddChange { keySource = ks }) k = Change + { changeTime = changeTime c + , _changeFile = keyFilename ks + , changeInfo = AddKeyChange k + } +finishedChange c _ = c diff --git a/Assistant/Types/Commits.hs b/Assistant/Types/Commits.hs new file mode 100644 index 0000000000..500faa9011 --- /dev/null +++ b/Assistant/Types/Commits.hs @@ -0,0 +1,19 @@ +{- git-annex assistant commit tracking + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Assistant.Types.Commits where + +import Utility.TList + +import Control.Concurrent.STM + +type CommitChan = TList Commit + +data Commit = Commit + +newCommitChan :: IO CommitChan +newCommitChan = atomically newTList diff --git a/Assistant/Types/DaemonStatus.hs b/Assistant/Types/DaemonStatus.hs new file mode 100644 index 0000000000..17e535b6db --- /dev/null +++ b/Assistant/Types/DaemonStatus.hs @@ -0,0 +1,96 @@ +{- git-annex assistant daemon status + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE RankNTypes, ImpredicativeTypes #-} + +module Assistant.Types.DaemonStatus where + +import Common.Annex +import Assistant.Pairing +import Utility.NotificationBroadcaster +import Logs.Transfer +import Assistant.Types.ThreadName +import Assistant.Types.NetMessager +import Assistant.Types.Alert + +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, + -- and actions to run to restart them. + { startedThreads :: M.Map ThreadName (Async (), IO ()) + -- False when the daemon is performing its startup scan + , scanComplete :: Bool + -- Time when a previous process of the daemon was running ok + , lastRunning :: Maybe POSIXTime + -- True when the sanity checker is running + , sanityCheckRunning :: Bool + -- Last time the sanity checker ran + , lastSanityCheck :: Maybe POSIXTime + -- True when a scan for file transfers is running + , transferScanRunning :: Bool + -- Currently running file content transfers + , currentTransfers :: TransferMap + -- Messages to display to the user. + , alertMap :: AlertMap + , lastAlertId :: AlertId + -- Ordered list of all remotes that can be synced with + , syncRemotes :: [Remote] + -- Ordered list of remotes to sync git with + , syncGitRemotes :: [Remote] + -- Ordered list of remotes to sync data with + , syncDataRemotes :: [Remote] + -- Are we syncing to any cloud remotes? + , syncingToCloudRemote :: Bool + -- 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 + , changeNotifier :: NotificationBroadcaster + -- Broadcasts notifications when queued or current transfers change. + , transferNotifier :: NotificationBroadcaster + -- Broadcasts notifications when there's a change to the alerts + , 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 + +{- This TMVar is never left empty, so accessing it will never block. -} +type DaemonStatusHandle = TMVar DaemonStatus + +newDaemonStatus :: IO DaemonStatus +newDaemonStatus = DaemonStatus + <$> pure M.empty + <*> pure False + <*> pure Nothing + <*> pure False + <*> pure Nothing + <*> pure False + <*> pure M.empty + <*> pure M.empty + <*> pure firstAlertId + <*> pure [] + <*> pure [] + <*> pure [] + <*> pure False + <*> pure S.empty + <*> pure Nothing + <*> newNotificationBroadcaster + <*> newNotificationBroadcaster + <*> newNotificationBroadcaster + <*> newNotificationBroadcaster + <*> pure Nothing diff --git a/Assistant/Types/NamedThread.hs b/Assistant/Types/NamedThread.hs new file mode 100644 index 0000000000..a65edc20d7 --- /dev/null +++ b/Assistant/Types/NamedThread.hs @@ -0,0 +1,17 @@ +{- named threads + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Assistant.Types.NamedThread where + +import Assistant.Monad +import Assistant.Types.ThreadName + +{- Information about a named thread that can be run. -} +data NamedThread = NamedThread ThreadName (Assistant ()) + +namedThread :: String -> Assistant () -> NamedThread +namedThread = NamedThread . ThreadName diff --git a/Assistant/Types/NetMessager.hs b/Assistant/Types/NetMessager.hs new file mode 100644 index 0000000000..0af262e9a2 --- /dev/null +++ b/Assistant/Types/NetMessager.hs @@ -0,0 +1,155 @@ +{- git-annex assistant out of band network messager types + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Assistant.Types.NetMessager where + +import Common.Annex +import Assistant.Pairing +import Git.Types + +import qualified Data.Text as T +import qualified Data.Set as S +import qualified Data.Map as M +import qualified Data.DList as D +import Control.Concurrent.STM +import Control.Concurrent.MSampleVar +import Data.ByteString (ByteString) +import qualified Data.ByteString.Char8 as B8 +import Data.Text (Text) + +{- Messages that can be sent out of band by a network messager. -} +data NetMessage + -- indicate that pushes have been made to the repos with these uuids + = NotifyPush [UUID] + -- requests other clients to inform us of their presence + | QueryPresence + -- notification about a stage in the pairing process, + -- involving a client, and a UUID. + | PairingNotification PairStage ClientID UUID + -- used for git push over the network messager + | Pushing ClientID PushStage + deriving (Show, Eq, Ord) + +{- Something used to identify the client, or clients to send the message to. -} +type ClientID = Text + +data PushStage + -- indicates that we have data to push over the out of band network + = CanPush UUID [Sha] + -- request that a git push be sent over the out of band network + | PushRequest UUID + -- indicates that a push is starting + | StartingPush UUID + -- a chunk of output of git receive-pack + | ReceivePackOutput SequenceNum ByteString + -- a chuck of output of git send-pack + | SendPackOutput SequenceNum ByteString + -- sent when git receive-pack exits, with its exit code + | ReceivePackDone ExitCode + deriving (Show, Eq, Ord) + +{- A sequence number. Incremented by one per packet in a sequence, + - starting with 1 for the first packet. 0 means sequence numbers are + - not being used. -} +type SequenceNum = Int + +{- 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 + +{- Checks if two important NetMessages are equivilant. + - That is to say, assuming they were sent to the same client, + - would it do the same thing for one as for the other? -} +equivilantImportantNetMessages :: NetMessage -> NetMessage -> Bool +equivilantImportantNetMessages (Pushing _ (CanPush _ _)) (Pushing _ (CanPush _ _)) = True +equivilantImportantNetMessages (Pushing _ (PushRequest _)) (Pushing _ (PushRequest _)) = True +equivilantImportantNetMessages _ _ = False + +readdressNetMessage :: NetMessage -> ClientID -> NetMessage +readdressNetMessage (PairingNotification stage _ uuid) c = PairingNotification stage c uuid +readdressNetMessage (Pushing _ stage) c = Pushing c stage +readdressNetMessage m _ = m + +{- Convert a NetMessage to something that can be logged. -} +logNetMessage :: NetMessage -> String +logNetMessage (Pushing c stage) = show $ Pushing (logClientID c) $ + case stage of + ReceivePackOutput n _ -> ReceivePackOutput n elided + SendPackOutput n _ -> SendPackOutput n elided + s -> s + where + elided = B8.pack "" +logNetMessage (PairingNotification stage c uuid) = + show $ PairingNotification stage (logClientID c) uuid +logNetMessage m = show m + +logClientID :: ClientID -> ClientID +logClientID c = T.concat [T.take 1 c, T.pack $ show $ T.length c] + +{- Things that initiate either side of a push, but do not actually send data. -} +isPushInitiation :: PushStage -> Bool +isPushInitiation (PushRequest _) = True +isPushInitiation (StartingPush _) = True +isPushInitiation _ = False + +isPushNotice :: PushStage -> Bool +isPushNotice (CanPush _ _) = True +isPushNotice _ = False + +data PushSide = SendPack | ReceivePack + deriving (Eq, Ord, Show) + +pushDestinationSide :: PushStage -> PushSide +pushDestinationSide (CanPush _ _) = ReceivePack +pushDestinationSide (PushRequest _) = SendPack +pushDestinationSide (StartingPush _) = ReceivePack +pushDestinationSide (ReceivePackOutput _ _) = SendPack +pushDestinationSide (SendPackOutput _ _) = ReceivePack +pushDestinationSide (ReceivePackDone _) = SendPack + +type SideMap a = PushSide -> a + +mkSideMap :: STM a -> IO (SideMap a) +mkSideMap gen = do + (sp, rp) <- atomically $ (,) <$> gen <*> gen + return $ lookupside sp rp + where + lookupside sp _ SendPack = sp + lookupside _ rp ReceivePack = rp + +getSide :: PushSide -> SideMap a -> a +getSide side m = m side + +type Inboxes = TVar (M.Map ClientID (Int, D.DList NetMessage)) + +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 () + -- queue of incoming messages that request the initiation of pushes + , netMessagerPushInitiations :: SideMap (TMVar [NetMessage]) + -- incoming messages containing data for a running + -- (or not yet started) push + , netMessagerInboxes :: SideMap Inboxes + } + +newNetMessager :: IO NetMessager +newNetMessager = NetMessager + <$> atomically newTChan + <*> atomically (newTMVar M.empty) + <*> atomically (newTMVar M.empty) + <*> newEmptySV + <*> mkSideMap newEmptyTMVar + <*> mkSideMap (newTVar M.empty) diff --git a/Assistant/Types/Pushes.hs b/Assistant/Types/Pushes.hs new file mode 100644 index 0000000000..99e0ee1628 --- /dev/null +++ b/Assistant/Types/Pushes.hs @@ -0,0 +1,24 @@ +{- git-annex assistant push tracking + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Assistant.Types.Pushes where + +import Common.Annex + +import Control.Concurrent.STM +import Data.Time.Clock +import qualified Data.Map as M + +{- Track the most recent push failure for each remote. -} +type PushMap = M.Map Remote UTCTime +type FailedPushMap = TMVar PushMap + +{- The TMVar starts empty, and is left empty when there are no + - failed pushes. This way we can block until there are some failed pushes. + -} +newFailedPushMap :: IO FailedPushMap +newFailedPushMap = atomically newEmptyTMVar diff --git a/Assistant/Types/ScanRemotes.hs b/Assistant/Types/ScanRemotes.hs new file mode 100644 index 0000000000..8219f9baf1 --- /dev/null +++ b/Assistant/Types/ScanRemotes.hs @@ -0,0 +1,25 @@ +{- git-annex assistant remotes needing scanning + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Assistant.Types.ScanRemotes where + +import Common.Annex + +import Control.Concurrent.STM +import qualified Data.Map as M + +data ScanInfo = ScanInfo + { scanPriority :: Float + , fullScan :: Bool + } + +type ScanRemoteMap = TMVar (M.Map Remote ScanInfo) + +{- The TMVar starts empty, and is left empty when there are no remotes + - to scan. -} +newScanRemoteMap :: IO ScanRemoteMap +newScanRemoteMap = atomically newEmptyTMVar diff --git a/Assistant/Types/ThreadName.hs b/Assistant/Types/ThreadName.hs new file mode 100644 index 0000000000..c8d264a381 --- /dev/null +++ b/Assistant/Types/ThreadName.hs @@ -0,0 +1,14 @@ +{- name of a thread + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Assistant.Types.ThreadName where + +newtype ThreadName = ThreadName String + deriving (Eq, Read, Show, Ord) + +fromThreadName :: ThreadName -> String +fromThreadName (ThreadName n) = n diff --git a/Assistant/Types/ThreadedMonad.hs b/Assistant/Types/ThreadedMonad.hs new file mode 100644 index 0000000000..1a2aa7eb7f --- /dev/null +++ b/Assistant/Types/ThreadedMonad.hs @@ -0,0 +1,38 @@ +{- making the Annex monad available across threads + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Assistant.Types.ThreadedMonad where + +import Common.Annex +import qualified Annex + +import Control.Concurrent +import Data.Tuple + +{- The Annex state is stored in a MVar, so that threaded actions can access + - it. -} +type ThreadState = MVar Annex.AnnexState + +{- Stores the Annex state in a MVar. + - + - Once the action is finished, retrieves the state from the MVar. + -} +withThreadState :: (ThreadState -> Annex a) -> Annex a +withThreadState a = do + state <- Annex.getState id + mvar <- liftIO $ newMVar state + r <- a mvar + newstate <- liftIO $ takeMVar mvar + Annex.changeState (const newstate) + return r + +{- Runs an Annex action, using the state from the MVar. + - + - This serializes calls by threads; only one thread can run in Annex at a + - time. -} +runThreadState :: ThreadState -> Annex a -> IO a +runThreadState mvar a = modifyMVar mvar $ \state -> swap <$> Annex.run state a diff --git a/Assistant/Types/TransferQueue.hs b/Assistant/Types/TransferQueue.hs new file mode 100644 index 0000000000..e4bf2ae922 --- /dev/null +++ b/Assistant/Types/TransferQueue.hs @@ -0,0 +1,29 @@ +{- git-annex assistant pending transfer queue + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Assistant.Types.TransferQueue where + +import Common.Annex +import Logs.Transfer + +import Control.Concurrent.STM +import Utility.TList + +data TransferQueue = TransferQueue + { queuesize :: TVar Int + , queuelist :: TList (Transfer, TransferInfo) + , deferreddownloads :: TList (Key, AssociatedFile) + } + +data Schedule = Next | Later + deriving (Eq) + +newTransferQueue :: IO TransferQueue +newTransferQueue = atomically $ TransferQueue + <$> newTVar 0 + <*> newTList + <*> newTList diff --git a/Assistant/Types/TransferSlots.hs b/Assistant/Types/TransferSlots.hs new file mode 100644 index 0000000000..5140995a37 --- /dev/null +++ b/Assistant/Types/TransferSlots.hs @@ -0,0 +1,34 @@ +{- git-annex assistant transfer slots + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE DeriveDataTypeable #-} + +module Assistant.Types.TransferSlots where + +import qualified Control.Exception as E +import qualified Control.Concurrent.MSemN as MSemN +import Data.Typeable + +type TransferSlots = MSemN.MSemN Int + +{- A special exception that can be thrown to pause or resume a transfer, while + - keeping its slot in use. -} +data TransferException = PauseTransfer | ResumeTransfer + deriving (Show, Eq, Typeable) + +instance E.Exception TransferException + +{- Number of concurrent transfers allowed to be run from the assistant. + - + - Transfers launched by other means, including by remote assistants, + - do not currently take up slots. + -} +numSlots :: Int +numSlots = 1 + +newTransferSlots :: IO TransferSlots +newTransferSlots = MSemN.new numSlots diff --git a/Assistant/Types/TransferrerPool.hs b/Assistant/Types/TransferrerPool.hs new file mode 100644 index 0000000000..2727a69190 --- /dev/null +++ b/Assistant/Types/TransferrerPool.hs @@ -0,0 +1,23 @@ +{- A pool of "git-annex transferkeys" processes + - + - Copyright 2013 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Assistant.Types.TransferrerPool where + +import Common.Annex + +import Control.Concurrent.STM + +type TransferrerPool = TChan Transferrer + +data Transferrer = Transferrer + { transferrerRead :: Handle + , transferrerWrite :: Handle + , transferrerHandle :: ProcessHandle + } + +newTransferrerPool :: IO TransferrerPool +newTransferrerPool = newTChanIO diff --git a/Assistant/Types/UrlRenderer.hs b/Assistant/Types/UrlRenderer.hs new file mode 100644 index 0000000000..521905bf3c --- /dev/null +++ b/Assistant/Types/UrlRenderer.hs @@ -0,0 +1,26 @@ +{- webapp url renderer access from the assistant + - + - Copyright 2013 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE CPP #-} + +module Assistant.Types.UrlRenderer ( + UrlRenderer, + newUrlRenderer +) where + +#ifdef WITH_WEBAPP + +import Assistant.WebApp (UrlRenderer, newUrlRenderer) + +#else + +data UrlRenderer = UrlRenderer -- dummy type + +newUrlRenderer :: IO UrlRenderer +newUrlRenderer = return UrlRenderer + +#endif diff --git a/Assistant/WebApp.hs b/Assistant/WebApp.hs new file mode 100644 index 0000000000..ece75d7ba6 --- /dev/null +++ b/Assistant/WebApp.hs @@ -0,0 +1,73 @@ +{- git-annex assistant webapp core + - + - Copyright 2012, 2013 Joey Hess + - + - Licensed under the GNU AGPL version 3 or higher. + -} + +{-# LANGUAGE TypeFamilies, QuasiQuotes, MultiParamTypeClasses #-} +{-# LANGUAGE TemplateHaskell, OverloadedStrings, RankNTypes #-} + +module Assistant.WebApp where + +import Assistant.WebApp.Types +import Assistant.Common +import Utility.NotificationBroadcaster +import Utility.Yesod + +import Data.Text (Text) +import Control.Concurrent +import qualified Network.Wai as W +import qualified Data.ByteString.Char8 as S8 +import qualified Data.Text as T + +waitNotifier :: Assistant NotificationBroadcaster -> NotificationId -> Handler () +waitNotifier getbroadcaster nid = liftAssistant $ do + b <- getbroadcaster + liftIO $ waitNotification $ notificationHandleFromId b nid + +newNotifier :: Assistant NotificationBroadcaster -> Handler NotificationId +newNotifier getbroadcaster = liftAssistant $ do + b <- getbroadcaster + liftIO $ notificationHandleToId <$> newNotificationHandle True b + +{- Adds the auth parameter as a hidden field on a form. Must be put into + - every form. -} +webAppFormAuthToken :: Widget +webAppFormAuthToken = do + webapp <- liftH getYesod + [whamlet||] + +{- A button with an icon, and maybe label or tooltip, that can be + - clicked to perform some action. + - With javascript, clicking it POSTs the Route, and remains on the same + - page. + - With noscript, clicking it GETs the Route. -} +actionButton :: Route WebApp -> (Maybe String) -> (Maybe String) -> String -> String -> Widget +actionButton route label tooltip buttonclass iconclass = $(widgetFile "actionbutton") + +type UrlRenderFunc = Route WebApp -> [(Text, Text)] -> Text +type UrlRenderer = MVar (UrlRenderFunc) + +newUrlRenderer :: IO UrlRenderer +newUrlRenderer = newEmptyMVar + +setUrlRenderer :: UrlRenderer -> (UrlRenderFunc) -> IO () +setUrlRenderer = putMVar + +inFirstRun :: Handler Bool +inFirstRun = isNothing . relDir <$> getYesod + +{- Blocks until the webapp is running and has called setUrlRenderer. -} +renderUrl :: UrlRenderer -> Route WebApp -> [(Text, Text)] -> IO Text +renderUrl urlrenderer route params = do + r <- readMVar urlrenderer + return $ r route params + +{- Redirects back to the referring page, or if there's none, DashboardR -} +redirectBack :: Handler () +redirectBack = do + mr <- lookup "referer" . W.requestHeaders <$> waiRequest + case mr of + Nothing -> redirect DashboardR + Just r -> redirect $ T.pack $ S8.unpack r diff --git a/Assistant/WebApp/Common.hs b/Assistant/WebApp/Common.hs new file mode 100644 index 0000000000..3bd164569f --- /dev/null +++ b/Assistant/WebApp/Common.hs @@ -0,0 +1,17 @@ +{- git-annex assistant webapp, common imports + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU AGPL version 3 or higher. + -} + +module Assistant.WebApp.Common (module X) where + +import Assistant.Common as X +import Assistant.WebApp as X +import Assistant.WebApp.Page as X +import Assistant.WebApp.Form as X +import Assistant.WebApp.Types as X +import Utility.Yesod as X hiding (textField, passwordField, insertBy, replace, joinPath, deleteBy, delete, insert, Key, Option) + +import Data.Text as X (Text) diff --git a/Assistant/WebApp/Configurators.hs b/Assistant/WebApp/Configurators.hs new file mode 100644 index 0000000000..a8e8228b16 --- /dev/null +++ b/Assistant/WebApp/Configurators.hs @@ -0,0 +1,44 @@ +{- git-annex assistant webapp configurators + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU AGPL version 3 or higher. + -} + +{-# LANGUAGE QuasiQuotes, TemplateHaskell, OverloadedStrings, CPP #-} + +module Assistant.WebApp.Configurators where + +import Assistant.WebApp.Common +import Assistant.WebApp.RepoList +#ifdef WITH_XMPP +import Assistant.XMPP.Client +#endif + +{- The main configuration screen. -} +getConfigurationR :: Handler Html +getConfigurationR = ifM (inFirstRun) + ( redirect FirstRepositoryR + , page "Configuration" (Just Configuration) $ do +#ifdef WITH_XMPP + xmppconfigured <- liftAnnex $ isJust <$> getXMPPCreds +#else + let xmppconfigured = False +#endif + $(widgetFile "configurators/main") + ) + +getAddRepositoryR :: Handler Html +getAddRepositoryR = page "Add Repository" (Just Configuration) $ do + let repolist = repoListDisplay mainRepoSelector + $(widgetFile "configurators/addrepository") + +makeMiscRepositories :: Widget +makeMiscRepositories = $(widgetFile "configurators/addrepository/misc") + +makeCloudRepositories :: Widget +makeCloudRepositories = $(widgetFile "configurators/addrepository/cloud") + +makeArchiveRepositories :: Widget +makeArchiveRepositories = $(widgetFile "configurators/addrepository/archive") + diff --git a/Assistant/WebApp/Configurators/AWS.hs b/Assistant/WebApp/Configurators/AWS.hs new file mode 100644 index 0000000000..bf39419527 --- /dev/null +++ b/Assistant/WebApp/Configurators/AWS.hs @@ -0,0 +1,239 @@ +{- git-annex assistant webapp configurators for Amazon AWS services + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU AGPL version 3 or higher. + -} + +{-# LANGUAGE CPP, QuasiQuotes, TemplateHaskell, OverloadedStrings #-} + +module Assistant.WebApp.Configurators.AWS where + +import Assistant.WebApp.Common +import Assistant.MakeRemote +import Assistant.Sync +#ifdef WITH_S3 +import qualified Remote.S3 as S3 +#endif +import qualified Remote.Glacier as Glacier +import qualified Remote.Helper.AWS as AWS +import Logs.Remote +import qualified Remote +import qualified Types.Remote as Remote +import Types.Remote (RemoteConfig) +import Types.StandardGroups +import Logs.PreferredContent +import Creds + +import qualified Data.Text as T +import qualified Data.Map as M +import Data.Char + +awsConfigurator :: Widget -> Handler Html +awsConfigurator = page "Add an Amazon repository" (Just Configuration) + +glacierConfigurator :: Widget -> Handler Html +glacierConfigurator a = do + ifM (liftIO $ inPath "glacier") + ( awsConfigurator a + , awsConfigurator needglaciercli + ) + where + needglaciercli = $(widgetFile "configurators/needglaciercli") + +data StorageClass = StandardRedundancy | ReducedRedundancy + deriving (Eq, Enum, Bounded) + +instance Show StorageClass where + show StandardRedundancy = "STANDARD" + show ReducedRedundancy = "REDUCED_REDUNDANCY" + +data AWSInput = AWSInput + { accessKeyID :: Text + , secretAccessKey :: Text + , datacenter :: Text + -- Only used for S3, not Glacier. + , storageClass :: StorageClass + , repoName :: Text + , enableEncryption :: EnableEncryption + } + +data AWSCreds = AWSCreds Text Text + +extractCreds :: AWSInput -> AWSCreds +extractCreds i = AWSCreds (accessKeyID i) (secretAccessKey i) + +s3InputAForm :: Maybe CredPair -> MkAForm AWSInput +s3InputAForm defcreds = AWSInput + <$> accessKeyIDFieldWithHelp (T.pack . fst <$> defcreds) + <*> secretAccessKeyField (T.pack . snd <$> defcreds) + <*> datacenterField AWS.S3 + <*> areq (selectFieldList storageclasses) "Storage class" (Just StandardRedundancy) + <*> areq textField "Repository name" (Just "S3") + <*> enableEncryptionField + where + storageclasses :: [(Text, StorageClass)] + storageclasses = + [ ("Standard redundancy", StandardRedundancy) + , ("Reduced redundancy (costs less)", ReducedRedundancy) + ] + +glacierInputAForm :: Maybe CredPair -> MkAForm AWSInput +glacierInputAForm defcreds = AWSInput + <$> accessKeyIDFieldWithHelp (T.pack . fst <$> defcreds) + <*> secretAccessKeyField (T.pack . snd <$> defcreds) + <*> datacenterField AWS.Glacier + <*> pure StandardRedundancy + <*> areq textField "Repository name" (Just "glacier") + <*> enableEncryptionField + +awsCredsAForm :: Maybe CredPair -> MkAForm AWSCreds +awsCredsAForm defcreds = AWSCreds + <$> accessKeyIDFieldWithHelp (T.pack . fst <$> defcreds) + <*> secretAccessKeyField (T.pack . snd <$> defcreds) + +accessKeyIDField :: Widget -> Maybe Text -> MkAForm Text +accessKeyIDField help def = areq (textField `withNote` help) "Access Key ID" def + +accessKeyIDFieldWithHelp :: Maybe Text -> MkAForm Text +accessKeyIDFieldWithHelp def = accessKeyIDField help def + where + help = [whamlet| + + Get Amazon access keys +|] + +secretAccessKeyField :: Maybe Text -> MkAForm Text +secretAccessKeyField def = areq passwordField "Secret Access Key" def + +datacenterField :: AWS.Service -> MkAForm Text +datacenterField service = areq (selectFieldList list) "Datacenter" defregion + where + list = M.toList $ AWS.regionMap service + defregion = Just $ AWS.defaultRegion service + +getAddS3R :: Handler Html +getAddS3R = postAddS3R + +postAddS3R :: Handler Html +#ifdef WITH_S3 +postAddS3R = awsConfigurator $ do + defcreds <- liftAnnex previouslyUsedAWSCreds + ((result, form), enctype) <- liftH $ + runFormPost $ renderBootstrap $ s3InputAForm defcreds + case result of + FormSuccess input -> liftH $ do + let name = T.unpack $ repoName input + makeAWSRemote initSpecialRemote S3.remote (extractCreds input) name setgroup $ M.fromList + [ configureEncryption $ enableEncryption input + , ("type", "S3") + , ("datacenter", T.unpack $ datacenter input) + , ("storageclass", show $ storageClass input) + ] + _ -> $(widgetFile "configurators/adds3") + where + setgroup r = liftAnnex $ + setStandardGroup (Remote.uuid r) TransferGroup +#else +postAddS3R = error "S3 not supported by this build" +#endif + +getAddGlacierR :: Handler Html +getAddGlacierR = postAddGlacierR + +postAddGlacierR :: Handler Html +#ifdef WITH_S3 +postAddGlacierR = glacierConfigurator $ do + defcreds <- liftAnnex previouslyUsedAWSCreds + ((result, form), enctype) <- liftH $ + runFormPost $ renderBootstrap $ glacierInputAForm defcreds + case result of + FormSuccess input -> liftH $ do + let name = T.unpack $ repoName input + makeAWSRemote initSpecialRemote Glacier.remote (extractCreds input) name setgroup $ M.fromList + [ configureEncryption $ enableEncryption input + , ("type", "glacier") + , ("datacenter", T.unpack $ datacenter input) + ] + _ -> $(widgetFile "configurators/addglacier") + where + setgroup r = liftAnnex $ + setStandardGroup (Remote.uuid r) SmallArchiveGroup +#else +postAddGlacierR = error "S3 not supported by this build" +#endif + +getEnableS3R :: UUID -> Handler Html +#ifdef WITH_S3 +getEnableS3R uuid = do + m <- liftAnnex readRemoteLog + if isIARemoteConfig $ fromJust $ M.lookup uuid m + then redirect $ EnableIAR uuid + else postEnableS3R uuid +#else +getEnableS3R = postEnableS3R +#endif + +postEnableS3R :: UUID -> Handler Html +#ifdef WITH_S3 +postEnableS3R uuid = awsConfigurator $ enableAWSRemote S3.remote uuid +#else +postEnableS3R _ = error "S3 not supported by this build" +#endif + +getEnableGlacierR :: UUID -> Handler Html +getEnableGlacierR = postEnableGlacierR + +postEnableGlacierR :: UUID -> Handler Html +postEnableGlacierR = glacierConfigurator . enableAWSRemote Glacier.remote + +enableAWSRemote :: RemoteType -> UUID -> Widget +#ifdef WITH_S3 +enableAWSRemote remotetype uuid = do + defcreds <- liftAnnex previouslyUsedAWSCreds + ((result, form), enctype) <- liftH $ + runFormPost $ renderBootstrap $ awsCredsAForm defcreds + case result of + FormSuccess creds -> liftH $ do + m <- liftAnnex readRemoteLog + let name = fromJust $ M.lookup "name" $ + fromJust $ M.lookup uuid m + makeAWSRemote enableSpecialRemote remotetype creds name (const noop) M.empty + _ -> do + description <- liftAnnex $ + T.pack <$> Remote.prettyUUID uuid + $(widgetFile "configurators/enableaws") +#else +enableAWSRemote _ _ = error "S3 not supported by this build" +#endif + +makeAWSRemote :: SpecialRemoteMaker -> RemoteType -> AWSCreds -> String -> (Remote -> Handler ()) -> RemoteConfig -> Handler () +makeAWSRemote maker remotetype (AWSCreds ak sk) name setup config = do + liftIO $ AWS.setCredsEnv (T.unpack ak, T.unpack sk) + r <- liftAnnex $ addRemote $ do + maker hostname remotetype config + setup r + liftAssistant $ syncRemote r + redirect $ EditNewCloudRepositoryR $ Remote.uuid r + where + {- AWS services use the remote name as the basis for a host + - name, so filter it to contain valid characters. -} + hostname = case filter isAlphaNum name of + [] -> "aws" + n -> n + +getRepoInfo :: RemoteConfig -> Widget +getRepoInfo c = [whamlet|S3 remote using bucket: #{bucket}|] + where + bucket = fromMaybe "" $ M.lookup "bucket" c + +#ifdef WITH_S3 +isIARemoteConfig :: RemoteConfig -> Bool +isIARemoteConfig = S3.isIAHost . fromMaybe "" . M.lookup "host" + +previouslyUsedAWSCreds :: Annex (Maybe CredPair) +previouslyUsedAWSCreds = getM gettype [S3.remote, Glacier.remote] + where + gettype t = previouslyUsedCredPair AWS.creds t $ + not . isIARemoteConfig . Remote.config +#endif diff --git a/Assistant/WebApp/Configurators/Delete.hs b/Assistant/WebApp/Configurators/Delete.hs new file mode 100644 index 0000000000..4a28cd3474 --- /dev/null +++ b/Assistant/WebApp/Configurators/Delete.hs @@ -0,0 +1,126 @@ +{- git-annex assistant webapp repository deletion + - + - Copyright 2013 Joey Hess + - + - Licensed under the GNU AGPL version 3 or higher. + -} + +{-# LANGUAGE QuasiQuotes, TemplateHaskell, OverloadedStrings #-} + +module Assistant.WebApp.Configurators.Delete where + +import Assistant.WebApp.Common +import Assistant.DeleteRemote +import Assistant.WebApp.Utility +import Assistant.DaemonStatus +import Assistant.ScanRemotes +import qualified Remote +import qualified Git +import Config.Files +import Utility.FileMode +import Logs.Trust +import Logs.Remote +import Logs.PreferredContent +import Types.StandardGroups + +import System.IO.HVFS (SystemFS(..)) +import qualified Data.Text as T +import qualified Data.Map as M +import System.Path + +notCurrentRepo :: UUID -> Handler Html -> Handler Html +notCurrentRepo uuid a = go =<< liftAnnex (Remote.remoteFromUUID uuid) + where + go Nothing = redirect DeleteCurrentRepositoryR + go (Just _) = a + +getDisableRepositoryR :: UUID -> Handler Html +getDisableRepositoryR uuid = notCurrentRepo uuid $ do + void $ liftAssistant $ disableRemote uuid + redirect DashboardR + +getDeleteRepositoryR :: UUID -> Handler Html +getDeleteRepositoryR uuid = notCurrentRepo uuid $ + deletionPage $ do + reponame <- liftAnnex $ Remote.prettyUUID uuid + $(widgetFile "configurators/delete/start") + +getStartDeleteRepositoryR :: UUID -> Handler Html +getStartDeleteRepositoryR uuid = do + remote <- fromMaybe (error "unknown remote") + <$> liftAnnex (Remote.remoteFromUUID uuid) + liftAnnex $ do + trustSet uuid UnTrusted + setStandardGroup uuid UnwantedGroup + liftAssistant $ addScanRemotes True [remote] + redirect DashboardR + +getFinishDeleteRepositoryR :: UUID -> Handler Html +getFinishDeleteRepositoryR uuid = deletionPage $ do + void $ liftAssistant $ removeRemote uuid + + reponame <- liftAnnex $ Remote.prettyUUID uuid + {- If it's not listed in the remote log, it must be a git repo. -} + gitrepo <- liftAnnex $ M.notMember uuid <$> readRemoteLog + $(widgetFile "configurators/delete/finished") + +getDeleteCurrentRepositoryR :: Handler Html +getDeleteCurrentRepositoryR = deleteCurrentRepository + +postDeleteCurrentRepositoryR :: Handler Html +postDeleteCurrentRepositoryR = deleteCurrentRepository + +deleteCurrentRepository :: Handler Html +deleteCurrentRepository = dangerPage $ do + reldir <- fromJust . relDir <$> liftH getYesod + havegitremotes <- haveremotes syncGitRemotes + havedataremotes <- haveremotes syncDataRemotes + ((result, form), enctype) <- liftH $ + runFormPost $ renderBootstrap $ sanityVerifierAForm $ + SanityVerifier magicphrase + case result of + FormSuccess _ -> liftH $ do + dir <- liftAnnex $ fromRepo Git.repoPath + liftIO $ removeAutoStartFile dir + + {- Disable syncing to this repository, and all + - remotes. This stops all transfers, and all + - file watching. -} + changeSyncable Nothing False + rs <- liftAssistant $ syncRemotes <$> getDaemonStatus + mapM_ (\r -> changeSyncable (Just r) False) rs + + {- Make all directories writable, so all annexed + - content can be deleted. -} + liftIO $ do + recurseDir SystemFS dir >>= + filterM doesDirectoryExist >>= + mapM_ allowWrite + removeDirectoryRecursive dir + + redirect ShutdownConfirmedR + _ -> $(widgetFile "configurators/delete/currentrepository") + where + haveremotes selector = not . null . selector + <$> liftAssistant getDaemonStatus + +data SanityVerifier = SanityVerifier T.Text + deriving (Eq) + +sanityVerifierAForm :: SanityVerifier -> MkAForm SanityVerifier +sanityVerifierAForm template = SanityVerifier + <$> areq checksanity "Confirm deletion?" Nothing + where + checksanity = checkBool (\input -> SanityVerifier input == template) + insane textField + + insane = "Maybe this is not a good idea..." :: Text + +deletionPage :: Widget -> Handler Html +deletionPage = page "Delete repository" (Just Configuration) + +dangerPage :: Widget -> Handler Html +dangerPage = page "Danger danger danger" (Just Configuration) + +magicphrase :: Text +magicphrase = "Yes, please do as I say!" diff --git a/Assistant/WebApp/Configurators/Edit.hs b/Assistant/WebApp/Configurators/Edit.hs new file mode 100644 index 0000000000..b277629aad --- /dev/null +++ b/Assistant/WebApp/Configurators/Edit.hs @@ -0,0 +1,223 @@ +{- git-annex assistant webapp configurator for editing existing repos + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU AGPL version 3 or higher. + -} + +{-# LANGUAGE CPP, QuasiQuotes, TemplateHaskell, OverloadedStrings #-} + +module Assistant.WebApp.Configurators.Edit where + +import Assistant.WebApp.Common +import Assistant.WebApp.Utility +import Assistant.DaemonStatus +import Assistant.MakeRemote (uniqueRemoteName) +import Assistant.WebApp.Configurators.XMPP (xmppNeeded) +import Assistant.ScanRemotes +import qualified Assistant.WebApp.Configurators.AWS as AWS +import qualified Assistant.WebApp.Configurators.IA as IA +#ifdef WITH_S3 +import qualified Remote.S3 as S3 +#endif +import qualified Remote +import qualified Types.Remote as Remote +import qualified Remote.List as Remote +import Logs.UUID +import Logs.Group +import Logs.PreferredContent +import Logs.Remote +import Types.StandardGroups +import qualified Git +import qualified Git.Command +import qualified Git.Config +import qualified Annex +import Git.Remote + +import qualified Data.Text as T +import qualified Data.Map as M +import qualified Data.Set as S + +data RepoGroup = RepoGroupCustom String | RepoGroupStandard StandardGroup + deriving (Show, Eq) + +data RepoConfig = RepoConfig + { repoName :: Text + , repoDescription :: Maybe Text + , repoGroup :: RepoGroup + , repoAssociatedDirectory :: Maybe Text + , repoSyncable :: Bool + } + deriving (Show) + +getRepoConfig :: UUID -> Maybe Remote -> Annex RepoConfig +getRepoConfig uuid mremote = do + groups <- lookupGroups uuid + remoteconfig <- M.lookup uuid <$> readRemoteLog + let (repogroup, associateddirectory) = case getStandardGroup groups of + Nothing -> (RepoGroupCustom $ unwords $ S.toList groups, Nothing) + Just g -> (RepoGroupStandard g, associatedDirectory remoteconfig g) + + description <- maybe Nothing (Just . T.pack) . M.lookup uuid <$> uuidMap + + syncable <- case mremote of + Just r -> return $ remoteAnnexSync $ Remote.gitconfig r + Nothing -> annexAutoCommit <$> Annex.getGitConfig + + return $ RepoConfig + (T.pack $ maybe "here" Remote.name mremote) + description + repogroup + (T.pack <$> associateddirectory) + syncable + +setRepoConfig :: UUID -> Maybe Remote -> RepoConfig -> RepoConfig -> Handler () +setRepoConfig uuid mremote oldc newc = do + when descriptionChanged $ liftAnnex $ do + maybe noop (describeUUID uuid . T.unpack) (repoDescription newc) + void uuidMapLoad + when nameChanged $ do + liftAnnex $ do + name <- fromRepo $ uniqueRemoteName (legalName newc) 0 + {- git remote rename expects there to be a + - remote..fetch, and exits nonzero if + - there's not. Special remotes don't normally + - have that, and don't use it. Temporarily add + - it if it's missing. -} + let remotefetch = "remote." ++ T.unpack (repoName oldc) ++ ".fetch" + needfetch <- isNothing <$> fromRepo (Git.Config.getMaybe remotefetch) + when needfetch $ + inRepo $ Git.Command.run + [Param "config", Param remotefetch, Param ""] + inRepo $ Git.Command.run + [ Param "remote" + , Param "rename" + , Param $ T.unpack $ repoName oldc + , Param name + ] + void $ Remote.remoteListRefresh + liftAssistant updateSyncRemotes + when associatedDirectoryChanged $ case repoAssociatedDirectory newc of + Nothing -> noop + Just t + | T.null t -> noop + | otherwise -> liftAnnex $ do + let dir = takeBaseName $ T.unpack t + m <- readRemoteLog + case M.lookup uuid m of + Nothing -> noop + Just remoteconfig -> configSet uuid $ + M.insert "preferreddir" dir remoteconfig + when groupChanged $ do + liftAnnex $ case repoGroup newc of + RepoGroupStandard g -> setStandardGroup uuid g + RepoGroupCustom s -> groupSet uuid $ S.fromList $ words s + {- Enabling syncing will cause a scan, + - so avoid queueing a duplicate scan. -} + when (repoSyncable newc && not syncableChanged) $ liftAssistant $ + case mremote of + Just remote -> do + addScanRemotes True [remote] + Nothing -> do + addScanRemotes True + =<< syncDataRemotes <$> getDaemonStatus + when syncableChanged $ + changeSyncable mremote (repoSyncable newc) + where + syncableChanged = repoSyncable oldc /= repoSyncable newc + associatedDirectoryChanged = repoAssociatedDirectory oldc /= repoAssociatedDirectory newc + groupChanged = repoGroup oldc /= repoGroup newc + nameChanged = isJust mremote && legalName oldc /= legalName newc + descriptionChanged = repoDescription oldc /= repoDescription newc + + legalName = makeLegalName . T.unpack . repoName + +editRepositoryAForm :: Bool -> RepoConfig -> MkAForm RepoConfig +editRepositoryAForm ishere def = RepoConfig + <$> areq (if ishere then readonlyTextField else textField) + "Name" (Just $ repoName def) + <*> aopt textField "Description" (Just $ repoDescription def) + <*> areq (selectFieldList groups `withNote` help) "Repository group" (Just $ repoGroup def) + <*> associateddirectory + <*> areq checkBoxField "Syncing enabled" (Just $ repoSyncable def) + where + groups = customgroups ++ standardgroups + standardgroups :: [(Text, RepoGroup)] + standardgroups = map (\g -> (T.pack $ descStandardGroup g , RepoGroupStandard g)) + [minBound :: StandardGroup .. maxBound :: StandardGroup] + customgroups :: [(Text, RepoGroup)] + customgroups = case repoGroup def of + RepoGroupCustom s -> [(T.pack s, RepoGroupCustom s)] + _ -> [] + help = [whamlet|What's this?|] + + associateddirectory = case repoAssociatedDirectory def of + Nothing -> aopt hiddenField "" Nothing + Just d -> aopt textField "Associated directory" (Just $ Just d) + +getEditRepositoryR :: UUID -> Handler Html +getEditRepositoryR = postEditRepositoryR + +postEditRepositoryR :: UUID -> Handler Html +postEditRepositoryR = editForm False + +getEditNewRepositoryR :: UUID -> Handler Html +getEditNewRepositoryR = postEditNewRepositoryR + +postEditNewRepositoryR :: UUID -> Handler Html +postEditNewRepositoryR = editForm True + +getEditNewCloudRepositoryR :: UUID -> Handler Html +getEditNewCloudRepositoryR = postEditNewCloudRepositoryR + +postEditNewCloudRepositoryR :: UUID -> Handler Html +postEditNewCloudRepositoryR uuid = xmppNeeded >> editForm True uuid + +editForm :: Bool -> UUID -> Handler Html +editForm new uuid = page "Edit repository" (Just Configuration) $ do + mremote <- liftAnnex $ Remote.remoteFromUUID uuid + curr <- liftAnnex $ getRepoConfig uuid mremote + liftAnnex $ checkAssociatedDirectory curr mremote + ((result, form), enctype) <- liftH $ + runFormPost $ renderBootstrap $ editRepositoryAForm (isNothing mremote) curr + case result of + FormSuccess input -> liftH $ do + setRepoConfig uuid mremote curr input + liftAnnex $ checkAssociatedDirectory input mremote + redirect DashboardR + _ -> do + let istransfer = repoGroup curr == RepoGroupStandard TransferGroup + repoInfo <- getRepoInfo mremote . M.lookup uuid + <$> liftAnnex readRemoteLog + $(widgetFile "configurators/editrepository") + +{- Makes any directory associated with the repository. -} +checkAssociatedDirectory :: RepoConfig -> Maybe Remote -> Annex () +checkAssociatedDirectory _ Nothing = noop +checkAssociatedDirectory cfg (Just r) = do + repoconfig <- M.lookup (Remote.uuid r) <$> readRemoteLog + case repoGroup cfg of + RepoGroupStandard gr -> case associatedDirectory repoconfig gr of + Just d -> inRepo $ \g -> + createDirectoryIfMissing True $ + Git.repoPath g d + Nothing -> noop + _ -> noop + +getRepoInfo :: Maybe Remote.Remote -> Maybe Remote.RemoteConfig -> Widget +getRepoInfo (Just r) (Just c) = case M.lookup "type" c of + Just "S3" +#ifdef WITH_S3 + | S3.isIA c -> IA.getRepoInfo c +#endif + | otherwise -> AWS.getRepoInfo c + Just t + | t /= "git" -> [whamlet|#{t} remote|] + _ -> getGitRepoInfo $ Remote.repo r +getRepoInfo (Just r) _ = getRepoInfo (Just r) (Just $ Remote.config r) +getRepoInfo _ _ = [whamlet|git repository|] + +getGitRepoInfo :: Git.Repo -> Widget +getGitRepoInfo r = do + let loc = Git.repoLocation r + [whamlet|git repository located at #{loc}|] diff --git a/Assistant/WebApp/Configurators/IA.hs b/Assistant/WebApp/Configurators/IA.hs new file mode 100644 index 0000000000..d0d60e25ae --- /dev/null +++ b/Assistant/WebApp/Configurators/IA.hs @@ -0,0 +1,211 @@ +{- git-annex assistant webapp configurators for Internet Archive + - + - Copyright 2013 Joey Hess + - + - Licensed under the GNU AGPL version 3 or higher. + -} + +{-# LANGUAGE CPP, QuasiQuotes, TemplateHaskell, OverloadedStrings #-} + +module Assistant.WebApp.Configurators.IA where + +import Assistant.WebApp.Common +import qualified Assistant.WebApp.Configurators.AWS as AWS +#ifdef WITH_S3 +import qualified Remote.S3 as S3 +import qualified Remote.Helper.AWS as AWS +import Assistant.MakeRemote +#endif +import qualified Remote +import qualified Types.Remote as Remote +import Types.StandardGroups +import Types.Remote (RemoteConfig) +import Logs.PreferredContent +import Logs.Remote +import qualified Utility.Url as Url +import Creds + +import qualified Data.Text as T +import qualified Data.Map as M +import Data.Char +import Network.URI + +iaConfigurator :: Widget -> Handler Html +iaConfigurator = page "Add an Internet Archive repository" (Just Configuration) + +data IAInput = IAInput + { accessKeyID :: Text + , secretAccessKey :: Text + , mediaType :: MediaType + , itemName :: Text + } + +extractCreds :: IAInput -> AWS.AWSCreds +extractCreds i = AWS.AWSCreds (accessKeyID i) (secretAccessKey i) + +{- IA defines only a few media types currently, or the media type + - may be omitted + - + - We add a few other common types, mapped to what we've been told + - is the closest match. + -} +data MediaType = MediaImages | MediaAudio | MediaVideo | MediaText | MediaSoftware | MediaOmitted + deriving (Eq, Ord, Enum, Bounded) + +{- Format a MediaType for entry into the IA metadata -} +formatMediaType :: MediaType -> String +formatMediaType MediaText = "texts" +formatMediaType MediaImages = "image" +formatMediaType MediaSoftware = "software" +formatMediaType MediaVideo = "movies" +formatMediaType MediaAudio = "audio" +formatMediaType MediaOmitted = "" + +{- A default collection to use for each Mediatype. -} +collectionMediaType :: MediaType -> Maybe String +collectionMediaType MediaText = Just "opensource" +collectionMediaType MediaImages = Just "opensource" -- not ideal +collectionMediaType MediaSoftware = Just "opensource" -- not ideal +collectionMediaType MediaVideo = Just "opensource_movies" +collectionMediaType MediaAudio = Just "opensource_audio" +collectionMediaType MediaOmitted = Just "opensource" + +{- Format a MediaType for user display. -} +showMediaType :: MediaType -> String +showMediaType MediaText = "texts" +showMediaType MediaImages = "photos & images" +showMediaType MediaSoftware = "software" +showMediaType MediaVideo = "videos & movies" +showMediaType MediaAudio = "audio & music" +showMediaType MediaOmitted = "other" + +iaInputAForm :: Maybe CredPair -> MkAForm IAInput +iaInputAForm defcreds = IAInput + <$> accessKeyIDFieldWithHelp (T.pack . fst <$> defcreds) + <*> AWS.secretAccessKeyField (T.pack . snd <$> defcreds) + <*> areq (selectFieldList mediatypes) "Media Type" (Just MediaOmitted) + <*> areq (textField `withExpandableNote` ("Help", itemNameHelp)) "Item Name" Nothing + where + mediatypes :: [(Text, MediaType)] + mediatypes = map (\t -> (T.pack $ showMediaType t, t)) [minBound..] + +itemNameHelp :: Widget +itemNameHelp = [whamlet| +
+ Each item stored in the Internet Archive must have a unique name. +
+ Once you create the item, a special directory will appear # + with a name matching the item name. Files you put in that directory # + will be uploaded to your Internet Archive item. +|] + +iaCredsAForm :: Maybe CredPair -> MkAForm AWS.AWSCreds +iaCredsAForm defcreds = AWS.AWSCreds + <$> accessKeyIDFieldWithHelp (T.pack . fst <$> defcreds) + <*> AWS.secretAccessKeyField (T.pack . snd <$> defcreds) + +#ifdef WITH_S3 +previouslyUsedIACreds :: Annex (Maybe CredPair) +previouslyUsedIACreds = previouslyUsedCredPair AWS.creds S3.remote $ + AWS.isIARemoteConfig . Remote.config +#endif + +accessKeyIDFieldWithHelp :: Maybe Text -> MkAForm Text +accessKeyIDFieldWithHelp def = AWS.accessKeyIDField help def + where + help = [whamlet| + + Get Internet Archive access keys +|] + +getAddIAR :: Handler Html +getAddIAR = postAddIAR + +postAddIAR :: Handler Html +#ifdef WITH_S3 +postAddIAR = iaConfigurator $ do + defcreds <- liftAnnex previouslyUsedIACreds + ((result, form), enctype) <- liftH $ + runFormPost $ renderBootstrap $ iaInputAForm defcreds + case result of + FormSuccess input -> liftH $ do + let name = escapeBucket $ T.unpack $ itemName input + AWS.makeAWSRemote initSpecialRemote S3.remote (extractCreds input) name setgroup $ + M.fromList $ catMaybes + [ Just $ configureEncryption NoEncryption + , Just ("type", "S3") + , Just ("host", S3.iaHost) + , Just ("bucket", escapeHeader name) + , Just ("x-archive-meta-title", escapeHeader $ T.unpack $ itemName input) + , if mediaType input == MediaOmitted + then Nothing + else Just ("x-archive-mediatype", formatMediaType $ mediaType input) + , (,) <$> pure "x-archive-meta-collection" <*> collectionMediaType (mediaType input) + -- Make item show up ASAP. + , Just ("x-archive-interactive-priority", "1") + , Just ("preferreddir", name) + ] + _ -> $(widgetFile "configurators/addia") + where + setgroup r = liftAnnex $ + setStandardGroup (Remote.uuid r) PublicGroup +#else +postAddIAR = error "S3 not supported by this build" +#endif + +getEnableIAR :: UUID -> Handler Html +getEnableIAR = postEnableIAR + +postEnableIAR :: UUID -> Handler Html +#ifdef WITH_S3 +postEnableIAR = iaConfigurator . enableIARemote +#else +postEnableIAR _ = error "S3 not supported by this build" +#endif + +#ifdef WITH_S3 +enableIARemote :: UUID -> Widget +enableIARemote uuid = do + defcreds <- liftAnnex previouslyUsedIACreds + ((result, form), enctype) <- liftH $ + runFormPost $ renderBootstrap $ iaCredsAForm defcreds + case result of + FormSuccess creds -> liftH $ do + m <- liftAnnex readRemoteLog + let name = fromJust $ M.lookup "name" $ + fromJust $ M.lookup uuid m + AWS.makeAWSRemote enableSpecialRemote S3.remote creds name (const noop) M.empty + _ -> do + description <- liftAnnex $ + T.pack <$> Remote.prettyUUID uuid + $(widgetFile "configurators/enableia") +#endif + +{- Convert a description into a bucket item name, which will also be + - used as the repository name, and the preferreddir. + - IA seems to need only lower case, and no spaces. -} +escapeBucket :: String -> String +escapeBucket = map toLower . replace " " "-" + +{- IA S3 API likes headers to be URI escaped, escaping spaces looks ugly. -} +escapeHeader :: String -> String +escapeHeader = escapeURIString (\c -> isUnescapedInURI c && c /= ' ') + +getRepoInfo :: RemoteConfig -> Widget +getRepoInfo c = do + exists <- liftIO $ catchDefaultIO False $ fst <$> Url.exists url [] + [whamlet| + + Internet Archive item +$if (not exists) +

+ The page will only be available once some files # + have been uploaded, and the Internet Archive has processed them. +|] + where + bucket = fromMaybe "" $ M.lookup "bucket" c +#ifdef WITH_S3 + url = S3.iaItemUrl bucket +#else + url = "" +#endif diff --git a/Assistant/WebApp/Configurators/Local.hs b/Assistant/WebApp/Configurators/Local.hs new file mode 100644 index 0000000000..e8b7512200 --- /dev/null +++ b/Assistant/WebApp/Configurators/Local.hs @@ -0,0 +1,412 @@ +{- git-annex assistant webapp configurators for making local repositories + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU AGPL version 3 or higher. + -} + +{-# LANGUAGE CPP, QuasiQuotes, TemplateHaskell, OverloadedStrings, RankNTypes, KindSignatures, TypeFamilies #-} + +module Assistant.WebApp.Configurators.Local where + +import Assistant.WebApp.Common +import Assistant.WebApp.OtherRepos +import Assistant.MakeRemote +import Assistant.Sync +import Init +import qualified Git +import qualified Git.Construct +import qualified Git.Config +import qualified Git.Command +import qualified Annex +import Config.Files +import Utility.FreeDesktop +#ifdef WITH_CLIBS +import Utility.Mounts +#endif +import Utility.DiskFree +import Utility.DataUnits +import Utility.Network +import Remote (prettyUUID) +import Annex.UUID +import Types.StandardGroups +import Logs.PreferredContent +import Logs.UUID +import Utility.UserInfo +import Config + +import qualified Data.Text as T +import qualified Data.Map as M +import Data.Char +import qualified Text.Hamlet as Hamlet + +data RepositoryPath = RepositoryPath Text + deriving Show + +{- Custom field display for a RepositoryPath, with an icon etc. + - + - Validates that the path entered is not empty, and is a safe value + - to use as a repository. -} +#if MIN_VERSION_yesod(1,2,0) +repositoryPathField :: forall (m :: * -> *). (MonadIO m, HandlerSite m ~ WebApp) => Bool -> Field m Text +#else +repositoryPathField :: forall sub. Bool -> Field sub WebApp Text +#endif +repositoryPathField autofocus = Field +#if ! MIN_VERSION_yesod_form(1,2,0) + { fieldParse = parse +#else + { fieldParse = \l _ -> parse l + , fieldEnctype = UrlEncoded +#endif + , fieldView = view + } + where + view idAttr nameAttr attrs val isReq = + [whamlet||] + + parse [path] + | T.null path = nopath + | otherwise = liftIO $ checkRepositoryPath path + parse [] = return $ Right Nothing + parse _ = nopath + + nopath = return $ Left "Enter a location for the repository" + +{- As well as checking the path for a lot of silly things, tilde is + - expanded in the returned path. -} +checkRepositoryPath :: Text -> IO (Either (SomeMessage WebApp) (Maybe Text)) +checkRepositoryPath p = do + home <- myHomeDir + let basepath = expandTilde home $ T.unpack p + path <- absPath basepath + let parent = parentDir path + problems <- catMaybes <$> mapM runcheck + [ (return $ path == "/", "Enter the full path to use for the repository.") + , (return $ all isSpace basepath, "A blank path? Seems unlikely.") + , (doesFileExist path, "A file already exists with that name.") + , (return $ path == home, "Sorry, using git-annex for your whole home directory is not currently supported.") + , (not <$> doesDirectoryExist parent, "Parent directory does not exist.") + , (not <$> canWrite path, "Cannot write a repository there.") + ] + return $ + case headMaybe problems of + Nothing -> Right $ Just $ T.pack basepath + Just prob -> Left prob + where + runcheck (chk, msg) = ifM (chk) ( return $ Just msg, return Nothing ) + expandTilde home ('~':'/':path) = home path + expandTilde _ path = path + +{- On first run, if run in the home directory, default to putting it in + - ~/Desktop/annex, when a Desktop directory exists, and ~/annex otherwise. + - + - If run in another directory, that the user can write to, + - the user probably wants to put it there. Unless that directory + - contains a git-annex file, in which case the user has probably + - browsed to a directory with git-annex and run it from there. -} +defaultRepositoryPath :: Bool -> IO FilePath +defaultRepositoryPath firstrun = do + cwd <- liftIO $ getCurrentDirectory + home <- myHomeDir + if home == cwd && firstrun + then inhome + else ifM (legit cwd <&&> canWrite cwd) + ( return cwd + , inhome + ) + where + inhome = do + desktop <- userDesktopDir + ifM (doesDirectoryExist desktop) + ( relHome $ desktop gitAnnexAssistantDefaultDir + , return $ "~" gitAnnexAssistantDefaultDir + ) + legit d = not <$> doesFileExist (d "git-annex") + +newRepositoryForm :: FilePath -> Hamlet.Html -> MkMForm RepositoryPath +newRepositoryForm defpath msg = do + (pathRes, pathView) <- mreq (repositoryPathField True) "" + (Just $ T.pack $ addTrailingPathSeparator defpath) + let (err, errmsg) = case pathRes of + FormMissing -> (False, "") + FormFailure l -> (True, concat $ map T.unpack l) + FormSuccess _ -> (False, "") + let form = do + webAppFormAuthToken + $(widgetFile "configurators/newrepository/form") + return (RepositoryPath <$> pathRes, form) + +{- Making the first repository, when starting the webapp for the first time. -} +getFirstRepositoryR :: Handler Html +getFirstRepositoryR = postFirstRepositoryR +postFirstRepositoryR :: Handler Html +postFirstRepositoryR = page "Getting started" (Just Configuration) $ do +#ifdef __ANDROID__ + androidspecial <- liftIO $ doesDirectoryExist "/sdcard/DCIM" + let path = "/sdcard/annex" +#else + let androidspecial = False + path <- liftIO . defaultRepositoryPath =<< liftH inFirstRun +#endif + ((res, form), enctype) <- liftH $ runFormPost $ newRepositoryForm path + case res of + FormSuccess (RepositoryPath p) -> liftH $ + startFullAssistant (T.unpack p) ClientGroup Nothing + _ -> $(widgetFile "configurators/newrepository/first") + +getAndroidCameraRepositoryR :: Handler () +getAndroidCameraRepositoryR = + startFullAssistant "/sdcard/DCIM" SourceGroup $ Just addignore + where + addignore = do + liftIO $ unlessM (doesFileExist ".gitignore") $ + writeFile ".gitignore" ".thumbnails/*" + void $ inRepo $ + Git.Command.runBool [Param "add", File ".gitignore"] + +{- Adding a new local repository, which may be entirely separate, or may + - be connected to the current repository. -} +getNewRepositoryR :: Handler Html +getNewRepositoryR = postNewRepositoryR +postNewRepositoryR :: Handler Html +postNewRepositoryR = page "Add another repository" (Just Configuration) $ do + home <- liftIO myHomeDir + ((res, form), enctype) <- liftH $ runFormPost $ newRepositoryForm home + case res of + FormSuccess (RepositoryPath p) -> do + let path = T.unpack p + isnew <- liftIO $ makeRepo path False + u <- liftIO $ initRepo isnew True path Nothing + liftH $ liftAnnexOr () $ setStandardGroup u ClientGroup + liftIO $ addAutoStartFile path + liftIO $ startAssistant path + askcombine u path + _ -> $(widgetFile "configurators/newrepository") + where + askcombine newrepouuid newrepopath = do + newrepo <- liftIO $ relHome newrepopath + mainrepo <- fromJust . relDir <$> liftH getYesod + $(widgetFile "configurators/newrepository/combine") + +getCombineRepositoryR :: FilePathAndUUID -> Handler Html +getCombineRepositoryR (FilePathAndUUID newrepopath newrepouuid) = do + r <- combineRepos newrepopath remotename + liftAssistant $ syncRemote r + redirect $ EditRepositoryR newrepouuid + where + remotename = takeFileName newrepopath + +selectDriveForm :: [RemovableDrive] -> Hamlet.Html -> MkMForm RemovableDrive +selectDriveForm drives = renderBootstrap $ RemovableDrive + <$> pure Nothing + <*> areq (selectFieldList pairs) "Select drive:" Nothing + <*> areq textField "Use this directory on the drive:" + (Just $ T.pack gitAnnexAssistantDefaultDir) + where + pairs = zip (map describe drives) (map mountPoint drives) + describe drive = case diskFree drive of + Nothing -> mountPoint drive + Just free -> + let sz = roughSize storageUnits True free + in T.unwords + [ mountPoint drive + , T.concat ["(", T.pack sz] + , "free)" + ] + +removableDriveRepository :: RemovableDrive -> FilePath +removableDriveRepository drive = + T.unpack (mountPoint drive) T.unpack (driveRepoPath drive) + +{- Adding a removable drive. -} +getAddDriveR :: Handler Html +getAddDriveR = postAddDriveR +postAddDriveR :: Handler Html +postAddDriveR = page "Add a removable drive" (Just Configuration) $ do + removabledrives <- liftIO $ driveList + writabledrives <- liftIO $ + filterM (canWrite . T.unpack . mountPoint) removabledrives + ((res, form), enctype) <- liftH $ runFormPost $ + selectDriveForm (sort writabledrives) + case res of + FormSuccess drive -> liftH $ redirect $ ConfirmAddDriveR drive + _ -> $(widgetFile "configurators/adddrive") + +{- The repo may already exist, when adding removable media + - that has already been used elsewhere. If so, check + - the UUID of the repo and see if it's one we know. If not, + - the user must confirm the repository merge. -} +getConfirmAddDriveR :: RemovableDrive -> Handler Html +getConfirmAddDriveR drive = do + ifM (needconfirm) + ( page "Combine repositories?" (Just Configuration) $ + $(widgetFile "configurators/adddrive/confirm") + , do + getFinishAddDriveR drive + ) + where + dir = removableDriveRepository drive + needconfirm = ifM (liftIO $ doesDirectoryExist dir) + ( liftAnnex $ do + mu <- liftIO $ catchMaybeIO $ + inDir dir $ getUUID + case mu of + Nothing -> return False + Just driveuuid -> not . + M.member driveuuid <$> uuidMap + , return False + ) + +cloneModal :: Widget +cloneModal = $(widgetFile "configurators/adddrive/clonemodal") + +getFinishAddDriveR :: RemovableDrive -> Handler Html +getFinishAddDriveR drive = make >>= redirect . EditNewRepositoryR + where + make = do + liftIO $ createDirectoryIfMissing True dir + isnew <- liftIO $ makeRepo dir True + u <- liftIO $ initRepo isnew False dir $ Just remotename + {- Removable drives are not reliable media, so enable fsync. -} + liftIO $ inDir dir $ + setConfig (ConfigKey "core.fsyncobjectfiles") + (Git.Config.boolConfig True) + r <- combineRepos dir remotename + liftAnnex $ setStandardGroup u TransferGroup + liftAssistant $ syncRemote r + return u + mountpoint = T.unpack (mountPoint drive) + dir = removableDriveRepository drive + remotename = takeFileName mountpoint + +{- 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 = liftAnnex $ do + hostname <- maybe "host" id <$> liftIO getHostname + hostlocation <- fromRepo Git.repoLocation + liftIO $ inDir dir $ void $ makeGitRemote hostname hostlocation + addRemote $ makeGitRemote name dir + +getEnableDirectoryR :: UUID -> Handler Html +getEnableDirectoryR uuid = page "Enable a repository" (Just Configuration) $ do + description <- liftAnnex $ T.pack <$> prettyUUID 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 + <$> getDiskFree dir + <*> pure (T.pack dir) + <*> pure (T.pack gitAnnexAssistantDefaultDir) + -- filter out some things that are surely not removable drives + sane Mntent { mnt_dir = dir, mnt_fsname = dev } + {- We want real disks like /dev/foo, not + - dummy mount points like proc or tmpfs or + - gvfs-fuse-daemon. -} + | not ('/' `elem` dev) = False + {- Just in case: These mount points are surely not + - removable disks. -} + | dir == "/" = False + | dir == "/tmp" = False + | dir == "/run/shm" = False + | dir == "/run/lock" = False +#ifdef __ANDROID__ + | dir == "/mnt/sdcard" = False + | dir == "/sdcard" = False +#endif + | 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 + - url to the new webapp. -} +startFullAssistant :: FilePath -> StandardGroup -> Maybe (Annex ())-> Handler () +startFullAssistant path repogroup setup = do + webapp <- getYesod + url <- liftIO $ do + isnew <- makeRepo path False + u <- initRepo isnew True path Nothing + inDir path $ do + setStandardGroup u repogroup + maybe noop id setup + addAutoStartFile path + setCurrentDirectory path + fromJust $ postFirstRun webapp + redirect $ T.pack url + +{- 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] + | otherwise = baseparams ++ [File path] + +{- Runs an action in the git-annex repository in the specified directory. -} +inDir :: FilePath -> Annex a -> IO a +inDir dir a = do + state <- Annex.new =<< Git.Config.read =<< Git.Construct.fromPath dir + Annex.eval state a + +{- 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 + initRepo' desc + {- Initialize the master branch, so things that expect + - to have it will work, before any files are added. -} + unlessM (Git.Config.isBare <$> gitRepo) $ + void $ inRepo $ Git.Command.runBool + [ Param "commit" + , Param "--quiet" + , Param "--allow-empty" + , Param "-m" + , Param "created repository" + ] + {- 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 +{- Repo already exists, could be a non-git-annex repo though. -} +initRepo False _ dir desc = inDir dir $ do + initRepo' desc + getUUID + +initRepo' :: Maybe String -> Annex () +initRepo' desc = do + unlessM isInitialized $ + initialize desc + +{- Checks if the user can write to a directory. + - + - The directory may be in the process of being created; if so + - the parent directory is checked instead. -} +canWrite :: FilePath -> IO Bool +canWrite dir = do + tocheck <- ifM (doesDirectoryExist dir) + (return dir, return $ parentDir dir) + catchBoolIO $ fileAccess tocheck False True False diff --git a/Assistant/WebApp/Configurators/Pairing.hs b/Assistant/WebApp/Configurators/Pairing.hs new file mode 100644 index 0000000000..ec3a8e43f1 --- /dev/null +++ b/Assistant/WebApp/Configurators/Pairing.hs @@ -0,0 +1,327 @@ +{- git-annex assistant webapp configurator for pairing + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU AGPL version 3 or higher. + -} + +{-# LANGUAGE TypeFamilies, QuasiQuotes, TemplateHaskell, OverloadedStrings #-} +{-# LANGUAGE CPP #-} + +module Assistant.WebApp.Configurators.Pairing where + +import Assistant.Pairing +import Assistant.WebApp.Common +import Assistant.WebApp.Configurators +import Assistant.Types.Buddies +import Annex.UUID +#ifdef WITH_PAIRING +import Assistant.Pairing.Network +import Assistant.Pairing.MakeRemote +import Assistant.Ssh +import Assistant.Alert +import Assistant.DaemonStatus +import Utility.Verifiable +import Utility.Network +#endif +#ifdef WITH_XMPP +import Assistant.XMPP.Client +import Assistant.XMPP.Buddies +import Assistant.XMPP.Git +import Network.Protocol.XMPP +import Assistant.Types.NetMessager +import Assistant.NetMessager +import Assistant.WebApp.RepoList +import Assistant.WebApp.Configurators.XMPP +#endif +import Utility.UserInfo +import Git + +import qualified Data.Text as T +#ifdef WITH_PAIRING +import qualified Data.Text.Encoding as T +import qualified Data.ByteString.Lazy as B +import Data.Char +import qualified Control.Exception as E +import Control.Concurrent +#endif +#ifdef WITH_XMPP +import qualified Data.Set as S +#endif + +getStartXMPPPairFriendR :: Handler Html +#ifdef WITH_XMPP +getStartXMPPPairFriendR = 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/friend/prompt") + , do + -- go get XMPP configured, then come back + redirect XMPPConfigForPairFriendR + ) +#else +getStartXMPPPairFriendR = noXMPPPairing + +noXMPPPairing :: Handler Html +noXMPPPairing = noPairing "XMPP" +#endif + +getStartXMPPPairSelfR :: Handler Html +#ifdef WITH_XMPP +getStartXMPPPairSelfR = go =<< liftAnnex getXMPPCreds + where + go Nothing = do + -- go get XMPP configured, then come back + redirect XMPPConfigForPairSelfR + go (Just creds) = do + {- Ask buddies to send presence info, to get + - the buddy list populated. -} + liftAssistant $ sendNetMessage QueryPresence + let account = xmppJID creds + pairPage $ + $(widgetFile "configurators/pairing/xmpp/self/prompt") +#else +getStartXMPPPairSelfR = noXMPPPairing +#endif + +getRunningXMPPPairFriendR :: BuddyKey -> Handler Html +getRunningXMPPPairFriendR = sendXMPPPairRequest . Just + +getRunningXMPPPairSelfR :: Handler Html +getRunningXMPPPairSelfR = sendXMPPPairRequest Nothing + +{- Sends a XMPP pair request, to a buddy or to self. -} +sendXMPPPairRequest :: Maybe BuddyKey -> Handler Html +#ifdef WITH_XMPP +sendXMPPPairRequest mbid = do + bid <- maybe getself return mbid + buddy <- liftAssistant $ getBuddy bid <<~ buddyList + go $ S.toList . buddyAssistants <$> buddy + where + go (Just (clients@((Client exemplar):_))) = do + u <- liftAnnex getUUID + liftAssistant $ forM_ clients $ \(Client c) -> sendNetMessage $ + PairingNotification PairReq (formatJID c) u + xmppPairStatus True $ + if selfpair then Nothing else Just exemplar + go _ + {- Nudge the user to turn on their other device. -} + | selfpair = do + liftAssistant $ sendNetMessage QueryPresence + pairPage $ + $(widgetFile "configurators/pairing/xmpp/self/retry") + {- Buddy could have logged out, etc. + - Go back to buddy list. -} + | otherwise = redirect StartXMPPPairFriendR + selfpair = isNothing mbid + getself = maybe (error "XMPP not configured") + (return . BuddyKey . xmppJID) + =<< liftAnnex getXMPPCreds +#else +sendXMPPPairRequest _ = noXMPPPairing +#endif + +{- Starts local pairing. -} +getStartLocalPairR :: Handler Html +getStartLocalPairR = postStartLocalPairR +postStartLocalPairR :: Handler Html +#ifdef WITH_PAIRING +postStartLocalPairR = promptSecret Nothing $ + startLocalPairing PairReq noop pairingAlert Nothing +#else +postStartLocalPairR = noLocalPairing + +noLocalPairing :: Handler Html +noLocalPairing = noPairing "local" +#endif + +{- Runs on the system that responds to a local pair request; sets up the ssh + - authorized key first so that the originating host can immediately sync + - with us. -} +getFinishLocalPairR :: PairMsg -> Handler Html +getFinishLocalPairR = postFinishLocalPairR +postFinishLocalPairR :: PairMsg -> Handler Html +#ifdef WITH_PAIRING +postFinishLocalPairR msg = promptSecret (Just msg) $ \_ secret -> do + repodir <- liftH $ repoPath <$> liftAnnex gitRepo + liftIO $ setup repodir + startLocalPairing PairAck (cleanup repodir) alert uuid "" secret + where + alert = pairRequestAcknowledgedAlert (pairRepo msg) . Just + setup repodir = setupAuthorizedKeys msg repodir + cleanup repodir = removeAuthorizedKeys False repodir $ + remoteSshPubKey $ pairMsgData msg + uuid = Just $ pairUUID $ pairMsgData msg +#else +postFinishLocalPairR _ = noLocalPairing +#endif + +getConfirmXMPPPairFriendR :: PairKey -> Handler Html +#ifdef WITH_XMPP +getConfirmXMPPPairFriendR pairkey@(PairKey _ t) = case parseJID t of + Nothing -> error "bad JID" + Just theirjid -> pairPage $ do + let name = buddyName theirjid + $(widgetFile "configurators/pairing/xmpp/friend/confirm") +#else +getConfirmXMPPPairFriendR _ = noXMPPPairing +#endif + +getFinishXMPPPairFriendR :: PairKey -> Handler Html +#ifdef WITH_XMPP +getFinishXMPPPairFriendR (PairKey theiruuid t) = case parseJID t of + Nothing -> error "bad JID" + Just theirjid -> do + selfuuid <- liftAnnex getUUID + liftAssistant $ do + sendNetMessage $ + PairingNotification PairAck (formatJID theirjid) selfuuid + finishXMPPPairing theirjid theiruuid + xmppPairStatus False $ Just theirjid +#else +getFinishXMPPPairFriendR _ = noXMPPPairing +#endif + +{- Displays a page indicating pairing status and + - prompting to set up cloud repositories. -} +#ifdef WITH_XMPP +xmppPairStatus :: Bool -> Maybe JID -> Handler Html +xmppPairStatus inprogress theirjid = pairPage $ do + let friend = buddyName <$> theirjid + $(widgetFile "configurators/pairing/xmpp/end") +#endif + +getRunningLocalPairR :: SecretReminder -> Handler Html +#ifdef WITH_PAIRING +getRunningLocalPairR s = pairPage $ do + let secret = fromSecretReminder s + $(widgetFile "configurators/pairing/local/inprogress") +#else +getRunningLocalPairR _ = noLocalPairing +#endif + +#ifdef WITH_PAIRING + +{- Starts local pairing, at either the PairReq (initiating host) or + - PairAck (responding host) stage. + - + - Displays an alert, and starts a thread sending the pairing message, + - which will continue running until the other host responds, or until + - canceled by the user. If canceled by the user, runs the oncancel action. + - + - Redirects to the pairing in progress page. + -} +startLocalPairing :: PairStage -> IO () -> (AlertButton -> Alert) -> Maybe UUID -> Text -> Secret -> Widget +startLocalPairing stage oncancel alert muuid displaysecret secret = do + urlrender <- liftH getUrlRender + reldir <- fromJust . relDir <$> liftH getYesod + + sendrequests <- liftAssistant $ asIO2 $ mksendrequests urlrender + {- Generating a ssh key pair can take a while, so do it in the + - background. -} + thread <- liftAssistant $ asIO $ do + keypair <- liftIO $ genSshKeyPair + pairdata <- liftIO $ PairData + <$> getHostname + <*> myUserName + <*> pure reldir + <*> pure (sshPubKey keypair) + <*> (maybe genUUID return muuid) + let sender = multicastPairMsg Nothing secret pairdata + let pip = PairingInProgress secret Nothing keypair pairdata stage + startSending pip stage $ sendrequests sender + void $ liftIO $ forkIO thread + + liftH $ redirect $ RunningLocalPairR $ toSecretReminder displaysecret + where + {- Sends pairing messages until the thread is killed, + - and shows an activity alert while doing it. + - + - 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. + -} + mksendrequests urlrender sender _stage = do + tid <- liftIO myThreadId + let selfdestruct = AlertButton + { buttonLabel = "Cancel" + , buttonUrl = urlrender DashboardR + , buttonAction = Just $ const $ do + oncancel + killThread tid + } + alertDuring (alert selfdestruct) $ liftIO $ do + _ <- E.try (sender stage) :: IO (Either E.SomeException ()) + return () + +data InputSecret = InputSecret { secretText :: Maybe Text } + +{- If a PairMsg is passed in, ensures that the user enters a secret + - that can validate it. -} +promptSecret :: Maybe PairMsg -> (Text -> Secret -> Widget) -> Handler Html +promptSecret msg cont = pairPage $ do + ((result, form), enctype) <- liftH $ + runFormPost $ renderBootstrap $ + InputSecret <$> aopt textField "Secret phrase" Nothing + case result of + FormSuccess v -> do + let rawsecret = fromMaybe "" $ secretText v + let secret = toSecret rawsecret + case msg of + Nothing -> case secretProblem secret of + Nothing -> cont rawsecret secret + Just problem -> + showform form enctype $ Just problem + Just m -> + if verify (fromPairMsg m) secret + then cont rawsecret secret + else showform form enctype $ Just + "That's not the right secret phrase." + _ -> showform form enctype Nothing + where + showform form enctype mproblem = do + let start = isNothing msg + let badphrase = isJust mproblem + let problem = fromMaybe "" mproblem + let (username, hostname) = maybe ("", "") + (\(_, v, a) -> (T.pack $ remoteUserName v, T.pack $ fromMaybe (showAddr a) (remoteHostName v))) + (verifiableVal . fromPairMsg <$> msg) + u <- T.pack <$> liftIO myUserName + let sameusername = username == u + $(widgetFile "configurators/pairing/local/prompt") + +{- This counts unicode characters as more than one character, + - but that's ok; they *do* provide additional entropy. -} +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 < 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 + +toSecret :: Text -> Secret +toSecret s = B.fromChunks [T.encodeUtf8 $ T.toLower $ T.filter isAlphaNum s] + +{- From Dickens -} +sampleQuote :: Text +sampleQuote = T.unwords + [ "It was the best of times," + , "it was the worst of times," + , "it was the age of wisdom," + , "it was the age of foolishness." + ] + +#else + +#endif + +pairPage :: Widget -> Handler Html +pairPage = page "Pairing" (Just Configuration) + +noPairing :: Text -> Handler Html +noPairing pairingtype = pairPage $ + $(widgetFile "configurators/pairing/disabled") diff --git a/Assistant/WebApp/Configurators/Preferences.hs b/Assistant/WebApp/Configurators/Preferences.hs new file mode 100644 index 0000000000..d50aea0df8 --- /dev/null +++ b/Assistant/WebApp/Configurators/Preferences.hs @@ -0,0 +1,103 @@ +{- git-annex assistant general preferences + - + - Copyright 2013 Joey Hess + - + - Licensed under the GNU AGPL version 3 or higher. + -} + +{-# LANGUAGE QuasiQuotes, TemplateHaskell, OverloadedStrings #-} + +module Assistant.WebApp.Configurators.Preferences ( + getPreferencesR, + postPreferencesR +) where + +import Assistant.WebApp.Common +import qualified Annex +import qualified Git +import Config +import Config.Files +import Utility.DataUnits +import Git.Config + +import qualified Data.Text as T + +data PrefsForm = PrefsForm + { diskReserve :: Text + , numCopies :: Int + , autoStart :: Bool + , debugEnabled :: Bool + } + +prefsAForm :: PrefsForm -> MkAForm 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|
Avoid downloading files from other repositories when there is too little free disk space.|] + numcopiesnote = [whamlet|
Only drop a file after verifying that other repositories contain this many copies.|] + debugnote = [whamlet|
View Log|] + 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 + <*> (annexDebug <$> Annex.getGitConfig) + +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 + setConfig (annexConfig "debug") (boolConfig $ debugEnabled p) + liftIO $ if debugEnabled p + then enableDebugOutput + else disableDebugOutput + +getPreferencesR :: Handler Html +getPreferencesR = postPreferencesR +postPreferencesR :: Handler Html +postPreferencesR = page "Preferences" (Just Configuration) $ do + ((result, form), enctype) <- liftH $ do + current <- liftAnnex getPrefs + runFormPost $ renderBootstrap $ prefsAForm current + case result of + FormSuccess new -> liftH $ 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 new file mode 100644 index 0000000000..945e2b55c6 --- /dev/null +++ b/Assistant/WebApp/Configurators/Ssh.hs @@ -0,0 +1,382 @@ +{- git-annex assistant webapp configurator for ssh-based remotes + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU AGPL version 3 or higher. + -} + +{-# LANGUAGE QuasiQuotes, TemplateHaskell, OverloadedStrings #-} +{-# LANGUAGE CPP #-} + +module Assistant.WebApp.Configurators.Ssh where + +import Assistant.WebApp.Common +import Assistant.Ssh +import Assistant.MakeRemote +import Utility.Rsync (rsyncUrlIsShell) +import Logs.Remote +import Remote +import Logs.PreferredContent +import Types.StandardGroups +import Utility.UserInfo + +import qualified Data.Text as T +import qualified Data.Map as M +import Network.Socket + +sshConfigurator :: Widget -> Handler Html +sshConfigurator = page "Add a remote server" (Just Configuration) + +data SshInput = SshInput + { inputHostname :: Maybe Text + , inputUsername :: Maybe Text + , inputDirectory :: Maybe Text + , inputPort :: Int + } + deriving (Show) + +{- SshInput is only used for applicative form prompting, this converts + - the result of such a form into a SshData. -} +mkSshData :: SshInput -> SshData +mkSshData s = SshData + { sshHostName = fromMaybe "" $ inputHostname s + , sshUserName = inputUsername s + , sshDirectory = fromMaybe "" $ inputDirectory s + , sshRepoName = genSshRepoName + (T.unpack $ fromJust $ inputHostname s) + (maybe "" T.unpack $ inputDirectory s) + , sshPort = inputPort s + , needsPubKey = False + , rsyncOnly = False + } + +mkSshInput :: SshData -> SshInput +mkSshInput s = SshInput + { inputHostname = Just $ sshHostName s + , inputUsername = sshUserName s + , inputDirectory = Just $ sshDirectory s + , inputPort = sshPort s + } + +#if MIN_VERSION_yesod(1,2,0) +sshInputAForm :: Field Handler Text -> SshInput -> AForm Handler SshInput +#else +sshInputAForm :: Field WebApp WebApp Text -> SshInput -> AForm WebApp WebApp SshInput +#endif +sshInputAForm hostnamefield def = SshInput + <$> aopt check_hostname "Host name" (Just $ inputHostname def) + <*> aopt check_username "User name" (Just $ inputUsername def) + <*> aopt textField "Directory" (Just $ Just $ fromMaybe (T.pack gitAnnexAssistantDefaultDir) $ inputDirectory def) + <*> areq intField "Port" (Just $ inputPort def) + where + check_username = checkBool (all (`notElem` "/:@ \t") . T.unpack) + bad_username textField + + bad_username = "bad user name" :: Text +#ifndef __ANDROID__ + bad_hostname = "cannot resolve host name" :: Text + + check_hostname = checkM (liftIO . checkdns) hostnamefield + checkdns t = do + let h = T.unpack t + let canonname = Just $ defaultHints { addrFlags = [AI_CANONNAME] } + r <- catchMaybeIO $ getAddrInfo canonname (Just h) Nothing + return $ case catMaybes . map addrCanonName <$> r of + -- canonicalize input hostname if it had no dot + Just (fullname:_) + | '.' `elem` h -> Right t + | otherwise -> Right $ T.pack fullname + Just [] -> Right t + Nothing -> Left bad_hostname +#else + -- getAddrInfo currently broken on Android + check_hostname = hostnamefield -- unchecked +#endif + +data ServerStatus + = UntestedServer + | UnusableServer Text -- reason why it's not usable + | UsableRsyncServer + | UsableSshInput + deriving (Eq) + +usable :: ServerStatus -> Bool +usable UntestedServer = False +usable (UnusableServer _) = False +usable UsableRsyncServer = True +usable UsableSshInput = True + +getAddSshR :: Handler Html +getAddSshR = postAddSshR +postAddSshR :: Handler Html +postAddSshR = sshConfigurator $ do + u <- liftIO $ T.pack <$> myUserName + ((result, form), enctype) <- liftH $ + runFormPost $ renderBootstrap $ sshInputAForm textField $ + SshInput Nothing (Just u) Nothing 22 + case result of + FormSuccess sshinput -> do + s <- liftIO $ testServer sshinput + case s of + Left status -> showform form enctype status + Right sshdata -> liftH $ redirect $ ConfirmSshR sshdata + _ -> showform form enctype UntestedServer + where + showform form enctype status = $(widgetFile "configurators/ssh/add") + +sshTestModal :: Widget +sshTestModal = $(widgetFile "configurators/ssh/testmodal") + +{- To enable an existing rsync special remote, parse the SshInput from + - its rsyncurl, and display a form whose only real purpose is to check + - if ssh public keys need to be set up. From there, we can proceed with + - the usual repo setup; all that code is idempotent. + - + - Note that there's no EnableSshR because ssh remotes are not special + - remotes, and so their configuration is not shared between repositories. + -} +getEnableRsyncR :: UUID -> Handler Html +getEnableRsyncR = postEnableRsyncR +postEnableRsyncR :: UUID -> Handler Html +postEnableRsyncR u = do + 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) <- liftH $ + runFormPost $ renderBootstrap $ sshInputAForm textField sshinput + case result of + FormSuccess sshinput' + | isRsyncNet (inputHostname sshinput') -> + void $ liftH $ makeRsyncNet sshinput' reponame (const noop) + | otherwise -> do + s <- liftIO $ testServer sshinput' + case s of + Left status -> showform form enctype status + Right sshdata -> enable sshdata + { sshRepoName = reponame } + _ -> showform form enctype UntestedServer + _ -> redirect AddSshR + where + showform form enctype status = do + description <- liftAnnex $ T.pack <$> prettyUUID u + $(widgetFile "configurators/ssh/enable") + enable sshdata = liftH $ redirect $ ConfirmSshR $ + sshdata { rsyncOnly = True } + +{- Converts a rsyncurl value to a SshInput. But only if it's a ssh rsync + - url; rsync:// urls or bare path names are not supported. + - + - The hostname is stored mangled in the remote log for rsync special + - remotes configured by this webapp. So that mangling has to reversed + - here to get back the original hostname. + -} +parseSshRsyncUrl :: String -> Maybe SshInput +parseSshRsyncUrl u + | not (rsyncUrlIsShell u) = Nothing + | otherwise = Just $ SshInput + { inputHostname = val $ unMangleSshHostName host + , inputUsername = if null user then Nothing else val user + , inputDirectory = val dir + , inputPort = 22 + } + where + val = Just . T.pack + (userhost, dir) = separate (== ':') u + (user, host) = if '@' `elem` userhost + then separate (== '@') userhost + else (userhost, "") + +{- Test if we can ssh into the server. + - + - Two probe attempts are made. First, try sshing in using the existing + - configuration, but don't let ssh prompt for any password. If + - passwordless login is already enabled, use it. Otherwise, + - a special ssh key will need to be generated just for this server. + - + - Once logged into the server, probe to see if git-annex-shell is + - available, or rsync. Note that, ~/.ssh/git-annex-shell may be + - present, while git-annex-shell is not in PATH. + -} +testServer :: SshInput -> IO (Either ServerStatus SshData) +testServer (SshInput { inputHostname = Nothing }) = return $ + Left $ UnusableServer "Please enter a host name." +testServer sshinput@(SshInput { inputHostname = Just hn }) = do + status <- probe [sshOpt "NumberOfPasswordPrompts" "0"] + if usable status + then ret status False + else do + status' <- probe [] + if usable status' + then ret status' True + else return $ Left status' + where + ret status needspubkey = return $ Right $ (mkSshData sshinput) + { needsPubKey = needspubkey + , rsyncOnly = status == UsableRsyncServer + } + probe extraopts = do + let remotecommand = shellWrap $ intercalate ";" + [ report "loggedin" + , checkcommand "git-annex-shell" + , checkcommand "rsync" + , checkcommand shim + ] + knownhost <- knownHost hn + let sshopts = filter (not . null) $ extraopts ++ + {- If this is an already known host, let + - ssh check it as usual. + - Otherwise, trust the host key. -} + [ if knownhost then "" else sshOpt "StrictHostKeyChecking" "no" + , "-n" -- don't read from stdin + , "-p", show (inputPort sshinput) + , genSshHost + (fromJust $ inputHostname sshinput) + (inputUsername sshinput) + , remotecommand + ] + parsetranscript . fst <$> sshTranscript sshopts Nothing + parsetranscript s + | reported "git-annex-shell" = UsableSshInput + | reported shim = UsableSshInput + | reported "rsync" = UsableRsyncServer + | reported "loggedin" = UnusableServer + "Neither rsync nor git-annex are installed on the server. Perhaps you should go install them?" + | otherwise = UnusableServer $ T.pack $ + "Failed to ssh to the server. Transcript: " ++ s + where + reported r = token r `isInfixOf` s + + checkcommand c = "if which " ++ c ++ "; then " ++ report c ++ "; fi" + token r = "git-annex-probe " ++ r + report r = "echo " ++ token r + shim = "~/.ssh/git-annex-shell" + +{- Runs a ssh command; if it fails shows the user the transcript, + - and if it succeeds, runs an action. -} +sshSetup :: [String] -> String -> Handler Html -> Handler Html +sshSetup opts input a = do + (transcript, ok) <- liftIO $ sshTranscript opts (Just input) + if ok + then a + else showSshErr transcript + +showSshErr :: String -> Handler Html +showSshErr msg = sshConfigurator $ + $(widgetFile "configurators/ssh/error") + +getConfirmSshR :: SshData -> Handler Html +getConfirmSshR sshdata = sshConfigurator $ + $(widgetFile "configurators/ssh/confirm") + +getRetrySshR :: SshData -> Handler () +getRetrySshR sshdata = do + s <- liftIO $ testServer $ mkSshInput sshdata + redirect $ either (const $ ConfirmSshR sshdata) ConfirmSshR s + +getMakeSshGitR :: SshData -> Handler Html +getMakeSshGitR = makeSsh False setupGroup + +getMakeSshRsyncR :: SshData -> Handler Html +getMakeSshRsyncR = makeSsh True setupGroup + +makeSsh :: Bool -> (Remote -> Handler ()) -> SshData -> Handler Html +makeSsh rsync setup sshdata + | needsPubKey sshdata = do + keypair <- liftIO genSshKeyPair + sshdata' <- liftIO $ setupSshKeyPair keypair sshdata + makeSsh' rsync setup sshdata sshdata' (Just keypair) + | sshPort sshdata /= 22 = do + sshdata' <- liftIO $ setSshConfig sshdata [] + makeSsh' rsync setup sshdata sshdata' Nothing + | otherwise = makeSsh' rsync setup sshdata sshdata Nothing + +makeSsh' :: Bool -> (Remote -> Handler ()) -> SshData -> SshData -> Maybe SshKeyPair -> Handler Html +makeSsh' rsync setup origsshdata sshdata keypair = do + sshSetup ["-p", show (sshPort origsshdata), sshhost, remoteCommand] "" $ + makeSshRepo rsync setup sshdata + where + sshhost = genSshHost (sshHostName origsshdata) (sshUserName origsshdata) + remotedir = T.unpack $ sshDirectory sshdata + remoteCommand = shellWrap $ intercalate "&&" $ catMaybes + [ Just $ "mkdir -p " ++ shellEscape remotedir + , Just $ "cd " ++ shellEscape remotedir + , if rsync then Nothing else Just "if [ ! -d .git ]; then git init --bare --shared; fi" + , if rsync then Nothing else Just "git annex init" + , if needsPubKey sshdata + then addAuthorizedKeysCommand (rsync || rsyncOnly sshdata) remotedir . sshPubKey <$> keypair + else Nothing + ] + +makeSshRepo :: Bool -> (Remote -> Handler ()) -> SshData -> Handler Html +makeSshRepo forcersync setup sshdata = do + r <- liftAssistant $ makeSshRemote forcersync sshdata Nothing + setup r + redirect $ EditNewCloudRepositoryR $ Remote.uuid r + +getAddRsyncNetR :: Handler Html +getAddRsyncNetR = postAddRsyncNetR +postAddRsyncNetR :: Handler Html +postAddRsyncNetR = do + ((result, form), enctype) <- runFormPost $ + renderBootstrap $ sshInputAForm hostnamefield $ + SshInput Nothing Nothing Nothing 22 + let showform status = page "Add a Rsync.net repository" (Just Configuration) $ + $(widgetFile "configurators/addrsync.net") + case result of + FormSuccess sshinput + | isRsyncNet (inputHostname sshinput) -> do + let reponame = genSshRepoName "rsync.net" + (maybe "" T.unpack $ inputDirectory sshinput) + makeRsyncNet sshinput reponame setupGroup + | otherwise -> + showform $ UnusableServer + "That is not a rsync.net host name." + _ -> showform UntestedServer + where + hostnamefield = textField `withExpandableNote` ("Help", help) + help = [whamlet| +

+ When you sign up for a Rsync.net account, you should receive an # + email from them with the host name and user name to put here. +
+ The host name will be something like "usw-s001.rsync.net", and the # + user name something like "7491" +|] + +makeRsyncNet :: SshInput -> String -> (Remote -> Handler ()) -> Handler Html +makeRsyncNet sshinput reponame setup = do + knownhost <- liftIO $ maybe (return False) knownHost (inputHostname sshinput) + keypair <- liftIO $ genSshKeyPair + sshdata <- liftIO $ setupSshKeyPair keypair $ + (mkSshData sshinput) + { sshRepoName = reponame + , needsPubKey = True + , rsyncOnly = True + } + {- I'd prefer to separate commands with && , but + - rsync.net's shell does not support that. + - + - The dd method of appending to the authorized_keys file is the + - one recommended by rsync.net documentation. I touch the file first + - to not need to use a different method to create it. + -} + let remotecommand = intercalate ";" + [ "mkdir -p .ssh" + , "touch .ssh/authorized_keys" + , "dd of=.ssh/authorized_keys oflag=append conv=notrunc" + , "mkdir -p " ++ T.unpack (sshDirectory sshdata) + ] + let sshopts = filter (not . null) + [ if knownhost then "" else sshOpt "StrictHostKeyChecking" "no" + , genSshHost (sshHostName sshdata) (sshUserName sshdata) + , remotecommand + ] + sshSetup sshopts (sshPubKey keypair) $ + makeSshRepo True setup sshdata + +isRsyncNet :: Maybe Text -> Bool +isRsyncNet Nothing = False +isRsyncNet (Just host) = ".rsync.net" `T.isSuffixOf` T.toLower host + +setupGroup :: Remote -> Handler () +setupGroup r = liftAnnex $ setStandardGroup (Remote.uuid r) TransferGroup diff --git a/Assistant/WebApp/Configurators/WebDAV.hs b/Assistant/WebApp/Configurators/WebDAV.hs new file mode 100644 index 0000000000..027abdf78d --- /dev/null +++ b/Assistant/WebApp/Configurators/WebDAV.hs @@ -0,0 +1,147 @@ +{- git-annex assistant webapp configurators for WebDAV remotes + - + - Copyright 2012, 2013 Joey Hess + - + - Licensed under the GNU AGPL version 3 or higher. + -} + +{-# LANGUAGE CPP, QuasiQuotes, TemplateHaskell, OverloadedStrings #-} + +module Assistant.WebApp.Configurators.WebDAV where + +import Assistant.WebApp.Common +import Creds +#ifdef WITH_WEBDAV +import qualified Remote.WebDAV as WebDAV +import Assistant.MakeRemote +import Assistant.Sync +import qualified Remote +import Types.Remote (RemoteConfig) +import Types.StandardGroups +import Logs.PreferredContent +import Logs.Remote + +import qualified Data.Map as M +#endif +import qualified Data.Text as T +import Network.URI + +webDAVConfigurator :: Widget -> Handler Html +webDAVConfigurator = page "Add a WebDAV repository" (Just Configuration) + +boxConfigurator :: Widget -> Handler Html +boxConfigurator = page "Add a Box.com repository" (Just Configuration) + +data WebDAVInput = WebDAVInput + { user :: Text + , password :: Text + , embedCreds :: Bool + , directory :: Text + , enableEncryption :: EnableEncryption + } + +toCredPair :: WebDAVInput -> CredPair +toCredPair input = (T.unpack $ user input, T.unpack $ password input) + +boxComAForm :: Maybe CredPair -> MkAForm WebDAVInput +boxComAForm defcreds = WebDAVInput + <$> areq textField "Username or Email" (T.pack . fst <$> defcreds) + <*> areq passwordField "Box.com Password" (T.pack . snd <$> defcreds) + <*> areq checkBoxField "Share this account with other devices and friends?" (Just True) + <*> areq textField "Directory" (Just "annex") + <*> enableEncryptionField + +webDAVCredsAForm :: Maybe CredPair -> MkAForm WebDAVInput +webDAVCredsAForm defcreds = WebDAVInput + <$> areq textField "Username or Email" (T.pack . fst <$> defcreds) + <*> areq passwordField "Password" (T.pack . snd <$> defcreds) + <*> pure False + <*> pure T.empty + <*> pure NoEncryption -- not used! + +getAddBoxComR :: Handler Html +getAddBoxComR = postAddBoxComR +postAddBoxComR :: Handler Html +#ifdef WITH_WEBDAV +postAddBoxComR = boxConfigurator $ do + defcreds <- liftAnnex $ previouslyUsedWebDAVCreds "box.com" + ((result, form), enctype) <- liftH $ + runFormPost $ renderBootstrap $ boxComAForm defcreds + case result of + FormSuccess input -> liftH $ + makeWebDavRemote initSpecialRemote "box.com" (toCredPair input) setgroup $ M.fromList + [ configureEncryption $ enableEncryption input + , ("embedcreds", if embedCreds input then "yes" else "no") + , ("type", "webdav") + , ("url", "https://www.box.com/dav/" ++ T.unpack (directory input)) + -- Box.com has a max file size of 100 mb, but + -- using smaller chunks has better memory + -- performance. + , ("chunksize", "10mb") + ] + _ -> $(widgetFile "configurators/addbox.com") + where + setgroup r = liftAnnex $ + setStandardGroup (Remote.uuid r) TransferGroup +#else +postAddBoxComR = error "WebDAV not supported by this build" +#endif + +getEnableWebDAVR :: UUID -> Handler Html +getEnableWebDAVR = postEnableWebDAVR +postEnableWebDAVR :: UUID -> Handler Html +#ifdef WITH_WEBDAV +postEnableWebDAVR uuid = do + 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 <- liftAnnex $ + getRemoteCredPairFor "webdav" c (WebDAV.davCreds uuid) + case mcreds of + Just creds -> webDAVConfigurator $ liftH $ + makeWebDavRemote enableSpecialRemote name creds (const noop) M.empty + Nothing + | "box.com/" `isInfixOf` url -> + boxConfigurator $ showform name url + | otherwise -> + webDAVConfigurator $ showform name url + where + showform name url = do + defcreds <- liftAnnex $ + maybe (pure Nothing) previouslyUsedWebDAVCreds $ + urlHost url + ((result, form), enctype) <- liftH $ + runFormPost $ renderBootstrap $ webDAVCredsAForm defcreds + case result of + FormSuccess input -> liftH $ + makeWebDavRemote enableSpecialRemote name (toCredPair input) (const noop) M.empty + _ -> do + description <- liftAnnex $ + T.pack <$> Remote.prettyUUID uuid + $(widgetFile "configurators/enablewebdav") +#else +postEnableWebDAVR _ = error "WebDAV not supported by this build" +#endif + +#ifdef WITH_WEBDAV +makeWebDavRemote :: SpecialRemoteMaker -> String -> CredPair -> (Remote -> Handler ()) -> RemoteConfig -> Handler () +makeWebDavRemote maker name creds setup config = do + liftIO $ WebDAV.setCredsEnv creds + r <- liftAnnex $ addRemote $ maker name WebDAV.remote config + setup r + liftAssistant $ syncRemote r + redirect $ EditNewCloudRepositoryR $ Remote.uuid r + +{- Only returns creds previously used for the same hostname. -} +previouslyUsedWebDAVCreds :: String -> Annex (Maybe CredPair) +previouslyUsedWebDAVCreds hostname = + previouslyUsedCredPair WebDAV.davCreds WebDAV.remote samehost + where + samehost url = case urlHost =<< WebDAV.configUrl url of + Nothing -> False + Just h -> h == hostname +#endif + +urlHost :: String -> Maybe String +urlHost url = uriRegName <$> (uriAuthority =<< parseURI url) diff --git a/Assistant/WebApp/Configurators/XMPP.hs b/Assistant/WebApp/Configurators/XMPP.hs new file mode 100644 index 0000000000..d2bc8d8a55 --- /dev/null +++ b/Assistant/WebApp/Configurators/XMPP.hs @@ -0,0 +1,220 @@ +{- git-annex assistant XMPP configuration + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU AGPL version 3 or higher. + -} + +{-# LANGUAGE TypeFamilies, QuasiQuotes, TemplateHaskell, OverloadedStrings, FlexibleContexts #-} +{-# LANGUAGE CPP #-} + +module Assistant.WebApp.Configurators.XMPP where + +import Assistant.WebApp.Common +import Assistant.WebApp.Notifications +import Utility.NotificationBroadcaster +#ifdef WITH_XMPP +import qualified Remote +import Assistant.XMPP.Client +import Assistant.XMPP.Buddies +import Assistant.Types.Buddies +import Assistant.NetMessager +import Assistant.Alert +import Assistant.DaemonStatus +import Assistant.WebApp.RepoList +import Assistant.WebApp.Configurators +import Assistant.XMPP +#endif + +#ifdef WITH_XMPP +import Network.Protocol.XMPP +import Network +import qualified Data.Text as T +#endif + +{- Displays an alert suggesting to configure XMPP. -} +xmppNeeded :: Handler () +#ifdef WITH_XMPP +xmppNeeded = whenM (isNothing <$> liftAnnex getXMPPCreds) $ do + urlrender <- getUrlRender + void $ liftAssistant $ do + close <- asIO1 removeAlert + addAlert $ xmppNeededAlert $ AlertButton + { buttonLabel = "Configure a Jabber account" + , buttonUrl = urlrender XMPPConfigR + , buttonAction = Just close + } +#else +xmppNeeded = return () +#endif + +{- When appropriate, displays an alert suggesting to configure a cloud repo + - to suppliment an XMPP remote. -} +checkCloudRepos :: UrlRenderer -> Remote -> Assistant () +#ifdef WITH_XMPP +checkCloudRepos urlrenderer r = + unlessM (syncingToCloudRemote <$> getDaemonStatus) $ do + buddyname <- getBuddyName $ Remote.uuid r + button <- mkAlertButton "Add a cloud repository" urlrenderer $ + NeedCloudRepoR $ Remote.uuid r + void $ addAlert $ cloudRepoNeededAlert buddyname button +#else +checkCloudRepos _ _ = noop +#endif + +#ifdef WITH_XMPP +{- Returns the name of the friend corresponding to a + - repository's UUID, but not if it's our name. -} +getBuddyName :: UUID -> Assistant (Maybe String) +getBuddyName u = go =<< getclientjid + where + go Nothing = return Nothing + go (Just myjid) = (T.unpack . buddyName <$>) + . headMaybe + . filter (\j -> baseJID j /= baseJID myjid) + . map fst + . filter (\(_, r) -> Remote.uuid r == u) + <$> getXMPPRemotes + getclientjid = maybe Nothing parseJID . xmppClientID + <$> getDaemonStatus +#endif + +getNeedCloudRepoR :: UUID -> Handler Html +#ifdef WITH_XMPP +getNeedCloudRepoR for = page "Cloud repository needed" (Just Configuration) $ do + buddyname <- liftAssistant $ getBuddyName for + $(widgetFile "configurators/xmpp/needcloudrepo") +#else +getNeedCloudRepoR _ = xmppPage $ + $(widgetFile "configurators/xmpp/disabled") +#endif + +getXMPPConfigR :: Handler Html +getXMPPConfigR = postXMPPConfigR + +postXMPPConfigR :: Handler Html +postXMPPConfigR = xmppform DashboardR + +getXMPPConfigForPairFriendR :: Handler Html +getXMPPConfigForPairFriendR = postXMPPConfigForPairFriendR + +postXMPPConfigForPairFriendR :: Handler Html +postXMPPConfigForPairFriendR = xmppform StartXMPPPairFriendR + +getXMPPConfigForPairSelfR :: Handler Html +getXMPPConfigForPairSelfR = postXMPPConfigForPairSelfR + +postXMPPConfigForPairSelfR :: Handler Html +postXMPPConfigForPairSelfR = xmppform StartXMPPPairSelfR + +xmppform :: Route WebApp -> Handler Html +#ifdef WITH_XMPP +xmppform next = xmppPage $ do + ((result, form), enctype) <- liftH $ do + oldcreds <- liftAnnex getXMPPCreds + runFormPost $ renderBootstrap $ xmppAForm $ + creds2Form <$> oldcreds + let showform problem = $(widgetFile "configurators/xmpp") + case result of + FormSuccess f -> either (showform . Just) (liftH . storecreds) + =<< liftIO (validateForm f) + _ -> showform Nothing + where + storecreds creds = do + void $ liftAnnex $ setXMPPCreds creds + liftAssistant notifyNetMessagerRestart + redirect next +#else +xmppform _ = xmppPage $ + $(widgetFile "configurators/xmpp/disabled") +#endif + +{- Called by client to get a list of buddies. + - + - Returns a div, which will be inserted into the calling page. + -} +getBuddyListR :: NotificationId -> Handler Html +getBuddyListR nid = do + waitNotifier getBuddyListBroadcaster nid + + p <- widgetToPageContent buddyListDisplay + giveUrlRenderer $ [hamlet|^{pageBody p}|] + +buddyListDisplay :: Widget +buddyListDisplay = do + autoUpdate ident NotifierBuddyListR (10 :: Int) (10 :: Int) +#ifdef WITH_XMPP + myjid <- liftAssistant $ xmppClientID <$> getDaemonStatus + let isself (BuddyKey b) = Just b == myjid + buddies <- liftAssistant $ do + pairedwith <- map fst <$> getXMPPRemotes + catMaybes . map (buddySummary pairedwith) + <$> (getBuddyList <<~ buddyList) + $(widgetFile "configurators/xmpp/buddylist") +#endif + where + ident = "buddylist" + +#ifdef WITH_XMPP + +getXMPPRemotes :: Assistant [(JID, Remote)] +getXMPPRemotes = catMaybes . map pair . filter isXMPPRemote . syncGitRemotes + <$> getDaemonStatus + where + pair r = maybe Nothing (\jid -> Just (jid, r)) $ + parseJID $ getXMPPClientID r + +data XMPPForm = XMPPForm + { formJID :: Text + , formPassword :: Text } + +creds2Form :: XMPPCreds -> XMPPForm +creds2Form c = XMPPForm (xmppJID c) (xmppPassword c) + +xmppAForm :: (Maybe XMPPForm) -> MkAForm XMPPForm +xmppAForm def = XMPPForm + <$> areq jidField "Jabber address" (formJID <$> def) + <*> areq passwordField "Password" Nothing + +jidField :: MkField Text +jidField = checkBool (isJust . parseJID) bad textField + where + bad :: Text + bad = "This should look like an email address.." + +validateForm :: XMPPForm -> IO (Either String XMPPCreds) +validateForm f = do + let jid = fromMaybe (error "bad JID") $ parseJID (formJID f) + let username = fromMaybe "" (strNode <$> jidNode jid) + testXMPP $ XMPPCreds + { xmppUsername = username + , xmppPassword = formPassword f + , xmppHostname = T.unpack $ strDomain $ jidDomain jid + , xmppPort = 5222 + , xmppJID = formJID f + } + +testXMPP :: XMPPCreds -> IO (Either String XMPPCreds) +testXMPP creds = do + (good, bad) <- partition (either (const False) (const True) . snd) + <$> connectXMPP creds (const noop) + case good of + (((h, PortNumber p), _):_) -> return $ Right $ creds + { xmppHostname = h + , xmppPort = fromIntegral p + } + (((h, _), _):_) -> return $ Right $ creds + { xmppHostname = h + } + _ -> return $ Left $ intercalate "; " $ map formatlog bad + where + formatlog ((h, p), Left e) = "host " ++ h ++ ":" ++ showport p ++ " failed: " ++ show e + formatlog _ = "" + + showport (PortNumber n) = show n + showport (Service s) = s + showport (UnixSocket s) = s +#endif + +xmppPage :: Widget -> Handler Html +xmppPage = page "Jabber" (Just Configuration) diff --git a/Assistant/WebApp/Control.hs b/Assistant/WebApp/Control.hs new file mode 100644 index 0000000000..7521c1e75b --- /dev/null +++ b/Assistant/WebApp/Control.hs @@ -0,0 +1,71 @@ +{- git-annex assistant webapp control + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU AGPL version 3 or higher. + -} + +{-# LANGUAGE CPP, TypeFamilies, QuasiQuotes, MultiParamTypeClasses, TemplateHaskell, OverloadedStrings, RankNTypes #-} + +module Assistant.WebApp.Control where + +import Assistant.WebApp.Common +import Config.Files +import Utility.LogFile +import Assistant.DaemonStatus +import Assistant.WebApp.Utility +import Assistant.Alert + +import Control.Concurrent +import System.Posix (getProcessID, signalProcess, sigTERM) +import qualified Data.Map as M + +getShutdownR :: Handler Html +getShutdownR = page "Shutdown" Nothing $ + $(widgetFile "control/shutdown") + +getShutdownConfirmedR :: Handler Html +getShutdownConfirmedR = do + {- Remove all alerts for currently running activities. -} + liftAssistant $ do + updateAlertMap $ M.filter $ \a -> alertClass a /= Activity + void $ addAlert shutdownAlert + {- Stop transfers the assistant is running, + - otherwise they would continue past shutdown. + - Pausing transfers prevents more being started up (and stops + - the transfer processes). -} + ts <- liftAssistant $ M.keys . currentTransfers <$> getDaemonStatus + mapM_ pauseTransfer ts + page "Shutdown" Nothing $ do + {- Wait 2 seconds before shutting down, to give the web + - page time to load in the browser. -} + void $ liftIO $ forkIO $ do + threadDelay 2000000 + signalProcess sigTERM =<< getProcessID + $(widgetFile "control/shutdownconfirmed") + +{- Quite a hack, and doesn't redirect the browser window. -} +getRestartR :: Handler Html +getRestartR = page "Restarting" Nothing $ do + void $ liftIO $ forkIO $ do + threadDelay 2000000 + program <- readProgramFile + unlessM (boolSystem "sh" [Param "-c", Param $ restartcommand program]) $ + error "restart failed" + $(widgetFile "control/restarting") + where + restartcommand program = program ++ " assistant --stop; exec " ++ + program ++ " webapp" + +getRestartThreadR :: ThreadName -> Handler () +getRestartThreadR name = do + m <- liftAssistant $ startedThreads <$> getDaemonStatus + liftIO $ maybe noop snd $ M.lookup name m + redirectBack + +getLogR :: Handler Html +getLogR = page "Logs" Nothing $ do + logfile <- 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 new file mode 100644 index 0000000000..1099f0cb0e --- /dev/null +++ b/Assistant/WebApp/DashBoard.hs @@ -0,0 +1,150 @@ +{- git-annex assistant webapp dashboard + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU AGPL version 3 or higher. + -} + +{-# LANGUAGE CPP, TypeFamilies, QuasiQuotes, MultiParamTypeClasses, TemplateHaskell, OverloadedStrings, RankNTypes #-} + +module Assistant.WebApp.DashBoard where + +import Assistant.WebApp.Common +import Assistant.WebApp.Utility +import Assistant.WebApp.RepoList +import Assistant.WebApp.Notifications +import Assistant.TransferQueue +import Assistant.DaemonStatus +import Utility.NotificationBroadcaster +import Logs.Transfer +import Utility.Percentage +import Utility.DataUnits +import Types.Key +import qualified Remote +import qualified Git + +import qualified Text.Hamlet as Hamlet +import qualified Data.Map as M +import Control.Concurrent + +{- A display of currently running and queued transfers. -} +transfersDisplay :: Bool -> Widget +transfersDisplay warnNoScript = do + webapp <- liftH getYesod + current <- liftH $ M.toList <$> getCurrentTransfers + queued <- take 10 <$> liftAssistant getTransferQueue + autoUpdate ident NotifierTransfersR (10 :: Int) (10 :: Int) + let transfers = simplifyTransfers $ current ++ queued + let transfersrunning = not $ null transfers + scanrunning <- if transfersrunning + then return False + else liftAssistant $ transferScanRunning <$> getDaemonStatus + $(widgetFile "dashboard/transfers") + where + ident = "transfers" + isrunning info = not $ + transferPaused info || isNothing (startedTime info) + +{- Simplifies a list of transfers, avoiding display of redundant + - equivilant transfers. -} +simplifyTransfers :: [(Transfer, TransferInfo)] -> [(Transfer, TransferInfo)] +simplifyTransfers [] = [] +simplifyTransfers (x:[]) = [x] +simplifyTransfers (v@(t1, _):r@((t2, _):l)) + | equivilantTransfer t1 t2 = simplifyTransfers (v:l) + | otherwise = v : (simplifyTransfers r) + +{- Called by client to get a display of currently in process transfers. + - + - Returns a div, which will be inserted into the calling page. + - + - 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 getDashboardR page. + -} +getTransfersR :: NotificationId -> Handler Html +getTransfersR nid = do + waitNotifier getTransferBroadcaster nid + + p <- widgetToPageContent $ transfersDisplay False + giveUrlRenderer $ [hamlet|^{pageBody p}|] + +{- The main dashboard. -} +dashboard :: Bool -> Widget +dashboard warnNoScript = do + let repolist = repoListDisplay $ + mainRepoSelector { nudgeAddMore = True } + let transferlist = transfersDisplay warnNoScript + $(widgetFile "dashboard/main") + +getDashboardR :: Handler Html +getDashboardR = ifM (inFirstRun) + ( redirect ConfigurationR + , page "" (Just DashBoard) $ dashboard True + ) + +{- Used to test if the webapp is running. -} +headDashboardR :: Handler () +headDashboardR = noop + +{- Same as DashboardR, except no autorefresh at all (and no noscript warning). -} +getNoScriptR :: Handler Html +getNoScriptR = page "" (Just DashBoard) $ dashboard False + +{- Same as DashboardR, except with autorefreshing via meta refresh. -} +getNoScriptAutoR :: Handler Html +getNoScriptAutoR = page "" (Just DashBoard) $ do + let ident = NoScriptR + let delayseconds = 3 :: Int + let this = NoScriptAutoR + toWidgetHead $(Hamlet.hamletFile $ hamletTemplate "dashboard/metarefresh") + dashboard False + +{- The javascript code does a post. -} +postFileBrowserR :: Handler () +postFileBrowserR = void openFileBrowser + +{- Used by non-javascript browsers, where clicking on the link actually + - opens this page, so we redirect back to the referrer. -} +getFileBrowserR :: Handler () +getFileBrowserR = whenM openFileBrowser $ redirectBack + +{- Opens the system file browser on the repo, or, as a fallback, + - goes to a file:// url. Returns True if it's ok to redirect away + - from the page (ie, the system file browser was opened). + - + - Note that the command is opened using a different thread, to avoid + - blocking the response to the browser on it. -} +openFileBrowser :: Handler Bool +openFileBrowser = do + path <- liftAnnex $ fromRepo Git.repoPath + ifM (liftIO $ inPath cmd <&&> inPath cmd) + ( do + void $ liftIO $ forkIO $ void $ + boolSystem cmd [Param path] + return True + , do + void $ redirect $ "file://" ++ path + return False + ) + where +#ifdef darwin_HOST_OS + cmd = "open" +#else + cmd = "xdg-open" +#endif + +{- Transfer controls. The GET is done in noscript mode and redirects back + - to the referring page. The POST is called by javascript. -} +getPauseTransferR :: Transfer -> Handler () +getPauseTransferR t = pauseTransfer t >> redirectBack +postPauseTransferR :: Transfer -> Handler () +postPauseTransferR t = pauseTransfer t +getStartTransferR :: Transfer -> Handler () +getStartTransferR t = startTransfer t >> redirectBack +postStartTransferR :: Transfer -> Handler () +postStartTransferR t = startTransfer t +getCancelTransferR :: Transfer -> Handler () +getCancelTransferR t = cancelTransfer False t >> redirectBack +postCancelTransferR :: Transfer -> Handler () +postCancelTransferR t = cancelTransfer False t diff --git a/Assistant/WebApp/Documentation.hs b/Assistant/WebApp/Documentation.hs new file mode 100644 index 0000000000..5bdb718510 --- /dev/null +++ b/Assistant/WebApp/Documentation.hs @@ -0,0 +1,42 @@ +{- git-annex assistant webapp documentation + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU AGPL version 3 or higher. + -} + +{-# LANGUAGE QuasiQuotes, TemplateHaskell, OverloadedStrings #-} + +module Assistant.WebApp.Documentation where + +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. -} +licenseFile :: IO (Maybe FilePath) +licenseFile = do + base <- standaloneAppBase + return $ ( "LICENSE") <$> base + +getAboutR :: Handler Html +getAboutR = page "About git-annex" (Just About) $ do + builtinlicense <- isJust <$> liftIO licenseFile + $(widgetFile "documentation/about") + +getLicenseR :: Handler Html +getLicenseR = do + v <- liftIO licenseFile + case v of + Nothing -> redirect AboutR + Just f -> customPage (Just About) $ do + -- no sidebar, just pages of legalese.. + setTitle "License" + license <- liftIO $ readFile f + $(widgetFile "documentation/license") + +getRepoGroupR :: Handler Html +getRepoGroupR = page "About repository groups" (Just About) $ do + $(widgetFile "documentation/repogroup") diff --git a/Assistant/WebApp/Form.hs b/Assistant/WebApp/Form.hs new file mode 100644 index 0000000000..90ce197e30 --- /dev/null +++ b/Assistant/WebApp/Form.hs @@ -0,0 +1,98 @@ +{- git-annex assistant webapp form utilities + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU AGPL version 3 or higher. + -} + +{-# LANGUAGE FlexibleContexts, TypeFamilies, QuasiQuotes #-} +{-# LANGUAGE MultiParamTypeClasses, TemplateHaskell #-} +{-# LANGUAGE OverloadedStrings, RankNTypes #-} +{-# LANGUAGE CPP #-} + +module Assistant.WebApp.Form where + +import Types.Remote (RemoteConfigKey) +import Assistant.WebApp.Types + +import Yesod hiding (textField, passwordField) +import Yesod.Form.Fields as F +import Data.Text (Text) + +{- Yesod's textField sets the required attribute for required fields. + - We don't want this, because many of the forms used in this webapp + - display a modal dialog when submitted, which interacts badly with + - required field handling by the browser. + - + - Required fields are still checked by Yesod. + -} +textField :: MkField Text +textField = F.textField + { fieldView = \theId name attrs val _isReq -> [whamlet| + +|] + } + +readonlyTextField :: MkField Text +readonlyTextField = F.textField + { fieldView = \theId name attrs val _isReq -> [whamlet| + +|] + } + +{- Also without required attribute. -} +passwordField :: MkField Text +passwordField = F.passwordField + { fieldView = \theId name attrs val _isReq -> toWidget [hamlet| + +|] + } + +{- Makes a note widget be displayed after a field. -} +#if MIN_VERSION_yesod(1,2,0) +withNote :: (Monad m, ToWidget (HandlerSite m) a) => Field m v -> a -> Field m v +#else +withNote :: Field sub master v -> GWidget sub master () -> Field sub master v +#endif +withNote field note = field { fieldView = newview } + where + newview theId name attrs val isReq = + let fieldwidget = (fieldView field) theId name attrs val isReq + in [whamlet|^{fieldwidget}  ^{note}|] + +{- Note that the toggle string must be unique on the form. -} +#if MIN_VERSION_yesod(1,2,0) +withExpandableNote :: (Monad m, ToWidget (HandlerSite m) w) => Field m v -> (String, w) -> Field m v +#else +withExpandableNote :: Field sub master v -> (String, GWidget sub master ()) -> Field sub master v +#endif +withExpandableNote field (toggle, note) = withNote field $ [whamlet| + + #{toggle} +
+ ^{note} +|] + where + ident = "toggle_" ++ toggle + +data EnableEncryption = SharedEncryption | NoEncryption + deriving (Eq) + +{- Adds a check box to an AForm to control encryption. -} +#if MIN_VERSION_yesod(1,2,0) +enableEncryptionField :: (RenderMessage site FormMessage) => AForm (HandlerT site IO) EnableEncryption +#else +enableEncryptionField :: RenderMessage master FormMessage => AForm sub master EnableEncryption +#endif +enableEncryptionField = areq (selectFieldList choices) "Encryption" (Just SharedEncryption) + where + choices :: [(Text, EnableEncryption)] + choices = + [ ("Encrypt all data", SharedEncryption) + , ("Disable encryption", NoEncryption) + ] + +{- Generates Remote configuration for encryption. -} +configureEncryption :: EnableEncryption -> (RemoteConfigKey, String) +configureEncryption SharedEncryption = ("encryption", "shared") +configureEncryption NoEncryption = ("encryption", "none") diff --git a/Assistant/WebApp/Notifications.hs b/Assistant/WebApp/Notifications.hs new file mode 100644 index 0000000000..b9da401781 --- /dev/null +++ b/Assistant/WebApp/Notifications.hs @@ -0,0 +1,95 @@ +{- git-annex assistant webapp notifications + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU AGPL version 3 or higher. + -} + +{-# LANGUAGE CPP, QuasiQuotes, TemplateHaskell, OverloadedStrings #-} + +#if defined VERSION_yesod_default +#if ! MIN_VERSION_yesod_default(1,1,0) +#define WITH_OLD_YESOD +#endif +#endif + +module Assistant.WebApp.Notifications where + +import Assistant.Common +import Assistant.WebApp +import Assistant.WebApp.Types +import Assistant.DaemonStatus +import Assistant.Types.Buddies +import Utility.NotificationBroadcaster +import Utility.Yesod + +import Data.Text (Text) +import qualified Data.Text as T +#ifndef WITH_OLD_YESOD +import qualified Data.Aeson.Types as Aeson +#endif + +{- Add to any widget to make it auto-update using long polling. + - + - The widget should have a html element with an id=ident, which will be + - replaced when it's updated. + - + - The geturl route should return the notifier url to use for polling. + - + - ms_delay is how long to delay between AJAX updates + - ms_startdelay is how long to delay before updating with AJAX at the start + -} +autoUpdate :: Text -> Route WebApp -> Int -> Int -> Widget +autoUpdate tident geturl ms_delay ms_startdelay = do +#ifdef WITH_OLD_YESOD + let delay = show ms_delay + let startdelay = show ms_startdelay + let ident = "'" ++ T.unpack tident ++ "'" +#else + let delay = Aeson.String (T.pack (show ms_delay)) + let startdelay = Aeson.String (T.pack (show ms_startdelay)) + let ident = Aeson.String tident +#endif + addScript $ StaticR longpolling_js + $(widgetFile "notifications/longpolling") + +{- Notifier urls are requested by the javascript, to avoid allocation + - of NotificationIds when noscript pages are loaded. This constructs a + - notifier url for a given Route and NotificationBroadcaster. + -} +notifierUrl :: (NotificationId -> Route WebApp) -> Assistant NotificationBroadcaster -> Handler RepPlain +notifierUrl route broadcaster = do + (urlbits, _params) <- renderRoute . route <$> newNotifier broadcaster + webapp <- getYesod + return $ RepPlain $ toContent $ T.concat + [ "/" + , T.intercalate "/" urlbits + , "?auth=" + , secretToken webapp + ] + +getNotifierTransfersR :: Handler RepPlain +getNotifierTransfersR = notifierUrl TransfersR getTransferBroadcaster + +getNotifierSideBarR :: Handler RepPlain +getNotifierSideBarR = notifierUrl SideBarR getAlertBroadcaster + +getNotifierBuddyListR :: Handler RepPlain +getNotifierBuddyListR = notifierUrl BuddyListR getBuddyListBroadcaster + +getNotifierRepoListR :: RepoSelector -> Handler RepPlain +getNotifierRepoListR reposelector = notifierUrl route getRepoListBroadcaster + where + route nid = RepoListR $ RepoListNotificationId nid reposelector + +getTransferBroadcaster :: Assistant NotificationBroadcaster +getTransferBroadcaster = transferNotifier <$> getDaemonStatus + +getAlertBroadcaster :: Assistant NotificationBroadcaster +getAlertBroadcaster = alertNotifier <$> getDaemonStatus + +getBuddyListBroadcaster :: Assistant NotificationBroadcaster +getBuddyListBroadcaster = getBuddyBroadcaster <$> getAssistant buddyList + +getRepoListBroadcaster :: Assistant NotificationBroadcaster +getRepoListBroadcaster = syncRemotesNotifier <$> getDaemonStatus diff --git a/Assistant/WebApp/OtherRepos.hs b/Assistant/WebApp/OtherRepos.hs new file mode 100644 index 0000000000..5219e8712f --- /dev/null +++ b/Assistant/WebApp/OtherRepos.hs @@ -0,0 +1,68 @@ +{- git-annex assistant webapp switching to other repos + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU AGPL version 3 or higher. + -} + +{-# LANGUAGE QuasiQuotes, TemplateHaskell, OverloadedStrings #-} + +module Assistant.WebApp.OtherRepos where + +import Assistant.Common +import Assistant.WebApp.Types +import Assistant.WebApp.Page +import qualified Git.Construct +import qualified Git.Config +import Config.Files +import qualified Utility.Url as Url +import Utility.Yesod + +import Control.Concurrent +import System.Process (cwd) + +getRepositorySwitcherR :: Handler Html +getRepositorySwitcherR = page "Switch repository" Nothing $ do + repolist <- liftIO listOtherRepos + $(widgetFile "control/repositoryswitcher") + +listOtherRepos :: IO [(String, String)] +listOtherRepos = do + dirs <- readAutoStartFile + pwd <- getCurrentDirectory + gooddirs <- filterM doesDirectoryExist $ + filter (\d -> not $ d `dirContains` pwd) dirs + names <- mapM relHome gooddirs + return $ sort $ zip names gooddirs + +{- Starts up the assistant in the repository, and waits for it to create + - a gitAnnexUrlFile. Waits for the assistant to be up and listening for + - connections by testing the url. Once it's running, redirect to it. + -} +getSwitchToRepositoryR :: FilePath -> Handler Html +getSwitchToRepositoryR repo = do + liftIO $ startAssistant repo + liftIO $ addAutoStartFile repo -- make this the new default repo + redirect =<< liftIO geturl + where + geturl = do + r <- Git.Config.read =<< Git.Construct.fromPath repo + waiturl $ gitAnnexUrlFile r + waiturl urlfile = do + v <- tryIO $ readFile urlfile + case v of + Left _ -> delayed $ waiturl urlfile + Right url -> ifM (listening url) + ( return url + , delayed $ waiturl urlfile + ) + listening url = catchBoolIO $ fst <$> Url.exists url [] + delayed a = do + threadDelay 100000 -- 1/10th of a second + a + +startAssistant :: FilePath -> IO () +startAssistant repo = do + program <- readProgramFile + void $ forkIO $ void $ createProcess $ + (proc program ["assistant"]) { cwd = Just repo } diff --git a/Assistant/WebApp/Page.hs b/Assistant/WebApp/Page.hs new file mode 100644 index 0000000000..8a6828d587 --- /dev/null +++ b/Assistant/WebApp/Page.hs @@ -0,0 +1,69 @@ +{- git-annex assistant webapp page display + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU AGPL version 3 or higher. + -} + +{-# LANGUAGE TypeFamilies, QuasiQuotes, MultiParamTypeClasses, TemplateHaskell, OverloadedStrings, RankNTypes #-} + +module Assistant.WebApp.Page where + +import Assistant.Common +import Assistant.WebApp +import Assistant.WebApp.Types +import Assistant.WebApp.SideBar +import Utility.Yesod + +import qualified Text.Hamlet as Hamlet +import Data.Text (Text) + +data NavBarItem = DashBoard | Configuration | About + deriving (Eq, Ord, Enum, Bounded) + +navBarName :: NavBarItem -> Text +navBarName DashBoard = "Dashboard" +navBarName Configuration = "Configuration" +navBarName About = "About" + +navBarRoute :: NavBarItem -> Route WebApp +navBarRoute DashBoard = DashboardR +navBarRoute Configuration = ConfigurationR +navBarRoute About = AboutR + +defaultNavBar :: [NavBarItem] +defaultNavBar = [minBound .. maxBound] + +firstRunNavBar :: [NavBarItem] +firstRunNavBar = [Configuration, About] + +selectNavBar :: Handler [NavBarItem] +selectNavBar = ifM (inFirstRun) (return firstRunNavBar, return defaultNavBar) + +{- A standard page of the webapp, with a title, a sidebar, and that may + - be highlighted on the navbar. -} +page :: Hamlet.Html -> Maybe NavBarItem -> Widget -> Handler Html +page title navbaritem content = customPage navbaritem $ do + setTitle title + sideBarDisplay + content + +{- A custom page, with no title or sidebar set. -} +customPage :: Maybe NavBarItem -> Widget -> Handler Html +customPage navbaritem content = do + webapp <- getYesod + navbar <- map navdetails <$> selectNavBar + pageinfo <- widgetToPageContent $ do + addStylesheet $ StaticR css_bootstrap_css + addStylesheet $ StaticR css_bootstrap_responsive_css + addScript $ StaticR jquery_full_js + addScript $ StaticR js_bootstrap_dropdown_js + addScript $ StaticR js_bootstrap_modal_js + addScript $ StaticR js_bootstrap_collapse_js + $(widgetFile "page") + giveUrlRenderer $(Hamlet.hamletFile $ hamletTemplate "bootstrap") + where + navdetails i = (navBarName i, navBarRoute i, Just i == navbaritem) + +controlMenu :: Widget +controlMenu = $(widgetFile "controlmenu") diff --git a/Assistant/WebApp/RepoList.hs b/Assistant/WebApp/RepoList.hs new file mode 100644 index 0000000000..9b90a4d563 --- /dev/null +++ b/Assistant/WebApp/RepoList.hs @@ -0,0 +1,255 @@ +{- git-annex assistant webapp repository list + - + - Copyright 2012,2013 Joey Hess + - + - Licensed under the GNU AGPL version 3 or higher. + -} + +{-# LANGUAGE QuasiQuotes, TemplateHaskell, OverloadedStrings, CPP #-} + +module Assistant.WebApp.RepoList where + +import Assistant.WebApp.Common +import Assistant.DaemonStatus +import Assistant.WebApp.Notifications +import Assistant.WebApp.Utility +import Assistant.Ssh +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 Logs.Group +import Config +import Git.Config +import Assistant.Sync +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 + +data Actions + = DisabledRepoActions + { setupRepoLink :: Route WebApp } + | SyncingRepoActions + { setupRepoLink :: Route WebApp + , syncToggleLink :: Route WebApp + } + | NotSyncingRepoActions + { setupRepoLink :: Route WebApp + , syncToggleLink :: Route WebApp + } + | UnwantedRepoActions + { setupRepoLink :: 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 + } + +mkUnwantedRepoActions :: UUID -> Actions +mkUnwantedRepoActions u = UnwantedRepoActions + { setupRepoLink = EditRepositoryR u + } + +needsEnabled :: Actions -> Bool +needsEnabled (DisabledRepoActions _) = True +needsEnabled _ = False + +notSyncing :: Actions -> Bool +notSyncing (SyncingRepoActions _ _) = False +notSyncing _ = True + +notWanted :: Actions -> Bool +notWanted (UnwantedRepoActions _) = True +notWanted _ = False + +{- Called by client to get a list of repos, that refreshes + - when new repos are added. + - + - Returns a div, which will be inserted into the calling page. + -} +getRepoListR :: RepoListNotificationId -> Handler Html +getRepoListR (RepoListNotificationId nid reposelector) = do + waitNotifier getRepoListBroadcaster nid + p <- widgetToPageContent $ repoListDisplay reposelector + giveUrlRenderer $ [hamlet|^{pageBody p}|] + +mainRepoSelector :: RepoSelector +mainRepoSelector = RepoSelector + { onlyCloud = False + , onlyConfigured = False + , includeHere = True + , nudgeAddMore = False + } + +{- List of cloud repositories, configured and not. -} +cloudRepoList :: Widget +cloudRepoList = repoListDisplay $ RepoSelector + { onlyCloud = True + , onlyConfigured = False + , includeHere = False + , nudgeAddMore = False + } + +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 <- liftH $ repoList reposelector + let addmore = nudgeAddMore reposelector + let nootherrepos = length repolist < 2 + + $(widgetFile "repolist") + where + ident = "repolist" + unfinished uuid = uuid == NoUUID + +type RepoList = [(String, UUID, Actions)] + +{- A 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 . map Remote.uuid . syncRemotes + <$> liftAssistant getDaemonStatus + liftAnnex $ do + unwanted <- S.fromList + <$> filterM inUnwantedGroup (S.toList syncing) + rs <- filter selectedrepo . concat . Remote.byCost + <$> Remote.remoteList + let us = map Remote.uuid rs + let maker u + | u `S.member` unwanted = mkUnwantedRepoActions u + | u `S.member` syncing = mkSyncingRepoActions u + | otherwise = mkNotSyncingRepoActions u + let l = zip us $ map (maker . Remote.uuid) 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 selectedremote + . map (findinfo m) + <$> (trustExclude DeadTrusted $ M.keys m) + selectedrepo r + | Remote.readonly r = False + | onlyCloud reposelector = Git.repoIsUrl (Remote.repo r) && not (isXMPPRemote r) + | otherwise = True + selectedremote Nothing = False + selectedremote (Just (iscloud, _)) + | onlyCloud reposelector = iscloud + | otherwise = True + findinfo m u = case M.lookup "type" =<< M.lookup u m 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'' <- zip + <$> Remote.prettyListUUIDs (map fst l') + <*> pure l' + return $ map (\(name, (uuid, actions)) -> (name, uuid, actions)) l'' + +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 + redirectBack + +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[]" + liftAnnex $ go list =<< Remote.remoteFromUUID moved + liftAssistant updateSyncRemotes + where + go _ Nothing = noop + go list (Just remote) = do + rs <- catMaybes <$> mapM Remote.remoteFromUUID list + forM_ (reorderCosts remote rs) $ \(r, newcost) -> + when (Remote.cost r /= newcost) $ + setRemoteCost r newcost + void remoteListRefresh + 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' + +{- Checks to see if any repositories with NoUUID have annex-ignore set. + - That could happen if there's a problem contacting a ssh remote + - soon after it was added. -} +getCheckUnfinishedRepositoriesR :: Handler Html +getCheckUnfinishedRepositoriesR = page "Unfinished repositories" (Just Configuration) $ do + stalled <- liftAnnex findStalled + $(widgetFile "configurators/checkunfinished") + +findStalled :: Annex [Remote] +findStalled = filter isstalled <$> remoteListRefresh + where + isstalled r = Remote.uuid r == NoUUID + && remoteAnnexIgnore (Remote.gitconfig r) + +getRetryUnfinishedRepositoriesR :: Handler () +getRetryUnfinishedRepositoriesR = do + liftAssistant $ mapM_ unstall =<< liftAnnex findStalled + redirect DashboardR + where + unstall r = do + liftIO $ fixSshKeyPair + liftAnnex $ setConfig + (remoteConfig (Remote.repo r) "ignore") + (boolConfig False) + syncRemote r + liftAnnex $ void remoteListRefresh diff --git a/Assistant/WebApp/SideBar.hs b/Assistant/WebApp/SideBar.hs new file mode 100644 index 0000000000..7e977a67be --- /dev/null +++ b/Assistant/WebApp/SideBar.hs @@ -0,0 +1,104 @@ +{- git-annex assistant webapp sidebar + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU AGPL version 3 or higher. + -} + +{-# LANGUAGE QuasiQuotes, TemplateHaskell, OverloadedStrings #-} + +module Assistant.WebApp.SideBar where + +import Assistant.Common +import Assistant.WebApp +import Assistant.WebApp.Types +import Assistant.WebApp.Notifications +import Assistant.Alert.Utility +import Assistant.DaemonStatus +import Utility.NotificationBroadcaster +import Utility.Yesod + +import Data.Text (Text) +import qualified Data.Text as T +import qualified Data.Map as M +import Control.Concurrent + +sideBarDisplay :: Widget +sideBarDisplay = do + let content = do + {- Add newest alerts to the sidebar. -} + alertpairs <- liftH $ M.toList . alertMap + <$> liftAssistant getDaemonStatus + mapM_ renderalert $ + take displayAlerts $ reverse $ sortAlertPairs alertpairs + let ident = "sidebar" + $(widgetFile "sidebar/main") + autoUpdate ident NotifierSideBarR (10 :: Int) (10 :: Int) + where + bootstrapclass :: AlertClass -> Text + bootstrapclass Activity = "alert-info" + bootstrapclass Warning = "alert" + bootstrapclass Error = "alert-error" + bootstrapclass Success = "alert-success" + bootstrapclass Message = "alert-info" + + renderalert (aid, alert) = do + let alertid = show aid + let closable = alertClosable alert + let block = alertBlockDisplay alert + let divclass = bootstrapclass $ alertClass alert + let message = renderAlertMessage alert + let messagelines = T.lines message + let multiline = length messagelines > 1 + $(widgetFile "sidebar/alert") + +{- Called by client to get a sidebar display. + - + - Returns a div, which will be inserted into the calling page. + - + - 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 all pages. + -} +getSideBarR :: NotificationId -> Handler Html +getSideBarR nid = do + waitNotifier getAlertBroadcaster nid + + {- This 0.1 second delay avoids very transient notifications from + - being displayed and churning the sidebar unnecesarily. + - + - This needs to be below the level perceptable by the user, + - to avoid slowing down user actions like closing alerts. -} + liftIO $ threadDelay 100000 + + page <- widgetToPageContent sideBarDisplay + giveUrlRenderer $ [hamlet|^{pageBody page}|] + +{- Called by the client to close an alert. -} +getCloseAlert :: AlertId -> Handler () +getCloseAlert = liftAssistant . removeAlert + +{- When an alert with a button is clicked on, the button takes us here. -} +getClickAlert :: AlertId -> Handler () +getClickAlert i = do + m <- alertMap <$> liftAssistant getDaemonStatus + case M.lookup i m of + Just (Alert { alertButton = Just b }) -> do + {- Spawn a thread to run the action while redirecting. -} + case buttonAction b of + Nothing -> noop + Just a -> liftIO $ void $ forkIO $ a i + redirect $ buttonUrl b + _ -> redirectBack + +htmlIcon :: AlertIcon -> Widget +htmlIcon ActivityIcon = [whamlet||] +htmlIcon SyncIcon = [whamlet||] +htmlIcon InfoIcon = bootstrapIcon "info-sign" +htmlIcon SuccessIcon = bootstrapIcon "ok" +htmlIcon ErrorIcon = bootstrapIcon "exclamation-sign" +-- utf-8 umbrella (utf-8 cloud looks too stormy) +htmlIcon TheCloud = [whamlet|☂|] + +bootstrapIcon :: Text -> Widget +bootstrapIcon name = [whamlet||] diff --git a/Assistant/WebApp/Types.hs b/Assistant/WebApp/Types.hs new file mode 100644 index 0000000000..fd11d94d70 --- /dev/null +++ b/Assistant/WebApp/Types.hs @@ -0,0 +1,221 @@ +{- git-annex assistant webapp types + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU AGPL version 3 or higher. + -} + +{-# LANGUAGE TypeFamilies, QuasiQuotes, MultiParamTypeClasses #-} +{-# LANGUAGE TemplateHaskell, OverloadedStrings, RankNTypes #-} +{-# LANGUAGE FlexibleInstances, FlexibleContexts #-} +{-# LANGUAGE CPP #-} +{-# OPTIONS_GHC -fno-warn-orphans #-} + +module Assistant.WebApp.Types where + +import Assistant.Common +import Assistant.Ssh +import Assistant.Pairing +import Assistant.Types.Buddies +import Utility.NotificationBroadcaster +import Utility.WebApp +import Utility.Yesod +import Logs.Transfer +import Build.SysConfig (packageversion) + +import Yesod.Static +import Text.Hamlet +import Data.Text (Text, pack, unpack) +import Network.Socket (HostName) + +publicFiles "static" + +staticRoutes :: Static +staticRoutes = $(embed "static") + +mkYesodData "WebApp" $(parseRoutesFile "Assistant/WebApp/routes") + +data WebApp = WebApp + { assistantData :: AssistantData + , secretToken :: Text + , relDir :: Maybe FilePath + , getStatic :: Static + , postFirstRun :: Maybe (IO String) + , noAnnex :: Bool + , listenHost ::Maybe HostName + } + +instance Yesod WebApp where + {- Require an auth token be set when accessing any (non-static) route -} + isAuthorized _ _ = checkAuthToken secretToken + + {- Add the auth token to every url generated, except static subsite + - urls (which can show up in Permission Denied pages). -} + joinPath = insertAuthToken secretToken excludeStatic + where + excludeStatic [] = True + excludeStatic (p:_) = p /= "static" + + makeSessionBackend = webAppSessionBackend + jsLoader _ = BottomOfHeadBlocking + + {- The webapp does not use defaultLayout, so this is only used + - for error pages or any other built-in yesod page. + - + - This can use static routes, but should use no other routes, + - as that would expose the auth token. + -} + defaultLayout content = do + webapp <- getYesod + pageinfo <- widgetToPageContent $ do + addStylesheet $ StaticR css_bootstrap_css + addStylesheet $ StaticR css_bootstrap_responsive_css + $(widgetFile "error") + giveUrlRenderer $(hamletFile $ hamletTemplate "bootstrap") + +instance RenderMessage WebApp FormMessage where + renderMessage _ _ = defaultFormMessage + +{- Runs an Annex action from the webapp. + - + - When the webapp is run outside a git-annex repository, the fallback + - value is returned. + -} +#if MIN_VERSION_yesod(1,2,0) +liftAnnexOr :: forall a. a -> Annex a -> Handler a +#else +liftAnnexOr :: forall sub a. a -> Annex a -> GHandler sub WebApp a +#endif +liftAnnexOr fallback a = ifM (noAnnex <$> getYesod) + ( return fallback + , liftAssistant $ liftAnnex a + ) + +#if MIN_VERSION_yesod(1,2,0) +instance LiftAnnex Handler where +#else +instance LiftAnnex (GHandler sub WebApp) where +#endif + liftAnnex = liftAnnexOr $ error "internal liftAnnex" + +#if MIN_VERSION_yesod(1,2,0) +instance LiftAnnex (WidgetT WebApp IO) where +#else +instance LiftAnnex (GWidget WebApp WebApp) where +#endif + liftAnnex = liftH . liftAnnex + +class LiftAssistant m where + liftAssistant :: Assistant a -> m a + +#if MIN_VERSION_yesod(1,2,0) +instance LiftAssistant Handler where +#else +instance LiftAssistant (GHandler sub WebApp) where +#endif + liftAssistant a = liftIO . flip runAssistant a + =<< assistantData <$> getYesod + +#if MIN_VERSION_yesod(1,2,0) +instance LiftAssistant (WidgetT WebApp IO) where +#else +instance LiftAssistant (GWidget WebApp WebApp) where +#endif + liftAssistant = liftH . liftAssistant + +#if MIN_VERSION_yesod(1,2,0) +type MkMForm x = MForm Handler (FormResult x, Widget) +#else +type MkMForm x = MForm WebApp WebApp (FormResult x, Widget) +#endif + +#if MIN_VERSION_yesod(1,2,0) +type MkAForm x = AForm Handler x +#else +type MkAForm x = AForm WebApp WebApp x +#endif + +#if MIN_VERSION_yesod(1,2,0) +type MkField x = Monad m => RenderMessage (HandlerSite m) FormMessage => Field m x +#else +type MkField x = RenderMessage master FormMessage => Field sub master x +#endif + +data RepoSelector = RepoSelector + { onlyCloud :: Bool + , onlyConfigured :: Bool + , includeHere :: Bool + , nudgeAddMore :: Bool + } + deriving (Read, Show, Eq) + +data RepoListNotificationId = RepoListNotificationId NotificationId RepoSelector + deriving (Read, Show, Eq) + +data RemovableDrive = RemovableDrive + { diskFree :: Maybe Integer + , mountPoint :: Text + , driveRepoPath :: Text + } + deriving (Read, Show, Eq, Ord) + +{- 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 RemovableDrive where + toPathPiece = pack . show + fromPathPiece = readish . unpack + +instance PathPiece SshData where + toPathPiece = pack . show + fromPathPiece = readish . unpack + +instance PathPiece NotificationId where + toPathPiece = pack . show + fromPathPiece = readish . unpack + +instance PathPiece AlertId where + toPathPiece = pack . show + fromPathPiece = readish . unpack + +instance PathPiece Transfer where + toPathPiece = pack . show + fromPathPiece = readish . unpack + +instance PathPiece PairMsg where + toPathPiece = pack . show + fromPathPiece = readish . unpack + +instance PathPiece SecretReminder where + toPathPiece = pack . show + fromPathPiece = readish . unpack + +instance PathPiece UUID where + toPathPiece = pack . show + fromPathPiece = readish . unpack + +instance PathPiece BuddyKey where + toPathPiece = pack . show + fromPathPiece = readish . unpack + +instance PathPiece PairKey where + toPathPiece = pack . show + fromPathPiece = readish . unpack + +instance PathPiece RepoListNotificationId where + toPathPiece = pack . show + fromPathPiece = readish . unpack + +instance PathPiece RepoSelector where + toPathPiece = pack . show + fromPathPiece = readish . unpack + +instance PathPiece ThreadName where + toPathPiece = pack . show + fromPathPiece = readish . unpack diff --git a/Assistant/WebApp/Utility.hs b/Assistant/WebApp/Utility.hs new file mode 100644 index 0000000000..027fc26544 --- /dev/null +++ b/Assistant/WebApp/Utility.hs @@ -0,0 +1,120 @@ +{- git-annex assistant webapp utilities + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU AGPL version 3 or higher. + -} + +module Assistant.WebApp.Utility where + +import Assistant.Common +import Assistant.WebApp.Types +import Assistant.DaemonStatus +import Assistant.TransferQueue +import Assistant.Types.TransferSlots +import Assistant.TransferSlots +import Assistant.Sync +import qualified Remote +import qualified Types.Remote as Remote +import qualified Remote.List as Remote +import qualified Assistant.Threads.Transferrer as Transferrer +import Logs.Transfer +import qualified Config +import Config.Files +import Git.Config +import Assistant.Threads.Watcher +import Assistant.NamedThread + +import qualified Data.Map as M +import Control.Concurrent +import System.Posix.Signals (signalProcessGroup, sigTERM, sigKILL) +import System.Posix.Process (getProcessGroupIDOf) + +{- Use Nothing to change autocommit setting; or a remote to change + - its sync setting. -} +changeSyncable :: (Maybe Remote) -> Bool -> Handler () +changeSyncable Nothing enable = do + liftAnnex $ Config.setConfig key (boolConfig enable) + liftIO . maybe noop (`throwTo` signal) + =<< liftAssistant (namedThreadId watchThread) + where + key = Config.annexConfig "autocommit" + signal + | enable = ResumeWatcher + | otherwise = PauseWatcher +changeSyncable (Just r) True = do + changeSyncFlag r True + liftAssistant $ syncRemote r +changeSyncable (Just r) False = do + changeSyncFlag r False + liftAssistant $ updateSyncRemotes + {- Stop all transfers to or from this remote. + - XXX Can't stop any ongoing scan, or git syncs. -} + void $ liftAssistant $ dequeueTransfers tofrom + mapM_ (cancelTransfer False) =<< + filter tofrom . M.keys <$> + liftAssistant (currentTransfers <$> getDaemonStatus) + where + tofrom t = transferUUID t == Remote.uuid r + +changeSyncFlag :: Remote -> Bool -> Handler () +changeSyncFlag r enabled = liftAnnex $ do + Config.setConfig key (boolConfig enabled) + void $ Remote.remoteListRefresh + where + key = Config.remoteConfig (Remote.repo r) "sync" + +pauseTransfer :: Transfer -> Handler () +pauseTransfer = cancelTransfer True + +cancelTransfer :: Bool -> Transfer -> Handler () +cancelTransfer pause t = do + m <- getCurrentTransfers + unless pause $ + {- remove queued transfer -} + void $ liftAssistant $ dequeueTransfers $ equivilantTransfer t + {- stop running transfer -} + maybe noop stop (M.lookup t m) + where + stop info = liftAssistant $ do + {- When there's a thread associated with the + - transfer, it's signaled first, to avoid it + - displaying any alert about the transfer having + - failed when the transfer process is killed. -} + liftIO $ maybe noop signalthread $ transferTid info + liftIO $ maybe noop killproc $ transferPid info + if pause + then void $ alterTransferInfo t $ + \i -> i { transferPaused = True } + else void $ removeTransfer t + signalthread tid + | pause = throwTo tid PauseTransfer + | otherwise = killThread tid + {- In order to stop helper processes like rsync, + - kill the whole process group of the process running the transfer. -} + killproc pid = void $ tryIO $ do + g <- getProcessGroupIDOf pid + void $ tryIO $ signalProcessGroup sigTERM g + threadDelay 50000 -- 0.05 second grace period + void $ tryIO $ signalProcessGroup sigKILL g + +startTransfer :: Transfer -> Handler () +startTransfer t = do + m <- getCurrentTransfers + maybe startqueued go (M.lookup t m) + where + go info = maybe (start info) resume $ transferTid info + startqueued = do + is <- liftAssistant $ map snd <$> getMatchingTransfers (== t) + maybe noop start $ headMaybe is + resume tid = do + liftAssistant $ alterTransferInfo t $ + \i -> i { transferPaused = False } + liftIO $ throwTo tid ResumeTransfer + start info = liftAssistant $ do + program <- liftIO readProgramFile + inImmediateTransferSlot program $ + Transferrer.genTransfer t info + +getCurrentTransfers :: Handler TransferMap +getCurrentTransfers = currentTransfers <$> liftAssistant getDaemonStatus diff --git a/Assistant/WebApp/routes b/Assistant/WebApp/routes new file mode 100644 index 0000000000..60e7e1a936 --- /dev/null +++ b/Assistant/WebApp/routes @@ -0,0 +1,100 @@ +/ DashboardR GET HEAD + +/noscript NoScriptR GET +/noscript/auto NoScriptAutoR GET + +/about AboutR GET +/about/license LicenseR GET +/about/repogroups RepoGroupR GET + +/shutdown ShutdownR GET +/shutdown/confirm ShutdownConfirmedR GET +/restart RestartR GET +/restart/thread/#ThreadName RestartThreadR GET +/log LogR GET + +/config ConfigurationR GET +/config/preferences PreferencesR GET POST +/config/xmpp XMPPConfigR GET POST +/config/xmpp/for/self XMPPConfigForPairSelfR GET POST +/config/xmpp/for/frield XMPPConfigForPairFriendR GET POST +/config/xmpp/needcloudrepo/#UUID NeedCloudRepoR GET + +/config/addrepository AddRepositoryR GET +/config/repository/new NewRepositoryR GET POST +/config/repository/new/first FirstRepositoryR GET POST +/config/repository/new/androidcamera AndroidCameraRepositoryR GET +/config/repository/switcher RepositorySwitcherR GET +/config/repository/switchto/#FilePath SwitchToRepositoryR GET +/config/repository/combine/#FilePathAndUUID CombineRepositoryR GET +/config/repository/edit/#UUID EditRepositoryR GET POST +/config/repository/edit/new/#UUID EditNewRepositoryR GET POST +/config/repository/edit/new/cloud/#UUID EditNewCloudRepositoryR GET POST +/config/repository/sync/disable/#UUID DisableSyncR GET +/config/repository/sync/enable/#UUID EnableSyncR GET +/config/repository/unfinished/check CheckUnfinishedRepositoriesR GET +/config/repository/unfinished/retry RetryUnfinishedRepositoriesR GET + +/config/repository/add/drive AddDriveR GET POST +/config/repository/add/drive/confirm/#RemovableDrive ConfirmAddDriveR GET +/config/repository/add/drive/finish/#RemovableDrive FinishAddDriveR GET +/config/repository/add/ssh AddSshR GET POST +/config/repository/add/ssh/confirm/#SshData ConfirmSshR GET +/config/repository/add/ssh/retry/#SshData RetrySshR GET +/config/repository/add/ssh/make/git/#SshData MakeSshGitR GET +/config/repository/add/ssh/make/rsync/#SshData MakeSshRsyncR GET +/config/repository/add/cloud/rsync.net AddRsyncNetR GET POST +/config/repository/add/cloud/S3 AddS3R GET POST +/config/repository/add/cloud/IA AddIAR GET POST +/config/repository/add/cloud/glacier AddGlacierR GET POST +/config/repository/add/cloud/box.com AddBoxComR GET POST + +/config/repository/pair/local/start StartLocalPairR GET POST +/config/repository/pair/local/running/#SecretReminder RunningLocalPairR GET +/config/repository/pair/local/finish/#PairMsg FinishLocalPairR GET POST + +/config/repository/pair/xmpp/self/start StartXMPPPairSelfR GET +/config/repository/pair/xmpp/self/running RunningXMPPPairSelfR GET + +/config/repository/pair/xmpp/friend/start StartXMPPPairFriendR GET +/config/repository/pair/xmpp/friend/running/#BuddyKey RunningXMPPPairFriendR GET +/config/repository/pair/xmpp/friend/accept/#PairKey ConfirmXMPPPairFriendR GET +/config/repository/pair/xmpp/friend/finish/#PairKey FinishXMPPPairFriendR GET + +/config/repository/enable/rsync/#UUID EnableRsyncR GET POST +/config/repository/enable/directory/#UUID EnableDirectoryR GET +/config/repository/enable/S3/#UUID EnableS3R GET POST +/config/repository/enable/IA/#UUID EnableIAR GET POST +/config/repository/enable/glacier/#UUID EnableGlacierR GET POST +/config/repository/enable/webdav/#UUID EnableWebDAVR GET POST + +/config/repository/reorder RepositoriesReorderR GET + +/config/repository/disable/#UUID DisableRepositoryR GET + +/config/repository/delete/confirm/#UUID DeleteRepositoryR GET +/config/repository/delete/start/#UUID StartDeleteRepositoryR GET +/config/repository/delete/finish/#UUID FinishDeleteRepositoryR GET +/config/repository/delete/here DeleteCurrentRepositoryR GET POST + +/transfers/#NotificationId TransfersR GET +/notifier/transfers NotifierTransfersR GET + +/sidebar/#NotificationId SideBarR GET +/notifier/sidebar NotifierSideBarR GET + +/buddylist/#NotificationId BuddyListR GET +/notifier/buddylist NotifierBuddyListR GET + +/repolist/#RepoListNotificationId RepoListR GET +/notifier/repolist/#RepoSelector NotifierRepoListR GET + +/alert/close/#AlertId CloseAlert GET +/alert/click/#AlertId ClickAlert GET +/filebrowser FileBrowserR GET POST + +/transfer/pause/#Transfer PauseTransferR GET POST +/transfer/start/#Transfer StartTransferR GET POST +/transfer/cancel/#Transfer CancelTransferR GET POST + +/static StaticR Static getStatic diff --git a/Assistant/XMPP.hs b/Assistant/XMPP.hs new file mode 100644 index 0000000000..0360ce8603 --- /dev/null +++ b/Assistant/XMPP.hs @@ -0,0 +1,273 @@ +{- core xmpp support + - + - Copyright 2012-2013 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE OverloadedStrings #-} + +module Assistant.XMPP where + +import Assistant.Common +import Assistant.Types.NetMessager +import Assistant.Pairing +import Git.Sha (extractSha) + +import Network.Protocol.XMPP hiding (Node) +import Data.Text (Text) +import qualified Data.Text as T +import qualified Data.Map as M +import Data.ByteString (ByteString) +import qualified Data.ByteString as B +import Data.XML.Types +import qualified Codec.Binary.Base64 as B64 + +{- Name of the git-annex tag, in our own XML namespace. + - (Not using a namespace URL to avoid unnecessary bloat.) -} +gitAnnexTagName :: Name +gitAnnexTagName = "{git-annex}git-annex" + +{- Creates a git-annex tag containing a particular attribute and value. -} +gitAnnexTag :: Name -> Text -> Element +gitAnnexTag attr val = gitAnnexTagContent attr val [] + +{- Also with some content. -} +gitAnnexTagContent :: Name -> Text -> [Node] -> Element +gitAnnexTagContent attr val = Element gitAnnexTagName [(attr, [ContentText val])] + +isGitAnnexTag :: Element -> Bool +isGitAnnexTag t = elementName t == gitAnnexTagName + +{- Things that a git-annex tag can inserted into. -} +class GitAnnexTaggable a where + insertGitAnnexTag :: a -> Element -> a + + extractGitAnnexTag :: a -> Maybe Element + + hasGitAnnexTag :: a -> Bool + hasGitAnnexTag = isJust . extractGitAnnexTag + +instance GitAnnexTaggable Message where + insertGitAnnexTag m elt = m { messagePayloads = elt : messagePayloads m } + 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 } + extractGitAnnexTag = headMaybe . filter isGitAnnexTag . presencePayloads + +data GitAnnexTagInfo = GitAnnexTagInfo + { tagAttr :: Name + , tagValue :: Text + , tagElement :: Element + } + +type Decoder = Message -> GitAnnexTagInfo -> Maybe NetMessage + +gitAnnexTagInfo :: GitAnnexTaggable a => a -> Maybe GitAnnexTagInfo +gitAnnexTagInfo v = case extractGitAnnexTag v of + {- Each git-annex tag has a single attribute. -} + Just (tag@(Element _ [(attr, _)] _)) -> GitAnnexTagInfo + <$> pure attr + <*> attributeText attr tag + <*> pure tag + _ -> Nothing + +{- A presence with a git-annex tag in it. + - Also includes a status tag, which may be visible in XMPP clients. -} +gitAnnexPresence :: Element -> Presence +gitAnnexPresence = insertGitAnnexTag $ addStatusTag $ emptyPresence PresenceAvailable + where + addStatusTag p = p + { presencePayloads = status : presencePayloads p } + status = Element "status" [] [statusMessage] + statusMessage = NodeContent $ ContentText $ T.pack "git-annex" + +{- A presence with an empty git-annex tag in it, used for letting other + - clients know we're around and are a git-annex client. -} +gitAnnexSignature :: Presence +gitAnnexSignature = gitAnnexPresence $ Element gitAnnexTagName [] [] + +{- XMPP client to server ping -} +xmppPing :: JID -> IQ +xmppPing selfjid = (emptyIQ IQGet) + { iqID = Just "c2s1" + , iqFrom = Just selfjid + , iqTo = Just $ JID Nothing (jidDomain selfjid) Nothing + , iqPayload = Just $ Element xmppPingTagName [] [] + } + +xmppPingTagName :: Name +xmppPingTagName = "{urn:xmpp}ping" + +{- A message with a git-annex tag in it. -} +gitAnnexMessage :: Element -> JID -> JID -> Message +gitAnnexMessage elt tojid fromjid = (insertGitAnnexTag silentMessage elt) + { messageTo = Just tojid + , messageFrom = Just fromjid + } + +{- A notification that we've pushed to some repositories, listing their + - UUIDs. -} +pushNotification :: [UUID] -> Presence +pushNotification = gitAnnexPresence . gitAnnexTag pushAttr . encodePushNotification + +encodePushNotification :: [UUID] -> Text +encodePushNotification = T.intercalate uuidSep . map (T.pack . fromUUID) + +decodePushNotification :: Text -> [UUID] +decodePushNotification = map (toUUID . T.unpack) . T.splitOn uuidSep + +uuidSep :: Text +uuidSep = "," + +{- A request for other git-annex clients to send presence. -} +presenceQuery :: Presence +presenceQuery = gitAnnexPresence $ gitAnnexTag queryAttr T.empty + +{- A notification about a stage of pairing. -} +pairingNotification :: PairStage -> UUID -> JID -> JID -> Message +pairingNotification pairstage u = gitAnnexMessage $ + gitAnnexTag pairAttr $ encodePairingNotification pairstage u + +encodePairingNotification :: PairStage -> UUID -> Text +encodePairingNotification pairstage u = T.unwords $ map T.pack + [ show pairstage + , fromUUID u + ] + +decodePairingNotification :: Decoder +decodePairingNotification m = parse . words . T.unpack . tagValue + where + parse [stage, u] = PairingNotification + <$> readish stage + <*> (formatJID <$> messageFrom m) + <*> pure (toUUID u) + parse _ = Nothing + +pushMessage :: PushStage -> JID -> JID -> Message +pushMessage = gitAnnexMessage . encode + where + encode (CanPush u shas) = + gitAnnexTag canPushAttr $ T.pack $ unwords $ + fromUUID u : map show shas + encode (PushRequest u) = + gitAnnexTag pushRequestAttr $ T.pack $ fromUUID u + encode (StartingPush u) = + gitAnnexTag startingPushAttr $ T.pack $ fromUUID u + encode (ReceivePackOutput n b) = + gitAnnexTagContent receivePackAttr (val n) $ encodeTagContent b + encode (SendPackOutput n b) = + gitAnnexTagContent sendPackAttr (val n) $ encodeTagContent b + encode (ReceivePackDone code) = + gitAnnexTag receivePackDoneAttr $ val $ encodeExitCode code + val = T.pack . show + +decodeMessage :: Message -> Maybe NetMessage +decodeMessage m = decode =<< gitAnnexTagInfo m + where + decode i = M.lookup (tagAttr i) decoders >>= rundecoder i + rundecoder i d = d m i + decoders = M.fromList $ zip + [ pairAttr + , canPushAttr + , pushRequestAttr + , startingPushAttr + , receivePackAttr + , sendPackAttr + , receivePackDoneAttr + ] + [ decodePairingNotification + , pushdecoder $ shasgen CanPush + , pushdecoder $ gen PushRequest + , pushdecoder $ gen StartingPush + , pushdecoder $ seqgen ReceivePackOutput + , pushdecoder $ seqgen SendPackOutput + , pushdecoder $ + fmap (ReceivePackDone . decodeExitCode) . readish . + T.unpack . tagValue + ] + pushdecoder a m' i = Pushing + <$> (formatJID <$> messageFrom m') + <*> a i + gen c i = c . toUUID <$> headMaybe (words (T.unpack (tagValue i))) + seqgen c i = do + packet <- decodeTagContent $ tagElement i + let seqnum = fromMaybe 0 $ readish $ T.unpack $ tagValue i + return $ c seqnum packet + shasgen c i = do + let (u:shas) = words $ T.unpack $ tagValue i + return $ c (toUUID u) (mapMaybe extractSha shas) + +decodeExitCode :: Int -> ExitCode +decodeExitCode 0 = ExitSuccess +decodeExitCode n = ExitFailure n + +encodeExitCode :: ExitCode -> Int +encodeExitCode ExitSuccess = 0 +encodeExitCode (ExitFailure n) = n + +{- Base 64 encoding a ByteString to use as the content of a tag. -} +encodeTagContent :: ByteString -> [Node] +encodeTagContent b = [NodeContent $ ContentText $ T.pack $ B64.encode $ B.unpack b] + +decodeTagContent :: Element -> Maybe ByteString +decodeTagContent elt = B.pack <$> B64.decode s + where + s = T.unpack $ T.concat $ elementText elt + +{- The JID without the client part. -} +baseJID :: JID -> JID +baseJID j = JID (jidNode j) (jidDomain j) Nothing + +{- An XMPP chat message with an empty body. This should not be displayed + - by clients, but can be used for communications. -} +silentMessage :: Message +silentMessage = (emptyMessage MessageChat) + { messagePayloads = [ emptybody ] } + where + emptybody = Element + { elementName = "body" + , 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 +negativePriority = Element "priority" [] [NodeContent $ ContentText "-1"] + +pushAttr :: Name +pushAttr = "push" + +queryAttr :: Name +queryAttr = "query" + +pairAttr :: Name +pairAttr = "pair" + +canPushAttr :: Name +canPushAttr = "canpush" + +pushRequestAttr :: Name +pushRequestAttr = "pushrequest" + +startingPushAttr :: Name +startingPushAttr = "startingpush" + +receivePackAttr :: Name +receivePackAttr = "rp" + +sendPackAttr :: Name +sendPackAttr = "sp" + +receivePackDoneAttr :: Name +receivePackDoneAttr = "rpdone" + +shasAttr :: Name +shasAttr = "shas" diff --git a/Assistant/XMPP/Buddies.hs b/Assistant/XMPP/Buddies.hs new file mode 100644 index 0000000000..0c466e51c9 --- /dev/null +++ b/Assistant/XMPP/Buddies.hs @@ -0,0 +1,87 @@ +{- xmpp buddies + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Assistant.XMPP.Buddies where + +import Assistant.XMPP +import Common.Annex +import Assistant.Types.Buddies + +import Network.Protocol.XMPP +import qualified Data.Map as M +import qualified Data.Set as S +import Data.Text (Text) +import qualified Data.Text as T + +genBuddyKey :: JID -> BuddyKey +genBuddyKey j = BuddyKey $ formatJID $ baseJID j + +buddyName :: JID -> Text +buddyName j = maybe (T.pack "") strNode (jidNode j) + +ucFirst :: Text -> Text +ucFirst s = let (first, rest) = T.splitAt 1 s + in T.concat [T.toUpper first, rest] + +{- Summary of info about a buddy. + - + - If the buddy has no clients at all anymore, returns Nothing. -} +buddySummary :: [JID] -> Buddy -> Maybe (Text, Bool, Bool, Bool, BuddyKey) +buddySummary pairedwith b = case clients of + ((Client j):_) -> Just (buddyName j, away, canpair, alreadypaired j, genBuddyKey j) + [] -> Nothing + where + away = S.null (buddyPresent b) && S.null (buddyAssistants b) + canpair = not $ S.null (buddyAssistants b) + clients = S.toList $ buddyPresent b `S.union` buddyAway b `S.union` buddyAssistants b + alreadypaired j = baseJID j `elem` pairedwith + +{- Updates the buddies with XMPP presence info. -} +updateBuddies :: Presence -> Buddies -> Buddies +updateBuddies p@(Presence { presenceFrom = Just jid }) = M.alter update key + where + key = genBuddyKey jid + update (Just b) = Just $ applyPresence p b + update Nothing = newBuddy p +updateBuddies _ = id + +{- Creates a new buddy based on XMPP presence info. -} +newBuddy :: Presence -> Maybe Buddy +newBuddy p + | presenceType p == PresenceAvailable = go + | presenceType p == PresenceUnavailable = go + | otherwise = Nothing + where + go = make <$> presenceFrom p + make _jid = applyPresence p $ Buddy + { buddyPresent = S.empty + , buddyAway = S.empty + , buddyAssistants = S.empty + , buddyPairing = False + } + +applyPresence :: Presence -> Buddy -> Buddy +applyPresence p b = fromMaybe b $! go <$> presenceFrom p + where + go jid + | presenceType p == PresenceUnavailable = b + { buddyAway = addto $ buddyAway b + , buddyPresent = removefrom $ buddyPresent b + , buddyAssistants = removefrom $ buddyAssistants b + } + | hasGitAnnexTag p = b + { buddyAssistants = addto $ buddyAssistants b + , buddyAway = removefrom $ buddyAway b } + | presenceType p == PresenceAvailable = b + { buddyPresent = addto $ buddyPresent b + , buddyAway = removefrom $ buddyAway b + } + | otherwise = b + where + client = Client jid + removefrom = S.filter (/= client) + addto = S.insert client diff --git a/Assistant/XMPP/Client.hs b/Assistant/XMPP/Client.hs new file mode 100644 index 0000000000..677bb2ff31 --- /dev/null +++ b/Assistant/XMPP/Client.hs @@ -0,0 +1,84 @@ +{- xmpp client support + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Assistant.XMPP.Client where + +import Assistant.Common +import Utility.SRV +import Creds + +import Network.Protocol.XMPP +import Network +import Control.Concurrent +import qualified Data.Text as T +import Control.Exception (SomeException) + +{- Everything we need to know to connect to an XMPP server. -} +data XMPPCreds = XMPPCreds + { xmppUsername :: T.Text + , xmppPassword :: T.Text + , xmppHostname :: HostName + , xmppPort :: Int + , xmppJID :: T.Text + } + deriving (Read, Show) + +connectXMPP :: XMPPCreds -> (JID -> XMPP a) -> IO [(HostPort, Either SomeException ())] +connectXMPP c a = case parseJID (xmppJID c) of + Nothing -> error "bad JID" + Just jid -> connectXMPP' jid c a + +{- Do a SRV lookup, but if it fails, fall back to the cached xmppHostname. -} +connectXMPP' :: JID -> XMPPCreds -> (JID -> XMPP a) -> IO [(HostPort, Either SomeException ())] +connectXMPP' jid c a = reverse <$> (handle =<< lookupSRV srvrecord) + where + srvrecord = mkSRVTcp "xmpp-client" $ + T.unpack $ strDomain $ jidDomain jid + serverjid = JID Nothing (jidDomain jid) Nothing + + handle [] = do + let h = xmppHostname c + let p = PortNumber $ fromIntegral $ xmppPort c + r <- run h p $ a jid + return [r] + handle srvs = go [] srvs + + go l [] = return l + go l ((h,p):rest) = do + {- Try each SRV record in turn, until one connects, + - at which point the MVar will be full. -} + mv <- newEmptyMVar + r <- run h p $ do + liftIO $ putMVar mv () + a jid + ifM (isEmptyMVar mv) + ( go (r : l) rest + , return (r : l) + ) + + {- Async exceptions are let through so the XMPP thread can + - be killed. -} + run h p a' = do + r <- tryNonAsync $ + runClientError (Server serverjid h p) jid + (xmppUsername c) (xmppPassword c) (void a') + return ((h, p), r) + +{- XMPP runClient, that throws errors rather than returning an Either -} +runClientError :: Server -> JID -> T.Text -> T.Text -> XMPP a -> IO a +runClientError s j u p x = either (error . show) return =<< runClient s j u p x + +getXMPPCreds :: Annex (Maybe XMPPCreds) +getXMPPCreds = parse <$> readCacheCreds xmppCredsFile + where + parse s = readish =<< s + +setXMPPCreds :: XMPPCreds -> Annex () +setXMPPCreds creds = writeCacheCreds (show creds) xmppCredsFile + +xmppCredsFile :: FilePath +xmppCredsFile = "xmpp" diff --git a/Assistant/XMPP/Git.hs b/Assistant/XMPP/Git.hs new file mode 100644 index 0000000000..97b974f82e --- /dev/null +++ b/Assistant/XMPP/Git.hs @@ -0,0 +1,382 @@ +{- git over XMPP + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE CPP #-} + +module Assistant.XMPP.Git where + +import Assistant.Common +import Assistant.NetMessager +import Assistant.Types.NetMessager +import Assistant.XMPP +import Assistant.XMPP.Buddies +import Assistant.DaemonStatus +import Assistant.Alert +import Assistant.MakeRemote +import Assistant.Sync +import qualified Command.Sync +import qualified Annex.Branch +import Annex.UUID +import Logs.UUID +import Annex.TaggedPush +import Annex.CatFile +import Config +import Git +import qualified Git.Branch +import Config.Files +import qualified Types.Remote as Remote +import qualified Remote as Remote +import Remote.List +import Utility.FileMode +import Utility.Shell +import Utility.Env + +import Network.Protocol.XMPP +import qualified Data.Text as T +import System.Posix.Types +import System.Process (std_in, std_out, std_err) +import Control.Concurrent +import System.Timeout +import qualified Data.ByteString as B +import qualified Data.Map as M + +{- Largest chunk of data to send in a single XMPP message. -} +chunkSize :: Int +chunkSize = 4096 + +{- How long to wait for an expected message before assuming the other side + - has gone away and canceling a push. + - + - This needs to be long enough to allow a message of up to 2+ times + - chunkSize to propigate up to a XMPP server, perhaps across to another + - server, and back down to us. On the other hand, other XMPP pushes can be + - delayed for running until the timeout is reached, so it should not be + - excessive. + -} +xmppTimeout :: Int +xmppTimeout = 120000000 -- 120 seconds + +finishXMPPPairing :: JID -> UUID -> Assistant () +finishXMPPPairing jid u = void $ alertWhile alert $ + makeXMPPGitRemote buddy (baseJID jid) u + where + buddy = T.unpack $ buddyName jid + alert = pairRequestAcknowledgedAlert buddy Nothing + +gitXMPPLocation :: JID -> String +gitXMPPLocation jid = "xmpp::" ++ T.unpack (formatJID $ baseJID jid) + +makeXMPPGitRemote :: String -> JID -> UUID -> Assistant Bool +makeXMPPGitRemote buddyname jid u = do + remote <- liftAnnex $ addRemote $ + makeGitRemote buddyname $ gitXMPPLocation jid + liftAnnex $ storeUUID (remoteConfig (Remote.repo remote) "uuid") u + liftAnnex $ void remoteListRefresh + remote' <- liftAnnex $ fromMaybe (error "failed to add remote") + <$> Remote.byName (Just buddyname) + syncRemote remote' + return True + +{- Pushes over XMPP, communicating with a specific client. + - Runs an arbitrary IO action to push, which should run git-push with + - an xmpp:: url. + - + - To handle xmpp:: urls, git push will run git-remote-xmpp, which is + - injected into its PATH, and in turn runs git-annex xmppgit. The + - dataflow them becomes: + - + - git push <--> git-annex xmppgit <--> xmppPush <-------> xmpp + - | + - git receive-pack <--> xmppReceivePack <---------------> xmpp + - + - The pipe between git-annex xmppgit and us is set up and communicated + - using two environment variables, relayIn and relayOut, that are set + - to the file descriptors to use. Another, relayControl, is used to + - propigate the exit status of git receive-pack. + - + - We listen at the other end of the pipe and relay to and from XMPP. + -} +xmppPush :: ClientID -> (Git.Repo -> IO Bool) -> Assistant Bool +xmppPush cid gitpush = do + u <- liftAnnex getUUID + sendNetMessage $ Pushing cid (StartingPush u) + + (Fd inf, writepush) <- liftIO createPipe + (readpush, Fd outf) <- liftIO createPipe + (Fd controlf, writecontrol) <- liftIO createPipe + + tmpdir <- gettmpdir + installwrapper tmpdir + + env <- liftIO getEnvironment + path <- liftIO getSearchPath + let myenv = M.fromList + [ ("PATH", intercalate [searchPathSeparator] $ tmpdir:path) + , (relayIn, show inf) + , (relayOut, show outf) + , (relayControl, show controlf) + ] + `M.union` M.fromList env + + inh <- liftIO $ fdToHandle readpush + outh <- liftIO $ fdToHandle writepush + controlh <- liftIO $ fdToHandle writecontrol + + t1 <- forkIO <~> toxmpp 0 inh + t2 <- forkIO <~> fromxmpp outh controlh + + {- This can take a long time to run, so avoid running it in the + - Annex monad. Also, override environment. -} + g <- liftAnnex gitRepo + r <- liftIO $ gitpush $ g { gitEnv = Just $ M.toList myenv } + + liftIO $ do + mapM_ killThread [t1, t2] + mapM_ hClose [inh, outh, controlh] + mapM_ closeFd [Fd inf, Fd outf, Fd controlf] + + return r + where + toxmpp seqnum inh = do + b <- liftIO $ B.hGetSome inh chunkSize + if B.null b + then liftIO $ killThread =<< myThreadId + else do + let seqnum' = succ seqnum + sendNetMessage $ Pushing cid $ + SendPackOutput seqnum' b + toxmpp seqnum' inh + + fromxmpp outh controlh = withPushMessagesInSequence cid SendPack handle + where + handle (Just (Pushing _ (ReceivePackOutput _ b))) = + liftIO $ writeChunk outh b + handle (Just (Pushing _ (ReceivePackDone exitcode))) = + liftIO $ do + hPrint controlh exitcode + hFlush controlh + handle (Just _) = noop + handle Nothing = do + debug ["timeout waiting for git receive-pack output via XMPP"] + -- Send a synthetic exit code to git-annex + -- xmppgit, which will exit and cause git push + -- to die. + liftIO $ do + hPrint controlh (ExitFailure 1) + hFlush controlh + killThread =<< myThreadId + + installwrapper tmpdir = liftIO $ do + createDirectoryIfMissing True tmpdir + let wrapper = tmpdir "git-remote-xmpp" + program <- readProgramFile + writeFile wrapper $ unlines + [ shebang_local + , "exec " ++ program ++ " xmppgit" + ] + modifyFileMode wrapper $ addModes executeModes + + {- Use GIT_ANNEX_TMP_DIR if set, since that may be a better temp + - dir (ie, not on a crippled filesystem where we can't make + - the wrapper executable). -} + gettmpdir = do + v <- liftIO $ getEnv "GIT_ANNEX_TMP_DIR" + case v of + Nothing -> do + tmp <- liftAnnex $ fromRepo gitAnnexTmpDir + return $ tmp "xmppgit" + Just d -> return $ d "xmppgit" + +type EnvVar = String + +envVar :: String -> EnvVar +envVar s = "GIT_ANNEX_XMPPGIT_" ++ s + +relayIn :: EnvVar +relayIn = envVar "IN" + +relayOut :: EnvVar +relayOut = envVar "OUT" + +relayControl :: EnvVar +relayControl = envVar "CONTROL" + +relayHandle :: EnvVar -> IO Handle +relayHandle var = do + v <- getEnv var + case readish =<< v of + Nothing -> error $ var ++ " not set" + Just n -> fdToHandle $ Fd n + +{- Called by git-annex xmppgit. + - + - git-push is talking to us on stdin + - we're talking to git-push on stdout + - git-receive-pack is talking to us on relayIn (via XMPP) + - we're talking to git-receive-pack on relayOut (via XMPP) + - git-receive-pack's exit code will be passed to us on relayControl + -} +xmppGitRelay :: IO () +xmppGitRelay = do + flip relay stdout =<< relayHandle relayIn + relay stdin =<< relayHandle relayOut + code <- hGetLine =<< relayHandle relayControl + exitWith $ fromMaybe (ExitFailure 1) $ readish code + where + {- Is it possible to set up pipes and not need to copy the data + - ourselves? See splice(2) -} + relay fromh toh = void $ forkIO $ forever $ do + b <- B.hGetSome fromh chunkSize + when (B.null b) $ do + hClose fromh + hClose toh + killThread =<< myThreadId + writeChunk toh b + +{- Relays git receive-pack stdin and stdout via XMPP, as well as propigating + - its exit status to XMPP. -} +xmppReceivePack :: ClientID -> Assistant Bool +xmppReceivePack cid = do + repodir <- liftAnnex $ fromRepo repoPath + let p = (proc "git" ["receive-pack", repodir]) + { std_in = CreatePipe + , std_out = CreatePipe + , std_err = Inherit + } + (Just inh, Just outh, _, pid) <- liftIO $ createProcess p + readertid <- forkIO <~> relayfromxmpp inh + relaytoxmpp 0 outh + code <- liftIO $ waitForProcess pid + void $ sendNetMessage $ Pushing cid $ ReceivePackDone code + liftIO $ do + killThread readertid + hClose inh + hClose outh + return $ code == ExitSuccess + where + relaytoxmpp seqnum outh = do + b <- liftIO $ B.hGetSome outh chunkSize + -- empty is EOF, so exit + unless (B.null b) $ do + let seqnum' = succ seqnum + sendNetMessage $ Pushing cid $ ReceivePackOutput seqnum' b + relaytoxmpp seqnum' outh + relayfromxmpp inh = withPushMessagesInSequence cid ReceivePack handle + where + handle (Just (Pushing _ (SendPackOutput _ b))) = + liftIO $ writeChunk inh b + handle (Just _) = noop + handle Nothing = do + debug ["timeout waiting for git send-pack output via XMPP"] + -- closing the handle will make git receive-pack exit + liftIO $ do + hClose inh + killThread =<< myThreadId + +xmppRemotes :: ClientID -> UUID -> Assistant [Remote] +xmppRemotes cid theiruuid = case baseJID <$> parseJID cid of + Nothing -> return [] + Just jid -> do + let loc = gitXMPPLocation jid + um <- liftAnnex uuidMap + filter (matching loc . Remote.repo) . filter (knownuuid um) . syncGitRemotes + <$> getDaemonStatus + where + matching loc r = repoIsUrl r && repoLocation r == loc + knownuuid um r = Remote.uuid r == theiruuid || M.member theiruuid um + +{- Returns the ClientID that it pushed to. -} +runPush :: (Remote -> Assistant ()) -> NetMessage -> Assistant (Maybe ClientID) +runPush checkcloudrepos (Pushing cid (PushRequest theiruuid)) = + go =<< liftAnnex (inRepo Git.Branch.current) + where + go Nothing = return Nothing + go (Just branch) = do + rs <- xmppRemotes cid theiruuid + liftAnnex $ Annex.Branch.commit "update" + (g, u) <- liftAnnex $ (,) + <$> gitRepo + <*> getUUID + liftIO $ Command.Sync.updateBranch (Command.Sync.syncBranch branch) g + selfjid <- ((T.unpack <$>) . xmppClientID) <$> getDaemonStatus + if null rs + then return Nothing + else do + forM_ rs $ \r -> do + void $ alertWhile (syncAlert [r]) $ + xmppPush cid (taggedPush u selfjid branch r) + checkcloudrepos r + return $ Just cid +runPush checkcloudrepos (Pushing cid (StartingPush theiruuid)) = do + rs <- xmppRemotes cid theiruuid + if null rs + then return Nothing + else do + void $ alertWhile (syncAlert rs) $ + xmppReceivePack cid + mapM_ checkcloudrepos rs + return $ Just cid +runPush _ _ = return Nothing + +{- Check if any of the shas that can be pushed are ones we do not + - have. + - + - (Older clients send no shas, so when there are none, always + - request a push.) + -} +handlePushNotice :: NetMessage -> Assistant () +handlePushNotice (Pushing cid (CanPush theiruuid shas)) = + unlessM (null <$> xmppRemotes cid theiruuid) $ + if null shas + then go + else ifM (haveall shas) + ( debug ["ignoring CanPush with known shas"] + , go + ) + where + go = do + u <- liftAnnex getUUID + sendNetMessage $ Pushing cid (PushRequest u) + haveall l = liftAnnex $ not <$> anyM donthave l + donthave sha = isNothing <$> catObjectDetails sha +handlePushNotice _ = noop + +writeChunk :: Handle -> B.ByteString -> IO () +writeChunk h b = do + B.hPut h b + hFlush h + +{- Gets NetMessages for a PushSide, ensures they are in order, + - and runs an action to handle each in turn. The action will be passed + - Nothing on timeout. + - + - Does not currently reorder messages, but does ensure that any + - duplicate messages, or messages not in the sequence, are discarded. + -} +withPushMessagesInSequence :: ClientID -> PushSide -> (Maybe NetMessage -> Assistant ()) -> Assistant () +withPushMessagesInSequence cid side a = loop 0 + where + loop seqnum = do + m <- timeout xmppTimeout <~> waitInbox cid side + let go s = a m >> loop s + let next = seqnum + 1 + case extractSequence =<< m of + Just seqnum' + | seqnum' == next -> go next + | seqnum' == 0 -> go seqnum + | seqnum' == seqnum -> do + debug ["ignoring duplicate sequence number", show seqnum] + loop seqnum + | otherwise -> do + debug ["ignoring out of order sequence number", show seqnum', "expected", show next] + loop seqnum + Nothing -> go seqnum + +extractSequence :: NetMessage -> Maybe Int +extractSequence (Pushing _ (ReceivePackOutput seqnum _)) = Just seqnum +extractSequence (Pushing _ (SendPackOutput seqnum _)) = Just seqnum +extractSequence _ = Nothing diff --git a/Backend.hs b/Backend.hs new file mode 100644 index 0000000000..2ee14acc61 --- /dev/null +++ b/Backend.hs @@ -0,0 +1,120 @@ +{- git-annex key/value backends + - + - Copyright 2010,2013 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Backend ( + list, + orderedList, + genKey, + lookupFile, + isAnnexLink, + chooseBackend, + lookupBackendName, + maybeLookupBackendName +) where + +import Common.Annex +import qualified Annex +import Annex.CheckAttr +import Annex.CatFile +import Annex.Link +import Types.Key +import Types.KeySource +import qualified Types.Backend as B +import Config + +-- When adding a new backend, import it here and add it to the list. +import qualified Backend.SHA +import qualified Backend.WORM +import qualified Backend.URL + +list :: [Backend] +list = Backend.SHA.backends ++ Backend.WORM.backends ++ Backend.URL.backends + +{- List of backends in the order to try them when storing a new key. -} +orderedList :: Annex [Backend] +orderedList = do + l <- Annex.getState Annex.backends -- list is cached here + if not $ null l + then return l + else do + f <- Annex.getState Annex.forcebackend + case f of + Just name | not (null name) -> + return [lookupBackendName name] + _ -> do + l' <- gen . annexBackends <$> Annex.getGitConfig + Annex.changeState $ \s -> s { Annex.backends = l' } + return l' + where + gen [] = list + gen l = map lookupBackendName l + +{- Generates a key for a file, trying each backend in turn until one + - accepts it. -} +genKey :: KeySource -> Maybe Backend -> Annex (Maybe (Key, Backend)) +genKey source trybackend = do + bs <- orderedList + let bs' = maybe bs (: bs) trybackend + genKey' bs' source +genKey' :: [Backend] -> KeySource -> Annex (Maybe (Key, Backend)) +genKey' [] _ = return Nothing +genKey' (b:bs) source = do + r <- B.getKey b source + case r of + Nothing -> genKey' bs source + Just k -> return $ Just (makesane k, b) + where + -- keyNames should not contain newline characters. + makesane k = k { keyName = map fixbadchar (keyName k) } + fixbadchar c + | c == '\n' = '_' + | otherwise = c + +{- Looks up the key and backend corresponding to an annexed file, + - by examining what the file links to. + - + - In direct mode, there is often no link on disk, in which case + - the symlink is looked up in git instead. However, a real link + - on disk still takes precedence over what was committed to git in direct + - mode. + -} +lookupFile :: FilePath -> Annex (Maybe (Key, Backend)) +lookupFile file = do + mkey <- isAnnexLink file + case mkey of + Just key -> makeret key + Nothing -> ifM isDirect + ( maybe (return Nothing) makeret =<< catKeyFile file + , return Nothing + ) + where + makeret k = let bname = keyBackendName k in + case maybeLookupBackendName bname of + Just backend -> return $ Just (k, backend) + Nothing -> do + warning $ + "skipping " ++ file ++ + " (unknown backend " ++ bname ++ ")" + return Nothing + +{- Looks up the backend that should be used for a file. + - That can be configured on a per-file basis in the gitattributes file. -} +chooseBackend :: FilePath -> Annex (Maybe Backend) +chooseBackend f = Annex.getState Annex.forcebackend >>= go + where + go Nothing = maybeLookupBackendName <$> checkAttr "annex.backend" f + go (Just _) = Just . Prelude.head <$> orderedList + +{- Looks up a backend by name. May fail if unknown. -} +lookupBackendName :: String -> Backend +lookupBackendName s = fromMaybe unknown $ maybeLookupBackendName s + where + unknown = error $ "unknown backend " ++ s +maybeLookupBackendName :: String -> Maybe Backend +maybeLookupBackendName s = headMaybe matches + where + matches = filter (\b -> s == B.name b) list diff --git a/Backend/SHA.hs b/Backend/SHA.hs new file mode 100644 index 0000000000..a735ce1e5b --- /dev/null +++ b/Backend/SHA.hs @@ -0,0 +1,146 @@ +{- git-annex SHA backends + - + - Copyright 2011,2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Backend.SHA (backends) where + +import Common.Annex +import qualified Annex +import Types.Backend +import Types.Key +import Types.KeySource +import Utility.ExternalSHA + +import qualified Build.SysConfig as SysConfig +import Data.Digest.Pure.SHA +import qualified Data.ByteString.Lazy as L +import Data.Char + +type SHASize = Int + +{- Order is slightly significant; want SHA256 first, and more general + - sizes earlier. -} +sizes :: [Int] +sizes = [256, 1, 512, 224, 384] + +{- The SHA256E backend is the default. -} +backends :: [Backend] +backends = catMaybes $ map genBackendE sizes ++ map genBackend sizes + +genBackend :: SHASize -> Maybe Backend +genBackend size = Just $ Backend + { name = shaName size + , getKey = keyValue size + , fsckKey = Just $ checkKeyChecksum size + , canUpgradeKey = Just $ needsUpgrade + } + +genBackendE :: SHASize -> Maybe Backend +genBackendE size = do + b <- genBackend size + return $ b + { name = shaNameE size + , getKey = keyValueE size + } + +shaName :: SHASize -> String +shaName size = "SHA" ++ show size + +shaNameE :: SHASize -> String +shaNameE size = shaName size ++ "E" + +shaN :: SHASize -> FilePath -> Integer -> Annex String +shaN shasize file filesize = do + showAction "checksum" + liftIO $ case shaCommand shasize filesize of + Left sha -> sha <$> L.readFile file + Right command -> + either error return + =<< externalSHA command shasize file + +shaCommand :: SHASize -> Integer -> Either (L.ByteString -> String) String +shaCommand shasize filesize + | shasize == 1 = use SysConfig.sha1 sha1 + | shasize == 256 = use SysConfig.sha256 sha256 + | shasize == 224 = use SysConfig.sha224 sha224 + | shasize == 384 = use SysConfig.sha384 sha384 + | shasize == 512 = use SysConfig.sha512 sha512 + | otherwise = error $ "bad sha size " ++ show shasize + where + use Nothing sha = Left $ showDigest . sha + use (Just c) sha + {- use builtin, but slower sha for small files + - benchmarking indicates it's faster up to + - and slightly beyond 50 kb files -} + | filesize < 51200 = use Nothing sha + | otherwise = Right c + +{- A key is a checksum of its contents. -} +keyValue :: SHASize -> KeySource -> Annex (Maybe Key) +keyValue shasize source = do + let file = contentLocation source + stat <- liftIO $ getFileStatus file + let filesize = fromIntegral $ fileSize stat + s <- shaN shasize file filesize + return $ Just $ stubKey + { keyName = s + , keyBackendName = shaName shasize + , keySize = Just filesize + } + +{- Extension preserving keys. -} +keyValueE :: SHASize -> KeySource -> Annex (Maybe Key) +keyValueE size source = keyValue size source >>= maybe (return Nothing) addE + where + addE k = return $ Just $ k + { keyName = keyName k ++ selectExtension (keyFilename source) + , keyBackendName = shaNameE size + } + +selectExtension :: FilePath -> String +selectExtension f + | null es = "" + | otherwise = intercalate "." ("":es) + where + es = filter (not . null) $ reverse $ + take 2 $ takeWhile shortenough $ + reverse $ split "." $ filter validExtension $ takeExtensions f + shortenough e = length e <= 4 -- long enough for "jpeg" + +{- A key's checksum is checked during fsck. -} +checkKeyChecksum :: SHASize -> Key -> FilePath -> Annex Bool +checkKeyChecksum size key file = do + fast <- Annex.getState Annex.fast + mstat <- liftIO $ catchMaybeIO $ getFileStatus file + case (mstat, fast) of + (Just stat, False) -> do + let filesize = fromIntegral $ fileSize stat + check <$> shaN size file filesize + _ -> return True + where + sha = keySha key + check s + | s == sha = True + {- A bug caused checksums to be prefixed with \ in some + - cases; still accept these as legal now that the bug has been + - fixed. -} + | '\\' : s == sha = True + | otherwise = False + +keySha :: Key -> String +keySha key = dropExtensions (keyName key) + +validExtension :: Char -> Bool +validExtension c + | isAlphaNum c = True + | c == '.' = True + | otherwise = False + +{- Upgrade keys that have the \ prefix on their sha due to a bug, or + - that contain non-alphanumeric characters in their extension. -} +needsUpgrade :: Key -> Bool +needsUpgrade key = "\\" `isPrefixOf` keySha key || + any (not . validExtension) (takeExtensions $ keyName key) diff --git a/Backend/URL.hs b/Backend/URL.hs new file mode 100644 index 0000000000..ace578a241 --- /dev/null +++ b/Backend/URL.hs @@ -0,0 +1,44 @@ +{- git-annex "URL" backend -- keys whose content is available from urls. + - + - Copyright 2011 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Backend.URL ( + backends, + fromUrl +) where + +import Data.Hash.MD5 + +import Common.Annex +import Types.Backend +import Types.Key + +backends :: [Backend] +backends = [backend] + +backend :: Backend +backend = Backend + { name = "URL" + , getKey = const $ return Nothing + , fsckKey = Nothing + , canUpgradeKey = Nothing + } + +{- When it's not too long, use the full url as the key name. + - If the url is too long, it's truncated at half the filename length + - limit, and the md5 of the url is prepended to ensure a unique key. -} +fromUrl :: String -> Maybe Integer -> Annex Key +fromUrl url size = do + limit <- liftIO . fileNameLengthLimit =<< fromRepo gitAnnexDir + let truncurl = truncateFilePath (limit `div` 2) url + let key = if url == truncurl + then url + else truncurl ++ "-" ++ md5s (Str url) + return $ stubKey + { keyName = key + , keyBackendName = "URL" + , keySize = size + } diff --git a/Backend/WORM.hs b/Backend/WORM.hs new file mode 100644 index 0000000000..3471eedc14 --- /dev/null +++ b/Backend/WORM.hs @@ -0,0 +1,41 @@ +{- git-annex "WORM" backend -- Write Once, Read Many + - + - Copyright 2010 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Backend.WORM (backends) where + +import Common.Annex +import Types.Backend +import Types.Key +import Types.KeySource + +backends :: [Backend] +backends = [backend] + +backend :: Backend +backend = Backend + { name = "WORM" + , getKey = keyValue + , fsckKey = Nothing + , canUpgradeKey = Nothing + } + +{- The key includes the file size, modification time, and the + - basename of the filename. + - + - That allows multiple files with the same names to have different keys, + - while also allowing a file to be moved around while retaining the + - same key. + -} +keyValue :: KeySource -> Annex (Maybe Key) +keyValue source = do + stat <- liftIO $ getFileStatus $ contentLocation source + return $ Just Key { + keyName = takeFileName $ keyFilename source, + keyBackendName = name backend, + keySize = Just $ fromIntegral $ fileSize stat, + keyMtime = Just $ modificationTime stat + } diff --git a/Build/BundledPrograms.hs b/Build/BundledPrograms.hs new file mode 100644 index 0000000000..9a50778d4e --- /dev/null +++ b/Build/BundledPrograms.hs @@ -0,0 +1,48 @@ +{- Bundled programs + - + - Copyright 2013 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE CPP #-} + +module Build.BundledPrograms where + +import Data.Maybe + +import Build.SysConfig as SysConfig + +{- Programs that git-annex uses, to include in the bundle. + - + - These may be just the command name, or the full path to it. -} +bundledPrograms :: [FilePath] +bundledPrograms = catMaybes + [ Nothing +#ifndef mingw32_HOST_OS + -- git is not included in the windows bundle + , Just "git" +#endif + , Just "cp" + , Just "xargs" + , Just "rsync" + , Just "ssh" +#ifndef mingw32_HOST_OS + , Just "sh" +#endif + , SysConfig.gpg + , ifset SysConfig.curl "curl" + , ifset SysConfig.wget "wget" + , ifset SysConfig.bup "bup" + , SysConfig.lsof + , SysConfig.sha1 + , SysConfig.sha256 + , SysConfig.sha512 + , SysConfig.sha224 + , SysConfig.sha384 + -- ionice is not included in the bundle; we rely on the system's + -- own version, which may better match its kernel + ] + where + ifset True s = Just s + ifset False _ = Nothing diff --git a/Build/Configure.hs b/Build/Configure.hs new file mode 100644 index 0000000000..15b90ebe3d --- /dev/null +++ b/Build/Configure.hs @@ -0,0 +1,183 @@ +{- Checks system configuration and generates SysConfig.hs. -} + +module Build.Configure where + +import System.Directory +import Data.List +import System.Process +import Control.Applicative +import System.FilePath +import System.Environment +import Data.Maybe +import Control.Monad.IfElse +import Data.Char + +import Build.TestConfig +import Utility.SafeCommand +import Utility.Monad +import Utility.Exception +import Utility.ExternalSHA +import qualified Git.Version + +tests :: [TestCase] +tests = + [ TestCase "version" getVersion + , TestCase "git" $ requireCmd "git" "git --version >/dev/null" + , TestCase "git version" getGitVersion + , testCp "cp_a" "-a" + , testCp "cp_p" "-p" + , testCp "cp_reflink_auto" "--reflink=auto" + , TestCase "xargs -0" $ requireCmd "xargs_0" "xargs -0 /dev/null" + , TestCase "curl" $ testCmd "curl" "curl --version >/dev/null" + , TestCase "wget" $ testCmd "wget" "wget --version >/dev/null" + , TestCase "bup" $ testCmd "bup" "bup --version >/dev/null" + , TestCase "ionice" $ testCmd "ionice" "ionice -c3 true >/dev/null" + , TestCase "gpg" $ maybeSelectCmd "gpg" + [ ("gpg", "--version >/dev/null") + , ("gpg2", "--version >/dev/null") ] + , TestCase "lsof" $ findCmdPath "lsof" "lsof" + , TestCase "ssh connection caching" getSshConnectionCaching + ] ++ shaTestCases + [ (1, "da39a3ee5e6b4b0d3255bfef95601890afd80709") + , (256, "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855") + , (512, "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e") + , (224, "d14a028c2a3a2bc9476102bb288234c415a2b01f828ea62ac5b3e42f") + , (384, "38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b") + ] + +{- shaNsum are the program names used by coreutils. Some systems like OSX + - sometimes install these with 'g' prefixes. + - + - On some systems, shaN is used instead, but on other + - systems, it might be "hashalot", which does not produce + - usable checksums. Only accept programs that produce + - known-good hashes when run on files. -} +shaTestCases :: [(Int, String)] -> [TestCase] +shaTestCases l = map make l + where + make (n, knowngood) = TestCase key $ + Config key . MaybeStringConfig <$> search (shacmds n) + where + key = "sha" ++ show n + search [] = return Nothing + search (c:cmds) = do + sha <- externalSHA c n "/dev/null" + if sha == Right knowngood + then return $ Just c + else search cmds + + shacmds n = concatMap (\x -> [x, 'g':x, osxpath x]) $ + map (\x -> "sha" ++ show n ++ x) ["sum", ""] + + {- Max OSX sometimes puts GNU tools outside PATH, so look in + - the location it uses, and remember where to run them + - from. -} + osxpath = "/opt/local/libexec/gnubin" + +tmpDir :: String +tmpDir = "tmp" + +testFile :: String +testFile = tmpDir ++ "/testfile" + +testCp :: ConfigKey -> String -> TestCase +testCp k option = TestCase cmd $ testCmd k cmdline + where + cmd = "cp " ++ option + cmdline = cmd ++ " " ++ testFile ++ " " ++ testFile ++ ".new" + +isReleaseBuild :: IO Bool +isReleaseBuild = isJust <$> catchMaybeIO (getEnv "RELEASE_BUILD") + +{- Version is usually based on the major version from the changelog, + - plus the date of the last commit, plus the git rev of that commit. + - This works for autobuilds, ad-hoc builds, etc. + - + - If git or a git repo is not available, or something goes wrong, + - or this is a release build, just use the version from the changelog. -} +getVersion :: Test +getVersion = do + changelogversion <- getChangelogVersion + version <- ifM (isReleaseBuild) + ( return changelogversion + , catchDefaultIO changelogversion $ do + let major = takeWhile (/= '.') changelogversion + autoversion <- readProcess "sh" + [ "-c" + , "git log -n 1 --format=format:'%ci %h'| sed -e 's/-//g' -e 's/ .* /-g/'" + ] "" + if null autoversion + then return changelogversion + else return $ concat [ major, ".", autoversion ] + ) + return $ Config "packageversion" (StringConfig version) + +getChangelogVersion :: IO String +getChangelogVersion = do + changelog <- readFile "debian/changelog" + let verline = takeWhile (/= '\n') changelog + return $ middle (words verline !! 1) + where + middle = drop 1 . init + +getGitVersion :: Test +getGitVersion = Config "gitversion" . StringConfig . show + <$> Git.Version.installed + +getSshConnectionCaching :: Test +getSshConnectionCaching = Config "sshconnectioncaching" . BoolConfig <$> + boolSystem "sh" [Param "-c", Param "ssh -o ControlPersist=yes -V >/dev/null 2>/dev/null"] + +{- Set up cabal file with version. -} +cabalSetup :: IO () +cabalSetup = do + version <- takeWhile (\c -> isDigit c || c == '.') + <$> getChangelogVersion + cabal <- readFile cabalfile + writeFile tmpcabalfile $ unlines $ + map (setfield "Version" version) $ + lines cabal + renameFile tmpcabalfile cabalfile + where + cabalfile = "git-annex.cabal" + tmpcabalfile = cabalfile++".tmp" + setfield field value s + | fullfield `isPrefixOf` s = fullfield ++ value + | otherwise = s + where + fullfield = field ++ ": " + +setup :: IO () +setup = do + createDirectoryIfMissing True tmpDir + writeFile testFile "test file contents" + +cleanup :: IO () +cleanup = removeDirectoryRecursive tmpDir + +run :: [TestCase] -> IO () +run ts = do + args <- getArgs + setup + config <- runTests ts + if args == ["Android"] + then writeSysConfig $ androidConfig config + else writeSysConfig config + cleanup + whenM (isReleaseBuild) $ + cabalSetup + +{- Hard codes some settings to cross-compile for Android. -} +androidConfig :: [Config] -> [Config] +androidConfig c = overrides ++ filter (not . overridden) c + where + overrides = + [ Config "cp_reflink_auto" $ BoolConfig False + , Config "curl" $ BoolConfig False + , Config "sha224" $ MaybeStringConfig Nothing + , Config "sha384" $ MaybeStringConfig Nothing + ] + overridden (Config k _) = k `elem` overridekeys + overridekeys = map (\(Config k _) -> k) overrides + diff --git a/Build/DesktopFile.hs b/Build/DesktopFile.hs new file mode 100644 index 0000000000..9f4ba5992c --- /dev/null +++ b/Build/DesktopFile.hs @@ -0,0 +1,82 @@ +{- Generating and installing a desktop menu entry file and icon, + - and a desktop autostart file. (And OSX equivilants.) + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE CPP #-} + +module Build.DesktopFile where + +import Utility.Exception +import Utility.FreeDesktop +import Utility.Path +import Utility.Monad +import Config.Files +import Utility.OSX +import Assistant.Install.AutoStart +import Assistant.Install.Menu + +import Control.Applicative +import System.Directory +import System.Environment +#ifndef mingw32_HOST_OS +import System.Posix.User +import System.Posix.Files +#endif +import System.FilePath +import Data.Maybe + +systemwideInstall :: IO Bool +#ifndef mingw32_HOST_OS +systemwideInstall = isroot <||> destdirset + where + isroot = do + uid <- fromIntegral <$> getRealUserID + return $ uid == (0 :: Int) + destdirset = isJust <$> catchMaybeIO (getEnv "DESTDIR") +#else +systemwideInstall = return False +#endif + +inDestDir :: FilePath -> IO FilePath +inDestDir f = do + destdir <- catchDefaultIO "" (getEnv "DESTDIR") + return $ destdir ++ "/" ++ f + +writeFDODesktop :: FilePath -> IO () +writeFDODesktop command = do + systemwide <- systemwideInstall + + datadir <- if systemwide then return systemDataDir else userDataDir + menufile <- inDestDir (desktopMenuFilePath "git-annex" datadir) + icondir <- inDestDir (iconDir datadir) + installMenu command menufile "doc" icondir + + configdir <- if systemwide then return systemConfigDir else userConfigDir + installAutoStart command + =<< inDestDir (autoStartPath "git-annex" configdir) + +writeOSXDesktop :: FilePath -> IO () +writeOSXDesktop command = do + installAutoStart command =<< inDestDir =<< ifM systemwideInstall + ( return $ systemAutoStart osxAutoStartLabel + , userAutoStart osxAutoStartLabel + ) + +install :: FilePath -> IO () +install command = do +#ifdef darwin_HOST_OS + writeOSXDesktop command +#else + writeFDODesktop command +#endif + ifM systemwideInstall + ( return () + , do + programfile <- inDestDir =<< programFile + createDirectoryIfMissing True (parentDir programfile) + writeFile programfile command + ) diff --git a/Build/EvilSplicer.hs b/Build/EvilSplicer.hs new file mode 100644 index 0000000000..8f203437a9 --- /dev/null +++ b/Build/EvilSplicer.hs @@ -0,0 +1,549 @@ +{- Expands template haskell splices + - + - You should probably just use http://hackage.haskell.org/package/zeroth + - instead. I wish I had known about it before writing this. + - + - First, the code must be built with a ghc that supports TH, + - and the splices dumped to a log. For example: + - cabal build --ghc-options=-ddump-splices 2>&1 | tee log + - + - Along with the log, a headers file may also be provided, containing + - additional imports needed by the template haskell code. + - + - This program will parse the log, and expand all splices therein, + - writing files to the specified destdir (which can be "." to modify + - the source tree directly). They can then be built a second + - time, with a ghc that does not support TH. + - + - Note that template haskell code may refer to symbols that are not + - exported by the library that defines the TH code. In this case, + - the library has to be modifed to export those symbols. + - + - There can also be other problems with the generated code; it may + - need modifications to compile. + - + - + - Copyright 2013 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Main where + +import Text.Parsec +import Text.Parsec.String +import Control.Applicative ((<$>)) +import Data.Either +import Data.List +import Data.String.Utils +import Data.Char +import System.Environment +import System.FilePath +import System.Directory +import Control.Monad + +import Utility.Monad +import Utility.Misc +import Utility.Exception +import Utility.Path + +data Coord = Coord + { coordLine :: Int + , coordColumn :: Int + } + deriving (Read, Show) + +offsetCoord :: Coord -> Coord -> Coord +offsetCoord a b = Coord + (coordLine a - coordLine b) + (coordColumn a - coordColumn b) + +data SpliceType = SpliceExpression | SpliceDeclaration + deriving (Read, Show, Eq) + +data Splice = Splice + { splicedFile :: FilePath + , spliceStart :: Coord + , spliceEnd :: Coord + , splicedExpression :: String + , splicedCode :: String + , spliceType :: SpliceType + } + deriving (Read, Show) + +isExpressionSplice :: Splice -> Bool +isExpressionSplice s = spliceType s == SpliceExpression + +number :: Parser Int +number = read <$> many1 digit + +{- A pair of Coords is written in one of three ways: + - "95:21-73", "1:1", or "(92,25)-(94,2)" + -} +coordsParser :: Parser (Coord, Coord) +coordsParser = (try singleline <|> try weird <|> multiline) "Coords" + where + singleline = do + line <- number + char ':' + startcol <- number + char '-' + endcol <- number + return $ (Coord line startcol, Coord line endcol) + + weird = do + line <- number + char ':' + col <- number + return $ (Coord line col, Coord line col) + + multiline = do + start <- fromparens + char '-' + end <- fromparens + return $ (start, end) + + fromparens = between (char '(') (char ')') $ do + line <- number + char ',' + col <- number + return $ Coord line col + +indent :: Parser String +indent = many1 $ char ' ' + +restOfLine :: Parser String +restOfLine = newline `after` many (noneOf "\n") + +indentedLine :: Parser String +indentedLine = indent >> restOfLine + +spliceParser :: Parser Splice +spliceParser = do + file <- many1 (noneOf ":\n") + char ':' + (start, end) <- coordsParser + string ": Splicing " + splicetype <- tosplicetype + <$> (string "expression" <|> string "declarations") + newline + + getthline <- expressionextractor + expression <- unlines <$> many1 getthline + + indent + string "======>" + newline + + getcodeline <- expressionextractor + realcoords <- try (Right <$> getrealcoords file) <|> (Left <$> getcodeline) + codelines <- many getcodeline + return $ case realcoords of + Left firstcodeline -> + Splice file start end expression + (unlines $ firstcodeline:codelines) + splicetype + Right (realstart, realend) -> + Splice file realstart realend expression + (unlines codelines) + splicetype + where + tosplicetype "declarations" = SpliceDeclaration + tosplicetype "expression" = SpliceExpression + tosplicetype s = error $ "unknown splice type: " ++ s + + {- All lines of the indented expression start with the same + - indent, which is stripped. Any other indentation is preserved. -} + expressionextractor = do + i <- lookAhead indent + return $ try $ do + string i + restOfLine + + {- When splicing declarations, GHC will output a splice + - at 1:1, and then inside the splice code block, + - the first line will give the actual coordinates of the + - line that was spliced. -} + getrealcoords file = do + indent + string file + char ':' + char '\n' `after` coordsParser + +{- Extracts the splices, ignoring the rest of the compiler output. -} +splicesExtractor :: Parser [Splice] +splicesExtractor = rights <$> many extract + where + extract = try (Right <$> spliceParser) <|> (Left <$> compilerJunkLine) + compilerJunkLine = restOfLine + +{- Modifies the source file, expanding the splices, which all must + - have the same splicedFile. Writes the new file to the destdir. + - + - Each splice's Coords refer to the original position in the file, + - and not to its position after any previous splices may have inserted + - or removed lines. + - + - To deal with this complication, the file is broken into logical lines + - (which can contain any String, including a multiline or empty string). + - Each splice is assumed to be on its own block of lines; two + - splices on the same line is not currently supported. + - This means that a splice can modify the logical lines within its block + - as it likes, without interfering with the Coords of other splices. + - + - As well as expanding splices, this can add a block of imports to the + - file. These are put right before the first line in the file that + - starts with "import " + -} +applySplices :: FilePath -> Maybe String -> [Splice] -> IO () +applySplices destdir imports splices@(first:_) = do + let f = splicedFile first + let dest = (destdir f) + lls <- map (++ "\n") . lines <$> readFileStrict f + createDirectoryIfMissing True (parentDir dest) + let newcontent = concat $ addimports $ expand lls splices + oldcontent <- catchMaybeIO $ readFileStrict dest + when (oldcontent /= Just newcontent) $ do + putStrLn $ "splicing " ++ f + writeFile dest newcontent + where + expand lls [] = lls + expand lls (s:rest) + | isExpressionSplice s = expand (expandExpressionSplice s lls) rest + | otherwise = expand (expandDeclarationSplice s lls) rest + + addimports lls = case imports of + Nothing -> lls + Just v -> + let (start, end) = break ("import " `isPrefixOf`) lls + in if null end + then start + else concat + [ start + , [v] + , end + ] + +{- Declaration splices are expanded to replace their whole line. -} +expandDeclarationSplice :: Splice -> [String] -> [String] +expandDeclarationSplice s lls = concat [before, [splice], end] + where + cs = spliceStart s + ce = spliceEnd s + + (before, rest) = splitAt (coordLine cs - 1) lls + (_oldlines, end) = splitAt (1 + coordLine (offsetCoord ce cs)) rest + splice = mangleCode $ splicedCode s + +{- Expression splices are expanded within their line. -} +expandExpressionSplice :: Splice -> [String] -> [String] +expandExpressionSplice s lls = concat [before, spliced:padding, end] + where + cs = spliceStart s + ce = spliceEnd s + + (before, rest) = splitAt (coordLine cs - 1) lls + (oldlines, end) = splitAt (1 + coordLine (offsetCoord ce cs)) rest + (splicestart, padding, spliceend) = case map expandtabs oldlines of + ss:r + | null r -> (ss, [], ss) + | otherwise -> (ss, take (length r) (repeat []), last r) + _ -> ([], [], []) + spliced = concat + [ joinsplice $ deqqstart $ take (coordColumn cs - 1) splicestart + , addindent (findindent splicestart) (mangleCode $ splicedCode s) + , deqqend $ drop (coordColumn ce) spliceend + ] + + {- coordinates assume tabs are expanded to 8 spaces -} + expandtabs = replace "\t" (take 8 $ repeat ' ') + + {- splicing leaves $() quasiquote behind; remove it -} + deqqstart s = case reverse s of + ('(':'$':rest) -> reverse rest + _ -> s + deqqend (')':s) = s + deqqend s = s + + {- Prepare the code that comes just before the splice so + - the splice will combine with it appropriately. -} + joinsplice s + -- all indentation? Skip it, we'll use the splice's indentation + | all isSpace s = "" + -- function definition needs no preparation + -- ie: foo = $(splice) + | "=" `isSuffixOf` s' = s + -- nor does lambda definition or case expression + | "->" `isSuffixOf` s' = s + -- nor does a let .. in declaration + | "in" `isSuffixOf` s' = s + -- already have a $ to set off the splice + -- ie: foo $ $(splice) + | "$" `isSuffixOf` s' = s + -- need to add a $ to set off the splice + -- ie: bar $(splice) + | otherwise = s ++ " $ " + where + s' = filter (not . isSpace) s + + findindent = length . takeWhile isSpace + addindent n = unlines . map (i ++) . lines + where + i = take n $ repeat ' ' + +{- Tweaks code output by GHC in splices to actually build. Yipes. -} +mangleCode :: String -> String +mangleCode = flip_colon + . lambdaparens + . declaration_parens + . case_layout + . case_layout_multiline + . yesod_url_render_hack + . text_builder_hack + . nested_instances + . collapse_multiline_strings + . remove_package_version + . emptylambda + where + {- Lambdas are often output without parens around them. + - This breaks when the lambda is immediately applied to a + - parameter. + - + - For example: + - + - renderRoute (StaticR sub_a1nUH) + - = \ (a_a1nUI, b_a1nUJ) + - -> (((pack "static") : a_a1nUI), + - b_a1nUJ) + - (renderRoute sub_a1nUH) + - + - There are sometimes many lines of lambda code that need to be + - parenthesised. Approach: find the "->" and scan down the + - column to the first non-whitespace. This is assumed + - to be the expression after the lambda. + - + - Runs recursively on the body of the lambda, to handle nested + - lambdas. + -} + lambdaparens = parsecAndReplace $ do + -- skip lambdas inside tuples or parens + prefix <- noneOf "(, \n" + preindent <- many1 $ oneOf " \n" + string "\\ " + lambdaparams <- restofline + indent <- many1 $ char ' ' + string "-> " + firstline <- restofline + lambdalines <- many $ try $ do + string indent + char ' ' + l <- restofline + return $ indent ++ " " ++ l + return $ concat + [ prefix:preindent + , "(\\ " ++ lambdaparams ++ "\n" + , indent ++ "-> " + , lambdaparens $ intercalate "\n" (firstline:lambdalines) + , ")\n" + ] + + restofline = manyTill (noneOf "\n") newline + + {- For some reason, GHC sometimes doesn't like the multiline + - strings it creates. It seems to get hung up on \{ at the + - start of a new line sometimes, wanting it to not be escaped. + - + - To work around what is likely a GHC bug, just collapse + - multiline strings. -} + collapse_multiline_strings = parsecAndReplace $ do + string "\\\n" + many1 $ oneOf " \t" + string "\\" + return "\\n" + + {- GHC outputs splices using explicit braces rather than layout. + - For a case expression, it does something weird: + - + - case foo of { + - xxx -> blah + - yyy -> blah }; + - + - This is not legal Haskell; the statements in the case must be + - separated by ';' + - + - To fix, we could just put a semicolon at the start of every line + - containing " -> " ... Except that lambdas also contain that. + - But we can get around that: GHC outputs lambas like this: + - + - \ foo + - -> bar + - + - Or like this: + - + - \ foo -> bar + - + - So, we can put the semicolon at the start of every line + - containing " -> " unless there's a "\ " first, or it's + - all whitespace up until it. + -} + case_layout = parsecAndReplace $ do + newline + indent <- many1 $ char ' ' + prefix <- manyTill (noneOf "\n") (try (string "-> ")) + if length prefix > 10 + then unexpected "too long a prefix" + else if "\\ " `isInfixOf` prefix + then unexpected "lambda expression" + else if null prefix + then unexpected "second line of lambda" + else return $ "\n" ++ indent ++ "; " ++ prefix ++ " -> " + {- Sometimes cases themselves span multiple lines: + - + - Nothing + - -> foo + -} + case_layout_multiline = parsecAndReplace $ do + newline + indent <- many1 $ char ' ' + firstline <- restofline + + string indent + indent2 <- many1 $ char ' ' + string "-> " + if "\\ " `isInfixOf` firstline + then unexpected "lambda expression" + else return $ "\n" ++ indent ++ "; " ++ firstline ++ "\n" + ++ indent ++ indent2 ++ "-> " + + {- (foo, \ -> bar) is not valid haskell, GHC. + - Change to (foo, bar) + - + - (Does this ever happen outside a tuple? Only saw + - it inside them.. + -} + emptylambda = replace ", \\ -> " ", " + + {- GHC may output this: + - + - instance RenderRoute WebApp where + - data instance Route WebApp + - ^^^^^^^^ + - The marked word should not be there. + - + - FIXME: This is a yesod-specific hack, it should look for the + - outer instance. + -} + nested_instances = replace " data instance Route" " data Route" + + {- GHC does not properly parenthesise generated data type + - declarations. -} + declaration_parens = replace "StaticR Route Static" "StaticR (Route Static)" + + {- GHC may add full package and version qualifications for + - symbols from unimported modules. We don't want these. + - + - Examples: + - "blaze-html-0.4.3.1:Text.Blaze.Internal.preEscapedText" + - "ghc-prim:GHC.Types.:" + -} + remove_package_version = parsecAndReplace $ + mangleSymbol <$> qualifiedSymbol + + mangleSymbol "GHC.Types." = "" + mangleSymbol "GHC.Tuple." = "" + mangleSymbol s = s + + qualifiedSymbol :: Parser String + qualifiedSymbol = do + s <- token + char ':' + if length s < 5 + then unexpected "too short to be a namespace" + else do + token + + token :: Parser String + token = do + t <- satisfy isLetter + oken <- many $ satisfy isAlphaNum <|> oneOf "-.'" + return $ t:oken + + {- This works when it's "GHC.Types.:", but we strip + - that above, so have to fix up after it here. + - The ; is added by case_layout. -} + flip_colon = replace "; : _ " "; _ : " + +{- This works around a problem in the expanded template haskell for Yesod + - type-safe url rendering. + - + - It generates code like this: + - + - (toHtml + - (\ u_a2ehE -> urender_a2ehD u_a2ehE [] + - (CloseAlert aid))))); + - + - Where urender_a2ehD is the function returned by getUrlRenderParams. + - But, that function that only takes 2 params, not 3. + - And toHtml doesn't take a parameter at all! + - + - So, this modifes the code, to look like this: + - + - (toHtml + - (flip urender_a2ehD [] + - (CloseAlert aid))))); + - + - FIXME: Investigate and fix this properly. + -} +yesod_url_render_hack :: String -> String +yesod_url_render_hack = parsecAndReplace $ do + string "(toHtml" + whitespace + string "(\\" + whitespace + wtf <- token + whitespace + string "->" + whitespace + renderer <- token + whitespace + string wtf + whitespace + return $ "(toHtml (flip " ++ renderer ++ " " + where + whitespace :: Parser String + whitespace = many $ oneOf " \t\r\n" + + token :: Parser String + token = many1 $ satisfy isAlphaNum <|> oneOf "_" + +{- Use exported symbol. -} +text_builder_hack :: String -> String +text_builder_hack = replace "Data.Text.Lazy.Builder.Internal.fromText" "Data.Text.Lazy.Builder.fromText" + +{- Given a Parser that finds strings it wants to modify, + - and returns the modified string, does a mass + - find and replace throughout the input string. + - Rather slow, but crazy powerful. -} +parsecAndReplace :: Parser String -> String -> String +parsecAndReplace p s = case parse find "" s of + Left e -> s + Right l -> concatMap (either (\c -> [c]) id) l + where + find :: Parser [Either Char String] + find = many $ try (Right <$> p) <|> (Left <$> anyChar) + +main :: IO () +main = go =<< getArgs + where + go (destdir:log:header:[]) = run destdir log (Just header) + go (destdir:log:[]) = run destdir log Nothing + go _ = error "usage: EvilSplicer destdir logfile [headerfile]" + + run destdir log mheader = do + r <- parseFromFile splicesExtractor log + case r of + Left e -> error $ show e + Right splices -> do + let groups = groupBy (\a b -> splicedFile a == splicedFile b) splices + imports <- maybe (return Nothing) (catchMaybeIO . readFile) mheader + mapM_ (applySplices destdir imports) groups diff --git a/Build/InstallDesktopFile.hs b/Build/InstallDesktopFile.hs new file mode 100644 index 0000000000..c8a3f07f58 --- /dev/null +++ b/Build/InstallDesktopFile.hs @@ -0,0 +1,19 @@ +{- Generating and installing a desktop menu entry file and icon, + - and a desktop autostart file. (And OSX equivilants.) + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Main where + +import Build.DesktopFile + +import System.Environment + +main :: IO () +main = getArgs >>= go + where + go [] = error "specify git-annex command" + go (command:_) = install command diff --git a/Build/NullSoftInstaller.hs b/Build/NullSoftInstaller.hs new file mode 100644 index 0000000000..427507b021 --- /dev/null +++ b/Build/NullSoftInstaller.hs @@ -0,0 +1,139 @@ +{- Generates a NullSoft installer program for git-annex on Windows. + - + - To build the installer, git-annex should already be built by cabal, + - and ssh and rsync, as well as cygwin libraries, already installed. + - + - This uses the Haskell nsis package (cabal install nsis) + - to generate a .nsi file, which is then used to produce + - git-annex-installer.exe + - + - The installer includes git-annex, and utilities it uses, with the + - exception of git. The user needs to install git separately, + - and the installer checks for that. + - + - Copyright 2013 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE OverloadedStrings #-} + +import Development.NSIS +import System.Directory +import System.FilePath +import Control.Monad +import Data.String +import Data.Maybe + +import Utility.Tmp +import Utility.Path +import Utility.CopyFile +import Utility.SafeCommand +import Build.BundledPrograms + +main = do + withTmpDir "nsis-build" $ \tmpdir -> do + let gitannex = tmpdir gitannexprogram + mustSucceed "ln" [File "dist/build/git-annex/git-annex.exe", File gitannex] + let license = tmpdir licensefile + mustSucceed "sh" [Param "-c", Param $ "zcat standalone/licences.gz > '" ++ license ++ "'"] + extrafiles <- forM (cygwinPrograms ++ cygwinDlls) $ \f -> do + p <- searchPath f + when (isNothing p) $ + print ("unable to find in PATH", f) + return p + writeFile nsifile $ makeInstaller gitannex license $ + catMaybes extrafiles + mustSucceed "makensis" [File nsifile] + removeFile nsifile -- left behind if makensis fails + where + nsifile = "git-annex.nsi" + mustSucceed cmd params = do + r <- boolSystem cmd params + case r of + True -> return () + False -> error $ cmd ++ " failed" + +gitannexprogram :: FilePath +gitannexprogram = "git-annex.exe" + +licensefile :: FilePath +licensefile = "git-annex-licenses.txt" + +installer :: FilePath +installer = "git-annex-installer.exe" + +uninstaller :: FilePath +uninstaller = "git-annex-uninstall.exe" + +gitInstallDir :: Exp FilePath +gitInstallDir = fromString "$PROGRAMFILES\\Git\\cmd" + +needGit :: Exp String +needGit = strConcat + [ fromString "You need git installed to use git-annex. Looking at " + , gitInstallDir + , fromString " , it seems to not be installed, " + , fromString "or may be installed in another location. " + , fromString "You can install git from http:////git-scm.com//" + ] + +makeInstaller :: FilePath -> FilePath -> [FilePath] -> String +makeInstaller gitannex license extrafiles = nsis $ do + name "git-annex" + outFile $ str installer + {- Installing into the same directory as git avoids needing to modify + - path myself, since the git installer already does it. -} + installDir gitInstallDir + requestExecutionLevel User + + iff (fileExists gitInstallDir) + (return ()) + (alert needGit) + + -- Pages to display + page Directory -- Pick where to install + page (License license) + page InstFiles -- Give a progress bar while installing + -- Groups of files to install + section "main" [] $ do + setOutPath "$INSTDIR" + addfile gitannex + addfile license + mapM_ addfile extrafiles + writeUninstaller $ str uninstaller + uninstall $ + mapM_ (\f -> delete [RebootOK] $ fromString $ "$INSTDIR/" ++ f) $ + [ gitannexprogram + , licensefile + , uninstaller + ] ++ cygwinPrograms ++ cygwinDlls + where + addfile f = file [] (str f) + +cygwinPrograms :: [FilePath] +cygwinPrograms = map (\p -> p ++ ".exe") bundledPrograms + +-- These are the dlls needed by Cygwin's rsync, ssh, etc. +cygwinDlls :: [FilePath] +cygwinDlls = + [ "cygwin1.dll" + , "cygasn1-8.dll" + , "cygattr-1.dll" + , "cygheimbase-1.dll" + , "cygroken-18.dll" + , "cygcom_err-2.dll" + , "cygheimntlm-0.dll" + , "cygsqlite3-0.dll" + , "cygcrypt-0.dll" + , "cyghx509-5.dll" + , "cygssp-0.dll" + , "cygcrypto-1.0.0.dll" + , "cygiconv-2.dll" + , "cyggcc_s-1.dll" + , "cygintl-8.dll" + , "cygwind-0.dll" + , "cyggssapi-3.dll" + , "cygkrb5-26.dll" + , "cygz.dll" + ] diff --git a/Build/OSXMkLibs.hs b/Build/OSXMkLibs.hs new file mode 100644 index 0000000000..ed12a945f1 --- /dev/null +++ b/Build/OSXMkLibs.hs @@ -0,0 +1,157 @@ +{- OSX library copier + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Main where + +import Control.Applicative +import System.Environment +import Data.Maybe +import System.FilePath +import System.Directory +import System.IO +import Control.Monad +import Data.List + +import Utility.PartialPrelude +import Utility.Directory +import Utility.Process +import Utility.Monad +import Utility.SafeCommand +import Utility.Path +import Utility.Exception + +import qualified Data.Map as M +import qualified Data.Set as S + +type LibMap = M.Map FilePath String + +{- Recursively find and install libs, until nothing new to install is found. -} +mklibs :: FilePath -> [FilePath] -> [(FilePath, FilePath)] -> LibMap -> IO () +mklibs appbase libdirs replacement_libs libmap = do + (new, replacement_libs', libmap') <- installLibs appbase replacement_libs libmap + unless (null new) $ + mklibs appbase (libdirs++new) replacement_libs' libmap' + +{- Returns directories into which new libs were installed. -} +installLibs :: FilePath -> [(FilePath, FilePath)] -> LibMap -> IO ([FilePath], [(FilePath, FilePath)], LibMap) +installLibs appbase replacement_libs libmap = do + (needlibs, replacement_libs', libmap') <- otool appbase replacement_libs libmap + libs <- forM needlibs $ \lib -> do + let shortlib = fromMaybe (error "internal") (M.lookup lib libmap') + let fulllib = dropWhile (== '/') lib + let dest = appbase fulllib + let symdest = appbase shortlib + ifM (doesFileExist dest) + ( return Nothing + , do + createDirectoryIfMissing True (parentDir dest) + putStrLn $ "installing " ++ lib ++ " as " ++ shortlib + _ <- boolSystem "cp" [File lib, File dest] + _ <- boolSystem "chmod" [Param "644", File dest] + _ <- boolSystem "ln" [Param "-s", File fulllib, File symdest] + return $ Just appbase + ) + return (catMaybes libs, replacement_libs', libmap') + +{- Returns libraries to install. -} +otool :: FilePath -> [(FilePath, FilePath)] -> LibMap -> IO ([FilePath], [(FilePath, FilePath)], LibMap) +otool appbase replacement_libs libmap = do + files <- filterM doesFileExist =<< dirContentsRecursive appbase + process [] files replacement_libs libmap + where + want s = not ("@executable_path" `isInfixOf` s) + && not (".framework" `isInfixOf` s) + && not ("libSystem.B" `isInfixOf` s) + process c [] rls m = return (nub $ concat c, rls, m) + process c (file:rest) rls m = do + _ <- boolSystem "chmod" [Param "755", File file] + libs <- filter want . parseOtool + <$> readProcess "otool" ["-L", file] + expanded_libs <- expand_rpath libs replacement_libs file + let rls' = nub $ rls ++ (zip libs expanded_libs) + m' <- install_name_tool file libs expanded_libs m + process (expanded_libs:c) rest rls' m' + +{- Expands any @rpath in the list of libraries. + - + - This is done by the nasty method of running the command with a dummy + - option (so it doesn't do anything.. hopefully!) and asking the dynamic + - linker to print expanded rpaths. + -} +expand_rpath :: [String] -> [(FilePath, FilePath)] -> FilePath -> IO [String] +expand_rpath libs replacement_libs cmd + | any ("@rpath" `isInfixOf`) libs = do + installed <- M.fromList . Prelude.read + <$> readFile "tmp/standalone-installed" + let origcmd = case M.lookup cmd installed of + Nothing -> cmd + Just cmd' -> cmd' + s <- catchDefaultIO "" $ readProcess "sh" ["-c", probe origcmd] + let m = if (null s) + then M.fromList replacement_libs + else M.fromList $ mapMaybe parse $ lines s + return $ map (replace m) libs + | otherwise = return libs + where + probe c = "DYLD_PRINT_RPATHS=1 " ++ c ++ " --getting-rpath-dummy-option 2>&1 | grep RPATH" + parse s = case words s of + ("RPATH":"successful":"expansion":"of":old:"to:":new:[]) -> + Just (old, new) + _ -> Nothing + replace m l = fromMaybe l $ M.lookup l m + +parseOtool :: String -> [FilePath] +parseOtool = catMaybes . map parse . lines + where + parse l + | "\t" `isPrefixOf` l = headMaybe $ words l + | otherwise = Nothing + +{- Adjusts binaries to use libraries bundled with it, rather than the + - system libraries. -} +install_name_tool :: FilePath -> [FilePath] -> [FilePath] -> LibMap -> IO LibMap +install_name_tool _ [] _ libmap = return libmap +install_name_tool binary libs expanded_libs libmap = do + let (libnames, libmap') = getLibNames expanded_libs libmap + let params = concatMap change $ zip libs libnames + ok <- boolSystem "install_name_tool" $ params ++ [File binary] + unless ok $ + error $ "install_name_tool failed for " ++ binary + return libmap' + where + change (lib, libname) = + [ Param "-change" + , File lib + , Param $ "@executable_path/" ++ libname + ] + +getLibNames :: [FilePath] -> LibMap -> ([FilePath], LibMap) +getLibNames libs libmap = go [] libs libmap + where + go c [] m = (reverse c, m) + go c (l:rest) m = + let (f, m') = getLibName l m + in go (f:c) rest m' + +{- Uses really short names for the library files it installs, because + - binaries have arbitrarily short RPATH field limits. -} +getLibName :: FilePath -> LibMap -> (FilePath, LibMap) +getLibName lib libmap = case M.lookup lib libmap of + Just n -> (n, libmap) + Nothing -> (nextfreename, M.insert lib nextfreename libmap) + where + names = map (\c -> [c]) ['A' .. 'Z'] ++ + [[n, l] | n <- ['0' .. '9'], l <- ['A' .. 'Z']] + used = S.fromList $ M.elems libmap + nextfreename = fromMaybe (error "ran out of short library names!") $ + headMaybe $ dropWhile (`S.member` used) names + +main :: IO () +main = getArgs >>= go + where + go [] = error "specify OSXAPP_BASE" + go (appbase:_) = mklibs appbase [] [] M.empty diff --git a/Build/Standalone.hs b/Build/Standalone.hs new file mode 100644 index 0000000000..343daf9c97 --- /dev/null +++ b/Build/Standalone.hs @@ -0,0 +1,54 @@ +{- Makes standalone bundle. + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE CPP #-} + +module Main where + +import Control.Applicative +import Control.Monad.IfElse +import System.Environment +import Data.Maybe +import System.FilePath +import System.Directory +import System.IO +import Control.Monad +import Data.List +import Build.BundledPrograms + +import Utility.PartialPrelude +import Utility.Directory +import Utility.Process +import Utility.Monad +import Utility.SafeCommand +import Utility.Path + +progDir :: FilePath -> FilePath +#ifdef darwin_HOST_OS +progDir topdir = topdir +#else +progDir topdir = topdir "bin" +#endif + +installProg :: FilePath -> FilePath -> IO (FilePath, FilePath) +installProg dir prog = searchPath prog >>= go + where + go Nothing = error $ "cannot find " ++ prog ++ " in PATH" + go (Just f) = do + let dest = dir takeFileName f + unlessM (boolSystem "install" [File f, File dest]) $ + error $ "install failed for " ++ prog + return (dest, f) + +main = getArgs >>= go + where + go [] = error "specify topdir" + go (topdir:_) = do + let dir = progDir topdir + createDirectoryIfMissing True dir + installed <- forM bundledPrograms $ installProg dir + writeFile "tmp/standalone-installed" (show installed) diff --git a/Build/TestConfig.hs b/Build/TestConfig.hs new file mode 100644 index 0000000000..8628ebe58f --- /dev/null +++ b/Build/TestConfig.hs @@ -0,0 +1,143 @@ +{- Tests the system and generates Build.SysConfig.hs. -} + +module Build.TestConfig where + +import Utility.Path +import Utility.Monad +import Utility.SafeCommand + +import System.IO +import System.Cmd +import System.Exit +import System.FilePath +import System.Directory + +type ConfigKey = String +data ConfigValue = + BoolConfig Bool | + StringConfig String | + MaybeStringConfig (Maybe String) | + MaybeBoolConfig (Maybe Bool) +data Config = Config ConfigKey ConfigValue + +type Test = IO Config +type TestName = String +data TestCase = TestCase TestName Test + +instance Show ConfigValue where + show (BoolConfig b) = show b + show (StringConfig s) = show s + show (MaybeStringConfig s) = show s + show (MaybeBoolConfig s) = show s + +instance Show Config where + show (Config key value) = unlines + [ key ++ " :: " ++ valuetype value + , key ++ " = " ++ show value + ] + where + valuetype (BoolConfig _) = "Bool" + valuetype (StringConfig _) = "String" + valuetype (MaybeStringConfig _) = "Maybe String" + valuetype (MaybeBoolConfig _) = "Maybe Bool" + +writeSysConfig :: [Config] -> IO () +writeSysConfig config = writeFile "Build/SysConfig.hs" body + where + body = unlines $ header ++ map show config ++ footer + header = [ + "{- Automatically generated. -}" + , "module Build.SysConfig where" + , "" + ] + footer = [] + +runTests :: [TestCase] -> IO [Config] +runTests [] = return [] +runTests (TestCase tname t : ts) = do + testStart tname + c <- t + testEnd c + rest <- runTests ts + return $ c:rest + +{- Tests that a command is available, aborting if not. -} +requireCmd :: ConfigKey -> String -> Test +requireCmd k cmdline = do + ret <- testCmd k cmdline + handle ret + where + handle r@(Config _ (BoolConfig True)) = return r + handle r = do + testEnd r + error $ "** the " ++ c ++ " command is required" + c = head $ words cmdline + +{- Checks if a command is available by running a command line. -} +testCmd :: ConfigKey -> String -> Test +testCmd k cmdline = do + ok <- boolSystem "sh" [ Param "-c", Param $ quiet cmdline ] + return $ Config k (BoolConfig ok) + +{- Ensures that one of a set of commands is available by running each in + - turn. The Config is set to the first one found. -} +selectCmd :: ConfigKey -> [(String, String)] -> Test +selectCmd k = searchCmd + (return . Config k . StringConfig) + (\cmds -> do + testEnd $ Config k $ BoolConfig False + error $ "* need one of these commands, but none are available: " ++ show cmds + ) + +maybeSelectCmd :: ConfigKey -> [(String, String)] -> Test +maybeSelectCmd k = searchCmd + (return . Config k . MaybeStringConfig . Just) + (\_ -> return $ Config k $ MaybeStringConfig Nothing) + +searchCmd :: (String -> Test) -> ([String] -> Test) -> [(String, String)] -> Test +searchCmd success failure cmdsparams = search cmdsparams + where + search [] = failure $ fst $ unzip cmdsparams + search ((c, params):cs) = do + ok <- boolSystem "sh" [ Param "-c", Param $ quiet $ c ++ " " ++ params ] + if ok + then success c + else search cs + +{- Finds a command, either in PATH or perhaps in a sbin directory not in + - PATH. If it's in PATH the config is set to just the command name, + - but if it's found outside PATH, the config is set to the full path to + - the command. -} +findCmdPath :: ConfigKey -> String -> Test +findCmdPath k command = do + ifM (inPath command) + ( return $ Config k $ MaybeStringConfig $ Just command + , do + r <- getM find ["/usr/sbin", "/sbin", "/usr/local/sbin"] + return $ Config k $ MaybeStringConfig r + ) + where + find d = + let f = d command + in ifM (doesFileExist f) ( return (Just f), return Nothing ) + +quiet :: String -> String +quiet s = s ++ " >/dev/null 2>&1" + +testStart :: TestName -> IO () +testStart s = do + putStr $ " checking " ++ s ++ "..." + hFlush stdout + +testEnd :: Config -> IO () +testEnd (Config _ (BoolConfig True)) = status "yes" +testEnd (Config _ (BoolConfig False)) = status "no" +testEnd (Config _ (StringConfig s)) = status s +testEnd (Config _ (MaybeStringConfig (Just s))) = status s +testEnd (Config _ (MaybeStringConfig Nothing)) = status "not available" +testEnd (Config _ (MaybeBoolConfig (Just True))) = status "yes" +testEnd (Config _ (MaybeBoolConfig (Just False))) = status "no" +testEnd (Config _ (MaybeBoolConfig Nothing)) = status "unknown" + +status :: String -> IO () +status s = putStrLn $ ' ':s diff --git a/Build/make-sdist.sh b/Build/make-sdist.sh new file mode 100755 index 0000000000..9503345327 --- /dev/null +++ b/Build/make-sdist.sh @@ -0,0 +1,21 @@ +#!/bin/sh +# +# Workaround for `cabal sdist` requiring all included files to be listed +# in .cabal. + +# Create target directory +sdist_dir=git-annex-$(grep '^Version:' git-annex.cabal | sed -re 's/Version: *//') +mkdir --parents dist/$sdist_dir + +find . \( -name .git -or -name dist -or -name cabal-dev \) -prune \ + -or -not -name \\*.orig -not -type d -print \ +| perl -ne "print unless length >= 100 - length q{$sdist_dir}" \ +| xargs cp --parents --target-directory dist/$sdist_dir + +cd dist +tar -caf $sdist_dir.tar.gz $sdist_dir + +# Check that tarball can be unpacked by cabal. +# It's picky about tar longlinks etc. +rm -rf $sdist_dir +cabal unpack $sdist_dir.tar.gz diff --git a/Build/mdwn2man b/Build/mdwn2man new file mode 100755 index 0000000000..ba5919b385 --- /dev/null +++ b/Build/mdwn2man @@ -0,0 +1,43 @@ +#!/usr/bin/env perl +# Warning: hack + +my $prog=shift; +my $section=shift; + +print ".TH $prog $section\n"; + +while (<>) { + s{(\\?)\[\[([^\s\|\]]+)(\|[^\s\]]+)?\]\]}{$1 ? "[[$2]]" : $2}eg; + s/\`//g; + s/^\s*\./\\&./g; + if (/^#\s/) { + s/^#\s/.SH /; + <>; # blank; + } + s/^[ \n]+//; + s/^\t/ /; + s/-/\\-/g; + s/^Warning:.*//g; + s/^$/.PP\n/; + s/^\*\s+(.*)/.IP "$1"/; + next if $_ eq ".PP\n" && $skippara; + if (/^.IP /) { + $inlist=1; + $spippara=0; + } + elsif (/.SH/) { + $skippara=0; + $inlist=0; + } + elsif (/^\./) { + $skippara=1; + } + else { + $skippara=0; + } + if ($inlist && $_ eq ".PP\n") { + $_=".IP\n"; + } + + print $_; +} diff --git a/BuildFlags.hs b/BuildFlags.hs new file mode 100644 index 0000000000..c14bfef11f --- /dev/null +++ b/BuildFlags.hs @@ -0,0 +1,54 @@ +{- git-annex build flags reporting + - + - Copyright 2013 Joey Hess + - + - 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 +#ifdef WITH_FEEDS + , "Feeds" +#endif + ] diff --git a/CHANGELOG b/CHANGELOG new file mode 120000 index 0000000000..d526672ce2 --- /dev/null +++ b/CHANGELOG @@ -0,0 +1 @@ +debian/changelog \ No newline at end of file diff --git a/COPYRIGHT b/COPYRIGHT new file mode 120000 index 0000000000..9060ce8208 --- /dev/null +++ b/COPYRIGHT @@ -0,0 +1 @@ +debian/copyright \ No newline at end of file diff --git a/Checks.hs b/Checks.hs new file mode 100644 index 0000000000..67aa51a2af --- /dev/null +++ b/Checks.hs @@ -0,0 +1,49 @@ +{- git-annex command checks + - + - Common sanity checks for commands, and an interface to selectively + - remove them, or add others. + - + - Copyright 2011-2013 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Checks where + +import Common.Annex +import Types.Command +import Init +import Config +import Utility.Daemon +import qualified Git + +commonChecks :: [CommandCheck] +commonChecks = [repoExists] + +repoExists :: CommandCheck +repoExists = CommandCheck 0 ensureInitialized + +notDirect :: Command -> Command +notDirect = addCheck $ whenM isDirect $ + error "You cannot run this command in a direct mode repository." + +notBareRepo :: Command -> Command +notBareRepo = addCheck $ whenM (fromRepo Git.repoIsLocalBare) $ + error "You cannot run this command in a bare repository." + +noDaemonRunning :: Command -> Command +noDaemonRunning = addCheck $ whenM (isJust <$> daemonpid) $ + error "You cannot run this command while git-annex watch or git-annex assistant is running." + where + daemonpid = liftIO . checkDaemon =<< fromRepo gitAnnexPidFile + +dontCheck :: CommandCheck -> Command -> Command +dontCheck check cmd = mutateCheck cmd $ \c -> filter (/= check) c + +addCheck :: Annex () -> Command -> Command +addCheck check cmd = mutateCheck cmd $ \c -> + CommandCheck (length c + 100) check : c + +mutateCheck :: Command -> ([CommandCheck] -> [CommandCheck]) -> Command +mutateCheck cmd@(Command { cmdcheck = c }) a = cmd { cmdcheck = a c } + diff --git a/CmdLine.hs b/CmdLine.hs new file mode 100644 index 0000000000..83a89ef7db --- /dev/null +++ b/CmdLine.hs @@ -0,0 +1,138 @@ +{- git-annex command line parsing and dispatch + - + - Copyright 2010-2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE CPP #-} + +module CmdLine ( + dispatch, + usage, + shutdown +) where + +import qualified Control.Exception as E +import qualified Data.Map as M +import Control.Exception (throw) +import System.Console.GetOpt +#ifndef mingw32_HOST_OS +import System.Posix.Signals +#endif + +import Common.Annex +import qualified Annex +import qualified Annex.Queue +import qualified Git +import qualified Git.AutoCorrect +import Annex.Content +import Annex.Ssh +import Annex.Environment +import Command +import Types.Messages + +type Params = [String] +type Flags = [Annex ()] + +{- Runs the passed command line. -} +dispatch :: Bool -> Params -> [Command] -> [Option] -> [(String, String)] -> String -> IO Git.Repo -> IO () +dispatch fuzzyok allargs allcmds commonoptions fields header getgitrepo = do + setupConsole + r <- E.try getgitrepo :: IO (Either E.SomeException Git.Repo) + case r of + Left e -> fromMaybe (throw e) (cmdnorepo cmd) + Right g -> do + state <- Annex.new g + (actions, state') <- Annex.run state $ do + checkEnvironment + checkfuzzy + forM_ fields $ uncurry Annex.setField + when (cmdnomessages cmd) $ + Annex.setOutput QuietOutput + sequence_ flags + whenM (annexDebug <$> Annex.getGitConfig) $ + liftIO enableDebugOutput + prepCommand cmd params + tryRun state' cmd $ [startup] ++ actions ++ [shutdown $ cmdnocommit cmd] + where + err msg = msg ++ "\n\n" ++ usage header allcmds + cmd = Prelude.head cmds + (fuzzy, cmds, name, args) = findCmd fuzzyok allargs allcmds err + (flags, params) = getOptCmd args cmd commonoptions + checkfuzzy = when fuzzy $ + inRepo $ Git.AutoCorrect.prepare name cmdname cmds + +{- Parses command line params far enough to find the Command to run, and + - returns the remaining params. + - Does fuzzy matching if necessary, which may result in multiple Commands. -} +findCmd :: Bool -> Params -> [Command] -> (String -> String) -> (Bool, [Command], String, Params) +findCmd fuzzyok argv cmds err + | isNothing name = error $ err "missing command" + | not (null exactcmds) = (False, exactcmds, fromJust name, args) + | fuzzyok && not (null inexactcmds) = (True, inexactcmds, fromJust name, args) + | otherwise = error $ err $ "unknown command " ++ fromJust name + where + (name, args) = findname argv [] + findname [] c = (Nothing, reverse c) + findname (a:as) c + | "-" `isPrefixOf` a = findname as (a:c) + | otherwise = (Just a, reverse c ++ as) + exactcmds = filter (\c -> name == Just (cmdname c)) cmds + inexactcmds = case name of + Nothing -> [] + Just n -> Git.AutoCorrect.fuzzymatches n cmdname cmds + +{- Parses command line options, and returns actions to run to configure flags + - and the remaining parameters for the command. -} +getOptCmd :: Params -> Command -> [Option] -> (Flags, Params) +getOptCmd argv cmd commonoptions = check $ + getOpt Permute (commonoptions ++ cmdoptions cmd) argv + where + check (flags, rest, []) = (flags, rest) + check (_, _, errs) = error $ unlines + [ concat errs + , commandUsage cmd + ] + +{- Runs a list of Annex actions. Catches IO errors and continues + - (but explicitly thrown errors terminate the whole command). + -} +tryRun :: Annex.AnnexState -> Command -> [CommandCleanup] -> IO () +tryRun = tryRun' 0 +tryRun' :: Integer -> Annex.AnnexState -> Command -> [CommandCleanup] -> IO () +tryRun' errnum _ cmd [] + | errnum > 0 = error $ cmdname cmd ++ ": " ++ show errnum ++ " failed" + | otherwise = noop +tryRun' errnum state cmd (a:as) = do + r <- run + handle $! r + where + run = tryIO $ Annex.run state $ do + Annex.Queue.flushWhenFull + a + handle (Left err) = showerr err >> cont False state + handle (Right (success, state')) = cont success state' + cont success s = do + let errnum' = if success then errnum else errnum + 1 + (tryRun' $! errnum') s cmd as + showerr err = Annex.eval state $ do + showErr err + showEndFail + +{- Actions to perform each time ran. -} +startup :: Annex Bool +startup = liftIO $ do +#ifndef mingw32_HOST_OS + void $ installHandler sigINT Default Nothing +#endif + return True + +{- Cleanup actions. -} +shutdown :: Bool -> Annex Bool +shutdown nocommit = do + saveState nocommit + sequence_ =<< M.elems <$> Annex.getState Annex.cleanup + liftIO reapZombies -- zombies from long-running git processes + sshCleanup -- ssh connection caching + return True diff --git a/Command.hs b/Command.hs new file mode 100644 index 0000000000..fec733f724 --- /dev/null +++ b/Command.hs @@ -0,0 +1,123 @@ +{- git-annex command infrastructure + - + - Copyright 2010-2011 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Command ( + command, + noRepo, + noCommit, + noMessages, + withOptions, + next, + stop, + stopUnless, + prepCommand, + doCommand, + whenAnnexed, + ifAnnexed, + isBareRepo, + numCopies, + numCopiesCheck, + checkAuto, + module ReExported +) where + +import Common.Annex +import qualified Backend +import qualified Annex +import qualified Git +import qualified Remote +import Types.Command as ReExported +import Types.Option as ReExported +import Seek as ReExported +import Checks as ReExported +import Usage as ReExported +import Logs.Trust +import Config +import Annex.CheckAttr + +{- Generates a normal command -} +command :: String -> String -> [CommandSeek] -> CommandSection -> String -> Command +command = Command [] Nothing commonChecks False False + +{- Indicates that a command doesn't need to commit any changes to + - the git-annex branch. -} +noCommit :: Command -> Command +noCommit c = c { cmdnocommit = True } + +{- Indicates that a command should not output anything other than what + - it directly sends to stdout. (--json can override this). -} +noMessages :: Command -> Command +noMessages c = c { cmdnomessages = True } + +{- Adds a fallback action to a command, that will be run if it's used + - outside a git repository. -} +noRepo :: IO () -> Command -> Command +noRepo a c = c { cmdnorepo = Just a } + +{- Adds options to a command. -} +withOptions :: [Option] -> Command -> Command +withOptions o c = c { cmdoptions = o } + +{- For start and perform stages to indicate what step to run next. -} +next :: a -> Annex (Maybe a) +next a = return $ Just a + +{- Or to indicate nothing needs to be done. -} +stop :: Annex (Maybe a) +stop = return Nothing + +{- Stops unless a condition is met. -} +stopUnless :: Annex Bool -> Annex (Maybe a) -> Annex (Maybe a) +stopUnless c a = ifM c ( a , stop ) + +{- Prepares to run a command via the check and seek stages, returning a + - list of actions to perform to run the command. -} +prepCommand :: Command -> [String] -> Annex [CommandCleanup] +prepCommand Command { cmdseek = seek, cmdcheck = c } params = do + mapM_ runCheck c + map doCommand . concat <$> mapM (\s -> s params) seek + +{- Runs a command through the start, perform and cleanup stages -} +doCommand :: CommandStart -> CommandCleanup +doCommand = start + where + start = stage $ maybe skip perform + perform = stage $ maybe failure cleanup + cleanup = stage $ status + stage = (=<<) + skip = return True + failure = showEndFail >> return False + status r = showEndResult r >> return r + +{- Modifies an action to only act on files that are already annexed, + - and passes the key and backend on to it. -} +whenAnnexed :: (FilePath -> (Key, Backend) -> Annex (Maybe a)) -> FilePath -> Annex (Maybe a) +whenAnnexed a file = ifAnnexed file (a file) (return Nothing) + +ifAnnexed :: FilePath -> ((Key, Backend) -> Annex a) -> Annex a -> Annex a +ifAnnexed file yes no = maybe no yes =<< Backend.lookupFile file + +isBareRepo :: Annex Bool +isBareRepo = fromRepo Git.repoIsLocalBare + +numCopies :: FilePath -> Annex (Maybe Int) +numCopies file = do + forced <- Annex.getState Annex.forcenumcopies + case forced of + Just n -> return $ Just n + Nothing -> readish <$> checkAttr "annex.numcopies" file + +numCopiesCheck :: FilePath -> Key -> (Int -> Int -> Bool) -> Annex Bool +numCopiesCheck file key vs = do + numcopiesattr <- numCopies file + needed <- getNumCopies numcopiesattr + have <- trustExclude UnTrusted =<< Remote.keyLocations key + return $ length have `vs` needed + +checkAuto :: Annex Bool -> Annex Bool +checkAuto checker = ifM (Annex.getState Annex.auto) + ( checker , return True ) diff --git a/Command/Add.hs b/Command/Add.hs new file mode 100644 index 0000000000..245ca2bd6c --- /dev/null +++ b/Command/Add.hs @@ -0,0 +1,251 @@ +{- git-annex command + - + - Copyright 2010, 2013 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE CPP #-} + +module Command.Add where + +import System.PosixCompat.Files + +import Common.Annex +import Annex.Exception +import Command +import Types.KeySource +import Backend +import Logs.Location +import Annex.Content +import Annex.Content.Direct +import Annex.Perms +import Annex.Link +import qualified Annex +import qualified Annex.Queue +#ifndef __ANDROID__ +import Utility.Touch +#endif +import Utility.FileMode +import Config +import Utility.InodeCache +import Annex.FileMatcher +import Annex.ReplaceFile +import Utility.Tmp + +def :: [Command] +def = [notBareRepo $ command "add" paramPaths seek SectionCommon + "add files to annex"] + +{- Add acts on both files not checked into git yet, and unlocked files. + - + - In direct mode, it acts on any files that have changed. -} +seek :: [CommandSeek] +seek = + [ go withFilesNotInGit + , whenNotDirect $ go withFilesUnlocked + , whenDirect $ go withFilesMaybeModified + ] + where + go a = withValue largeFilesMatcher $ \matcher -> + a $ \file -> ifM (checkFileMatcher matcher file <||> Annex.getState Annex.force) + ( start file + , stop + ) + +{- The add subcommand annexes a file, generating a key for it using a + - backend, and then moving it into the annex directory and setting up + - the symlink pointing to its content. -} +start :: FilePath -> CommandStart +start file = ifAnnexed file addpresent add + where + add = do + ms <- liftIO $ catchMaybeIO $ getSymbolicLinkStatus file + case ms of + Nothing -> stop + Just s + | isSymbolicLink s || not (isRegularFile s) -> stop + | otherwise -> do + showStart "add" file + next $ perform file + addpresent (key, _) = ifM isDirect + ( ifM (goodContent key file) ( stop , add ) + , fixup key + ) + fixup key = do + -- fixup from an interrupted add; the symlink + -- is present but not yet added to git + showStart "add" file + liftIO $ removeFile file + next $ next $ cleanup file key =<< inAnnex key + +{- The file that's being added is locked down before a key is generated, + - to prevent it from being modified in between. This lock down is not + - perfect at best (and pretty weak at worst). For example, it does not + - guard against files that are already opened for write by another process. + - So a KeySource is returned. Its inodeCache can be used to detect any + - changes that might be made to the file after it was locked down. + - + - In indirect mode, the write bit is removed from the file as part of lock + - down to guard against further writes, and because objects in the annex + - have their write bit disabled anyway. This is not done in direct mode, + - because files there need to remain writable at all times. + - + - When possible, the file is hard linked to a temp directory. This guards + - against some changes, like deletion or overwrite of the file, and + - allows lsof checks to be done more efficiently when adding a lot of files. + - + - Lockdown can fail if a file gets deleted, and Nothing will be returned. + -} +lockDown :: FilePath -> Annex (Maybe KeySource) +lockDown file = ifM (crippledFileSystem) + ( liftIO $ catchMaybeIO nohardlink + , do + tmp <- fromRepo gitAnnexTmpDir + createAnnexDirectory tmp + unlessM (isDirect) $ liftIO $ + void $ tryIO $ preventWrite file + liftIO $ catchMaybeIO $ do + (tmpfile, h) <- openTempFile tmp $ + relatedTemplate $ takeFileName file + hClose h + nukeFile tmpfile + withhardlink tmpfile `catchIO` const nohardlink + ) + where + nohardlink = do + cache <- genInodeCache file + return $ KeySource + { keyFilename = file + , contentLocation = file + , inodeCache = cache + } + withhardlink tmpfile = do + createLink file tmpfile + cache <- genInodeCache tmpfile + return $ KeySource + { keyFilename = file + , contentLocation = tmpfile + , inodeCache = cache + } + +{- Ingests a locked down file into the annex. + - + - In direct mode, leaves the file alone, and just updates bookkeeping + - information. + -} +ingest :: (Maybe KeySource) -> Annex (Maybe Key) +ingest Nothing = return Nothing +ingest (Just source) = do + backend <- chooseBackend $ keyFilename source + k <- genKey source backend + cache <- liftIO $ genInodeCache $ contentLocation source + case (cache, inodeCache source) of + (_, Nothing) -> go k cache + (Just newc, Just c) | compareStrong c newc -> go k cache + _ -> failure "changed while it was being added" + where + go k cache = ifM isDirect ( godirect k cache , goindirect k cache ) + + goindirect (Just (key, _)) _ = do + catchAnnex (moveAnnex key $ contentLocation source) + (undo (keyFilename source) key) + liftIO $ nukeFile $ keyFilename source + return $ Just key + goindirect Nothing _ = failure "failed to generate a key" + + godirect (Just (key, _)) (Just cache) = do + addInodeCache key cache + finishIngestDirect key source + return $ Just key + godirect _ _ = failure "failed to generate a key" + + failure msg = do + warning $ keyFilename source ++ " " ++ msg + when (contentLocation source /= keyFilename source) $ + liftIO $ nukeFile $ contentLocation source + return Nothing + +finishIngestDirect :: Key -> KeySource -> Annex () +finishIngestDirect key source = do + void $ addAssociatedFile key $ keyFilename source + when (contentLocation source /= keyFilename source) $ + liftIO $ nukeFile $ contentLocation source + + {- Copy to any other locations using the same key. -} + otherfs <- filter (/= keyFilename source) <$> associatedFiles key + forM_ otherfs $ + addContentWhenNotPresent key (keyFilename source) + +perform :: FilePath -> CommandPerform +perform file = + maybe stop (\key -> next $ cleanup file key True) + =<< ingest =<< lockDown file + +{- On error, put the file back so it doesn't seem to have vanished. + - This can be called before or after the symlink is in place. -} +undo :: FilePath -> Key -> IOException -> Annex a +undo file key e = do + whenM (inAnnex key) $ do + liftIO $ nukeFile file + catchAnnex (fromAnnex key file) tryharder + logStatus key InfoMissing + throwAnnex e + where + -- fromAnnex could fail if the file ownership is weird + tryharder :: IOException -> Annex () + tryharder _ = do + src <- calcRepo $ gitAnnexLocation key + liftIO $ moveFile src file + +{- Creates the symlink to the annexed content, returns the link target. -} +link :: FilePath -> Key -> Bool -> Annex String +link file key hascontent = flip catchAnnex (undo file key) $ do + l <- inRepo $ gitAnnexLink file key + replaceFile file $ makeAnnexLink l + +#ifndef __ANDROID__ + when hascontent $ do + -- touch the symlink to have the same mtime as the + -- file it points to + liftIO $ do + mtime <- modificationTime <$> getFileStatus file + touch file (TimeSpec mtime) False +#endif + + return l + +{- Creates the symlink to the annexed content, and stages it in git. + - + - As long as the filesystem supports symlinks, we use + - git add, rather than directly staging the symlink to git. + - Using git add is best because it allows the queuing to work + - and is faster (staging the symlink runs hash-object commands each time). + - Also, using git add allows it to skip gitignored files, unless forced + - to include them. + -} +addLink :: FilePath -> Key -> Bool -> Annex () +addLink file key hascontent = ifM (coreSymlinks <$> Annex.getGitConfig) + ( do + _ <- link file key hascontent + params <- ifM (Annex.getState Annex.force) + ( return [Param "-f"] + , return [] + ) + Annex.Queue.addCommand "add" (params++[Param "--"]) [file] + , do + l <- link file key hascontent + addAnnexLink l file + ) + +cleanup :: FilePath -> Key -> Bool -> CommandCleanup +cleanup file key hascontent = do + when hascontent $ + logStatus key InfoPresent + ifM (isDirect <&&> pure hascontent) + ( do + l <- inRepo $ gitAnnexLink file key + stageSymlink file =<< hashSymlink l + , addLink file key hascontent + ) + return True diff --git a/Command/AddUnused.hs b/Command/AddUnused.hs new file mode 100644 index 0000000000..21a75137ff --- /dev/null +++ b/Command/AddUnused.hs @@ -0,0 +1,41 @@ +{- git-annex command + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Command.AddUnused where + +import Common.Annex +import Logs.Location +import Command +import qualified Command.Add +import Command.Unused (withUnusedMaps, UnusedMaps(..), startUnused) +import Types.Key + +def :: [Command] +def = [notDirect $ command "addunused" (paramRepeating paramNumRange) + seek SectionMaintenance "add back unused files"] + +seek :: [CommandSeek] +seek = [withUnusedMaps start] + +start :: UnusedMaps -> Int -> CommandStart +start = startUnused "addunused" perform + (performOther "bad") + (performOther "tmp") + +perform :: Key -> CommandPerform +perform key = next $ do + logStatus key InfoPresent + Command.Add.addLink file key False + return True + where + file = "unused." ++ key2file key + +{- The content is not in the annex, but in another directory, and + - it seems better to error out, rather than moving bad/tmp content into + - the annex. -} +performOther :: String -> Key -> CommandPerform +performOther other _ = error $ "cannot addunused " ++ other ++ "content" diff --git a/Command/AddUrl.hs b/Command/AddUrl.hs new file mode 100644 index 0000000000..d172a6869a --- /dev/null +++ b/Command/AddUrl.hs @@ -0,0 +1,174 @@ +{- git-annex command + - + - Copyright 2011-2013 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Command.AddUrl where + +import Network.URI + +import Common.Annex +import Command +import Backend +import qualified Command.Add +import qualified Annex +import qualified Annex.Queue +import qualified Backend.URL +import qualified Utility.Url as Url +import Annex.Content +import Logs.Web +import qualified Option +import Types.Key +import Types.KeySource +import Config +import Annex.Content.Direct +import Logs.Location +import qualified Logs.Transfer as Transfer +import Utility.Daemon (checkDaemon) + +def :: [Command] +def = [notBareRepo $ withOptions [fileOption, pathdepthOption, relaxedOption] $ + command "addurl" (paramRepeating paramUrl) seek + SectionCommon "add urls to annex"] + +fileOption :: Option +fileOption = Option.field [] "file" paramFile "specify what file the url is added to" + +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 relaxed f d] + +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 + pathmax <- liftIO $ fileNameLengthLimit "." + let file = fromMaybe (url2file url pathdepth pathmax) optfile + showStart "addurl" file + next $ perform relaxed s file + +perform :: Bool -> String -> FilePath -> CommandPerform +perform relaxed url file = ifAnnexed file addurl geturl + where + geturl = next $ addUrlFile relaxed 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 exists: " ++ url + stop + ) + +addUrlFile :: Bool -> String -> FilePath -> Annex Bool +addUrlFile relaxed url file = do + liftIO $ createDirectoryIfMissing True (parentDir file) + ifM (Annex.getState Annex.fast <||> pure relaxed) + ( nodownload relaxed url file + , do + showAction $ "downloading " ++ url ++ " " + download url file + ) + +download :: String -> FilePath -> Annex Bool +download url file = do + dummykey <- genkey + tmp <- fromRepo $ gitAnnexTmpLocation dummykey + showOutput + ifM (runtransfer dummykey tmp) + ( do + backend <- chooseBackend file + let source = KeySource + { keyFilename = file + , contentLocation = tmp + , inodeCache = Nothing + } + k <- genKey source backend + case k of + Nothing -> return False + Just (key, _) -> cleanup url file key (Just tmp) + , return False + ) + where + {- Generate a dummy key to use for this download, before we can + - examine the file and find its real key. This allows resuming + - downloads, as the dummy key for a given url is stable. + - + - If the assistant is running, actually hits the url here, + - to get the size, so it can display a pretty progress bar. + -} + genkey = do + pidfile <- fromRepo gitAnnexPidFile + size <- ifM (liftIO $ isJust <$> checkDaemon pidfile) + ( do + headers <- getHttpHeaders + liftIO $ snd <$> Url.exists url headers + , return Nothing + ) + Backend.URL.fromUrl url size + runtransfer dummykey tmp = + Transfer.download webUUID dummykey (Just file) Transfer.forwardRetry $ const $ do + liftIO $ createDirectoryIfMissing True (parentDir tmp) + downloadUrl [url] tmp + + +cleanup :: String -> FilePath -> Key -> Maybe FilePath -> Annex Bool +cleanup url file key mtmp = do + when (isJust mtmp) $ + logStatus key InfoPresent + setUrlPresent key url + Command.Add.addLink file key False + whenM isDirect $ do + void $ addAssociatedFile key file + {- For moveAnnex to work in direct mode, the symlink + - must already exist, so flush the queue. -} + Annex.Queue.flush + maybe noop (moveAnnex key) mtmp + return True + +nodownload :: Bool -> String -> FilePath -> Annex Bool +nodownload relaxed url file = do + headers <- getHttpHeaders + (exists, size) <- if relaxed + then pure (True, Nothing) + else liftIO $ Url.exists url headers + if exists + then do + key <- Backend.URL.fromUrl url size + cleanup url file key Nothing + else do + warning $ "unable to access url: " ++ url + return False + +url2file :: URI -> Maybe Int -> Int -> FilePath +url2file url pathdepth pathmax = case pathdepth of + Nothing -> truncateFilePath pathmax $ escape fullurl + Just depth + | depth >= length urlbits -> frombits id + | depth > 0 -> frombits $ drop depth + | depth < 0 -> frombits $ reverse . take (negate depth) . reverse + | otherwise -> error "bad --pathdepth" + where + fullurl = uriRegName auth ++ uriPath url ++ uriQuery url + frombits a = intercalate "/" $ a urlbits + urlbits = map (truncateFilePath pathmax . escape) $ filter (not . null) $ split "/" fullurl + auth = fromMaybe (error $ "bad url " ++ show url) $ uriAuthority url + escape = replace "/" "_" . replace "?" "_" diff --git a/Command/Assistant.hs b/Command/Assistant.hs new file mode 100644 index 0000000000..f65bed7367 --- /dev/null +++ b/Command/Assistant.hs @@ -0,0 +1,71 @@ +{- git-annex assistant + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Command.Assistant where + +import Common.Annex +import Command +import qualified Option +import qualified Command.Watch +import Init +import Config.Files +import qualified Build.SysConfig + +import System.Environment + +def :: [Command] +def = [noRepo checkAutoStart $ dontCheck repoExists $ + withOptions [Command.Watch.foregroundOption, Command.Watch.stopOption, autoStartOption] $ + command "assistant" paramNothing seek SectionCommon + "automatically handle changes"] + +autoStartOption :: Option +autoStartOption = Option.flag [] "autostart" "start in known repositories" + +seek :: [CommandSeek] +seek = [withFlag Command.Watch.stopOption $ \stopdaemon -> + withFlag Command.Watch.foregroundOption $ \foreground -> + withFlag autoStartOption $ \autostart -> + withNothing $ start foreground stopdaemon autostart] + +start :: Bool -> Bool -> Bool -> CommandStart +start foreground stopdaemon autostart + | autostart = do + liftIO autoStart + stop + | otherwise = do + ensureInitialized + Command.Watch.start True foreground stopdaemon + +{- Run outside a git repository. Check to see if any parameter is + - --autostart and enter autostart mode. -} +checkAutoStart :: IO () +checkAutoStart = ifM (elem "--autostart" <$> getArgs) + ( autoStart + , error "Not in a git repository." + ) + +autoStart :: IO () +autoStart = do + dirs <- liftIO readAutoStartFile + when (null dirs) $ do + f <- autoStartFile + error $ "Nothing listed in " ++ f + program <- readProgramFile + haveionice <- pure Build.SysConfig.ionice <&&> inPath "ionice" + forM_ dirs $ \d -> do + putStrLn $ "git-annex autostart in " ++ d + ifM (catchBoolIO $ go haveionice program d) + ( putStrLn "ok" + , putStrLn "failed" + ) + where + go haveionice program dir = do + setCurrentDirectory dir + if haveionice + then boolSystem "ionice" [Param "-c3", Param program, Param "assistant"] + else boolSystem program [Param "assistant"] diff --git a/Command/Commit.hs b/Command/Commit.hs new file mode 100644 index 0000000000..6f3f9df285 --- /dev/null +++ b/Command/Commit.hs @@ -0,0 +1,29 @@ +{- git-annex command + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Command.Commit where + +import Common.Annex +import Command +import qualified Annex.Branch +import qualified Git + +def :: [Command] +def = [command "commit" paramNothing seek + SectionPlumbing "commits any staged changes to the git-annex branch"] + +seek :: [CommandSeek] +seek = [withNothing start] + +start :: CommandStart +start = next $ next $ do + Annex.Branch.commit "update" + _ <- runhook <=< inRepo $ Git.hookPath "annex-content" + return True + where + runhook (Just hook) = liftIO $ boolSystem hook [] + runhook Nothing = return True diff --git a/Command/ConfigList.hs b/Command/ConfigList.hs new file mode 100644 index 0000000000..703d6882d8 --- /dev/null +++ b/Command/ConfigList.hs @@ -0,0 +1,25 @@ +{- git-annex command + - + - Copyright 2010 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Command.ConfigList where + +import Common.Annex +import Command +import Annex.UUID + +def :: [Command] +def = [noCommit $ command "configlist" paramNothing seek + SectionPlumbing "outputs relevant git configuration"] + +seek :: [CommandSeek] +seek = [withNothing start] + +start :: CommandStart +start = do + u <- getUUID + liftIO $ putStrLn $ "annex.uuid=" ++ fromUUID u + stop diff --git a/Command/Content.hs b/Command/Content.hs new file mode 100644 index 0000000000..d10bdde3c7 --- /dev/null +++ b/Command/Content.hs @@ -0,0 +1,48 @@ +{- git-annex command + - + - Copyright 2013 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Command.Content where + +import Common.Annex +import Command +import qualified Remote +import Logs.PreferredContent + +import qualified Data.Map as M + +def :: [Command] +def = [command "content" (paramPair paramRemote (paramOptional paramExpression)) seek + SectionSetup "get or set preferred content expression"] + +seek :: [CommandSeek] +seek = [withWords start] + +start :: [String] -> CommandStart +start = parse + where + parse (name:[]) = go name performGet + parse (name:expr:[]) = go name $ \uuid -> do + showStart "content" name + performSet expr uuid + parse _ = error "Specify a repository." + + go name a = do + u <- Remote.nameToUUID name + next $ a u + +performGet :: UUID -> CommandPerform +performGet uuid = do + m <- preferredContentMapRaw + liftIO $ putStrLn $ fromMaybe "" $ M.lookup uuid m + next $ return True + +performSet :: String -> UUID -> CommandPerform +performSet expr uuid = case checkPreferredContentExpression expr of + Just e -> error $ "Parse error: " ++ e + Nothing -> do + preferredContentSet uuid expr + next $ return True diff --git a/Command/Copy.hs b/Command/Copy.hs new file mode 100644 index 0000000000..4e1646ad1e --- /dev/null +++ b/Command/Copy.hs @@ -0,0 +1,38 @@ +{- git-annex command + - + - Copyright 2010 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Command.Copy where + +import Common.Annex +import Command +import qualified Command.Move +import qualified Remote +import Annex.Wanted + +def :: [Command] +def = [withOptions Command.Move.moveOptions $ command "copy" paramPaths seek + SectionCommon "copy content of files to/from another repository"] + +seek :: [CommandSeek] +seek = + [ withField Command.Move.toOption Remote.byNameWithUUID $ \to -> + withField Command.Move.fromOption Remote.byNameWithUUID $ \from -> + withKeyOptions (Command.Move.startKey to from False) $ + withFilesInGit $ whenAnnexed $ start to from + ] + +{- A copy is just a move that does not delete the source file. + - However, --auto mode avoids unnecessary copies, and avoids getting or + - sending non-preferred content. -} +start :: Maybe Remote -> Maybe Remote -> FilePath -> (Key, Backend) -> CommandStart +start to from file (key, backend) = stopUnless shouldCopy $ + Command.Move.start to from False file (key, backend) + where + shouldCopy = checkAuto (check <||> numCopiesCheck file key (<)) + check = case to of + Nothing -> wantGet False (Just file) + Just r -> wantSend False (Just file) (Remote.uuid r) diff --git a/Command/Dead.hs b/Command/Dead.hs new file mode 100644 index 0000000000..180f2fda90 --- /dev/null +++ b/Command/Dead.hs @@ -0,0 +1,40 @@ +{- git-annex command + - + - Copyright 2011 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Command.Dead where + +import Common.Annex +import Command +import qualified Remote +import Logs.Trust +import Logs.Group + +import qualified Data.Set as S + +def :: [Command] +def = [command "dead" (paramRepeating paramRemote) seek + SectionSetup "hide a lost repository"] + +seek :: [CommandSeek] +seek = [withWords start] + +start :: [String] -> CommandStart +start ws = do + let name = unwords ws + showStart "dead" name + u <- Remote.nameToUUID name + next $ perform u + +perform :: UUID -> CommandPerform +perform uuid = do + markDead uuid + next $ return True + +markDead :: UUID -> Annex () +markDead uuid = do + trustSet uuid DeadTrusted + groupSet uuid S.empty diff --git a/Command/Describe.hs b/Command/Describe.hs new file mode 100644 index 0000000000..18851b1726 --- /dev/null +++ b/Command/Describe.hs @@ -0,0 +1,32 @@ +{- git-annex command + - + - Copyright 2011 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Command.Describe where + +import Common.Annex +import Command +import qualified Remote +import Logs.UUID + +def :: [Command] +def = [command "describe" (paramPair paramRemote paramDesc) seek + SectionSetup "change description of a repository"] + +seek :: [CommandSeek] +seek = [withWords start] + +start :: [String] -> CommandStart +start (name:description) = do + showStart "describe" name + u <- Remote.nameToUUID name + next $ perform u $ unwords description +start _ = error "Specify a repository and a description." + +perform :: UUID -> String -> CommandPerform +perform u description = do + describeUUID u description + next $ return True diff --git a/Command/Direct.hs b/Command/Direct.hs new file mode 100644 index 0000000000..7835988b46 --- /dev/null +++ b/Command/Direct.hs @@ -0,0 +1,63 @@ +{- git-annex command + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Command.Direct where + +import Common.Annex +import Command +import qualified Git +import qualified Git.Command +import qualified Git.LsFiles +import Config +import Annex.Direct +import Annex.Version + +def :: [Command] +def = [notBareRepo $ noDaemonRunning $ + command "direct" paramNothing seek + SectionSetup "switch repository to direct mode"] + +seek :: [CommandSeek] +seek = [withNothing start] + +start :: CommandStart +start = ifM isDirect ( stop , next perform ) + +perform :: CommandPerform +perform = do + showStart "commit" "" + showOutput + _ <- inRepo $ Git.Command.runBool + [ Param "commit" + , Param "-a" + , Param "-m" + , Param "commit before switching to direct mode" + ] + showEndOk + + top <- fromRepo Git.repoPath + (l, clean) <- inRepo $ Git.LsFiles.inRepo [top] + forM_ l go + void $ liftIO clean + next cleanup + where + go = whenAnnexed $ \f (k, _) -> do + r <- toDirectGen k f + case r of + Nothing -> noop + Just a -> do + showStart "direct" f + a + showEndOk + return Nothing + +cleanup :: CommandCleanup +cleanup = do + showStart "direct" "" + setDirect True + setVersion directModeVersion + return True diff --git a/Command/Drop.hs b/Command/Drop.hs new file mode 100644 index 0000000000..b3f7d75740 --- /dev/null +++ b/Command/Drop.hs @@ -0,0 +1,161 @@ +{- git-annex command + - + - Copyright 2010 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Command.Drop where + +import Common.Annex +import Command +import qualified Remote +import qualified Annex +import Annex.UUID +import Logs.Location +import Logs.Trust +import Annex.Content +import Config +import qualified Option +import Annex.Wanted + +def :: [Command] +def = [withOptions [fromOption] $ command "drop" paramPaths seek + SectionCommon "indicate content of files not currently wanted"] + +fromOption :: Option +fromOption = Option.field ['f'] "from" paramRemote "drop content from a remote" + +seek :: [CommandSeek] +seek = [withField fromOption Remote.byNameWithUUID $ \from -> + withFilesInGit $ whenAnnexed $ start from] + +start :: Maybe Remote -> FilePath -> (Key, Backend) -> CommandStart +start from file (key, _) = checkDropAuto from file key $ \numcopies -> + stopUnless (checkAuto $ wantDrop False (Remote.uuid <$> from) (Just file)) $ + case from of + Nothing -> startLocal file numcopies key Nothing + Just remote -> do + u <- getUUID + if Remote.uuid remote == u + then startLocal file numcopies key Nothing + else startRemote file numcopies key remote + +startLocal :: FilePath -> Maybe Int -> Key -> Maybe Remote -> CommandStart +startLocal file numcopies key knownpresentremote = stopUnless (inAnnex key) $ do + showStart "drop" file + next $ performLocal key numcopies knownpresentremote + +startRemote :: FilePath -> Maybe Int -> Key -> Remote -> CommandStart +startRemote file numcopies key remote = do + showStart ("drop " ++ Remote.name remote) file + next $ performRemote key numcopies remote + +performLocal :: Key -> Maybe Int -> Maybe Remote -> CommandPerform +performLocal key numcopies knownpresentremote = lockContent key $ do + (remotes, trusteduuids) <- Remote.keyPossibilitiesTrusted key + let trusteduuids' = case knownpresentremote of + Nothing -> trusteduuids + Just r -> nub (Remote.uuid r:trusteduuids) + untrusteduuids <- trustGet UnTrusted + let tocheck = Remote.remotesWithoutUUID remotes (trusteduuids'++untrusteduuids) + stopUnless (canDropKey key numcopies trusteduuids' tocheck []) $ do + removeAnnex key + next $ cleanupLocal key + +performRemote :: Key -> Maybe Int -> Remote -> CommandPerform +performRemote key numcopies remote = lockContent key $ do + -- Filter the remote it's being dropped from out of the lists of + -- places assumed to have the key, and places to check. + -- When the local repo has the key, that's one additional copy. + (remotes, trusteduuids) <- Remote.keyPossibilitiesTrusted key + present <- inAnnex key + u <- getUUID + let have = filter (/= uuid) $ + if present then u:trusteduuids else trusteduuids + untrusteduuids <- trustGet UnTrusted + let tocheck = filter (/= remote) $ + Remote.remotesWithoutUUID remotes (have++untrusteduuids) + stopUnless (canDropKey key numcopies have tocheck [uuid]) $ do + ok <- Remote.removeKey remote key + next $ cleanupRemote key remote ok + where + uuid = Remote.uuid remote + +cleanupLocal :: Key -> CommandCleanup +cleanupLocal key = do + logStatus key InfoMissing + return True + +cleanupRemote :: Key -> Remote -> Bool -> CommandCleanup +cleanupRemote key remote ok = do + when ok $ + Remote.logStatus remote key InfoMissing + return ok + +{- Checks specified remotes to verify that enough copies of a key exist to + - allow it to be safely removed (with no data loss). Can be provided with + - some locations where the key is known/assumed to be present. -} +canDropKey :: Key -> Maybe Int -> [UUID] -> [Remote] -> [UUID] -> Annex Bool +canDropKey key numcopiesM have check skip = do + force <- Annex.getState Annex.force + if force || numcopiesM == Just 0 + then return True + else do + need <- getNumCopies numcopiesM + findCopies key need skip have check + +findCopies :: Key -> Int -> [UUID] -> [UUID] -> [Remote] -> Annex Bool +findCopies key need skip = helper [] [] + where + helper bad missing have [] + | length have >= need = return True + | otherwise = notEnoughCopies key need have (skip++missing) bad + helper bad missing have (r:rs) + | length have >= need = return True + | otherwise = do + let u = Remote.uuid r + let duplicate = u `elem` have + haskey <- Remote.hasKey r key + case (duplicate, haskey) of + (False, Right True) -> helper bad missing (u:have) rs + (False, Left _) -> helper (r:bad) missing have rs + (False, Right False) -> helper bad (u:missing) have rs + _ -> helper bad missing have rs + +notEnoughCopies :: Key -> Int -> [UUID] -> [UUID] -> [Remote] -> Annex Bool +notEnoughCopies key need have skip bad = do + unsafe + showLongNote $ + "Could only verify the existence of " ++ + show (length have) ++ " out of " ++ show need ++ + " necessary copies" + Remote.showTriedRemotes bad + Remote.showLocations key (have++skip) + "Rather than dropping this file, try using: git annex move" + hint + return False + where + unsafe = showNote "unsafe" + hint = showLongNote "(Use --force to override this check, or adjust annex.numcopies.)" + +{- In auto mode, only runs the action if there are enough copies + - copies on other semitrusted repositories. + - + - Passes any numcopies attribute of the file on to the action as an + - optimisation. -} +checkDropAuto :: Maybe Remote -> FilePath -> Key -> (Maybe Int -> CommandStart) -> CommandStart +checkDropAuto mremote file key a = do + numcopiesattr <- numCopies file + Annex.getState Annex.auto >>= auto numcopiesattr + where + auto numcopiesattr False = a numcopiesattr + auto numcopiesattr True = do + needed <- getNumCopies numcopiesattr + locs <- Remote.keyLocations key + uuid <- getUUID + let remoteuuid = fromMaybe uuid $ Remote.uuid <$> mremote + locs' <- trustExclude UnTrusted $ filter (/= remoteuuid) locs + if length locs' >= needed + then a numcopiesattr + else stop diff --git a/Command/DropKey.hs b/Command/DropKey.hs new file mode 100644 index 0000000000..6249195840 --- /dev/null +++ b/Command/DropKey.hs @@ -0,0 +1,39 @@ +{- git-annex command + - + - Copyright 2010 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Command.DropKey where + +import Common.Annex +import Command +import qualified Annex +import Logs.Location +import Annex.Content +import Types.Key + +def :: [Command] +def = [noCommit $ command "dropkey" (paramRepeating paramKey) seek + SectionPlumbing "drops annexed content for specified keys"] + +seek :: [CommandSeek] +seek = [withKeys start] + +start :: Key -> CommandStart +start key = stopUnless (inAnnex key) $ do + unlessM (Annex.getState Annex.force) $ + error "dropkey can cause data loss; use --force if you're sure you want to do this" + showStart "dropkey" (key2file key) + next $ perform key + +perform :: Key -> CommandPerform +perform key = lockContent key $ do + removeAnnex key + next $ cleanup key + +cleanup :: Key -> CommandCleanup +cleanup key = do + logStatus key InfoMissing + return True diff --git a/Command/DropUnused.hs b/Command/DropUnused.hs new file mode 100644 index 0000000000..bf2635e00f --- /dev/null +++ b/Command/DropUnused.hs @@ -0,0 +1,43 @@ +{- git-annex command + - + - Copyright 2010,2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Command.DropUnused where + +import Common.Annex +import Command +import qualified Annex +import qualified Command.Drop +import qualified Remote +import qualified Git +import qualified Option +import Command.Unused (withUnusedMaps, UnusedMaps(..), startUnused) + +def :: [Command] +def = [withOptions [Command.Drop.fromOption] $ + command "dropunused" (paramRepeating paramNumRange) + seek SectionMaintenance "drop unused file content"] + +seek :: [CommandSeek] +seek = [withUnusedMaps start] + +start :: UnusedMaps -> Int -> CommandStart +start = startUnused "dropunused" perform (performOther gitAnnexBadLocation) (performOther gitAnnexTmpLocation) + +perform :: Key -> CommandPerform +perform key = maybe droplocal dropremote =<< Remote.byNameWithUUID =<< from + where + dropremote r = do + showAction $ "from " ++ Remote.name r + Command.Drop.performRemote key Nothing r + droplocal = Command.Drop.performLocal key Nothing Nothing + from = Annex.getField $ Option.name Command.Drop.fromOption + +performOther :: (Key -> Git.Repo -> FilePath) -> Key -> CommandPerform +performOther filespec key = do + f <- fromRepo $ filespec key + liftIO $ nukeFile f + next $ return True diff --git a/Command/EnableRemote.hs b/Command/EnableRemote.hs new file mode 100644 index 0000000000..ea606c2843 --- /dev/null +++ b/Command/EnableRemote.hs @@ -0,0 +1,56 @@ +{- git-annex command + - + - Copyright 2013 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Command.EnableRemote where + +import Common.Annex +import Command +import qualified Logs.Remote +import qualified Types.Remote as R +import qualified Command.InitRemote as InitRemote + +import qualified Data.Map as M + +def :: [Command] +def = [command "enableremote" + (paramPair paramName $ paramOptional $ paramRepeating paramKeyValue) + seek SectionSetup "enables use of an existing special remote"] + +seek :: [CommandSeek] +seek = [withWords start] + +start :: [String] -> CommandStart +start [] = unknownNameError "Specify the name of the special remote to enable." +start (name:ws) = go =<< InitRemote.findExisting name + where + config = Logs.Remote.keyValToConfig ws + + go Nothing = unknownNameError "Unknown special remote name." + go (Just (u, c)) = do + let fullconfig = config `M.union` c + t <- InitRemote.findType fullconfig + + showStart "enableremote" name + next $ perform t u fullconfig + +unknownNameError :: String -> Annex a +unknownNameError prefix = do + names <- InitRemote.remoteNames + error $ prefix ++ + if null names + then "" + else " Known special remotes: " ++ intercalate " " names + +perform :: RemoteType -> UUID -> R.RemoteConfig -> CommandPerform +perform t u c = do + c' <- R.setup t u c + next $ cleanup u c' + +cleanup :: UUID -> R.RemoteConfig -> CommandCleanup +cleanup u c = do + Logs.Remote.configSet u c + return True diff --git a/Command/Find.hs b/Command/Find.hs new file mode 100644 index 0000000000..4b8c7ce0e9 --- /dev/null +++ b/Command/Find.hs @@ -0,0 +1,61 @@ +{- git-annex command + - + - Copyright 2010-2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Command.Find where + +import qualified Data.Map as M + +import Common.Annex +import Command +import Annex.Content +import Limit +import qualified Annex +import qualified Utility.Format +import Utility.DataUnits +import Types.Key +import qualified Option + +def :: [Command] +def = [noCommit $ noMessages $ withOptions [formatOption, print0Option] $ + command "find" paramPaths seek SectionQuery "lists available files"] + +formatOption :: Option +formatOption = Option.field [] "format" paramFormat "control format of output" + +print0Option :: Option +print0Option = Option.Option [] ["print0"] (Option.NoArg set) + "terminate output with null" + where + set = Annex.setField (Option.name formatOption) "${file}\0" + +seek :: [CommandSeek] +seek = [withField formatOption formatconverter $ \f -> + withFilesInGit $ whenAnnexed $ start f] + where + formatconverter = return . fmap Utility.Format.gen + +start :: Maybe Utility.Format.Format -> FilePath -> (Key, Backend) -> CommandStart +start format file (key, _) = do + -- only files inAnnex are shown, unless the user has requested + -- others via a limit + whenM (limited <||> inAnnex key) $ + unlessM (showFullJSON vars) $ + case format of + Nothing -> liftIO $ putStrLn file + Just formatter -> liftIO $ putStr $ + Utility.Format.format formatter $ + M.fromList vars + stop + where + vars = + [ ("file", file) + , ("key", key2file key) + , ("backend", keyBackendName key) + , ("bytesize", size show) + , ("humansize", size $ roughSize storageUnits True) + ] + size c = maybe "unknown" c $ keySize key diff --git a/Command/Fix.hs b/Command/Fix.hs new file mode 100644 index 0000000000..da26276193 --- /dev/null +++ b/Command/Fix.hs @@ -0,0 +1,55 @@ +{- git-annex command + - + - Copyright 2010 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE CPP #-} + +module Command.Fix where + +import System.PosixCompat.Files + +import Common.Annex +import Command +import qualified Annex.Queue +#ifndef __ANDROID__ +import Utility.Touch +#endif + +def :: [Command] +def = [notDirect $ noCommit $ command "fix" paramPaths seek + SectionMaintenance "fix up symlinks to point to annexed content"] + +seek :: [CommandSeek] +seek = [withFilesInGit $ whenAnnexed start] + +{- Fixes the symlink to an annexed file. -} +start :: FilePath -> (Key, Backend) -> CommandStart +start file (key, _) = do + link <- inRepo $ gitAnnexLink file key + stopUnless ((/=) (Just link) <$> liftIO (catchMaybeIO $ readSymbolicLink file)) $ do + showStart "fix" file + next $ perform file link + +perform :: FilePath -> FilePath -> CommandPerform +perform file link = do + liftIO $ do +#ifndef __ANDROID__ + -- preserve mtime of symlink + mtime <- catchMaybeIO $ TimeSpec . modificationTime + <$> getSymbolicLinkStatus file +#endif + createDirectoryIfMissing True (parentDir file) + removeFile file + createSymbolicLink link file +#ifndef __ANDROID__ + maybe noop (\t -> touch file t False) mtime +#endif + next $ cleanup file + +cleanup :: FilePath -> CommandCleanup +cleanup file = do + Annex.Queue.addCommand "add" [Param "--force", Param "--"] [file] + return True diff --git a/Command/FromKey.hs b/Command/FromKey.hs new file mode 100644 index 0000000000..c3d2daafe2 --- /dev/null +++ b/Command/FromKey.hs @@ -0,0 +1,46 @@ +{- git-annex command + - + - Copyright 2010 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Command.FromKey where + +import System.PosixCompat.Files + +import Common.Annex +import Command +import qualified Annex.Queue +import Annex.Content +import Types.Key + +def :: [Command] +def = [notDirect $ notBareRepo $ + command "fromkey" (paramPair paramKey paramPath) seek + SectionPlumbing "adds a file using a specific key"] + +seek :: [CommandSeek] +seek = [withWords start] + +start :: [String] -> CommandStart +start (keyname:file:[]) = do + let key = fromMaybe (error "bad key") $ file2key keyname + inbackend <- inAnnex key + unless inbackend $ error $ + "key ("++ keyname ++") is not present in backend" + showStart "fromkey" file + next $ perform key file +start _ = error "specify a key and a dest file" + +perform :: Key -> FilePath -> CommandPerform +perform key file = do + link <- inRepo $ gitAnnexLink file key + liftIO $ createDirectoryIfMissing True (parentDir file) + liftIO $ createSymbolicLink link file + next $ cleanup file + +cleanup :: FilePath -> CommandCleanup +cleanup file = do + Annex.Queue.addCommand "add" [Param "--"] [file] + return True diff --git a/Command/Fsck.hs b/Command/Fsck.hs new file mode 100644 index 0000000000..6464fc002f --- /dev/null +++ b/Command/Fsck.hs @@ -0,0 +1,509 @@ +{- git-annex command + - + - Copyright 2010-2013 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE CPP #-} + +module Command.Fsck where + +import System.PosixCompat.Files + +import Common.Annex +import Command +import qualified Annex +import qualified Remote +import qualified Types.Backend +import qualified Types.Key +import qualified Backend +import Annex.Content +import Annex.Content.Direct +import Annex.Direct +import Annex.Perms +import Annex.Link +import Logs.Location +import Logs.Trust +import Annex.UUID +import Utility.DataUnits +import Utility.FileMode +import Config +import qualified Option +import Types.Key +import Utility.HumanTime +import Git.FilePath +import GitAnnex.Options + +#ifndef mingw32_HOST_OS +import System.Posix.Process (getProcessID) +#else +import System.Random (getStdRandom, random) +#endif +import Data.Time.Clock.POSIX +import Data.Time +import System.Posix.Types (EpochTime) +import System.Locale + +def :: [Command] +def = [withOptions fsckOptions $ command "fsck" paramPaths seek + SectionMaintenance "check for problems"] + +fromOption :: Option +fromOption = Option.field ['f'] "from" paramRemote "check remote" + +startIncrementalOption :: Option +startIncrementalOption = Option.flag ['S'] "incremental" "start an incremental fsck" + +moreIncrementalOption :: Option +moreIncrementalOption = Option.flag ['m'] "more" "continue an incremental fsck" + +incrementalScheduleOption :: Option +incrementalScheduleOption = Option.field [] "incremental-schedule" paramTime + "schedule incremental fscking" + +fsckOptions :: [Option] +fsckOptions = + [ fromOption + , startIncrementalOption + , moreIncrementalOption + , incrementalScheduleOption + ] ++ keyOptions + +seek :: [CommandSeek] +seek = + [ withField fromOption Remote.byNameWithUUID $ \from -> + withIncremental $ \i -> + withKeyOptions (startKey i) $ + withFilesInGit $ whenAnnexed $ start from i + ] + +withIncremental :: (Incremental -> CommandSeek) -> CommandSeek +withIncremental = withValue $ do + i <- maybe (return False) (checkschedule . parseDuration) + =<< Annex.getField (Option.name incrementalScheduleOption) + starti <- Annex.getFlag (Option.name startIncrementalOption) + morei <- Annex.getFlag (Option.name moreIncrementalOption) + case (i, starti, morei) of + (False, False, False) -> return NonIncremental + (False, True, _) -> startIncremental + (False ,False, True) -> ContIncremental <$> getStartTime + (True, _, _) -> + maybe startIncremental (return . ContIncremental . Just) + =<< getStartTime + where + startIncremental = do + recordStartTime + return StartIncremental + + checkschedule Nothing = error "bad --incremental-schedule value" + checkschedule (Just delta) = do + Annex.addCleanup "" $ do + v <- getStartTime + case v of + Nothing -> noop + Just started -> do + now <- liftIO getPOSIXTime + when (now - realToFrac started >= delta) $ + resetStartTime + return True + +start :: Maybe Remote -> Incremental -> FilePath -> (Key, Backend) -> CommandStart +start from inc file (key, backend) = do + numcopies <- numCopies file + case from of + Nothing -> go $ perform key file backend numcopies + Just r -> go $ performRemote key file backend numcopies r + where + go = runFsck inc file key + +perform :: Key -> FilePath -> Backend -> Maybe Int -> Annex Bool +perform key file backend numcopies = check + -- order matters + [ fixLink key file + , verifyLocationLog key file + , verifyDirectMapping key file + , verifyDirectMode key file + , checkKeySize key + , checkBackend backend key (Just file) + , checkKeyNumCopies key file numcopies + ] + +{- To fsck a remote, the content is retrieved to a tmp file, + - and checked locally. -} +performRemote :: Key -> FilePath -> Backend -> Maybe Int -> Remote -> Annex Bool +performRemote key file backend numcopies remote = + dispatch =<< Remote.hasKey remote key + where + dispatch (Left err) = do + showNote err + return False + dispatch (Right True) = withtmp $ \tmpfile -> + ifM (getfile tmpfile) + ( go True (Just tmpfile) + , go True Nothing + ) + dispatch (Right False) = go False Nothing + go present localcopy = check + [ verifyLocationLogRemote key file remote present + , checkKeySizeRemote key remote localcopy + , checkBackendRemote backend key remote localcopy + , checkKeyNumCopies key file numcopies + ] + withtmp a = do +#ifndef mingw32_HOST_OS + v <- liftIO getProcessID +#else + v <- liftIO (getStdRandom random :: IO Int) +#endif + t <- fromRepo gitAnnexTmpDir + createAnnexDirectory t + let tmp = t "fsck" ++ show v ++ "." ++ keyFile key + let cleanup = liftIO $ catchIO (removeFile tmp) (const noop) + cleanup + cleanup `after` a tmp + getfile tmp = + ifM (Remote.retrieveKeyFileCheap remote key tmp) + ( return True + , ifM (Annex.getState Annex.fast) + ( return False + , Remote.retrieveKeyFile remote key Nothing tmp dummymeter + ) + ) + dummymeter _ = noop + +startKey :: Incremental -> Key -> CommandStart +startKey inc key = case Backend.maybeLookupBackendName (Types.Key.keyBackendName key) of + Nothing -> stop + Just backend -> runFsck inc (key2file key) key $ performAll key backend + +{- Note that numcopies cannot be checked in --all mode, since we do not + - have associated filenames to look up in the .gitattributes file. -} +performAll :: Key -> Backend -> Annex Bool +performAll key backend = check + [ verifyLocationLog key (key2file key) + , checkKeySize key + , checkBackend backend key Nothing + ] + +check :: [Annex Bool] -> Annex Bool +check cs = all id <$> sequence cs + +{- Checks that the file's link points correctly to the content. + - + - In direct mode, there is only a link when the content is not present. + -} +fixLink :: Key -> FilePath -> Annex Bool +fixLink key file = do + want <- inRepo $ gitAnnexLink file key + have <- getAnnexLinkTarget file + maybe noop (go want) have + return True + where + go want have + | want /= fromInternalGitPath have = do + showNote "fixing link" + liftIO $ createDirectoryIfMissing True (parentDir file) + liftIO $ removeFile file + addAnnexLink want file + | otherwise = noop + +{- Checks that the location log reflects the current status of the key, + - in this repository only. -} +verifyLocationLog :: Key -> String -> Annex Bool +verifyLocationLog key desc = do + present <- inAnnex key + direct <- isDirect + u <- getUUID + + {- Since we're checking that a key's file is present, throw + - in a permission fixup here too. -} + when (present && not direct) $ do + file <- calcRepo $ gitAnnexLocation key + freezeContent file + freezeContentDir file + + {- In direct mode, modified files will show up as not present, + - but that is expected and not something to do anything about. -} + if (direct && not present) + then return True + else verifyLocationLog' key desc present u (logChange key u) + +verifyLocationLogRemote :: Key -> String -> Remote -> Bool -> Annex Bool +verifyLocationLogRemote key desc remote present = + verifyLocationLog' key desc present (Remote.uuid remote) + (Remote.logStatus remote key) + +verifyLocationLog' :: Key -> String -> Bool -> UUID -> (LogStatus -> Annex ()) -> Annex Bool +verifyLocationLog' key desc present u bad = do + uuids <- Remote.keyLocations key + case (present, u `elem` uuids) of + (True, False) -> do + fix InfoPresent + -- There is no data loss, so do not fail. + return True + (False, True) -> do + fix InfoMissing + warning $ + "** Based on the location log, " ++ desc + ++ "\n** was expected to be present, " ++ + "but its content is missing." + return False + _ -> return True + where + fix s = do + showNote "fixing location log" + bad s + +{- Ensures the direct mode mapping file is consistent. Each file + - it lists for the key should exist, and the specified file should be + - included in it. + -} +verifyDirectMapping :: Key -> FilePath -> Annex Bool +verifyDirectMapping key file = do + whenM isDirect $ do + fs <- addAssociatedFile key file + forM_ fs $ \f -> + unlessM (liftIO $ doesFileExist f) $ + void $ removeAssociatedFile key f + return True + +{- Ensures that files whose content is available are in direct mode. -} +verifyDirectMode :: Key -> FilePath -> Annex Bool +verifyDirectMode key file = do + whenM (isDirect <&&> islink) $ do + v <- toDirectGen key file + case v of + Nothing -> noop + Just a -> do + showNote "fixing direct mode" + a + return True + where + islink = liftIO $ isSymbolicLink <$> getSymbolicLinkStatus file + +{- The size of the data for a key is checked against the size encoded in + - the key's metadata, if available. + - + - Not checked in direct mode, because files can be changed directly. + -} +checkKeySize :: Key -> Annex Bool +checkKeySize key = ifM isDirect + ( return True + , do + file <- calcRepo $ gitAnnexLocation key + ifM (liftIO $ doesFileExist file) + ( checkKeySizeOr badContent key file + , return True + ) + ) + +checkKeySizeRemote :: Key -> Remote -> Maybe FilePath -> Annex Bool +checkKeySizeRemote _ _ Nothing = return True +checkKeySizeRemote key remote (Just file) = + checkKeySizeOr (badContentRemote remote) key file + +checkKeySizeOr :: (Key -> Annex String) -> Key -> FilePath -> Annex Bool +checkKeySizeOr bad key file = case Types.Key.keySize key of + Nothing -> return True + Just size -> do + size' <- fromIntegral . fileSize + <$> liftIO (getFileStatus file) + comparesizes size size' + where + comparesizes a b = do + let same = a == b + unless same $ badsize a b + return same + badsize a b = do + msg <- bad key + warning $ concat + [ "Bad file size (" + , compareSizes storageUnits True a b + , "); " + , msg + ] + +{- Runs the backend specific check on a key's content. + - + - In direct mode this is not done if the file has clearly been modified, + - because modification of direct mode files is allowed. It's still done + - if the file does not appear modified, to catch disk corruption, etc. + -} +checkBackend :: Backend -> Key -> Maybe FilePath -> Annex Bool +checkBackend backend key mfile = go =<< isDirect + where + go False = do + content <- calcRepo $ gitAnnexLocation key + checkBackendOr badContent backend key content + go True = maybe nocheck checkdirect mfile + checkdirect file = ifM (goodContent key file) + ( checkBackendOr' (badContentDirect file) backend key file + (goodContent key file) + , nocheck + ) + nocheck = return True + +checkBackendRemote :: Backend -> Key -> Remote -> Maybe FilePath -> Annex Bool +checkBackendRemote backend key remote = maybe (return True) go + where + go file = checkBackendOr (badContentRemote remote) backend key file + +checkBackendOr :: (Key -> Annex String) -> Backend -> Key -> FilePath -> Annex Bool +checkBackendOr bad backend key file = + checkBackendOr' bad backend key file (return True) + +checkBackendOr' :: (Key -> Annex String) -> Backend -> Key -> FilePath -> Annex Bool -> Annex Bool +checkBackendOr' bad backend key file postcheck = + case Types.Backend.fsckKey backend of + Nothing -> return True + Just a -> do + ok <- a key file + ifM postcheck + ( do + unless ok $ do + msg <- bad key + warning $ "Bad file content; " ++ msg + return ok + , return True + ) + +checkKeyNumCopies :: Key -> FilePath -> Maybe Int -> Annex Bool +checkKeyNumCopies key file numcopies = do + needed <- getNumCopies numcopies + (untrustedlocations, safelocations) <- trustPartition UnTrusted =<< Remote.keyLocations key + let present = length safelocations + if present < needed + then do + ppuuids <- Remote.prettyPrintUUIDs "untrusted" untrustedlocations + warning $ missingNote file present needed ppuuids + return False + else return True + +missingNote :: String -> Int -> Int -> String -> String +missingNote file 0 _ [] = + "** No known copies exist of " ++ file +missingNote file 0 _ untrusted = + "Only these untrusted locations may have copies of " ++ file ++ + "\n" ++ untrusted ++ + "Back it up to trusted locations with git-annex copy." +missingNote file present needed [] = + "Only " ++ show present ++ " of " ++ show needed ++ + " trustworthy copies exist of " ++ file ++ + "\nBack it up with git-annex copy." +missingNote file present needed untrusted = + missingNote file present needed [] ++ + "\nThe following untrusted locations may also have copies: " ++ + "\n" ++ untrusted + +{- Bad content is moved aside. -} +badContent :: Key -> Annex String +badContent key = do + dest <- moveBad key + return $ "moved to " ++ dest + +{- Bad content is left where it is, but we touch the file, so it'll be + - committed to a new key. -} +badContentDirect :: FilePath -> Key -> Annex String +badContentDirect file key = do + void $ liftIO $ catchMaybeIO $ touchFile file + logStatus key InfoMissing + return $ "left in place for you to examine" + +badContentRemote :: Remote -> Key -> Annex String +badContentRemote remote key = do + ok <- Remote.removeKey remote key + when ok $ + Remote.logStatus remote key InfoMissing + return $ (if ok then "dropped from " else "failed to drop from ") + ++ Remote.name remote + +data Incremental = StartIncremental | ContIncremental (Maybe EpochTime) | NonIncremental + deriving (Eq) + +runFsck :: Incremental -> FilePath -> Key -> Annex Bool -> CommandStart +runFsck inc file key a = ifM (needFsck inc key) + ( do + showStart "fsck" file + next $ do + ok <- a + when ok $ + recordFsckTime key + next $ return ok + , stop + ) + +{- Check if a key needs to be fscked, with support for incremental fscks. -} +needFsck :: Incremental -> Key -> Annex Bool +needFsck (ContIncremental Nothing) _ = return True +needFsck (ContIncremental starttime) key = do + fscktime <- getFsckTime key + return $ fscktime < starttime +needFsck _ _ = return True + +{- To record the time that a key was last fscked, without + - modifying its mtime, we set the timestamp of its parent directory. + - Each annexed file is the only thing in its directory, so this is fine. + - + - To record that the file was fscked, the directory's sticky bit is set. + - (None of the normal unix behaviors of the sticky bit should matter, so + - we can reuse this permission bit.) + - + - Note that this relies on the parent directory being deleted when a file + - is dropped. That way, if it's later added back, the fsck record + - won't still be present. + -} +recordFsckTime :: Key -> Annex () +recordFsckTime key = do + parent <- parentDir <$> calcRepo (gitAnnexLocation key) + liftIO $ void $ tryIO $ do + touchFile parent +#ifndef mingw32_HOST_OS + setSticky parent +#endif + +getFsckTime :: Key -> Annex (Maybe EpochTime) +getFsckTime key = do + parent <- parentDir <$> calcRepo (gitAnnexLocation key) + liftIO $ catchDefaultIO Nothing $ do + s <- getFileStatus parent + return $ if isSticky $ fileMode s + then Just $ modificationTime s + else Nothing + +{- Records the start time of an incremental fsck. + - + - To guard against time stamp damange (for example, if an annex directory + - is copied without -a), the fsckstate file contains a time that should + - be identical to its modification time. -} +recordStartTime :: Annex () +recordStartTime = do + f <- fromRepo gitAnnexFsckState + createAnnexDirectory $ parentDir f + liftIO $ do + nukeFile f + h <- openFile f WriteMode + t <- modificationTime <$> getFileStatus f + hPutStr h $ showTime $ realToFrac t + hClose h + where + showTime :: POSIXTime -> String + showTime = show + +resetStartTime :: Annex () +resetStartTime = liftIO . nukeFile =<< fromRepo gitAnnexFsckState + +{- Gets the incremental fsck start time. -} +getStartTime :: Annex (Maybe EpochTime) +getStartTime = do + f <- fromRepo gitAnnexFsckState + liftIO $ catchDefaultIO Nothing $ do + timestamp <- modificationTime <$> getFileStatus f + t <- readishTime <$> readFile f + return $ if Just (realToFrac timestamp) == t + then Just timestamp + else Nothing + where + readishTime :: String -> Maybe POSIXTime + readishTime s = utcTimeToPOSIXSeconds <$> + parseTime defaultTimeLocale "%s%Qs" s diff --git a/Command/FuzzTest.hs b/Command/FuzzTest.hs new file mode 100644 index 0000000000..34e74b4334 --- /dev/null +++ b/Command/FuzzTest.hs @@ -0,0 +1,288 @@ +{- git-annex fuzz generator + - + - Copyright 2013 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Command.FuzzTest where + +import Common.Annex +import qualified Annex +import Command +import qualified Git.Config +import Config +import Utility.ThreadScheduler +import Annex.Exception +import Utility.DiskFree + +import Data.Time.Clock +import System.Random (getStdRandom, random, randomR) +import Test.QuickCheck +import Control.Concurrent + +def :: [Command] +def = [ notBareRepo $ command "fuzztest" paramNothing seek SectionPlumbing + "generates fuzz test files"] + +seek :: [CommandSeek] +seek = [withNothing start] + +start :: CommandStart +start = do + guardTest + logf <- fromRepo gitAnnexFuzzTestLogFile + showStart "fuzztest" logf + logh <-liftIO $ openFile logf WriteMode + void $ forever $ fuzz logh + stop + +guardTest :: Annex () +guardTest = unlessM (fromMaybe False . Git.Config.isTrue <$> getConfig key "") $ + error $ unlines + [ "Running fuzz tests *writes* to and *deletes* files in" + , "this repository, and pushes those changes to other" + , "repositories! This is a developer tool, not something" + , "to play with." + , "" + , "Refusing to run fuzz tests, since " ++ keyname ++ " is not set!" + ] + where + key = annexConfig "eat-my-repository" + (ConfigKey keyname) = key + + +fuzz :: Handle -> Annex () +fuzz logh = do + action <- genFuzzAction + record logh $ flip Started action + result <- tryAnnex $ runFuzzAction action + record logh $ flip Finished $ + either (const False) (const True) result + +record :: Handle -> (UTCTime -> TimeStampedFuzzAction) -> Annex () +record h tmpl = liftIO $ do + now <- getCurrentTime + let s = show $ tmpl now + print s + hPrint h s + hFlush h + +{- Delay for either a fraction of a second, or a few seconds, or up + - to 1 minute. + - + - The MinutesDelay is used as an opportunity to do housekeeping tasks. + -} +randomDelay :: Delay -> Annex () +randomDelay TinyDelay = liftIO $ + threadDelay =<< getStdRandom (randomR (10000, 1000000)) +randomDelay SecondsDelay = liftIO $ + threadDelaySeconds =<< Seconds <$> getStdRandom (randomR (1, 10)) +randomDelay MinutesDelay = do + liftIO $ threadDelaySeconds =<< Seconds <$> getStdRandom (randomR (1, 60)) + reserve <- annexDiskReserve <$> Annex.getGitConfig + free <- liftIO $ getDiskFree "." + case free of + Just have | have < reserve -> do + warning "Low disk space; fuzz test paused." + liftIO $ threadDelaySeconds (Seconds 60) + randomDelay MinutesDelay + _ -> noop + +data Delay + = TinyDelay + | SecondsDelay + | MinutesDelay + deriving (Read, Show, Eq) + +instance Arbitrary Delay where + arbitrary = elements [TinyDelay, SecondsDelay, MinutesDelay] + +data FuzzFile = FuzzFile FilePath + deriving (Read, Show, Eq) + +data FuzzDir = FuzzDir FilePath + deriving (Read, Show, Eq) + +instance Arbitrary FuzzFile where + arbitrary = FuzzFile <$> arbitrary + +instance Arbitrary FuzzDir where + arbitrary = FuzzDir <$> arbitrary + +class ToFilePath a where + toFilePath :: a -> FilePath + +instance ToFilePath FuzzFile where + toFilePath (FuzzFile f) = f + +instance ToFilePath FuzzDir where + toFilePath (FuzzDir d) = d + +isFuzzFile :: FilePath -> Bool +isFuzzFile f = "fuzzfile_" `isPrefixOf` takeFileName f + +isFuzzDir :: FilePath -> Bool +isFuzzDir d = "fuzzdir_" `isPrefixOf` d + +mkFuzzFile :: FilePath -> [FuzzDir] -> FuzzFile +mkFuzzFile file dirs = FuzzFile $ joinPath (map toFilePath dirs) ("fuzzfile_" ++ file) + +mkFuzzDir :: Int -> FuzzDir +mkFuzzDir n = FuzzDir $ "fuzzdir_" ++ show n + +{- File is placed inside a directory hierarchy up to 4 subdirectories deep. -} +genFuzzFile :: IO FuzzFile +genFuzzFile = do + n <- getStdRandom $ randomR (0, 4) + dirs <- replicateM n genFuzzDir + file <- show <$> (getStdRandom random :: IO Int) + return $ mkFuzzFile file dirs + +{- Only 16 distinct subdirectories are used. When nested 4 deep, this + - yields 69904 total directories max, which is below the default Linux + - inotify limit of 81920. The goal is not to run the assistant out of + - inotify descriptors. -} +genFuzzDir :: IO FuzzDir +genFuzzDir = mkFuzzDir <$> (getStdRandom (randomR (1,16)) :: IO Int) + +localFile :: FilePath -> Bool +localFile f + | isAbsolute f = False + | ".." `isInfixOf` f = False + | ".git" `isPrefixOf` f = False + | otherwise = True + +data TimeStampedFuzzAction + = Started UTCTime FuzzAction + | Finished UTCTime Bool + deriving (Read, Show) + +data FuzzAction + = FuzzAdd FuzzFile + | FuzzDelete FuzzFile + | FuzzMove FuzzFile FuzzFile + | FuzzModify FuzzFile + | FuzzDeleteDir FuzzDir + | FuzzMoveDir FuzzDir FuzzDir + | FuzzPause Delay + deriving (Read, Show, Eq) + +instance Arbitrary FuzzAction where + arbitrary = frequency + [ (50, FuzzAdd <$> arbitrary) + , (50, FuzzDelete <$> arbitrary) + , (10, FuzzMove <$> arbitrary <*> arbitrary) + , (10, FuzzModify <$> arbitrary) + , (10, FuzzDeleteDir <$> arbitrary) + , (10, FuzzMoveDir <$> arbitrary <*> arbitrary) + , (10, FuzzPause <$> arbitrary) + ] + +runFuzzAction :: FuzzAction -> Annex () +runFuzzAction (FuzzAdd (FuzzFile f)) = liftIO $ do + createDirectoryIfMissing True $ parentDir f + n <- getStdRandom random :: IO Int + writeFile f $ show n ++ "\n" +runFuzzAction (FuzzDelete (FuzzFile f)) = liftIO $ nukeFile f +runFuzzAction (FuzzMove (FuzzFile src) (FuzzFile dest)) = liftIO $ + rename src dest +runFuzzAction (FuzzModify (FuzzFile f)) = whenM isDirect $ liftIO $ do + n <- getStdRandom random :: IO Int + appendFile f $ show n ++ "\n" +runFuzzAction (FuzzDeleteDir (FuzzDir d)) = liftIO $ + removeDirectoryRecursive d +runFuzzAction (FuzzMoveDir (FuzzDir src) (FuzzDir dest)) = liftIO $ + rename src dest +runFuzzAction (FuzzPause d) = randomDelay d + +genFuzzAction :: Annex FuzzAction +genFuzzAction = do + tmpl <- liftIO $ Prelude.head <$> sample' (arbitrary :: Gen FuzzAction) + -- Fix up template action to make sense in the current repo tree. + case tmpl of + FuzzAdd _ -> do + f <- liftIO newFile + maybe genFuzzAction (return . FuzzAdd) f + FuzzDelete _ -> do + f <- liftIO $ existingFile 0 "" + maybe genFuzzAction (return . FuzzDelete) f + FuzzMove _ _ -> do + src <- liftIO $ existingFile 0 "" + dest <- liftIO newFile + case (src, dest) of + (Just s, Just d) -> return $ FuzzMove s d + _ -> genFuzzAction + FuzzMoveDir _ _ -> do + md <- liftIO existingDir + case md of + Nothing -> genFuzzAction + Just d -> do + newd <- liftIO $ newDir (parentDir $ toFilePath d) + maybe genFuzzAction (return . FuzzMoveDir d) newd + FuzzDeleteDir _ -> do + d <- liftIO existingDir + maybe genFuzzAction (return . FuzzDeleteDir) d + FuzzModify _ -> do + f <- liftIO $ existingFile 0 "" + maybe genFuzzAction (return . FuzzModify) f + FuzzPause _ -> return tmpl + +existingFile :: Int -> FilePath -> IO (Maybe FuzzFile) +existingFile 0 _ = return Nothing +existingFile n top = do + dir <- existingDirIncludingTop + contents <- catchDefaultIO [] (getDirectoryContents dir) + let files = filter isFuzzFile contents + if null files + then do + let dirs = filter isFuzzDir contents + if null dirs + then return Nothing + else do + i <- getStdRandom $ randomR (0, length dirs - 1) + existingFile (n - 1) (top dirs !! i) + else do + i <- getStdRandom $ randomR (0, length files - 1) + return $ Just $ FuzzFile $ top dir files !! i + +existingDirIncludingTop :: IO FilePath +existingDirIncludingTop = do + dirs <- filter isFuzzDir <$> getDirectoryContents "." + if null dirs + then return "." + else do + n <- getStdRandom $ randomR (0, length dirs) + return $ ("." : dirs) !! n + +existingDir :: IO (Maybe FuzzDir) +existingDir = do + d <- existingDirIncludingTop + return $ if isFuzzDir d + then Just $ FuzzDir d + else Nothing + +newFile :: IO (Maybe FuzzFile) +newFile = go (100 :: Int) + where + go 0 = return Nothing + go n = do + f <- genFuzzFile + ifM (doesnotexist (toFilePath f)) + ( return $ Just f + , go (n - 1) + ) + +newDir :: FilePath -> IO (Maybe FuzzDir) +newDir parent = go (100 :: Int) + where + go 0 = return Nothing + go n = do + (FuzzDir d) <- genFuzzDir + ifM (doesnotexist (parent d)) + ( return $ Just $ FuzzDir d + , go (n - 1) + ) + +doesnotexist :: FilePath -> IO Bool +doesnotexist f = isNothing <$> catchMaybeIO (getSymbolicLinkStatus f) diff --git a/Command/Get.hs b/Command/Get.hs new file mode 100644 index 0000000000..31a75c3e1b --- /dev/null +++ b/Command/Get.hs @@ -0,0 +1,90 @@ +{- git-annex command + - + - Copyright 2010, 2013 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Command.Get where + +import Common.Annex +import Command +import qualified Remote +import Annex.Content +import qualified Command.Move +import Logs.Transfer +import Annex.Wanted +import GitAnnex.Options +import Types.Key + +def :: [Command] +def = [withOptions getOptions $ command "get" paramPaths seek + SectionCommon "make content of annexed files available"] + +getOptions :: [Option] +getOptions = [Command.Move.fromOption] ++ keyOptions + +seek :: [CommandSeek] +seek = + [ withField Command.Move.fromOption Remote.byNameWithUUID $ \from -> + withKeyOptions (startKeys from) $ + withFilesInGit $ whenAnnexed $ start from + ] + +start :: Maybe Remote -> FilePath -> (Key, Backend) -> CommandStart +start from file (key, _) = start' expensivecheck from key (Just file) + where + expensivecheck = checkAuto (numCopiesCheck file key (<) <||> wantGet False (Just file)) + +startKeys :: Maybe Remote -> Key -> CommandStart +startKeys from key = start' (return True) from key Nothing + +start' :: Annex Bool -> Maybe Remote -> Key -> AssociatedFile -> CommandStart +start' expensivecheck from key afile = stopUnless (not <$> inAnnex key) $ + stopUnless expensivecheck $ + case from of + Nothing -> go $ perform key afile + Just src -> + stopUnless (Command.Move.fromOk src key) $ + go $ Command.Move.fromPerform src False key afile + where + go a = do + showStart "get" (fromMaybe (key2file key) afile) + next a + +perform :: Key -> AssociatedFile -> CommandPerform +perform key afile = stopUnless (getViaTmp key $ getKeyFile key afile) $ + next $ return True -- no cleanup needed + +{- Try to find a copy of the file in one of the remotes, + - and copy it to here. -} +getKeyFile :: Key -> AssociatedFile -> FilePath -> Annex Bool +getKeyFile key afile dest = dispatch =<< Remote.keyPossibilities key + where + dispatch [] = do + showNote "not available" + showlocs + return False + dispatch remotes = trycopy remotes remotes + trycopy full [] = do + Remote.showTriedRemotes full + showlocs + return False + trycopy full (r:rs) = + ifM (probablyPresent r) + ( docopy r (trycopy full rs) + , trycopy full rs + ) + showlocs = Remote.showLocations key [] $ + "No other repository is known to contain the file." + -- This check is to avoid an ugly message if a remote is a + -- drive that is not mounted. + probablyPresent r + | Remote.hasKeyCheap r = + either (const False) id <$> Remote.hasKey r key + | otherwise = return True + docopy r continue = do + ok <- download (Remote.uuid r) key afile noRetry $ \p -> do + showAction $ "from " ++ Remote.name r + Remote.retrieveKeyFile r key afile dest p + if ok then return ok else continue diff --git a/Command/Group.hs b/Command/Group.hs new file mode 100644 index 0000000000..4c0bf4899a --- /dev/null +++ b/Command/Group.hs @@ -0,0 +1,35 @@ +{- git-annex command + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Command.Group where + +import Common.Annex +import Command +import qualified Remote +import Logs.Group +import Types.Group + +import qualified Data.Set as S + +def :: [Command] +def = [command "group" (paramPair paramRemote paramDesc) seek + SectionSetup "add a repository to a group"] + +seek :: [CommandSeek] +seek = [withWords start] + +start :: [String] -> CommandStart +start (name:g:[]) = do + showStart "group" name + u <- Remote.nameToUUID name + next $ perform u g +start _ = error "Specify a repository and a group." + +perform :: UUID -> Group -> CommandPerform +perform uuid g = do + groupChange uuid (S.insert g) + next $ return True diff --git a/Command/Help.hs b/Command/Help.hs new file mode 100644 index 0000000000..c77f739c15 --- /dev/null +++ b/Command/Help.hs @@ -0,0 +1,62 @@ +{- git-annex command + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Command.Help where + +import Common.Annex +import Command +import qualified Command.Init +import qualified Command.Add +import qualified Command.Drop +import qualified Command.Get +import qualified Command.Move +import qualified Command.Copy +import qualified Command.Sync +import qualified Command.Whereis +import qualified Command.Fsck +import GitAnnex.Options + +import System.Console.GetOpt + +def :: [Command] +def = [noCommit $ noRepo showGeneralHelp $ dontCheck repoExists $ + command "help" paramNothing seek SectionQuery "display help"] + +seek :: [CommandSeek] +seek = [withWords start] + +start :: [String] -> CommandStart +start ["options"] = do + liftIO showCommonOptions + stop +start _ = do + liftIO showGeneralHelp + stop + +showCommonOptions :: IO () +showCommonOptions = putStrLn $ usageInfo "Common options:" options + +showGeneralHelp :: IO () +showGeneralHelp = putStrLn $ unlines + [ "The most frequently used git-annex commands are:" + , unlines $ map cmdline $ concat + [ Command.Init.def + , Command.Add.def + , Command.Drop.def + , Command.Get.def + , Command.Move.def + , Command.Copy.def + , Command.Sync.def + , Command.Whereis.def + , Command.Fsck.def + ] + , "Run 'git-annex' for a complete command list." + , "Run 'git-annex command --help' for help on a specific command." + , "Run `git annex help options' for a list of common options." + ] + where + cmdline c = "\t" ++ cmdname c ++ "\t" ++ cmddesc c diff --git a/Command/Import.hs b/Command/Import.hs new file mode 100644 index 0000000000..518666af91 --- /dev/null +++ b/Command/Import.hs @@ -0,0 +1,42 @@ +{- git-annex command + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Command.Import where + +import System.PosixCompat.Files + +import Common.Annex +import Command +import qualified Annex +import qualified Command.Add + +def :: [Command] +def = [notBareRepo $ command "import" paramPaths seek + SectionCommon "move and add files from outside git working copy"] + +seek :: [CommandSeek] +seek = [withPathContents start] + +start :: (FilePath, FilePath) -> CommandStart +start (srcfile, destfile) = + ifM (liftIO $ isRegularFile <$> getSymbolicLinkStatus srcfile) + ( do + showStart "import" destfile + next $ perform srcfile destfile + , stop + ) + +perform :: FilePath -> FilePath -> CommandPerform +perform srcfile destfile = do + whenM (liftIO $ doesFileExist destfile) $ + unlessM (Annex.getState Annex.force) $ + error $ "not overwriting existing " ++ destfile ++ + " (use --force to override)" + + liftIO $ createDirectoryIfMissing True (parentDir destfile) + liftIO $ moveFile srcfile destfile + Command.Add.perform destfile diff --git a/Command/ImportFeed.hs b/Command/ImportFeed.hs new file mode 100644 index 0000000000..5ad5686479 --- /dev/null +++ b/Command/ImportFeed.hs @@ -0,0 +1,222 @@ +{- git-annex command + - + - Copyright 2013 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Command.ImportFeed where + +import Text.Feed.Import +import Text.Feed.Query +import Text.Feed.Types +import qualified Data.Set as S +import qualified Data.Map as M +import Data.Char +import Data.Time.Clock + +import Common.Annex +import qualified Annex +import Command +import qualified Utility.Url as Url +import Logs.Web +import qualified Option +import qualified Utility.Format +import Utility.Tmp +import Command.AddUrl (addUrlFile, relaxedOption) +import Annex.Perms +import Backend.URL (fromUrl) + +def :: [Command] +def = [notBareRepo $ withOptions [templateOption, relaxedOption] $ + command "importfeed" (paramRepeating paramUrl) seek + SectionCommon "import files from podcast feeds"] + +templateOption :: Option +templateOption = Option.field [] "template" paramFormat "template for filenames" + +seek :: [CommandSeek] +seek = [withField templateOption return $ \tmpl -> + withFlag relaxedOption $ \relaxed -> + withValue (getCache tmpl) $ \cache -> + withStrings $ start relaxed cache] + +start :: Bool -> Cache -> URLString -> CommandStart +start relaxed cache url = do + showStart "importfeed" url + next $ perform relaxed cache url + +perform :: Bool -> Cache -> URLString -> CommandPerform +perform relaxed cache url = do + v <- findEnclosures url + case v of + Just l | not (null l) -> do + ok <- all id + <$> mapM (downloadEnclosure relaxed cache) l + next $ cleanup url ok + _ -> do + feedProblem url "bad feed content" + next $ return True + +cleanup :: URLString -> Bool -> CommandCleanup +cleanup url ok = do + when ok $ + clearFeedProblem url + return ok + +data ToDownload = ToDownload + { feed :: Feed + , feedurl :: URLString + , item :: Item + , location :: URLString + } + +mkToDownload :: Feed -> URLString -> Item -> Maybe ToDownload +mkToDownload f u i = case getItemEnclosure i of + Nothing -> Nothing + Just (enclosureurl, _, _) -> Just $ ToDownload f u i enclosureurl + +data Cache = Cache + { knownurls :: S.Set URLString + , template :: Utility.Format.Format + } + +getCache :: Maybe String -> Annex Cache +getCache opttemplate = ifM (Annex.getState Annex.force) + ( ret S.empty + , do + showSideAction "checking known urls" + ret =<< S.fromList <$> knownUrls + ) + where + tmpl = Utility.Format.gen $ fromMaybe defaultTemplate opttemplate + ret s = return $ Cache s tmpl + +findEnclosures :: URLString -> Annex (Maybe [ToDownload]) +findEnclosures url = extract <$> downloadFeed url + where + extract Nothing = Nothing + extract (Just f) = Just $ mapMaybe (mkToDownload f url) (feedItems f) + +{- Feeds change, so a feed download cannot be resumed. -} +downloadFeed :: URLString -> Annex (Maybe Feed) +downloadFeed url = do + showOutput + liftIO $ withTmpFile "feed" $ \f h -> do + fileEncoding h + ifM (Url.download url [] [] f) + ( liftIO $ parseFeedString <$> hGetContentsStrict h + , return Nothing + ) + +{- Avoids downloading any urls that are already known to be associated + - with a file in the annex, unless forced. -} +downloadEnclosure :: Bool -> Cache -> ToDownload -> Annex Bool +downloadEnclosure relaxed cache enclosure + | S.member url (knownurls cache) = ifM forced (go, return True) + | otherwise = go + where + forced = Annex.getState Annex.force + url = location enclosure + go = do + dest <- makeunique (1 :: Integer) $ feedFile (template cache) enclosure + case dest of + Nothing -> return True + Just f -> do + showStart "addurl" f + ok <- addUrlFile relaxed url f + if ok + then do + showEndOk + return True + else do + showEndFail + checkFeedBroken (feedurl enclosure) + {- Find a unique filename to save the url to. + - If the file exists, prefixes it with a number. + - When forced, the file may already exist and have the same + - url, in which case Nothing is returned as it does not need + - to be re-downloaded. -} + makeunique n file = ifM alreadyexists + ( ifM forced + ( ifAnnexed f checksameurl tryanother + , tryanother + ) + , return $ Just f + ) + where + f = if n < 2 + then file + else + let (d, base) = splitFileName file + in d show n ++ "_" ++ base + tryanother = makeunique (n + 1) file + alreadyexists = liftIO $ isJust <$> catchMaybeIO (getSymbolicLinkStatus f) + checksameurl (k, _) = ifM (elem url <$> getUrls k) + ( return Nothing + , tryanother + ) + +defaultTemplate :: String +defaultTemplate = "${feedtitle}/${itemtitle}${extension}" + +{- Generates a filename to use for a feed item by filling out the template. + - The filename may not be unique. -} +feedFile :: Utility.Format.Format -> ToDownload -> FilePath +feedFile tmpl i = Utility.Format.format tmpl $ M.fromList + [ field "feedtitle" $ getFeedTitle $ feed i + , fieldMaybe "itemtitle" $ getItemTitle $ item i + , fieldMaybe "feedauthor" $ getFeedAuthor $ feed i + , fieldMaybe "itemauthor" $ getItemAuthor $ item i + , fieldMaybe "itemsummary" $ getItemSummary $ item i + , fieldMaybe "itemdescription" $ getItemDescription $ item i + , fieldMaybe "itemrights" $ getItemRights $ item i + , fieldMaybe "itemid" $ snd <$> getItemId (item i) + , ("extension", map sanitize $ takeExtension $ location i) + ] + where + field k v = + let s = map sanitize v in + if null s then (k, "none") else (k, s) + fieldMaybe k Nothing = (k, "none") + fieldMaybe k (Just v) = field k v + + sanitize c + | c == '.' = c + | isSpace c || isPunctuation c || c == '/' = '_' + | otherwise = c + +{- Called when there is a problem with a feed. + - Throws an error if the feed is broken, otherwise shows a warning. -} +feedProblem :: URLString -> String -> Annex () +feedProblem url message = ifM (checkFeedBroken url) + ( error $ message ++ " (having repeated problems with this feed!)" + , warning $ "warning: " ++ message + ) + +{- A feed is only broken if problems have occurred repeatedly, for at + - least 23 hours. -} +checkFeedBroken :: URLString -> Annex Bool +checkFeedBroken url = checkFeedBroken' url =<< feedState url +checkFeedBroken' :: URLString -> FilePath -> Annex Bool +checkFeedBroken' url f = do + prev <- maybe Nothing readish <$> liftIO (catchMaybeIO $ readFile f) + now <- liftIO getCurrentTime + case prev of + Nothing -> do + createAnnexDirectory (parentDir f) + liftIO $ writeFile f $ show now + return False + Just prevtime -> do + let broken = diffUTCTime now prevtime > 60 * 60 * 23 + when broken $ + -- Avoid repeatedly complaining about + -- broken feed. + clearFeedProblem url + return broken + +clearFeedProblem :: URLString -> Annex () +clearFeedProblem url = void $ liftIO . tryIO . removeFile =<< feedState url + +feedState :: URLString -> Annex FilePath +feedState url = fromRepo . gitAnnexFeedState =<< fromUrl url Nothing diff --git a/Command/InAnnex.hs b/Command/InAnnex.hs new file mode 100644 index 0000000000..4410d722d0 --- /dev/null +++ b/Command/InAnnex.hs @@ -0,0 +1,27 @@ +{- git-annex command + - + - Copyright 2010 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Command.InAnnex where + +import Common.Annex +import Command +import Annex.Content + +def :: [Command] +def = [noCommit $ command "inannex" (paramRepeating paramKey) seek + SectionPlumbing "checks if keys are present in the annex"] + +seek :: [CommandSeek] +seek = [withKeys start] + +start :: Key -> CommandStart +start key = inAnnexSafe key >>= dispatch + where + dispatch (Just True) = stop + dispatch (Just False) = exit 1 + dispatch Nothing = exit 100 + exit n = liftIO $ exitWith $ ExitFailure n diff --git a/Command/Indirect.hs b/Command/Indirect.hs new file mode 100644 index 0000000000..e63c4cb8a6 --- /dev/null +++ b/Command/Indirect.hs @@ -0,0 +1,103 @@ +{- git-annex command + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Command.Indirect where + +import System.PosixCompat.Files + +import Common.Annex +import Command +import qualified Git +import qualified Git.Command +import qualified Git.LsFiles +import Config +import qualified Annex +import Annex.Direct +import Annex.Content +import Annex.CatFile +import Annex.Version +import Annex.Perms +import Init + +def :: [Command] +def = [notBareRepo $ noDaemonRunning $ + command "indirect" paramNothing seek + SectionSetup "switch repository to indirect mode"] + +seek :: [CommandSeek] +seek = [withNothing start] + +start :: CommandStart +start = ifM isDirect + ( do + unlessM (coreSymlinks <$> Annex.getGitConfig) $ + error "Git is configured to not use symlinks, so you must use direct mode." + whenM probeCrippledFileSystem $ + error "This repository seems to be on a crippled filesystem, you must use direct mode." + next perform + , stop + ) + +perform :: CommandPerform +perform = do + showStart "commit" "" + whenM (stageDirect) $ do + showOutput + 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 + -- moveAnnex in indirect mode. + setDirect False + + top <- fromRepo Git.repoPath + (l, clean) <- inRepo $ Git.LsFiles.stagedOthersDetails [top] + forM_ l go + void $ liftIO clean + next cleanup + where + {- Walk tree from top and move all present direct mode files into + - the annex, replacing with symlinks. Also delete direct mode + - caches and mappings. -} + go (_, Nothing) = noop + go (f, Just sha) = do + r <- liftIO $ catchMaybeIO $ getSymbolicLinkStatus f + case r of + Just s + | isSymbolicLink s -> void $ flip whenAnnexed f $ + \_ (k, _) -> do + cleandirect k + return Nothing + | otherwise -> + maybe noop (fromdirect f) + =<< catKey sha + _ -> noop + + fromdirect f k = do + showStart "indirect" f + thawContentDir =<< calcRepo (gitAnnexLocation k) + cleandirect k -- clean before content directory gets frozen + whenM (liftIO $ not . isSymbolicLink <$> getSymbolicLinkStatus f) $ do + moveAnnex k f + l <- inRepo $ gitAnnexLink f k + liftIO $ createSymbolicLink l f + showEndOk + + cleandirect k = do + liftIO . nukeFile =<< calcRepo (gitAnnexInodeCache k) + liftIO . nukeFile =<< calcRepo (gitAnnexMapping k) + +cleanup :: CommandCleanup +cleanup = do + setVersion defaultVersion + showStart "indirect" "" + showEndOk + return True diff --git a/Command/Init.hs b/Command/Init.hs new file mode 100644 index 0000000000..3db9a6be3e --- /dev/null +++ b/Command/Init.hs @@ -0,0 +1,31 @@ +{- git-annex command + - + - Copyright 2010 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Command.Init where + +import Common.Annex +import Command +import Init + +def :: [Command] +def = [dontCheck repoExists $ + command "init" paramDesc seek SectionSetup "initialize git-annex"] + +seek :: [CommandSeek] +seek = [withWords start] + +start :: [String] -> CommandStart +start ws = do + showStart "init" description + next $ perform description + where + description = unwords ws + +perform :: String -> CommandPerform +perform description = do + initialize $ if null description then Nothing else Just description + next $ return True diff --git a/Command/InitRemote.hs b/Command/InitRemote.hs new file mode 100644 index 0000000000..684a2cc91c --- /dev/null +++ b/Command/InitRemote.hs @@ -0,0 +1,101 @@ +{- git-annex command + - + - Copyright 2011,2013 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Command.InitRemote where + +import qualified Data.Map as M + +import Common.Annex +import Command +import qualified Remote +import qualified Logs.Remote +import qualified Types.Remote as R +import Annex.UUID +import Logs.UUID +import Logs.Trust + +import Data.Ord + +def :: [Command] +def = [command "initremote" + (paramPair paramName $ paramOptional $ paramRepeating paramKeyValue) + seek SectionSetup "creates a special (non-git) remote"] + +seek :: [CommandSeek] +seek = [withWords start] + +start :: [String] -> CommandStart +start [] = error "Specify a name for the remote." +start (name:ws) = ifM (isJust <$> findExisting name) + ( error $ "There is already a special remote named \"" ++ name ++ + "\". (Use enableremote to enable an existing special remote.)" + , do + (u, c) <- generateNew name + t <- findType config + + showStart "initremote" name + next $ perform t u name $ M.union config c + ) + where + config = Logs.Remote.keyValToConfig ws + +perform :: RemoteType -> UUID -> String -> R.RemoteConfig -> CommandPerform +perform t u name c = do + c' <- R.setup t u c + next $ cleanup u name c' + +cleanup :: UUID -> String -> R.RemoteConfig -> CommandCleanup +cleanup u name c = do + describeUUID u name + Logs.Remote.configSet u c + return True + +{- See if there's an existing special remote with this name. -} +findExisting :: String -> Annex (Maybe (UUID, R.RemoteConfig)) +findExisting name = do + t <- trustMap + matches <- sortBy (comparing $ \(u, _c) -> M.lookup u t ) + . findByName name + <$> Logs.Remote.readRemoteLog + return $ headMaybe matches + +generateNew :: String -> Annex (UUID, R.RemoteConfig) +generateNew name = do + uuid <- liftIO genUUID + return (uuid, M.singleton nameKey name) + +findByName :: String -> M.Map UUID R.RemoteConfig -> [(UUID, R.RemoteConfig)] +findByName n = filter (matching . snd) . M.toList + where + matching c = case M.lookup nameKey c of + Nothing -> False + Just n' + | n' == n -> True + | otherwise -> False + +remoteNames :: Annex [String] +remoteNames = do + m <- Logs.Remote.readRemoteLog + return $ mapMaybe (M.lookup nameKey . snd) $ M.toList m + +{- find the specified remote type -} +findType :: R.RemoteConfig -> Annex RemoteType +findType config = maybe unspecified specified $ M.lookup typeKey config + where + unspecified = error "Specify the type of remote with type=" + specified s = case filter (findtype s) Remote.remoteTypes of + [] -> error $ "Unknown remote type " ++ s + (t:_) -> return t + findtype s i = R.typename i == s + +{- The name of a configured remote is stored in its config using this key. -} +nameKey :: String +nameKey = "name" + +{- The type of a remote is stored in its config using this key. -} +typeKey :: String +typeKey = "type" diff --git a/Command/Lock.hs b/Command/Lock.hs new file mode 100644 index 0000000000..6dc58df749 --- /dev/null +++ b/Command/Lock.hs @@ -0,0 +1,29 @@ +{- git-annex command + - + - Copyright 2010 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Command.Lock where + +import Common.Annex +import Command +import qualified Annex.Queue + +def :: [Command] +def = [notDirect $ command "lock" paramPaths seek SectionCommon + "undo unlock command"] + +seek :: [CommandSeek] +seek = [withFilesUnlocked start, withFilesUnlockedToBeCommitted start] + +start :: FilePath -> CommandStart +start file = do + showStart "lock" file + next $ perform file + +perform :: FilePath -> CommandPerform +perform file = do + Annex.Queue.addCommand "checkout" [Param "--"] [file] + next $ return True -- no cleanup needed diff --git a/Command/Log.hs b/Command/Log.hs new file mode 100644 index 0000000000..2d4819f7ff --- /dev/null +++ b/Command/Log.hs @@ -0,0 +1,171 @@ +{- git-annex command + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Command.Log where + +import qualified Data.Set as S +import qualified Data.Map as M +import qualified Data.ByteString.Lazy.Char8 as L +import Data.Time.Clock.POSIX +import Data.Time +import System.Locale +import Data.Char + +import Common.Annex +import Command +import qualified Logs.Location +import qualified Logs.Presence +import Annex.CatFile +import qualified Annex.Branch +import qualified Git +import Git.Command +import qualified Remote +import qualified Option +import qualified Annex + +data RefChange = RefChange + { changetime :: POSIXTime + , oldref :: Git.Ref + , newref :: Git.Ref + } + +type Outputter = Bool -> POSIXTime -> [UUID] -> Annex () + +def :: [Command] +def = [withOptions options $ + command "log" paramPaths seek SectionQuery "shows location log"] + +options :: [Option] +options = passthruOptions ++ [gourceOption] + +passthruOptions :: [Option] +passthruOptions = map odate ["since", "after", "until", "before"] ++ + [ Option.field ['n'] "max-count" paramNumber + "limit number of logs displayed" + ] + where + odate n = Option.field [] n paramDate $ "show log " ++ n ++ " date" + +gourceOption :: Option +gourceOption = Option.flag [] "gource" "format output for gource" + +seek :: [CommandSeek] +seek = [withValue Remote.uuidDescriptions $ \m -> + withValue (liftIO getCurrentTimeZone) $ \zone -> + withValue (concat <$> mapM getoption passthruOptions) $ \os -> + withFlag gourceOption $ \gource -> + withFilesInGit $ whenAnnexed $ start m zone os gource] + where + getoption o = maybe [] (use o) <$> + Annex.getField (Option.name o) + use o v = [Param ("--" ++ Option.name o), Param v] + +start :: M.Map UUID String -> TimeZone -> [CommandParam] -> Bool -> + FilePath -> (Key, Backend) -> CommandStart +start m zone os gource file (key, _) = do + showLog output =<< readLog <$> getLog key os + -- getLog produces a zombie; reap it + liftIO reapZombies + stop + where + output + | gource = gourceOutput lookupdescription file + | otherwise = normalOutput lookupdescription file zone + lookupdescription u = fromMaybe (fromUUID u) $ M.lookup u m + +showLog :: Outputter -> [RefChange] -> Annex () +showLog outputter ps = do + sets <- mapM (getset newref) ps + previous <- maybe (return genesis) (getset oldref) (lastMaybe ps) + sequence_ $ compareChanges outputter $ sets ++ [previous] + where + genesis = (0, S.empty) + getset select change = do + s <- S.fromList <$> get (select change) + return (changetime change, s) + get ref = map toUUID . Logs.Presence.getLog . L.unpack <$> + catObject ref + +normalOutput :: (UUID -> String) -> FilePath -> TimeZone -> Outputter +normalOutput lookupdescription file zone present ts us = + liftIO $ mapM_ (putStrLn . format) us + where + time = showTimeStamp zone ts + addel = if present then "+" else "-" + format u = unwords [ addel, time, file, "|", + fromUUID u ++ " -- " ++ lookupdescription u ] + +gourceOutput :: (UUID -> String) -> FilePath -> Outputter +gourceOutput lookupdescription file present ts us = + liftIO $ mapM_ (putStrLn . intercalate "|" . format) us + where + time = takeWhile isDigit $ show ts + addel = if present then "A" else "M" + format u = [ time, lookupdescription u, addel, file ] + +{- Generates a display of the changes (which are ordered with newest first), + - by comparing each change with the previous change. + - Uses a formatter to generate a display of items that are added and + - removed. -} +compareChanges :: Ord a => (Bool -> POSIXTime -> [a] -> b) -> [(POSIXTime, S.Set a)] -> [b] +compareChanges format changes = concatMap diff $ zip changes (drop 1 changes) + where + diff ((ts, new), (_, old)) = + [format True ts added, format False ts removed] + where + added = S.toList $ S.difference new old + removed = S.toList $ S.difference old new + +{- Gets the git log for a given location log file. + - + - This is complicated by git log using paths relative to the current + - directory, even when looking at files in a different branch. A wacky + - relative path to the log file has to be used. + - + - The --remove-empty is a significant optimisation. It relies on location + - log files never being deleted in normal operation. Letting git stop + - once the location log file is gone avoids it checking all the way back + - to commit 0 to see if it used to exist, so generally speeds things up a + - *lot* for newish files. -} +getLog :: Key -> [CommandParam] -> Annex [String] +getLog key os = do + top <- fromRepo Git.repoPath + p <- liftIO $ relPathCwdToFile top + let logfile = p Logs.Location.logFile key + inRepo $ pipeNullSplitZombie $ + [ Params "log -z --pretty=format:%ct --raw --abbrev=40" + , Param "--remove-empty" + ] ++ os ++ + [ Param $ show Annex.Branch.fullname + , Param "--" + , Param logfile + ] + +readLog :: [String] -> [RefChange] +readLog = mapMaybe (parse . lines) + where + parse (ts:raw:[]) = let (old, new) = parseRaw raw in + Just RefChange + { changetime = parseTimeStamp ts + , oldref = old + , newref = new + } + parse _ = Nothing + +-- Parses something like ":100644 100644 oldsha newsha M" +parseRaw :: String -> (Git.Ref, Git.Ref) +parseRaw l = go $ words l + where + go (_:_:oldsha:newsha:_) = (Git.Ref oldsha, Git.Ref newsha) + go _ = error $ "unable to parse git log output: " ++ l + +parseTimeStamp :: String -> POSIXTime +parseTimeStamp = utcTimeToPOSIXSeconds . fromMaybe (error "bad timestamp") . + parseTime defaultTimeLocale "%s" + +showTimeStamp :: TimeZone -> POSIXTime -> String +showTimeStamp zone = show . utcToLocalTime zone . posixSecondsToUTCTime diff --git a/Command/Map.hs b/Command/Map.hs new file mode 100644 index 0000000000..c88520b079 --- /dev/null +++ b/Command/Map.hs @@ -0,0 +1,247 @@ +{- git-annex command + - + - Copyright 2010 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Command.Map where + +import Control.Exception.Extensible +import qualified Data.Map as M + +import Common.Annex +import Command +import qualified Git +import qualified Git.Url +import qualified Git.Config +import qualified Git.Construct +import qualified Annex +import Annex.UUID +import Logs.UUID +import Logs.Trust +import Remote.Helper.Ssh +import qualified Utility.Dot as Dot + +-- a link from the first repository to the second (its remote) +data Link = Link Git.Repo Git.Repo + +def :: [Command] +def = [dontCheck repoExists $ + command "map" paramNothing seek SectionQuery + "generate map of repositories"] + +seek :: [CommandSeek] +seek = [withNothing start] + +start :: CommandStart +start = do + rs <- combineSame <$> (spider =<< gitRepo) + + umap <- uuidMap + trusted <- trustGet Trusted + + file <- () <$> fromRepo gitAnnexDir <*> pure "map.dot" + + liftIO $ writeFile file (drawMap rs umap trusted) + next $ next $ + ifM (Annex.getState Annex.fast) + ( return True + , do + showLongNote $ "running: dot -Tx11 " ++ file + showOutput + liftIO $ boolSystem "dot" [Param "-Tx11", File file] + ) + +{- Generates a graph for dot(1). Each repository, and any other uuids, are + - displayed as a node, and each of its remotes is represented as an edge + - pointing at the node for the remote. + - + - The order nodes are added to the graph matters, since dot will draw + - the first ones near to the top and left. So it looks better to put + - the repositories first, followed by uuids that were not matched + - to a repository. + -} +drawMap :: [Git.Repo] -> M.Map UUID String -> [UUID] -> String +drawMap rs umap ts = Dot.graph $ repos ++ trusted ++ others + where + repos = map (node umap rs) rs + ruuids = ts ++ map getUncachedUUID rs + others = map (unreachable . uuidnode) $ + filter (`notElem` ruuids) (M.keys umap) + trusted = map (trustworthy . uuidnode) ts + uuidnode u = Dot.graphNode (fromUUID u) $ M.findWithDefault "" u umap + +hostname :: Git.Repo -> String +hostname r + | Git.repoIsUrl r = Git.Url.host r + | otherwise = "localhost" + +basehostname :: Git.Repo -> String +basehostname r = fromMaybe "" $ headMaybe $ split "." $ hostname r + +{- A name to display for a repo. Uses the name from uuid.log if available, + - or the remote name if not. -} +repoName :: M.Map UUID String -> Git.Repo -> String +repoName umap r + | repouuid == NoUUID = fallback + | otherwise = M.findWithDefault fallback repouuid umap + where + repouuid = getUncachedUUID r + fallback = fromMaybe "unknown" $ Git.remoteName r + +{- A unique id for the node for a repo. Uses the annex.uuid if available. -} +nodeId :: Git.Repo -> String +nodeId r = + case getUncachedUUID r of + NoUUID -> Git.repoLocation r + UUID u -> u + +{- A node representing a repo. -} +node :: M.Map UUID String -> [Git.Repo] -> Git.Repo -> String +node umap fullinfo r = unlines $ n:edges + where + n = Dot.subGraph (hostname r) (basehostname r) "lightblue" $ + decorate $ Dot.graphNode (nodeId r) (repoName umap r) + edges = map (edge umap fullinfo r) (Git.remotes r) + decorate + | Git.config r == M.empty = unreachable + | otherwise = reachable + +{- An edge between two repos. The second repo is a remote of the first. -} +edge :: M.Map UUID String -> [Git.Repo] -> Git.Repo -> Git.Repo -> String +edge umap fullinfo from to = + Dot.graphEdge (nodeId from) (nodeId fullto) edgename + where + -- get the full info for the remote, to get its UUID + fullto = findfullinfo to + findfullinfo n = + case filter (same n) fullinfo of + [] -> n + (n':_) -> n' + {- Only name an edge if the name is different than the name + - that will be used for the destination node, and is + - different from its hostname. (This reduces visual clutter.) -} + edgename = maybe Nothing calcname $ Git.remoteName to + calcname n + | n `elem` [repoName umap fullto, hostname fullto] = Nothing + | otherwise = Just n + +unreachable :: String -> String +unreachable = Dot.fillColor "red" +reachable :: String -> String +reachable = Dot.fillColor "white" +trustworthy :: String -> String +trustworthy = Dot.fillColor "green" + +{- Recursively searches out remotes starting with the specified repo. -} +spider :: Git.Repo -> Annex [Git.Repo] +spider r = spider' [r] [] +spider' :: [Git.Repo] -> [Git.Repo] -> Annex [Git.Repo] +spider' [] known = return known +spider' (r:rs) known + | any (same r) known = spider' rs known + | otherwise = do + r' <- scan r + + -- The remotes will be relative to r', and need to be + -- made absolute for later use. + remotes <- mapM (absRepo r') (Git.remotes r') + let r'' = r' { Git.remotes = remotes } + + spider' (rs ++ remotes) (r'':known) + +{- Converts repos to a common absolute form. -} +absRepo :: Git.Repo -> Git.Repo -> Annex Git.Repo +absRepo reference r + | Git.repoIsUrl reference = return $ Git.Construct.localToUrl reference r + | Git.repoIsUrl r = return r + | otherwise = liftIO $ Git.Construct.fromAbsPath =<< absPath (Git.repoPath r) + +{- Checks if two repos are the same. -} +same :: Git.Repo -> Git.Repo -> Bool +same a b + | both Git.repoIsSsh = matching Git.Url.authority && matching Git.repoPath + | both Git.repoIsUrl && neither Git.repoIsSsh = matching show + | neither Git.repoIsSsh = matching Git.repoPath + | otherwise = False + where + matching t = t a == t b + both t = t a && t b + neither t = not (t a) && not (t b) + +{- reads the config of a remote, with progress display -} +scan :: Git.Repo -> Annex Git.Repo +scan r = do + showStart "map" $ Git.repoDescribe r + v <- tryScan r + case v of + Just r' -> do + showEndOk + return r' + Nothing -> do + showOutput + showEndFail + return r + +{- tries to read the config of a remote, returning it only if it can + - be accessed -} +tryScan :: Git.Repo -> Annex (Maybe Git.Repo) +tryScan r + | Git.repoIsSsh r = sshscan + | Git.repoIsUrl r = return Nothing + | otherwise = safely $ Git.Config.read r + where + safely a = do + result <- liftIO (try a :: IO (Either SomeException Git.Repo)) + case result of + Left _ -> return Nothing + Right r' -> return $ Just r' + pipedconfig cmd params = safely $ + withHandle StdoutHandle createProcessSuccess p $ + Git.Config.hRead r + where + p = proc cmd $ toCommand params + + configlist = onRemote r (pipedconfig, Nothing) "configlist" [] [] + manualconfiglist = do + sshparams <- sshToRepo r [Param sshcmd] + liftIO $ pipedconfig "ssh" sshparams + where + sshcmd = cddir ++ " && " ++ + "git config --null --list" + dir = Git.repoPath r + cddir + | "/~" `isPrefixOf` dir = + let (userhome, reldir) = span (/= '/') (drop 1 dir) + in "cd " ++ userhome ++ " && cd " ++ shellEscape (drop 1 reldir) + | otherwise = "cd " ++ shellEscape dir + + -- First, try sshing and running git config manually, + -- only fall back to git-annex-shell configlist if that + -- fails. + -- + -- This is done for two reasons, first I'd like this + -- subcommand to be usable on non-git-annex repos. + -- Secondly, configlist doesn't include information about + -- the remote's remotes. + sshscan = do + sshnote + v <- manualconfiglist + case v of + Nothing -> do + sshnote + configlist + ok -> return ok + + sshnote = do + showAction "sshing" + showOutput + +{- Spidering can find multiple paths to the same repo, so this is used + - to combine (really remove) duplicate repos with the same UUID. -} +combineSame :: [Git.Repo] -> [Git.Repo] +combineSame = map snd . nubBy sameuuid . map pair + where + sameuuid (u1, _) (u2, _) = u1 == u2 && u1 /= NoUUID + pair r = (getUncachedUUID r, r) diff --git a/Command/Merge.hs b/Command/Merge.hs new file mode 100644 index 0000000000..659f14080c --- /dev/null +++ b/Command/Merge.hs @@ -0,0 +1,38 @@ +{- git-annex command + - + - Copyright 2011, 2013 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Command.Merge where + +import Common.Annex +import Command +import qualified Annex.Branch +import qualified Git.Branch +import Command.Sync (mergeLocal) + +def :: [Command] +def = [command "merge" paramNothing seek SectionMaintenance + "automatically merge changes from remotes"] + +seek :: [CommandSeek] +seek = + [ withNothing mergeBranch + , withNothing mergeSynced + ] + +mergeBranch :: CommandStart +mergeBranch = do + showStart "merge" "git-annex" + next $ do + Annex.Branch.update + -- commit explicitly, in case no remote branches were merged + Annex.Branch.commit "update" + next $ return True + +mergeSynced :: CommandStart +mergeSynced = do + branch <- inRepo Git.Branch.current + maybe stop mergeLocal branch diff --git a/Command/Migrate.hs b/Command/Migrate.hs new file mode 100644 index 0000000000..0fdf0e8176 --- /dev/null +++ b/Command/Migrate.hs @@ -0,0 +1,77 @@ +{- git-annex command + - + - Copyright 2011 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Command.Migrate where + +import Common.Annex +import Command +import Backend +import qualified Types.Key +import qualified Types.Backend +import Types.KeySource +import Annex.Content +import qualified Command.ReKey +import qualified Command.Fsck + +def :: [Command] +def = [notDirect $ + command "migrate" paramPaths seek + SectionUtility "switch data to different backend"] + +seek :: [CommandSeek] +seek = [withFilesInGit $ whenAnnexed start] + +start :: FilePath -> (Key, Backend) -> CommandStart +start file (key, oldbackend) = do + exists <- inAnnex key + newbackend <- choosebackend =<< chooseBackend file + if (newbackend /= oldbackend || upgradableKey oldbackend key) && exists + then do + showStart "migrate" file + next $ perform file key oldbackend newbackend + else stop + where + choosebackend Nothing = Prelude.head <$> orderedList + choosebackend (Just backend) = return backend + +{- Checks if a key is upgradable to a newer representation. + - + - Reasons for migration: + - - Ideally, all keys have file size metadata. Old keys may not. + - - Something has changed in the backend, such as a bug fix. + -} +upgradableKey :: Backend -> Key -> Bool +upgradableKey backend key = isNothing (Types.Key.keySize key) || backendupgradable + where + backendupgradable = maybe False (\a -> a key) + (Types.Backend.canUpgradeKey backend) + +{- Store the old backend's key in the new backend + - The old backend's key is not dropped from it, because there may + - be other files still pointing at that key. + - + - To ensure that the data we have for the old key is valid, it's + - fscked here. First we generate the new key. This ensures that the + - data cannot get corrupted after the fsck but before the new key is + - generated. + -} +perform :: FilePath -> Key -> Backend -> Backend -> CommandPerform +perform file oldkey oldbackend newbackend = go =<< genkey + where + go Nothing = stop + go (Just newkey) = stopUnless checkcontent $ finish newkey + checkcontent = Command.Fsck.checkBackend oldbackend oldkey $ Just file + finish newkey = stopUnless (Command.ReKey.linkKey oldkey newkey) $ + next $ Command.ReKey.cleanup file oldkey newkey + genkey = do + content <- calcRepo $ gitAnnexLocation oldkey + let source = KeySource + { keyFilename = file + , contentLocation = content + , inodeCache = Nothing + } + liftM fst <$> genKey source (Just newbackend) diff --git a/Command/Move.hs b/Command/Move.hs new file mode 100644 index 0000000000..357ccc21ea --- /dev/null +++ b/Command/Move.hs @@ -0,0 +1,173 @@ +{- git-annex command + - + - Copyright 2010-2013 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Command.Move where + +import Common.Annex +import Command +import qualified Command.Drop +import qualified Annex +import Annex.Content +import qualified Remote +import Annex.UUID +import qualified Option +import Logs.Presence +import Logs.Transfer +import GitAnnex.Options +import Types.Key + +def :: [Command] +def = [withOptions moveOptions $ command "move" paramPaths seek + SectionCommon "move content of files to/from another repository"] + +fromOption :: Option +fromOption = Option.field ['f'] "from" paramRemote "source remote" + +toOption :: Option +toOption = Option.field ['t'] "to" paramRemote "destination remote" + +moveOptions :: [Option] +moveOptions = [fromOption, toOption] ++ keyOptions + +seek :: [CommandSeek] +seek = + [ withField toOption Remote.byNameWithUUID $ \to -> + withField fromOption Remote.byNameWithUUID $ \from -> + withKeyOptions (startKey to from True) $ + withFilesInGit $ whenAnnexed $ start to from True + ] + +start :: Maybe Remote -> Maybe Remote -> Bool -> FilePath -> (Key, Backend) -> CommandStart +start to from move file (key, _) = start' to from move (Just file) key + +startKey :: Maybe Remote -> Maybe Remote -> Bool -> Key -> CommandStart +startKey to from move key = start' to from move Nothing key + +start' :: Maybe Remote -> Maybe Remote -> Bool -> AssociatedFile -> Key -> CommandStart +start' to from move afile key = do + noAuto + case (from, to) of + (Nothing, Nothing) -> error "specify either --from or --to" + (Nothing, Just dest) -> toStart dest move afile key + (Just src, Nothing) -> fromStart src move afile key + (_ , _) -> error "only one of --from or --to can be specified" + where + noAuto = when move $ whenM (Annex.getState Annex.auto) $ error + "--auto is not supported for move" + +showMoveAction :: Bool -> Key -> AssociatedFile -> Annex () +showMoveAction True _ (Just file) = showStart "move" file +showMoveAction False _ (Just file) = showStart "copy" file +showMoveAction True key Nothing = showStart "move" (key2file key) +showMoveAction False key Nothing = showStart "copy" (key2file key) + +{- Moves (or copies) the content of an annexed file to a remote. + - + - If the remote already has the content, it is still removed from + - the current repository. + - + - Note that unlike drop, this does not honor annex.numcopies. + - A file's content can be moved even if there are insufficient copies to + - allow it to be dropped. + -} +toStart :: Remote -> Bool -> AssociatedFile -> Key -> CommandStart +toStart dest move afile key = do + u <- getUUID + ishere <- inAnnex key + if not ishere || u == Remote.uuid dest + then stop -- not here, so nothing to do + else do + showMoveAction move key afile + next $ toPerform dest move key afile +toPerform :: Remote -> Bool -> Key -> AssociatedFile -> CommandPerform +toPerform dest move key afile = moveLock move key $ do + -- Checking the remote is expensive, so not done in the start step. + -- In fast mode, location tracking is assumed to be correct, + -- and an explicit check is not done, when copying. When moving, + -- it has to be done, to avoid inaverdent data loss. + fast <- Annex.getState Annex.fast + let fastcheck = fast && not move && not (Remote.hasKeyCheap dest) + isthere <- if fastcheck + then Right <$> expectedpresent + else Remote.hasKey dest key + case isthere of + Left err -> do + showNote err + stop + Right False -> do + showAction $ "to " ++ Remote.name dest + ok <- upload (Remote.uuid dest) key afile noRetry $ + Remote.storeKey dest key afile + if ok + then do + Remote.logStatus dest key InfoPresent + finish + else do + when fastcheck $ + warning "This could have failed because --fast is enabled." + stop + Right True -> do + unlessM expectedpresent $ + Remote.logStatus dest key InfoPresent + finish + where + finish + | move = do + removeAnnex key + next $ Command.Drop.cleanupLocal key + | otherwise = next $ return True + expectedpresent = do + remotes <- Remote.keyPossibilities key + return $ dest `elem` remotes + +{- Moves (or copies) the content of an annexed file from a remote + - to the current repository. + - + - If the current repository already has the content, it is still removed + - from the remote. + -} +fromStart :: Remote -> Bool -> AssociatedFile -> Key -> CommandStart +fromStart src move afile key + | move = go + | otherwise = stopUnless (not <$> inAnnex key) go + where + go = stopUnless (fromOk src key) $ do + showMoveAction move key afile + next $ fromPerform src move key afile + +fromOk :: Remote -> Key -> Annex Bool +fromOk src key + | Remote.hasKeyCheap src = + either (const expensive) return =<< Remote.hasKey src key + | otherwise = expensive + where + expensive = do + u <- getUUID + remotes <- Remote.keyPossibilities key + return $ u /= Remote.uuid src && elem src remotes + +fromPerform :: Remote -> Bool -> Key -> AssociatedFile -> CommandPerform +fromPerform src move key afile = moveLock move key $ + ifM (inAnnex key) + ( handle move True + , handle move =<< go + ) + where + go = download (Remote.uuid src) key afile noRetry $ \p -> do + showAction $ "from " ++ Remote.name src + getViaTmp key $ \t -> Remote.retrieveKeyFile src key afile t p + handle _ False = stop -- failed + handle False True = next $ return True -- copy complete + handle True True = do -- finish moving + ok <- Remote.removeKey src key + next $ Command.Drop.cleanupRemote key src ok + +{- Locks a key in order for it to be moved. + - No lock is needed when a key is being copied. -} +moveLock :: Bool -> Key -> Annex a -> Annex a +moveLock True key a = lockContent key a +moveLock False _ a = a diff --git a/Command/PreCommit.hs b/Command/PreCommit.hs new file mode 100644 index 0000000000..565344d257 --- /dev/null +++ b/Command/PreCommit.hs @@ -0,0 +1,53 @@ +{- git-annex command + - + - Copyright 2010, 2013 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Command.PreCommit where + +import Common.Annex +import Command +import qualified Command.Add +import qualified Command.Fix +import qualified Git.DiffTree +import Annex.CatFile +import Annex.Content.Direct +import Git.Sha + +def :: [Command] +def = [command "pre-commit" paramPaths seek SectionPlumbing + "run by git pre-commit hook"] + +seek :: [CommandSeek] +seek = + -- fix symlinks to files being committed + [ whenNotDirect $ withFilesToBeCommitted $ whenAnnexed $ Command.Fix.start + -- inject unlocked files into the annex + , whenNotDirect $ withFilesUnlockedToBeCommitted startIndirect + -- update direct mode mappings for committed files + , whenDirect $ withWords startDirect + ] + +startIndirect :: FilePath -> CommandStart +startIndirect file = next $ do + unlessM (doCommand $ Command.Add.start file) $ + error $ "failed to add " ++ file ++ "; canceling commit" + next $ return True + +startDirect :: [String] -> CommandStart +startDirect _ = next $ do + (diffs, clean) <- inRepo $ Git.DiffTree.diffIndex + forM_ diffs go + next $ liftIO clean + where + go diff = do + withkey (Git.DiffTree.srcsha diff) removeAssociatedFile + withkey (Git.DiffTree.dstsha diff) addAssociatedFile + where + withkey sha a = when (sha /= nullSha) $ do + k <- catKey sha + case k of + Nothing -> noop + Just key -> void $ a key (Git.DiffTree.file diff) diff --git a/Command/ReKey.hs b/Command/ReKey.hs new file mode 100644 index 0000000000..d7b277fa69 --- /dev/null +++ b/Command/ReKey.hs @@ -0,0 +1,71 @@ +{- git-annex command + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Command.ReKey where + +import Common.Annex +import Command +import qualified Annex +import Types.Key +import Annex.Content +import qualified Command.Add +import Logs.Web +import Logs.Location +import Utility.CopyFile + +def :: [Command] +def = [notDirect $ command "rekey" + (paramOptional $ paramRepeating $ paramPair paramPath paramKey) + seek SectionPlumbing "change keys used for files"] + +seek :: [CommandSeek] +seek = [withPairs start] + +start :: (FilePath, String) -> CommandStart +start (file, keyname) = ifAnnexed file go stop + where + newkey = fromMaybe (error "bad key") $ file2key keyname + go (oldkey, _) + | oldkey == newkey = stop + | otherwise = do + showStart "rekey" file + next $ perform file oldkey newkey + +perform :: FilePath -> Key -> Key -> CommandPerform +perform file oldkey newkey = do + present <- inAnnex oldkey + _ <- if present + then linkKey oldkey newkey + else do + unlessM (Annex.getState Annex.force) $ + error $ file ++ " is not available (use --force to override)" + return True + next $ cleanup file oldkey newkey + +{- Make a hard link to the old key content (when supported), + - to avoid wasting disk space. -} +linkKey :: Key -> Key -> Annex Bool +linkKey oldkey newkey = getViaTmpUnchecked newkey $ \tmp -> do + src <- calcRepo $ gitAnnexLocation oldkey + liftIO $ ifM (doesFileExist tmp) + ( return True + , createLinkOrCopy src tmp + ) + +cleanup :: FilePath -> Key -> Key -> CommandCleanup +cleanup file oldkey newkey = do + -- If the old key had some associated urls, record them for + -- the new key as well. + urls <- getUrls oldkey + unless (null urls) $ + mapM_ (setUrlPresent newkey) urls + + -- Update symlink to use the new key. + liftIO $ removeFile file + Command.Add.addLink file newkey True + logStatus newkey InfoPresent + return True diff --git a/Command/RecvKey.hs b/Command/RecvKey.hs new file mode 100644 index 0000000000..c316e2ca54 --- /dev/null +++ b/Command/RecvKey.hs @@ -0,0 +1,78 @@ +{- git-annex command + - + - Copyright 2010 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Command.RecvKey where + +import System.PosixCompat.Files + +import Common.Annex +import Command +import CmdLine +import Annex.Content +import Annex +import Utility.Rsync +import Logs.Transfer +import Command.SendKey (fieldTransfer) +import qualified Fields +import qualified Types.Key +import qualified Types.Backend +import qualified Backend + +def :: [Command] +def = [noCommit $ command "recvkey" paramKey seek + SectionPlumbing "runs rsync in server mode to receive content"] + +seek :: [CommandSeek] +seek = [withKeys start] + +start :: Key -> CommandStart +start key = ifM (inAnnex key) + ( error "key is already present in annex" + , fieldTransfer Download key $ \_p -> do + ifM (getViaTmp key go) + ( do + -- forcibly quit after receiving one key, + -- and shutdown cleanly + _ <- shutdown True + return True + , return False + ) + ) + where + go tmp = do + opts <- filterRsyncSafeOptions . maybe [] words + <$> getField "RsyncOptions" + ok <- liftIO $ rsyncServerReceive (map Param opts) tmp + + -- The file could have been received with permissions that + -- do not allow reading it, so this is done before the + -- directcheck. + freezeContent tmp + + if ok + then ifM (isJust <$> Fields.getField Fields.direct) + ( directcheck tmp + , return True + ) + else return False + {- If the sending repository uses direct mode, the file + - it sends could be modified as it's sending it. So check + - that the right size file was received, and that the key/value + - Backend is happy with it. -} + directcheck tmp = do + oksize <- case Types.Key.keySize key of + Nothing -> return True + Just size -> do + size' <- fromIntegral . fileSize + <$> liftIO (getFileStatus tmp) + return $ size == size' + if oksize + then case Backend.maybeLookupBackendName (Types.Key.keyBackendName key) of + Nothing -> return False + Just backend -> maybe (return True) (\a -> a key tmp) + (Types.Backend.fsckKey backend) + else return False diff --git a/Command/Reinject.hs b/Command/Reinject.hs new file mode 100644 index 0000000000..642f38947f --- /dev/null +++ b/Command/Reinject.hs @@ -0,0 +1,58 @@ +{- git-annex command + - + - Copyright 2011 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Command.Reinject where + +import Common.Annex +import Command +import Logs.Location +import Annex.Content +import qualified Command.Fsck + +def :: [Command] +def = [notDirect $ command "reinject" (paramPair "SRC" "DEST") seek + SectionUtility "sets content of annexed file"] + +seek :: [CommandSeek] +seek = [withWords start] + +start :: [FilePath] -> CommandStart +start (src:dest:[]) + | src == dest = stop + | otherwise = + ifAnnexed src + (error $ "cannot used annexed file as src: " ++ src) + go + where + go = do + showStart "reinject" dest + next $ whenAnnexed (perform src) dest +start _ = error "specify a src file and a dest file" + +perform :: FilePath -> FilePath -> (Key, Backend) -> CommandPerform +perform src _dest (key, backend) = do + {- Check the content before accepting it. -} + ifM (Command.Fsck.checkKeySizeOr reject key src + <&&> Command.Fsck.checkBackendOr reject backend key src) + ( do + unlessM move $ error "mv failed!" + next $ cleanup key + , error "not reinjecting" + ) + where + -- the file might be on a different filesystem, + -- so mv is used rather than simply calling + -- moveToObjectDir; disk space is also + -- checked this way. + move = getViaTmp key $ \tmp -> + liftIO $ boolSystem "mv" [File src, File tmp] + reject = const $ return "wrong file?" + +cleanup :: Key -> CommandCleanup +cleanup key = do + logStatus key InfoPresent + return True diff --git a/Command/RmUrl.hs b/Command/RmUrl.hs new file mode 100644 index 0000000000..d3ded38a39 --- /dev/null +++ b/Command/RmUrl.hs @@ -0,0 +1,30 @@ +{- git-annex command + - + - Copyright 2013 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Command.RmUrl where + +import Common.Annex +import Command +import Logs.Web + +def :: [Command] +def = [notBareRepo $ + command "rmurl" (paramPair paramFile paramUrl) seek + SectionCommon "record file is not available at url"] + +seek :: [CommandSeek] +seek = [withPairs start] + +start :: (FilePath, String) -> CommandStart +start (file, url) = flip whenAnnexed file $ \_ (key, _) -> do + showStart "rmurl" file + next $ next $ cleanup url key + +cleanup :: String -> Key -> CommandCleanup +cleanup url key = do + setUrlMissing key url + return True diff --git a/Command/Semitrust.hs b/Command/Semitrust.hs new file mode 100644 index 0000000000..e205636726 --- /dev/null +++ b/Command/Semitrust.hs @@ -0,0 +1,32 @@ +{- git-annex command + - + - Copyright 2010 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Command.Semitrust where + +import Common.Annex +import Command +import qualified Remote +import Logs.Trust + +def :: [Command] +def = [command "semitrust" (paramRepeating paramRemote) seek + SectionSetup "return repository to default trust level"] + +seek :: [CommandSeek] +seek = [withWords start] + +start :: [String] -> CommandStart +start ws = do + let name = unwords ws + showStart "semitrust" name + u <- Remote.nameToUUID name + next $ perform u + +perform :: UUID -> CommandPerform +perform uuid = do + trustSet uuid SemiTrusted + next $ return True diff --git a/Command/SendKey.hs b/Command/SendKey.hs new file mode 100644 index 0000000000..afd1ac1e01 --- /dev/null +++ b/Command/SendKey.hs @@ -0,0 +1,51 @@ +{- git-annex command + - + - Copyright 2010,2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Command.SendKey where + +import Common.Annex +import Command +import Annex.Content +import Annex +import Utility.Rsync +import Logs.Transfer +import qualified Fields +import Utility.Metered + +def :: [Command] +def = [noCommit $ command "sendkey" paramKey seek + SectionPlumbing "runs rsync in server mode to send content"] + +seek :: [CommandSeek] +seek = [withKeys start] + +start :: Key -> CommandStart +start key = do + opts <- filterRsyncSafeOptions . maybe [] words + <$> getField "RsyncOptions" + ifM (inAnnex key) + ( fieldTransfer Upload key $ \_p -> + sendAnnex key rollback $ liftIO . rsyncServerSend (map Param opts) + , do + warning "requested key is not present" + liftIO exitFailure + ) + where + {- No need to do any rollback; when sendAnnex fails, a nonzero + - exit will be propigated, and the remote will know the transfer + - failed. -} + rollback = noop + +fieldTransfer :: Direction -> Key -> (MeterUpdate -> Annex Bool) -> CommandStart +fieldTransfer direction key a = do + afile <- Fields.getField Fields.associatedFile + ok <- maybe (a $ const noop) + (\u -> runTransfer (Transfer direction (toUUID u) key) afile noRetry a) + =<< Fields.getField Fields.remoteUUID + if ok + then liftIO exitSuccess + else liftIO exitFailure diff --git a/Command/Status.hs b/Command/Status.hs new file mode 100644 index 0000000000..af85fcc2a8 --- /dev/null +++ b/Command/Status.hs @@ -0,0 +1,345 @@ +{- git-annex command + - + - Copyright 2011 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE BangPatterns #-} + +module Command.Status where + +import "mtl" Control.Monad.State.Strict +import qualified Data.Map as M +import Text.JSON +import Data.Tuple +import System.PosixCompat.Files + +import Common.Annex +import qualified Types.Backend as B +import qualified Types.Remote as R +import qualified Remote +import qualified Command.Unused +import qualified Git +import qualified Annex +import Command +import Utility.DataUnits +import Utility.DiskFree +import Annex.Content +import Types.Key +import Backend +import Logs.UUID +import Logs.Trust +import Remote +import Config +import Utility.Percentage +import Logs.Transfer +import Types.TrustLevel +import Types.FileMatcher +import qualified Limit + +-- a named computation that produces a statistic +type Stat = StatState (Maybe (String, StatState String)) + +-- data about a set of keys +data KeyData = KeyData + { countKeys :: Integer + , sizeKeys :: Integer + , unknownSizeKeys :: Integer + , backendsKeys :: M.Map String Integer + } + +-- cached info that multiple Stats use +data StatInfo = StatInfo + { presentData :: Maybe KeyData + , referencedData :: Maybe KeyData + } + +-- a state monad for running Stats in +type StatState = StateT StatInfo Annex + +def :: [Command] +def = [command "status" paramPaths seek + SectionQuery "shows status information about the annex"] + +seek :: [CommandSeek] +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. + -} +global_fast_stats :: [Stat] +global_fast_stats = + [ supported_backends + , supported_remote_types + , repository_mode + , remote_list Trusted + , remote_list SemiTrusted + , remote_list UnTrusted + , transfer_list + , disk_size + ] +global_slow_stats :: [Stat] +global_slow_stats = + [ tmp_size + , bad_data_size + , local_annex_keys + , local_annex_size + , known_annex_keys + , known_annex_size + , bloom_info + , backend_usage + ] +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) + +nostat :: Stat +nostat = return Nothing + +json :: JSON j => (j -> String) -> StatState j -> String -> StatState String +json serialize a desc = do + j <- a + lift $ maybeShowJSON [(desc, j)] + return $ serialize j + +nojson :: StatState String -> String -> StatState String +nojson a _ = a + +showStat :: Stat -> StatState () +showStat s = maybe noop calc =<< s + where + calc (desc, a) = do + (lift . showHeader) desc + lift . showRaw =<< a + +supported_backends :: Stat +supported_backends = stat "supported backends" $ json unwords $ + return $ map B.name Backend.list + +supported_remote_types :: Stat +supported_remote_types = stat "supported remote types" $ json unwords $ + return $ map R.typename Remote.remoteTypes + +repository_mode :: Stat +repository_mode = stat "repository mode" $ json id $ lift $ + ifM isDirect + ( return "direct", return "indirect" ) + +remote_list :: TrustLevel -> Stat +remote_list level = stat n $ nojson $ lift $ do + us <- M.keys <$> (M.union <$> uuidMap <*> remoteMap Remote.name) + rs <- fst <$> trustPartition level us + s <- prettyPrintUUIDs n rs + return $ if null s then "0" else show (length rs) ++ "\n" ++ beginning s + 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 + +local_annex_keys :: Stat +local_annex_keys = stat "local annex keys" $ json show $ + countKeys <$> cachedPresentData + +known_annex_size :: Stat +known_annex_size = stat "known annex size" $ json id $ + showSizeKeys <$> cachedReferencedData + +known_annex_keys :: Stat +known_annex_keys = stat "known annex keys" $ json show $ + countKeys <$> cachedReferencedData + +tmp_size :: Stat +tmp_size = staleSize "temporary directory size" gitAnnexTmpDir + +bad_data_size :: Stat +bad_data_size = staleSize "bad keys size" gitAnnexBadDir + +bloom_info :: Stat +bloom_info = stat "bloom filter size" $ json id $ do + localkeys <- countKeys <$> cachedPresentData + capacity <- fromIntegral <$> lift Command.Unused.bloomCapacity + let note = aside $ + if localkeys >= capacity + then "appears too small for this repository; adjust annex.bloomcapacity" + else showPercentage 1 (percentage capacity localkeys) ++ " full" + + -- Two bloom filters are used at the same time, so double the size + -- of one. + size <- roughSize memoryUnits False . (* 2) . fromIntegral . fst <$> + lift Command.Unused.bloomBitsHashes + + return $ size ++ note + +transfer_list :: Stat +transfer_list = stat "transfers in progress" $ nojson $ lift $ do + uuidmap <- Remote.remoteMap id + ts <- getTransfers + if null ts + then return "none" + else return $ multiLine $ + map (\(t, i) -> line uuidmap t i) $ sort ts + where + line uuidmap t i = unwords + [ showLcDirection (transferDirection t) ++ "ing" + , fromMaybe (key2file $ transferKey t) (associatedFile i) + , if transferDirection t == Upload then "to" else "from" + , maybe (fromUUID $ transferUUID t) Remote.name $ + M.lookup (transferUUID t) uuidmap + ] + +disk_size :: Stat +disk_size = stat "available local disk space" $ json id $ lift $ + calcfree + <$> (annexDiskReserve <$> Annex.getGitConfig) + <*> inRepo (getDiskFree . gitAnnexDir) + where + calcfree reserve (Just have) = unwords + [ roughSize storageUnits False $ nonneg $ have - reserve + , "(+" ++ roughSize storageUnits False reserve + , "reserved)" + ] + calcfree _ _ = "unknown" + + nonneg x + | x >= 0 = x + | otherwise = 0 + +backend_usage :: Stat +backend_usage = stat "backend usage" $ nojson $ + calc + <$> (backendsKeys <$> cachedReferencedData) + <*> (backendsKeys <$> cachedPresentData) + where + calc x y = multiLine $ + map (\(n, b) -> b ++ ": " ++ show n) $ + reverse $ sort $ map swap $ M.toList $ + M.unionWith (+) x y + +cachedPresentData :: StatState KeyData +cachedPresentData = do + s <- get + case presentData s of + Just v -> return v + Nothing -> do + v <- foldKeys <$> lift getKeysPresent + put s { presentData = Just v } + return v + +cachedReferencedData :: StatState KeyData +cachedReferencedData = do + s <- get + case referencedData s of + Just v -> return v + Nothing -> do + !v <- lift $ Command.Unused.withKeysReferenced + emptyKeyData addKey + 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 $ 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 + +foldKeys :: [Key] -> KeyData +foldKeys = foldl' (flip addKey) emptyKeyData + +addKey :: Key -> KeyData -> KeyData +addKey key (KeyData count size unknownsize backends) = + KeyData count' size' unknownsize' backends' + where + {- All calculations strict to avoid thunks when repeatedly + - applied to many keys. -} + !count' = count + 1 + !backends' = M.insertWith' (+) (keyBackendName key) 1 backends + !size' = maybe size (+ size) ks + !unknownsize' = maybe (unknownsize + 1) (const unknownsize) ks + ks = keySize key + +showSizeKeys :: KeyData -> String +showSizeKeys d = total ++ missingnote + where + total = roughSize storageUnits False $ sizeKeys d + missingnote + | unknownSizeKeys d == 0 = "" + | otherwise = aside $ + "+ " ++ show (unknownSizeKeys d) ++ + " keys of unknown size" + +staleSize :: String -> (Git.Repo -> FilePath) -> Stat +staleSize label dirspec = go =<< lift (Command.Unused.staleKeys dirspec) + where + go [] = nostat + go keys = onsize =<< sum <$> keysizes keys + onsize 0 = nostat + onsize size = stat label $ + json (++ aside "clean up with git-annex unused") $ + return $ roughSize storageUnits False size + keysizes keys = map (fromIntegral . fileSize) <$> stats keys + stats keys = do + dir <- lift $ fromRepo dirspec + liftIO $ forM keys $ \k -> getFileStatus (dir keyFile k) + +aside :: String -> String +aside s = " (" ++ s ++ ")" + +multiLine :: [String] -> String +multiLine = concatMap (\l -> "\n\t" ++ l) diff --git a/Command/Sync.hs b/Command/Sync.hs new file mode 100644 index 0000000000..a6ae610f84 --- /dev/null +++ b/Command/Sync.hs @@ -0,0 +1,344 @@ +{- git-annex command + - + - Copyright 2011 Joachim Breitner + - Copyright 2011,2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Command.Sync where + +import Common.Annex +import Command +import qualified Remote +import qualified Annex +import qualified Annex.Branch +import qualified Annex.Queue +import Annex.Direct +import Annex.CatFile +import Annex.Link +import qualified Git.Command +import qualified Git.LsFiles as LsFiles +import qualified Git.Merge +import qualified Git.Branch +import qualified Git.Ref +import qualified Git +import Git.Types (BlobType(..)) +import qualified Types.Remote +import qualified Remote.Git +import Types.Key +import Config +import Annex.ReplaceFile + +import Data.Hash.MD5 + +def :: [Command] +def = [command "sync" (paramOptional (paramRepeating paramRemote)) + [seek] SectionCommon "synchronize local repository with remotes"] + +-- syncing involves several operations, any of which can independently fail +seek :: CommandSeek +seek rs = do + branch <- fromMaybe nobranch <$> inRepo Git.Branch.current + remotes <- syncRemotes rs + return $ concat + [ [ commit ] + , [ mergeLocal branch ] + , [ pullRemote remote branch | remote <- remotes ] + , [ mergeAnnex ] + , [ pushLocal branch ] + , [ pushRemote remote branch | remote <- remotes ] + ] + where + nobranch = error "no branch is checked out" + +syncBranch :: Git.Ref -> Git.Ref +syncBranch = Git.Ref.under "refs/heads/synced/" + +remoteBranch :: Remote -> Git.Ref -> Git.Ref +remoteBranch remote = Git.Ref.under $ "refs/remotes/" ++ Remote.name remote + +syncRemotes :: [String] -> Annex [Remote] +syncRemotes rs = ifM (Annex.getState Annex.fast) ( nub <$> pickfast , wanted ) + where + pickfast = (++) <$> listed <*> (good =<< fastest <$> available) + wanted + | null rs = good =<< concat . Remote.byCost <$> available + | otherwise = listed + listed = do + l <- catMaybes <$> mapM (Remote.byName . Just) rs + let s = filter Remote.specialRemote l + unless (null s) $ + error $ "cannot sync special remotes: " ++ + unwords (map Types.Remote.name s) + return l + available = filter (not . Remote.specialRemote) + . filter (remoteAnnexSync . Types.Remote.gitconfig) + <$> Remote.remoteList + good = filterM $ Remote.Git.repoAvail . Types.Remote.repo + fastest = fromMaybe [] . headMaybe . Remote.byCost + +commit :: CommandStart +commit = next $ next $ do + ifM isDirect + ( do + void $ stageDirect + runcommit [] + , runcommit [Param "-a"] + ) + where + runcommit ps = do + showStart "commit" "" + showOutput + Annex.Branch.commit "update" + -- Commit will fail when the tree is clean, so ignore failure. + let params = (Param "commit") : ps ++ + [Param "-m", Param "git-annex automatic sync"] + _ <- inRepo $ tryIO . Git.Command.runQuiet params + return True + +mergeLocal :: Git.Ref -> CommandStart +mergeLocal branch = go =<< needmerge + where + syncbranch = syncBranch branch + needmerge = do + unlessM (inRepo $ Git.Ref.exists syncbranch) $ + inRepo $ updateBranch syncbranch + inRepo $ Git.Branch.changed branch syncbranch + go False = stop + go True = do + showStart "merge" $ Git.Ref.describe syncbranch + next $ next $ mergeFrom syncbranch + +pushLocal :: Git.Ref -> CommandStart +pushLocal branch = do + inRepo $ updateBranch $ syncBranch branch + stop + +updateBranch :: Git.Ref -> Git.Repo -> IO () +updateBranch syncbranch g = + unlessM go $ error $ "failed to update " ++ show syncbranch + where + go = Git.Command.runBool + [ Param "branch" + , Param "-f" + , Param $ show $ Git.Ref.base syncbranch + ] g + +pullRemote :: Remote -> Git.Ref -> CommandStart +pullRemote remote branch = do + showStart "pull" (Remote.name remote) + next $ do + showOutput + stopUnless fetch $ + next $ mergeRemote remote (Just branch) + where + 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 + - were committed (or pushed changes, if this is a bare remote), + - while the synced/master may have changes that some + - other remote synced to this remote. So, merge them both. -} +mergeRemote :: Remote -> (Maybe Git.Ref) -> CommandCleanup +mergeRemote remote b = case b of + Nothing -> do + branch <- inRepo Git.Branch.currentUnsafe + all id <$> (mapM merge $ branchlist branch) + Just _ -> all id <$> (mapM merge =<< tomerge (branchlist b)) + where + merge = mergeFrom . remoteBranch remote + tomerge branches = filterM (changed remote) branches + branchlist Nothing = [] + branchlist (Just branch) = [branch, syncBranch branch] + +pushRemote :: Remote -> Git.Ref -> CommandStart +pushRemote remote branch = go =<< needpush + where + needpush = anyM (newer remote) [syncBranch branch, Annex.Branch.name] + go False = stop + go True = do + showStart "push" (Remote.name remote) + next $ next $ do + showOutput + inRepo $ pushBranch remote branch + +{- If the remote is a bare git repository, it's best to push the branch + - directly to it. On the other hand, if it's not bare, pushing to the + - checked out branch will fail, and this is why we use the syncBranch. + - + - Git offers no way to tell if a remote is bare or not, so both methods + - are tried. + - + - The direct push is likely to spew an ugly error message, so stderr is + - elided. Since progress is output to stderr too, the sync push is done + - first, and actually sends the data. Then the direct push is tried, + - with stderr discarded, to update the branch ref on the remote. + -} +pushBranch :: Remote -> Git.Ref -> Git.Repo -> IO Bool +pushBranch remote branch g = tryIO directpush `after` syncpush + where + syncpush = Git.Command.runBool (pushparams (refspec branch)) g + directpush = Git.Command.runQuiet (pushparams (show $ Git.Ref.base branch)) g + pushparams b = + [ Param "push" + , Param $ Remote.name remote + , Param $ refspec Annex.Branch.name + , Param b + ] + refspec b = concat + [ show $ Git.Ref.base b + , ":" + , show $ Git.Ref.base $ syncBranch b + ] + +mergeAnnex :: CommandStart +mergeAnnex = do + void $ Annex.Branch.forceUpdate + stop + +{- Merges from a branch into the current branch. -} +mergeFrom :: Git.Ref -> Annex Bool +mergeFrom branch = do + showOutput + ifM isDirect + ( maybe go godirect =<< inRepo Git.Branch.current + , go + ) + where + go = runmerge $ inRepo $ Git.Merge.mergeNonInteractive branch + godirect currbranch = do + old <- inRepo $ Git.Ref.sha currbranch + d <- fromRepo gitAnnexMergeDir + r <- runmerge $ inRepo $ mergeDirect d branch + new <- inRepo $ Git.Ref.sha currbranch + case (old, new) of + (Just oldsha, Just newsha) -> + mergeDirectCleanup d oldsha newsha + _ -> noop + return r + runmerge a = ifM (a) + ( return True + , resolveMerge + ) + +{- Resolves a conflicted merge. It's important that any conflicts be + - resolved in a way that itself avoids later merge conflicts, since + - multiple repositories may be doing this concurrently. + - + - Only annexed files are resolved; other files are left for the user to + - handle. + - + - This uses the Keys pointed to by the files to construct new + - filenames. So when both sides modified file foo, + - it will be deleted, and replaced with files foo.KEYA and foo.KEYB. + - + - On the other hand, when one side deleted foo, and the other modified it, + - it will be deleted, and the modified version stored as file + - foo.KEYA (or KEYB). + -} +resolveMerge :: Annex Bool +resolveMerge = do + top <- fromRepo Git.repoPath + (fs, cleanup) <- inRepo (LsFiles.unmerged [top]) + merged <- all id <$> mapM resolveMerge' fs + void $ liftIO cleanup + + (deleted, cleanup2) <- inRepo (LsFiles.deleted [top]) + unless (null deleted) $ + Annex.Queue.addCommand "rm" [Params "--quiet -f --"] deleted + void $ liftIO cleanup2 + + when merged $ do + Annex.Queue.flush + void $ inRepo $ Git.Command.runBool + [ Param "commit" + , Param "-m" + , Param "git-annex automatic merge conflict fix" + ] + return merged + +resolveMerge' :: LsFiles.Unmerged -> Annex Bool +resolveMerge' u + | issymlink LsFiles.valUs && issymlink LsFiles.valThem = + withKey LsFiles.valUs $ \keyUs -> + withKey LsFiles.valThem $ \keyThem -> do + ifM isDirect + ( maybe noop (\k -> removeDirect k file) keyUs + , liftIO $ nukeFile file + ) + Annex.Queue.addCommand "rm" [Params "--quiet -f --"] [file] + go keyUs keyThem + | otherwise = return False + where + go keyUs keyThem + | keyUs == keyThem = do + makelink keyUs + return True + | otherwise = do + makelink keyUs + makelink keyThem + return True + file = LsFiles.unmergedFile u + issymlink select = any (select (LsFiles.unmergedBlobType u) ==) + [Just SymlinkBlob, Nothing] + makelink (Just key) = do + let dest = mergeFile file key + l <- inRepo $ gitAnnexLink dest key + replaceFile dest $ makeAnnexLink l + stageSymlink dest =<< hashSymlink l + whenM (isDirect) $ + toDirect key dest + makelink _ = noop + withKey select a = do + let msha = select $ LsFiles.unmergedSha u + case msha of + Nothing -> a Nothing + Just sha -> do + key <- catKey sha + maybe (return False) (a . Just) key + +{- The filename to use when resolving a conflicted merge of a file, + - that points to a key. + - + - Something derived from the key needs to be included in the filename, + - but rather than exposing the whole key to the user, a very weak hash + - is used. There is a very real, although still unlikely, chance of + - conflicts using this hash. + - + - In the event that there is a conflict with the filename generated + - for some other key, that conflict will itself be handled by the + - conflicted merge resolution code. That case is detected, and the full + - key is used in the filename. + -} +mergeFile :: FilePath -> Key -> FilePath +mergeFile file key + | doubleconflict = go $ key2file key + | otherwise = go $ shortHash $ key2file key + where + varmarker = ".variant-" + doubleconflict = varmarker `isInfixOf` file + go v = takeDirectory file + dropExtension (takeFileName file) + ++ varmarker ++ v + ++ takeExtension file + +shortHash :: String -> String +shortHash = take 4 . md5s . md5FilePath + +changed :: Remote -> Git.Ref -> Annex Bool +changed remote b = do + let r = remoteBranch remote b + ifM (inRepo $ Git.Ref.exists r) + ( inRepo $ Git.Branch.changed b r + , return False + ) + +newer :: Remote -> Git.Ref -> Annex Bool +newer remote b = do + let r = remoteBranch remote b + ifM (inRepo $ Git.Ref.exists r) + ( inRepo $ Git.Branch.changed r b + , return True + ) diff --git a/Command/Test.hs b/Command/Test.hs new file mode 100644 index 0000000000..bf15dcf50f --- /dev/null +++ b/Command/Test.hs @@ -0,0 +1,24 @@ +{- git-annex command + - + - Copyright 2013 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Command.Test where + +import Command + +def :: [Command] +def = [ dontCheck repoExists $ + command "test" paramNothing seek SectionPlumbing + "run built-in test suite"] + +seek :: [CommandSeek] +seek = [withWords start] + +{- We don't actually run the test suite here because of a dependency loop. + - The main program notices when the command is test and runs it; this + - function is never run if that works. -} +start :: [String] -> CommandStart +start _ = error "Cannot specify any additional parameters when running test" diff --git a/Command/TransferInfo.hs b/Command/TransferInfo.hs new file mode 100644 index 0000000000..4bebdebcd9 --- /dev/null +++ b/Command/TransferInfo.hs @@ -0,0 +1,64 @@ +{- git-annex command + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Command.TransferInfo where + +import Common.Annex +import Command +import Annex.Content +import Logs.Transfer +import Types.Key +import qualified Fields +import Utility.Metered + +def :: [Command] +def = [noCommit $ command "transferinfo" paramKey seek SectionPlumbing + "updates sender on number of bytes of content received"] + +seek :: [CommandSeek] +seek = [withWords start] + +{- Security: + - + - The transfer info file contains the user-supplied key, but + - the built-in guards prevent slashes in it from showing up in the filename. + - It also contains the UUID of the remote. But slashes are also filtered + - out of that when generating the filename. + - + - Checks that the key being transferred is inAnnex, to prevent + - malicious spamming of bogus keys. Does not check that a transfer + - of the key is actually in progress, because this could be started + - concurrently with sendkey, and win the race. + -} +start :: [String] -> CommandStart +start (k:[]) = do + case (file2key k) of + Nothing -> error "bad key" + (Just key) -> whenM (inAnnex key) $ do + file <- Fields.getField Fields.associatedFile + u <- maybe (error "missing remoteuuid") toUUID + <$> Fields.getField Fields.remoteUUID + let t = Transfer + { transferDirection = Upload + , transferUUID = u + , transferKey = key + } + info <- liftIO $ startTransferInfo file + (update, tfile, _) <- mkProgressUpdater t info + liftIO $ mapM_ void + [ tryIO $ forever $ do + bytes <- readUpdate + maybe (error "transferinfo protocol error") + (update . toBytesProcessed) bytes + , tryIO $ removeFile tfile + , exitSuccess + ] + stop +start _ = error "wrong number of parameters" + +readUpdate :: IO (Maybe Integer) +readUpdate = readish <$> getLine diff --git a/Command/TransferKey.hs b/Command/TransferKey.hs new file mode 100644 index 0000000000..849cbc12b3 --- /dev/null +++ b/Command/TransferKey.hs @@ -0,0 +1,59 @@ +{- git-annex command, used internally by old versions of assistant; + - kept around for now so running daemons don't break when upgraded + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Command.TransferKey where + +import Common.Annex +import Command +import Annex.Content +import Logs.Location +import Logs.Transfer +import qualified Remote +import Types.Remote +import qualified Command.Move +import qualified Option + +def :: [Command] +def = [withOptions options $ + noCommit $ command "transferkey" paramKey seek SectionPlumbing + "transfers a key from or to a remote"] + +options :: [Option] +options = [fileOption, Command.Move.fromOption, Command.Move.toOption] + +fileOption :: Option +fileOption = Option.field [] "file" paramFile "the associated file" + +seek :: [CommandSeek] +seek = [withField Command.Move.toOption Remote.byNameWithUUID $ \to -> + withField Command.Move.fromOption Remote.byNameWithUUID $ \from -> + withField fileOption return $ \file -> + withKeys $ start to from file] + +start :: Maybe Remote -> Maybe Remote -> AssociatedFile -> Key -> CommandStart +start to from file key = + case (from, to) of + (Nothing, Just dest) -> next $ toPerform dest key file + (Just src, Nothing) -> next $ fromPerform src key file + _ -> error "specify either --from or --to" + +toPerform :: Remote -> Key -> AssociatedFile -> CommandPerform +toPerform remote key file = go $ + upload (uuid remote) key file forwardRetry $ \p -> do + ok <- Remote.storeKey remote key file p + when ok $ + Remote.logStatus remote key InfoPresent + return ok + +fromPerform :: Remote -> Key -> AssociatedFile -> CommandPerform +fromPerform remote key file = go $ + download (uuid remote) key file forwardRetry $ \p -> + getViaTmp key $ \t -> Remote.retrieveKeyFile remote key file t p + +go :: Annex Bool -> CommandPerform +go a = ifM a ( liftIO exitSuccess, liftIO exitFailure) diff --git a/Command/TransferKeys.hs b/Command/TransferKeys.hs new file mode 100644 index 0000000000..8da29e211e --- /dev/null +++ b/Command/TransferKeys.hs @@ -0,0 +1,142 @@ +{- git-annex command, used internally by assistant + - + - Copyright 2012, 2013 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE TypeSynonymInstances, FlexibleInstances #-} + +module Command.TransferKeys where + +import Common.Annex +import Command +import Annex.Content +import Logs.Location +import Logs.Transfer +import qualified Remote +import Types.Key +import qualified Option + +data TransferRequest = TransferRequest Direction Remote Key AssociatedFile + +def :: [Command] +def = [withOptions options $ + command "transferkeys" paramNothing seek + SectionPlumbing "transfers keys"] + +options :: [Option] +options = [readFdOption, writeFdOption] + +readFdOption :: Option +readFdOption = Option.field [] "readfd" paramNumber "read from this fd" + +writeFdOption :: Option +writeFdOption = Option.field [] "writefd" paramNumber "write to this fd" + +seek :: [CommandSeek] +seek = [withField readFdOption convertFd $ \readh -> + withField writeFdOption convertFd $ \writeh -> + withNothing $ start readh writeh] + +convertFd :: Maybe String -> Annex (Maybe Handle) +convertFd Nothing = return Nothing +convertFd (Just s) = liftIO $ do + case readish s of + Nothing -> error "bad fd" + Just fd -> Just <$> fdToHandle fd + +start :: Maybe Handle -> Maybe Handle -> CommandStart +start readh writeh = do + runRequests (fromMaybe stdin readh) (fromMaybe stdout writeh) runner + stop + where + runner (TransferRequest direction remote key file) + | direction == Upload = + upload (Remote.uuid remote) key file forwardRetry $ \p -> do + ok <- Remote.storeKey remote key file p + when ok $ + Remote.logStatus remote key InfoPresent + return ok + | otherwise = download (Remote.uuid remote) key file forwardRetry $ \p -> + getViaTmp key $ \t -> Remote.retrieveKeyFile remote key file t p + +runRequests + :: Handle + -> Handle + -> (TransferRequest -> Annex Bool) + -> Annex () +runRequests readh writeh a = do + liftIO $ do + hSetBuffering readh NoBuffering + fileEncoding readh + fileEncoding writeh + go =<< readrequests + where + go (d:u:k:f:rest) = do + case (deserialize d, deserialize u, deserialize k, deserialize f) of + (Just direction, Just uuid, Just key, Just file) -> do + mremote <- Remote.remoteFromUUID uuid + case mremote of + Nothing -> sendresult False + Just remote -> sendresult =<< a + (TransferRequest direction remote key file) + _ -> sendresult False + go rest + go [] = noop + go [""] = noop + go v = error $ "transferkeys protocol error: " ++ show v + + readrequests = liftIO $ split fieldSep <$> hGetContents readh + sendresult b = liftIO $ do + hPutStrLn writeh $ serialize b + hFlush writeh + +sendRequest :: Transfer -> AssociatedFile -> Handle -> IO () +sendRequest t f h = do + hPutStr h $ intercalate fieldSep + [ serialize (transferDirection t) + , serialize (transferUUID t) + , serialize (transferKey t) + , serialize f + , "" -- adds a trailing null + ] + hFlush h + +readResponse :: Handle -> IO Bool +readResponse h = fromMaybe False . deserialize <$> hGetLine h + +fieldSep :: String +fieldSep = "\0" + +class Serialized a where + serialize :: a -> String + deserialize :: String -> Maybe a + +instance Serialized Bool where + serialize True = "1" + serialize False = "0" + deserialize "1" = Just True + deserialize "0" = Just False + deserialize _ = Nothing + +instance Serialized Direction where + serialize Upload = "u" + serialize Download = "d" + deserialize "u" = Just Upload + deserialize "d" = Just Download + deserialize _ = Nothing + +instance Serialized AssociatedFile where + serialize (Just f) = f + serialize Nothing = "" + deserialize "" = Just Nothing + deserialize f = Just $ Just f + +instance Serialized UUID where + serialize = fromUUID + deserialize = Just . toUUID + +instance Serialized Key where + serialize = key2file + deserialize = file2key diff --git a/Command/Trust.hs b/Command/Trust.hs new file mode 100644 index 0000000000..26993ef771 --- /dev/null +++ b/Command/Trust.hs @@ -0,0 +1,32 @@ +{- git-annex command + - + - Copyright 2010 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Command.Trust where + +import Common.Annex +import Command +import qualified Remote +import Logs.Trust + +def :: [Command] +def = [command "trust" (paramRepeating paramRemote) seek + SectionSetup "trust a repository"] + +seek :: [CommandSeek] +seek = [withWords start] + +start :: [String] -> CommandStart +start ws = do + let name = unwords ws + showStart "trust" name + u <- Remote.nameToUUID name + next $ perform u + +perform :: UUID -> CommandPerform +perform uuid = do + trustSet uuid Trusted + next $ return True diff --git a/Command/Unannex.hs b/Command/Unannex.hs new file mode 100644 index 0000000000..fbeaffa52a --- /dev/null +++ b/Command/Unannex.hs @@ -0,0 +1,102 @@ +{- git-annex command + - + - Copyright 2010-2013 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE CPP #-} + +module Command.Unannex where + +import Common.Annex +import Command +import Config +import qualified Annex +import Logs.Location +import Annex.Content +import Annex.Content.Direct +import qualified Git.Command +import qualified Git.LsFiles as LsFiles + +def :: [Command] +def = [command "unannex" paramPaths seek SectionUtility + "undo accidential add command"] + +seek :: [CommandSeek] +seek = [withFilesInGit $ whenAnnexed start] + +start :: FilePath -> (Key, Backend) -> CommandStart +start file (key, _) = stopUnless (inAnnex key) $ do + showStart "unannex" file + next $ ifM isDirect + ( performDirect file key + , performIndirect file key) + +performIndirect :: FilePath -> Key -> CommandPerform +performIndirect file key = do + liftIO $ removeFile file + + -- git rm deletes empty directory without --cached + inRepo $ Git.Command.run [Params "rm --cached --force --quiet --", File file] + + -- If the file was already committed, it is now staged for removal. + -- Commit that removal now, to avoid later confusing the + -- pre-commit hook, if this file is later added back to + -- git as a normal non-annexed file, to thinking that the + -- file has been unlocked and needs to be re-annexed. + (s, reap) <- inRepo $ LsFiles.staged [file] + when (not $ null s) $ + inRepo $ Git.Command.run + [ Param "commit" + , Param "-q" + , Param "--no-verify" + , Param "-m", Param "content removed from git annex" + , Param "--", File file + ] + void $ liftIO reap + + next $ cleanupIndirect file key + +cleanupIndirect :: FilePath -> Key -> CommandCleanup +cleanupIndirect file key = do + ifM (Annex.getState Annex.fast) + ( goFast + , go + ) + return True + where +#ifdef mingw32_HOST_OS + goFast = go +#else + goFast = do + -- fast mode: hard link to content in annex + src <- calcRepo $ gitAnnexLocation key + -- creating a hard link could fall; fall back to non fast mode + ifM (liftIO $ catchBoolIO $ createLink src file >> return True) + ( thawContent file + , go + ) +#endif + go = do + fromAnnex key file + logStatus key InfoMissing + + +performDirect :: FilePath -> Key -> CommandPerform +performDirect file key = do + -- --force is needed when the file is not committed + inRepo $ Git.Command.run [Params "rm --cached --force --quiet --", File file] + next $ cleanupDirect file key + +{- The direct mode file is not touched during unannex, so the content + - is already where it needs to be, so this does not need to do anything + - except remove it from the associated file map (which also updates + - the location log if this was the last copy), and, if this was the last + - associated file, remove the inode cache. -} +cleanupDirect :: FilePath -> Key -> CommandCleanup +cleanupDirect file key = do + fs <- removeAssociatedFile key file + when (null fs) $ + removeInodeCache key + return True diff --git a/Command/Ungroup.hs b/Command/Ungroup.hs new file mode 100644 index 0000000000..a6557f21d3 --- /dev/null +++ b/Command/Ungroup.hs @@ -0,0 +1,35 @@ +{- git-annex command + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Command.Ungroup where + +import Common.Annex +import Command +import qualified Remote +import Logs.Group +import Types.Group + +import qualified Data.Set as S + +def :: [Command] +def = [command "ungroup" (paramPair paramRemote paramDesc) seek + SectionSetup "remove a repository from a group"] + +seek :: [CommandSeek] +seek = [withWords start] + +start :: [String] -> CommandStart +start (name:g:[]) = do + showStart "ungroup" name + u <- Remote.nameToUUID name + next $ perform u g +start _ = error "Specify a repository and a group." + +perform :: UUID -> Group -> CommandPerform +perform uuid g = do + groupChange uuid (S.delete g) + next $ return True diff --git a/Command/Uninit.hs b/Command/Uninit.hs new file mode 100644 index 0000000000..a40e283995 --- /dev/null +++ b/Command/Uninit.hs @@ -0,0 +1,109 @@ +{- git-annex command + - + - Copyright 2010 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Command.Uninit where + +import Common.Annex +import Command +import qualified Git +import qualified Git.Command +import qualified Annex +import qualified Command.Unannex +import Init +import qualified Annex.Branch +import Annex.Content + +def :: [Command] +def = [addCheck check $ command "uninit" paramPaths seek + SectionUtility "de-initialize git-annex and clean out repository"] + +check :: Annex () +check = do + b <- current_branch + when (b == Annex.Branch.name) $ error $ + "cannot uninit when the " ++ show b ++ " branch is checked out" + top <- fromRepo Git.repoPath + cwd <- liftIO getCurrentDirectory + whenM ((/=) <$> liftIO (absPath top) <*> liftIO (absPath cwd)) $ + error "can only run uninit from the top of the git repository" + where + current_branch = Git.Ref . Prelude.head . lines <$> revhead + revhead = inRepo $ Git.Command.pipeReadStrict + [Params "rev-parse --abbrev-ref HEAD"] + +seek :: [CommandSeek] +seek = + [ withFilesNotInGit $ whenAnnexed startCheckIncomplete + , withFilesInGit $ whenAnnexed startUnannex + , withNothing start + ] + +{- git annex symlinks that are not checked into git could be left by an + - interrupted add. -} +startCheckIncomplete :: FilePath -> (Key, Backend) -> CommandStart +startCheckIncomplete file _ = error $ unlines + [ file ++ " points to annexed content, but is not checked into git." + , "Perhaps this was left behind by an interrupted git annex add?" + , "Not continuing with uninit; either delete or git annex add the file and retry." + ] + +startUnannex :: FilePath -> (Key, Backend) -> CommandStart +startUnannex file info = do + -- Force fast mode before running unannex. This way, if multiple + -- files link to a key, it will be left in the annex and hardlinked + -- to by each. + Annex.changeState $ \s -> s { Annex.fast = True } + Command.Unannex.start file info + +start :: CommandStart +start = next $ next $ do + annexdir <- fromRepo gitAnnexDir + annexobjectdir <- fromRepo gitAnnexObjectDir + leftovers <- removeUnannexed =<< getKeysPresent + if null leftovers + then liftIO $ removeDirectoryRecursive annexdir + else error $ unlines + [ "Not fully uninitialized" + , "Some annexed data is still left in " ++ annexobjectdir + , "This may include deleted files, or old versions of modified files." + , "" + , "If you don't care about preserving the data, just delete the" + , "directory." + , "" + , "Or, you can move it to another location, in case it turns out" + , "something in there is important." + , "" + , "Or, you can run `git annex unused` followed by `git annex dropunused`" + , "to remove data that is not used by any tag or branch, which might" + , "take care of all the data." + , "" + , "Then run `git annex uninit` again to finish." + ] + uninitialize + -- avoid normal shutdown + saveState False + inRepo $ Git.Command.run + [Param "branch", Param "-D", Param $ show Annex.Branch.name] + liftIO exitSuccess + +{- Keys that were moved out of the annex have a hard link still in the + - annex, with > 1 link count, and those can be removed. + - + - Returns keys that cannot be removed. -} +removeUnannexed :: [Key] -> Annex [Key] +removeUnannexed = go [] + where + go c [] = return c + go c (k:ks) = ifM (inAnnexCheck k $ liftIO . enoughlinks) + ( do + removeAnnex k + go c ks + , go (k:c) ks + ) + enoughlinks f = catchBoolIO $ do + s <- getFileStatus f + return $ linkCount s > 1 diff --git a/Command/Unlock.hs b/Command/Unlock.hs new file mode 100644 index 0000000000..1eba26ff72 --- /dev/null +++ b/Command/Unlock.hs @@ -0,0 +1,50 @@ +{- git-annex command + - + - Copyright 2010 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Command.Unlock where + +import Common.Annex +import Command +import Annex.Content +import Utility.CopyFile + +def :: [Command] +def = + [ c "unlock" "unlock files for modification" + , c "edit" "same as unlock" + ] + where + c n = notDirect . command n paramPaths seek SectionCommon + +seek :: [CommandSeek] +seek = [withFilesInGit $ whenAnnexed start] + +{- The unlock subcommand replaces the symlink with a copy of the file's + - content. -} +start :: FilePath -> (Key, Backend) -> CommandStart +start file (key, _) = do + showStart "unlock" file + next $ perform file key + +perform :: FilePath -> Key -> CommandPerform +perform dest key = do + unlessM (inAnnex key) $ error "content not present" + unlessM (checkDiskSpace Nothing key 0) $ error "cannot unlock" + + src <- calcRepo $ gitAnnexLocation key + tmpdest <- fromRepo $ gitAnnexTmpLocation key + liftIO $ createDirectoryIfMissing True (parentDir tmpdest) + showAction "copying" + ifM (liftIO $ copyFileExternal src tmpdest) + ( do + liftIO $ do + removeFile dest + moveFile tmpdest dest + thawContent dest + next $ return True + , error "copy failed!" + ) diff --git a/Command/Untrust.hs b/Command/Untrust.hs new file mode 100644 index 0000000000..f18637838e --- /dev/null +++ b/Command/Untrust.hs @@ -0,0 +1,32 @@ +{- git-annex command + - + - Copyright 2010 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Command.Untrust where + +import Common.Annex +import Command +import qualified Remote +import Logs.Trust + +def :: [Command] +def = [command "untrust" (paramRepeating paramRemote) seek + SectionSetup "do not trust a repository"] + +seek :: [CommandSeek] +seek = [withWords start] + +start :: [String] -> CommandStart +start ws = do + let name = unwords ws + showStart "untrust" name + u <- Remote.nameToUUID name + next $ perform u + +perform :: UUID -> CommandPerform +perform uuid = do + trustSet uuid UnTrusted + next $ return True diff --git a/Command/Unused.hs b/Command/Unused.hs new file mode 100644 index 0000000000..0a060aae61 --- /dev/null +++ b/Command/Unused.hs @@ -0,0 +1,366 @@ +{- git-annex command + - + - Copyright 2010-2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE BangPatterns #-} + +module Command.Unused where + +import qualified Data.Set as S +import qualified Data.ByteString.Lazy as L +import Data.BloomFilter +import Data.BloomFilter.Easy +import Data.BloomFilter.Hash +import Control.Monad.ST +import qualified Data.Map as M + +import Common.Annex +import Command +import Logs.Unused +import Annex.Content +import Utility.FileMode +import Logs.Location +import Logs.Transfer +import qualified Annex +import qualified Git +import qualified Git.Command +import qualified Git.Ref +import qualified Git.LsFiles as LsFiles +import qualified Git.LsTree as LsTree +import qualified Backend +import qualified Remote +import qualified Annex.Branch +import qualified Option +import Annex.CatFile +import Types.Key + +def :: [Command] +def = [withOptions [fromOption] $ command "unused" paramNothing seek + SectionMaintenance "look for unused file content"] + +fromOption :: Option +fromOption = Option.field ['f'] "from" paramRemote "remote to check for unused content" + +seek :: [CommandSeek] +seek = [withNothing start] + +{- Finds unused content in the annex. -} +start :: CommandStart +start = do + from <- Annex.getField $ Option.name fromOption + let (name, action) = case from of + Nothing -> (".", checkUnused) + Just "." -> (".", checkUnused) + Just "here" -> (".", checkUnused) + Just n -> (n, checkRemoteUnused n) + showStart "unused" name + next action + +checkUnused :: CommandPerform +checkUnused = chain 0 + [ check "" unusedMsg $ findunused =<< Annex.getState Annex.fast + , check "bad" staleBadMsg $ staleKeysPrune gitAnnexBadDir False + , check "tmp" staleTmpMsg $ staleKeysPrune gitAnnexTmpDir True + ] + where + findunused True = do + showNote "fast mode enabled; only finding stale files" + return [] + findunused False = do + showAction "checking for unused data" + excludeReferenced =<< getKeysPresent + chain _ [] = next $ return True + chain v (a:as) = do + v' <- a v + chain v' as + +checkRemoteUnused :: String -> CommandPerform +checkRemoteUnused name = go =<< fromJust <$> Remote.byNameWithUUID (Just name) + where + go r = do + showAction "checking for unused data" + _ <- check "" (remoteUnusedMsg r) (remoteunused r) 0 + next $ return True + remoteunused r = excludeReferenced <=< loggedKeysFor $ Remote.uuid r + +check :: FilePath -> ([(Int, Key)] -> String) -> Annex [Key] -> Int -> Annex Int +check file msg a c = do + l <- a + let unusedlist = number c l + unless (null l) $ showLongNote $ msg unusedlist + writeUnusedLog file unusedlist + return $ c + length l + +number :: Int -> [a] -> [(Int, a)] +number _ [] = [] +number n (x:xs) = (n+1, x) : number (n+1) xs + +table :: [(Int, Key)] -> [String] +table l = " NUMBER KEY" : map cols l + where + cols (n,k) = " " ++ pad 6 (show n) ++ " " ++ key2file k + pad n s = s ++ replicate (n - length s) ' ' + +staleTmpMsg :: [(Int, Key)] -> String +staleTmpMsg t = unlines $ + ["Some partially transferred data exists in temporary files:"] + ++ table t ++ [dropMsg Nothing] + +staleBadMsg :: [(Int, Key)] -> String +staleBadMsg t = unlines $ + ["Some corrupted files have been preserved by fsck, just in case:"] + ++ table t ++ [dropMsg Nothing] + +unusedMsg :: [(Int, Key)] -> String +unusedMsg u = unusedMsg' u + ["Some annexed data is no longer used by any files:"] + [dropMsg Nothing] +unusedMsg' :: [(Int, Key)] -> [String] -> [String] -> String +unusedMsg' u header trailer = unlines $ + header ++ + table u ++ + ["(To see where data was previously used, try: git log --stat -S'KEY')"] ++ + trailer + +remoteUnusedMsg :: Remote -> [(Int, Key)] -> String +remoteUnusedMsg r u = unusedMsg' u + ["Some annexed data on " ++ name ++ " is not used by any files:"] + [dropMsg $ Just r] + where + name = Remote.name r + +dropMsg :: Maybe Remote -> String +dropMsg Nothing = dropMsg' "" +dropMsg (Just r) = dropMsg' $ " --from " ++ Remote.name r +dropMsg' :: String -> String +dropMsg' s = "\nTo remove unwanted data: git-annex dropunused" ++ s ++ " NUMBER\n" + +{- Finds keys in the list that are not referenced in the git repository. + - + - Strategy: + - + - * Build a bloom filter of all keys referenced by symlinks. This + - is the fastest one to build and will filter out most keys. + - * If keys remain, build a second bloom filter of keys referenced by + - all branches. + - * The list is streamed through these bloom filters lazily, so both will + - exist at the same time. This means that twice the memory is used, + - but they're relatively small, so the added complexity of using a + - mutable bloom filter does not seem worthwhile. + - * Generating the second bloom filter can take quite a while, since + - it needs enumerating all keys in all git branches. But, the common + - case, if the second filter is needed, is for some keys to be globally + - unused, and in that case, no short-circuit is possible. + - Short-circuiting if the first filter filters all the keys handles the + - other common case. + -} +excludeReferenced :: [Key] -> Annex [Key] +excludeReferenced ks = runfilter firstlevel ks >>= runfilter secondlevel + where + runfilter _ [] = return [] -- optimisation + runfilter a l = bloomFilter show l <$> genBloomFilter show a + firstlevel = withKeysReferencedM + secondlevel = withKeysReferencedInGit + +{- Finds items in the first, smaller list, that are not + - present in the second, larger list. + - + - Constructing a single set, of the list that tends to be + - smaller, appears more efficient in both memory and CPU + - than constructing and taking the S.difference of two sets. -} +exclude :: Ord a => [a] -> [a] -> [a] +exclude [] _ = [] -- optimisation +exclude smaller larger = S.toList $ remove larger $ S.fromList smaller + where + remove a b = foldl (flip S.delete) b a + +{- A bloom filter capable of holding half a million keys with a + - false positive rate of 1 in 1000 uses around 8 mb of memory, + - so will easily fit on even my lowest memory systems. + -} +bloomCapacity :: Annex Int +bloomCapacity = fromMaybe 500000 . annexBloomCapacity <$> Annex.getGitConfig +bloomAccuracy :: Annex Int +bloomAccuracy = fromMaybe 1000 . annexBloomAccuracy <$> Annex.getGitConfig +bloomBitsHashes :: Annex (Int, Int) +bloomBitsHashes = do + capacity <- bloomCapacity + accuracy <- bloomAccuracy + return $ suggestSizing capacity (1/ fromIntegral accuracy) + +{- Creates a bloom filter, and runs an action, such as withKeysReferenced, + - to populate it. + - + - The action is passed a callback that it can use to feed values into the + - bloom filter. + - + - Once the action completes, the mutable filter is frozen + - for later use. + -} +genBloomFilter :: Hashable t => (v -> t) -> ((v -> Annex ()) -> Annex b) -> Annex (Bloom t) +genBloomFilter convert populate = do + (numbits, numhashes) <- bloomBitsHashes + bloom <- lift $ newMB (cheapHashes numhashes) numbits + _ <- populate $ \v -> lift $ insertMB bloom (convert v) + lift $ unsafeFreezeMB bloom + where + lift = liftIO . stToIO + +bloomFilter :: Hashable t => (v -> t) -> [v] -> Bloom t -> [v] +bloomFilter convert l bloom = filter (\k -> convert k `notElemB` bloom) l + +{- 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' Nothing initial folda + where + 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' Nothing () calla + where + calla k _ _ = a k + +{- 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 = 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 f v + go v' fs + +withKeysReferencedInGit :: (Key -> Annex ()) -> Annex () +withKeysReferencedInGit a = do + rs <- relevantrefs <$> showref + forM_ rs (withKeysReferencedInGitRef a) + where + showref = inRepo $ Git.Command.pipeReadStrict [Param "show-ref"] + relevantrefs = map (Git.Ref . snd) . + nubBy uniqref . + filter ourbranches . + map (separate (== ' ')) . lines + uniqref (x, _) (y, _) = x == y + ourbranchend = '/' : show Annex.Branch.name + ourbranches (_, b) = not (ourbranchend `isSuffixOf` b) + && not ("refs/synced/" `isPrefixOf` b) + +withKeysReferencedInGitRef :: (Key -> Annex ()) -> Git.Ref -> Annex () +withKeysReferencedInGitRef a ref = do + showAction $ "checking " ++ Git.Ref.describe ref + go <=< inRepo $ LsTree.lsTree ref + where + go [] = noop + go (l:ls) + | isSymLink (LsTree.mode l) = do + content <- encodeW8 . L.unpack + <$> catFile ref (LsTree.file l) + case fileKey (takeFileName content) of + Nothing -> go ls + Just k -> do + a k + go ls + | otherwise = go ls + +{- Looks in the specified directory for bad/tmp keys, and returns a list + - of those that might still have value, or might be stale and removable. + - + - Also, stale keys that can be proven to have no value are deleted. + -} +staleKeysPrune :: (Git.Repo -> FilePath) -> Bool -> Annex [Key] +staleKeysPrune dirspec nottransferred = do + contents <- staleKeys dirspec + + dups <- filterM inAnnex contents + let stale = contents `exclude` dups + + dir <- fromRepo dirspec + liftIO $ forM_ dups $ \t -> removeFile $ dir keyFile t + + if nottransferred + then do + inprogress <- S.fromList . map (transferKey . fst) + <$> getTransfers + return $ filter (`S.notMember` inprogress) stale + else return stale + +staleKeys :: (Git.Repo -> FilePath) -> Annex [Key] +staleKeys dirspec = do + dir <- fromRepo dirspec + ifM (liftIO $ doesDirectoryExist dir) + ( do + contents <- liftIO $ getDirectoryContents dir + files <- liftIO $ filterM doesFileExist $ + map (dir ) contents + return $ mapMaybe (fileKey . takeFileName) files + , return [] + ) + +data UnusedMaps = UnusedMaps + { unusedMap :: UnusedMap + , unusedBadMap :: UnusedMap + , unusedTmpMap :: UnusedMap + } + +{- Read unused logs once, and pass the maps to each start action. -} +withUnusedMaps :: (UnusedMaps -> Int -> CommandStart) -> CommandSeek +withUnusedMaps a params = do + unused <- readUnusedLog "" + unusedbad <- readUnusedLog "bad" + unusedtmp <- readUnusedLog "tmp" + return $ map (a $ UnusedMaps unused unusedbad unusedtmp) $ + concatMap unusedSpec params + +unusedSpec :: String -> [Int] +unusedSpec spec + | "-" `isInfixOf` spec = range $ separate (== '-') 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. -} +startUnused :: String + -> (Key -> CommandPerform) + -> (Key -> CommandPerform) + -> (Key -> CommandPerform) + -> UnusedMaps -> Int -> CommandStart +startUnused message unused badunused tmpunused maps n = search + [ (unusedMap maps, unused) + , (unusedBadMap maps, badunused) + , (unusedTmpMap maps, tmpunused) + ] + where + search [] = error $ show n ++ " not valid (run git annex unused for list)" + search ((m, a):rest) = + case M.lookup n m of + Nothing -> search rest + Just key -> do + showStart message (show n) + next $ a key diff --git a/Command/Upgrade.hs b/Command/Upgrade.hs new file mode 100644 index 0000000000..88ca8622d3 --- /dev/null +++ b/Command/Upgrade.hs @@ -0,0 +1,28 @@ +{- git-annex command + - + - Copyright 2011 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Command.Upgrade where + +import Common.Annex +import Command +import Upgrade +import Annex.Version + +def :: [Command] +def = [dontCheck repoExists $ -- because an old version may not seem to exist + command "upgrade" paramNothing seek + SectionMaintenance "upgrade repository layout"] + +seek :: [CommandSeek] +seek = [withNothing start] + +start :: CommandStart +start = do + showStart "upgrade" "." + r <- upgrade + setVersion defaultVersion + next $ next $ return r diff --git a/Command/Version.hs b/Command/Version.hs new file mode 100644 index 0000000000..c8507cd5ac --- /dev/null +++ b/Command/Version.hs @@ -0,0 +1,37 @@ +{- git-annex command + - + - Copyright 2010 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Command.Version where + +import Common.Annex +import Command +import qualified Build.SysConfig as SysConfig +import Annex.Version +import BuildFlags + +def :: [Command] +def = [noCommit $ noRepo showPackageVersion $ dontCheck repoExists $ + command "version" paramNothing seek SectionQuery "show version info"] + +seek :: [CommandSeek] +seek = [withNothing start] + +start :: CommandStart +start = do + v <- getVersion + liftIO $ do + showPackageVersion + putStrLn $ "local repository version: " ++ fromMaybe "unknown" v + putStrLn $ "default repository version: " ++ defaultVersion + putStrLn $ "supported repository versions: " ++ unwords supportedVersions + putStrLn $ "upgrade supported from repository versions: " ++ unwords upgradableVersions + stop + +showPackageVersion :: IO () +showPackageVersion = do + putStrLn $ "git-annex version: " ++ SysConfig.packageversion + putStrLn $ "build flags: " ++ unwords buildFlags diff --git a/Command/Vicfg.hs b/Command/Vicfg.hs new file mode 100644 index 0000000000..1aa8722c58 --- /dev/null +++ b/Command/Vicfg.hs @@ -0,0 +1,194 @@ +{- git-annex command + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Command.Vicfg where + +import qualified Data.Map as M +import qualified Data.Set as S +import System.Environment (getEnv) +import Data.Tuple (swap) +import Data.Char (isSpace) + +import Common.Annex +import Command +import Annex.Perms +import Types.TrustLevel +import Types.Group +import Logs.Trust +import Logs.Group +import Logs.PreferredContent +import Types.StandardGroups +import Remote + +def :: [Command] +def = [command "vicfg" paramNothing seek + SectionSetup "edit git-annex's configuration"] + +seek :: [CommandSeek] +seek = [withNothing start] + +start :: CommandStart +start = do + f <- fromRepo gitAnnexTmpCfgFile + createAnnexDirectory $ parentDir f + cfg <- getCfg + descs <- uuidDescriptions + liftIO $ writeFile f $ genCfg cfg descs + vicfg cfg f + stop + +vicfg :: Cfg -> FilePath -> Annex () +vicfg curcfg f = do + vi <- liftIO $ catchDefaultIO "vi" $ getEnv "EDITOR" + -- Allow EDITOR to be processed by the shell, so it can contain options. + unlessM (liftIO $ boolSystem "sh" [Param "-c", Param $ unwords [vi, shellEscape f]]) $ + error $ vi ++ " exited nonzero; aborting" + r <- parseCfg curcfg <$> liftIO (readFileStrict f) + liftIO $ nukeFile f + case r of + Left s -> do + liftIO $ writeFile f s + vicfg curcfg f + Right newcfg -> setCfg curcfg newcfg + +data Cfg = Cfg + { cfgTrustMap :: TrustMap + , cfgGroupMap :: M.Map UUID (S.Set Group) + , cfgPreferredContentMap :: M.Map UUID String + } + +getCfg :: Annex Cfg +getCfg = Cfg + <$> trustMapRaw -- without local trust overrides + <*> (groupsByUUID <$> groupMap) + <*> preferredContentMapRaw + +setCfg :: Cfg -> Cfg -> Annex () +setCfg curcfg newcfg = do + let (trustchanges, groupchanges, preferredcontentchanges) = diffCfg curcfg newcfg + mapM_ (uncurry trustSet) $ M.toList trustchanges + mapM_ (uncurry groupSet) $ M.toList groupchanges + mapM_ (uncurry preferredContentSet) $ M.toList preferredcontentchanges + +diffCfg :: Cfg -> Cfg -> (TrustMap, M.Map UUID (S.Set Group), M.Map UUID String) +diffCfg curcfg newcfg = (diff cfgTrustMap, diff cfgGroupMap, diff cfgPreferredContentMap) + where + diff f = M.differenceWith (\x y -> if x == y then Nothing else Just x) + (f newcfg) (f curcfg) + +genCfg :: Cfg -> M.Map UUID String -> String +genCfg cfg descs = unlines $ concat [intro, trust, groups, preferredcontent] + where + intro = + [ com "git-annex configuration" + , com "" + , com "Changes saved to this file will be recorded in the git-annex branch." + , com "" + , com "Lines in this file have the format:" + , com " setting uuid = value" + ] + + trust = settings cfgTrustMap + [ "" + , com "Repository trust configuration" + , com "(Valid trust levels: " ++ trustlevels ++ ")" + ] + (\(t, u) -> line "trust" u $ showTrustLevel t) + (\u -> lcom $ line "trust" u $ showTrustLevel SemiTrusted) + where + trustlevels = unwords $ map showTrustLevel [Trusted .. DeadTrusted] + + groups = settings cfgGroupMap + [ "" + , com "Repository groups" + , com $ "(Standard groups: " ++ grouplist ++ ")" + , com "(Separate group names with spaces)" + ] + (\(s, u) -> line "group" u $ unwords $ S.toList s) + (\u -> lcom $ line "group" u "") + where + grouplist = unwords $ map fromStandardGroup [minBound..] + + preferredcontent = settings cfgPreferredContentMap + [ "" + , com "Repository preferred contents" + ] + (\(s, u) -> line "content" u s) + (\u -> line "content" u "") + + settings field desc showvals showdefaults = concat + [ desc + , concatMap showvals $ sort $ map swap $ M.toList $ field cfg + , concatMap (\u -> lcom $ showdefaults u) $ missing field + ] + + line setting u value = + [ com $ "(for " ++ (fromMaybe "" $ M.lookup u descs) ++ ")" + , unwords [setting, fromUUID u, "=", value] + ] + lcom = map (\l -> if "#" `isPrefixOf` l then l else "#" ++ l) + missing field = S.toList $ M.keysSet descs `S.difference` M.keysSet (field cfg) + +{- If there's a parse error, returns a new version of the file, + - with the problem lines noted. -} +parseCfg :: Cfg -> String -> Either String Cfg +parseCfg curcfg = go [] curcfg . lines + where + go c cfg [] + | null (catMaybes $ map fst c) = Right cfg + | otherwise = Left $ unlines $ + badheader ++ concatMap showerr (reverse c) + go c cfg (l:ls) = case parse (dropWhile isSpace l) cfg of + Left msg -> go ((Just msg, l):c) cfg ls + Right cfg' -> go ((Nothing, l):c) cfg' ls + + parse l cfg + | null l = Right cfg + | "#" `isPrefixOf` l = Right cfg + | null setting || null u = Left "missing repository uuid" + | otherwise = handle cfg (toUUID u) setting value' + where + (setting, rest) = separate isSpace l + (r, value) = separate (== '=') rest + value' = trimspace value + u = reverse $ trimspace $ reverse $ trimspace r + trimspace = dropWhile isSpace + + handle cfg u setting value + | setting == "trust" = case readTrustLevel value of + Nothing -> badval "trust value" value + Just t -> + let m = M.insert u t (cfgTrustMap cfg) + in Right $ cfg { cfgTrustMap = m } + | setting == "group" = + let m = M.insert u (S.fromList $ words value) (cfgGroupMap cfg) + in Right $ cfg { cfgGroupMap = m } + | setting == "content" = + case checkPreferredContentExpression value of + Just e -> Left e + Nothing -> + let m = M.insert u value (cfgPreferredContentMap cfg) + in Right $ cfg { cfgPreferredContentMap = m } + | otherwise = badval "setting" setting + + showerr (Just msg, l) = [parseerr ++ msg, l] + showerr (Nothing, l) + -- filter out the header and parse error lines + -- from any previous parse failure + | any (`isPrefixOf` l) (parseerr:badheader) = [] + | otherwise = [l] + + badval desc val = Left $ "unknown " ++ desc ++ " \"" ++ val ++ "\"" + badheader = + [ com "There was a problem parsing your input." + , com "Search for \"Parse error\" to find the bad lines." + , com "Either fix the bad lines, or delete them (to discard your changes)." + ] + parseerr = com "Parse error in next line: " + +com :: String -> String +com s = "# " ++ s diff --git a/Command/Watch.hs b/Command/Watch.hs new file mode 100644 index 0000000000..c5fd1a8cd1 --- /dev/null +++ b/Command/Watch.hs @@ -0,0 +1,35 @@ +{- git-annex watch command + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Command.Watch where + +import Common.Annex +import Assistant +import Command +import Option + +def :: [Command] +def = [notBareRepo $ withOptions [foregroundOption, stopOption] $ + command "watch" paramNothing seek SectionCommon "watch for changes"] + +seek :: [CommandSeek] +seek = [withFlag stopOption $ \stopdaemon -> + withFlag foregroundOption $ \foreground -> + withNothing $ start False foreground stopdaemon] + +foregroundOption :: Option +foregroundOption = Option.flag [] "foreground" "do not daemonize" + +stopOption :: Option +stopOption = Option.flag [] "stop" "stop daemon" + +start :: Bool -> Bool -> Bool -> CommandStart +start assistant foreground stopdaemon = do + if stopdaemon + then stopDaemon + else startDaemon assistant foreground Nothing Nothing -- does not return + stop diff --git a/Command/WebApp.hs b/Command/WebApp.hs new file mode 100644 index 0000000000..eeb23a164d --- /dev/null +++ b/Command/WebApp.hs @@ -0,0 +1,220 @@ +{- git-annex webapp launcher + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE CPP #-} + +module Command.WebApp where + +import Common.Annex +import Command +import Assistant +import Assistant.Common +import Assistant.NamedThread +import Assistant.Threads.WebApp +import Assistant.WebApp +import Assistant.Install +import Annex.Environment +import Utility.WebApp +import Utility.Daemon (checkDaemon) +#ifdef __ANDROID__ +import Utility.Env +#endif +import Init +import qualified Git +import qualified Git.Config +import qualified Git.CurrentRepo +import qualified Annex +import Config.Files +import qualified Option + +import Control.Concurrent +import Control.Concurrent.STM +import System.Process (env, std_out, std_err) +import Network.Socket (HostName) +import System.Environment (getArgs) + +def :: [Command] +def = [ withOptions [listenOption] $ + noCommit $ noRepo startNoRepo $ dontCheck repoExists $ notBareRepo $ + command "webapp" paramNothing seek SectionCommon "launch webapp"] + +listenOption :: Option +listenOption = Option.field [] "listen" paramAddress + "accept connections to this address" + +seek :: [CommandSeek] +seek = [withField listenOption return $ \listenhost -> + withNothing $ start listenhost] + +start :: Maybe HostName -> CommandStart +start = start' True + +start' :: Bool -> Maybe HostName -> CommandStart +start' allowauto listenhost = do + liftIO $ ensureInstalled + ifM isInitialized ( go , auto ) + stop + where + go = do + browser <- fromRepo webBrowser + f <- liftIO . absPath =<< fromRepo gitAnnexHtmlShim + ifM (checkpid <&&> checkshim f) + ( if isJust listenhost + then error "The assistant is already running, so --listen cannot be used." + else do + url <- liftIO . readFile + =<< fromRepo gitAnnexUrlFile + liftIO $ openBrowser browser f url Nothing Nothing + , startDaemon True True listenhost $ Just $ + \origout origerr url htmlshim -> + if isJust listenhost + then maybe noop (`hPutStrLn` url) origout + else openBrowser browser htmlshim url origout origerr + ) + auto + | allowauto = liftIO startNoRepo + | otherwise = do + d <- liftIO getCurrentDirectory + error $ "no git repository in " ++ d + checkpid = do + pidfile <- fromRepo gitAnnexPidFile + liftIO $ isJust <$> checkDaemon pidfile + checkshim f = liftIO $ doesFileExist f + +{- 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 + -- FIXME should be able to reuse regular getopt, but + -- it currently runs in the Annex monad. + args <- getArgs + let listenhost = headMaybe $ map (snd . separate (== '=')) $ + filter ("--listen=" `isPrefixOf`) args + + dirs <- liftIO $ filterM doesDirectoryExist =<< readAutoStartFile + case dirs of + [] -> firstRun listenhost + (d:_) -> do + setCurrentDirectory d + state <- Annex.new =<< Git.CurrentRepo.get + void $ Annex.eval state $ doCommand $ + start' False listenhost + +{- Run the webapp without a repository, which prompts the user, makes one, + - changes to it, starts the regular assistant, and redirects the + - browser to its url. + - + - This is a very tricky dance -- The first webapp calls the signaler, + - which signals the main thread when it's ok to continue by writing to a + - MVar. The main thread starts the second webapp, and uses its callback + - to write its url back to the MVar, from where the signaler retrieves it, + - returning it to the first webapp, which does the redirect. + - + - Note that it's important that mainthread never terminates! Much + - of this complication is due to needing to keep the mainthread running. + -} +firstRun :: Maybe HostName -> IO () +firstRun listenhost = do + checkEnvironmentIO + {- Without a repository, we cannot have an Annex monad, so cannot + - get a ThreadState. Using undefined is only safe because the + - webapp checks its noAnnex field before accessing the + - threadstate. -} + let st = undefined + {- Get a DaemonStatus without running in the Annex monad. -} + dstatus <- atomically . newTMVar =<< newDaemonStatus + d <- newAssistantData st dstatus + urlrenderer <- newUrlRenderer + v <- newEmptyMVar + let callback a = Just $ a v + runAssistant d $ do + startNamedThread urlrenderer $ + webAppThread d urlrenderer True listenhost + (callback signaler) + (callback mainthread) + waitNamedThreads + where + signaler v = do + putMVar v "" + takeMVar v + mainthread v url htmlshim + | isJust listenhost = do + putStrLn url + hFlush stdout + go + | otherwise = do + browser <- maybe Nothing webBrowser <$> Git.Config.global + openBrowser browser htmlshim url Nothing Nothing + go + where + go = do + _wait <- takeMVar v + state <- Annex.new =<< Git.CurrentRepo.get + Annex.eval state $ + startDaemon True True listenhost $ Just $ + sendurlback v + sendurlback v _origout _origerr url _htmlshim = do + recordUrl url + putMVar v url + +recordUrl :: String -> IO () +#ifdef __ANDROID__ +{- The Android app has a menu item that opens the url recorded + - in this file. -} +recordUrl url = writeFile "/sdcard/git-annex.home/.git-annex-url" url +#else +recordUrl _ = noop +#endif + +openBrowser :: Maybe FilePath -> FilePath -> String -> Maybe Handle -> Maybe Handle -> IO () +#ifndef __ANDROID__ +openBrowser mcmd htmlshim _realurl outh errh = runbrowser +#else +openBrowser mcmd htmlshim realurl outh errh = do + recordUrl url + {- Android's `am` command does not work reliably across the + - wide range of Android devices. Intead, FIFO should be set to + - the filename of a fifo that we can write the URL to. -} + v <- getEnv "FIFO" + case v of + Nothing -> runbrowser + Just f -> void $ forkIO $ do + fd <- openFd f WriteOnly Nothing defaultFileFlags + void $ fdWrite fd url + closeFd fd +#endif + where + p = case mcmd of + Just cmd -> proc cmd [htmlshim] + Nothing -> browserProc url +#ifdef __ANDROID__ + {- Android does not support file:// urls, but neither is + - the security of the url in the process table important + - there, so just use the real url. -} + url = realurl +#else + url = fileUrl htmlshim +#endif + runbrowser = do + hPutStrLn (fromMaybe stdout outh) $ "Launching web browser on " ++ url + hFlush stdout + environ <- cleanEnvironment + (_, _, _, pid) <- createProcess p + { env = environ + , std_out = maybe Inherit UseHandle outh + , std_err = maybe Inherit UseHandle errh + } + exitcode <- waitForProcess pid + unless (exitcode == ExitSuccess) $ do + hPutStrLn (fromMaybe stderr errh) "failed to start web browser" + +{- web.browser is a generic git config setting for a web browser program -} +webBrowser :: Git.Repo -> Maybe FilePath +webBrowser = Git.Config.getMaybe "web.browser" + +fileUrl :: FilePath -> String +fileUrl file = "file://" ++ file diff --git a/Command/Whereis.hs b/Command/Whereis.hs new file mode 100644 index 0000000000..7086bf645e --- /dev/null +++ b/Command/Whereis.hs @@ -0,0 +1,54 @@ +{- git-annex command + - + - Copyright 2010 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Command.Whereis where + +import qualified Data.Map as M + +import Common.Annex +import Command +import Remote +import Logs.Trust + +def :: [Command] +def = [noCommit $ command "whereis" paramPaths seek + SectionQuery "lists repositories that have file content"] + +seek :: [CommandSeek] +seek = [withValue (remoteMap id) $ \m -> + withFilesInGit $ whenAnnexed $ start m] + +start :: M.Map UUID Remote -> FilePath -> (Key, Backend) -> CommandStart +start remotemap file (key, _) = do + showStart "whereis" file + next $ perform remotemap key + +perform :: M.Map UUID Remote -> Key -> CommandPerform +perform remotemap key = do + locations <- keyLocations key + (untrustedlocations, safelocations) <- trustPartition UnTrusted locations + let num = length safelocations + showNote $ show num ++ " " ++ copiesplural num + pp <- prettyPrintUUIDs "whereis" safelocations + unless (null safelocations) $ showLongNote pp + pp' <- prettyPrintUUIDs "untrusted" untrustedlocations + unless (null untrustedlocations) $ showLongNote $ untrustedheader ++ pp' + forM_ (mapMaybe (`M.lookup` remotemap) locations) $ + performRemote key + if null safelocations then stop else next $ return True + where + copiesplural 1 = "copy" + copiesplural _ = "copies" + untrustedheader = "The following untrusted locations may also have copies:\n" + +performRemote :: Key -> Remote -> Annex () +performRemote key remote = maybe noop go $ whereisKey remote + where + go a = do + ls <- a key + unless (null ls) $ showLongNote $ unlines $ + map (\l -> name remote ++ ": " ++ l) ls diff --git a/Command/XMPPGit.hs b/Command/XMPPGit.hs new file mode 100644 index 0000000000..c1ff0b1087 --- /dev/null +++ b/Command/XMPPGit.hs @@ -0,0 +1,43 @@ +{- git-annex command + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Command.XMPPGit where + +import Common.Annex +import Command +import Assistant.XMPP.Git + +def :: [Command] +def = [noCommit $ noRepo xmppGitRelay $ dontCheck repoExists $ + command "xmppgit" paramNothing seek + SectionPlumbing "git to XMPP relay"] + +seek :: [CommandSeek] +seek = [withWords start] + +start :: [String] -> CommandStart +start _ = do + liftIO gitRemoteHelper + liftIO xmppGitRelay + stop + +{- A basic implementation of the git-remote-helpers protocol. -} +gitRemoteHelper :: IO () +gitRemoteHelper = do + expect "capabilities" + respond ["connect"] + expect "connect git-receive-pack" + respond [] + where + expect s = do + cmd <- getLine + unless (cmd == s) $ + error $ "git-remote-helpers protocol error: expected: " ++ s ++ ", but got: " ++ cmd + respond l = do + mapM_ putStrLn l + putStrLn "" + hFlush stdout diff --git a/Common.hs b/Common.hs new file mode 100644 index 0000000000..5dc3cfbb23 --- /dev/null +++ b/Common.hs @@ -0,0 +1,34 @@ +{-# LANGUAGE PackageImports, CPP #-} + +module Common (module X) where + +import Control.Monad as X +import Control.Monad.IfElse as X +import Control.Applicative as X +import "mtl" Control.Monad.State.Strict as X (liftIO) +import Control.Exception.Extensible as X (IOException) + +import Data.Maybe as X +import Data.List as X hiding (head, tail, init, last) +import Data.String.Utils as X hiding (join) + +import System.FilePath as X +import System.Directory as X +import System.IO as X hiding (FilePath) +import System.PosixCompat.Files as X +#ifndef mingw32_HOST_OS +import System.Posix.IO as X +#endif +import System.Exit as X + +import Utility.Misc as X +import Utility.Exception as X +import Utility.SafeCommand as X +import Utility.Process as X +import Utility.Path as X +import Utility.Directory as X +import Utility.Monad as X +import Utility.Applicative as X +import Utility.FileSystemEncoding as X + +import Utility.PartialPrelude as X diff --git a/Common/Annex.hs b/Common/Annex.hs new file mode 100644 index 0000000000..3b8bcdbdd7 --- /dev/null +++ b/Common/Annex.hs @@ -0,0 +1,8 @@ +module Common.Annex (module X) where + +import Common as X +import Types as X +import Types.UUID as X (toUUID, fromUUID) +import Annex as X (gitRepo, inRepo, fromRepo, calcRepo) +import Locations as X +import Messages as X diff --git a/Config.hs b/Config.hs new file mode 100644 index 0000000000..4d93a2af51 --- /dev/null +++ b/Config.hs @@ -0,0 +1,84 @@ +{- Git configuration + - + - Copyright 2011-2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Config where + +import Common.Annex +import qualified Git +import qualified Git.Config +import qualified Git.Command +import qualified Annex +import qualified Types.Remote as Remote +import Config.Cost + +type UnqualifiedConfigKey = String +data ConfigKey = ConfigKey String + +{- Looks up a setting in git config. -} +getConfig :: ConfigKey -> String -> Annex String +getConfig (ConfigKey key) def = fromRepo $ Git.Config.get key def + +{- 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 [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 + [Param "config", Param "--unset", Param key] + +{- A per-remote config setting in git config. -} +remoteConfig :: Git.Repo -> UnqualifiedConfigKey -> ConfigKey +remoteConfig r key = ConfigKey $ + "remote." ++ fromMaybe "" (Git.remoteName r) ++ ".annex-" ++ key + +{- A global annex setting in git config. -} +annexConfig :: UnqualifiedConfigKey -> ConfigKey +annexConfig key = ConfigKey $ "annex." ++ key + +{- Calculates cost for a remote. Either the specific default, or as configured + - by remote..annex-cost, or if remote..annex-cost-command + - is set and prints a number, that is used. -} +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 + +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 +getNumCopies Nothing = annexNumCopies <$> Annex.getGitConfig + +isDirect :: Annex Bool +isDirect = annexDirect <$> Annex.getGitConfig + +setDirect :: Bool -> Annex () +setDirect b = do + setConfig (annexConfig "direct") (Git.Config.boolConfig b) + Annex.changeGitConfig $ \c -> c { annexDirect = b } + +crippledFileSystem :: Annex Bool +crippledFileSystem = annexCrippledFileSystem <$> Annex.getGitConfig + +setCrippledFileSystem :: Bool -> Annex () +setCrippledFileSystem b = do + setConfig (annexConfig "crippledfilesystem") (Git.Config.boolConfig b) + Annex.changeGitConfig $ \c -> c { annexCrippledFileSystem = b } + +{- Gets the http headers to use. -} +getHttpHeaders :: Annex [String] +getHttpHeaders = do + v <- annexHttpHeadersCommand <$> Annex.getGitConfig + case v of + Just cmd -> lines <$> liftIO (readProcess "sh" ["-c", cmd]) + Nothing -> annexHttpHeaders <$> Annex.getGitConfig diff --git a/Config/Cost.hs b/Config/Cost.hs new file mode 100644 index 0000000000..dc391a5a57 --- /dev/null +++ b/Config/Cost.hs @@ -0,0 +1,82 @@ +{- Remote costs. + - + - Copyright 2011-2013 Joey Hess + - + - 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 [] _ = [] +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/Config/Files.hs b/Config/Files.hs new file mode 100644 index 0000000000..3db2bb74c3 --- /dev/null +++ b/Config/Files.hs @@ -0,0 +1,69 @@ +{- git-annex extra config files + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Config.Files where + +import Common +import Utility.Tmp +import Utility.FreeDesktop + +{- ~/.config/git-annex/file -} +userConfigFile :: FilePath -> IO FilePath +userConfigFile file = do + dir <- userConfigDir + return $ dir "git-annex" file + +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 . map dropTrailingPathSeparator . lines + <$> catchDefaultIO "" (readFile f) + +modifyAutoStartFile :: ([FilePath] -> [FilePath]) -> IO () +modifyAutoStartFile func = do + dirs <- readAutoStartFile + let dirs' = nubBy equalFilePath $ func dirs + when (dirs' /= dirs) $ do + f <- autoStartFile + createDirectoryIfMissing True (parentDir f) + viaTmp writeFile f $ unlines $ dirs' + +{- Adds a directory to the autostart file. If the directory is already + - present, it's moved to the top, so it will be used as the default + - when opening the webapp. -} +addAutoStartFile :: FilePath -> IO () +addAutoStartFile path = modifyAutoStartFile $ (:) path + +{- Removes a directory from the autostart file. -} +removeAutoStartFile :: FilePath -> IO () +removeAutoStartFile path = modifyAutoStartFile $ + filter (not . equalFilePath path) + +{- The path to git-annex is written here; which is useful when cabal + - has installed it to some awful non-PATH location. -} +programFile :: IO FilePath +programFile = userConfigFile "program" + +{- Returns a command to run for git-annex. -} +readProgramFile :: IO FilePath +readProgramFile = do + programfile <- programFile + p <- catchDefaultIO cmd $ + fromMaybe cmd . headMaybe . lines <$> readFile programfile + ifM (inPath p) + ( return p + , ifM (inPath cmd) + ( return cmd + , error $ "cannot find git-annex program in PATH or in the location listed in " ++ programfile + ) + ) + where + cmd = "git-annex" diff --git a/Creds.hs b/Creds.hs new file mode 100644 index 0000000000..7791ce85df --- /dev/null +++ b/Creds.hs @@ -0,0 +1,151 @@ +{- Credentials storage + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE CPP #-} + +module Creds where + +import Common.Annex +import Annex.Perms +import Utility.FileMode +import Crypto +import Types.Remote (RemoteConfig, RemoteConfigKey) +import Remote.Helper.Encryptable (remoteCipher, embedCreds) +#ifndef mingw32_HOST_OS +import Utility.Env (setEnv) +#endif + +import System.Environment +import qualified Data.ByteString.Lazy.Char8 as L +import qualified Data.Map as M +import Utility.Base64 + +type Creds = String -- can be any data +type CredPair = (String, String) -- login, password + +{- A CredPair can be stored in a file, or in the environment, or perhaps + - in a remote's configuration. -} +data CredPairStorage = CredPairStorage + { credPairFile :: FilePath + , credPairEnvironment :: (String, String) + , credPairRemoteKey :: Maybe RemoteConfigKey + } + +{- Stores creds in a remote's configuration, if the remote allows + - that. Otherwise, caches them locally. -} +setRemoteCredPair :: RemoteConfig -> CredPairStorage -> Annex RemoteConfig +setRemoteCredPair c storage = go =<< getRemoteCredPair c storage + where + go (Just creds) + | embedCreds c = case credPairRemoteKey storage of + Nothing -> localcache creds + Just key -> storeconfig creds key =<< remoteCipher c + | otherwise = localcache creds + go Nothing = return c + + localcache creds = do + writeCacheCredPair creds storage + return c + + storeconfig creds key (Just cipher) = do + s <- liftIO $ encrypt (GpgOpts []) cipher + (feedBytes $ L.pack $ encodeCredPair creds) + (readBytes $ return . L.unpack) + return $ M.insert key (toB64 s) c + storeconfig creds key Nothing = + return $ M.insert key (toB64 $ encodeCredPair creds) c + +{- Gets a remote's credpair, from the environment if set, otherwise + - from the cache in gitAnnexCredsDir, or failing that, from the + - value in RemoteConfig. -} +getRemoteCredPairFor :: String -> RemoteConfig -> CredPairStorage -> Annex (Maybe CredPair) +getRemoteCredPairFor this c storage = maybe missing (return . Just) =<< getRemoteCredPair c storage + where + (loginvar, passwordvar) = credPairEnvironment storage + missing = do + warning $ unwords + [ "Set both", loginvar + , "and", passwordvar + , "to use", this + ] + return Nothing + +getRemoteCredPair :: RemoteConfig -> CredPairStorage -> Annex (Maybe CredPair) +getRemoteCredPair c storage = maybe fromcache (return . Just) =<< fromenv + where + fromenv = liftIO $ getEnvCredPair storage + fromcache = maybe fromconfig (return . Just) =<< readCacheCredPair storage + fromconfig = case credPairRemoteKey storage of + Just key -> do + mcipher <- remoteCipher c + case (M.lookup key c, mcipher) of + (Nothing, _) -> return Nothing + (Just enccreds, Just cipher) -> do + creds <- liftIO $ decrypt cipher + (feedBytes $ L.pack $ fromB64 enccreds) + (readBytes $ return . L.unpack) + fromcreds creds + (Just bcreds, Nothing) -> + fromcreds $ fromB64 bcreds + Nothing -> return Nothing + fromcreds creds = case decodeCredPair creds of + Just credpair -> do + writeCacheCredPair credpair storage + return $ Just credpair + _ -> error "bad creds" + +{- Gets a CredPair from the environment. -} +getEnvCredPair :: CredPairStorage -> IO (Maybe CredPair) +getEnvCredPair storage = liftM2 (,) + <$> get uenv + <*> get penv + where + (uenv, penv) = credPairEnvironment storage + get = catchMaybeIO . getEnv + +{- Stores a CredPair in the environment. -} +setEnvCredPair :: CredPair -> CredPairStorage -> IO () +#ifndef mingw32_HOST_OS +setEnvCredPair (l, p) storage = do + set uenv l + set penv p + where + (uenv, penv) = credPairEnvironment storage + set var val = void $ setEnv var val True +#else +setEnvCredPair _ _ = error "setEnvCredPair TODO" +#endif + +writeCacheCredPair :: CredPair -> CredPairStorage -> Annex () +writeCacheCredPair credpair storage = + writeCacheCreds (encodeCredPair credpair) (credPairFile storage) + +{- Stores the creds in a file inside gitAnnexCredsDir that only the user + - can read. -} +writeCacheCreds :: Creds -> FilePath -> Annex () +writeCacheCreds creds file = do + d <- fromRepo gitAnnexCredsDir + createAnnexDirectory d + liftIO $ writeFileProtected (d file) creds + +readCacheCredPair :: CredPairStorage -> Annex (Maybe CredPair) +readCacheCredPair storage = maybe Nothing decodeCredPair + <$> readCacheCreds (credPairFile storage) + +readCacheCreds :: FilePath -> Annex (Maybe Creds) +readCacheCreds file = do + d <- fromRepo gitAnnexCredsDir + let f = d file + liftIO $ catchMaybeIO $ readFile f + +encodeCredPair :: CredPair -> Creds +encodeCredPair (l, p) = unlines [l, p] + +decodeCredPair :: Creds -> Maybe CredPair +decodeCredPair creds = case lines creds of + l:p:[] -> Just (l, p) + _ -> Nothing diff --git a/Crypto.hs b/Crypto.hs new file mode 100644 index 0000000000..21b1ae41b5 --- /dev/null +++ b/Crypto.hs @@ -0,0 +1,163 @@ +{- git-annex crypto + - + - Currently using gpg; could later be modified to support different + - crypto backends if neccessary. + - + - Copyright 2011-2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Crypto ( + Cipher, + KeyIds(..), + StorableCipher(..), + genEncryptedCipher, + genSharedCipher, + updateEncryptedCipher, + describeCipher, + decryptCipher, + encryptKey, + feedFile, + feedBytes, + readBytes, + encrypt, + decrypt, + GpgOpts(..), + getGpgOpts, + + prop_HmacSha1WithCipher_sane +) where + +import qualified Data.ByteString.Lazy as L +import Data.ByteString.Lazy.UTF8 (fromString) +import Control.Applicative + +import Common.Annex +import qualified Utility.Gpg as Gpg +import Utility.Gpg.Types +import Types.Key +import Types.Crypto + +{- The beginning of a Cipher is used for MAC'ing; the remainder is used + - as the GPG symmetric encryption passphrase. Note that the cipher + - itself is base-64 encoded, hence the string is longer than + - 'cipherSize': 683 characters, padded to 684. + - + - The 256 first characters that feed the MAC represent at best 192 + - bytes of entropy. However that's more than enough for both the + - default MAC algorithm, namely HMAC-SHA1, and the "strongest" + - currently supported, namely HMAC-SHA512, which respectively need + - (ideally) 64 and 128 bytes of entropy. + - + - The remaining characters (320 bytes of entropy) is enough for GnuPG's + - symetric cipher; unlike weaker public key crypto, the key does not + - need to be too large. + -} +cipherBeginning :: Int +cipherBeginning = 256 + +cipherSize :: Int +cipherSize = 512 + +cipherPassphrase :: Cipher -> String +cipherPassphrase (Cipher c) = drop cipherBeginning c + +cipherMac :: Cipher -> String +cipherMac (Cipher c) = take cipherBeginning c + +{- Creates a new Cipher, encrypted to the specified key id. -} +genEncryptedCipher :: String -> Bool -> IO StorableCipher +genEncryptedCipher keyid highQuality = do + ks <- Gpg.findPubKeys keyid + random <- Gpg.genRandom highQuality cipherSize + encryptCipher (Cipher random) ks + +{- Creates a new, shared Cipher. -} +genSharedCipher :: Bool -> IO StorableCipher +genSharedCipher highQuality = + SharedCipher <$> Gpg.genRandom highQuality cipherSize + +{- Updates an existing Cipher, re-encrypting it to add a keyid. -} +updateEncryptedCipher :: String -> StorableCipher -> IO StorableCipher +updateEncryptedCipher _ (SharedCipher _) = undefined +updateEncryptedCipher keyid encipher@(EncryptedCipher _ ks) = do + ks' <- Gpg.findPubKeys keyid + cipher <- decryptCipher encipher + encryptCipher cipher (merge ks ks') + where + merge (KeyIds a) (KeyIds b) = KeyIds $ a ++ b + +describeCipher :: StorableCipher -> String +describeCipher (SharedCipher _) = "shared cipher" +describeCipher (EncryptedCipher _ (KeyIds ks)) = + "with gpg " ++ keys ks ++ " " ++ unwords ks + where + keys [_] = "key" + keys _ = "keys" + +{- Encrypts a Cipher to the specified KeyIds. -} +encryptCipher :: Cipher -> KeyIds -> IO StorableCipher +encryptCipher (Cipher c) (KeyIds ks) = do + -- 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 + recipients l = force_recipients : + concatMap (\k -> [Param "--recipient", Param k]) l + -- Force gpg to only encrypt to the specified + -- recipients, not configured defaults. + force_recipients = Params "--no-encrypt-to --no-default-recipient" + +{- Decrypting an EncryptedCipher is expensive; the Cipher should be cached. -} +decryptCipher :: StorableCipher -> IO Cipher +decryptCipher (SharedCipher t) = return $ Cipher 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 + - on content. It does need to be repeatable. -} +encryptKey :: Mac -> Cipher -> Key -> Key +encryptKey mac c k = Key + { keyName = macWithCipher mac c (key2file k) + , keyBackendName = "GPG" ++ showMac mac + , keySize = Nothing -- size and mtime omitted + , keyMtime = Nothing -- to avoid leaking data + } + +type Feeder = Handle -> IO () +type Reader a = Handle -> IO a + +feedFile :: FilePath -> Feeder +feedFile f h = L.hPut h =<< L.readFile f + +feedBytes :: L.ByteString -> Feeder +feedBytes = flip L.hPut + +readBytes :: (L.ByteString -> IO a) -> Reader a +readBytes a h = L.hGetContents h >>= a + +{- 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. -} +decrypt :: Cipher -> Feeder -> Reader a -> IO a +decrypt = Gpg.feedRead [Param "--decrypt"] . cipherPassphrase + +macWithCipher :: Mac -> Cipher -> String -> String +macWithCipher mac c = macWithCipher' mac (cipherMac c) +macWithCipher' :: Mac -> String -> String -> String +macWithCipher' mac c s = calcMac mac (fromString c) (fromString s) + +{- Ensure that macWithCipher' returns the same thing forevermore. -} +prop_HmacSha1WithCipher_sane :: Bool +prop_HmacSha1WithCipher_sane = known_good == macWithCipher' HmacSha1 "foo" "bar" + where + known_good = "46b4ec586117154dacd49d664e5d63fdc88efb51" diff --git a/Fields.hs b/Fields.hs new file mode 100644 index 0000000000..ffd273be67 --- /dev/null +++ b/Fields.hs @@ -0,0 +1,35 @@ +{- git-annex fields + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Fields where + +import Common.Annex +import qualified Annex + +import Data.Char + +{- A field, stored in Annex state, with a value sanity checker. -} +data Field = Field + { fieldName :: String + , fieldCheck :: String -> Bool + } + +getField :: Field -> Annex (Maybe String) +getField = Annex.getField . fieldName + +remoteUUID :: Field +remoteUUID = Field "remoteuuid" $ + -- does it look like a UUID? + all (\c -> isAlphaNum c || c == '-') + +associatedFile :: Field +associatedFile = Field "associatedfile" $ \f -> + -- is the file a safe relative filename? + not (isAbsolute f) && not ("../" `isPrefixOf` f) + +direct :: Field +direct = Field "direct" $ \f -> f == "1" diff --git a/Git.hs b/Git.hs new file mode 100644 index 0000000000..cad4668538 --- /dev/null +++ b/Git.hs @@ -0,0 +1,140 @@ +{- git repository handling + - + - This is written to be completely independant of git-annex and should be + - suitable for other uses. + - + - Copyright 2010-2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE CPP #-} + +module Git ( + Repo(..), + Ref(..), + Branch, + Sha, + Tag, + repoIsUrl, + repoIsSsh, + repoIsHttp, + repoIsLocal, + repoIsLocalBare, + repoIsLocalUnknown, + repoDescribe, + repoLocation, + repoPath, + localGitDir, + attributes, + hookPath, + assertLocal, +) where + +import Network.URI (uriPath, uriScheme, unEscapeString) +#ifndef mingw32_HOST_OS +import System.Posix.Files +#endif + +import Common +import Git.Types +#ifndef mingw32_HOST_OS +import Utility.FileMode +#endif + +{- User-visible description of a git repo. -} +repoDescribe :: Repo -> String +repoDescribe Repo { remoteName = Just name } = name +repoDescribe Repo { location = Url url } = show url +repoDescribe Repo { location = Local { worktree = Just dir } } = dir +repoDescribe Repo { location = Local { gitdir = dir } } = dir +repoDescribe Repo { location = LocalUnknown dir } = dir +repoDescribe Repo { location = Unknown } = "UNKNOWN" + +{- Location of the repo, either as a path or url. -} +repoLocation :: Repo -> String +repoLocation Repo { location = Url url } = show url +repoLocation Repo { location = Local { worktree = Just dir } } = dir +repoLocation Repo { location = Local { gitdir = dir } } = dir +repoLocation Repo { location = LocalUnknown dir } = dir +repoLocation Repo { location = Unknown } = undefined + +{- Path to a repository. For non-bare, this is the worktree, for bare, + - it's the gitdir, and for URL repositories, is the path on the remote + - host. -} +repoPath :: Repo -> FilePath +repoPath Repo { location = Url u } = unEscapeString $ uriPath u +repoPath Repo { location = Local { worktree = Just d } } = d +repoPath Repo { location = Local { gitdir = d } } = d +repoPath Repo { location = LocalUnknown dir } = dir +repoPath Repo { location = Unknown } = undefined + +{- Path to a local repository's .git directory. -} +localGitDir :: Repo -> FilePath +localGitDir Repo { location = Local { gitdir = d } } = d +localGitDir _ = undefined + +{- Some code needs to vary between URL and normal repos, + - or bare and non-bare, these functions help with that. -} +repoIsUrl :: Repo -> Bool +repoIsUrl Repo { location = Url _ } = True +repoIsUrl _ = False + +repoIsSsh :: Repo -> Bool +repoIsSsh Repo { location = Url url } + | scheme == "ssh:" = True + -- git treats these the same as ssh + | scheme == "git+ssh:" = True + | scheme == "ssh+git:" = True + | otherwise = False + where + scheme = uriScheme url +repoIsSsh _ = False + +repoIsHttp :: Repo -> Bool +repoIsHttp Repo { location = Url url } + | uriScheme url == "http:" = True + | uriScheme url == "https:" = True + | otherwise = False +repoIsHttp _ = False + +repoIsLocal :: Repo -> Bool +repoIsLocal Repo { location = Local { } } = True +repoIsLocal _ = False + +repoIsLocalBare :: Repo -> Bool +repoIsLocalBare Repo { location = Local { worktree = Nothing } } = True +repoIsLocalBare _ = False + +repoIsLocalUnknown :: Repo -> Bool +repoIsLocalUnknown Repo { location = LocalUnknown { } } = True +repoIsLocalUnknown _ = False + +assertLocal :: Repo -> a -> a +assertLocal repo action + | repoIsUrl repo = error $ unwords + [ "acting on non-local git repo" + , repoDescribe repo + , "not supported" + ] + | otherwise = action + +{- Path to a repository's gitattributes file. -} +attributes :: Repo -> FilePath +attributes repo + | repoIsLocalBare repo = repoPath repo ++ "/info/.gitattributes" + | otherwise = repoPath repo ++ "/.gitattributes" + +{- Path to a given hook script in a repository, only if the hook exists + - and is executable. -} +hookPath :: String -> Repo -> IO (Maybe FilePath) +hookPath script repo = do + let hook = localGitDir repo "hooks" script + ifM (catchBoolIO $ isexecutable hook) + ( return $ Just hook , return Nothing ) + where +#if mingw32_HOST_OS + isexecutable f = doesFileExist f +#else + isexecutable f = isExecutable . fileMode <$> getFileStatus f +#endif diff --git a/Git/AutoCorrect.hs b/Git/AutoCorrect.hs new file mode 100644 index 0000000000..325632de95 --- /dev/null +++ b/Git/AutoCorrect.hs @@ -0,0 +1,71 @@ +{- git autocorrection using Damerau-Levenshtein edit distance + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Git.AutoCorrect where + +import Common +import Git.Types +import qualified Git.Config + +import Text.EditDistance +import Control.Concurrent + +{- These are the same cost values as used in git. -} +gitEditCosts :: EditCosts +gitEditCosts = EditCosts + { deletionCosts = ConstantCost 4 + , insertionCosts = ConstantCost 1 + , substitutionCosts = ConstantCost 2 + , transpositionCosts = ConstantCost 0 + } + +{- Git's source calls this "an empirically derived magic number" -} +similarityFloor :: Int +similarityFloor = 7 + +{- Finds inexact matches for the input amoung the choices. + - Returns an ordered list of good enough matches, or an empty list if + - nothing matches well. -} +fuzzymatches :: String -> (c -> String) -> [c] -> [c] +fuzzymatches input showchoice choices = fst $ unzip $ + sortBy comparecost $ filter similarEnough $ zip choices costs + where + distance = restrictedDamerauLevenshteinDistance gitEditCosts input + costs = map (distance . showchoice) choices + comparecost a b = compare (snd a) (snd b) + similarEnough (_, cst) = cst < similarityFloor + +{- Takes action based on git's autocorrect configuration, in preparation for + - an autocorrected command being run. -} +prepare :: String -> (c -> String) -> [c] -> Repo -> IO () +prepare input showmatch matches r = + case readish $ Git.Config.get "help.autocorrect" "0" r of + Just n + | n == 0 -> list + | n < 0 -> warn + | otherwise -> sleep n + Nothing -> list + where + list = error $ unlines $ + [ "Unknown command '" ++ input ++ "'" + , "" + , "Did you mean one of these?" + ] ++ map (\m -> "\t" ++ showmatch m) matches + warn = + hPutStr stderr $ unlines + [ "WARNING: You called a command named '" ++ + input ++ "', which does not exist." + , "Continuing under the assumption that you meant '" ++ + showmatch (Prelude.head matches) ++ "'" + ] + sleep n = do + warn + hPutStrLn stderr $ unwords + [ "in" + , show (fromIntegral n / 10 :: Float) + , "seconds automatically..."] + threadDelay (n * 100000) -- deciseconds to microseconds diff --git a/Git/Branch.hs b/Git/Branch.hs new file mode 100644 index 0000000000..d4a6840165 --- /dev/null +++ b/Git/Branch.hs @@ -0,0 +1,103 @@ +{- git branch stuff + - + - Copyright 2011 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE BangPatterns #-} + +module Git.Branch where + +import Common +import Git +import Git.Sha +import Git.Command +import Git.Ref (headRef) + +{- The currently checked out branch. + - + - In a just initialized git repo before the first commit, + - symbolic-ref will show the master branch, even though that + - branch is not created yet. So, this also looks at show-ref HEAD + - to double-check. + -} +current :: Repo -> IO (Maybe Git.Ref) +current r = do + v <- currentUnsafe r + case v of + Nothing -> return Nothing + Just branch -> + ifM (null <$> pipeReadStrict [Param "show-ref", Param $ show branch] r) + ( return Nothing + , return v + ) + +{- The current branch, which may not really exist yet. -} +currentUnsafe :: Repo -> IO (Maybe Git.Ref) +currentUnsafe r = parse . firstLine + <$> pipeReadStrict [Param "symbolic-ref", Param $ show headRef] r + where + parse l + | null l = Nothing + | otherwise = Just $ Git.Ref l + +{- Checks if the second branch has any commits not present on the first + - branch. -} +changed :: Branch -> Branch -> Repo -> IO Bool +changed origbranch newbranch repo + | origbranch == newbranch = return False + | otherwise = not . null <$> diffs + where + diffs = pipeReadStrict + [ Param "log" + , Param (show origbranch ++ ".." ++ show newbranch) + , Params "--oneline -n1" + ] repo + +{- Given a set of refs that are all known to have commits not + - on the branch, tries to update the branch by a fast-forward. + - + - In order for that to be possible, one of the refs must contain + - every commit present in all the other refs. + -} +fastForward :: Branch -> [Ref] -> Repo -> IO Bool +fastForward _ [] _ = return True +fastForward branch (first:rest) repo = + -- First, check that the branch does not contain any + -- new commits that are not in the first ref. If it does, + -- cannot fast-forward. + ifM (changed first branch repo) + ( no_ff + , maybe no_ff do_ff =<< findbest first rest + ) + where + no_ff = return False + do_ff to = do + run [Param "update-ref", Param $ show branch, Param $ show to] repo + return True + findbest c [] = return $ Just c + findbest c (r:rs) + | c == r = findbest c rs + | otherwise = do + better <- changed c r repo + worse <- changed r c repo + case (better, worse) of + (True, True) -> return Nothing -- divergent fail + (True, False) -> findbest r rs -- better + (False, True) -> findbest c rs -- worse + (False, False) -> findbest c rs -- same + +{- Commits the index into the specified branch (or other ref), + - with the specified parent refs, and returns the committed sha -} +commit :: String -> Branch -> [Ref] -> Repo -> IO Sha +commit message branch parentrefs repo = do + tree <- getSha "write-tree" $ + pipeReadStrict [Param "write-tree"] repo + sha <- getSha "commit-tree" $ pipeWriteRead + (map Param $ ["commit-tree", show tree] ++ ps) + message 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/BuildVersion.hs b/Git/BuildVersion.hs new file mode 100644 index 0000000000..832ee8ab72 --- /dev/null +++ b/Git/BuildVersion.hs @@ -0,0 +1,21 @@ +{- git build version + - + - Copyright 2011 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Git.BuildVersion where + +import Git.Version +import qualified Build.SysConfig + +{- Using the version it was configured for avoids running git to check its + - version, at the cost that upgrading git won't be noticed. + - This is only acceptable because it's rare that git's version influences + - code's behavior. -} +buildVersion :: GitVersion +buildVersion = normalize Build.SysConfig.gitversion + +older :: String -> Bool +older n = buildVersion < normalize n diff --git a/Git/CatFile.hs b/Git/CatFile.hs new file mode 100644 index 0000000000..46b59c631d --- /dev/null +++ b/Git/CatFile.hs @@ -0,0 +1,107 @@ +{- git cat-file interface + - + - Copyright 2011, 2013 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Git.CatFile ( + CatFileHandle, + catFileStart, + catFileStop, + catFile, + catObject, + catObjectDetails, +) where + +import System.IO +import qualified Data.ByteString as S +import qualified Data.ByteString.Lazy as L +import Data.Digest.Pure.SHA +import Data.Char +import System.Process (std_out, std_err) + +import Common +import Git +import Git.Sha +import Git.Command +import Git.Types +import Git.FilePath +import qualified Utility.CoProcess as CoProcess + +data CatFileHandle = CatFileHandle CoProcess.CoProcessHandle Repo + +catFileStart :: Repo -> IO CatFileHandle +catFileStart repo = do + coprocess <- CoProcess.rawMode =<< gitCoProcessStart True + [ Param "cat-file" + , Param "--batch" + ] repo + return $ CatFileHandle coprocess repo + +catFileStop :: CatFileHandle -> IO () +catFileStop (CatFileHandle p _) = CoProcess.stop p + +{- Reads a file from a specified branch. -} +catFile :: CatFileHandle -> Branch -> FilePath -> IO L.ByteString +catFile h branch file = catObject h $ Ref $ + show branch ++ ":" ++ toInternalGitPath file + +{- Uses a running git cat-file read the content of an object. + - Objects that do not exist will have "" returned. -} +catObject :: CatFileHandle -> Ref -> IO L.ByteString +catObject h object = maybe L.empty fst <$> catObjectDetails h object + +{- Gets both the content of an object, and its Sha. -} +catObjectDetails :: CatFileHandle -> Ref -> IO (Maybe (L.ByteString, Sha)) +catObjectDetails (CatFileHandle hdl repo) object = CoProcess.query hdl send receive + where + query = show object + send to = hPutStrLn to query + receive from = do + header <- hGetLine from + case words header of + [sha, objtype, size] + | length sha == shaSize && + isJust (readObjectType objtype) -> + case reads size of + [(bytes, "")] -> readcontent bytes from sha + _ -> dne + | otherwise -> dne + _ + | header == show object ++ " missing" -> dne + | otherwise -> + if any isSpace query + then fallback + else error $ "unknown response from git cat-file " ++ show (header, object) + readcontent bytes from sha = do + content <- S.hGet from bytes + eatchar '\n' from + return $ Just (L.fromChunks [content], Ref sha) + dne = return Nothing + eatchar expected from = do + c <- hGetChar from + when (c /= expected) $ + error $ "missing " ++ (show expected) ++ " from git cat-file" + + {- Work around a bug in git 1.8.4 rc0 which broke it for filenames + - containing spaces. http://bugs.debian.org/718517 + - Slow! Also can use a lot of memory, if the object is large. -} + fallback = do + let p = gitCreateProcess + [ Param "cat-file" + , Param "-p" + , Param query + ] repo + (_, Just h, _, pid) <- withNullHandle $ \h -> + createProcess p + { std_out = CreatePipe + , std_err = UseHandle h + } + fileEncoding h + content <- L.hGetContents h + let sha = (\s -> length s `seq` s) (showDigest $ sha1 content) + ok <- checkSuccessProcess pid + return $ if ok + then Just (content, Ref sha) + else Nothing diff --git a/Git/CheckAttr.hs b/Git/CheckAttr.hs new file mode 100644 index 0000000000..24fa2be87e --- /dev/null +++ b/Git/CheckAttr.hs @@ -0,0 +1,64 @@ +{- git check-attr interface + - + - Copyright 2010-2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Git.CheckAttr where + +import Common +import Git +import Git.Command +import qualified Git.BuildVersion +import qualified Utility.CoProcess as CoProcess + +type CheckAttrHandle = (CoProcess.CoProcessHandle, [Attr], String) + +type Attr = String + +{- Starts git check-attr running to look up the specified gitattributes + - values and returns a handle. -} +checkAttrStart :: [Attr] -> Repo -> IO CheckAttrHandle +checkAttrStart attrs repo = do + cwd <- getCurrentDirectory + h <- CoProcess.rawMode =<< gitCoProcessStart True params repo + return (h, attrs, cwd) + where + params = + [ Param "check-attr" + , Params "-z --stdin" + ] ++ map Param attrs ++ + [ Param "--" ] + +checkAttrStop :: CheckAttrHandle -> IO () +checkAttrStop (h, _, _) = CoProcess.stop h + +{- Gets an attribute of a file. -} +checkAttr :: CheckAttrHandle -> Attr -> FilePath -> IO String +checkAttr (h, attrs, cwd) want file = do + pairs <- CoProcess.query h send receive + let vals = map snd $ filter (\(attr, _) -> attr == want) pairs + case vals of + [v] -> return v + _ -> error $ "unable to determine " ++ want ++ " attribute of " ++ file + where + send to = hPutStr to $ file' ++ "\0" + receive from = forM attrs $ \attr -> do + l <- hGetLine from + return (attr, attrvalue attr l) + {- Before git 1.7.7, git check-attr worked best with + - absolute filenames; using them worked around some bugs + - with relative filenames. + - + - With newer git, git check-attr chokes on some absolute + - filenames, and the bugs that necessitated them were fixed, + - so use relative filenames. -} + oldgit = Git.BuildVersion.older "1.7.7" + file' + | oldgit = absPathFrom cwd file + | otherwise = relPathDirToFile cwd $ absPathFrom cwd file + attrvalue attr l = end bits !! 0 + where + bits = split sep l + sep = ": " ++ attr ++ ": " diff --git a/Git/CheckIgnore.hs b/Git/CheckIgnore.hs new file mode 100644 index 0000000000..2ab7cb3dcc --- /dev/null +++ b/Git/CheckIgnore.hs @@ -0,0 +1,71 @@ +{- git check-ignore interface + - + - Copyright 2013 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Git.CheckIgnore ( + CheckIgnoreHandle, + checkIgnoreStart, + checkIgnoreStop, + checkIgnored +) where + +import Common +import Git +import Git.Command +import qualified Git.Version +import qualified Utility.CoProcess as CoProcess + +import System.IO.Error + +type CheckIgnoreHandle = CoProcess.CoProcessHandle + +{- Starts git check-ignore running, and returns a handle. + - + - This relies on git check-ignore --non-matching -v outputting + - lines for both matching an non-matching files. Also relies on + - GIT_FLUSH behavior flushing the output buffer when git check-ignore + - is piping to us. + - + - The first version of git to support what we need is 1.8.4. + - Nothing is returned if an older git is installed. + -} +checkIgnoreStart :: Repo -> IO (Maybe CheckIgnoreHandle) +checkIgnoreStart repo = ifM supportedGitVersion + ( Just <$> (CoProcess.rawMode =<< gitCoProcessStart True params repo) + , return Nothing + ) + where + params = + [ Param "check-ignore" + , Params "-z --stdin --verbose --non-matching" + ] + +supportedGitVersion :: IO Bool +supportedGitVersion = do + v <- Git.Version.installed + return $ v >= Git.Version.normalize "1.8.4" + +checkIgnoreStop :: CheckIgnoreHandle -> IO () +checkIgnoreStop = CoProcess.stop + +{- Returns True if a file is ignored. -} +checkIgnored :: CheckIgnoreHandle -> FilePath -> IO Bool +checkIgnored h file = CoProcess.query h send (receive "") + where + send to = do + hPutStr to $ file ++ "\0" + hFlush to + receive c from = do + s <- hGetSomeString from 1024 + if null s + then eofError + else do + let v = c ++ s + maybe (receive v from) return (parse v) + parse s = case segment (== '\0') s of + (_source:_line:pattern:_pathname:_eol:[]) -> Just $ not $ null pattern + _ -> Nothing + eofError = ioError $ mkIOError userErrorType "git cat-file EOF" Nothing Nothing diff --git a/Git/Command.hs b/Git/Command.hs new file mode 100644 index 0000000000..2d68540e6e --- /dev/null +++ b/Git/Command.hs @@ -0,0 +1,124 @@ +{- running git commands + - + - Copyright 2010-2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Git.Command where + +import System.Process (std_out, env) + +import Common +import Git +import Git.Types +import qualified Utility.CoProcess as CoProcess + +{- Constructs a git command line operating on the specified repo. -} +gitCommandLine :: [CommandParam] -> Repo -> [CommandParam] +gitCommandLine params Repo { location = l@(Local _ _ ) } = setdir : settree ++ params + where + setdir = Param $ "--git-dir=" ++ gitdir l + settree = case worktree l of + Nothing -> [] + Just t -> [Param $ "--work-tree=" ++ t] +gitCommandLine _ repo = assertLocal repo $ error "internal" + +{- Runs git in the specified repo. -} +runBool :: [CommandParam] -> Repo -> IO Bool +runBool params repo = assertLocal repo $ + boolSystemEnv "git" + (gitCommandLine params repo) + (gitEnv repo) + +{- Runs git in the specified repo, throwing an error if it fails. -} +run :: [CommandParam] -> Repo -> IO () +run params repo = assertLocal repo $ + unlessM (runBool params repo) $ + error $ "git " ++ show params ++ " failed" + +{- 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 + - return True if it succeeded. Failure to wait will result in zombies. + -} +pipeReadLazy :: [CommandParam] -> Repo -> IO (String, IO Bool) +pipeReadLazy params repo = assertLocal repo $ do + (_, Just h, _, pid) <- createProcess p { std_out = CreatePipe } + fileEncoding h + c <- hGetContents h + return (c, checkSuccessProcess pid) + where + p = gitCreateProcess params repo + +{- Runs a git command, and returns its output, strictly. + - + - Nonzero exit status is ignored. + -} +pipeReadStrict :: [CommandParam] -> Repo -> IO String +pipeReadStrict params repo = assertLocal repo $ + withHandle StdoutHandle (createProcessChecked ignoreFailureProcess) p $ \h -> do + fileEncoding h + output <- hGetContentsStrict h + hClose h + return output + where + p = gitCreateProcess params repo + +{- 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 +pipeWriteRead params s repo = assertLocal repo $ + writeReadProcessEnv "git" (toCommand $ gitCommandLine params repo) + (gitEnv repo) s (Just adjusthandle) + where + adjusthandle h = do + fileEncoding h + hSetNewlineMode h noNewlineTranslation + +{- 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 + +{- Reads null terminated output of a git command (as enabled by the -z + - parameter), and splits it. -} +pipeNullSplit :: [CommandParam] -> Repo -> IO ([String], IO Bool) +pipeNullSplit params repo = do + (s, cleanup) <- pipeReadLazy params repo + return (filter (not . null) $ split sep s, cleanup) + where + sep = "\0" + +pipeNullSplitStrict :: [CommandParam] -> Repo -> IO [String] +pipeNullSplitStrict params repo = do + s <- pipeReadStrict params repo + return $ filter (not . null) $ split sep s + where + sep = "\0" + +pipeNullSplitZombie :: [CommandParam] -> Repo -> IO [String] +pipeNullSplitZombie params repo = leaveZombie <$> pipeNullSplit params repo + +{- Doesn't run the cleanup action. A zombie results. -} +leaveZombie :: (a, IO Bool) -> a +leaveZombie = fst + +{- Runs a git command as a coprocess. -} +gitCoProcessStart :: Bool -> [CommandParam] -> Repo -> IO CoProcess.CoProcessHandle +gitCoProcessStart restartable params repo = CoProcess.start restartable "git" + (toCommand $ gitCommandLine params repo) + (gitEnv repo) + +gitCreateProcess :: [CommandParam] -> Repo -> CreateProcess +gitCreateProcess params repo = + (proc "git" $ toCommand $ gitCommandLine params repo) + { env = gitEnv repo } diff --git a/Git/Config.hs b/Git/Config.hs new file mode 100644 index 0000000000..adc75a2085 --- /dev/null +++ b/Git/Config.hs @@ -0,0 +1,155 @@ +{- git repository configuration handling + - + - Copyright 2010-2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Git.Config where + +import qualified Data.Map as M +import Data.Char +import System.Process (cwd, env) + +import Common +import Git +import Git.Types +import qualified Git.Construct +import Utility.UserInfo + +{- Returns a single git config setting, or a default value if not set. -} +get :: String -> String -> Repo -> String +get key defaultValue repo = M.findWithDefault defaultValue key (config repo) + +{- Returns a list with each line of a multiline config setting. -} +getList :: String -> Repo -> [String] +getList key repo = M.findWithDefault [] key (fullconfig repo) + +{- Returns a single git config setting, if set. -} +getMaybe :: String -> Repo -> Maybe String +getMaybe key repo = M.lookup key (config repo) + +{- Runs git config and populates a repo with its config. + - Avoids re-reading config when run repeatedly. -} +read :: Repo -> IO Repo +read repo@(Repo { config = c }) + | c == M.empty = read' repo + | otherwise = return repo + +{- Reads config even if it was read before. -} +reRead :: Repo -> IO Repo +reRead r = read' $ r + { config = M.empty + , fullconfig = M.empty + } + +{- Cannot use pipeRead because it relies on the config having been already + - read. Instead, chdir to the repo and run git config. + -} +read' :: Repo -> IO Repo +read' repo = go repo + where + go Repo { location = Local { gitdir = d } } = git_config d + go Repo { location = LocalUnknown d } = git_config d + go _ = assertLocal repo $ error "internal" + git_config d = withHandle StdoutHandle createProcessSuccess p $ + hRead repo + where + params = ["config", "--null", "--list"] + p = (proc "git" params) + { cwd = Just d + , env = gitEnv repo + } + +{- Gets the global git config, returning a dummy Repo containing it. -} +global :: IO (Maybe Repo) +global = do + home <- myHomeDir + ifM (doesFileExist $ home ".gitconfig") + ( do + repo <- Git.Construct.fromUnknown + repo' <- withHandle StdoutHandle createProcessSuccess p $ + hRead repo + return $ Just repo' + , return Nothing + ) + where + params = ["config", "--null", "--list", "--global"] + p = (proc "git" params) + +{- Reads git config from a handle and populates a repo with it. -} +hRead :: Repo -> Handle -> IO Repo +hRead repo h = do + -- We use the FileSystemEncoding when reading from git-config, + -- because it can contain arbitrary filepaths (and other strings) + -- in any encoding. + fileEncoding h + val <- hGetContentsStrict h + store val repo + +{- Stores a git config into a Repo, returning the new version of the Repo. + - The git config may be multiple lines, or a single line. + - Config settings can be updated incrementally. + -} +store :: String -> Repo -> IO Repo +store s repo = do + let c = parse s + repo' <- updateLocation $ repo + { config = (M.map Prelude.head c) `M.union` config repo + , fullconfig = M.unionWith (++) c (fullconfig repo) + } + rs <- Git.Construct.fromRemotes repo' + return $ repo' { remotes = rs } + +{- Updates the location of a repo, based on its configuration. + - + - Git.Construct makes LocalUknown repos, of which only a directory is + - known. Once the config is read, this can be fixed up to a Local repo, + - based on the core.bare and core.worktree settings. + -} +updateLocation :: Repo -> IO Repo +updateLocation r@(Repo { location = LocalUnknown d }) + | isBare r = updateLocation' r $ Local d Nothing + | otherwise = updateLocation' r $ Local (d ".git") (Just d) +updateLocation r@(Repo { location = l@(Local {}) }) = updateLocation' r l +updateLocation r = return r + +updateLocation' :: Repo -> RepoLocation -> IO Repo +updateLocation' r l = do + l' <- case getMaybe "core.worktree" r of + Nothing -> return l + Just d -> do + {- core.worktree is relative to the gitdir -} + top <- absPath $ gitdir l + return $ l { worktree = Just $ absPathFrom top d } + return $ r { location = l' } + +{- Parses git config --list or git config --null --list output into a + - config map. -} +parse :: String -> M.Map String [String] +parse [] = M.empty +parse s + -- --list output will have an = in the first line + | all ('=' `elem`) (take 1 ls) = sep '=' ls + -- --null --list output separates keys from values with newlines + | otherwise = sep '\n' $ split "\0" s + where + ls = lines s + sep c = M.fromListWith (++) . map (\(k,v) -> (k, [v])) . + map (separate (== c)) + +{- Checks if a string from git config is a true value. -} +isTrue :: String -> Maybe Bool +isTrue s + | s' == "true" = Just True + | s' == "false" = Just False + | otherwise = Nothing + where + s' = map toLower s + +boolConfig :: Bool -> String +boolConfig True = "true" +boolConfig False = "false" + +isBare :: Repo -> Bool +isBare r = fromMaybe False $ isTrue =<< getMaybe "core.bare" r diff --git a/Git/Construct.hs b/Git/Construct.hs new file mode 100644 index 0000000000..586fa8c03c --- /dev/null +++ b/Git/Construct.hs @@ -0,0 +1,277 @@ +{- Construction of Git Repo objects + - + - Copyright 2010-2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE CPP #-} + +module Git.Construct ( + fromCwd, + fromAbsPath, + fromPath, + fromUrl, + fromUnknown, + localToUrl, + remoteNamed, + remoteNamedFromKey, + fromRemotes, + fromRemoteLocation, + repoAbsPath, + newFrom, + checkForRepo, +) where + +{-# LANGUAGE CPP #-} + +#ifndef mingw32_HOST_OS +import System.Posix.User +#else +import Git.FilePath +#endif +import qualified Data.Map as M hiding (map, split) +import Network.URI + +import Common +import Git.Types +import Git +import qualified Git.Url as Url +import Utility.UserInfo + +{- Finds the git repository used for the cwd, which may be in a parent + - directory. -} +fromCwd :: IO (Maybe Repo) +fromCwd = getCurrentDirectory >>= seekUp + where + seekUp dir = do + r <- checkForRepo dir + case r of + Nothing -> case parentDir dir of + "" -> return Nothing + d -> seekUp d + Just loc -> Just <$> newFrom loc + +{- Local Repo constructor, accepts a relative or absolute path. -} +fromPath :: FilePath -> IO Repo +fromPath dir = fromAbsPath =<< absPath dir + +{- Local Repo constructor, requires an absolute path to the repo be + - specified. -} +fromAbsPath :: FilePath -> IO Repo +fromAbsPath dir + | isAbsolute dir = ifM (doesDirectoryExist dir') ( ret dir' , hunt ) + | otherwise = + error $ "internal error, " ++ dir ++ " is not absolute" + where + ret = newFrom . LocalUnknown + {- Git always looks for "dir.git" in preference to + - to "dir", even if dir ends in a "/". -} + canondir = dropTrailingPathSeparator dir + dir' = canondir ++ ".git" + {- When dir == "foo/.git", git looks for "foo/.git/.git", + - and failing that, uses "foo" as the repository. -} + hunt + | (pathSeparator:".git") `isSuffixOf` canondir = + ifM (doesDirectoryExist $ dir ".git") + ( ret dir + , ret $ takeDirectory canondir + ) + | otherwise = ret dir + +{- Remote Repo constructor. Throws exception on invalid url. + - + - Git is somewhat forgiving about urls to repositories, allowing + - eg spaces that are not normally allowed unescaped in urls. + -} +fromUrl :: String -> IO Repo +fromUrl url + | not (isURI url) = fromUrlStrict $ escapeURIString isUnescapedInURI url + | otherwise = fromUrlStrict url + +fromUrlStrict :: String -> IO Repo +fromUrlStrict url + | startswith "file://" url = fromAbsPath $ uriPath u + | otherwise = newFrom $ Url u + where + u = fromMaybe bad $ parseURI url + bad = error $ "bad url " ++ url + +{- Creates a repo that has an unknown location. -} +fromUnknown :: IO Repo +fromUnknown = newFrom Unknown + +{- Converts a local Repo into a remote repo, using the reference repo + - which is assumed to be on the same host. -} +localToUrl :: Repo -> Repo -> Repo +localToUrl reference r + | not $ repoIsUrl reference = error "internal error; reference repo not url" + | repoIsUrl r = r + | otherwise = r { location = Url $ fromJust $ parseURI absurl } + where + absurl = concat + [ Url.scheme reference + , "//" + , Url.authority reference + , repoPath r + ] + +{- Calculates a list of a repo's configured remotes, by parsing its config. -} +fromRemotes :: Repo -> IO [Repo] +fromRemotes repo = mapM construct remotepairs + where + filterconfig f = filter f $ M.toList $ config repo + filterkeys f = filterconfig (\(k,_) -> f k) + remotepairs = filterkeys isremote + isremote k = startswith "remote." k && endswith ".url" k + construct (k,v) = remoteNamedFromKey k $ fromRemoteLocation v repo + +{- Sets the name of a remote when constructing the Repo to represent it. -} +remoteNamed :: String -> IO Repo -> IO Repo +remoteNamed n constructor = do + r <- constructor + return $ r { remoteName = Just n } + +{- Sets the name of a remote based on the git config key, such as + - "remote.foo.url". -} +remoteNamedFromKey :: String -> IO Repo -> IO Repo +remoteNamedFromKey k = remoteNamed basename + where + basename = intercalate "." $ + reverse $ drop 1 $ reverse $ drop 1 $ split "." k + +{- Constructs a new Repo for one of a Repo's remotes using a given + - location (ie, an url). -} +fromRemoteLocation :: String -> Repo -> IO Repo +fromRemoteLocation s repo = gen $ calcloc s + where + gen v +#ifdef mingw32_HOST_OS + | dosstyle v = fromRemotePath (dospath v) repo +#endif + | scpstyle v = fromUrl $ scptourl v + | urlstyle v = fromUrl v + | otherwise = fromRemotePath v repo + -- insteadof config can rewrite remote location + calcloc l + | null insteadofs = l + | otherwise = replacement ++ drop (length bestvalue) l + where + replacement = drop (length prefix) $ + take (length bestkey - length suffix) bestkey + (bestkey, bestvalue) = maximumBy longestvalue insteadofs + longestvalue (_, a) (_, b) = compare b a + insteadofs = filterconfig $ \(k, v) -> + startswith prefix k && + endswith suffix k && + startswith v l + filterconfig f = filter f $ + concatMap splitconfigs $ M.toList $ fullconfig repo + splitconfigs (k, vs) = map (\v -> (k, v)) vs + (prefix, suffix) = ("url." , ".insteadof") + urlstyle v = isURI v || ":" `isInfixOf` v && "//" `isInfixOf` v + -- git remotes can be written scp style -- [user@]host:dir + -- but foo::bar is a git-remote-helper location instead + scpstyle v = ":" `isInfixOf` v + && not ("//" `isInfixOf` v) + && not ("::" `isInfixOf` v) + scptourl v = "ssh://" ++ host ++ slash dir + where + (host, dir) = separate (== ':') v + slash d | d == "" = "/~/" ++ d + | "/" `isPrefixOf` d = d + | "~" `isPrefixOf` d = '/':d + | otherwise = "/~/" ++ d +#ifdef mingw32_HOST_OS + -- git on Windows will write a path to .git/config with "drive:", + -- which is not to be confused with a "host:" + dosstyle = hasDrive + dospath = fromInternalGitPath +#endif + +{- Constructs a Repo from the path specified in the git remotes of + - another Repo. -} +fromRemotePath :: FilePath -> Repo -> IO Repo +fromRemotePath dir repo = do + dir' <- expandTilde dir + fromAbsPath $ repoPath repo dir' + +{- Git remotes can have a directory that is specified relative + - to the user's home directory, or that contains tilde expansions. + - This converts such a directory to an absolute path. + - Note that it has to run on the system where the remote is. + -} +repoAbsPath :: FilePath -> IO FilePath +repoAbsPath d = do + d' <- expandTilde d + h <- myHomeDir + return $ h d' + +expandTilde :: FilePath -> IO FilePath +#ifdef mingw32_HOST_OS +expandTilde = return +#else +expandTilde = expandt True + where + expandt _ [] = return "" + expandt _ ('/':cs) = do + v <- expandt True cs + return ('/':v) + expandt True ('~':'/':cs) = do + h <- myHomeDir + return $ h cs + expandt True ('~':cs) = do + let (name, rest) = findname "" cs + u <- getUserEntryForName name + return $ homeDirectory u rest + expandt _ (c:cs) = do + v <- expandt False cs + return (c:v) + findname n [] = (n, "") + findname n (c:cs) + | c == '/' = (n, cs) + | otherwise = findname (n++[c]) cs +#endif + +{- 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 $ + check gitDirFile $ + check isBareRepo $ + return Nothing + where + check test cont = maybe cont (return . Just) =<< test + checkdir c = ifM c + ( return $ Just $ LocalUnknown dir + , return Nothing + ) + isRepo = checkdir $ gitSignature $ ".git" "config" + isBareRepo = checkdir $ gitSignature "config" + <&&> doesDirectoryExist (dir "objects") + gitDirFile = do + c <- firstLine <$> + catchDefaultIO "" (readFile $ dir ".git") + return $ if gitdirprefix `isPrefixOf` c + then Just $ Local + { gitdir = absPathFrom dir $ + drop (length gitdirprefix) c + , worktree = Just dir + } + else Nothing + where + gitdirprefix = "gitdir: " + gitSignature file = doesFileExist $ dir file + +newFrom :: RepoLocation -> IO Repo +newFrom l = return Repo + { location = l + , config = M.empty + , fullconfig = M.empty + , remotes = [] + , remoteName = Nothing + , gitEnv = Nothing + } + + diff --git a/Git/CurrentRepo.hs b/Git/CurrentRepo.hs new file mode 100644 index 0000000000..ee91a6b815 --- /dev/null +++ b/Git/CurrentRepo.hs @@ -0,0 +1,67 @@ +{- The current git repository. + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE CPP #-} + +module Git.CurrentRepo where + +import Common +import Git.Types +import Git.Construct +import qualified Git.Config +#ifndef mingw32_HOST_OS +import Utility.Env +#endif + +{- Gets the current git repository. + - + - Honors GIT_DIR and GIT_WORK_TREE. + - Both environment variables are unset, to avoid confusing other git + - commands that also look at them. Instead, the Git module passes + - --work-tree and --git-dir to git commands it runs. + - + - When GIT_WORK_TREE or core.worktree are set, changes the working + - directory if necessary to ensure it is within the repository's work + - tree. While not needed for git commands, this is useful for anything + - else that looks for files in the worktree. + -} +get :: IO Repo +get = do + gd <- pathenv "GIT_DIR" + r <- configure gd =<< fromCwd + wt <- maybe (worktree $ location r) Just <$> pathenv "GIT_WORK_TREE" + case wt of + Nothing -> return r + Just d -> do + cwd <- getCurrentDirectory + unless (d `dirContains` cwd) $ + setCurrentDirectory d + return $ addworktree wt r + where +#ifndef mingw32_HOST_OS + pathenv s = do + v <- getEnv s + case v of + Just d -> do + void $ unsetEnv s + Just <$> absPath d + Nothing -> return Nothing +#else + pathenv _ = return Nothing +#endif + + configure Nothing (Just r) = Git.Config.read r + configure (Just d) _ = do + absd <- absPath d + cwd <- getCurrentDirectory + r <- newFrom $ Local { gitdir = absd, worktree = Just cwd } + Git.Config.read r + configure Nothing Nothing = error "Not in a git repository." + + addworktree w r = changelocation r $ + Local { gitdir = gitdir (location r), worktree = w } + changelocation r l = r { location = l } diff --git a/Git/DiffTree.hs b/Git/DiffTree.hs new file mode 100644 index 0000000000..cf8a376008 --- /dev/null +++ b/Git/DiffTree.hs @@ -0,0 +1,89 @@ +{- git diff-tree interface + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Git.DiffTree ( + DiffTreeItem(..), + diffTree, + diffTreeRecursive, + diffIndex, +) where + +import Numeric +import System.Posix.Types + +import Common +import Git +import Git.Sha +import Git.Command +import qualified Git.Filename +import qualified Git.Ref + +data DiffTreeItem = DiffTreeItem + { srcmode :: FileMode + , dstmode :: FileMode + , srcsha :: Sha -- nullSha if file was added + , dstsha :: Sha -- nullSha if file was deleted + , status :: String + , file :: FilePath + } deriving Show + +{- Diffs two tree Refs. -} +diffTree :: Ref -> Ref -> Repo -> IO ([DiffTreeItem], IO Bool) +diffTree src dst = getdiff (Param "diff-tree") + [Param (show src), Param (show dst)] + +{- Diffs two tree Refs, recursing into sub-trees -} +diffTreeRecursive :: Ref -> Ref -> Repo -> IO ([DiffTreeItem], IO Bool) +diffTreeRecursive src dst = getdiff (Param "diff-tree") + [Param "-r", Param (show src), Param (show dst)] + +{- Diffs between the repository and index. Does nothing if there is not + - yet a commit in the repository. -} +diffIndex :: Repo -> IO ([DiffTreeItem], IO Bool) +diffIndex repo = do + ifM (Git.Ref.headExists repo) + ( getdiff (Param "diff-index") + [ Param "--cached" + , Param $ show Git.Ref.headRef + ] repo + , return ([], return True) + ) + +getdiff :: CommandParam -> [CommandParam] -> Repo -> IO ([DiffTreeItem], IO Bool) +getdiff command params repo = do + (diff, cleanup) <- pipeNullSplit ps repo + return (parseDiffTree diff, cleanup) + where + ps = command : Params "-z --raw --no-renames -l0" : params + +{- Parses diff-tree output. -} +parseDiffTree :: [String] -> [DiffTreeItem] +parseDiffTree l = go l [] + where + go [] c = c + go (info:f:rest) c = go rest (mk info f : c) + go (s:[]) _ = error $ "diff-tree parse error " ++ s + + mk info f = DiffTreeItem + { srcmode = readmode srcm + , dstmode = readmode dstm + , srcsha = fromMaybe (error "bad srcsha") $ extractSha ssha + , dstsha = fromMaybe (error "bad dstsha") $ extractSha dsha + , status = s + , file = Git.Filename.decode f + } + where + readmode = fst . Prelude.head . readOct + + -- info = : SP SP SP SP + -- All fields are fixed, so we can pull them out of + -- specific positions in the line. + (srcm, past_srcm) = splitAt 7 $ drop 1 info + (dstm, past_dstm) = splitAt 7 past_srcm + (ssha, past_ssha) = splitAt shaSize past_dstm + (dsha, past_dsha) = splitAt shaSize $ drop 1 past_ssha + s = drop 1 past_dsha diff --git a/Git/FilePath.hs b/Git/FilePath.hs new file mode 100644 index 0000000000..891f9991d3 --- /dev/null +++ b/Git/FilePath.hs @@ -0,0 +1,58 @@ +{- git FilePath library + - + - Different git commands use different types of FilePaths to refer to + - files in the repository. Some commands use paths relative to the + - top of the repository even when run in a subdirectory. Adding some + - types helps keep that straight. + - + - Copyright 2012-2013 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE CPP #-} + +module Git.FilePath ( + TopFilePath, + getTopFilePath, + toTopFilePath, + asTopFilePath, + InternalGitPath, + toInternalGitPath, + fromInternalGitPath +) where + +import Common +import Git + +{- A FilePath, relative to the top of the git repository. -} +newtype TopFilePath = TopFilePath { getTopFilePath :: FilePath } + +{- The input FilePath can be absolute, or relative to the CWD. -} +toTopFilePath :: FilePath -> Git.Repo -> IO TopFilePath +toTopFilePath file repo = TopFilePath <$> + relPathDirToFile (repoPath repo) <$> absPath file + +{- The input FilePath must already be relative to the top of the git + - repository -} +asTopFilePath :: FilePath -> TopFilePath +asTopFilePath file = TopFilePath file + +{- Git may use a different representation of a path when storing + - it internally. For example, on Windows, git uses '/' to separate paths + - stored in the repository, despite Windows using '\' -} +type InternalGitPath = String + +toInternalGitPath :: FilePath -> InternalGitPath +#ifndef mingw32_HOST_OS +toInternalGitPath = id +#else +toInternalGitPath = replace "\\" "/" +#endif + +fromInternalGitPath :: InternalGitPath -> FilePath +#ifndef mingw32_HOST_OS +fromInternalGitPath = id +#else +fromInternalGitPath = replace "/" "\\" +#endif diff --git a/Git/Filename.hs b/Git/Filename.hs new file mode 100644 index 0000000000..5e076d3b5a --- /dev/null +++ b/Git/Filename.hs @@ -0,0 +1,28 @@ +{- Some git commands output encoded filenames, in a rather annoyingly complex + - C-style encoding. + - + - Copyright 2010, 2011 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Git.Filename where + +import Utility.Format (decode_c, encode_c) + +import Common + +decode :: String -> FilePath +decode [] = [] +decode f@(c:s) + -- encoded strings will be inside double quotes + | c == '"' && end s == ['"'] = decode_c $ beginning s + | otherwise = f + +{- Should not need to use this, except for testing decode. -} +encode :: FilePath -> String +encode s = "\"" ++ encode_c s ++ "\"" + +{- for quickcheck -} +prop_idempotent_deencode :: String -> Bool +prop_idempotent_deencode s = s == decode (encode s) diff --git a/Git/HashObject.hs b/Git/HashObject.hs new file mode 100644 index 0000000000..c6e1d23490 --- /dev/null +++ b/Git/HashObject.hs @@ -0,0 +1,43 @@ +{- git hash-object interface + - + - Copyright 2011-2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Git.HashObject where + +import Common +import Git +import Git.Sha +import Git.Command +import Git.Types +import qualified Utility.CoProcess as CoProcess + +type HashObjectHandle = CoProcess.CoProcessHandle + +hashObjectStart :: Repo -> IO HashObjectHandle +hashObjectStart = CoProcess.rawMode <=< gitCoProcessStart True + [ Param "hash-object" + , Param "-w" + , Param "--stdin-paths" + , Param "--no-filters" + ] + +hashObjectStop :: HashObjectHandle -> IO () +hashObjectStop = CoProcess.stop + +{- Injects a file into git, returning the Sha of the object. -} +hashFile :: HashObjectHandle -> FilePath -> IO Sha +hashFile h file = CoProcess.query h send receive + where + send to = hPutStrLn to file + receive from = getSha "hash-object" $ hGetLine from + +{- Injects some content into git, returning its Sha. -} +hashObject :: ObjectType -> String -> Repo -> IO Sha +hashObject objtype content repo = getSha subcmd $ + pipeWriteRead (map Param params) content repo + where + subcmd = "hash-object" + params = [subcmd, "-t", show objtype, "-w", "--stdin", "--no-filters"] diff --git a/Git/Index.hs b/Git/Index.hs new file mode 100644 index 0000000000..5b660bb307 --- /dev/null +++ b/Git/Index.hs @@ -0,0 +1,27 @@ +{- git index file stuff + - + - Copyright 2011 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Git.Index where + +import Utility.Env + +{- Forces git to use the specified index file. + - + - Returns an action that will reset back to the default + - index file. + - + - Warning: Not thread safe. + -} +override :: FilePath -> IO (IO ()) +override index = do + res <- getEnv var + setEnv var index True + return $ reset res + where + var = "GIT_INDEX_FILE" + reset (Just v) = setEnv var v True + reset _ = unsetEnv var diff --git a/Git/LsFiles.hs b/Git/LsFiles.hs new file mode 100644 index 0000000000..f4e4672158 --- /dev/null +++ b/Git/LsFiles.hs @@ -0,0 +1,193 @@ +{- git ls-files interface + - + - Copyright 2010,2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Git.LsFiles ( + inRepo, + notInRepo, + deleted, + modified, + staged, + stagedNotDeleted, + stagedOthersDetails, + stagedDetails, + typeChanged, + typeChangedStaged, + Conflicting(..), + Unmerged(..), + unmerged, +) where + +import Common +import Git +import Git.Command +import Git.Types +import Git.Sha + +{- Scans for files that are checked into git at the specified locations. -} +inRepo :: [FilePath] -> Repo -> IO ([FilePath], IO Bool) +inRepo l = pipeNullSplit $ Params "ls-files --cached -z --" : map File l + +{- Scans for files at the specified locations that are not checked into git. -} +notInRepo :: Bool -> [FilePath] -> Repo -> IO ([FilePath], IO Bool) +notInRepo include_ignored l repo = pipeNullSplit params repo + where + params = [Params "ls-files --others"] ++ exclude ++ + [Params "-z --"] ++ map File l + exclude + | include_ignored = [] + | otherwise = [Param "--exclude-standard"] + +{- Returns a list of files in the specified locations that have been + - deleted. -} +deleted :: [FilePath] -> Repo -> IO ([FilePath], IO Bool) +deleted l repo = pipeNullSplit params repo + where + params = [Params "ls-files --deleted -z --"] ++ map File l + +{- Returns a list of files in the specified locations that have been + - modified. -} +modified :: [FilePath] -> Repo -> IO ([FilePath], IO Bool) +modified l repo = pipeNullSplit params repo + where + params = [Params "ls-files --modified -z --"] ++ map File l + +{- Returns a list of all files that are staged for commit. -} +staged :: [FilePath] -> Repo -> IO ([FilePath], IO Bool) +staged = staged' [] + +{- Returns a list of the files, staged for commit, that are being added, + - moved, or changed (but not deleted), from the specified locations. -} +stagedNotDeleted :: [FilePath] -> Repo -> IO ([FilePath], IO Bool) +stagedNotDeleted = staged' [Param "--diff-filter=ACMRT"] + +staged' :: [CommandParam] -> [FilePath] -> Repo -> IO ([FilePath], IO Bool) +staged' ps l = pipeNullSplit $ prefix ++ ps ++ suffix + where + prefix = [Params "diff --cached --name-only -z"] + suffix = Param "--" : map File l + +{- Returns details about files that are staged in the index, + - as well as files not yet in git. Skips ignored files. -} +stagedOthersDetails :: [FilePath] -> Repo -> IO ([(FilePath, Maybe Sha)], IO Bool) +stagedOthersDetails = stagedDetails' [Params "--others --exclude-standard"] + +{- Returns details about all files that are staged in the index. -} +stagedDetails :: [FilePath] -> Repo -> IO ([(FilePath, Maybe Sha)], IO Bool) +stagedDetails = stagedDetails' [] + +{- Gets details about staged files, including the Sha of their staged + - contents. -} +stagedDetails' :: [CommandParam] -> [FilePath] -> Repo -> IO ([(FilePath, Maybe Sha)], IO Bool) +stagedDetails' ps l repo = do + (ls, cleanup) <- pipeNullSplit params repo + return (map parse ls, cleanup) + where + params = Params "ls-files --stage -z" : ps ++ + Param "--" : map File l + parse s + | null file = (s, Nothing) + | otherwise = (file, extractSha $ take shaSize $ drop 7 metadata) + where + (metadata, file) = separate (== '\t') s + +{- Returns a list of the files in the specified locations that are staged + - for commit, and whose type has changed. -} +typeChangedStaged :: [FilePath] -> Repo -> IO ([FilePath], IO Bool) +typeChangedStaged = typeChanged' [Param "--cached"] + +{- Returns a list of the files in the specified locations whose type has + - changed. Files only staged for commit will not be included. -} +typeChanged :: [FilePath] -> Repo -> IO ([FilePath], IO Bool) +typeChanged = typeChanged' [] + +typeChanged' :: [CommandParam] -> [FilePath] -> Repo -> IO ([FilePath], IO Bool) +typeChanged' ps l repo = do + (fs, cleanup) <- pipeNullSplit (prefix ++ ps ++ suffix) repo + -- git diff returns filenames relative to the top of the git repo; + -- convert to filenames relative to the cwd, like git ls-files. + let top = repoPath repo + cwd <- getCurrentDirectory + return (map (\f -> relPathDirToFile cwd $ top f) fs, cleanup) + where + prefix = [Params "diff --name-only --diff-filter=T -z"] + 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. -} +data Conflicting v = Conflicting + { valUs :: Maybe v + , valThem :: Maybe v + } deriving (Show) + +data Unmerged = Unmerged + { unmergedFile :: FilePath + , unmergedBlobType :: Conflicting BlobType + , unmergedSha :: Conflicting Sha + } deriving (Show) + +{- Returns a list of the files in the specified locations that have + - unresolved merge conflicts. + - + - ls-files outputs multiple lines per conflicting file, each with its own + - stage number: + - 1 = old version, can be ignored + - 2 = us + - 3 = them + - If a line is omitted, that side removed the file. + -} +unmerged :: [FilePath] -> Repo -> IO ([Unmerged], IO Bool) +unmerged l repo = do + (fs, cleanup) <- pipeNullSplit params repo + return (reduceUnmerged [] $ catMaybes $ map parseUnmerged fs, cleanup) + where + params = Params "ls-files --unmerged -z --" : map File l + +data InternalUnmerged = InternalUnmerged + { isus :: Bool + , ifile :: FilePath + , iblobtype :: Maybe BlobType + , isha :: Maybe Sha + } deriving (Show) + +parseUnmerged :: String -> Maybe InternalUnmerged +parseUnmerged s + | null file = Nothing + | otherwise = case words metadata of + (rawblobtype:rawsha:rawstage:_) -> do + stage <- readish rawstage :: Maybe Int + unless (stage == 2 || stage == 3) $ + fail undefined -- skip stage 1 + blobtype <- readBlobType rawblobtype + sha <- extractSha rawsha + return $ InternalUnmerged (stage == 2) file + (Just blobtype) (Just sha) + _ -> Nothing + where + (metadata, file) = separate (== '\t') s + +reduceUnmerged :: [Unmerged] -> [InternalUnmerged] -> [Unmerged] +reduceUnmerged c [] = c +reduceUnmerged c (i:is) = reduceUnmerged (new:c) rest + where + (rest, sibi) = findsib i is + (blobtypeA, blobtypeB, shaA, shaB) + | isus i = (iblobtype i, iblobtype sibi, isha i, isha sibi) + | otherwise = (iblobtype sibi, iblobtype i, isha sibi, isha i) + new = Unmerged + { unmergedFile = ifile i + , unmergedBlobType = Conflicting blobtypeA blobtypeB + , unmergedSha = Conflicting shaA shaB + } + findsib templatei [] = ([], removed templatei) + findsib templatei (l:ls) + | ifile l == ifile templatei = (ls, l) + | otherwise = (l:ls, removed templatei) + removed templatei = templatei + { isus = not (isus templatei) + , iblobtype = Nothing + , isha = Nothing + } diff --git a/Git/LsTree.hs b/Git/LsTree.hs new file mode 100644 index 0000000000..6e4cd8470a --- /dev/null +++ b/Git/LsTree.hs @@ -0,0 +1,60 @@ +{- git ls-tree interface + - + - Copyright 2011 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Git.LsTree ( + TreeItem(..), + lsTree, + lsTreeFiles, + parseLsTree +) where + +import Numeric +import Control.Applicative +import System.Posix.Types + +import Common +import Git +import Git.Command +import Git.Sha +import qualified Git.Filename + +data TreeItem = TreeItem + { mode :: FileMode + , typeobj :: String + , sha :: String + , file :: FilePath + } deriving Show + +{- Lists the complete contents of a tree, with lazy output. -} +lsTree :: Ref -> Repo -> IO [TreeItem] +lsTree t repo = map parseLsTree <$> pipeNullSplitZombie ps repo + where + ps = [Params "ls-tree --full-tree -z -r --", File $ show t] + +{- Lists specified files in a tree. -} +lsTreeFiles :: Ref -> [FilePath] -> Repo -> IO [TreeItem] +lsTreeFiles t fs repo = map parseLsTree <$> pipeNullSplitStrict ps repo + where + ps = [Params "ls-tree -z --", File $ show t] ++ map File fs + +{- Parses a line of ls-tree output. + - (The --long format is not currently supported.) -} +parseLsTree :: String -> TreeItem +parseLsTree l = TreeItem + { mode = fst $ Prelude.head $ readOct m + , typeobj = t + , sha = s + , file = Git.Filename.decode f + } + where + -- l = SP SP TAB + -- All fields are fixed, so we can pull them out of + -- specific positions in the line. + (m, past_m) = splitAt 7 l + (t, past_t) = splitAt 4 past_m + (s, past_s) = splitAt shaSize $ Prelude.tail past_t + f = Prelude.tail past_s diff --git a/Git/Merge.hs b/Git/Merge.hs new file mode 100644 index 0000000000..f5791274f5 --- /dev/null +++ b/Git/Merge.hs @@ -0,0 +1,21 @@ +{- git merging + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Git.Merge where + +import Common +import Git +import Git.Command +import Git.BuildVersion + +{- Avoids recent git's interactive merge. -} +mergeNonInteractive :: Ref -> Repo -> IO Bool +mergeNonInteractive branch + | older "1.7.7.6" = merge [Param $ show branch] + | otherwise = merge [Param "--no-edit", Param $ show branch] + where + merge ps = runBool $ Param "merge" : ps diff --git a/Git/Queue.hs b/Git/Queue.hs new file mode 100644 index 0000000000..b8e863658a --- /dev/null +++ b/Git/Queue.hs @@ -0,0 +1,159 @@ +{- git repository command queue + - + - Copyright 2010,2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE BangPatterns #-} + +module Git.Queue ( + Queue, + new, + addCommand, + addUpdateIndex, + size, + full, + flush, +) where + +import qualified Data.Map as M +import System.IO +import System.Process + +import Utility.SafeCommand +import Common +import Git +import Git.Command +import qualified Git.UpdateIndex + +{- Queable actions that can be performed in a git repository. + -} +data Action + {- Updating the index file, using a list of streamers that can + - be added to as the queue grows. -} + = UpdateIndexAction + { getStreamers :: [Git.UpdateIndex.Streamer] -- in reverse order + } + {- A git command to run, on a list of files that can be added to + - as the queue grows. -} + | CommandAction + { getSubcommand :: String + , getParams :: [CommandParam] + , getFiles :: [CommandParam] + } + +{- A key that can uniquely represent an action in a Map. -} +data ActionKey = UpdateIndexActionKey | CommandActionKey String + deriving (Eq, Ord) + +actionKey :: Action -> ActionKey +actionKey (UpdateIndexAction _) = UpdateIndexActionKey +actionKey CommandAction { getSubcommand = s } = CommandActionKey s + +{- A queue of actions to perform (in any order) on a git repository, + - with lists of files to perform them on. This allows coalescing + - similar git commands. -} +data Queue = Queue + { size :: Int + , _limit :: Int + , items :: M.Map ActionKey Action + } + +{- A recommended maximum size for the queue, after which it should be + - run. + - + - 10240 is semi-arbitrary. If we assume git filenames are between 10 and + - 255 characters long, then the queue will build up between 100kb and + - 2550kb long commands. The max command line length on linux is somewhere + - above 20k, so this is a fairly good balance -- the queue will buffer + - only a few megabytes of stuff and a minimal number of commands will be + - run by xargs. -} +defaultLimit :: Int +defaultLimit = 10240 + +{- Constructor for empty queue. -} +new :: Maybe Int -> Queue +new lim = Queue 0 (fromMaybe defaultLimit lim) M.empty + +{- Adds an git command to the queue. + - + - Git commands with the same subcommand but different parameters are + - assumed to be equivilant enough to perform in any order with the same + - result. + -} +addCommand :: String -> [CommandParam] -> [FilePath] -> Queue -> Repo -> IO Queue +addCommand subcommand params files q repo = + updateQueue action different (length newfiles) q repo + where + key = actionKey action + action = CommandAction + { getSubcommand = subcommand + , getParams = params + , getFiles = newfiles + } + newfiles = map File files ++ maybe [] getFiles (M.lookup key $ items q) + + different (CommandAction { getSubcommand = s }) = s /= subcommand + different _ = True + +{- Adds an update-index streamer to the queue. -} +addUpdateIndex :: Git.UpdateIndex.Streamer -> Queue -> Repo -> IO Queue +addUpdateIndex streamer q repo = + updateQueue action different 1 q repo + where + key = actionKey action + -- the list is built in reverse order + action = UpdateIndexAction $ streamer : streamers + streamers = maybe [] getStreamers $ M.lookup key $ items q + + different (UpdateIndexAction _) = False + different _ = True + +{- Updates or adds an action in the queue. If the queue already contains a + - different action, it will be flushed; this is to ensure that conflicting + - actions, like add and rm, are run in the right order.-} +updateQueue :: Action -> (Action -> Bool) -> Int -> Queue -> Repo -> IO Queue +updateQueue !action different sizeincrease q repo + | null (filter different (M.elems (items q))) = return $ go q + | otherwise = go <$> flush q repo + where + go q' = newq + where + !newq = q' + { size = newsize + , items = newitems + } + !newsize = size q' + sizeincrease + !newitems = M.insertWith' const (actionKey action) action (items q') + +{- Is a queue large enough that it should be flushed? -} +full :: Queue -> Bool +full (Queue cur lim _) = cur > lim + +{- Runs a queue on a git repository. -} +flush :: Queue -> Repo -> IO Queue +flush (Queue _ lim m) repo = do + forM_ (M.elems m) $ runAction repo + return $ Queue 0 lim M.empty + +{- Runs an Action on a list of files in a git repository. + - + - Complicated by commandline length limits. + - + - Intentionally runs the command even if the list of files is empty; + - this allows queueing commands that do not need a list of files. -} +runAction :: Repo -> Action -> IO () +runAction repo (UpdateIndexAction streamers) = + -- list is stored in reverse order + Git.UpdateIndex.streamUpdateIndex repo $ reverse streamers +runAction repo action@(CommandAction {}) = + withHandle StdinHandle createProcessSuccess p $ \h -> do + fileEncoding h + hPutStr h $ intercalate "\0" $ toCommand $ getFiles action + hClose h + where + p = (proc "xargs" params) { env = gitEnv repo } + params = "-0":"git":baseparams + baseparams = toCommand $ gitCommandLine + (Param (getSubcommand action):getParams action) repo diff --git a/Git/Ref.hs b/Git/Ref.hs new file mode 100644 index 0000000000..954b61a2e5 --- /dev/null +++ b/Git/Ref.hs @@ -0,0 +1,108 @@ +{- git ref stuff + - + - Copyright 2011-2013 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Git.Ref where + +import Common +import Git +import Git.Command + +import Data.Char (chr) + +headRef :: Ref +headRef = Ref "HEAD" + +{- Converts a fully qualified git ref into a user-visible string. -} +describe :: Ref -> String +describe = show . base + +{- Often git refs are fully qualified (eg: refs/heads/master). + - Converts such a fully qualified ref into a base ref (eg: master). -} +base :: Ref -> Ref +base = Ref . remove "refs/heads/" . remove "refs/remotes/" . show + where + remove prefix s + | prefix `isPrefixOf` s = drop (length prefix) s + | otherwise = s + +{- Given a directory such as "refs/remotes/origin", and a ref such as + - refs/heads/master, yields a version of that ref under the directory, + - such as refs/remotes/origin/master. -} +under :: String -> Ref -> Ref +under dir r = Ref $ dir show (base r) + +{- Checks if a ref exists. -} +exists :: Ref -> Repo -> IO Bool +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. -} +headExists :: Repo -> IO Bool +headExists repo = do + ls <- lines <$> pipeReadStrict [Param "show-ref", Param "--head"] repo + return $ any (" HEAD" `isSuffixOf`) ls + +{- Get the sha of a fully qualified git ref, if it exists. -} +sha :: Branch -> Repo -> IO (Maybe Sha) +sha branch repo = process <$> showref repo + where + showref = pipeReadStrict [Param "show-ref", + Param "--hash", -- get the hash + Param $ show branch] + process [] = Nothing + process s = Just $ Ref $ firstLine s + +{- List of (shas, branches) matching a given ref or refs. -} +matching :: [Ref] -> Repo -> IO [(Sha, Branch)] +matching refs repo = matching' (map show refs) repo + +{- Includes HEAD in the output, if asked for it. -} +matchingWithHEAD :: [Ref] -> Repo -> IO [(Sha, Branch)] +matchingWithHEAD refs repo = matching' ("--head" : map show refs) repo + +{- List of (shas, branches) matching a given ref or refs. -} +matching' :: [String] -> Repo -> IO [(Sha, Branch)] +matching' ps repo = map gen . lines <$> + pipeReadStrict (Param "show-ref" : map Param ps) repo + where + gen l = let (r, b) = separate (== ' ') l + in (Ref r, Ref b) + +{- List of (shas, branches) matching a given ref spec. + - Duplicate shas are filtered out. -} +matchingUniq :: [Ref] -> Repo -> IO [(Sha, Branch)] +matchingUniq refs repo = nubBy uniqref <$> matching refs repo + where + uniqref (a, _) (b, _) = a == b + +{- Checks if a String is a legal git ref name. + - + - The rules for this are complex; see git-check-ref-format(1) -} +legal :: Bool -> String -> Bool +legal allowonelevel s = all (== False) illegal + where + illegal = + [ any ("." `isPrefixOf`) pathbits + , any (".lock" `isSuffixOf`) pathbits + , not allowonelevel && length pathbits < 2 + , contains ".." + , any (\c -> contains [c]) illegalchars + , begins "/" + , ends "/" + , contains "//" + , ends "." + , contains "@{" + , null s + ] + contains v = v `isInfixOf` s + ends v = v `isSuffixOf` s + begins v = v `isPrefixOf` s + + pathbits = split "/" s + illegalchars = " ~^:?*[\\" ++ controlchars + controlchars = chr 0o177 : [chr 0 .. chr (0o40-1)] diff --git a/Git/Remote.hs b/Git/Remote.hs new file mode 100644 index 0000000000..5640e9ff27 --- /dev/null +++ b/Git/Remote.hs @@ -0,0 +1,33 @@ +{- git remote stuff + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Git.Remote where + +import Common +import Data.Char + +{- Construct a legal git remote name out of an arbitrary input string. + - + - There seems to be no formal definition of this in the git source, + - just some ad-hoc checks, and some other things that fail with certian + - types of names (like ones starting with '-'). + -} +makeLegalName :: String -> String +makeLegalName s = case filter legal $ replace "/" "_" s of + -- it can't be empty + [] -> "unnamed" + -- it can't start with / or - or . + '.':s' -> makeLegalName s' + '/':s' -> makeLegalName s' + '-':s' -> makeLegalName s' + s' -> s' + where + {- Only alphanumerics, and a few common bits of punctuation common + - in hostnames. -} + legal '_' = True + legal '.' = True + legal c = isAlphaNum c diff --git a/Git/Sha.hs b/Git/Sha.hs new file mode 100644 index 0000000000..ee1b6d6691 --- /dev/null +++ b/Git/Sha.hs @@ -0,0 +1,39 @@ +{- git SHA stuff + - + - Copyright 2011 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Git.Sha where + +import Common +import Git.Types + +{- Runs an action that causes a git subcommand to emit a Sha, and strips + - any trailing newline, returning the sha. -} +getSha :: String -> IO String -> IO Sha +getSha subcommand a = maybe bad return =<< extractSha <$> a + where + bad = error $ "failed to read sha from git " ++ subcommand + +{- Extracts the Sha from a string. There can be a trailing newline after + - it, but nothing else. -} +extractSha :: String -> Maybe Sha +extractSha s + | len == shaSize = val s + | len == shaSize + 1 && length s' == shaSize = val s' + | otherwise = Nothing + where + len = length s + s' = firstLine s + val v + | all (`elem` "1234567890ABCDEFabcdef") v = Just $ Ref v + | otherwise = Nothing + +{- Size of a git sha. -} +shaSize :: Int +shaSize = 40 + +nullSha :: Ref +nullSha = Ref $ replicate shaSize '0' diff --git a/Git/SharedRepository.hs b/Git/SharedRepository.hs new file mode 100644 index 0000000000..f3efa8fde9 --- /dev/null +++ b/Git/SharedRepository.hs @@ -0,0 +1,27 @@ +{- git core.sharedRepository handling + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Git.SharedRepository where + +import Data.Char + +import Common +import Git +import qualified Git.Config + +data SharedRepository = UnShared | GroupShared | AllShared | UmaskShared Int + +getSharedRepository :: Repo -> SharedRepository +getSharedRepository r = + case map toLower $ Git.Config.get "core.sharedrepository" "" r of + "1" -> GroupShared + "group" -> GroupShared + "true" -> GroupShared + "all" -> AllShared + "world" -> AllShared + "everybody" -> AllShared + v -> maybe UnShared UmaskShared (readish v) diff --git a/Git/Types.hs b/Git/Types.hs new file mode 100644 index 0000000000..4765aad6c9 --- /dev/null +++ b/Git/Types.hs @@ -0,0 +1,83 @@ +{- git data types + - + - Copyright 2010-2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Git.Types where + +import Network.URI +import qualified Data.Map as M + +{- Support repositories on local disk, and repositories accessed via an URL. + - + - Repos on local disk have a git directory, and unless bare, a worktree. + - + - A local repo may not have had its config read yet, in which case all + - that's known about it is its path. + - + - Finally, an Unknown repository may be known to exist, but nothing + - else known about it. + -} +data RepoLocation + = Local { gitdir :: FilePath, worktree :: Maybe FilePath } + | LocalUnknown FilePath + | Url URI + | Unknown + deriving (Show, Eq) + +data Repo = Repo + { location :: RepoLocation + , config :: M.Map String String + -- a given git config key can actually have multiple values + , fullconfig :: M.Map String [String] + , remotes :: [Repo] + -- remoteName holds the name used for this repo in remotes + , remoteName :: Maybe String + -- alternate environment to use when running git commands + , gitEnv :: Maybe [(String, String)] + } deriving (Show, Eq) + +{- A git ref. Can be a sha1, or a branch or tag name. -} +newtype Ref = Ref String + deriving (Eq, Ord) + +instance Show Ref where + show (Ref v) = v + +{- Aliases for Ref. -} +type Branch = Ref +type Sha = Ref +type Tag = Ref + +{- Types of objects that can be stored in git. -} +data ObjectType = BlobObject | CommitObject | TreeObject + deriving (Eq) + +instance Show ObjectType where + show BlobObject = "blob" + show CommitObject = "commit" + show TreeObject = "tree" + +readObjectType :: String -> Maybe ObjectType +readObjectType "blob" = Just BlobObject +readObjectType "commit" = Just CommitObject +readObjectType "tree" = Just TreeObject +readObjectType _ = Nothing + +{- Types of blobs. -} +data BlobType = FileBlob | ExecutableBlob | SymlinkBlob + deriving (Eq) + +{- Git uses magic numbers to denote the type of a blob. -} +instance Show BlobType where + show FileBlob = "100644" + show ExecutableBlob = "100755" + show SymlinkBlob = "120000" + +readBlobType :: String -> Maybe BlobType +readBlobType "100644" = Just FileBlob +readBlobType "100755" = Just ExecutableBlob +readBlobType "120000" = Just SymlinkBlob +readBlobType _ = Nothing diff --git a/Git/UnionMerge.hs b/Git/UnionMerge.hs new file mode 100644 index 0000000000..464200af4f --- /dev/null +++ b/Git/UnionMerge.hs @@ -0,0 +1,110 @@ +{- git-union-merge library + - + - Copyright 2011 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Git.UnionMerge ( + merge, + mergeIndex +) where + +import qualified Data.ByteString.Lazy as L +import qualified Data.Set as S + +import Common +import Git +import Git.Sha +import Git.CatFile +import Git.Command +import Git.UpdateIndex +import Git.HashObject +import Git.Types +import Git.FilePath + +{- Performs a union merge between two branches, staging it in the index. + - Any previously staged changes in the index will be lost. + - + - Should be run with a temporary index file configured by useIndex. + -} +merge :: Ref -> Ref -> Repo -> IO () +merge x y repo = do + h <- catFileStart repo + streamUpdateIndex repo + [ lsTree x repo + , mergeTrees x y h repo + ] + catFileStop h + +{- Merges a list of branches into the index. Previously staged changes in + - the index are preserved (and participate in the merge). + - + - update-index is run once per ref in turn, so that each ref is merged on + - top of the merge for the previous ref. It would be more efficient, but + - harder to calculate a single union merge involving all the refs, as well + - as the index. + -} +mergeIndex :: CatFileHandle -> Repo -> [Ref] -> IO () +mergeIndex h repo bs = forM_ bs $ \b -> + streamUpdateIndex repo [mergeTreeIndex b h repo] + +{- For merging two trees. -} +mergeTrees :: Ref -> Ref -> CatFileHandle -> Repo -> Streamer +mergeTrees (Ref x) (Ref y) h = doMerge h $ "diff-tree":diffOpts ++ [x, y] + +{- For merging a single tree into the index. -} +mergeTreeIndex :: Ref -> CatFileHandle -> Repo -> Streamer +mergeTreeIndex (Ref x) h = doMerge h $ + "diff-index" : diffOpts ++ ["--cached", x] + +diffOpts :: [String] +diffOpts = ["--raw", "-z", "-r", "--no-renames", "-l0"] + +{- Streams update-index changes to perform a merge, + - using git to get a raw diff. -} +doMerge :: CatFileHandle -> [String] -> Repo -> Streamer +doMerge ch differ repo streamer = do + (diff, cleanup) <- pipeNullSplit (map Param differ) repo + go diff + void $ cleanup + where + go [] = noop + go (info:file:rest) = mergeFile info file ch repo >>= + maybe (go rest) (\l -> streamer l >> go rest) + go (_:[]) = error $ "parse error " ++ show differ + +{- Given an info line from a git raw diff, and the filename, generates + - a line suitable for update-index that union merges the two sides of the + - diff. -} +mergeFile :: String -> FilePath -> CatFileHandle -> Repo -> IO (Maybe String) +mergeFile info file h repo = case filter (/= nullSha) [Ref asha, Ref bsha] of + [] -> return Nothing + (sha:[]) -> use sha + shas -> use + =<< either return (\s -> hashObject BlobObject (unlines s) repo) + =<< calcMerge . zip shas <$> mapM getcontents shas + where + [_colonmode, _bmode, asha, bsha, _status] = words info + use sha = return $ Just $ + updateIndexLine sha FileBlob $ asTopFilePath file + -- We don't know how the file is encoded, but need to + -- split it into lines to union merge. Using the + -- FileSystemEncoding for this is a hack, but ensures there + -- are no decoding errors. Note that this works because + -- hashObject sets fileEncoding on its write handle. + getcontents s = lines . encodeW8 . L.unpack <$> catObject h s + +{- Calculates a union merge between a list of refs, with contents. + - + - When possible, reuses the content of an existing ref, rather than + - generating new content. + -} +calcMerge :: [(Ref, [String])] -> Either Ref [String] +calcMerge shacontents + | null reuseable = Right $ new + | otherwise = Left $ fst $ Prelude.head reuseable + where + reuseable = filter (\c -> sorteduniq (snd c) == new) shacontents + new = sorteduniq $ concat $ map snd shacontents + sorteduniq = S.toList . S.fromList diff --git a/Git/UpdateIndex.hs b/Git/UpdateIndex.hs new file mode 100644 index 0000000000..5d07e20112 --- /dev/null +++ b/Git/UpdateIndex.hs @@ -0,0 +1,80 @@ +{- git-update-index library + - + - Copyright 2011-2013 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE BangPatterns, CPP #-} + +module Git.UpdateIndex ( + Streamer, + pureStreamer, + streamUpdateIndex, + lsTree, + updateIndexLine, + unstageFile, + stageSymlink +) where + +import Common +import Git +import Git.Types +import Git.Command +import Git.FilePath +import Git.Sha + +{- Streamers are passed a callback and should feed it lines in the form + - read by update-index, and generated by ls-tree. -} +type Streamer = (String -> IO ()) -> IO () + +{- A streamer with a precalculated value. -} +pureStreamer :: String -> Streamer +pureStreamer !s = \streamer -> streamer s + +{- Streams content into update-index from a list of Streamers. -} +streamUpdateIndex :: Repo -> [Streamer] -> IO () +streamUpdateIndex repo as = pipeWrite params repo $ \h -> do + fileEncoding h + forM_ as (stream h) + hClose h + where + params = map Param ["update-index", "-z", "--index-info"] + stream h a = a (streamer h) + streamer h s = do + hPutStr h s + hPutStr h "\0" + +{- A streamer that adds the current tree for a ref. Useful for eg, copying + - and modifying branches. -} +lsTree :: Ref -> Repo -> Streamer +lsTree (Ref x) repo streamer = do + (s, cleanup) <- pipeNullSplit params repo + mapM_ streamer s + void $ cleanup + where + params = map Param ["ls-tree", "-z", "-r", "--full-tree", x] + +{- Generates a line suitable to be fed into update-index, to add + - a given file with a given sha. -} +updateIndexLine :: Sha -> BlobType -> TopFilePath -> String +updateIndexLine sha filetype file = + show filetype ++ " blob " ++ show sha ++ "\t" ++ indexPath file + +{- A streamer that removes a file from the index. -} +unstageFile :: FilePath -> Repo -> IO Streamer +unstageFile file repo = do + p <- toTopFilePath file repo + return $ pureStreamer $ "0 " ++ show nullSha ++ "\t" ++ indexPath p + +{- A streamer that adds a symlink to the index. -} +stageSymlink :: FilePath -> Sha -> Repo -> IO Streamer +stageSymlink file sha repo = do + !line <- updateIndexLine + <$> pure sha + <*> pure SymlinkBlob + <*> toTopFilePath file repo + return $ pureStreamer line + +indexPath :: TopFilePath -> InternalGitPath +indexPath = toInternalGitPath . getTopFilePath diff --git a/Git/Url.hs b/Git/Url.hs new file mode 100644 index 0000000000..7befc46690 --- /dev/null +++ b/Git/Url.hs @@ -0,0 +1,70 @@ +{- git repository urls + - + - Copyright 2010, 2011 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Git.Url ( + scheme, + host, + port, + hostuser, + authority, +) where + +import Network.URI hiding (scheme, authority) + +import Common +import Git.Types +import Git + +{- Scheme of an URL repo. -} +scheme :: Repo -> String +scheme Repo { location = Url u } = uriScheme u +scheme repo = notUrl repo + +{- Work around a bug in the real uriRegName + - -} +uriRegName' :: URIAuth -> String +uriRegName' a = fixup $ uriRegName a + where + fixup x@('[':rest) + | rest !! len == ']' = take len rest + | otherwise = x + where + len = length rest - 1 + fixup x = x + +{- Hostname of an URL repo. -} +host :: Repo -> String +host = authpart uriRegName' + +{- Port of an URL repo, if it has a nonstandard one. -} +port :: Repo -> Maybe Integer +port r = + case authpart uriPort r of + ":" -> Nothing + (':':p) -> readish p + _ -> Nothing + +{- Hostname of an URL repo, including any username (ie, "user@host") -} +hostuser :: Repo -> String +hostuser r = authpart uriUserInfo r ++ authpart uriRegName' r + +{- The full authority portion an URL repo. (ie, "user@host:port") -} +authority :: Repo -> String +authority = authpart assemble + where + assemble a = uriUserInfo a ++ uriRegName' a ++ uriPort a + +{- Applies a function to extract part of the uriAuthority of an URL repo. -} +authpart :: (URIAuth -> a) -> Repo -> a +authpart a Repo { location = Url u } = a auth + where + auth = fromMaybe (error $ "bad url " ++ show u) (uriAuthority u) +authpart _ repo = notUrl repo + +notUrl :: Repo -> a +notUrl repo = error $ + "acting on local git repo " ++ repoDescribe repo ++ " not supported" diff --git a/Git/Version.hs b/Git/Version.hs new file mode 100644 index 0000000000..5ad1d59592 --- /dev/null +++ b/Git/Version.hs @@ -0,0 +1,43 @@ +{- git versions + - + - Copyright 2011, 2013 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Git.Version where + +import Common + +data GitVersion = GitVersion String Integer + deriving (Eq) + +instance Ord GitVersion where + compare (GitVersion _ x) (GitVersion _ y) = compare x y + +instance Show GitVersion where + show (GitVersion s _) = s + +installed :: IO GitVersion +installed = normalize . extract <$> readProcess "git" ["--version"] + where + extract s = case lines s of + [] -> "" + (l:_) -> unwords $ drop 2 $ words l + +{- To compare dotted versions like 1.7.7 and 1.8, they are normalized to + - a somewhat arbitrary integer representation. -} +normalize :: String -> GitVersion +normalize v = GitVersion v $ + sum $ mult 1 $ reverse $ extend precision $ take precision $ + map readi $ split "." v + where + extend n l = l ++ replicate (n - length l) 0 + mult _ [] = [] + mult n (x:xs) = (n*x) : mult (n*10^width) xs + readi :: String -> Integer + readi s = case reads s of + ((x,_):_) -> x + _ -> 0 + precision = 10 -- number of segments of the version to compare + width = length "yyyymmddhhmmss" -- maximum width of a segment diff --git a/GitAnnex.hs b/GitAnnex.hs new file mode 100644 index 0000000000..9553f22774 --- /dev/null +++ b/GitAnnex.hs @@ -0,0 +1,162 @@ +{- git-annex main program + - + - Copyright 2010 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE CPP #-} + +module GitAnnex where + +import qualified Git.CurrentRepo +import CmdLine +import Command +import GitAnnex.Options + +import qualified Command.Add +import qualified Command.Unannex +import qualified Command.Drop +import qualified Command.Move +import qualified Command.Copy +import qualified Command.Get +import qualified Command.FromKey +import qualified Command.DropKey +import qualified Command.TransferKey +#ifndef mingw32_HOST_OS +import qualified Command.TransferKeys +#endif +import qualified Command.ReKey +import qualified Command.Reinject +import qualified Command.Fix +import qualified Command.Init +import qualified Command.Describe +import qualified Command.InitRemote +import qualified Command.EnableRemote +import qualified Command.Fsck +import qualified Command.Unused +import qualified Command.DropUnused +import qualified Command.AddUnused +import qualified Command.Unlock +import qualified Command.Lock +import qualified Command.PreCommit +import qualified Command.Find +import qualified Command.Whereis +import qualified Command.Log +import qualified Command.Merge +import qualified Command.Status +import qualified Command.Migrate +import qualified Command.Uninit +import qualified Command.Trust +import qualified Command.Untrust +import qualified Command.Semitrust +import qualified Command.Dead +import qualified Command.Group +import qualified Command.Content +import qualified Command.Ungroup +import qualified Command.Vicfg +import qualified Command.Sync +import qualified Command.AddUrl +#ifdef WITH_FEED +import qualified Command.ImportFeed +#endif +import qualified Command.RmUrl +import qualified Command.Import +import qualified Command.Map +import qualified Command.Direct +import qualified Command.Indirect +import qualified Command.Upgrade +import qualified Command.Version +import qualified Command.Help +#ifdef WITH_ASSISTANT +import qualified Command.Watch +import qualified Command.Assistant +#ifdef WITH_WEBAPP +import qualified Command.WebApp +#endif +#ifdef WITH_XMPP +import qualified Command.XMPPGit +#endif +#endif +#ifdef WITH_TESTSUITE +import qualified Command.Test +import qualified Command.FuzzTest +#endif + +cmds :: [Command] +cmds = concat + [ Command.Add.def + , Command.Get.def + , Command.Drop.def + , Command.Move.def + , Command.Copy.def + , Command.Unlock.def + , Command.Lock.def + , Command.Sync.def + , Command.AddUrl.def +#ifdef WITH_FEED + , Command.ImportFeed.def +#endif + , Command.RmUrl.def + , Command.Import.def + , Command.Init.def + , Command.Describe.def + , Command.InitRemote.def + , Command.EnableRemote.def + , Command.Reinject.def + , Command.Unannex.def + , Command.Uninit.def + , Command.PreCommit.def + , Command.Trust.def + , Command.Untrust.def + , Command.Semitrust.def + , Command.Dead.def + , Command.Group.def + , Command.Content.def + , Command.Ungroup.def + , Command.Vicfg.def + , Command.FromKey.def + , Command.DropKey.def + , Command.TransferKey.def +#ifndef mingw32_HOST_OS + , Command.TransferKeys.def +#endif + , Command.ReKey.def + , Command.Fix.def + , Command.Fsck.def + , Command.Unused.def + , Command.DropUnused.def + , Command.AddUnused.def + , Command.Find.def + , Command.Whereis.def + , Command.Log.def + , Command.Merge.def + , Command.Status.def + , Command.Migrate.def + , Command.Map.def + , Command.Direct.def + , Command.Indirect.def + , Command.Upgrade.def + , Command.Version.def + , Command.Help.def +#ifdef WITH_ASSISTANT + , Command.Watch.def + , Command.Assistant.def +#ifdef WITH_WEBAPP + , Command.WebApp.def +#endif +#ifdef WITH_XMPP + , Command.XMPPGit.def +#endif +#endif +#ifdef WITH_TESTSUITE + , Command.Test.def + , Command.FuzzTest.def +#endif + ] + +header :: String +header = "git-annex command [option ...]" + +run :: [String] -> IO () +run args = dispatch True args cmds options [] header Git.CurrentRepo.get diff --git a/GitAnnex/Options.hs b/GitAnnex/Options.hs new file mode 100644 index 0000000000..2cfdfafd2c --- /dev/null +++ b/GitAnnex/Options.hs @@ -0,0 +1,67 @@ +{- git-annex options + - + - Copyright 2010, 2013 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module GitAnnex.Options where + +import System.Console.GetOpt + +import Common.Annex +import qualified Git.Config +import Command +import Types.TrustLevel +import qualified Annex +import qualified Remote +import qualified Limit +import qualified Option + +options :: [Option] +options = Option.common ++ + [ Option ['N'] ["numcopies"] (ReqArg setnumcopies paramNumber) + "override default number of copies" + , Option [] ["trust"] (trustArg Trusted) + "override trust setting" + , Option [] ["semitrust"] (trustArg SemiTrusted) + "override trust setting back to default" + , Option [] ["untrust"] (trustArg UnTrusted) + "override trust setting to untrusted" + , Option ['c'] ["config"] (ReqArg setgitconfig "NAME=VALUE") + "override git configuration setting" + , Option ['x'] ["exclude"] (ReqArg Limit.addExclude paramGlob) + "skip files matching the glob pattern" + , Option ['I'] ["include"] (ReqArg Limit.addInclude paramGlob) + "don't skip files matching the glob pattern" + , Option ['i'] ["in"] (ReqArg Limit.addIn paramRemote) + "skip files not present in a remote" + , Option ['C'] ["copies"] (ReqArg Limit.addCopies paramNumber) + "skip files with fewer copies" + , Option ['B'] ["inbackend"] (ReqArg Limit.addInBackend paramName) + "skip files not using a key-value backend" + , Option [] ["inallgroup"] (ReqArg Limit.addInAllGroup paramGroup) + "skip files not present in all remotes in a group" + , Option [] ["largerthan"] (ReqArg Limit.addLargerThan paramSize) + "skip files larger than a size" + , Option [] ["smallerthan"] (ReqArg Limit.addSmallerThan paramSize) + "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.matcher + where + setnumcopies v = maybe noop + (\n -> Annex.changeState $ \s -> s { Annex.forcenumcopies = Just n }) + (readish v) + setgitconfig v = Annex.changeGitRepo =<< inRepo (Git.Config.store v) + trustArg t = ReqArg (Remote.forceTrust t) paramRemote + +keyOptions :: [Option] +keyOptions = + [ Option ['A'] ["all"] (NoArg (Annex.setFlag "all")) + "operate on all versions of all files" + , Option ['U'] ["unused"] (NoArg (Annex.setFlag "unused")) + "operate on files found by last run of git-annex unused" + ] diff --git a/GitAnnexShell.hs b/GitAnnexShell.hs new file mode 100644 index 0000000000..6f03ac73b9 --- /dev/null +++ b/GitAnnexShell.hs @@ -0,0 +1,182 @@ +{- git-annex-shell main program + - + - Copyright 2010-2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module GitAnnexShell where + +import System.Environment +import System.Console.GetOpt + +import Common.Annex +import qualified Git.Construct +import CmdLine +import Command +import Annex.UUID +import Annex (setField) +import qualified Option +import Fields +import Utility.UserInfo + +import qualified Command.ConfigList +import qualified Command.InAnnex +import qualified Command.DropKey +import qualified Command.RecvKey +import qualified Command.SendKey +import qualified Command.TransferInfo +import qualified Command.Commit + +cmds_readonly :: [Command] +cmds_readonly = concat + [ Command.ConfigList.def + , Command.InAnnex.def + , Command.SendKey.def + , Command.TransferInfo.def + ] + +cmds_notreadonly :: [Command] +cmds_notreadonly = concat + [ Command.RecvKey.def + , Command.DropKey.def + , Command.Commit.def + ] + +cmds :: [Command] +cmds = map adddirparam $ cmds_readonly ++ cmds_notreadonly + where + adddirparam c = c { cmdparamdesc = "DIRECTORY " ++ cmdparamdesc c } + +options :: [OptDescr (Annex ())] +options = Option.common ++ + [ Option [] ["uuid"] (ReqArg checkuuid paramUUID) "local repository uuid" + ] + where + checkuuid expected = getUUID >>= check + where + check u | u == toUUID expected = noop + check NoUUID = unexpected "uninitialized repository" + check u = unexpected $ "UUID " ++ fromUUID u + unexpected s = error $ + "expected repository UUID " ++ + expected ++ " but found " ++ s + +header :: String +header = "git-annex-shell [-c] command [parameters ...] [option ...]" + +run :: [String] -> IO () +run [] = failure +-- skip leading -c options, passed by eg, ssh +run ("-c":p) = run p +-- a command can be either a builtin or something to pass to git-shell +run c@(cmd:dir:params) + | cmd `elem` builtins = builtin cmd dir params + | otherwise = external c +run c@(cmd:_) + -- Handle the case of being the user's login shell. It will be passed + -- a single string containing all the real parameters. + | "git-annex-shell " `isPrefixOf` cmd = run $ drop 1 $ shellUnEscape cmd + | cmd `elem` builtins = failure + | otherwise = external c + +builtins :: [String] +builtins = map cmdname cmds + +builtin :: String -> String -> [String] -> IO () +builtin cmd dir params = do + checkNotReadOnly cmd + checkDirectory $ Just dir + let (params', fieldparams, opts) = partitionParams params + fields = filter checkField $ parseFields fieldparams + cmds' = map (newcmd $ unwords opts) cmds + dispatch False (cmd : params') cmds' options fields header $ + Git.Construct.repoAbsPath dir >>= Git.Construct.fromAbsPath + where + addrsyncopts opts seek k = setField "RsyncOptions" opts >> seek k + newcmd opts c = c { cmdseek = map (addrsyncopts opts) (cmdseek c) } + +external :: [String] -> IO () +external params = do + {- Normal git-shell commands all have the directory as their last + - parameter. -} + let lastparam = lastMaybe =<< shellUnEscape <$> lastMaybe params + (params', _, _) = partitionParams params + checkDirectory lastparam + checkNotLimited + unlessM (boolSystem "git-shell" $ map Param $ "-c":params') $ + error "git-shell failed" + +{- Split the input list into 3 groups separated with a double dash --. + - Parameters between two -- markers are field settings, in the form: + - field=value field=value + - + - Parameters after the last -- are the command itself and its arguments e.g., + - rsync --bandwidth=100. + -} +partitionParams :: [String] -> ([String], [String], [String]) +partitionParams ps = case segment (== "--") ps of + params:fieldparams:rest -> ( params, fieldparams, intercalate ["--"] rest ) + [params] -> (params, [], []) + _ -> ([], [], []) + +parseFields :: [String] -> [(String, String)] +parseFields = map (separate (== '=')) + +{- Only allow known fields to be set, ignore others. + - Make sure that field values make sense. -} +checkField :: (String, String) -> Bool +checkField (field, value) + | field == fieldName remoteUUID = fieldCheck remoteUUID value + | field == fieldName associatedFile = fieldCheck associatedFile value + | field == fieldName direct = fieldCheck direct value + | otherwise = False + +failure :: IO () +failure = error $ "bad parameters\n\n" ++ usage header cmds + +checkNotLimited :: IO () +checkNotLimited = checkEnv "GIT_ANNEX_SHELL_LIMITED" + +checkNotReadOnly :: String -> IO () +checkNotReadOnly cmd + | cmd `elem` map cmdname cmds_readonly = noop + | otherwise = checkEnv "GIT_ANNEX_SHELL_READONLY" + +checkDirectory :: Maybe FilePath -> IO () +checkDirectory mdir = do + v <- catchMaybeIO $ getEnv "GIT_ANNEX_SHELL_DIRECTORY" + case (v, mdir) of + (Nothing, _) -> noop + (Just d, Nothing) -> req d Nothing + (Just d, Just dir) + | d `equalFilePath` dir -> noop + | otherwise -> do + home <- myHomeDir + d' <- canondir home d + dir' <- canondir home dir + if d' `equalFilePath` dir' + then noop + else req d' (Just dir') + where + req d mdir' = error $ unwords + [ "Only allowed to access" + , d + , maybe "and could not determine directory from command line" ("not " ++) mdir' + ] + + {- A directory may start with ~/ or in some cases, even /~/, + - or could just be relative to home, or of course could + - be absolute. -} + canondir home d + | "~/" `isPrefixOf` d = return d + | "/~/" `isPrefixOf` d = return $ drop 1 d + | otherwise = relHome $ absPathFrom home d + +checkEnv :: String -> IO () +checkEnv var = do + v <- catchMaybeIO $ getEnv var + case v of + Nothing -> noop + Just "" -> noop + Just _ -> error $ "Action blocked by " ++ var diff --git a/INSTALL b/INSTALL new file mode 120000 index 0000000000..67566818f0 --- /dev/null +++ b/INSTALL @@ -0,0 +1 @@ +doc/install.mdwn \ No newline at end of file diff --git a/Init.hs b/Init.hs new file mode 100644 index 0000000000..7e7e5041d0 --- /dev/null +++ b/Init.hs @@ -0,0 +1,201 @@ +{- git-annex repository initialization + - + - Copyright 2011 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE CPP #-} + +module Init ( + ensureInitialized, + isInitialized, + initialize, + uninitialize, + probeCrippledFileSystem +) where + +import Common.Annex +import Utility.Tmp +import Utility.Network +import qualified Annex +import qualified Git +import qualified Git.LsFiles +import qualified Git.Config +import qualified Annex.Branch +import Logs.UUID +import Annex.Version +import Annex.UUID +import Utility.Shell +import Config +import Annex.Direct +import Annex.Content.Direct +import Annex.Environment +import Backend +#ifndef mingw32_HOST_OS +import Utility.UserInfo +import Utility.FileMode +#endif + +genDescription :: Maybe String -> Annex String +genDescription (Just d) = return d +genDescription Nothing = do + reldir <- liftIO . relHome =<< fromRepo Git.repoPath + hostname <- fromMaybe "" <$> liftIO getHostname +#ifndef mingw32_HOST_OS + let at = if null hostname then "" else "@" + username <- liftIO myUserName + return $ concat [username, at, hostname, ":", reldir] +#else + return $ concat [hostname, ":", reldir] +#endif + +initialize :: Maybe String -> Annex () +initialize mdescription = do + prepUUID + setVersion defaultVersion + checkCrippledFileSystem + checkFifoSupport + gitPreCommitHookWrite + createInodeSentinalFile + u <- getUUID + {- This will make the first commit to git, so ensure git is set up + - properly to allow commits when running it. -} + ensureCommit $ do + Annex.Branch.create + describeUUID u =<< genDescription mdescription + +uninitialize :: Annex () +uninitialize = do + gitPreCommitHookUnWrite + removeRepoUUID + removeVersion + +{- Will automatically initialize if there is already a git-annex + - branch from somewhere. Otherwise, require a manual init + - to avoid git-annex accidentially being run in git + - repos that did not intend to use it. -} +ensureInitialized :: Annex () +ensureInitialized = getVersion >>= maybe needsinit checkVersion + where + needsinit = ifM Annex.Branch.hasSibling + ( initialize Nothing + , error "First run: git-annex init" + ) + +{- Checks if a repository is initialized. Does not check version for ugrade. -} +isInitialized :: Annex Bool +isInitialized = maybe Annex.Branch.hasSibling (const $ return True) =<< getVersion + +{- set up a git pre-commit hook, if one is not already present -} +gitPreCommitHookWrite :: Annex () +gitPreCommitHookWrite = unlessBare $ do + hook <- preCommitHook + ifM (liftIO $ doesFileExist hook) + ( do + content <- liftIO $ readFile hook + when (content /= preCommitScript) $ + warning $ "pre-commit hook (" ++ hook ++ ") already exists, not configuring" + , unlessM crippledFileSystem $ + liftIO $ do + viaTmp writeFile hook preCommitScript + p <- getPermissions hook + setPermissions hook $ p {executable = True} + ) + +gitPreCommitHookUnWrite :: Annex () +gitPreCommitHookUnWrite = unlessBare $ do + hook <- preCommitHook + whenM (liftIO $ doesFileExist hook) $ + ifM (liftIO $ (==) preCommitScript <$> readFile hook) + ( liftIO $ removeFile hook + , warning $ "pre-commit hook (" ++ hook ++ + ") contents modified; not deleting." ++ + " Edit it to remove call to git annex." + ) + +unlessBare :: Annex () -> Annex () +unlessBare = unlessM $ fromRepo Git.repoIsLocalBare + +preCommitHook :: Annex FilePath +preCommitHook = () <$> fromRepo Git.localGitDir <*> pure "hooks/pre-commit" + +preCommitScript :: String +preCommitScript = unlines + [ shebang_local + , "# automatically configured by git-annex" + , "git annex pre-commit ." + ] + +{- A crippled filesystem is one that does not allow making symlinks, + - or removing write access from files. -} +probeCrippledFileSystem :: Annex Bool +probeCrippledFileSystem = do +#ifdef mingw32_HOST_OS + return True +#else + tmp <- fromRepo gitAnnexTmpDir + let f = tmp "gaprobe" + liftIO $ do + createDirectoryIfMissing True tmp + writeFile f "" + uncrippled <- liftIO $ probe f + liftIO $ removeFile f + return $ not uncrippled + where + probe f = catchBoolIO $ do + let f2 = f ++ "2" + nukeFile f2 + createSymbolicLink f f2 + nukeFile f2 + preventWrite f + allowWrite f + return True +#endif + +checkCrippledFileSystem :: Annex () +checkCrippledFileSystem = whenM probeCrippledFileSystem $ do + warning "Detected a crippled filesystem." + setCrippledFileSystem True + + {- Normally git disables core.symlinks itself when the filesystem does + - not support them, but in Cygwin, git does support symlinks, while + - git-annex, not linking with Cygwin, does not. -} + whenM (coreSymlinks <$> Annex.getGitConfig) $ do + warning "Disabling core.symlinks." + setConfig (ConfigKey "core.symlinks") + (Git.Config.boolConfig False) + + unlessBare $ do + unlessM isDirect $ do + warning "Enabling direct mode." + top <- fromRepo Git.repoPath + (l, clean) <- inRepo $ Git.LsFiles.inRepo [top] + forM_ l $ \f -> + maybe noop (`toDirect` f) =<< isAnnexLink f + void $ liftIO clean + setDirect True + setVersion directModeVersion + +probeFifoSupport :: Annex Bool +probeFifoSupport = do +#ifdef mingw32_HOST_OS + return False +#else + tmp <- fromRepo gitAnnexTmpDir + let f = tmp "gaprobe" + liftIO $ do + createDirectoryIfMissing True tmp + nukeFile f + ms <- tryIO $ do + createNamedPipe f ownerReadMode + getFileStatus f + nukeFile f + return $ either (const False) isNamedPipe ms +#endif + +checkFifoSupport :: Annex () +checkFifoSupport = unlessM probeFifoSupport $ do + warning "Detected a filesystem without fifo support." + warning "Disabling ssh connection caching." + setConfig (annexConfig "sshcaching") (Git.Config.boolConfig False) diff --git a/Limit.hs b/Limit.hs new file mode 100644 index 0000000000..71dbce1687 --- /dev/null +++ b/Limit.hs @@ -0,0 +1,253 @@ +{- user-specified limits on files to act on + - + - Copyright 2011-2013 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE CPP #-} + +module Limit where + +import Data.Time.Clock.POSIX +import qualified Data.Set as S +import qualified Data.Map as M +import System.Path.WildMatch +import System.PosixCompat.Files + +import Common.Annex +import qualified Annex +import qualified Utility.Matcher +import qualified Remote +import qualified Backend +import Annex.Content +import Annex.UUID +import Logs.Trust +import Types.TrustLevel +import Types.Key +import Types.Group +import Types.FileMatcher +import Logs.Group +import Utility.HumanTime +import Utility.DataUnits + +#ifdef WITH_TDFA +import Text.Regex.TDFA +import Text.Regex.TDFA.String +#else +#ifndef mingw32_HOST_OS +import System.Path.WildMatch +import Types.FileMatcher +#endif +#endif + +type MatchFiles = AssumeNotPresent -> FileInfo -> Annex Bool +type MkLimit = String -> Either String MatchFiles +type AssumeNotPresent = S.Set UUID + +{- Checks if there are user-specified limits. -} +limited :: Annex Bool +limited = (not . Utility.Matcher.isEmpty) <$> getMatcher' + +{- Gets a matcher for the user-specified limits. The matcher is cached for + - speed; once it's obtained the user-specified limits can't change. -} +getMatcher :: Annex (FileInfo -> Annex Bool) +getMatcher = Utility.Matcher.matchM <$> getMatcher' + +getMatcher' :: Annex (Utility.Matcher.Matcher (FileInfo -> Annex Bool)) +getMatcher' = do + m <- Annex.getState Annex.limit + case m of + Right r -> return r + Left l -> do + let matcher = Utility.Matcher.generate (reverse l) + Annex.changeState $ \s -> + s { Annex.limit = Right matcher } + return matcher + +{- Adds something to the limit list, which is built up reversed. -} +add :: Utility.Matcher.Token (FileInfo -> Annex Bool) -> Annex () +add l = Annex.changeState $ \s -> s { Annex.limit = prepend $ Annex.limit s } + where + prepend (Left ls) = Left $ l:ls + prepend _ = error "internal" + +{- Adds a new token. -} +addToken :: String -> Annex () +addToken = add . Utility.Matcher.token + +{- Adds a new limit. -} +addLimit :: Either String MatchFiles -> Annex () +addLimit = either error (\l -> add $ Utility.Matcher.Operation $ l S.empty) + +{- Add a limit to skip files that do not match the glob. -} +addInclude :: String -> Annex () +addInclude = addLimit . limitInclude + +limitInclude :: MkLimit +limitInclude glob = Right $ const $ return . matchglob glob + +{- Add a limit to skip files that match the glob. -} +addExclude :: String -> Annex () +addExclude = addLimit . limitExclude + +limitExclude :: MkLimit +limitExclude glob = Right $ const $ return . not . matchglob glob + +{- Could just use wildCheckCase, but this way the regex is only compiled + - once. Also, we use regex-TDFA when available, because it's less buggy + - in its support of non-unicode characters. -} +matchglob :: String -> FileInfo -> Bool +matchglob glob fi = +#ifdef WITH_TDFA + case cregex of + Right r -> case execute r (matchFile fi) of + Right (Just _) -> True + _ -> False + Left _ -> error $ "failed to compile regex: " ++ regex + where + cregex = compile defaultCompOpt defaultExecOpt regex + regex = '^':wildToRegex glob +#else + wildCheckCase glob (matchFile fi) +#endif + +{- Adds a limit to skip files not believed to be present + - in a specfied repository. -} +addIn :: String -> Annex () +addIn = addLimit . limitIn + +limitIn :: MkLimit +limitIn name = Right $ \notpresent -> check $ + if name == "." + then inhere notpresent + else inremote notpresent + where + check a = lookupFile >=> handle a + handle _ Nothing = return False + handle a (Just (key, _)) = a key + inremote notpresent key = do + u <- Remote.nameToUUID name + us <- Remote.keyLocations key + return $ u `elem` us && u `S.notMember` notpresent + inhere notpresent key + | S.null notpresent = inAnnex key + | otherwise = do + u <- getUUID + if u `S.member` notpresent + then return False + else inAnnex key + +{- Limit to content that is currently present on a uuid. -} +limitPresent :: Maybe UUID -> MkLimit +limitPresent u _ = Right $ const $ check $ \key -> do + hereu <- getUUID + if u == Just hereu || isNothing u + then inAnnex key + else do + us <- Remote.keyLocations key + return $ maybe False (`elem` us) u + where + check a = lookupFile >=> handle a + handle _ Nothing = return False + handle a (Just (key, _)) = a key + +{- Limit to content that is in a directory, anywhere in the repository tree -} +limitInDir :: FilePath -> MkLimit +limitInDir dir = const $ Right $ const $ \fi -> return $ + any (== dir) $ splitPath $ takeDirectory $ matchFile fi + +{- Adds a limit to skip files not believed to have the specified number + - of copies. -} +addCopies :: String -> Annex () +addCopies = addLimit . limitCopies + +limitCopies :: MkLimit +limitCopies want = case split ":" want of + [v, n] -> case parsetrustspec v of + Just checker -> go n $ checktrust checker + Nothing -> go n $ checkgroup v + [n] -> go n $ const $ return True + _ -> Left "bad value for copies" + where + go num good = case readish num of + Nothing -> Left "bad number for copies" + Just n -> Right $ \notpresent f -> + lookupFile f >>= handle n good notpresent + handle _ _ _ Nothing = return False + handle n good notpresent (Just (key, _)) = do + us <- filter (`S.notMember` notpresent) + <$> (filterM good =<< Remote.keyLocations key) + return $ length us >= n + checktrust checker u = checker <$> lookupTrust u + checkgroup g u = S.member g <$> lookupGroups u + parsetrustspec s + | "+" `isSuffixOf` s = (>=) <$> readTrustLevel (beginning s) + | otherwise = (==) <$> readTrustLevel s + +{- Adds a limit to skip files not believed to be present in all + - repositories in the specified group. -} +addInAllGroup :: String -> Annex () +addInAllGroup groupname = do + m <- groupMap + addLimit $ limitInAllGroup m groupname + +limitInAllGroup :: GroupMap -> MkLimit +limitInAllGroup m groupname + | S.null want = Right $ const $ const $ return True + | otherwise = Right $ \notpresent -> lookupFile >=> check notpresent + where + want = fromMaybe S.empty $ M.lookup groupname $ uuidsByGroup m + check _ Nothing = return False + check notpresent (Just (key, _)) + -- optimisation: Check if a wanted uuid is notpresent. + | not (S.null (S.intersection want notpresent)) = return False + | otherwise = do + present <- S.fromList <$> Remote.keyLocations key + return $ S.null $ want `S.difference` present + +{- Adds a limit to skip files not using a specified key-value backend. -} +addInBackend :: String -> Annex () +addInBackend = addLimit . limitInBackend + +limitInBackend :: MkLimit +limitInBackend name = Right $ const $ lookupFile >=> check + where + wanted = Backend.lookupBackendName name + check = return . maybe False ((==) wanted . snd) + +{- Adds a limit to skip files that are too large or too small -} +addLargerThan :: String -> Annex () +addLargerThan = addLimit . limitSize (>) + +addSmallerThan :: String -> Annex () +addSmallerThan = addLimit . limitSize (<) + +limitSize :: (Maybe Integer -> Maybe Integer -> Bool) -> MkLimit +limitSize vs s = case readSize dataUnits s of + Nothing -> Left "bad size" + Just sz -> Right $ go sz + where + go sz _ fi = lookupFile fi >>= check fi sz + check _ sz (Just (key, _)) = return $ keySize key `vs` Just sz + check fi sz Nothing = do + filesize <- liftIO $ catchMaybeIO $ + fromIntegral . fileSize + <$> getFileStatus (relFile fi) + return $ filesize `vs` Just sz + +addTimeLimit :: String -> Annex () +addTimeLimit s = do + let seconds = fromMaybe (error "bad time-limit") $ parseDuration s + start <- liftIO getPOSIXTime + let cutoff = start + seconds + addLimit $ Right $ const $ const $ do + now <- liftIO getPOSIXTime + if now > cutoff + then do + warning $ "Time limit (" ++ s ++ ") reached!" + liftIO $ exitWith $ ExitFailure 101 + else return True + +lookupFile :: FileInfo -> Annex (Maybe (Key, Backend)) +lookupFile = Backend.lookupFile . relFile diff --git a/Locations.hs b/Locations.hs new file mode 100644 index 0000000000..1cbbb9886a --- /dev/null +++ b/Locations.hs @@ -0,0 +1,358 @@ +{- git-annex file locations + - + - Copyright 2010-2011 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Locations ( + keyFile, + fileKey, + keyPaths, + keyPath, + gitAnnexLocation, + gitAnnexLink, + gitAnnexMapping, + gitAnnexInodeCache, + gitAnnexInodeSentinal, + gitAnnexInodeSentinalCache, + annexLocations, + annexLocation, + gitAnnexDir, + gitAnnexObjectDir, + gitAnnexTmpDir, + gitAnnexTmpLocation, + gitAnnexBadDir, + gitAnnexBadLocation, + gitAnnexUnusedLog, + gitAnnexFsckState, + gitAnnexTransferDir, + gitAnnexCredsDir, + gitAnnexFeedStateDir, + gitAnnexFeedState, + gitAnnexMergeDir, + gitAnnexJournalDir, + gitAnnexJournalLock, + gitAnnexIndex, + gitAnnexIndexLock, + gitAnnexPidFile, + gitAnnexDaemonStatusFile, + gitAnnexLogFile, + gitAnnexFuzzTestLogFile, + gitAnnexHtmlShim, + gitAnnexUrlFile, + gitAnnexTmpCfgFile, + gitAnnexSshDir, + gitAnnexRemotesDir, + gitAnnexAssistantDefaultDir, + isLinkToAnnex, + annexHashes, + hashDirMixed, + hashDirLower, + + prop_idempotent_fileKey +) where + +import Data.Bits +import Data.Word +import Data.Hash.MD5 + +import Common +import Types +import Types.Key +import qualified Git + +{- Conventions: + - + - Functions ending in "Dir" should always return values ending with a + - trailing path separator. Most code does not rely on that, but a few + - things do. + - + - Everything else should not end in a trailing path sepatator. + - + - Only functions (with names starting with "git") that build a path + - based on a git repository should return an absolute path. + - Everything else should use relative paths. + -} + +{- The directory git annex uses for local state, relative to the .git + - directory -} +annexDir :: FilePath +annexDir = addTrailingPathSeparator "annex" + +{- The directory git annex uses for locally available object content, + - relative to the .git directory -} +objectDir :: FilePath +objectDir = addTrailingPathSeparator $ annexDir "objects" + +{- Annexed file's possible locations relative to the .git directory. + - There are two different possibilities, using different hashes. -} +annexLocations :: Key -> [FilePath] +annexLocations key = map (annexLocation key) annexHashes +annexLocation :: Key -> Hasher -> FilePath +annexLocation key hasher = objectDir keyPath key hasher + +{- Annexed object's absolute location in a repository. + - + - When there are multiple possible locations, returns the one where the + - file is actually present. + - + - When the file is not present, returns the location where the file should + - be stored. + - + - This does not take direct mode into account, so in direct mode it is not + - the actual location of the file's content. + -} +gitAnnexLocation :: Key -> Git.Repo -> GitConfig -> IO FilePath +gitAnnexLocation key r config = gitAnnexLocation' key r (annexCrippledFileSystem config) +gitAnnexLocation' :: Key -> Git.Repo -> Bool -> IO FilePath +gitAnnexLocation' key r crippled + {- Bare repositories default to hashDirLower for new + - content, as it's more portable. + - + - Repositories on filesystems that are crippled also use + - hashDirLower, since they do not use symlinks and it's + - more portable. -} + | Git.repoIsLocalBare r || crippled = + check $ map inrepo $ annexLocations key + {- Non-bare repositories only use hashDirMixed, so + - don't need to do any work to check if the file is + - present. -} + | otherwise = return $ inrepo $ annexLocation key hashDirMixed + where + inrepo d = Git.localGitDir r d + check locs@(l:_) = fromMaybe l <$> firstM doesFileExist locs + check [] = error "internal" + +{- Calculates a symlink to link a file to an annexed object. -} +gitAnnexLink :: FilePath -> Key -> Git.Repo -> IO FilePath +gitAnnexLink file key r = do + cwd <- getCurrentDirectory + let absfile = fromMaybe whoops $ absNormPath cwd file + loc <- gitAnnexLocation' key r False + return $ relPathDirToFile (parentDir absfile) loc + where + whoops = error $ "unable to normalize " ++ file + +{- File that maps from a key to the file(s) in the git repository. + - Used in direct mode. -} +gitAnnexMapping :: Key -> Git.Repo -> GitConfig -> IO FilePath +gitAnnexMapping key r config = do + loc <- gitAnnexLocation key r config + return $ loc ++ ".map" + +{- File that caches information about a key's content, used to determine + - if a file has changed. + - Used in direct mode. -} +gitAnnexInodeCache :: Key -> Git.Repo -> GitConfig -> IO FilePath +gitAnnexInodeCache key r config = do + loc <- gitAnnexLocation key r config + return $ loc ++ ".cache" + +gitAnnexInodeSentinal :: Git.Repo -> FilePath +gitAnnexInodeSentinal r = gitAnnexDir r "sentinal" + +gitAnnexInodeSentinalCache :: Git.Repo -> FilePath +gitAnnexInodeSentinalCache r = gitAnnexInodeSentinal r ++ ".cache" + +{- The annex directory of a repository. -} +gitAnnexDir :: Git.Repo -> FilePath +gitAnnexDir r = addTrailingPathSeparator $ Git.localGitDir r annexDir + +{- The part of the annex directory where file contents are stored. -} +gitAnnexObjectDir :: Git.Repo -> FilePath +gitAnnexObjectDir r = addTrailingPathSeparator $ Git.localGitDir r objectDir + +{- .git/annex/tmp/ is used for temp files -} +gitAnnexTmpDir :: Git.Repo -> FilePath +gitAnnexTmpDir r = addTrailingPathSeparator $ gitAnnexDir r "tmp" + +{- The temp file to use for a given key's content. -} +gitAnnexTmpLocation :: Key -> Git.Repo -> FilePath +gitAnnexTmpLocation key r = gitAnnexTmpDir r keyFile key + +{- .git/annex/bad/ is used for bad files found during fsck -} +gitAnnexBadDir :: Git.Repo -> FilePath +gitAnnexBadDir r = addTrailingPathSeparator $ gitAnnexDir r "bad" + +{- The bad file to use for a given key. -} +gitAnnexBadLocation :: Key -> Git.Repo -> FilePath +gitAnnexBadLocation key r = gitAnnexBadDir r keyFile key + +{- .git/annex/foounused is used to number possibly unused keys -} +gitAnnexUnusedLog :: FilePath -> Git.Repo -> FilePath +gitAnnexUnusedLog prefix r = gitAnnexDir r (prefix ++ "unused") + +{- .git/annex/fsckstate is used to store information about incremental fscks. -} +gitAnnexFsckState :: Git.Repo -> FilePath +gitAnnexFsckState r = gitAnnexDir r "fsckstate" + +{- .git/annex/creds/ is used to store credentials to access some special + - remotes. -} +gitAnnexCredsDir :: Git.Repo -> FilePath +gitAnnexCredsDir r = addTrailingPathSeparator $ gitAnnexDir r "creds" + +{- .git/annex/feeds/ is used to record per-key (url) state by importfeeds -} +gitAnnexFeedStateDir :: Git.Repo -> FilePath +gitAnnexFeedStateDir r = addTrailingPathSeparator $ gitAnnexDir r "feedstate" + +gitAnnexFeedState :: Key -> Git.Repo -> FilePath +gitAnnexFeedState k r = gitAnnexFeedStateDir r keyFile k + +{- .git/annex/merge/ is used for direct mode merges. -} +gitAnnexMergeDir :: Git.Repo -> FilePath +gitAnnexMergeDir r = addTrailingPathSeparator $ gitAnnexDir r "merge" + +{- .git/annex/transfer/ is used to record keys currently + - being transferred, and other transfer bookkeeping info. -} +gitAnnexTransferDir :: Git.Repo -> FilePath +gitAnnexTransferDir r = addTrailingPathSeparator $ gitAnnexDir r "transfer" + +{- .git/annex/journal/ is used to journal changes made to the git-annex + - branch -} +gitAnnexJournalDir :: Git.Repo -> FilePath +gitAnnexJournalDir r = addTrailingPathSeparator $ gitAnnexDir r "journal" + +{- Lock file for the journal. -} +gitAnnexJournalLock :: Git.Repo -> FilePath +gitAnnexJournalLock r = gitAnnexDir r "journal.lck" + +{- .git/annex/index is used to stage changes to the git-annex branch -} +gitAnnexIndex :: Git.Repo -> FilePath +gitAnnexIndex r = gitAnnexDir r "index" + +{- Lock file for .git/annex/index. -} +gitAnnexIndexLock :: Git.Repo -> FilePath +gitAnnexIndexLock r = gitAnnexDir r "index.lck" + +{- Pid file for daemon mode. -} +gitAnnexPidFile :: Git.Repo -> FilePath +gitAnnexPidFile r = gitAnnexDir r "daemon.pid" + +{- Status file for daemon mode. -} +gitAnnexDaemonStatusFile :: Git.Repo -> FilePath +gitAnnexDaemonStatusFile r = gitAnnexDir r "daemon.status" + +{- Log file for daemon mode. -} +gitAnnexLogFile :: Git.Repo -> FilePath +gitAnnexLogFile r = gitAnnexDir r "daemon.log" + +{- Log file for fuzz test. -} +gitAnnexFuzzTestLogFile :: Git.Repo -> FilePath +gitAnnexFuzzTestLogFile r = gitAnnexDir r "fuzztest.log" + +{- Html shim file used to launch the webapp. -} +gitAnnexHtmlShim :: Git.Repo -> FilePath +gitAnnexHtmlShim r = gitAnnexDir r "webapp.html" + +{- File containing the url to the webapp. -} +gitAnnexUrlFile :: Git.Repo -> FilePath +gitAnnexUrlFile r = gitAnnexDir r "url" + +{- Temporary file used to edit configuriation from the git-annex branch. -} +gitAnnexTmpCfgFile :: Git.Repo -> FilePath +gitAnnexTmpCfgFile r = gitAnnexDir r "config.tmp" + +{- .git/annex/ssh/ is used for ssh connection caching -} +gitAnnexSshDir :: Git.Repo -> FilePath +gitAnnexSshDir r = addTrailingPathSeparator $ gitAnnexDir r "ssh" + +{- .git/annex/remotes/ is used for remote-specific state. -} +gitAnnexRemotesDir :: Git.Repo -> FilePath +gitAnnexRemotesDir r = addTrailingPathSeparator $ gitAnnexDir r "remotes" + +{- This is the base directory name used by the assistant when making + - repositories, by default. -} +gitAnnexAssistantDefaultDir :: FilePath +gitAnnexAssistantDefaultDir = "annex" + +{- Checks a symlink target to see if it appears to point to annexed content. + - + - We only look at paths inside the .git directory, and not at the .git + - directory itself, because GIT_DIR may cause a directory name other + - than .git to be used. + -} +isLinkToAnnex :: FilePath -> Bool +isLinkToAnnex s = (pathSeparator:objectDir) `isInfixOf` s + +{- Converts a key into a filename fragment without any directory. + - + - Escape "/" in the key name, to keep a flat tree of files and avoid + - issues with keys containing "/../" or ending with "/" etc. + - + - "/" is escaped to "%" because it's short and rarely used, and resembles + - a slash + - "%" is escaped to "&s", and "&" to "&a"; this ensures that the mapping + - is one to one. + - ":" is escaped to "&c", because despite it being 2011, people still care + - about FAT. + -} +keyFile :: Key -> FilePath +keyFile key = replace "/" "%" $ replace ":" "&c" $ + replace "%" "&s" $ replace "&" "&a" $ key2file key + +{- A location to store a key on the filesystem. A directory hash is used, + - to protect against filesystems that dislike having many items in a + - single directory. + - + - The file is put in a directory with the same name, this allows + - write-protecting the directory to avoid accidental deletion of the file. + -} +keyPath :: Key -> Hasher -> FilePath +keyPath key hasher = hasher key f f + where + f = keyFile key + +{- All possibile locations to store a key using different directory hashes. -} +keyPaths :: Key -> [FilePath] +keyPaths key = map (keyPath key) annexHashes + +{- Reverses keyFile, converting a filename fragment (ie, the basename of + - the symlink target) into a key. -} +fileKey :: FilePath -> Maybe Key +fileKey file = file2key $ + replace "&a" "&" $ replace "&s" "%" $ + replace "&c" ":" $ replace "%" "/" file + +{- for quickcheck -} +prop_idempotent_fileKey :: String -> Bool +prop_idempotent_fileKey s = Just k == fileKey (keyFile k) + where + k = stubKey { keyName = s, keyBackendName = "test" } + +{- Two different directory hashes may be used. The mixed case hash + - came first, and is fine, except for the problem of case-strict + - filesystems such as Linux VFAT (mounted with shortname=mixed), + - which do not allow using a directory "XX" when "xx" already exists. + - To support that, most repositories use the lower case hash for new data. -} +type Hasher = Key -> FilePath +annexHashes :: [Hasher] +annexHashes = [hashDirLower, hashDirMixed] + +hashDirMixed :: Hasher +hashDirMixed k = addTrailingPathSeparator $ take 2 dir drop 2 dir + where + dir = take 4 $ display_32bits_as_dir =<< [a,b,c,d] + ABCD (a,b,c,d) = md5 $ md5FilePath $ key2file k + +hashDirLower :: Hasher +hashDirLower k = addTrailingPathSeparator $ take 3 dir drop 3 dir + where + dir = take 6 $ md5s $ md5FilePath $ key2file k + +{- modified version of display_32bits_as_hex from Data.Hash.MD5 + - Copyright (C) 2001 Ian Lynagh + - License: Either BSD or GPL + -} +display_32bits_as_dir :: Word32 -> String +display_32bits_as_dir w = trim $ swap_pairs cs + where + -- Need 32 characters to use. To avoid inaverdently making + -- a real word, use letters that appear less frequently. + chars = ['0'..'9'] ++ "zqjxkmvwgpfZQJXKMVWGPF" + cs = map (\x -> getc $ (shiftR w (6*x)) .&. 31) [0..7] + getc n = chars !! fromIntegral n + swap_pairs (x1:x2:xs) = x2:x1:swap_pairs xs + swap_pairs _ = [] + -- Last 2 will always be 00, so omit. + trim = take 6 diff --git a/Logs/Group.hs b/Logs/Group.hs new file mode 100644 index 0000000000..ee3b75b860 --- /dev/null +++ b/Logs/Group.hs @@ -0,0 +1,86 @@ +{- git-annex group log + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Logs.Group ( + groupLog, + groupChange, + groupSet, + lookupGroups, + groupMap, + groupMapLoad, + getStandardGroup, + inUnwantedGroup +) where + +import qualified Data.Map as M +import qualified Data.Set as S +import Data.Time.Clock.POSIX + +import Common.Annex +import qualified Annex.Branch +import qualified Annex +import Logs.UUIDBased +import Types.Group +import Types.StandardGroups + +{- Filename of group.log. -} +groupLog :: FilePath +groupLog = "group.log" + +{- Returns the groups of a given repo UUID. -} +lookupGroups :: UUID -> Annex (S.Set Group) +lookupGroups u = (fromMaybe S.empty . M.lookup u) . groupsByUUID <$> groupMap + +{- Applies a set modifier to change the groups for a uuid in the groupLog. -} +groupChange :: UUID -> (S.Set Group -> S.Set Group) -> Annex () +groupChange uuid@(UUID _) modifier = do + curr <- lookupGroups uuid + ts <- liftIO getPOSIXTime + Annex.Branch.change groupLog $ + showLog (unwords . S.toList) . + changeLog ts uuid (modifier curr) . + parseLog (Just . S.fromList . words) + + -- The changed group invalidates the preferred content cache. + Annex.changeState $ \s -> s + { Annex.groupmap = Nothing + , Annex.preferredcontentmap = Nothing + } +groupChange NoUUID _ = error "unknown UUID; cannot modify" + +groupSet :: UUID -> S.Set Group -> Annex () +groupSet u g = groupChange u (const g) + +{- The map is cached for speed. -} +groupMap :: Annex GroupMap +groupMap = maybe groupMapLoad return =<< Annex.getState Annex.groupmap + +{- Loads the map, updating the cache. -} +groupMapLoad :: Annex GroupMap +groupMapLoad = do + m <- makeGroupMap . simpleMap . + parseLog (Just . S.fromList . words) <$> + Annex.Branch.get groupLog + Annex.changeState $ \s -> s { Annex.groupmap = Just m } + return m + +makeGroupMap :: M.Map UUID (S.Set Group) -> GroupMap +makeGroupMap byuuid = GroupMap byuuid bygroup + where + bygroup = M.fromListWith S.union $ + concatMap explode $ M.toList byuuid + explode (u, s) = map (\g -> (g, S.singleton u)) (S.toList s) + +{- If a repository is in exactly one standard group, returns it. -} +getStandardGroup :: S.Set Group -> Maybe StandardGroup +getStandardGroup s = case mapMaybe toStandardGroup $ S.toList s of + [g] -> Just g + _ -> Nothing + +inUnwantedGroup :: UUID -> Annex Bool +inUnwantedGroup u = elem UnwantedGroup + . mapMaybe toStandardGroup . S.toList <$> lookupGroups u diff --git a/Logs/Location.hs b/Logs/Location.hs new file mode 100644 index 0000000000..0f57b66634 --- /dev/null +++ b/Logs/Location.hs @@ -0,0 +1,76 @@ +{-# LANGUAGE BangPatterns #-} + +{- git-annex location log + - + - git-annex keeps track of which repositories have the contents of annexed + - files. + - + - Repositories record their UUID and the date when they --get or --drop + - a value. + - + - Copyright 2010-2011 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Logs.Location ( + LogStatus(..), + logStatus, + logChange, + loggedLocations, + loggedKeys, + loggedKeysFor, + logFile, + logFileKey +) where + +import Common.Annex +import qualified Annex.Branch +import Logs.Presence +import Annex.UUID + +{- Log a change in the presence of a key's value in current repository. -} +logStatus :: Key -> LogStatus -> Annex () +logStatus key status = do + u <- getUUID + logChange key u status + +{- Log a change in the presence of a key's value in a repository. -} +logChange :: Key -> UUID -> LogStatus -> Annex () +logChange key (UUID u) s = addLog (logFile key) =<< logNow s u +logChange _ NoUUID _ = noop + +{- Returns a list of repository UUIDs that, according to the log, have + - the value of a key. + -} +loggedLocations :: Key -> Annex [UUID] +loggedLocations key = map toUUID <$> (currentLog . logFile) key + +{- Finds all keys that have location log information. + - (There may be duplicate keys in the list.) -} +loggedKeys :: Annex [Key] +loggedKeys = mapMaybe (logFileKey . takeFileName) <$> Annex.Branch.files + +{- Finds all keys that have location log information indicating + - they are present for the specified repository. -} +loggedKeysFor :: UUID -> Annex [Key] +loggedKeysFor u = filterM isthere =<< loggedKeys + where + {- This should run strictly to avoid the filterM + - building many thunks containing keyLocations data. -} + isthere k = do + us <- loggedLocations k + let !there = u `elem` us + return there + +{- The filename of the log file for a given key. -} +logFile :: Key -> String +logFile key = hashDirLower key ++ keyFile key ++ ".log" + +{- Converts a log filename into a key. -} +logFileKey :: FilePath -> Maybe Key +logFileKey file + | ext == ".log" = fileKey base + | otherwise = Nothing + where + (base, ext) = splitAt (length file - 4) file diff --git a/Logs/PreferredContent.hs b/Logs/PreferredContent.hs new file mode 100644 index 0000000000..8005fc0d30 --- /dev/null +++ b/Logs/PreferredContent.hs @@ -0,0 +1,117 @@ +{- git-annex preferred content matcher configuration + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Logs.PreferredContent ( + preferredContentLog, + preferredContentSet, + isPreferredContent, + preferredContentMap, + preferredContentMapLoad, + preferredContentMapRaw, + checkPreferredContentExpression, + setStandardGroup, +) where + +import qualified Data.Map as M +import qualified Data.Set as S +import Data.Either +import Data.Time.Clock.POSIX + +import Common.Annex +import qualified Annex.Branch +import qualified Annex +import Logs.UUIDBased +import Limit +import qualified Utility.Matcher +import Annex.FileMatcher +import Annex.UUID +import Types.Group +import Types.Remote (RemoteConfig) +import Logs.Group +import Logs.Remote +import Types.StandardGroups + +{- Filename of preferred-content.log. -} +preferredContentLog :: FilePath +preferredContentLog = "preferred-content.log" + +{- Changes the preferred content configuration of a remote. -} +preferredContentSet :: UUID -> String -> Annex () +preferredContentSet uuid@(UUID _) val = do + ts <- liftIO getPOSIXTime + Annex.Branch.change preferredContentLog $ + showLog id . changeLog ts uuid val . parseLog Just + Annex.changeState $ \s -> s { Annex.preferredcontentmap = Nothing } +preferredContentSet NoUUID _ = error "unknown UUID; cannot modify" + +{- Checks if a file is preferred content for the specified repository + - (or the current repository if none is specified). -} +isPreferredContent :: Maybe UUID -> AssumeNotPresent -> FilePath -> Bool -> Annex Bool +isPreferredContent mu notpresent file def = do + u <- maybe getUUID return mu + m <- preferredContentMap + case M.lookup u m of + Nothing -> return def + Just matcher -> checkFileMatcher' matcher file notpresent def + +{- The map is cached for speed. -} +preferredContentMap :: Annex Annex.PreferredContentMap +preferredContentMap = maybe preferredContentMapLoad return + =<< Annex.getState Annex.preferredcontentmap + +{- Loads the map, updating the cache. -} +preferredContentMapLoad :: Annex Annex.PreferredContentMap +preferredContentMapLoad = do + groupmap <- groupMap + configmap <- readRemoteLog + m <- simpleMap + . parseLogWithUUID ((Just .) . makeMatcher groupmap configmap) + <$> Annex.Branch.get preferredContentLog + Annex.changeState $ \s -> s { Annex.preferredcontentmap = Just m } + return m + +preferredContentMapRaw :: Annex (M.Map UUID String) +preferredContentMapRaw = simpleMap . parseLog Just + <$> Annex.Branch.get preferredContentLog + +{- This intentionally never fails, even on unparsable expressions, + - because the configuration is shared amoung repositories and newer + - versions of git-annex may add new features. Instead, parse errors + - result in a Matcher that will always succeed. -} +makeMatcher :: GroupMap -> M.Map UUID RemoteConfig -> UUID -> String -> FileMatcher +makeMatcher groupmap configmap u expr + | expr == "standard" = standardMatcher groupmap configmap u + | null (lefts tokens) = Utility.Matcher.generate $ rights tokens + | otherwise = matchAll + where + tokens = exprParser groupmap configmap (Just u) expr + +{- Standard matchers are pre-defined for some groups. If none is defined, + - or a repository is in multiple groups with standard matchers, match all. -} +standardMatcher :: GroupMap -> M.Map UUID RemoteConfig -> UUID -> FileMatcher +standardMatcher groupmap configmap u = + maybe matchAll (makeMatcher groupmap configmap u . preferredContent) $ + getStandardGroup =<< u `M.lookup` groupsByUUID groupmap + +{- Checks if an expression can be parsed, if not returns Just error -} +checkPreferredContentExpression :: String -> Maybe String +checkPreferredContentExpression expr + | expr == "standard" = Nothing + | otherwise = case parsedToMatcher tokens of + Left e -> Just e + Right _ -> Nothing + where + tokens = exprParser emptyGroupMap M.empty Nothing expr + +{- Puts a UUID in a standard group, and sets its preferred content to use + - the standard expression for that group, unless something is already set. -} +setStandardGroup :: UUID -> StandardGroup -> Annex () +setStandardGroup u g = do + groupSet u $ S.singleton $ fromStandardGroup g + m <- preferredContentMap + unless (isJust $ M.lookup u m) $ + preferredContentSet u "standard" diff --git a/Logs/Presence.hs b/Logs/Presence.hs new file mode 100644 index 0000000000..ec5cec209a --- /dev/null +++ b/Logs/Presence.hs @@ -0,0 +1,122 @@ +{- git-annex presence log + - + - This is used to store presence information in the git-annex branch in + - a way that can be union merged. + - + - A line of the log will look like: "date N INFO" + - Where N=1 when the INFO is present, and 0 otherwise. + - + - Copyright 2010-2011 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Logs.Presence ( + LogStatus(..), + LogLine(LogLine), + addLog, + readLog, + getLog, + parseLog, + showLog, + logNow, + compactLog, + currentLog, + prop_parse_show_log, +) where + +import Data.Time.Clock.POSIX +import Data.Time +import System.Locale +import qualified Data.Map as M + +import Common.Annex +import qualified Annex.Branch +import Utility.QuickCheck + +data LogLine = LogLine { + date :: POSIXTime, + status :: LogStatus, + info :: String +} deriving (Eq, Show) + +data LogStatus = InfoPresent | InfoMissing + deriving (Eq, Show, Bounded, Enum) + +addLog :: FilePath -> LogLine -> Annex () +addLog file line = Annex.Branch.change file $ \s -> + showLog $ compactLog (line : parseLog s) + +{- Reads a log file. + - Note that the LogLines returned may be in any order. -} +readLog :: FilePath -> Annex [LogLine] +readLog = parseLog <$$> Annex.Branch.get + +{- Parses a log file. Unparseable lines are ignored. -} +parseLog :: String -> [LogLine] +parseLog = mapMaybe parseline . lines + where + parseline l = LogLine + <$> (utcTimeToPOSIXSeconds <$> parseTime defaultTimeLocale "%s%Qs" d) + <*> parsestatus s + <*> pure rest + where + (d, pastd) = separate (== ' ') l + (s, rest) = separate (== ' ') pastd + parsestatus "1" = Just InfoPresent + parsestatus "0" = Just InfoMissing + parsestatus _ = Nothing + +{- Generates a log file. -} +showLog :: [LogLine] -> String +showLog = unlines . map genline + where + genline (LogLine d s i) = unwords [show d, genstatus s, i] + genstatus InfoPresent = "1" + genstatus InfoMissing = "0" + +{- Generates a new LogLine with the current date. -} +logNow :: LogStatus -> String -> Annex LogLine +logNow s i = do + now <- liftIO getPOSIXTime + return $ LogLine now s i + +{- Reads a log and returns only the info that is still in effect. -} +currentLog :: FilePath -> Annex [String] +currentLog file = map info . filterPresent <$> readLog file + +{- Given a log, returns only the info that is are still in effect. -} +getLog :: String -> [String] +getLog = map info . filterPresent . parseLog + +{- Returns the info from LogLines that are in effect. -} +filterPresent :: [LogLine] -> [LogLine] +filterPresent = filter (\l -> InfoPresent == status l) . compactLog + +{- Compacts a set of logs, returning a subset that contains the current + - status. -} +compactLog :: [LogLine] -> [LogLine] +compactLog = M.elems . foldr mapLog M.empty + +type LogMap = M.Map String LogLine + +{- Inserts a log into a map of logs, if the log has better (ie, newer) + - information than the other logs in the map -} +mapLog :: LogLine -> LogMap -> LogMap +mapLog l m + | better = M.insert i l m + | otherwise = m + where + 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/Remote.hs b/Logs/Remote.hs new file mode 100644 index 0000000000..89792b0545 --- /dev/null +++ b/Logs/Remote.hs @@ -0,0 +1,100 @@ +{- git-annex remote log + - + - Copyright 2011 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Logs.Remote ( + remoteLog, + readRemoteLog, + configSet, + keyValToConfig, + configToKeyVal, + showConfig, + parseConfig, + + prop_idempotent_configEscape, + prop_parse_show_Config, +) where + +import qualified Data.Map as M +import Data.Time.Clock.POSIX +import Data.Char + +import Common.Annex +import qualified Annex.Branch +import Types.Remote +import Logs.UUIDBased + +{- Filename of remote.log. -} +remoteLog :: FilePath +remoteLog = "remote.log" + +{- Adds or updates a remote's config in the log. -} +configSet :: UUID -> RemoteConfig -> Annex () +configSet u c = do + ts <- liftIO getPOSIXTime + Annex.Branch.change remoteLog $ + showLog showConfig . changeLog ts u c . parseLog parseConfig + +{- Map of remotes by uuid containing key/value config maps. -} +readRemoteLog :: Annex (M.Map UUID RemoteConfig) +readRemoteLog = simpleMap . parseLog parseConfig <$> Annex.Branch.get remoteLog + +parseConfig :: String -> Maybe RemoteConfig +parseConfig = Just . keyValToConfig . words + +showConfig :: RemoteConfig -> String +showConfig = unwords . configToKeyVal + +{- Given Strings like "key=value", generates a RemoteConfig. -} +keyValToConfig :: [String] -> RemoteConfig +keyValToConfig ws = M.fromList $ map (/=/) ws + where + (/=/) s = (k, v) + where + k = takeWhile (/= '=') s + v = configUnEscape $ drop (1 + length k) s + +configToKeyVal :: M.Map String String -> [String] +configToKeyVal m = map toword $ sort $ M.toList m + where + toword (k, v) = k ++ "=" ++ configEscape v + +configEscape :: String -> String +configEscape = concatMap escape + where + escape c + | isSpace c || c `elem` "&" = "&" ++ show (ord c) ++ ";" + | otherwise = [c] + +configUnEscape :: String -> String +configUnEscape = unescape + where + unescape [] = [] + unescape (c:rest) + | c == '&' = entity rest + | otherwise = c : unescape rest + entity s + | not (null num) && ";" `isPrefixOf` r = + chr (Prelude.read num) : unescape rest + | otherwise = + '&' : unescape s + where + num = takeWhile isNumber s + r = drop (length num) s + rest = drop 1 r + +{- for quickcheck -} +prop_idempotent_configEscape :: String -> Bool +prop_idempotent_configEscape s = s == (configUnEscape . configEscape) s + +prop_parse_show_Config :: RemoteConfig -> Bool +prop_parse_show_Config c + -- whitespace and '=' are not supported in keys + | any (\k -> any isSpace k || elem '=' k) (M.keys c) = True + | otherwise = parseConfig (showConfig c) ~~ Just c + where + normalize v = sort . M.toList <$> v + a ~~ b = normalize a == normalize b diff --git a/Logs/Transfer.hs b/Logs/Transfer.hs new file mode 100644 index 0000000000..13f94ea20d --- /dev/null +++ b/Logs/Transfer.hs @@ -0,0 +1,388 @@ +{- git-annex transfer information files and lock files + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE CPP #-} + +module Logs.Transfer where + +import Common.Annex +import Annex.Perms +import Annex.Exception +import qualified Git +import Types.Key +import Utility.Metered +import Utility.Percentage +import Utility.QuickCheck + +import System.Posix.Types +import Data.Time.Clock +import Data.Time.Clock.POSIX +import Data.Time +import System.Locale +import Control.Concurrent + +{- Enough information to uniquely identify a transfer, used as the filename + - of the transfer information file. -} +data Transfer = Transfer + { transferDirection :: Direction + , transferUUID :: UUID + , transferKey :: Key + } + deriving (Eq, Ord, Read, Show) + +{- Information about a Transfer, stored in the transfer information file. + - + - Note that the associatedFile may not correspond to a file in the local + - git repository. It's some file, possibly relative to some directory, + - of some repository, that was acted on to initiate the transfer. + -} +data TransferInfo = TransferInfo + { startedTime :: Maybe POSIXTime + , transferPid :: Maybe ProcessID + , transferTid :: Maybe ThreadId + , transferRemote :: Maybe Remote + , bytesComplete :: Maybe Integer + , associatedFile :: Maybe FilePath + , transferPaused :: Bool + } + deriving (Show, Eq, Ord) + +stubTransferInfo :: TransferInfo +stubTransferInfo = TransferInfo Nothing Nothing Nothing Nothing Nothing Nothing False + +data Direction = Upload | Download + deriving (Eq, Ord, Read, Show) + +showLcDirection :: Direction -> String +showLcDirection Upload = "upload" +showLcDirection Download = "download" + +readLcDirection :: String -> Maybe Direction +readLcDirection "upload" = Just Upload +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) + , show $ bytesComplete info + ] + +{- Transfers that will accomplish the same task. -} +equivilantTransfer :: Transfer -> Transfer -> Bool +equivilantTransfer t1 t2 + | transferDirection t1 == Download && transferDirection t2 == Download && + transferKey t1 == transferKey t2 = True + | otherwise = t1 == t2 + +percentComplete :: Transfer -> TransferInfo -> Maybe Percentage +percentComplete (Transfer { transferKey = key }) info = + percentage <$> keySize key <*> Just (fromMaybe 0 $ bytesComplete info) + +type RetryDecider = TransferInfo -> TransferInfo -> Bool + +noRetry :: RetryDecider +noRetry _ _ = False + +{- Retries a transfer when it fails, as long as the failed transfer managed + - to send some data. -} +forwardRetry :: RetryDecider +forwardRetry old new = bytesComplete old < bytesComplete new + +upload :: UUID -> Key -> AssociatedFile -> RetryDecider -> (MeterUpdate -> Annex Bool) -> Annex Bool +upload u key = runTransfer (Transfer Upload u key) + +download :: UUID -> Key -> AssociatedFile -> RetryDecider -> (MeterUpdate -> Annex Bool) -> Annex Bool +download u key = runTransfer (Transfer Download u key) + +{- Runs a transfer action. Creates and locks the lock file while the + - action is running, and stores info in the transfer information + - file. + - + - If the transfer action returns False, the transfer info is + - left in the failedTransferDir. + - + - If the transfer is already in progress, returns False. + - + - An upload can be run from a read-only filesystem, and in this case + - no transfer information or lock file is used. + -} +runTransfer :: Transfer -> Maybe FilePath -> RetryDecider -> (MeterUpdate -> Annex Bool) -> Annex Bool +runTransfer t file shouldretry a = do + info <- liftIO $ startTransferInfo file + (meter, tfile, metervar) <- mkProgressUpdater t info + mode <- annexFileMode + (fd, inprogress) <- liftIO $ prep tfile mode info + if inprogress + then do + showNote "transfer already in progress" + return False + else do + ok <- retry info metervar $ + bracketIO (return fd) (cleanup tfile) (const $ a meter) + unless ok $ recordFailedTransfer t info + return ok + where +#ifndef mingw32_HOST_OS + prep tfile mode info = do + mfd <- catchMaybeIO $ + openFd (transferLockFile tfile) ReadWrite (Just mode) + defaultFileFlags { trunc = True } + case mfd of + Nothing -> return (mfd, False) + Just fd -> do + locked <- catchMaybeIO $ + setLock fd (WriteLock, AbsoluteSeek, 0, 0) + if isNothing locked + then return (Nothing, True) + else do + void $ tryIO $ writeTransferInfoFile info tfile + return (mfd, False) +#else + prep tfile _mode info = do + mfd <- catchMaybeIO $ do + writeFile (transferLockFile tfile) "" + writeTransferInfoFile info tfile + return (mfd, False) +#endif + cleanup _ Nothing = noop + cleanup tfile (Just fd) = do + void $ tryIO $ removeFile tfile + void $ tryIO $ removeFile $ transferLockFile tfile +#ifndef mingw32_HOST_OS + closeFd fd +#endif + retry oldinfo metervar run = do + v <- tryAnnex run + case v of + Right b -> return b + Left _ -> do + b <- getbytescomplete metervar + let newinfo = oldinfo { bytesComplete = Just b } + if shouldretry oldinfo newinfo + then retry newinfo metervar run + else return False + getbytescomplete metervar + | transferDirection t == Upload = + liftIO $ readMVar metervar + | otherwise = do + f <- fromRepo $ gitAnnexTmpLocation (transferKey t) + liftIO $ catchDefaultIO 0 $ + fromIntegral . fileSize <$> getFileStatus f + +{- Generates a callback that can be called as transfer progresses to update + - the transfer info file. Also returns the file it'll be updating, and a + - MVar that can be used to read the number of bytesComplete. -} +mkProgressUpdater :: Transfer -> TransferInfo -> Annex (MeterUpdate, FilePath, MVar Integer) +mkProgressUpdater t info = do + tfile <- fromRepo $ transferFile t + _ <- tryAnnex $ createAnnexDirectory $ takeDirectory tfile + mvar <- liftIO $ newMVar 0 + return (liftIO . updater tfile mvar, tfile, mvar) + where + updater tfile mvar b = modifyMVar_ mvar $ \oldbytes -> do + let newbytes = fromBytesProcessed b + if newbytes - oldbytes >= mindelta + then do + let info' = info { bytesComplete = Just newbytes } + _ <- tryIO $ writeTransferInfoFile info' tfile + return newbytes + else return oldbytes + {- The minimum change in bytesComplete that is worth + - updating a transfer info file for is 1% of the total + - keySize, rounded down. -} + mindelta = case keySize (transferKey t) of + Just sz -> sz `div` 100 + Nothing -> 100 * 1024 -- arbitrarily, 100 kb + +startTransferInfo :: Maybe FilePath -> IO TransferInfo +startTransferInfo file = TransferInfo + <$> (Just . utcTimeToPOSIXSeconds <$> getCurrentTime) + <*> pure Nothing -- pid not stored in file, so omitted for speed + <*> pure Nothing -- tid ditto + <*> pure Nothing -- not 0; transfer may be resuming + <*> pure Nothing + <*> pure file + <*> pure False + +{- If a transfer is still running, returns its TransferInfo. -} +checkTransfer :: Transfer -> Annex (Maybe TransferInfo) +checkTransfer t = do + tfile <- fromRepo $ transferFile t +#ifndef mingw32_HOST_OS + mode <- annexFileMode + mfd <- liftIO $ catchMaybeIO $ + openFd (transferLockFile tfile) ReadOnly (Just mode) defaultFileFlags + case mfd of + Nothing -> return Nothing -- failed to open file; not running + Just fd -> do + locked <- liftIO $ + getLock fd (WriteLock, AbsoluteSeek, 0, 0) + liftIO $ closeFd fd + case locked of + Nothing -> return Nothing + Just (pid, _) -> liftIO $ catchDefaultIO Nothing $ + readTransferInfoFile (Just pid) tfile +#else + ifM (liftIO $ doesFileExist $ transferLockFile tfile) + ( liftIO $ catchDefaultIO Nothing $ + readTransferInfoFile Nothing tfile + , return Nothing + ) +#endif + +{- Gets all currently running transfers. -} +getTransfers :: Annex [(Transfer, TransferInfo)] +getTransfers = do + transfers <- mapMaybe parseTransferFile . concat <$> findfiles + infos <- mapM checkTransfer transfers + return $ map (\(t, Just i) -> (t, i)) $ + filter running $ zip transfers infos + where + findfiles = liftIO . mapM dirContentsRecursive + =<< mapM (fromRepo . transferDir) [Download, Upload] + running (_, i) = isJust i + +{- Gets failed transfers for a given remote UUID. -} +getFailedTransfers :: UUID -> Annex [(Transfer, TransferInfo)] +getFailedTransfers u = catMaybes <$> (liftIO . getpairs =<< concat <$> findfiles) + where + getpairs = mapM $ \f -> do + let mt = parseTransferFile f + mi <- readTransferInfoFile Nothing f + return $ case (mt, mi) of + (Just t, Just i) -> Just (t, i) + _ -> Nothing + findfiles = liftIO . mapM dirContentsRecursive + =<< mapM (fromRepo . failedTransferDir u) [Download, Upload] + +removeFailedTransfer :: Transfer -> Annex () +removeFailedTransfer t = do + f <- fromRepo $ failedTransferFile t + liftIO $ void $ tryIO $ removeFile f + +recordFailedTransfer :: Transfer -> TransferInfo -> Annex () +recordFailedTransfer t info = do + failedtfile <- fromRepo $ failedTransferFile t + createAnnexDirectory $ takeDirectory failedtfile + liftIO $ writeTransferInfoFile info failedtfile + +{- The transfer information file to use for a given Transfer. -} +transferFile :: Transfer -> Git.Repo -> FilePath +transferFile (Transfer direction u key) r = transferDir direction r + filter (/= '/') (fromUUID u) + keyFile key + +{- The transfer information file to use to record a failed Transfer -} +failedTransferFile :: Transfer -> Git.Repo -> FilePath +failedTransferFile (Transfer direction u key) r = failedTransferDir u direction r + keyFile key + +{- The transfer lock file corresponding to a given transfer info file. -} +transferLockFile :: FilePath -> FilePath +transferLockFile infofile = let (d,f) = splitFileName infofile in + combine d ("lck." ++ f) + +{- Parses a transfer information filename to a Transfer. -} +parseTransferFile :: FilePath -> Maybe Transfer +parseTransferFile file + | "lck." `isPrefixOf` takeFileName file = Nothing + | otherwise = case drop (length bits - 3) bits of + [direction, u, key] -> Transfer + <$> readLcDirection direction + <*> pure (toUUID u) + <*> fileKey key + _ -> Nothing + where + bits = splitDirectories file + +writeTransferInfoFile :: TransferInfo -> FilePath -> IO () +writeTransferInfoFile info tfile = do + h <- openFile tfile WriteMode + fileEncoding h + hPutStr h $ writeTransferInfo info + hClose h + +{- File format is a header line containing the startedTime and any + - bytesComplete value. Followed by a newline and the associatedFile. + - + - The transferPid is not included; instead it is obtained by looking + - at the process that locks the file. + -} +writeTransferInfo :: TransferInfo -> String +writeTransferInfo info = unlines + [ (maybe "" show $ startedTime info) ++ + (maybe "" (\b -> ' ' : show b) (bytesComplete info)) + , fromMaybe "" $ associatedFile info -- comes last; arbitrary content + ] + +readTransferInfoFile :: Maybe ProcessID -> FilePath -> IO (Maybe TransferInfo) +readTransferInfoFile mpid tfile = catchDefaultIO Nothing $ do + h <- openFile tfile ReadMode + fileEncoding h + hClose h `after` (readTransferInfo mpid <$> hGetContentsStrict h) + +readTransferInfo :: Maybe ProcessID -> String -> Maybe TransferInfo +readTransferInfo mpid s = TransferInfo + <$> time + <*> pure mpid + <*> pure Nothing + <*> pure Nothing + <*> bytes + <*> pure (if null filename then Nothing else Just filename) + <*> pure False + where + (firstline, rest) = separate (== '\n') s + filename + | end rest == "\n" = beginning rest + | otherwise = rest + bits = split " " firstline + numbits = length bits + time = if numbits > 0 + then Just <$> parsePOSIXTime =<< headMaybe bits + else pure Nothing -- not failure + bytes = if numbits > 1 + then Just <$> readish =<< headMaybe (drop 1 bits) + else pure Nothing -- not failure + +parsePOSIXTime :: String -> Maybe POSIXTime +parsePOSIXTime s = utcTimeToPOSIXSeconds + <$> parseTime defaultTimeLocale "%s%Qs" s + +{- The directory holding transfer information files for a given Direction. -} +transferDir :: Direction -> Git.Repo -> FilePath +transferDir direction r = gitAnnexTransferDir r showLcDirection direction + +{- The directory holding failed transfer information files for a given + - Direction and UUID -} +failedTransferDir :: UUID -> Direction -> Git.Repo -> FilePath +failedTransferDir u direction r = gitAnnexTransferDir r + "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 + | isJust (transferRemote info) = True -- remote not stored + | isJust (transferTid info) = True -- tid not stored + | otherwise = Just (info { transferPaused = False }) == info' + where + info' = readTransferInfo (transferPid info) (writeTransferInfo info) + diff --git a/Logs/Trust.hs b/Logs/Trust.hs new file mode 100644 index 0000000000..eb6e42ad7e --- /dev/null +++ b/Logs/Trust.hs @@ -0,0 +1,122 @@ +{- git-annex trust log + - + - Copyright 2010-2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Logs.Trust ( + trustLog, + TrustLevel(..), + trustGet, + trustMap, + trustSet, + trustPartition, + trustExclude, + lookupTrust, + trustMapLoad, + trustMapRaw, + + prop_parse_show_TrustLog, +) where + +import qualified Data.Map as M +import Data.Time.Clock.POSIX + +import Common.Annex +import Types.TrustLevel +import qualified Annex.Branch +import qualified Annex +import Logs.UUIDBased +import Remote.List +import qualified Types.Remote + +{- Filename of trust.log. -} +trustLog :: FilePath +trustLog = "trust.log" + +{- Returns a list of UUIDs that the trustLog indicates have the + - specified trust level. + - Note that the list can be incomplete for SemiTrusted, since that's + - the default. -} +trustGet :: TrustLevel -> Annex [UUID] +trustGet level = M.keys . M.filter (== level) <$> trustMap + +{- Changes the trust level for a uuid in the trustLog. -} +trustSet :: UUID -> TrustLevel -> Annex () +trustSet uuid@(UUID _) level = do + ts <- liftIO getPOSIXTime + Annex.Branch.change trustLog $ + showLog showTrustLog . + changeLog ts uuid level . + parseLog (Just . parseTrustLog) + Annex.changeState $ \s -> s { Annex.trustmap = Nothing } +trustSet NoUUID _ = error "unknown UUID; cannot modify" + +{- Returns the TrustLevel of a given repo UUID. -} +lookupTrust :: UUID -> Annex TrustLevel +lookupTrust u = (fromMaybe SemiTrusted . M.lookup u) <$> trustMap + +{- Partitions a list of UUIDs to those matching a TrustLevel and not. -} +trustPartition :: TrustLevel -> [UUID] -> Annex ([UUID], [UUID]) +trustPartition level ls + | level == SemiTrusted = do + t <- trustGet Trusted + u <- trustGet UnTrusted + d <- trustGet DeadTrusted + let uncandidates = t ++ u ++ d + return $ partition (`notElem` uncandidates) ls + | otherwise = do + candidates <- trustGet level + return $ partition (`elem` candidates) ls + +{- Filters UUIDs to those not matching a TrustLevel. -} +trustExclude :: TrustLevel -> [UUID] -> Annex [UUID] +trustExclude level ls = snd <$> trustPartition level ls + +{- trustLog in a map, overridden with any values from forcetrust or + - the git config. The map is cached for speed. -} +trustMap :: Annex TrustMap +trustMap = maybe trustMapLoad return =<< Annex.getState Annex.trustmap + +{- Loads the map, updating the cache, -} +trustMapLoad :: Annex TrustMap +trustMapLoad = do + overrides <- Annex.getState Annex.forcetrust + logged <- trustMapRaw + configured <- M.fromList . catMaybes + <$> (map configuredtrust <$> remoteList) + let m = M.union overrides $ M.union configured logged + Annex.changeState $ \s -> s { Annex.trustmap = Just m } + return m + where + configuredtrust r = (\l -> Just (Types.Remote.uuid r, l)) + =<< readTrustLevel + =<< remoteAnnexTrustLevel (Types.Remote.gitconfig r) + +{- Does not include forcetrust or git config values, just those from the + - log file. -} +trustMapRaw :: Annex TrustMap +trustMapRaw = simpleMap . parseLog (Just . parseTrustLog) + <$> Annex.Branch.get trustLog + +{- The trust.log used to only list trusted repos, without a field for the + - trust status, which is why this defaults to Trusted. -} +parseTrustLog :: String -> TrustLevel +parseTrustLog s = maybe Trusted parse $ headMaybe $ words s + where + parse "1" = Trusted + parse "0" = UnTrusted + parse "X" = DeadTrusted + parse _ = SemiTrusted + +showTrustLog :: TrustLevel -> String +showTrustLog Trusted = "1" +showTrustLog UnTrusted = "0" +showTrustLog DeadTrusted = "X" +showTrustLog SemiTrusted = "?" + +prop_parse_show_TrustLog :: Bool +prop_parse_show_TrustLog = all check [minBound .. maxBound] + where + check l = parseTrustLog (showTrustLog l) == l diff --git a/Logs/UUID.hs b/Logs/UUID.hs new file mode 100644 index 0000000000..2f24a388e4 --- /dev/null +++ b/Logs/UUID.hs @@ -0,0 +1,99 @@ +{- git-annex uuids + - + - Each git repository used by git-annex has an annex.uuid setting that + - uniquely identifies that repository. + - + - UUIDs of remotes are cached in git config, using keys named + - remote..annex-uuid + - + - uuid.log stores a list of known uuids, and their descriptions. + - + - Copyright 2010-2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Logs.UUID ( + uuidLog, + describeUUID, + recordUUID, + uuidMap, + uuidMapLoad +) where + +import qualified Data.Map as M +import Data.Time.Clock.POSIX + +import Types.UUID +import Common.Annex +import qualified Annex +import qualified Annex.Branch +import Logs.UUIDBased +import qualified Annex.UUID + +{- Filename of uuid.log. -} +uuidLog :: FilePath +uuidLog = "uuid.log" + +{- Records a description for a uuid in the log. -} +describeUUID :: UUID -> String -> Annex () +describeUUID uuid desc = do + ts <- liftIO getPOSIXTime + Annex.Branch.change uuidLog $ + showLog id . changeLog ts uuid desc . fixBadUUID . parseLog Just + +{- Temporarily here to fix badly formatted uuid logs generated by + - versions 3.20111105 and 3.20111025. + - + - Those logs contain entries with the UUID and description flipped. + - Due to parsing, if the description is multiword, only the first + - will be taken to be the UUID. So, if the UUID of an entry does + - not look like a UUID, and the last word of the description does, + - flip them back. + -} +fixBadUUID :: Log String -> Log String +fixBadUUID = M.fromList . map fixup . M.toList + where + fixup (k, v) + | isbad = (fixeduuid, LogEntry (Date $ newertime v) fixedvalue) + | otherwise = (k, v) + where + kuuid = fromUUID k + isbad = not (isuuid kuuid) && isuuid lastword + ws = words $ value v + lastword = Prelude.last ws + fixeduuid = toUUID lastword + fixedvalue = unwords $ kuuid: Prelude.init ws + -- For the fixed line to take precidence, it should be + -- slightly newer, but only slightly. + newertime (LogEntry (Date d) _) = d + minimumPOSIXTimeSlice + newertime (LogEntry Unknown _) = minimumPOSIXTimeSlice + minimumPOSIXTimeSlice = 0.000001 + isuuid s = length s == 36 && length (split "-" s) == 5 + +{- Records the uuid in the log, if it's not already there. -} +recordUUID :: UUID -> Annex () +recordUUID u = go . M.lookup u =<< uuidMap + where + go (Just "") = set + go Nothing = set + go _ = noop + set = describeUUID u "" + +{- The map is cached for speed. -} +uuidMap :: Annex UUIDMap +uuidMap = maybe uuidMapLoad return =<< Annex.getState Annex.uuidmap + +{- Read the uuidLog into a simple Map. + - + - The UUID of the current repository is included explicitly, since + - it may not have been described and so otherwise would not appear. -} +uuidMapLoad :: Annex UUIDMap +uuidMapLoad = do + m <- (simpleMap . parseLog Just) <$> Annex.Branch.get uuidLog + u <- Annex.UUID.getUUID + let m' = M.insertWith' preferold u "" m + Annex.changeState $ \s -> s { Annex.uuidmap = Just m' } + return m' + where + preferold = flip const diff --git a/Logs/UUIDBased.hs b/Logs/UUIDBased.hs new file mode 100644 index 0000000000..c1901eef7f --- /dev/null +++ b/Logs/UUIDBased.hs @@ -0,0 +1,114 @@ +{- git-annex uuid-based logs + - + - This is used to store information about a UUID in a way that can + - be union merged. + - + - A line of the log will look like: "UUID[ INFO[ timestamp=foo]]" + - The timestamp is last for backwards compatability reasons, + - and may not be present on old log lines. + - + - Copyright 2011 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Logs.UUIDBased ( + Log, + LogEntry(..), + TimeStamp(..), + parseLog, + parseLogWithUUID, + showLog, + changeLog, + addLog, + simpleMap, + + prop_TimeStamp_sane, + prop_addLog_sane, +) where + +import qualified Data.Map as M +import Data.Time.Clock.POSIX +import Data.Time +import System.Locale + +import Common +import Types.UUID + +data TimeStamp = Unknown | Date POSIXTime + deriving (Eq, Ord, Show) + +data LogEntry a = LogEntry + { changed :: TimeStamp + , value :: a + } deriving (Eq, Show) + +type Log a = M.Map UUID (LogEntry a) + +tskey :: String +tskey = "timestamp=" + +showLog :: (a -> String) -> Log a -> String +showLog shower = unlines . map showpair . M.toList + where + showpair (k, LogEntry (Date p) v) = + unwords [fromUUID k, shower v, tskey ++ show p] + showpair (k, LogEntry Unknown v) = + unwords [fromUUID k, shower v] + +parseLog :: (String -> Maybe a) -> String -> Log a +parseLog = parseLogWithUUID . const + +parseLogWithUUID :: (UUID -> String -> Maybe a) -> String -> Log a +parseLogWithUUID parser = M.fromListWith best . mapMaybe parse . lines + where + parse line + | null ws = Nothing + | otherwise = parser u (unwords info) >>= makepair + where + makepair v = Just (u, LogEntry ts v) + ws = words line + u = toUUID $ Prelude.head ws + t = Prelude.last ws + ts + | tskey `isPrefixOf` t = + pdate $ drop 1 $ dropWhile (/= '=') t + | otherwise = Unknown + info + | ts == Unknown = drop 1 ws + | otherwise = drop 1 $ beginning ws + pdate s = case parseTime defaultTimeLocale "%s%Qs" s of + Nothing -> Unknown + Just d -> Date $ utcTimeToPOSIXSeconds d + +changeLog :: POSIXTime -> UUID -> a -> Log a -> Log a +changeLog t u v = M.insert u $ LogEntry (Date t) v + +{- Only add an LogEntry if it's newer (or at least as new as) than any + - existing LogEntry for a UUID. -} +addLog :: UUID -> LogEntry a -> Log a -> Log a +addLog = M.insertWith' best + +{- Converts a Log into a simple Map without the timestamp information. + - This is a one-way trip, but useful for code that never needs to change + - the log. -} +simpleMap :: Log a -> M.Map UUID a +simpleMap = M.map value + +best :: LogEntry a -> LogEntry a -> LogEntry a +best new old + | changed old > changed new = old + | otherwise = new + +-- Unknown is oldest. +prop_TimeStamp_sane :: Bool +prop_TimeStamp_sane = Unknown < Date 1 + +prop_addLog_sane :: Bool +prop_addLog_sane = newWins && newestWins + where + newWins = addLog (UUID "foo") (LogEntry (Date 1) "new") l == l2 + newestWins = addLog (UUID "foo") (LogEntry (Date 1) "newest") l2 /= l2 + + l = M.fromList [(UUID "foo", LogEntry (Date 0) "old")] + l2 = M.fromList [(UUID "foo", LogEntry (Date 1) "new")] diff --git a/Logs/Unused.hs b/Logs/Unused.hs new file mode 100644 index 0000000000..4de5bc17a3 --- /dev/null +++ b/Logs/Unused.hs @@ -0,0 +1,45 @@ +{- git-annex unused log file + - + - Copyright 2010,2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Logs.Unused ( + UnusedMap, + writeUnusedLog, + readUnusedLog, + unusedKeys, +) where + +import qualified Data.Map as M + +import Common.Annex +import Types.Key +import Utility.Tmp + +type UnusedMap = M.Map Int Key + +writeUnusedLog :: FilePath -> [(Int, Key)] -> Annex () +writeUnusedLog prefix l = do + logfile <- fromRepo $ gitAnnexUnusedLog prefix + liftIO $ viaTmp writeFile logfile $ + unlines $ map (\(n, k) -> show n ++ " " ++ key2file k) l + +readUnusedLog :: FilePath -> Annex UnusedMap +readUnusedLog prefix = do + f <- fromRepo $ gitAnnexUnusedLog prefix + ifM (liftIO $ doesFileExist f) + ( M.fromList . mapMaybe parse . lines + <$> liftIO (readFile f) + , return M.empty + ) + where + parse line = case (readish tag, file2key rest) of + (Just num, Just key) -> Just (num, key) + _ -> Nothing + where + (tag, rest) = separate (== ' ') line + +unusedKeys :: Annex [Key] +unusedKeys = M.elems <$> readUnusedLog "" diff --git a/Logs/Web.hs b/Logs/Web.hs new file mode 100644 index 0000000000..cbce7a36e7 --- /dev/null +++ b/Logs/Web.hs @@ -0,0 +1,103 @@ +{- Web url logs. + - + - Copyright 2011, 2013 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Logs.Web ( + URLString, + webUUID, + getUrls, + setUrlPresent, + setUrlMissing, + urlLog, + urlLogKey, + knownUrls +) where + +import qualified Data.ByteString.Lazy.Char8 as L + +import Common.Annex +import Logs.Presence +import Logs.Location +import Types.Key +import qualified Annex.Branch +import Annex.CatFile +import qualified Git +import qualified Git.LsFiles + +type URLString = String + +-- Dummy uuid for the whole web. Do not alter. +webUUID :: UUID +webUUID = UUID "00000000-0000-0000-0000-000000000001" + +urlLogExt :: String +urlLogExt = ".log.web" + +urlLog :: Key -> FilePath +urlLog key = hashDirLower key keyFile key ++ urlLogExt + +{- Converts a url log file into a key. + - (Does not work on oldurlLogs.) -} +urlLogKey :: FilePath -> Maybe Key +urlLogKey file + | ext == urlLogExt = fileKey base + | otherwise = Nothing + where + (base, ext) = splitAt (length file - extlen) file + extlen = length urlLogExt + +isUrlLog :: FilePath -> Bool +isUrlLog file = urlLogExt `isSuffixOf` file + +{- Used to store the urls elsewhere. -} +oldurlLogs :: Key -> [FilePath] +oldurlLogs key = + [ "remote/web" hashDirLower key key2file key ++ ".log" + , "remote/web" hashDirLower key keyFile key ++ ".log" + ] + +{- Gets all urls that a key might be available from. -} +getUrls :: Key -> Annex [URLString] +getUrls key = go $ urlLog key : oldurlLogs key + where + go [] = return [] + go (l:ls) = do + us <- currentLog l + if null us + then go ls + else return us + +setUrlPresent :: Key -> URLString -> Annex () +setUrlPresent key url = do + us <- getUrls key + unless (url `elem` us) $ do + addLog (urlLog key) =<< logNow InfoPresent url + -- update location log to indicate that the web has the key + logChange key webUUID InfoPresent + +setUrlMissing :: Key -> URLString -> Annex () +setUrlMissing key url = do + addLog (urlLog key) =<< logNow InfoMissing url + whenM (null <$> getUrls key) $ + logChange key webUUID InfoMissing + +{- Finds all known urls. -} +knownUrls :: Annex [URLString] +knownUrls = do + {- Ensure the git-annex branch's index file is up-to-date and + - any journaled changes are reflected in it, since we're going + - to query its index directly. -} + Annex.Branch.update + Annex.Branch.commit "update" + Annex.Branch.withIndex $ do + top <- fromRepo Git.repoPath + (l, cleanup) <- inRepo $ Git.LsFiles.stagedDetails [top] + r <- mapM (geturls . snd) $ filter (isUrlLog . fst) l + void $ liftIO cleanup + return $ concat r + where + geturls Nothing = return [] + geturls (Just logsha) = getLog . L.unpack <$> catObject logsha diff --git a/Makefile b/Makefile new file mode 100644 index 0000000000..4d3542f13b --- /dev/null +++ b/Makefile @@ -0,0 +1,216 @@ +mans=git-annex.1 git-annex-shell.1 +all=git-annex $(mans) docs + +GHC?=ghc +GHCMAKE=$(GHC) $(GHCFLAGS) --make +PREFIX=/usr +CABAL?=cabal # set to "./Setup" if you lack a cabal program + +# Am I typing :make in vim? Do a fast build. +ifdef VIM +all=fast +endif + +build: build-stamp +build-stamp: $(all) + touch $@ + +Build/SysConfig.hs: configure.hs Build/TestConfig.hs Build/Configure.hs + if [ "$(CABAL)" = ./Setup ]; then ghc --make Setup; fi + $(CABAL) configure + +git-annex: Build/SysConfig.hs + $(CABAL) build + ln -sf dist/build/git-annex/git-annex git-annex + +git-annex.1: doc/git-annex.mdwn + ./Build/mdwn2man git-annex 1 doc/git-annex.mdwn > git-annex.1 +git-annex-shell.1: doc/git-annex-shell.mdwn + ./Build/mdwn2man git-annex-shell 1 doc/git-annex-shell.mdwn > git-annex-shell.1 + +git-union-merge.1: doc/git-union-merge.mdwn + ./Build/mdwn2man git-union-merge 1 doc/git-union-merge.mdwn > git-union-merge.1 + +install-mans: $(mans) + install -d $(DESTDIR)$(PREFIX)/share/man/man1 + install -m 0644 $(mans) $(DESTDIR)$(PREFIX)/share/man/man1 + +install-docs: docs install-mans + install -d $(DESTDIR)$(PREFIX)/share/doc/git-annex + if [ -d html ]; then \ + rsync -a --delete html/ $(DESTDIR)$(PREFIX)/share/doc/git-annex/html/; \ + fi + +install: build install-docs Build/InstallDesktopFile + install -d $(DESTDIR)$(PREFIX)/bin + install git-annex $(DESTDIR)$(PREFIX)/bin + ln -sf git-annex $(DESTDIR)$(PREFIX)/bin/git-annex-shell + ./Build/InstallDesktopFile $(PREFIX)/bin/git-annex || true + +test: git-annex + ./git-annex test + +# hothasktags chokes on some template haskell etc, so ignore errors +tags: + find . | grep -v /.git/ | grep -v /tmp/ | grep -v /dist/ | 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. +ifeq ($(shell which ikiwiki),) +IKIWIKI=@echo "** ikiwiki not found, skipping building docs" >&2; true +else +IKIWIKI=ikiwiki +endif + +docs: $(mans) + $(IKIWIKI) doc html -v --wikiname git-annex --plugin=goodstuff \ + --no-usedirs --disable-plugin=openid --plugin=sidebar \ + --underlaydir=/dev/null --disable-plugin=shortcut \ + --disable-plugin=smiley \ + --plugin=comments --set comments_pagespec="*" \ + --exclude='news/.*' --exclude='design/assistant/blog/*' \ + --exclude='bugs/*' --exclude='todo/*' --exclude='forum/*' + +clean: + rm -rf tmp dist git-annex $(mans) configure *.tix .hpc \ + doc/.ikiwiki html dist tags Build/SysConfig.hs build-stamp \ + Setup Build/InstallDesktopFile Build/EvilSplicer \ + Build/Standalone Build/OSXMkLibs + find -name \*.o -or -name \*.hi -exec rm {} \; + +Build/InstallDesktopFile: Build/InstallDesktopFile.hs + $(GHC) --make $@ +Build/EvilSplicer: Build/EvilSplicer.hs + $(GHC) --make $@ +Build/Standalone: Build/Standalone.hs Build/SysConfig.hs + $(GHC) --make $@ +Build/OSXMkLibs: Build/OSXMkLibs.hs + $(GHC) --make $@ + +sdist: clean $(mans) + ./Build/make-sdist.sh + +# Upload to hackage. +hackage: sdist + @cabal upload dist/*.tar.gz + +LINUXSTANDALONE_DEST=tmp/git-annex.linux +linuxstandalone: Build/Standalone + $(MAKE) git-annex + + rm -rf "$(LINUXSTANDALONE_DEST)" + mkdir -p tmp + cp -R standalone/linux "$(LINUXSTANDALONE_DEST)" + + install -d "$(LINUXSTANDALONE_DEST)/bin" + cp git-annex "$(LINUXSTANDALONE_DEST)/bin/" + strip "$(LINUXSTANDALONE_DEST)/bin/git-annex" + ln -sf git-annex "$(LINUXSTANDALONE_DEST)/bin/git-annex-shell" + zcat standalone/licences.gz > $(LINUXSTANDALONE_DEST)/LICENSE + cp doc/favicon.png doc/logo.svg $(LINUXSTANDALONE_DEST) + + ./Build/Standalone "$(LINUXSTANDALONE_DEST)" + + install -d "$(LINUXSTANDALONE_DEST)/git-core" + (cd "$(shell git --exec-path)" && tar c .) | (cd "$(LINUXSTANDALONE_DEST)"/git-core && tar x) + install -d "$(LINUXSTANDALONE_DEST)/templates" + + touch "$(LINUXSTANDALONE_DEST)/libdirs.tmp" + for lib in $$(ldd "$(LINUXSTANDALONE_DEST)"/bin/* $$(find "$(LINUXSTANDALONE_DEST)"/git-core/ -type f) | grep -v -f standalone/linux/glibc-libs | grep -v "not a dynamic executable" | egrep '^ ' | sed 's/^\t//' | sed 's/.*=> //' | cut -d ' ' -f 1 | sort | uniq); do \ + dir=$$(dirname "$$lib"); \ + install -d "$(LINUXSTANDALONE_DEST)/$$dir"; \ + echo "$$dir" >> "$(LINUXSTANDALONE_DEST)/libdirs.tmp"; \ + cp "$$lib" "$(LINUXSTANDALONE_DEST)/$$dir"; \ + if [ -L "$lib" ]; then \ + link=$$(readlink -f "$$lib"); \ + cp "$$link" "$(LINUXSTANDALONE_DEST)/$$(dirname "$$link")"; \ + fi; \ + done + sort "$(LINUXSTANDALONE_DEST)/libdirs.tmp" | uniq > "$(LINUXSTANDALONE_DEST)/libdirs" + rm -f "$(LINUXSTANDALONE_DEST)/libdirs.tmp" + + cd tmp && tar czf git-annex-standalone-$(shell dpkg --print-architecture).tar.gz git-annex.linux + +OSXAPP_DEST=tmp/build-dmg/git-annex.app +OSXAPP_BASE=$(OSXAPP_DEST)/Contents/MacOS/bundle +osxapp: Build/Standalone Build/OSXMkLibs + $(MAKE) git-annex + + rm -rf "$(OSXAPP_DEST)" + install -d tmp/build-dmg + cp -R standalone/osx/git-annex.app "$(OSXAPP_DEST)" + + install -d "$(OSXAPP_BASE)" + cp git-annex "$(OSXAPP_BASE)" + strip "$(OSXAPP_BASE)/git-annex" + ln -sf git-annex "$(OSXAPP_BASE)/git-annex-shell" + gzcat standalone/licences.gz > $(OSXAPP_BASE)/LICENSE + cp $(OSXAPP_BASE)/LICENSE tmp/build-dmg/LICENSE.txt + + ./Build/Standalone $(OSXAPP_BASE) + + (cd "$(shell git --exec-path)" && tar c .) | (cd "$(OSXAPP_BASE)" && tar x) + install -d "$(OSXAPP_BASE)/templates" + + ./Build/OSXMkLibs $(OSXAPP_BASE) + rm -f tmp/git-annex.dmg + hdiutil create -size 640m -format UDRW -srcfolder tmp/build-dmg \ + -volname git-annex -o tmp/git-annex.dmg + rm -f tmp/git-annex.dmg.bz2 + bzip2 --fast tmp/git-annex.dmg + +ANDROID_FLAGS?= +# Cross compile for Android. +# Uses https://github.com/neurocyte/ghc-android +android: Build/EvilSplicer + echo "Running native build, to get TH splices.." + if [ ! -e dist/setup/setup ]; then $(CABAL) configure -f"-Production $(ANDROID_FLAGS)" -O0; fi + mkdir -p tmp + if ! $(CABAL) build --ghc-options=-ddump-splices 2> tmp/dump-splices; then tail tmp/dump-splices >&2; exit 1; fi + echo "Setting up Android build tree.." + ./Build/EvilSplicer tmp/splices tmp/dump-splices standalone/android/evilsplicer-headers.hs + rsync -az --exclude tmp --exclude dist . tmp/androidtree +# Copy the files with expanded splices to the source tree, but +# only if the existing source file is not newer. (So, if a file +# used to have TH splices but they were removed, it will be newer, +# and not overwritten.) + cp -uR tmp/splices/* tmp/androidtree || true +# Some additional dependencies needed by the expanded splices. + sed -i 's/^ Build-Depends: / Build-Depends: yesod-routes, yesod-core, shakespeare-css, shakespeare-js, shakespeare, blaze-markup, file-embed, wai-app-static, /' tmp/androidtree/git-annex.cabal +# Avoid warnings due to sometimes unused imports added for the splices. + sed -i 's/GHC-Options: \(.*\)-Wall/GHC-Options: \1-Wall -fno-warn-unused-imports /i' tmp/androidtree/git-annex.cabal +# Cabal cannot cross compile with custom build type, so workaround. + sed -i 's/Build-type: Custom/Build-type: Simple/' tmp/androidtree/git-annex.cabal + if [ ! -e tmp/androidtree/dist/setup/setup ]; then \ + cd tmp/androidtree && $$HOME/.ghc/android-14/arm-linux-androideabi-4.7/arm-linux-androideabi/bin/cabal configure -f"Android $(ANDROID_FLAGS)"; \ + fi + cd tmp/androidtree && $(CABAL) build + +adb: + ANDROID_FLAGS="-Production" $(MAKE) android + adb push tmp/androidtree/dist/build/git-annex/git-annex /data/data/ga.androidterm/bin/git-annex + +androidapp: + $(MAKE) android + $(MAKE) -C standalone/android + +# 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 -g -XPackageImports + +.PHONY: git-annex tags build-stamp diff --git a/Messages.hs b/Messages.hs new file mode 100644 index 0000000000..0357da12df --- /dev/null +++ b/Messages.hs @@ -0,0 +1,245 @@ +{- git-annex output messages + - + - Copyright 2010-2011 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Messages ( + showStart, + showNote, + showAction, + showProgress, + metered, + meteredBytes, + showSideAction, + doSideAction, + doQuietSideAction, + showStoringStateAction, + showOutput, + showLongNote, + showEndOk, + showEndFail, + showEndResult, + showErr, + warning, + warningIO, + fileNotFound, + indent, + maybeShowJSON, + showFullJSON, + showCustom, + showHeader, + showRaw, + setupConsole, + enableDebugOutput, + disableDebugOutput +) where + +import Text.JSON +import Data.Progress.Meter +import Data.Progress.Tracker +import Data.Quantity +import System.Log.Logger +import System.Log.Formatter +import System.Log.Handler (setFormatter, LogHandler) +import System.Log.Handler.Simple +import qualified Data.Set as S + +import Common +import Types +import Types.Messages +import qualified Messages.JSON as JSON +import Types.Key +import qualified Annex +import Utility.Metered + +showStart :: String -> String -> Annex () +showStart command file = handle (JSON.start command $ Just file) $ + flushed $ putStr $ command ++ " " ++ file ++ " " + +showNote :: String -> Annex () +showNote s = handle (JSON.note s) $ + flushed $ putStr $ "(" ++ s ++ ") " + +showAction :: String -> Annex () +showAction s = showNote $ s ++ "..." + +{- Progress dots. -} +showProgress :: Annex () +showProgress = handle q $ + flushed $ putStr "." + +{- Shows a progress meter while performing a transfer of a key. + - The action is passed a callback to use to update the meter. -} +metered :: Maybe MeterUpdate -> Key -> (MeterUpdate -> Annex a) -> Annex a +metered combinemeterupdate key a = go (keySize key) + where + go (Just size) = meteredBytes combinemeterupdate size a + go _ = a (const noop) + +{- Shows a progress meter while performing an action on a given number + - of bytes. -} +meteredBytes :: Maybe MeterUpdate -> Integer -> (MeterUpdate -> Annex a) -> Annex a +meteredBytes combinemeterupdate size a = withOutputType go + where + go NormalOutput = do + progress <- liftIO $ newProgress "" size + meter <- liftIO $ newMeter progress "B" 25 (renderNums binaryOpts 1) + showOutput + r <- a $ \n -> liftIO $ do + setP progress $ fromBytesProcessed n + displayMeter stdout meter + maybe noop (\m -> m n) combinemeterupdate + liftIO $ clearMeter stdout meter + return r + go _ = a (const noop) + +showSideAction :: String -> Annex () +showSideAction m = Annex.getState Annex.output >>= go + where + go st + | sideActionBlock st == StartBlock = do + p + let st' = st { sideActionBlock = InBlock } + Annex.changeState $ \s -> s { Annex.output = st' } + | sideActionBlock st == InBlock = return () + | otherwise = p + p = handle q $ putStrLn $ "(" ++ m ++ "...)" + +showStoringStateAction :: Annex () +showStoringStateAction = showSideAction "Recording state in git" + +{- Performs an action, supressing showSideAction messages. -} +doQuietSideAction :: Annex a -> Annex a +doQuietSideAction = doSideAction' InBlock + +{- Performs an action, that may call showSideAction multiple times. + - Only the first will be displayed. -} +doSideAction :: Annex a -> Annex a +doSideAction = doSideAction' StartBlock + +doSideAction' :: SideActionBlock -> Annex a -> Annex a +doSideAction' b a = do + o <- Annex.getState Annex.output + set $ o { sideActionBlock = b } + set o `after` a + where + set o = Annex.changeState $ \s -> s { Annex.output = o } + +showOutput :: Annex () +showOutput = handle q $ + putStr "\n" + +showLongNote :: String -> Annex () +showLongNote s = handle (JSON.note s) $ + putStrLn $ '\n' : indent s + +showEndOk :: Annex () +showEndOk = showEndResult True + +showEndFail :: Annex () +showEndFail = showEndResult False + +showEndResult :: Bool -> Annex () +showEndResult ok = handle (JSON.end ok) $ putStrLn msg + where + msg + | ok = "ok" + | otherwise = "failed" + +showErr :: (Show a) => a -> Annex () +showErr e = warning' $ "git-annex: " ++ show e + +warning :: String -> Annex () +warning = warning' . indent + +warning' :: String -> Annex () +warning' w = do + handle q $ putStr "\n" + 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 () +fileNotFound file = do + st <- Annex.getState Annex.output + let shown = fileNotFoundShown st + when (S.notMember file shown) $ do + let shown' = S.insert file shown + let st' = st { fileNotFoundShown = shown' } + Annex.changeState $ \s -> s { Annex.output = st' } + liftIO $ hPutStrLn stderr $ unwords + [ "git-annex:", file, "not found" ] + +indent :: String -> String +indent = intercalate "\n" . map (\l -> " " ++ l) . lines + +{- Shows a JSON fragment only when in json mode. -} +maybeShowJSON :: JSON a => [(String, a)] -> Annex () +maybeShowJSON v = handle (JSON.add v) q + +{- Shows a complete JSON value, only when in json mode. -} +showFullJSON :: JSON a => [(String, a)] -> Annex Bool +showFullJSON v = withOutputType $ liftIO . go + where + go JSONOutput = JSON.complete v >> return True + go _ = return False + +{- Performs an action that outputs nonstandard/customized output, and + - in JSON mode wraps its output in JSON.start and JSON.end, so it's + - a complete JSON document. + - This is only needed when showStart and showEndOk is not used. -} +showCustom :: String -> Annex Bool -> Annex () +showCustom command a = do + handle (JSON.start command Nothing) q + r <- a + handle (JSON.end r) q + +showHeader :: String -> Annex () +showHeader h = handle q $ + flushed $ putStr $ h ++ ": " + +showRaw :: String -> Annex () +showRaw s = handle q $ putStrLn s + +setupConsole :: IO () +setupConsole = do + s <- setFormatter + <$> streamHandler stderr DEBUG + <*> pure (simpleLogFormatter "[$time] $msg") + updateGlobalLogger rootLoggerName (setLevel NOTICE . setHandlers [s]) + {- This avoids ghc's output layer crashing on + - invalid encoded characters in + - filenames when printing them out. -} + fileEncoding stdout + fileEncoding stderr + +enableDebugOutput :: IO () +enableDebugOutput = updateGlobalLogger rootLoggerName $ setLevel DEBUG + +disableDebugOutput :: IO () +disableDebugOutput = updateGlobalLogger rootLoggerName $ setLevel NOTICE + +handle :: IO () -> IO () -> Annex () +handle json normal = withOutputType go + where + go NormalOutput = liftIO normal + go QuietOutput = q + go JSONOutput = liftIO $ flushed json + +q :: Monad m => m () +q = noop + +flushed :: IO () -> IO () +flushed a = a >> hFlush stdout + +withOutputType :: (OutputType -> Annex a) -> Annex a +withOutputType a = outputType <$> Annex.getState Annex.output >>= a diff --git a/Messages/JSON.hs b/Messages/JSON.hs new file mode 100644 index 0000000000..d57d69318b --- /dev/null +++ b/Messages/JSON.hs @@ -0,0 +1,37 @@ +{- git-annex JSON output + - + - Copyright 2011 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Messages.JSON ( + start, + end, + note, + add, + complete +) where + +import Text.JSON + +import qualified Utility.JSONStream as Stream + +start :: String -> Maybe String -> IO () +start command file = + putStr $ Stream.start $ ("command", command) : filepart file + where + filepart Nothing = [] + filepart (Just f) = [("file", f)] + +end :: Bool -> IO () +end b = putStr $ Stream.add [("success", b)] ++ Stream.end + +note :: String -> IO () +note s = add [("note", s)] + +add :: JSON a => [(String, a)] -> IO () +add v = putStr $ Stream.add v + +complete :: JSON a => [(String, a)] -> IO () +complete v = putStr $ Stream.start v ++ Stream.end diff --git a/NEWS b/NEWS new file mode 120000 index 0000000000..798088bec2 --- /dev/null +++ b/NEWS @@ -0,0 +1 @@ +debian/NEWS \ No newline at end of file diff --git a/Option.hs b/Option.hs new file mode 100644 index 0000000000..64ba56f6d2 --- /dev/null +++ b/Option.hs @@ -0,0 +1,79 @@ +{- common command-line options + - + - Copyright 2010-2011 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Option ( + common, + matcher, + flag, + field, + name, + ArgDescr(..), + OptDescr(..), +) where + +import System.Console.GetOpt + +import Common.Annex +import qualified Annex +import Types.Messages +import Limit +import Usage + +common :: [Option] +common = + [ Option [] ["force"] (NoArg (setforce True)) + "allow actions that may lose annexed data" + , Option ['F'] ["fast"] (NoArg (setfast True)) + "avoid slow operations" + , Option ['a'] ["auto"] (NoArg (setauto True)) + "automatic mode" + , Option ['q'] ["quiet"] (NoArg (Annex.setOutput QuietOutput)) + "avoid verbose output" + , Option ['v'] ["verbose"] (NoArg (Annex.setOutput NormalOutput)) + "allow verbose output (default)" + , Option ['j'] ["json"] (NoArg (Annex.setOutput JSONOutput)) + "enable JSON output" + , Option ['d'] ["debug"] (NoArg setdebug) + "show debug messages" + , Option [] ["no-debug"] (NoArg unsetdebug) + "don't show debug messages" + , Option ['b'] ["backend"] (ReqArg setforcebackend paramName) + "specify key-value backend to use" + ] + where + setforce v = Annex.changeState $ \s -> s { Annex.force = v } + setfast v = Annex.changeState $ \s -> s { Annex.fast = v } + setauto v = Annex.changeState $ \s -> s { Annex.auto = v } + setforcebackend v = Annex.changeState $ \s -> s { Annex.forcebackend = Just v } + setdebug = Annex.changeGitConfig $ \c -> c { annexDebug = True } + unsetdebug = Annex.changeGitConfig $ \c -> c { annexDebug = False } + +matcher :: [Option] +matcher = + [ longopt "not" "negate next option" + , longopt "and" "both previous and next option must match" + , longopt "or" "either previous or next option must match" + , shortopt "(" "open group of options" + , shortopt ")" "close group of options" + ] + where + longopt o = Option [] [o] $ NoArg $ addToken o + shortopt o = Option o [] $ NoArg $ addToken o + +{- An option that sets a flag. -} +flag :: String -> String -> String -> Option +flag short opt description = + Option short [opt] (NoArg (Annex.setFlag opt)) description + +{- An option that sets a field. -} +field :: String -> String -> String -> String -> Option +field short opt paramdesc description = + Option short [opt] (ReqArg (Annex.setField opt) paramdesc) description + +{- The flag or field name used for an option. -} +name :: Option -> String +name (Option _ o _ _) = Prelude.head o diff --git a/README b/README new file mode 100644 index 0000000000..ce67d68166 --- /dev/null +++ b/README @@ -0,0 +1,6 @@ +git-annex allows managing files with git, without checking the file +contents into git. While that may seem paradoxical, it is useful when +dealing with files larger than git can currently easily handle, whether due +to limitations in memory, checksumming time, or disk space. + +For documentation, see doc/ or diff --git a/Remote.hs b/Remote.hs new file mode 100644 index 0000000000..ea93172825 --- /dev/null +++ b/Remote.hs @@ -0,0 +1,261 @@ +{- git-annex remotes + - + - Copyright 2011 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Remote ( + Remote, + uuid, + name, + storeKey, + retrieveKeyFile, + retrieveKeyFileCheap, + removeKey, + hasKey, + hasKeyCheap, + whereisKey, + + remoteTypes, + remoteList, + specialRemote, + remoteMap, + uuidDescriptions, + byName, + byNameWithUUID, + byCost, + prettyPrintUUIDs, + prettyListUUIDs, + prettyUUID, + remoteFromUUID, + remotesWithUUID, + remotesWithoutUUID, + keyLocations, + keyPossibilities, + keyPossibilitiesTrusted, + nameToUUID, + showTriedRemotes, + showLocations, + forceTrust, + logStatus +) where + +import qualified Data.Map as M +import Text.JSON +import Text.JSON.Generic +import Data.Tuple +import Data.Ord + +import Common.Annex +import Types.Remote +import qualified Annex +import Annex.UUID +import Logs.UUID +import Logs.Trust +import Logs.Location hiding (logStatus) +import Remote.List + +{- Map from UUIDs of Remotes to a calculated value. -} +remoteMap :: (Remote -> a) -> Annex (M.Map UUID a) +remoteMap c = M.fromList . map (\r -> (uuid r, c r)) . + filter (\r -> uuid r /= NoUUID) <$> remoteList + +{- Map of UUIDs of remotes and their descriptions. + - The names of Remotes are added to suppliment any description that has + - been set for a repository. -} +uuidDescriptions :: Annex (M.Map UUID String) +uuidDescriptions = M.unionWith addName <$> uuidMap <*> remoteMap name + +addName :: String -> String -> String +addName desc n + | desc == n = desc + | null desc = n + | otherwise = n ++ " (" ++ desc ++ ")" + +{- When a name is specified, looks up the remote matching that name. + - (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 match + matching r = n == name r || toUUID n == uuid r + +{- Looks up a remote by name (or by UUID, or even by description), + - and returns its UUID. Finds even remotes that are not configured in + - .git/config. -} +nameToUUID :: String -> Annex UUID +nameToUUID "." = getUUID -- special case for current repo +nameToUUID "here" = getUUID +nameToUUID "" = error "no remote specified" +nameToUUID n = byName' n >>= go + where + go (Right r) = return $ uuid r + go (Left e) = fromMaybe (error e) <$> bydescription + bydescription = do + m <- uuidMap + case M.lookup n $ transform swap m of + Just u -> return $ Just u + Nothing -> return $ byuuid m + byuuid m = M.lookup (toUUID n) $ transform double m + transform a = M.fromList . map a . M.toList + double (a, _) = (a, a) + +{- Pretty-prints a list of UUIDs of remotes, for human display. + - + - When JSON is enabled, also generates a machine-readable description + - of the UUIDs. -} +prettyPrintUUIDs :: String -> [UUID] -> Annex String +prettyPrintUUIDs desc uuids = do + hereu <- getUUID + m <- uuidDescriptions + maybeShowJSON [(desc, map (jsonify m hereu) uuids)] + return $ unwords $ map (\u -> "\t" ++ prettify m hereu u ++ "\n") uuids + where + finddescription m u = M.findWithDefault "" u m + prettify m hereu u + | not (null d) = fromUUID u ++ " -- " ++ d + | otherwise = fromUUID u + where + ishere = hereu == u + n = finddescription m u + d + | null n && ishere = "here" + | ishere = addName n "here" + | otherwise = n + jsonify m hereu u = toJSObject + [ ("uuid", toJSON $ fromUUID u) + , ("description", toJSON $ finddescription m u) + , ("here", toJSON $ hereu == u) + ] + +{- List of remote names and/or descriptions, for human display. -} +prettyListUUIDs :: [UUID] -> Annex [String] +prettyListUUIDs uuids = do + hereu <- getUUID + m <- uuidDescriptions + return $ map (prettify m hereu) uuids + where + finddescription m u = M.findWithDefault "" u m + prettify m hereu u + | u == hereu = addName n "here" + | otherwise = n + where + n = finddescription m u + +{- Nice display of a remote's name and/or description. -} +prettyUUID :: UUID -> Annex String +prettyUUID u = concat <$> prettyListUUIDs [u] + +{- Gets the remote associated with a UUID. + - There's no associated remote when this is the UUID of the local repo. -} +remoteFromUUID :: UUID -> Annex (Maybe Remote) +remoteFromUUID u = ifM ((==) u <$> getUUID) + ( return Nothing + , Just . fromMaybe (error "Unknown UUID") . M.lookup u <$> remoteMap id + ) + +{- Filters a list of remotes to ones that have the listed uuids. -} +remotesWithUUID :: [Remote] -> [UUID] -> [Remote] +remotesWithUUID rs us = filter (\r -> uuid r `elem` us) rs + +{- Filters a list of remotes to ones that do not have the listed uuids. -} +remotesWithoutUUID :: [Remote] -> [UUID] -> [Remote] +remotesWithoutUUID rs us = filter (\r -> uuid r `notElem` us) rs + +{- List of repository UUIDs that the location log indicates may have a key. + - Dead repositories are excluded. -} +keyLocations :: Key -> Annex [UUID] +keyLocations key = trustExclude DeadTrusted =<< loggedLocations key + +{- Cost ordered lists of remotes that the location log indicates + - may have a key. + -} +keyPossibilities :: Key -> Annex [Remote] +keyPossibilities key = fst <$> keyPossibilities' key [] + +{- Cost ordered lists of remotes that the location log indicates + - may have a key. + - + - Also returns a list of UUIDs that are trusted to have the key + - (some may not have configured remotes). + -} +keyPossibilitiesTrusted :: Key -> Annex ([Remote], [UUID]) +keyPossibilitiesTrusted key = keyPossibilities' key =<< trustGet Trusted + +keyPossibilities' :: Key -> [UUID] -> Annex ([Remote], [UUID]) +keyPossibilities' key trusted = do + u <- getUUID + + -- uuids of all remotes that are recorded to have the key + validuuids <- filter (/= u) <$> keyLocations key + + -- note that validuuids is assumed to not have dups + let validtrusteduuids = validuuids `intersect` trusted + + -- remotes that match uuids that have the key + allremotes <- filter (not . remoteAnnexIgnore . gitconfig) + <$> remoteList + let validremotes = remotesWithUUID allremotes validuuids + + return (sortBy (comparing cost) validremotes, validtrusteduuids) + +{- Displays known locations of a key. -} +showLocations :: Key -> [UUID] -> String -> Annex () +showLocations key exclude nolocmsg = do + u <- getUUID + uuids <- keyLocations key + untrusteduuids <- trustGet UnTrusted + let uuidswanted = filteruuids uuids (u:exclude++untrusteduuids) + let uuidsskipped = filteruuids uuids (u:exclude++uuidswanted) + ppuuidswanted <- Remote.prettyPrintUUIDs "wanted" uuidswanted + ppuuidsskipped <- Remote.prettyPrintUUIDs "skipped" uuidsskipped + showLongNote $ message ppuuidswanted ppuuidsskipped + where + filteruuids l x = filter (`notElem` x) l + message [] [] = nolocmsg + message rs [] = "Try making some of these repositories available:\n" ++ rs + message [] us = "Also these untrusted repositories may contain the file:\n" ++ us + message rs us = message rs [] ++ message [] us + +showTriedRemotes :: [Remote] -> Annex () +showTriedRemotes [] = noop +showTriedRemotes remotes = + showLongNote $ "Unable to access these remotes: " ++ + intercalate ", " (map name remotes) + +forceTrust :: TrustLevel -> String -> Annex () +forceTrust level remotename = do + u <- nameToUUID remotename + Annex.changeState $ \s -> + s { Annex.forcetrust = M.insert u level (Annex.forcetrust s) } + +{- Used to log a change in a remote's having a key. The change is logged + - in the local repo, not on the remote. The process of transferring the + - key to the remote, or removing the key from it *may* log the change + - on the remote, but this cannot always be relied on. -} +logStatus :: Remote -> Key -> LogStatus -> Annex () +logStatus remote key = logChange key (uuid remote) + +{- Orders remotes by cost, with ones with the lowest cost grouped together. -} +byCost :: [Remote] -> [[Remote]] +byCost = map snd . sortBy (comparing fst) . M.toList . costmap + where + costmap = M.fromListWith (++) . map costpair + costpair r = (cost r, [r]) diff --git a/Remote/Bup.hs b/Remote/Bup.hs new file mode 100644 index 0000000000..9b3675cfa5 --- /dev/null +++ b/Remote/Bup.hs @@ -0,0 +1,282 @@ +{- Using bup as a remote. + - + - Copyright 2011 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Remote.Bup (remote) where + +import qualified Data.ByteString.Lazy as L +import qualified Data.Map as M +import System.Process + +import Common.Annex +import Types.Remote +import Types.Key +import qualified Git +import qualified Git.Command +import qualified Git.Config +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 +import Crypto +import Data.ByteString.Lazy.UTF8 (fromString) +import Data.Digest.Pure.SHA +import Utility.UserInfo +import Annex.Content +import Utility.Metered + +type BupRepo = String + +remote :: RemoteType +remote = RemoteType { + typename = "bup", + enumerate = findSpecialRemotes "buprepo", + generate = gen, + setup = bupSetup +} + +gen :: Git.Repo -> UUID -> RemoteConfig -> RemoteGitConfig -> Annex Remote +gen r u c gc = do + bupr <- liftIO $ bup2GitRemote buprepo + cst <- remoteCost gc $ + if bupLocal buprepo + then nearlyCheapRemoteCost + else expensiveRemoteCost + (u', bupr') <- getBupUUID bupr u + + let new = Remote + { uuid = u' + , cost = cst + , name = Git.repoDescribe r + , storeKey = store new buprepo + , retrieveKeyFile = retrieve buprepo + , retrieveKeyFileCheap = retrieveCheap buprepo + , removeKey = remove + , hasKey = checkPresent r bupr' + , hasKeyCheap = bupLocal buprepo + , whereisKey = Nothing + , config = c + , repo = r + , gitconfig = gc + , localpath = if bupLocal buprepo && not (null buprepo) + then Just buprepo + else Nothing + , remotetype = remote + , globallyAvailable = not $ bupLocal buprepo + , readonly = False + } + return $ encryptableRemote c + (storeEncrypted new buprepo) + (retrieveEncrypted buprepo) + new + where + buprepo = fromMaybe (error "missing buprepo") $ remoteAnnexBupRepo gc + +bupSetup :: UUID -> RemoteConfig -> Annex RemoteConfig +bupSetup u c = do + -- verify configuration is sane + let buprepo = fromMaybe (error "Specify buprepo=") $ + M.lookup "buprepo" c + c' <- encryptionSetup c + + -- bup init will create the repository. + -- (If the repository already exists, bup init again appears safe.) + showAction "bup init" + unlessM (bup "init" buprepo []) $ error "bup init failed" + + storeBupUUID u buprepo + + -- The buprepo is stored in git config, as well as this repo's + -- persistant state, so it can vary between hosts. + gitConfigSpecialRemote u c' "buprepo" buprepo + + return c' + +bupParams :: String -> BupRepo -> [CommandParam] -> [CommandParam] +bupParams command buprepo params = + Param command : [Param "-r", Param buprepo] ++ params + +bup :: String -> BupRepo -> [CommandParam] -> Annex Bool +bup command buprepo params = do + showOutput -- make way for bup output + liftIO $ boolSystem "bup" $ bupParams command buprepo params + +pipeBup :: [CommandParam] -> Maybe Handle -> Maybe Handle -> IO Bool +pipeBup params inh outh = do + p <- runProcess "bup" (toCommand params) + Nothing Nothing inh outh Nothing + ok <- waitForProcess p + case ok of + ExitSuccess -> return True + _ -> return False + +bupSplitParams :: Remote -> BupRepo -> Key -> [CommandParam] -> Annex [CommandParam] +bupSplitParams r buprepo k src = do + let os = map Param $ remoteAnnexBupSplitOptions $ gitconfig r + showOutput -- make way for bup output + return $ bupParams "split" buprepo + (os ++ [Param "-n", Param (bupRef k)] ++ src) + +store :: Remote -> BupRepo -> Key -> AssociatedFile -> MeterUpdate -> Annex Bool +store r buprepo k _f _p = sendAnnex k (rollback k buprepo) $ \src -> do + params <- bupSplitParams r buprepo k [File src] + liftIO $ boolSystem "bup" params + +storeEncrypted :: Remote -> BupRepo -> (Cipher, Key) -> Key -> MeterUpdate -> Annex Bool +storeEncrypted r buprepo (cipher, enck) k _p = + sendAnnex k (rollback enck buprepo) $ \src -> do + params <- bupSplitParams r buprepo enck [] + liftIO $ catchBoolIO $ + encrypt (getGpgOpts r) cipher (feedFile src) $ \h -> + pipeBup params (Just h) Nothing + +retrieve :: BupRepo -> Key -> AssociatedFile -> FilePath -> MeterUpdate -> Annex Bool +retrieve buprepo k _f d _p = do + let params = bupParams "join" buprepo [Param $ bupRef k] + liftIO $ catchBoolIO $ do + tofile <- openFile d WriteMode + pipeBup params Nothing (Just tofile) + +retrieveCheap :: BupRepo -> Key -> FilePath -> Annex Bool +retrieveCheap _ _ _ = return False + +retrieveEncrypted :: BupRepo -> (Cipher, Key) -> Key -> FilePath -> MeterUpdate -> Annex Bool +retrieveEncrypted buprepo (cipher, enck) _ f _p = liftIO $ catchBoolIO $ + withHandle StdoutHandle createProcessSuccess p $ \h -> do + decrypt cipher (\toh -> L.hPut toh =<< L.hGetContents h) $ + readBytes $ L.writeFile f + return True + where + params = bupParams "join" buprepo [Param $ bupRef enck] + p = proc "bup" $ toCommand params + +remove :: Key -> Annex Bool +remove _ = do + warning "content cannot be removed from bup remote" + return False + +{- Cannot revert having stored a key in bup, but at least the data for the + - key will be used for deltaing data of other keys stored later. + - + - We can, however, remove the git branch that bup created for the key. + -} +rollback :: Key -> BupRepo -> Annex () +rollback k bupr = go =<< liftIO (bup2GitRemote bupr) + where + go r + | Git.repoIsUrl r = void $ onBupRemote r boolSystem "git" params + | otherwise = void $ liftIO $ catchMaybeIO $ + boolSystem "git" $ Git.Command.gitCommandLine params r + params = [ Params "branch -D", Param (bupRef k) ] + +{- Bup does not provide a way to tell if a given dataset is present + - in a bup repository. One way it to check if the git repository has + - a branch matching the name (as created by bup split -n). + -} +checkPresent :: Git.Repo -> Git.Repo -> Key -> Annex (Either String Bool) +checkPresent r bupr k + | Git.repoIsUrl bupr = do + showAction $ "checking " ++ Git.repoDescribe r + ok <- onBupRemote bupr boolSystem "git" params + return $ Right ok + | otherwise = liftIO $ catchMsgIO $ + boolSystem "git" $ Git.Command.gitCommandLine params bupr + where + params = + [ Params "show-ref --quiet --verify" + , Param $ "refs/heads/" ++ bupRef k + ] + +{- Store UUID in the annex.uuid setting of the bup repository. -} +storeBupUUID :: UUID -> BupRepo -> Annex () +storeBupUUID u buprepo = do + r <- liftIO $ bup2GitRemote buprepo + if Git.repoIsUrl r + then do + showAction "storing uuid" + unlessM (onBupRemote r boolSystem "git" + [Params $ "config annex.uuid " ++ v]) $ + error "ssh failed" + else liftIO $ do + r' <- Git.Config.read r + let olduuid = Git.Config.get "annex.uuid" "" r' + when (olduuid == "") $ + Git.Command.run + [ Param "config" + , Param "annex.uuid" + , Param v + ] r' + where + v = fromUUID u + +onBupRemote :: Git.Repo -> (FilePath -> [CommandParam] -> IO a) -> FilePath -> [CommandParam] -> Annex a +onBupRemote r a command params = do + sshparams <- sshToRepo r [Param $ + "cd " ++ dir ++ " && " ++ unwords (command : toCommand params)] + liftIO $ a "ssh" sshparams + where + path = Git.repoPath r + base = fromMaybe path (stripPrefix "/~/" path) + dir = shellEscape base + +{- Allow for bup repositories on removable media by checking + - local bup repositories to see if they are available, and getting their + - uuid (which may be different from the stored uuid for the bup remote). + - + - If a bup repository is not available, returns NoUUID. + - This will cause checkPresent to indicate nothing from the bup remote + - is known to be present. + - + - Also, returns a version of the repo with config read, if it is local. + -} +getBupUUID :: Git.Repo -> UUID -> Annex (UUID, Git.Repo) +getBupUUID r u + | Git.repoIsUrl r = return (u, r) + | otherwise = liftIO $ do + ret <- tryIO $ Git.Config.read r + case ret of + Right r' -> return (toUUID $ Git.Config.get "annex.uuid" "" r', r') + Left _ -> return (NoUUID, r) + +{- Converts a bup remote path spec into a Git.Repo. There are some + - differences in path representation between git and bup. -} +bup2GitRemote :: BupRepo -> IO Git.Repo +bup2GitRemote "" = do + -- bup -r "" operates on ~/.bup + h <- myHomeDir + Git.Construct.fromAbsPath $ h ".bup" +bup2GitRemote r + | bupLocal r = + if "/" `isPrefixOf` r + then Git.Construct.fromAbsPath r + else error "please specify an absolute path" + | otherwise = Git.Construct.fromUrl $ "ssh://" ++ host ++ slash dir + where + bits = split ":" r + host = Prelude.head bits + dir = intercalate ":" $ drop 1 bits + -- "host:~user/dir" is not supported specially by bup; + -- "host:dir" is relative to the home directory; + -- "host:" goes in ~/.bup + slash d + | null d = "/~/.bup" + | "/" `isPrefixOf` d = d + | otherwise = "/~/" ++ d + +{- Converts a key into a git ref name, which bup-split -n will use to point + - to it. -} +bupRef :: Key -> String +bupRef k + | Git.Ref.legal True shown = shown + | otherwise = "git-annex-" ++ showDigest (sha256 (fromString shown)) + where + shown = key2file k + +bupLocal :: BupRepo -> Bool +bupLocal = notElem ':' diff --git a/Remote/Directory.hs b/Remote/Directory.hs new file mode 100644 index 0000000000..6cc75d2f1d --- /dev/null +++ b/Remote/Directory.hs @@ -0,0 +1,247 @@ +{- A "remote" that is just a filesystem directory. + - + - Copyright 2011-2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE CPP #-} + +module Remote.Directory (remote) where + +import qualified Data.ByteString.Lazy as L +import qualified Data.ByteString as S +import qualified Data.Map as M +import qualified Control.Exception as E +import Data.Int + +import Common.Annex +import Types.Remote +import qualified Git +import Config.Cost +import Config +import Utility.FileMode +import Remote.Helper.Special +import Remote.Helper.Encryptable +import Remote.Helper.Chunked +import Crypto +import Annex.Content +import Utility.Metered + +remote :: RemoteType +remote = RemoteType { + typename = "directory", + enumerate = findSpecialRemotes "directory", + generate = gen, + setup = directorySetup +} + +gen :: Git.Repo -> UUID -> RemoteConfig -> RemoteGitConfig -> Annex Remote +gen r u c gc = do + cst <- remoteCost gc cheapRemoteCost + let chunksize = chunkSize c + return $ encryptableRemote c + (storeEncrypted dir (getGpgOpts gc) chunksize) + (retrieveEncrypted dir chunksize) + Remote { + uuid = u, + cost = cst, + name = Git.repoDescribe r, + storeKey = store dir chunksize, + retrieveKeyFile = retrieve dir chunksize, + retrieveKeyFileCheap = retrieveCheap dir chunksize, + removeKey = remove dir, + hasKey = checkPresent dir chunksize, + hasKeyCheap = True, + whereisKey = Nothing, + config = M.empty, + repo = r, + gitconfig = gc, + localpath = Just dir, + readonly = False, + globallyAvailable = False, + remotetype = remote + } + where + dir = fromMaybe (error "missing directory") $ remoteAnnexDirectory gc + +directorySetup :: UUID -> RemoteConfig -> Annex RemoteConfig +directorySetup u c = do + -- verify configuration is sane + let dir = fromMaybe (error "Specify directory=") $ + M.lookup "directory" c + absdir <- liftIO $ absPath dir + liftIO $ unlessM (doesDirectoryExist absdir) $ + error $ "Directory does not exist: " ++ absdir + c' <- encryptionSetup c + + -- The directory is stored in git config, not in this remote's + -- persistant state, so it can vary between hosts. + gitConfigSpecialRemote u c' "directory" absdir + return $ M.delete "directory" c' + +{- Locations to try to access a given Key in the Directory. + - We try more than since we used to write to different hash directories. -} +locations :: FilePath -> Key -> [FilePath] +locations d k = map (d ) (keyPaths k) + +{- Directory where the file(s) for a key are stored. -} +storeDir :: FilePath -> Key -> FilePath +storeDir d k = addTrailingPathSeparator $ d hashDirLower k keyFile k + +{- Where we store temporary data for a key as it's being uploaded. -} +tmpDir :: FilePath -> Key -> FilePath +tmpDir d k = addTrailingPathSeparator $ d "tmp" keyFile k + +withCheckedFiles :: (FilePath -> IO Bool) -> ChunkSize -> FilePath -> Key -> ([FilePath] -> IO Bool) -> IO Bool +withCheckedFiles _ _ [] _ _ = return False +withCheckedFiles check Nothing d k a = go $ locations d k + where + go [] = return False + go (f:fs) = ifM (check f) ( a [f] , go fs ) +withCheckedFiles check (Just _) d k a = go $ locations d k + where + go [] = return False + go (f:fs) = do + let chunkcount = f ++ chunkCount + ifM (check chunkcount) + ( do + chunks <- listChunks f <$> readFile chunkcount + ifM (all id <$> mapM check chunks) + ( a chunks , return False ) + , go fs + ) + +withStoredFiles :: ChunkSize -> FilePath -> Key -> ([FilePath] -> IO Bool) -> IO Bool +withStoredFiles = withCheckedFiles doesFileExist + +store :: FilePath -> ChunkSize -> Key -> AssociatedFile -> MeterUpdate -> Annex Bool +store d chunksize k _f p = sendAnnex k (void $ remove d k) $ \src -> + metered (Just p) k $ \meterupdate -> + storeHelper d chunksize k k $ \dests -> + case chunksize of + Nothing -> do + let dest = Prelude.head dests + meteredWriteFile meterupdate dest + =<< L.readFile src + return [dest] + Just _ -> + storeSplit meterupdate chunksize dests + =<< L.readFile 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 k $ \dests -> + encrypt gpgOpts cipher (feedFile src) $ readBytes $ \b -> + case chunksize of + Nothing -> do + let dest = Prelude.head dests + meteredWriteFile meterupdate dest b + return [dest] + Just _ -> storeSplit meterupdate chunksize dests b + +{- Splits a ByteString into chunks and writes to dests, obeying configured + - chunk size (not to be confused with the L.ByteString chunk size). + - Note: Must always write at least one file, even for empty ByteString. -} +storeSplit :: MeterUpdate -> ChunkSize -> [FilePath] -> L.ByteString -> IO [FilePath] +storeSplit _ Nothing _ _ = error "bad storeSplit call" +storeSplit _ _ [] _ = error "bad storeSplit call" +storeSplit meterupdate (Just chunksize) alldests@(firstdest:_) b + | L.null b = do + -- must always write at least one file, even for empty + L.writeFile firstdest b + return [firstdest] + | otherwise = storeSplit' meterupdate chunksize alldests (L.toChunks b) [] +storeSplit' :: MeterUpdate -> Int64 -> [FilePath] -> [S.ByteString] -> [FilePath] -> IO [FilePath] +storeSplit' _ _ [] _ _ = error "ran out of dests" +storeSplit' _ _ _ [] c = return $ reverse c +storeSplit' meterupdate chunksize (d:dests) bs c = do + bs' <- E.bracket (openFile d WriteMode) hClose $ + feed zeroBytesProcessed chunksize bs + storeSplit' meterupdate chunksize dests bs' (d:c) + where + feed _ _ [] _ = return [] + feed bytes sz (l:ls) h = do + let len = S.length l + let s = fromIntegral len + if s <= sz || sz == chunksize + then do + S.hPut h l + let bytes' = addBytesProcessed bytes len + meterupdate bytes' + feed bytes' (sz - s) ls h + else return (l:ls) + +storeHelper :: FilePath -> ChunkSize -> Key -> Key -> ([FilePath] -> IO [FilePath]) -> Annex Bool +storeHelper d chunksize key origkey storer = check <&&> go + where + tmpdir = tmpDir d key + destdir = storeDir d key + {- An encrypted key does not have a known size, + - so check that the size of the original key is available as free + - space. -} + check = do + liftIO $ createDirectoryIfMissing True tmpdir + checkDiskSpace (Just tmpdir) origkey 0 + go = liftIO $ catchBoolIO $ + storeChunks key tmpdir destdir chunksize storer recorder finalizer + finalizer tmp dest = do + void $ tryIO $ allowWrite dest -- may already exist + void $ tryIO $ removeDirectoryRecursive dest -- or not exist + createDirectoryIfMissing True (parentDir dest) + renameDirectory tmp dest + -- may fail on some filesystems + void $ tryIO $ do + mapM_ preventWrite =<< dirContents dest + preventWrite dest + recorder f s = do + void $ tryIO $ allowWrite f + writeFile f s + void $ tryIO $ preventWrite f + +retrieve :: FilePath -> ChunkSize -> Key -> AssociatedFile -> FilePath -> MeterUpdate -> Annex Bool +retrieve d chunksize k _ f p = metered (Just p) k $ \meterupdate -> + liftIO $ withStoredFiles chunksize d k $ \files -> + catchBoolIO $ do + meteredWriteFileChunks meterupdate f files $ L.readFile + return True + +retrieveEncrypted :: FilePath -> ChunkSize -> (Cipher, Key) -> Key -> FilePath -> MeterUpdate -> Annex Bool +retrieveEncrypted d chunksize (cipher, enck) k f p = metered (Just p) k $ \meterupdate -> + liftIO $ withStoredFiles chunksize d enck $ \files -> + catchBoolIO $ do + decrypt cipher (feeder files) $ + readBytes $ meteredWriteFile meterupdate f + return True + where + feeder files h = forM_ files $ \file -> L.hPut h =<< L.readFile file + +retrieveCheap :: FilePath -> ChunkSize -> Key -> FilePath -> Annex Bool +retrieveCheap _ (Just _) _ _ = return False -- no cheap retrieval for chunks +#ifndef mingw32_HOST_OS +retrieveCheap d _ k f = liftIO $ withStoredFiles Nothing d k go + where + go [file] = catchBoolIO $ createSymbolicLink file f >> return True + go _files = return False +#else +retrieveCheap _ _ _ _ = return False +#endif + +remove :: FilePath -> Key -> Annex Bool +remove d k = liftIO $ do + void $ tryIO $ allowWrite dir +#ifdef mingw32_HOST_OS + {- Windows needs the files inside the directory to be writable + - before it can delete them. -} + void $ tryIO $ mapM_ allowWrite =<< dirContents dir +#endif + catchBoolIO $ do + removeDirectoryRecursive dir + return True + where + dir = storeDir d k + +checkPresent :: FilePath -> ChunkSize -> Key -> Annex (Either String Bool) +checkPresent d chunksize k = liftIO $ catchMsgIO $ withStoredFiles chunksize d k $ + const $ return True -- withStoredFiles checked that it exists diff --git a/Remote/Git.hs b/Remote/Git.hs new file mode 100644 index 0000000000..e269b9ad81 --- /dev/null +++ b/Remote/Git.hs @@ -0,0 +1,507 @@ +{- Standard git remotes. + - + - Copyright 2011-2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE CPP #-} + +module Remote.Git ( + remote, + configRead, + repoAvail, +) where + +import qualified Data.Map as M +import Control.Exception.Extensible + +import Common.Annex +import Utility.Rsync +import Remote.Helper.Ssh +import Annex.Ssh +import Types.Remote +import Types.GitConfig +import qualified Git +import qualified Git.Config +import qualified Git.Construct +import qualified Git.Command +import qualified Annex +import Logs.Presence +import Logs.Transfer +import Annex.UUID +import Annex.Exception +import qualified Annex.Content +import qualified Annex.BranchState +import qualified Annex.Branch +import qualified Utility.Url as Url +import Utility.Tmp +import Config +import Config.Cost +import Init +import Types.Key +import qualified Fields +import Logs.Location +import Utility.Metered +#ifndef mingw32_HOST_OS +import Utility.CopyFile +#endif + +import Control.Concurrent +import Control.Concurrent.MSampleVar +import System.Process (std_in, std_err) + +remote :: RemoteType +remote = RemoteType { + typename = "git", + enumerate = list, + generate = gen, + setup = error "not supported" +} + +list :: Annex [Git.Repo] +list = do + c <- fromRepo Git.config + rs <- mapM (tweakurl c) =<< fromRepo Git.remotes + mapM configRead rs + where + annexurl n = "remote." ++ n ++ ".annexurl" + tweakurl c r = do + let n = fromJust $ Git.remoteName r + case M.lookup (annexurl n) c of + Nothing -> return r + Just url -> inRepo $ \g -> + Git.Construct.remoteNamed n $ + Git.Construct.fromRemoteLocation url g + +{- It's assumed to be cheap to read the config of non-URL remotes, so this is + - done each time git-annex is run in a way that uses remotes. + - + - Conversely, the config of an URL remote is only read when there is no + - cached UUID value. -} +configRead :: Git.Repo -> Annex Git.Repo +configRead r = do + g <- fromRepo id + let c = extractRemoteGitConfig g (Git.repoDescribe r) + u <- getRepoUUID r + case (repoCheap r, remoteAnnexIgnore c, u) of + (_, True, _) -> return r + (True, _, _) -> tryGitConfigRead r + (False, _, NoUUID) -> tryGitConfigRead r + _ -> return r + +repoCheap :: Git.Repo -> Bool +repoCheap = not . Git.repoIsUrl + +gen :: Git.Repo -> UUID -> RemoteConfig -> RemoteGitConfig -> Annex Remote +gen r u _ gc = go <$> remoteCost gc defcst + where + defcst = if repoCheap r then cheapRemoteCost else expensiveRemoteCost + go cst = new + where + new = Remote + { uuid = u + , cost = cst + , name = Git.repoDescribe r + , storeKey = copyToRemote new + , retrieveKeyFile = copyFromRemote new + , retrieveKeyFileCheap = copyFromRemoteCheap new + , removeKey = dropKey new + , hasKey = inAnnex r + , hasKeyCheap = repoCheap r + , whereisKey = Nothing + , config = M.empty + , localpath = if Git.repoIsLocal r || Git.repoIsLocalUnknown r + then Just $ Git.repoPath r + else Nothing + , repo = r + , gitconfig = gc + { remoteGitConfig = Just $ extractGitConfig r } + , readonly = Git.repoIsHttp r + , globallyAvailable = not $ Git.repoIsLocal r || Git.repoIsLocalUnknown r + , remotetype = remote + } + +{- Checks relatively inexpensively if a repository is available for use. -} +repoAvail :: Git.Repo -> Annex Bool +repoAvail r + | Git.repoIsHttp r = return True + | Git.repoIsUrl r = return True + | Git.repoIsLocalUnknown r = return False + | otherwise = liftIO $ catchBoolIO $ onLocal r $ return True + +{- Avoids performing an action on a local repository that's not usable. + - Does not check that the repository is still available on disk. -} +guardUsable :: Git.Repo -> a -> Annex a -> Annex a +guardUsable r onerr a + | Git.repoIsLocalUnknown r = return onerr + | otherwise = a + +{- Tries to read the config for a specified remote, updates state, and + - returns the updated repo. -} +tryGitConfigRead :: Git.Repo -> Annex Git.Repo +tryGitConfigRead r + | haveconfig r = return r -- already read + | Git.repoIsSsh r = store $ do + v <- onRemote r (pipedconfig, Left undefined) "configlist" [] [] + case v of + Right r' + | haveconfig r' -> return r' + | otherwise -> configlist_failed + Left _ -> configlist_failed + | Git.repoIsHttp r = do + headers <- getHttpHeaders + store $ geturlconfig headers + | Git.repoIsUrl r = return r + | otherwise = store $ safely $ onLocal r $ do + ensureInitialized + Annex.getState Annex.repo + where + haveconfig = not . M.null . Git.config + + -- Reading config can fail due to IO error or + -- for other reasons; catch all possible exceptions. + safely a = either (const $ return r) return + =<< liftIO (try a :: IO (Either SomeException Git.Repo)) + + pipedconfig cmd params = try run :: IO (Either SomeException Git.Repo) + where + run = 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' + p = proc cmd $ toCommand params + + geturlconfig headers = do + v <- liftIO $ withTmpFile "git-annex.tmp" $ \tmpfile h -> do + hClose h + ifM (Url.downloadQuiet (Git.repoLocation r ++ "/config") headers [] tmpfile) + ( pipedconfig "git" [Param "config", Param "--null", Param "--list", Param "--file", File tmpfile] + , return $ Left undefined + ) + case v of + Left _ -> do + set_ignore "not usable by git-annex" + return r + Right r' -> return r' + + store = observe $ \r' -> do + g <- gitRepo + let l = Git.remotes g + let g' = g { Git.remotes = exchange l r' } + Annex.changeState $ \s -> s { Annex.repo = g' } + + exchange [] _ = [] + exchange (old:ls) new + | Git.remoteName old == Git.remoteName new = + new : exchange ls new + | otherwise = + old : exchange ls new + + {- Is this remote just not available, or does + - it not have git-annex-shell? + - Find out by trying to fetch from the remote. -} + configlist_failed = case Git.remoteName r of + Nothing -> return r + Just n -> do + whenM (inRepo $ Git.Command.runBool [Param "fetch", Param "--quiet", Param n]) $ + set_ignore $ "does not have git-annex installed" + return r + + set_ignore msg = case Git.remoteName r of + Nothing -> noop + Just n -> do + let k = "remote." ++ n ++ ".annex-ignore" + warning $ "Remote " ++ n ++ " " ++ msg ++ "; setting " ++ k + inRepo $ Git.Command.run [Param "config", Param k, Param "true"] + +{- Checks if a given remote has the content for a key inAnnex. + - If the remote cannot be accessed, or if it cannot determine + - whether it has the content, returns a Left error message. + -} +inAnnex :: Git.Repo -> Key -> Annex (Either String Bool) +inAnnex r key + | Git.repoIsHttp r = checkhttp =<< getHttpHeaders + | Git.repoIsUrl r = checkremote + | otherwise = checklocal + where + checkhttp headers = do + showchecking + liftIO $ ifM (anyM (\u -> Url.check u headers (keySize key)) (keyUrls r key)) + ( return $ Right True + , return $ Left "not found" + ) + checkremote = do + showchecking + onRemote r (check, unknown) "inannex" [Param (key2file key)] [] + where + check c p = dispatch <$> safeSystem c p + dispatch ExitSuccess = Right True + dispatch (ExitFailure 1) = Right False + dispatch _ = unknown + checklocal = guardUsable r unknown $ dispatch <$> check + where + check = liftIO $ catchMsgIO $ onLocal r $ + Annex.Content.inAnnexSafe key + dispatch (Left e) = Left e + dispatch (Right (Just b)) = Right b + dispatch (Right Nothing) = unknown + unknown = Left $ "unable to check " ++ Git.repoDescribe r + showchecking = showAction $ "checking " ++ Git.repoDescribe r + +{- Runs an action on a local repository inexpensively, by making an annex + - monad using that repository. -} +onLocal :: Git.Repo -> Annex a -> IO a +onLocal r a = do + s <- Annex.new r + Annex.eval s $ do + -- No need to update the branch; its data is not used + -- for anything onLocal is used to do. + Annex.BranchState.disableUpdate + a + +keyUrls :: Git.Repo -> Key -> [String] +keyUrls r key = map tourl locs + where + tourl l = Git.repoLocation r ++ "/" ++ l +#ifndef mingw32_HOST_OS + locs = annexLocations key +#else + locs = map (replace "\\" "/") (annexLocations key) +#endif + +dropKey :: Remote -> Key -> Annex Bool +dropKey r key + | not $ Git.repoIsUrl (repo r) = + guardUsable (repo r) False $ commitOnCleanup r $ liftIO $ onLocal (repo r) $ do + ensureInitialized + whenM (Annex.Content.inAnnex key) $ do + Annex.Content.lockContent key $ + Annex.Content.removeAnnex key + logStatus key InfoMissing + Annex.Content.saveState True + return True + | Git.repoIsHttp (repo r) = error "dropping from http repo not supported" + | otherwise = commitOnCleanup r $ onRemote (repo r) (boolSystem, False) "dropkey" + [ Params "--quiet --force" + , Param $ key2file key + ] + [] + +{- Tries to copy a key's content from a remote's annex to a file. -} +copyFromRemote :: Remote -> Key -> AssociatedFile -> FilePath -> MeterUpdate -> Annex Bool +copyFromRemote r key file dest _p = copyFromRemote' r key file dest +copyFromRemote' :: Remote -> Key -> AssociatedFile -> FilePath -> Annex Bool +copyFromRemote' r key file dest + | not $ Git.repoIsUrl (repo r) = guardUsable (repo r) False $ do + let params = rsyncParams r + u <- getUUID + -- run copy from perspective of remote + liftIO $ onLocal (repo r) $ do + ensureInitialized + v <- Annex.Content.prepSendAnnex key + case v of + Nothing -> return False + Just (object, checksuccess) -> + upload u key file noRetry + (rsyncOrCopyFile params object dest) + <&&> checksuccess + | Git.repoIsSsh (repo r) = feedprogressback $ \feeder -> + rsyncHelper (Just feeder) + =<< rsyncParamsRemote r Download key dest file + | Git.repoIsHttp (repo r) = Annex.Content.downloadUrl (keyUrls (repo r) key) dest + | otherwise = error "copying from non-ssh, non-http repo not supported" + where + {- Feed local rsync's progress info back to the remote, + - by forking a feeder thread that runs + - git-annex-shell transferinfo at the same time + - git-annex-shell sendkey is running. + - + - To avoid extra password prompts, this is only done when ssh + - connection caching is supported. + - Note that it actually waits for rsync to indicate + - progress before starting transferinfo, in order + - to ensure ssh connection caching works and reuses + - the connection set up for the sendkey. + - + - Also note that older git-annex-shell does not support + - transferinfo, so stderr is dropped and failure ignored. + -} + feedprogressback a = ifM (isJust <$> sshCacheDir) + ( feedprogressback' a + , a $ const noop + ) + feedprogressback' a = do + u <- getUUID + let fields = (Fields.remoteUUID, fromUUID u) + : maybe [] (\f -> [(Fields.associatedFile, f)]) file + Just (cmd, params) <- git_annex_shell (repo r) "transferinfo" + [Param $ key2file key] fields + v <- liftIO $ (newEmptySV :: IO (MSampleVar Integer)) + tid <- liftIO $ forkIO $ void $ tryIO $ do + bytes <- readSV v + p <- createProcess $ + (proc cmd (toCommand params)) + { std_in = CreatePipe + , std_err = CreatePipe + } + hClose $ stderrHandle p + let h = stdinHandle p + let send b = do + hPutStrLn h $ show b + hFlush h + send bytes + forever $ + send =<< readSV v + let feeder = writeSV v . fromBytesProcessed + bracketIO noop (const $ tryIO $ killThread tid) (const $ a feeder) + +copyFromRemoteCheap :: Remote -> Key -> FilePath -> Annex Bool +#ifndef mingw32_HOST_OS +copyFromRemoteCheap r key file + | not $ Git.repoIsUrl (repo r) = guardUsable (repo r) False $ do + loc <- liftIO $ gitAnnexLocation key (repo r) $ + fromJust $ remoteGitConfig $ gitconfig r + liftIO $ catchBoolIO $ createSymbolicLink loc file >> return True + | Git.repoIsSsh (repo r) = + ifM (Annex.Content.preseedTmp key file) + ( copyFromRemote' r key Nothing file + , return False + ) + | otherwise = return False +#else +copyFromRemoteCheap _ _ _ = return False +#endif + +{- Tries to copy a key's content to a remote's annex. -} +copyToRemote :: Remote -> Key -> AssociatedFile -> MeterUpdate -> Annex Bool +copyToRemote r key file p + | not $ Git.repoIsUrl (repo r) = + guardUsable (repo r) False $ commitOnCleanup r $ + copylocal =<< Annex.Content.prepSendAnnex key + | Git.repoIsSsh (repo r) = commitOnCleanup r $ + Annex.Content.sendAnnex key noop $ \object -> + rsyncHelper (Just p) =<< rsyncParamsRemote r Upload key object file + | otherwise = error "copying to non-ssh repo not supported" + 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 True + , do + ensureInitialized + download u key file noRetry $ const $ + Annex.Content.saveState True `after` + Annex.Content.getViaTmpChecked (liftIO checksuccessio) key + (\d -> rsyncOrCopyFile params object d p) + ) + +rsyncHelper :: Maybe MeterUpdate -> [CommandParam] -> Annex Bool +rsyncHelper callback params = do + showOutput -- make way for progress bar + ifM (liftIO $ (maybe rsync rsyncProgress callback) params) + ( return True + , do + showLongNote "rsync failed -- run git annex again to resume file transfer" + return False + ) + +{- Copys a file with rsync unless both locations are on the same + - filesystem. Then cp could be faster. -} +rsyncOrCopyFile :: [CommandParam] -> FilePath -> FilePath -> MeterUpdate -> Annex Bool +rsyncOrCopyFile rsyncparams src dest p = +#ifdef mingw32_HOST_OS + dorsync + where +#else + ifM (sameDeviceIds src dest) (docopy, dorsync) + where + sameDeviceIds a b = (==) <$> (getDeviceId a) <*> (getDeviceId b) + getDeviceId f = deviceID <$> liftIO (getFileStatus $ parentDir f) + docopy = liftIO $ bracket + (forkIO $ watchfilesize zeroBytesProcessed) + (void . tryIO . killThread) + (const $ copyFileExternal src dest) + watchfilesize oldsz = do + threadDelay 500000 -- 0.5 seconds + v <- catchMaybeIO $ + toBytesProcessed . fileSize + <$> getFileStatus dest + case v of + Just sz + | sz /= oldsz -> do + p sz + watchfilesize sz + _ -> watchfilesize oldsz +#endif + dorsync = rsyncHelper (Just p) $ + rsyncparams ++ [File src, File dest] + +{- Generates rsync parameters that ssh to the remote and asks it + - to either receive or send the key's content. -} +rsyncParamsRemote :: Remote -> Direction -> Key -> FilePath -> AssociatedFile -> Annex [CommandParam] +rsyncParamsRemote r direction key file afile = do + u <- getUUID + direct <- isDirect + let fields = (Fields.remoteUUID, fromUUID u) + : (Fields.direct, if direct then "1" else "") + : maybe [] (\f -> [(Fields.associatedFile, f)]) afile + Just (shellcmd, shellparams) <- git_annex_shell (repo r) + (if direction == Download then "sendkey" else "recvkey") + [ Param $ key2file key ] + fields + -- Convert the ssh command into rsync command line. + let eparam = rsyncShell (Param shellcmd:shellparams) + let o = rsyncParams r + if direction == Download + then return $ o ++ rsyncopts eparam dummy (File file) + else return $ o ++ rsyncopts eparam (File file) dummy + where + rsyncopts ps source dest + | end ps == [dashdash] = ps ++ [source, dest] + | otherwise = ps ++ [dashdash, source, dest] + dashdash = Param "--" + {- The rsync shell parameter controls where rsync + - goes, so the source/dest parameter can be a dummy value, + - that just enables remote rsync mode. + - For maximum compatability with some patched rsyncs, + - the dummy value needs to still contain a hostname, + - even though this hostname will never be used. -} + dummy = Param "dummy:" + +-- --inplace to resume partial files +rsyncParams :: Remote -> [CommandParam] +rsyncParams r = [Params "--progress --inplace"] ++ + map Param (remoteAnnexRsyncOptions $ gitconfig r) + +commitOnCleanup :: Remote -> Annex a -> Annex a +commitOnCleanup r a = go `after` a + where + go = Annex.addCleanup (Git.repoLocation $ repo r) cleanup + cleanup + | not $ Git.repoIsUrl (repo r) = liftIO $ onLocal (repo r) $ + doQuietSideAction $ + Annex.Branch.commit "update" + | otherwise = void $ do + Just (shellcmd, shellparams) <- + git_annex_shell (repo r) "commit" [] [] + + -- Throw away stderr, since the remote may not + -- have a new enough git-annex shell to + -- support committing. + liftIO $ catchMaybeIO $ do + withQuietOutput createProcessSuccess $ + proc shellcmd $ + toCommand shellparams diff --git a/Remote/Glacier.hs b/Remote/Glacier.hs new file mode 100644 index 0000000000..c1a53347d8 --- /dev/null +++ b/Remote/Glacier.hs @@ -0,0 +1,294 @@ +{- Amazon Glacier remotes. + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Remote.Glacier (remote, jobList) where + +import qualified Data.Map as M +import qualified Data.Text as T +import System.Environment + +import Common.Annex +import Types.Remote +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 +import Crypto +import Creds +import Utility.Metered +import qualified Annex +import Annex.Content + +import System.Process + +type Vault = String +type Archive = FilePath + +remote :: RemoteType +remote = RemoteType { + typename = "glacier", + enumerate = findSpecialRemotes "glacier", + generate = gen, + setup = glacierSetup +} + +gen :: Git.Repo -> UUID -> RemoteConfig -> RemoteGitConfig -> Annex Remote +gen r u c gc = new <$> remoteCost gc veryExpensiveRemoteCost + where + new cst = encryptableRemote c + (storeEncrypted this) + (retrieveEncrypted this) + this + where + this = Remote { + uuid = u, + cost = cst, + name = Git.repoDescribe r, + storeKey = store this, + retrieveKeyFile = retrieve this, + retrieveKeyFileCheap = retrieveCheap this, + removeKey = remove this, + hasKey = checkPresent this, + hasKeyCheap = False, + whereisKey = Nothing, + config = c, + repo = r, + gitconfig = gc, + localpath = Nothing, + readonly = False, + globallyAvailable = True, + remotetype = remote + } + +glacierSetup :: UUID -> RemoteConfig -> Annex RemoteConfig +glacierSetup u c = do + c' <- encryptionSetup c + let fullconfig = c' `M.union` defaults + genVault fullconfig u + gitConfigSpecialRemote u fullconfig "glacier" "true" + setRemoteCredPair fullconfig (AWS.creds u) + where + remotename = fromJust (M.lookup "name" c) + defvault = remotename ++ "-" ++ fromUUID u + defaults = M.fromList + [ ("datacenter", T.unpack $ AWS.defaultRegion AWS.Glacier) + , ("vault", defvault) + ] + +store :: Remote -> Key -> AssociatedFile -> MeterUpdate -> Annex Bool +store r k _f p + | keySize k == Just 0 = do + warning "Cannot store empty files in Glacier." + return False + | otherwise = sendAnnex k (void $ remove r k) $ \src -> + metered (Just p) k $ \meterupdate -> + storeHelper r k $ streamMeteredFile src meterupdate + +storeEncrypted :: Remote -> (Cipher, Key) -> Key -> MeterUpdate -> Annex Bool +storeEncrypted r (cipher, enck) k p = sendAnnex k (void $ remove r enck) $ \src -> do + metered (Just p) k $ \meterupdate -> + storeHelper r enck $ \h -> + encrypt (getGpgOpts r) cipher (feedFile src) + (readBytes $ meteredWrite meterupdate h) + +retrieve :: Remote -> Key -> AssociatedFile -> FilePath -> MeterUpdate -> Annex Bool +retrieve r k _f d p = metered (Just p) k $ \meterupdate -> + retrieveHelper r k $ + readBytes $ meteredWriteFile meterupdate d + +retrieveCheap :: Remote -> Key -> FilePath -> Annex Bool +retrieveCheap _ _ _ = return False + +retrieveEncrypted :: Remote -> (Cipher, Key) -> Key -> FilePath -> MeterUpdate -> Annex Bool +retrieveEncrypted r (cipher, enck) k d p = metered (Just p) k $ \meterupdate -> + retrieveHelper r enck $ readBytes $ \b -> + decrypt cipher (feedBytes b) $ + readBytes $ meteredWriteFile meterupdate d + +storeHelper :: Remote -> Key -> (Handle -> IO ()) -> Annex Bool +storeHelper r k feeder = go =<< glacierEnv c u + where + c = config r + u = uuid r + params = glacierParams c + [ Param "archive" + , Param "upload" + , Param "--name", Param $ archive r k + , Param $ getVault $ config r + , Param "-" + ] + go Nothing = return False + go (Just e) = do + let p = (proc "glacier" (toCommand params)) { env = Just e } + liftIO $ catchBoolIO $ + withHandle StdinHandle createProcessSuccess p $ \h -> do + feeder h + return True + +retrieveHelper :: Remote -> Key -> (Handle -> IO ()) -> Annex Bool +retrieveHelper r k reader = go =<< glacierEnv c u + where + c = config r + u = uuid r + params = glacierParams c + [ Param "archive" + , Param "retrieve" + , Param "-o-" + , Param $ getVault $ config r + , Param $ archive r k + ] + go Nothing = return False + go (Just e) = do + let p = (proc "glacier" (toCommand params)) { env = Just e } + ok <- liftIO $ catchBoolIO $ + withHandle StdoutHandle createProcessSuccess p $ \h -> + ifM (hIsEOF h) + ( return False + , do + reader h + return True + ) + unless ok later + return ok + later = showLongNote "Recommend you wait up to 4 hours, and then run this command again." + +remove :: Remote -> Key -> Annex Bool +remove r k = glacierAction r + [ Param "archive" + , Param "delete" + , Param $ getVault $ config r + , Param $ archive r k + ] + +checkPresent :: Remote -> Key -> Annex (Either String Bool) +checkPresent r k = do + showAction $ "checking " ++ name r + go =<< glacierEnv (config r) (uuid r) + where + go Nothing = return $ Left "cannot check glacier" + go (Just e) = do + {- glacier checkpresent outputs the archive name to stdout if + - it's present. -} + v <- liftIO $ catchMsgIO $ + readProcessEnv "glacier" (toCommand params) (Just e) + case v of + Right s -> do + let probablypresent = key2file k `elem` lines s + if probablypresent + then ifM (Annex.getFlag "trustglacier") + ( return $ Right True, untrusted ) + else return $ Right False + Left err -> return $ Left err + + params = + [ Param "archive" + , Param "checkpresent" + , Param $ getVault $ config r + , Param "--quiet" + , Param $ archive r k + ] + + untrusted = return $ Left $ unlines + [ "Glacier's inventory says it has a copy." + , "However, the inventory could be out of date, if it was recently removed." + , "(Use --trust-glacier if you're sure it's still in Glacier.)" + , "" + ] + +glacierAction :: Remote -> [CommandParam] -> Annex Bool +glacierAction r params = runGlacier (config r) (uuid r) params + +runGlacier :: RemoteConfig -> UUID -> [CommandParam] -> Annex Bool +runGlacier c u params = go =<< glacierEnv c u + where + go Nothing = return False + go (Just e) = liftIO $ + boolSystemEnv "glacier" (glacierParams c params) (Just e) + +glacierParams :: RemoteConfig -> [CommandParam] -> [CommandParam] +glacierParams c params = datacenter:params + where + datacenter = Param $ "--region=" ++ + (fromJust $ M.lookup "datacenter" c) + +glacierEnv :: RemoteConfig -> UUID -> Annex (Maybe [(String, String)]) +glacierEnv c u = go =<< getRemoteCredPairFor "glacier" c creds + where + go Nothing = return Nothing + go (Just (user, pass)) = do + e <- liftIO getEnvironment + return $ Just $ (uk, user):(pk, pass):e + + creds = AWS.creds u + (uk, pk) = credPairEnvironment creds + +getVault :: RemoteConfig -> Vault +getVault = fromJust . M.lookup "vault" + +archive :: Remote -> Key -> Archive +archive r k = fileprefix ++ key2file k + where + fileprefix = M.findWithDefault "" "fileprefix" $ config r + +-- glacier vault create will succeed even if the vault already exists. +genVault :: RemoteConfig -> UUID -> Annex () +genVault c u = unlessM (runGlacier c u params) $ + error "Failed creating glacier vault." + where + params = + [ Param "vault" + , Param "create" + , Param $ getVault c + ] + +{- Partitions the input list of keys into ones which have + - glacier retieval jobs that have succeeded, or failed. + - + - A complication is that `glacier job list` will display the encrypted + - keys when the remote is encrypted. + -} +jobList :: Remote -> [Key] -> Annex ([Key], [Key]) +jobList r keys = go =<< glacierEnv (config r) (uuid r) + where + params = [ Param "job", Param "list" ] + nada = ([], []) + myvault = getVault $ config r + + go Nothing = return nada + go (Just e) = do + v <- liftIO $ catchMaybeIO $ + readProcessEnv "glacier" (toCommand params) (Just e) + maybe (return nada) extract v + + extract s = do + let result@(succeeded, failed) = + parse nada $ (map words . lines) s + if result == nada + then return nada + else do + enckeys <- forM keys $ \k -> + maybe k snd <$> cipherKey (config r) k + let keymap = M.fromList $ zip enckeys keys + let convert = catMaybes . map (`M.lookup` keymap) + return (convert succeeded, convert failed) + + parse c [] = c + parse c@(succeeded, failed) ((status:_date:vault:key:[]):rest) + | vault == myvault = + case file2key key of + Nothing -> parse c rest + Just k + | "a/d" `isPrefixOf` status -> + parse (k:succeeded, failed) rest + | "a/e" `isPrefixOf` status -> + parse (succeeded, k:failed) rest + | otherwise -> + parse c rest + parse c (_:rest) = parse c rest diff --git a/Remote/Helper/AWS.hs b/Remote/Helper/AWS.hs new file mode 100644 index 0000000000..1d80ff1b4f --- /dev/null +++ b/Remote/Helper/AWS.hs @@ -0,0 +1,66 @@ +{- Amazon Web Services common infrastructure. + - + - Copyright 2011,2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE OverloadedStrings, TupleSections #-} + +module Remote.Helper.AWS where + +import Common.Annex +import Creds + +import qualified Data.Map as M +import Data.Text (Text) + +creds :: UUID -> CredPairStorage +creds u = CredPairStorage + { credPairFile = fromUUID u + , credPairEnvironment = ("AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY") + , credPairRemoteKey = Just "s3creds" + } + +setCredsEnv :: CredPair -> IO () +setCredsEnv p = setEnvCredPair p $ creds undefined + +data Service = S3 | Glacier + deriving (Eq) + +type Region = Text + +regionMap :: Service -> M.Map Text Region +regionMap = M.fromList . regionInfo + +defaultRegion :: Service -> Region +defaultRegion = snd . Prelude.head . regionInfo + +{- S3 and Glacier use different names for some regions. Ie, "us-east-1" + - cannot be used with S3, while "US" cannot be used with Glacier. Dunno why. + - Also, Glacier is not yet available in all regions. -} +regionInfo :: Service -> [(Text, Region)] +regionInfo service = map (\(t, r) -> (t, fromServiceRegion r)) $ + filter (matchingService . snd) $ + concatMap (\(t, l) -> map (t,) l) regions + where + regions = + [ ("US East (N. Virginia)", [S3Region "US", GlacierRegion "us-east-1"]) + , ("US West (Oregon)", [BothRegion "us-west-2"]) + , ("US West (N. California)", [BothRegion "us-west-1"]) + , ("EU (Ireland)", [S3Region "EU", GlacierRegion "eu-west-1"]) + , ("Asia Pacific (Singapore)", [S3Region "ap-southeast-1"]) + , ("Asia Pacific (Tokyo)", [BothRegion "ap-northeast-1"]) + , ("Asia Pacific (Sydney)", [S3Region "ap-southeast-2"]) + , ("South America (São Paulo)", [S3Region "sa-east-1"]) + ] + + fromServiceRegion (BothRegion s) = s + fromServiceRegion (S3Region s) = s + fromServiceRegion (GlacierRegion s) = s + + matchingService (BothRegion _) = True + matchingService (S3Region _) = service == S3 + matchingService (GlacierRegion _) = service == Glacier + +data ServiceRegion = BothRegion Region | S3Region Region | GlacierRegion Region diff --git a/Remote/Helper/Chunked.hs b/Remote/Helper/Chunked.hs new file mode 100644 index 0000000000..46678de702 --- /dev/null +++ b/Remote/Helper/Chunked.hs @@ -0,0 +1,127 @@ +{- git-annex chunked remotes + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Remote.Helper.Chunked where + +import Common.Annex +import Utility.DataUnits +import Types.Remote +import Utility.Metered + +import qualified Data.Map as M +import qualified Data.ByteString.Lazy as L +import Data.Int +import qualified Control.Exception as E + +type ChunkSize = Maybe Int64 + +{- Gets a remote's configured chunk size. -} +chunkSize :: RemoteConfig -> ChunkSize +chunkSize m = + case M.lookup "chunksize" m of + Nothing -> Nothing + Just v -> case readSize dataUnits v of + Nothing -> error "bad chunksize" + Just size + | size <= 0 -> error "bad chunksize" + | otherwise -> Just $ fromInteger size + +{- This is an extension that's added to the usual file (or whatever) + - where the remote stores a key. -} +type ChunkExt = String + +{- A record of the number of chunks used. + - + - While this can be guessed at based on the size of the key, encryption + - makes that larger. Also, using this helps deal with changes to chunksize + - over the life of a remote. + -} +chunkCount :: ChunkExt +chunkCount = ".chunkcount" + +{- Parses the String from the chunkCount file, and returns the files that + - are used to store the chunks. -} +listChunks :: FilePath -> String -> [FilePath] +listChunks basedest chunkcount = take count $ map (basedest ++) chunkStream + where + count = fromMaybe 0 $ readish chunkcount + +{- An infinite stream of extensions to use for chunks. -} +chunkStream :: [ChunkExt] +chunkStream = map (\n -> ".chunk" ++ show n) [1 :: Integer ..] + +{- Given the base destination to use to store a value, + - generates a stream of temporary destinations (just one when not chunking) + - and passes it to an action, which should chunk and store the data, + - and return the destinations it stored to, or [] on error. Then + - calls the storer to write the chunk count (if chunking). Finally, the + - finalizer is called to rename the tmp into the dest + - (and do any other cleanup). + -} +storeChunks :: Key -> FilePath -> FilePath -> ChunkSize -> ([FilePath] -> IO [FilePath]) -> (FilePath -> String -> IO ()) -> (FilePath -> FilePath -> IO ()) -> IO Bool +storeChunks key tmp dest chunksize storer recorder finalizer = either onerr return + =<< (E.try go :: IO (Either E.SomeException Bool)) + where + go = do + stored <- storer tmpdests + when (chunksize /= Nothing) $ do + let chunkcount = basef ++ chunkCount + recorder chunkcount (show $ length stored) + finalizer tmp dest + return (not $ null stored) + onerr e = do + print e + return False + + basef = tmp ++ keyFile key + tmpdests + | chunksize == Nothing = [basef] + | otherwise = map (basef ++ ) chunkStream + +{- Given a list of destinations to use, chunks the data according to the + - ChunkSize, and runs the storer action to store each chunk. Returns + - the destinations where data was stored, or [] on error. + - + - This buffers each chunk in memory. + - More optimal versions of this can be written, that rely + - on L.toChunks to split the lazy bytestring into chunks (typically + - smaller than the ChunkSize), and eg, write those chunks to a Handle. + - But this is the best that can be done with the storer interface that + - writes a whole L.ByteString at a time. + -} +storeChunked :: ChunkSize -> [FilePath] -> (FilePath -> L.ByteString -> IO ()) -> L.ByteString -> IO [FilePath] +storeChunked chunksize dests storer content = either onerr return + =<< (E.try (go chunksize dests) :: IO (Either E.SomeException [FilePath])) + where + go _ [] = return [] -- no dests!? + go Nothing (d:_) = do + storer d content + return [d] + go (Just sz) _ + -- always write a chunk, even if the data is 0 bytes + | L.null content = go Nothing dests + | otherwise = storechunks sz [] dests content + + onerr e = do + print e + return [] + + storechunks _ _ [] _ = return [] -- ran out of dests + storechunks sz useddests (d:ds) b + | L.null b = return $ reverse useddests + | otherwise = do + let (chunk, b') = L.splitAt sz b + storer d chunk + storechunks sz (d:useddests) ds b' + +{- Writes a series of chunks to a file. The feeder is called to get + - each chunk. -} +meteredWriteFileChunks :: MeterUpdate -> FilePath -> [v] -> (v -> IO L.ByteString) -> IO () +meteredWriteFileChunks meterupdate dest chunks feeder = + withBinaryFile dest WriteMode $ \h -> + forM_ chunks $ \c -> + meteredWrite meterupdate h =<< feeder c diff --git a/Remote/Helper/Encryptable.hs b/Remote/Helper/Encryptable.hs new file mode 100644 index 0000000000..22e1c9fc0e --- /dev/null +++ b/Remote/Helper/Encryptable.hs @@ -0,0 +1,137 @@ +{- common functions for encryptable remotes + - + - Copyright 2011 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Remote.Helper.Encryptable where + +import qualified Data.Map as M + +import Common.Annex +import Types.Remote +import Crypto +import Types.Crypto +import qualified Annex +import Config.Cost +import Utility.Base64 +import Utility.Metered + +{- Encryption setup for a remote. The user must specify whether to use + - an encryption key, or not encrypt. An encrypted cipher is created, or is + - updated to be accessible to an additional encryption key. Or the user + - could opt to use a shared cipher, which is stored unencrypted. -} +encryptionSetup :: RemoteConfig -> Annex RemoteConfig +encryptionSetup c = case (M.lookup "encryption" c, extractCipher c) of + (Nothing, Nothing) -> error "Specify encryption=key or encryption=none or encryption=shared" + (Just "none", Nothing) -> return c + (Nothing, Just _) -> return c + (Just "shared", Just (SharedCipher _)) -> return c + (Just "none", Just _) -> cannotchange + (Just "shared", Just (EncryptedCipher _ _)) -> cannotchange + (Just _, Just (SharedCipher _)) -> cannotchange + (Just "shared", Nothing) -> use "encryption setup" . genSharedCipher + =<< highRandomQuality + (Just keyid, Nothing) -> use "encryption setup" . genEncryptedCipher keyid + =<< highRandomQuality + (Just keyid, Just v) -> use "encryption update" $ updateEncryptedCipher keyid v + where + cannotchange = error "Cannot change encryption type of existing remote." + use m a = do + showNote m + cipher <- liftIO a + showNote $ describeCipher cipher + return $ M.delete "encryption" $ M.delete "highRandomQuality" $ + storeCipher c cipher + highRandomQuality = + (&&) (maybe True ( /= "false") $ M.lookup "highRandomQuality" c) + <$> fmap not (Annex.getState Annex.fast) + +{- Modifies a Remote to support encryption. + - + - Two additional functions must be provided by the remote, + - to support storing and retrieving encrypted content. -} +encryptableRemote + :: RemoteConfig + -> ((Cipher, Key) -> Key -> MeterUpdate -> Annex Bool) + -> ((Cipher, Key) -> Key -> FilePath -> MeterUpdate -> Annex Bool) + -> Remote + -> Remote +encryptableRemote c storeKeyEncrypted retrieveKeyFileEncrypted r = + r { + storeKey = store, + retrieveKeyFile = retrieve, + retrieveKeyFileCheap = retrieveCheap, + removeKey = withkey $ removeKey r, + hasKey = withkey $ hasKey r, + cost = cost r + encryptedRemoteCostAdj + } + where + store k f p = cip k >>= maybe + (storeKey r k f p) + (\enck -> storeKeyEncrypted enck k p) + retrieve k f d p = cip k >>= maybe + (retrieveKeyFile r k f d p) + (\enck -> retrieveKeyFileEncrypted enck k d p) + retrieveCheap k d = cip k >>= maybe + (retrieveKeyFileCheap r k d) + (\_ -> return False) + withkey a k = cip k >>= maybe (a k) (a . snd) + cip = cipherKey c + +{- Gets encryption Cipher. The decrypted Ciphers are cached in the Annex + - state. -} +remoteCipher :: RemoteConfig -> Annex (Maybe Cipher) +remoteCipher c = go $ extractCipher c + where + go Nothing = return Nothing + go (Just encipher) = do + cache <- Annex.getState Annex.ciphers + case M.lookup encipher cache of + Just cipher -> return $ Just cipher + Nothing -> do + showNote "gpg" + cipher <- liftIO $ decryptCipher encipher + Annex.changeState (\s -> s { Annex.ciphers = M.insert encipher cipher cache }) + return $ Just cipher + +{- Checks if the remote's config allows storing creds in the remote's config. + - + - embedcreds=yes allows this, and embedcreds=no prevents it. + - + - If not set, the default is to only store creds when it's surely safe: + - When gpg encryption is used, in which case the creds will be encrypted + - using it. Not when a shared cipher is used. + -} +embedCreds :: RemoteConfig -> Bool +embedCreds c + | M.lookup "embedcreds" c == Just "yes" = True + | M.lookup "embedcreds" c == Just "no" = False + | isJust (M.lookup "cipherkeys" c) && isJust (M.lookup "cipher" c) = True + | otherwise = False + +{- Gets encryption Cipher, and encrypted version of Key. -} +cipherKey :: RemoteConfig -> Key -> Annex (Maybe (Cipher, Key)) +cipherKey c k = maybe Nothing make <$> remoteCipher c + where + make ciphertext = Just (ciphertext, encryptKey mac ciphertext k) + mac = fromMaybe defaultMac $ M.lookup "mac" c >>= readMac + +{- Stores an StorableCipher in a remote's configuration. -} +storeCipher :: RemoteConfig -> StorableCipher -> RemoteConfig +storeCipher c (SharedCipher t) = M.insert "cipher" (toB64 t) c +storeCipher c (EncryptedCipher t ks) = + M.insert "cipher" (toB64 t) $ M.insert "cipherkeys" (showkeys ks) c + where + showkeys (KeyIds l) = intercalate "," l + +{- Extracts an StorableCipher from a remote's configuration. -} +extractCipher :: RemoteConfig -> Maybe StorableCipher +extractCipher c = + case (M.lookup "cipher" c, M.lookup "cipherkeys" c) of + (Just t, Just ks) -> Just $ EncryptedCipher (fromB64 t) (readkeys ks) + (Just t, Nothing) -> Just $ SharedCipher (fromB64 t) + _ -> Nothing + where + readkeys = KeyIds . split "," diff --git a/Remote/Helper/Hooks.hs b/Remote/Helper/Hooks.hs new file mode 100644 index 0000000000..7c2bf68ca7 --- /dev/null +++ b/Remote/Helper/Hooks.hs @@ -0,0 +1,94 @@ +{- Adds hooks to remotes. + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE CPP #-} + +module Remote.Helper.Hooks (addHooks) where + +import qualified Data.Map as M + +import Common.Annex +import Types.Remote +import qualified Annex +import Annex.LockPool +#ifndef mingw32_HOST_OS +import Annex.Perms +#endif + +{- Modifies a remote's access functions to first run the + - annex-start-command hook, and trigger annex-stop-command on shutdown. + - This way, the hooks are only run when a remote is actively being used. + -} +addHooks :: Remote -> Remote +addHooks r = addHooks' r + (remoteAnnexStartCommand $ gitconfig r) + (remoteAnnexStopCommand $ gitconfig r) +addHooks' :: Remote -> Maybe String -> Maybe String -> Remote +addHooks' r Nothing Nothing = r +addHooks' r starthook stophook = r' + where + r' = r + { storeKey = \k f p -> wrapper $ storeKey r k f p + , retrieveKeyFile = \k f d p -> wrapper $ retrieveKeyFile r k f d p + , retrieveKeyFileCheap = \k f -> wrapper $ retrieveKeyFileCheap r k f + , removeKey = \k -> wrapper $ removeKey r k + , hasKey = \k -> wrapper $ hasKey r k + } + where + wrapper = runHooks r' starthook stophook + +runHooks :: Remote -> Maybe String -> Maybe String -> Annex a -> Annex a +runHooks r starthook stophook a = do + dir <- fromRepo gitAnnexRemotesDir + let lck = dir remoteid ++ ".lck" + whenM (not . any (== lck) . M.keys <$> getPool) $ do + liftIO $ createDirectoryIfMissing True dir + firstrun lck + a + where + remoteid = show (uuid r) + run Nothing = noop + run (Just command) = void $ liftIO $ + boolSystem "sh" [Param "-c", Param command] + firstrun lck = do + -- Take a shared lock; This indicates that git-annex + -- is using the remote, and prevents other instances + -- of it from running the stophook. If another + -- instance is shutting down right now, this + -- will block waiting for its exclusive lock to clear. + lockFile lck + + -- The starthook is run even if some other git-annex + -- is already running, and ran it before. + -- It would be difficult to use locking to ensure + -- it's only run once, and it's also possible for + -- git-annex to be interrupted before it can run the + -- stophook, in which case the starthook + -- would be run again by the next git-annex. + -- So, requiring idempotency is the right approach. + run starthook + + Annex.addCleanup (remoteid ++ "-stop-command") $ runstop lck +#ifndef __WINDOWS__ + runstop lck = do + -- Drop any shared lock we have, and take an + -- exclusive lock, without blocking. If the lock + -- succeeds, we're the only process using this remote, + -- so can stop it. + unlockFile lck + mode <- annexFileMode + fd <- liftIO $ noUmask mode $ + openFd lck ReadWrite (Just mode) defaultFileFlags + v <- liftIO $ tryIO $ + setLock fd (WriteLock, AbsoluteSeek, 0, 0) + case v of + Left _ -> noop + Right _ -> run stophook + liftIO $ closeFd fd +#else + runstop _lck = run stophook +#endif diff --git a/Remote/Helper/Special.hs b/Remote/Helper/Special.hs new file mode 100644 index 0000000000..7fc421f46f --- /dev/null +++ b/Remote/Helper/Special.hs @@ -0,0 +1,40 @@ +{- common functions for special remotes + - + - Copyright 2011 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Remote.Helper.Special where + +import qualified Data.Map as M + +import Common.Annex +import Types.Remote +import qualified Git +import qualified Git.Command +import qualified Git.Construct + +{- Special remotes don't have a configured url, so Git.Repo does not + - automatically generate remotes for them. This looks for a different + - configuration key instead. + -} +findSpecialRemotes :: String -> Annex [Git.Repo] +findSpecialRemotes s = do + m <- fromRepo Git.config + liftIO $ mapM construct $ remotepairs m + where + remotepairs = M.toList . M.filterWithKey match + construct (k,_) = Git.Construct.remoteNamedFromKey k Git.Construct.fromUnknown + match k _ = startswith "remote." k && endswith (".annex-"++s) k + +{- Sets up configuration for a special remote in .git/config. -} +gitConfigSpecialRemote :: UUID -> RemoteConfig -> String -> String -> Annex () +gitConfigSpecialRemote u c k v = do + set ("annex-"++k) v + set ("annex-uuid") (fromUUID u) + where + 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/Helper/Ssh.hs b/Remote/Helper/Ssh.hs new file mode 100644 index 0000000000..f8e9353b77 --- /dev/null +++ b/Remote/Helper/Ssh.hs @@ -0,0 +1,73 @@ +{- git-annex remote access with ssh + - + - Copyright 2011,2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Remote.Helper.Ssh where + +import Common.Annex +import qualified Git +import qualified Git.Url +import Annex.UUID +import Annex.Ssh +import Fields +import Types.GitConfig + +{- Generates parameters to ssh to a repository's host and run a command. + - Caller is responsible for doing any neccessary shellEscaping of the + - passed command. -} +sshToRepo :: Git.Repo -> [CommandParam] -> Annex [CommandParam] +sshToRepo repo sshcmd = do + g <- fromRepo id + let c = extractRemoteGitConfig g (Git.repoDescribe repo) + let opts = map Param $ remoteAnnexSshOptions c + let host = Git.Url.hostuser repo + params <- sshCachingOptions (host, Git.Url.port repo) opts + return $ params ++ Param host : sshcmd + +{- Generates parameters to run a git-annex-shell command on a remote + - repository. -} +git_annex_shell :: Git.Repo -> String -> [CommandParam] -> [(Field, String)] -> Annex (Maybe (FilePath, [CommandParam])) +git_annex_shell r command params fields + | not $ Git.repoIsUrl r = return $ Just (shellcmd, shellopts ++ fieldopts) + | Git.repoIsSsh r = do + uuid <- getRepoUUID r + sshparams <- sshToRepo r [Param $ sshcmd uuid ] + return $ Just ("ssh", sshparams) + | otherwise = return Nothing + where + dir = Git.repoPath r + shellcmd = "git-annex-shell" + shellopts = Param command : File dir : params + sshcmd uuid = unwords $ + shellcmd : map shellEscape (toCommand shellopts) ++ + uuidcheck uuid ++ + map shellEscape (toCommand fieldopts) + uuidcheck NoUUID = [] + uuidcheck (UUID u) = ["--uuid", u] + fieldopts + | null fields = [] + | otherwise = fieldsep : map fieldopt fields ++ [fieldsep] + fieldsep = Param "--" + fieldopt (field, value) = Param $ + fieldName field ++ "=" ++ value + +{- Uses a supplied function (such as boolSystem) to run a git-annex-shell + - command on a remote. + - + - Or, if the remote does not support running remote commands, returns + - a specified error value. -} +onRemote + :: Git.Repo + -> (FilePath -> [CommandParam] -> IO a, a) + -> String + -> [CommandParam] + -> [(Field, String)] + -> Annex a +onRemote r (with, errorval) command params fields = do + s <- git_annex_shell r command params fields + case s of + Just (c, ps) -> liftIO $ with c ps + Nothing -> return errorval diff --git a/Remote/Hook.hs b/Remote/Hook.hs new file mode 100644 index 0000000000..03f182a160 --- /dev/null +++ b/Remote/Hook.hs @@ -0,0 +1,155 @@ +{- A remote that provides hooks to run shell commands. + - + - Copyright 2011 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Remote.Hook (remote) where + +import qualified Data.ByteString.Lazy as L +import qualified Data.Map as M +import System.Environment + +import Common.Annex +import Types.Remote +import Types.Key +import qualified Git +import Config +import Config.Cost +import Annex.Content +import Remote.Helper.Special +import Remote.Helper.Encryptable +import Crypto +import Utility.Metered + +type Action = String +type HookName = String + +remote :: RemoteType +remote = RemoteType { + typename = "hook", + enumerate = findSpecialRemotes "hooktype", + generate = gen, + setup = hookSetup +} + +gen :: Git.Repo -> UUID -> RemoteConfig -> RemoteGitConfig -> Annex Remote +gen r u c gc = do + cst <- remoteCost gc expensiveRemoteCost + return $ encryptableRemote c + (storeEncrypted hooktype $ getGpgOpts gc) + (retrieveEncrypted hooktype) + Remote { + uuid = u, + cost = cst, + name = Git.repoDescribe r, + storeKey = store hooktype, + retrieveKeyFile = retrieve hooktype, + retrieveKeyFileCheap = retrieveCheap hooktype, + removeKey = remove hooktype, + hasKey = checkPresent r hooktype, + hasKeyCheap = False, + whereisKey = Nothing, + config = M.empty, + localpath = Nothing, + repo = r, + gitconfig = gc, + readonly = False, + globallyAvailable = False, + remotetype = remote + } + where + hooktype = fromMaybe (error "missing hooktype") $ remoteAnnexHookType gc + +hookSetup :: UUID -> RemoteConfig -> Annex RemoteConfig +hookSetup u c = do + let hooktype = fromMaybe (error "Specify hooktype=") $ + M.lookup "hooktype" c + c' <- encryptionSetup c + gitConfigSpecialRemote u c' "hooktype" hooktype + return c' + +hookEnv :: Action -> Key -> Maybe FilePath -> IO (Maybe [(String, String)]) +hookEnv action k f = Just <$> mergeenv (fileenv f ++ keyenv) + where + mergeenv l = M.toList . M.union (M.fromList l) + <$> M.fromList <$> getEnvironment + env s v = ("ANNEX_" ++ s, v) + keyenv = catMaybes + [ Just $ env "KEY" (key2file k) + , Just $ env "ACTION" action + , env "HASH_1" <$> headMaybe hashbits + , env "HASH_2" <$> headMaybe (drop 1 hashbits) + ] + fileenv Nothing = [] + fileenv (Just file) = [env "FILE" file] + hashbits = map takeDirectory $ splitPath $ hashDirMixed k + +lookupHook :: HookName -> Action -> Annex (Maybe String) +lookupHook hookname action = do + command <- getConfig (annexConfig hook) "" + if null command + then do + fallback <- getConfig (annexConfig $ hookfallback) "" + if null fallback + then do + warning $ "missing configuration for " ++ hook ++ " or " ++ hookfallback + return Nothing + else return $ Just fallback + else return $ Just command + where + hook = hookname ++ "-" ++ action ++ "-hook" + hookfallback = hookname ++ "-hook" + +runHook :: HookName -> Action -> Key -> Maybe FilePath -> Annex Bool -> Annex Bool +runHook hook action k f a = maybe (return False) run =<< lookupHook hook action + where + run command = do + showOutput -- make way for hook output + ifM (liftIO $ boolSystemEnv "sh" [Param "-c", Param command] =<< hookEnv action k f) + ( a + , do + warning $ hook ++ " hook exited nonzero!" + return False + ) + +store :: HookName -> Key -> AssociatedFile -> MeterUpdate -> Annex Bool +store h k _f _p = sendAnnex k (void $ remove h k) $ \src -> + runHook h "store" k (Just src) $ return True + +storeEncrypted :: HookName -> 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 gpgOpts cipher (feedFile src) $ + readBytes $ L.writeFile tmp + runHook h "store" enck (Just tmp) $ return True + +retrieve :: HookName -> Key -> AssociatedFile -> FilePath -> MeterUpdate -> Annex Bool +retrieve h k _f d _p = runHook h "retrieve" k (Just d) $ return True + +retrieveCheap :: HookName -> Key -> FilePath -> Annex Bool +retrieveCheap _ _ _ = return False + +retrieveEncrypted :: HookName -> (Cipher, Key) -> Key -> FilePath -> MeterUpdate -> Annex Bool +retrieveEncrypted h (cipher, enck) _ f _p = withTmp enck $ \tmp -> + runHook h "retrieve" enck (Just tmp) $ liftIO $ catchBoolIO $ do + decrypt cipher (feedFile tmp) $ + readBytes $ L.writeFile f + return True + +remove :: HookName -> Key -> Annex Bool +remove h k = runHook h "remove" k Nothing $ return True + +checkPresent :: Git.Repo -> HookName -> Key -> Annex (Either String Bool) +checkPresent r h k = do + showAction $ "checking " ++ Git.repoDescribe r + v <- lookupHook h action + liftIO $ catchMsgIO $ check v + where + action = "checkpresent" + findkey s = key2file k `elem` lines s + check Nothing = error $ action ++ " hook misconfigured" + check (Just hook) = do + env <- hookEnv action k Nothing + findkey <$> readProcessEnv "sh" ["-c", hook] env diff --git a/Remote/List.hs b/Remote/List.hs new file mode 100644 index 0000000000..0651d83aac --- /dev/null +++ b/Remote/List.hs @@ -0,0 +1,103 @@ +{-# LANGUAGE CPP #-} + +{- git-annex remote list + - + - Copyright 2011,2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Remote.List where + +import qualified Data.Map as M + +import Common.Annex +import qualified Annex +import Logs.Remote +import Types.Remote +import Types.GitConfig +import Annex.UUID +import Remote.Helper.Hooks +import qualified Git +import qualified Git.Config + +import qualified Remote.Git +#ifdef WITH_S3 +import qualified Remote.S3 +#endif +import qualified Remote.Bup +import qualified Remote.Directory +import qualified Remote.Rsync +import qualified Remote.Web +#ifdef WITH_WEBDAV +import qualified Remote.WebDAV +#endif +import qualified Remote.Glacier +import qualified Remote.Hook + +remoteTypes :: [RemoteType] +remoteTypes = + [ Remote.Git.remote +#ifdef WITH_S3 + , Remote.S3.remote +#endif + , Remote.Bup.remote + , Remote.Directory.remote + , Remote.Rsync.remote + , Remote.Web.remote +#ifdef WITH_WEBDAV + , Remote.WebDAV.remote +#endif + , Remote.Glacier.remote + , Remote.Hook.remote + ] + +{- Builds a list of all available Remotes. + - Since doing so can be expensive, the list is cached. -} +remoteList :: Annex [Remote] +remoteList = do + rs <- Annex.getState Annex.remotes + if null rs + then do + m <- readRemoteLog + rs' <- concat <$> mapM (process m) remoteTypes + Annex.changeState $ \s -> s { Annex.remotes = rs' } + return rs' + else return rs + where + process m t = enumerate t >>= mapM (remoteGen m t) + +{- Forces the remoteList to be re-generated, re-reading the git config. -} +remoteListRefresh :: Annex [Remote] +remoteListRefresh = do + newg <- inRepo Git.Config.reRead + Annex.changeState $ \s -> s + { Annex.remotes = [] + , Annex.repo = newg + } + remoteList + +{- Generates a Remote. -} +remoteGen :: (M.Map UUID RemoteConfig) -> RemoteType -> Git.Repo -> Annex Remote +remoteGen m t r = do + u <- getRepoUUID r + g <- fromRepo id + let gc = extractRemoteGitConfig g (Git.repoDescribe r) + let c = fromMaybe M.empty $ M.lookup u m + addHooks <$> generate t r u c gc + +{- Updates a local git Remote, re-reading its git config. -} +updateRemote :: Remote -> Annex Remote +updateRemote remote = do + m <- readRemoteLog + remote' <- updaterepo $ repo remote + remoteGen m (remotetype remote) remote' + where + updaterepo r + | Git.repoIsLocal r || Git.repoIsLocalUnknown r = + Remote.Git.configRead r + | otherwise = return r + +{- Checks if a remote is a special remote -} +specialRemote :: Remote -> Bool +specialRemote r = remotetype r /= Remote.Git.remote diff --git a/Remote/Rsync.hs b/Remote/Rsync.hs new file mode 100644 index 0000000000..a8efd84e7e --- /dev/null +++ b/Remote/Rsync.hs @@ -0,0 +1,289 @@ +{- A remote that is only accessible by rsync. + - + - Copyright 2011 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE CPP #-} + +module Remote.Rsync (remote) where + +import qualified Data.ByteString.Lazy as L +import qualified Data.Map as M +#ifndef mingw32_HOST_OS +import System.Posix.Process (getProcessID) +#else +import System.Random (getStdRandom, random) +#endif + +import Common.Annex +import Types.Remote +import qualified Git +import Config +import Config.Cost +import Annex.Content +import Annex.Ssh +import Remote.Helper.Special +import Remote.Helper.Encryptable +import Crypto +import Utility.Rsync +import Utility.CopyFile +import Utility.Metered +import Annex.Perms + +type RsyncUrl = String + +data RsyncOpts = RsyncOpts + { rsyncUrl :: RsyncUrl + , rsyncOptions :: [CommandParam] + , rsyncShellEscape :: Bool +} + +remote :: RemoteType +remote = RemoteType { + typename = "rsync", + enumerate = findSpecialRemotes "rsyncurl", + generate = gen, + setup = rsyncSetup +} + +gen :: Git.Repo -> UUID -> RemoteConfig -> RemoteGitConfig -> Annex Remote +gen r u c gc = do + cst <- remoteCost gc expensiveRemoteCost + (transport, url) <- rsyncTransport + let o = RsyncOpts url (transport ++ opts) escape + islocal = rsyncUrlIsPath $ rsyncUrl o + return $ encryptableRemote c + (storeEncrypted o $ getGpgOpts gc) + (retrieveEncrypted o) + Remote + { uuid = u + , cost = cst + , name = Git.repoDescribe r + , storeKey = store o + , retrieveKeyFile = retrieve o + , retrieveKeyFileCheap = retrieveCheap o + , removeKey = remove o + , hasKey = checkPresent r o + , hasKeyCheap = False + , whereisKey = Nothing + , config = M.empty + , repo = r + , gitconfig = gc + , localpath = if islocal + then Just $ rsyncUrl o + else Nothing + , readonly = False + , globallyAvailable = not $ islocal + , remotetype = remote + } + where + opts = map Param $ filter safe $ remoteAnnexRsyncOptions gc + escape = M.lookup "shellescape" c /= Just "no" + safe opt + -- Don't allow user to pass --delete to rsync; + -- that could cause it to delete other keys + -- in the same hash bucket as a key it sends. + | opt == "--delete" = False + | opt == "--delete-excluded" = False + | otherwise = True + rawurl = fromMaybe (error "missing rsyncurl") $ remoteAnnexRsyncUrl gc + (login,resturl) = case separate (=='@') rawurl of + (h, "") -> (Nothing, h) + (l, h) -> (Just l, h) + loginopt = maybe [] (\l -> ["-l",l]) login + fromNull as xs | null xs = as + | otherwise = xs + rsyncTransport = if rsyncUrlIsShell rawurl + then (\rsh -> return (rsyncShell rsh, resturl)) =<< + case fromNull ["ssh"] (remoteAnnexRsyncTransport gc) of + "ssh":sshopts -> do + let (port, sshopts') = sshReadPort sshopts + host = takeWhile (/=':') resturl + -- Connection caching + (Param "ssh":) <$> sshCachingOptions + (host, port) + (map Param $ loginopt ++ sshopts') + "rsh":rshopts -> return $ map Param $ "rsh" : + loginopt ++ rshopts + rsh -> error $ "Unknown Rsync transport: " + ++ unwords rsh + else return ([], rawurl) + +rsyncSetup :: UUID -> RemoteConfig -> Annex RemoteConfig +rsyncSetup u c = do + -- verify configuration is sane + let url = fromMaybe (error "Specify rsyncurl=") $ + M.lookup "rsyncurl" c + c' <- encryptionSetup c + + -- The rsyncurl is stored in git config, not only in this remote's + -- persistant state, so it can vary between hosts. + gitConfigSpecialRemote u c' "rsyncurl" url + return c' + +rsyncEscape :: RsyncOpts -> String -> String +rsyncEscape o s + | rsyncShellEscape o && rsyncUrlIsShell (rsyncUrl o) = shellEscape s + | otherwise = s + +rsyncUrls :: RsyncOpts -> Key -> [String] +rsyncUrls o k = map use annexHashes + where + use h = rsyncUrl o h k rsyncEscape o (f f) + f = keyFile k + +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 -> 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 gpgOpts cipher (feedFile src) $ + readBytes $ L.writeFile tmp + rsyncSend o p enck True tmp + +retrieve :: RsyncOpts -> Key -> AssociatedFile -> FilePath -> MeterUpdate -> Annex Bool +retrieve o k _ f p = rsyncRetrieve o k f (Just p) + +retrieveCheap :: RsyncOpts -> Key -> FilePath -> Annex Bool +retrieveCheap o k f = ifM (preseedTmp k f) ( rsyncRetrieve o k f Nothing , return False ) + +retrieveEncrypted :: RsyncOpts -> (Cipher, Key) -> Key -> FilePath -> MeterUpdate -> Annex Bool +retrieveEncrypted o (cipher, enck) _ f p = withTmp enck $ \tmp -> + ifM (rsyncRetrieve o enck tmp (Just p)) + ( liftIO $ catchBoolIO $ do + decrypt cipher (feedFile tmp) $ + readBytes $ L.writeFile f + return True + , return False + ) + +remove :: RsyncOpts -> Key -> Annex Bool +remove o k = do + ps <- sendParams + withRsyncScratchDir $ \tmp -> liftIO $ do + {- Send an empty directory to rysnc to make it delete. -} + let dummy = tmp keyFile k + createDirectoryIfMissing True dummy + rsync $ rsyncOptions o ++ ps ++ + map (\s -> Param $ "--include=" ++ s) includes ++ + [ Param "--exclude=*" -- exclude everything else + , Params "--quiet --delete --recursive" + , partialParams + , Param $ addTrailingPathSeparator dummy + , Param $ rsyncUrl o + ] + where + {- Specify include rules to match the directories where the + - content could be. Note that the parent directories have + - to also be explicitly included, due to how rsync + - traverses directories. -} + includes = concatMap use annexHashes + use h = let dir = h k in + [ parentDir dir + , dir + -- match content directory and anything in it + , dir keyFile k "***" + ] + +checkPresent :: Git.Repo -> RsyncOpts -> Key -> Annex (Either String Bool) +checkPresent r o k = do + showAction $ "checking " ++ Git.repoDescribe r + -- note: Does not currently differentiate between rsync failing + -- to connect, and the file not being present. + Right <$> check + where + check = untilTrue (rsyncUrls o k) $ \u -> + liftIO $ catchBoolIO $ do + withQuietOutput createProcessSuccess $ + proc "rsync" $ toCommand $ + rsyncOptions o ++ [Param u] + return True + +{- Rsync params to enable resumes of sending files safely, + - ensure that files are only moved into place once complete + -} +partialParams :: CommandParam +partialParams = Params "--partial --partial-dir=.rsync-partial" + +{- When sending files from crippled filesystems, the permissions can be all + - messed up, and it's better to use the default permissions on the + - destination. -} +sendParams :: Annex [CommandParam] +sendParams = ifM crippledFileSystem + ( return [rsyncUseDestinationPermissions] + , return [] + ) + +{- Runs an action in an empty scratch directory that can be used to build + - up trees for rsync. -} +withRsyncScratchDir :: (FilePath -> Annex Bool) -> Annex Bool +withRsyncScratchDir a = do +#ifndef mingw32_HOST_OS + v <- liftIO getProcessID +#else + v <- liftIO (getStdRandom random :: IO Int) +#endif + t <- fromRepo gitAnnexTmpDir + createAnnexDirectory t + let tmp = t "rsynctmp" show v + nuke tmp + liftIO $ createDirectoryIfMissing True tmp + nuke tmp `after` a tmp + where + nuke d = liftIO $ whenM (doesDirectoryExist d) $ + removeDirectoryRecursive d + +rsyncRetrieve :: RsyncOpts -> Key -> FilePath -> Maybe MeterUpdate -> Annex Bool +rsyncRetrieve o k dest callback = + untilTrue (rsyncUrls o k) $ \u -> rsyncRemote o callback + -- use inplace when retrieving to support resuming + [ Param "--inplace" + , Param u + , File dest + ] + +rsyncRemote :: RsyncOpts -> (Maybe MeterUpdate) -> [CommandParam] -> Annex Bool +rsyncRemote o callback params = do + showOutput -- make way for progress bar + ifM (liftIO $ (maybe rsync rsyncProgress callback) ps) + ( return True + , do + showLongNote "rsync failed -- run git annex again to resume file transfer" + return False + ) + where + defaultParams = [Params "--progress"] + ps = rsyncOptions o ++ defaultParams ++ params + +{- To send a single key is slightly tricky; need to build up a temporary + - directory structure to pass to rsync so it can create the hash + - directories. + - + - This would not be necessary if the hash directory structure used locally + - was always the same as that used on the rsync remote. So if that's ever + - unified, this gets nicer. + - (When we have the right hash directory structure, we can just + - pass --include=X --include=X/Y --include=X/Y/file --exclude=*) + -} +rsyncSend :: RsyncOpts -> MeterUpdate -> Key -> Bool -> FilePath -> Annex Bool +rsyncSend o callback k canrename src = withRsyncScratchDir $ \tmp -> do + let dest = tmp Prelude.head (keyPaths k) + liftIO $ createDirectoryIfMissing True $ parentDir dest + ok <- liftIO $ if canrename + then do + renameFile src dest + return True + else createLinkOrCopy src dest + ps <- sendParams + if ok + then rsyncRemote o (Just callback) $ ps ++ + [ Param "--recursive" + , partialParams + -- tmp/ to send contents of tmp dir + , File $ addTrailingPathSeparator tmp + , Param $ rsyncUrl o + ] + else return False diff --git a/Remote/S3.hs b/Remote/S3.hs new file mode 100644 index 0000000000..582bc2fdab --- /dev/null +++ b/Remote/S3.hs @@ -0,0 +1,336 @@ +{- S3 remotes + - + - Copyright 2011-2013 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Remote.S3 (remote, iaHost, isIA, isIAHost, iaItemUrl) where + +import Network.AWS.AWSConnection +import Network.AWS.S3Object hiding (getStorageClass) +import Network.AWS.S3Bucket hiding (size) +import Network.AWS.AWSResult +import qualified Data.Text as T +import qualified Data.ByteString.Lazy.Char8 as L +import qualified Data.Map as M +import Data.Char +import Network.Socket (HostName) + +import Common.Annex +import Types.Remote +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 +import Crypto +import Creds +import Utility.Metered +import Annex.Content +import Logs.Web + +type Bucket = String + +remote :: RemoteType +remote = RemoteType { + typename = "S3", + enumerate = findSpecialRemotes "s3", + generate = gen, + setup = s3Setup +} + +gen :: Git.Repo -> UUID -> RemoteConfig -> RemoteGitConfig -> Annex Remote +gen r u c gc = new <$> remoteCost gc expensiveRemoteCost + where + new cst = encryptableRemote c + (storeEncrypted this) + (retrieveEncrypted this) + this + where + this = Remote { + uuid = u, + cost = cst, + name = Git.repoDescribe r, + storeKey = store this, + retrieveKeyFile = retrieve this, + retrieveKeyFileCheap = retrieveCheap this, + removeKey = remove this c, + hasKey = checkPresent this, + hasKeyCheap = False, + whereisKey = Nothing, + config = c, + repo = r, + gitconfig = gc, + localpath = Nothing, + readonly = False, + globallyAvailable = True, + remotetype = remote + } + +s3Setup :: UUID -> RemoteConfig -> Annex RemoteConfig +s3Setup u c = if isIA c then archiveorg else defaulthost + where + remotename = fromJust (M.lookup "name" c) + defbucket = remotename ++ "-" ++ fromUUID u + defaults = M.fromList + [ ("datacenter", T.unpack $ AWS.defaultRegion AWS.S3) + , ("storageclass", "STANDARD") + , ("host", defaultAmazonS3Host) + , ("port", show defaultAmazonS3Port) + , ("bucket", defbucket) + ] + + use fullconfig = do + gitConfigSpecialRemote u fullconfig "s3" "true" + setRemoteCredPair fullconfig (AWS.creds u) + + defaulthost = do + c' <- encryptionSetup c + let fullconfig = c' `M.union` defaults + genBucket fullconfig u + use fullconfig + + archiveorg = do + showNote "Internet Archive mode" + maybe (error "specify bucket=") (const noop) $ + getBucket archiveconfig + writeUUIDFile archiveconfig u + use archiveconfig + where + archiveconfig = + -- hS3 does not pass through x-archive-* headers + M.mapKeys (replace "x-archive-" "x-amz-") $ + -- encryption does not make sense here + M.insert "encryption" "none" $ + M.union c $ + -- special constraints on key names + M.insert "mungekeys" "ia" $ + -- bucket created only when files are uploaded + M.insert "x-amz-auto-make-bucket" "1" $ + -- no default bucket name; should be human-readable + M.delete "bucket" defaults + +store :: Remote -> Key -> AssociatedFile -> MeterUpdate -> Annex Bool +store r k _f p = s3Action r False $ \(conn, bucket) -> + sendAnnex k (void $ remove' r k) $ \src -> do + ok <- s3Bool =<< storeHelper (conn, bucket) r k p src + + -- Store public URL to item in Internet Archive. + when (ok && isIA (config r)) $ + setUrlPresent k (iaKeyUrl r k) + + return ok + +storeEncrypted :: Remote -> (Cipher, Key) -> Key -> MeterUpdate -> Annex Bool +storeEncrypted r (cipher, enck) k p = s3Action r False $ \(conn, bucket) -> + -- 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 (getGpgOpts r) cipher (feedFile src) $ + readBytes $ L.writeFile tmp + s3Bool =<< storeHelper (conn, bucket) r enck p tmp + +storeHelper :: (AWSConnection, Bucket) -> Remote -> Key -> MeterUpdate -> FilePath -> Annex (AWSResult ()) +storeHelper (conn, bucket) r k p file = do + size <- maybe getsize (return . fromIntegral) $ keySize k + meteredBytes (Just p) size $ \meterupdate -> + liftIO $ withMeteredFile file meterupdate $ \content -> do + -- size is provided to S3 so the whole content + -- does not need to be buffered to calculate it + let object = S3Object + bucket (bucketFile r k) "" + (("Content-Length", show size) : getXheaders (config r)) + content + sendObject conn $ + setStorageClass (getStorageClass $ config r) object + where + getsize = liftIO $ fromIntegral . fileSize <$> getFileStatus file + +retrieve :: Remote -> Key -> AssociatedFile -> FilePath -> MeterUpdate -> Annex Bool +retrieve r k _f d p = s3Action r False $ \(conn, bucket) -> + metered (Just p) k $ \meterupdate -> do + res <- liftIO $ getObject conn $ bucketKey r bucket k + case res of + Right o -> do + liftIO $ meteredWriteFile meterupdate d $ + obj_data o + return True + Left e -> s3Warning e + +retrieveCheap :: Remote -> Key -> FilePath -> Annex Bool +retrieveCheap _ _ _ = return False + +retrieveEncrypted :: Remote -> (Cipher, Key) -> Key -> FilePath -> MeterUpdate -> Annex Bool +retrieveEncrypted r (cipher, enck) k d p = s3Action r False $ \(conn, bucket) -> + metered (Just p) k $ \meterupdate -> do + res <- liftIO $ getObject conn $ bucketKey r bucket enck + case res of + Right o -> liftIO $ decrypt cipher (\h -> meteredWrite meterupdate h $ obj_data o) $ + readBytes $ \content -> do + L.writeFile d content + return True + Left e -> s3Warning e + +{- Internet Archive doesn't easily allow removing content. + - While it may remove the file, there are generally other files + - derived from it that it does not remove. -} +remove :: Remote -> RemoteConfig -> Key -> Annex Bool +remove r c k + | isIA c = do + warning "Cannot remove content from the Internet Archive" + return False + | otherwise = remove' r k + +remove' :: Remote -> Key -> Annex Bool +remove' r k = s3Action r False $ \(conn, bucket) -> + s3Bool =<< liftIO (deleteObject conn $ bucketKey r bucket k) + +checkPresent :: Remote -> Key -> Annex (Either String Bool) +checkPresent r k = s3Action r noconn $ \(conn, bucket) -> do + showAction $ "checking " ++ name r + res <- liftIO $ getObjectInfo conn $ bucketKey r bucket k + case res of + Right _ -> return $ Right True + Left (AWSError _ _) -> return $ Right False + Left e -> return $ Left (s3Error e) + where + noconn = Left $ error "S3 not configured" + +s3Warning :: ReqError -> Annex Bool +s3Warning e = do + warning $ prettyReqError e + return False + +s3Error :: ReqError -> a +s3Error e = error $ prettyReqError e + +s3Bool :: AWSResult () -> Annex Bool +s3Bool (Right _) = return True +s3Bool (Left e) = s3Warning e + +s3Action :: Remote -> a -> ((AWSConnection, Bucket) -> Annex a) -> Annex a +s3Action r noconn action = do + let bucket = M.lookup "bucket" $ config r + conn <- s3Connection (config r) (uuid r) + case (bucket, conn) of + (Just b, Just c) -> action (c, b) + _ -> return noconn + +bucketFile :: Remote -> Key -> FilePath +bucketFile r = munge . key2file + where + munge s = case M.lookup "mungekeys" c of + Just "ia" -> iaMunge $ filePrefix c ++ s + _ -> filePrefix c ++ s + c = config r + +filePrefix :: RemoteConfig -> String +filePrefix = M.findWithDefault "" "fileprefix" + +bucketKey :: Remote -> Bucket -> Key -> S3Object +bucketKey r bucket k = S3Object bucket (bucketFile r k) "" [] L.empty + +{- Internet Archive limits filenames to a subset of ascii, + - with no whitespace. Other characters are xml entity + - encoded. -} +iaMunge :: String -> String +iaMunge = (>>= munge) + where + munge c + | isAsciiUpper c || isAsciiLower c || isNumber c = [c] + | c `elem` "_-.\"" = [c] + | isSpace c = [] + | otherwise = "&" ++ show (ord c) ++ ";" + +genBucket :: RemoteConfig -> UUID -> Annex () +genBucket c u = do + conn <- s3ConnectionRequired c u + showAction "checking bucket" + loc <- liftIO $ getBucketLocation conn bucket + case loc of + Right _ -> writeUUIDFile c u + Left err@(NetworkError _) -> s3Error err + Left (AWSError _ _) -> do + showAction $ "creating bucket in " ++ datacenter + res <- liftIO $ createBucketIn conn bucket datacenter + case res of + Right _ -> writeUUIDFile c u + Left err -> s3Error err + where + bucket = fromJust $ getBucket c + datacenter = fromJust $ M.lookup "datacenter" c + +{- Writes the UUID to an annex-uuid file within the bucket. + - + - If the file already exists in the bucket, it must match. + - + - Note that IA items do not get created by createBucketIn. + - Rather, they are created the first time a file is stored in them. + - So this also takes care of that. + -} +writeUUIDFile :: RemoteConfig -> UUID -> Annex () +writeUUIDFile c u = do + conn <- s3ConnectionRequired c u + go conn =<< liftIO (tryNonAsync $ getObject conn $ mkobject L.empty) + where + go _conn (Right (Right o)) = unless (obj_data o == uuidb) $ + error $ "This bucket is already in use by a different S3 special remote, with UUID: " ++ L.unpack (obj_data o) + go conn _ = do + let object = setStorageClass (getStorageClass c) (mkobject uuidb) + either s3Error return =<< liftIO (sendObject conn object) + + file = filePrefix c ++ "annex-uuid" + uuidb = L.pack $ fromUUID u + bucket = fromJust $ getBucket c + + mkobject = S3Object bucket file "" (getXheaders c) + +s3ConnectionRequired :: RemoteConfig -> UUID -> Annex AWSConnection +s3ConnectionRequired c u = + maybe (error "Cannot connect to S3") return =<< s3Connection c u + +s3Connection :: RemoteConfig -> UUID -> Annex (Maybe AWSConnection) +s3Connection c u = go =<< getRemoteCredPairFor "S3" c (AWS.creds u) + where + go Nothing = return Nothing + go (Just (ak, sk)) = return $ Just $ AWSConnection host port ak sk + + host = fromJust $ M.lookup "host" c + port = let s = fromJust $ M.lookup "port" c in + case reads s of + [(p, _)] -> p + _ -> error $ "bad S3 port value: " ++ s + +getBucket :: RemoteConfig -> Maybe Bucket +getBucket = M.lookup "bucket" + +getStorageClass :: RemoteConfig -> StorageClass +getStorageClass c = case fromJust $ M.lookup "storageclass" c of + "REDUCED_REDUNDANCY" -> REDUCED_REDUNDANCY + _ -> STANDARD + +getXheaders :: RemoteConfig -> [(String, String)] +getXheaders = filter isxheader . M.assocs + where + isxheader (h, _) = "x-amz-" `isPrefixOf` h + +{- Hostname to use for archive.org S3. -} +iaHost :: HostName +iaHost = "s3.us.archive.org" + +isIA :: RemoteConfig -> Bool +isIA c = maybe False isIAHost (M.lookup "host" c) + +isIAHost :: HostName -> Bool +isIAHost h = ".archive.org" `isSuffixOf` map toLower h + +iaItemUrl :: Bucket -> URLString +iaItemUrl bucket = "http://archive.org/details/" ++ bucket + +iaKeyUrl :: Remote -> Key -> URLString +iaKeyUrl r k = "http://archive.org/download/" ++ bucket ++ "/" ++ bucketFile r k + where + bucket = fromMaybe "" $ getBucket $ config r diff --git a/Remote/Web.hs b/Remote/Web.hs new file mode 100644 index 0000000000..2c59528ef9 --- /dev/null +++ b/Remote/Web.hs @@ -0,0 +1,95 @@ +{- Web remotes. + - + - Copyright 2011 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Remote.Web (remote) where + +import Common.Annex +import Types.Remote +import qualified Git +import qualified Git.Construct +import Annex.Content +import Config +import Config.Cost +import Logs.Web +import qualified Utility.Url as Url +import Types.Key +import Utility.Metered + +import qualified Data.Map as M + +remote :: RemoteType +remote = RemoteType { + typename = "web", + enumerate = list, + generate = gen, + setup = error "not supported" +} + +-- There is only one web remote, and it always exists. +-- (If the web should cease to exist, remove this module and redistribute +-- a new release to the survivors by carrier pigeon.) +list :: Annex [Git.Repo] +list = do + r <- liftIO $ Git.Construct.remoteNamed "web" Git.Construct.fromUnknown + return [r] + +gen :: Git.Repo -> UUID -> RemoteConfig -> RemoteGitConfig -> Annex Remote +gen r _ _ gc = + return Remote { + uuid = webUUID, + cost = expensiveRemoteCost, + name = Git.repoDescribe r, + storeKey = uploadKey, + retrieveKeyFile = downloadKey, + retrieveKeyFileCheap = downloadKeyCheap, + removeKey = dropKey, + hasKey = checkKey, + hasKeyCheap = False, + whereisKey = Just getUrls, + config = M.empty, + gitconfig = gc, + localpath = Nothing, + repo = r, + readonly = True, + globallyAvailable = True, + remotetype = remote + } + +downloadKey :: Key -> AssociatedFile -> FilePath -> MeterUpdate -> Annex Bool +downloadKey key _file dest _p = get =<< getUrls key + where + get [] = do + warning "no known url" + return False + get urls = do + showOutput -- make way for download progress bar + downloadUrl urls dest + +downloadKeyCheap :: Key -> FilePath -> Annex Bool +downloadKeyCheap _ _ = return False + +uploadKey :: Key -> AssociatedFile -> MeterUpdate -> Annex Bool +uploadKey _ _ _ = do + warning "upload to web not supported" + return False + +dropKey :: Key -> Annex Bool +dropKey k = do + mapM_ (setUrlMissing k) =<< getUrls k + return True + +checkKey :: Key -> Annex (Either String Bool) +checkKey key = do + us <- getUrls key + if null us + then return $ Right False + else return . Right =<< checkKey' key us +checkKey' :: Key -> [URLString] -> Annex Bool +checkKey' key us = untilTrue us $ \u -> do + showAction $ "checking " ++ u + headers <- getHttpHeaders + liftIO $ Url.check u headers (keySize key) diff --git a/Remote/WebDAV.hs b/Remote/WebDAV.hs new file mode 100644 index 0000000000..52fc32b3a7 --- /dev/null +++ b/Remote/WebDAV.hs @@ -0,0 +1,345 @@ +{- WebDAV remotes. + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE ScopedTypeVariables, CPP #-} + +module Remote.WebDAV (remote, davCreds, setCredsEnv, configUrl) where + +import Network.Protocol.HTTP.DAV +import qualified Data.Map as M +import qualified Data.ByteString.UTF8 as B8 +import qualified Data.ByteString.Lazy.UTF8 as L8 +import qualified Data.ByteString.Lazy as L +import Network.URI (normalizePathSegments) +import qualified Control.Exception as E +import Network.HTTP.Conduit (HttpException(..)) +import Network.HTTP.Types +import System.IO.Error + +import Common.Annex +import Types.Remote +import qualified Git +import Config +import Config.Cost +import Remote.Helper.Special +import Remote.Helper.Encryptable +import Remote.Helper.Chunked +import Crypto +import Creds +import Utility.Metered +import Annex.Content + +type DavUrl = String +type DavUser = B8.ByteString +type DavPass = B8.ByteString + +remote :: RemoteType +remote = RemoteType { + typename = "webdav", + enumerate = findSpecialRemotes "webdav", + generate = gen, + setup = webdavSetup +} + +gen :: Git.Repo -> UUID -> RemoteConfig -> RemoteGitConfig -> Annex Remote +gen r u c gc = new <$> remoteCost gc expensiveRemoteCost + where + new cst = encryptableRemote c + (storeEncrypted this) + (retrieveEncrypted this) + this + where + this = Remote { + uuid = u, + cost = cst, + name = Git.repoDescribe r, + storeKey = store this, + retrieveKeyFile = retrieve this, + retrieveKeyFileCheap = retrieveCheap this, + removeKey = remove this, + hasKey = checkPresent this, + hasKeyCheap = False, + whereisKey = Nothing, + config = c, + repo = r, + gitconfig = gc, + localpath = Nothing, + readonly = False, + globallyAvailable = True, + remotetype = remote + } + +webdavSetup :: UUID -> RemoteConfig -> Annex RemoteConfig +webdavSetup u c = do + let url = fromMaybe (error "Specify url=") $ + M.lookup "url" c + c' <- encryptionSetup c + creds <- getCreds c' u + testDav url creds + gitConfigSpecialRemote u c' "webdav" "true" + setRemoteCredPair c' (davCreds u) + +store :: Remote -> Key -> AssociatedFile -> MeterUpdate -> Annex Bool +store r k _f p = metered (Just p) k $ \meterupdate -> + davAction r False $ \(baseurl, user, pass) -> + sendAnnex k (void $ remove r k) $ \src -> + liftIO $ withMeteredFile src meterupdate $ + storeHelper r k baseurl user pass + +storeEncrypted :: Remote -> (Cipher, Key) -> Key -> MeterUpdate -> Annex Bool +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 (getGpgOpts r) cipher + (streamMeteredFile src meterupdate) $ + readBytes $ storeHelper r enck baseurl user pass + +storeHelper :: Remote -> Key -> DavUrl -> DavUser -> DavPass -> L.ByteString -> IO Bool +storeHelper r k baseurl user pass b = catchBoolIO $ do + davMkdir tmpurl user pass + storeChunks k tmpurl keyurl chunksize storer recorder finalizer + where + tmpurl = tmpLocation baseurl k + keyurl = davLocation baseurl k + chunksize = chunkSize $ config r + storer urls = storeChunked chunksize urls storehttp b + recorder url s = storehttp url (L8.fromString s) + finalizer srcurl desturl = do + void $ catchMaybeHttp (deleteContent desturl user pass) + davMkdir (urlParent desturl) user pass + moveContent srcurl (B8.fromString desturl) user pass + storehttp url v = putContent url user pass + (contentType, v) + +retrieveCheap :: Remote -> Key -> FilePath -> Annex Bool +retrieveCheap _ _ _ = return False + +retrieve :: Remote -> Key -> AssociatedFile -> FilePath -> MeterUpdate -> Annex Bool +retrieve r k _f d p = metered (Just p) k $ \meterupdate -> + davAction r False $ \(baseurl, user, pass) -> liftIO $ catchBoolIO $ + withStoredFiles r k baseurl user pass onerr $ \urls -> do + meteredWriteFileChunks meterupdate d urls $ \url -> do + mb <- davGetUrlContent url user pass + case mb of + Nothing -> throwIO "download failed" + Just b -> return b + return True + where + onerr _ = return False + +retrieveEncrypted :: Remote -> (Cipher, Key) -> Key -> FilePath -> MeterUpdate -> Annex Bool +retrieveEncrypted r (cipher, enck) k d p = metered (Just p) k $ \meterupdate -> + davAction r False $ \(baseurl, user, pass) -> liftIO $ catchBoolIO $ + withStoredFiles r enck baseurl user pass onerr $ \urls -> do + decrypt cipher (feeder user pass urls) $ + readBytes $ meteredWriteFile meterupdate d + return True + where + onerr _ = return False + + feeder _ _ [] _ = noop + feeder user pass (url:urls) h = do + mb <- davGetUrlContent url user pass + case mb of + Nothing -> throwIO "download failed" + Just b -> do + L.hPut h b + feeder user pass urls h + +remove :: Remote -> Key -> Annex Bool +remove r k = davAction r False $ \(baseurl, user, pass) -> liftIO $ do + -- Delete the key's whole directory, including any chunked + -- files, etc, in a single action. + let url = davLocation baseurl k + isJust <$> catchMaybeHttp (deleteContent url user pass) + +checkPresent :: Remote -> Key -> Annex (Either String Bool) +checkPresent r k = davAction r noconn go + where + noconn = Left $ error $ name r ++ " not configured" + + go (baseurl, user, pass) = do + showAction $ "checking " ++ name r + liftIO $ withStoredFiles r k baseurl user pass onerr check + where + check [] = return $ Right True + check (url:urls) = do + v <- davUrlExists url user pass + if v == Right True + then check urls + else return v + + {- Failed to read the chunkcount file; see if it's missing, + - or if there's a problem accessing it, + - or perhaps this was an intermittent error. -} + onerr url = do + v <- davUrlExists url user pass + if v == Right True + then return $ Left $ "failed to read " ++ url + else return v + +withStoredFiles + :: Remote + -> Key + -> DavUrl + -> DavUser + -> DavPass + -> (DavUrl -> IO a) + -> ([DavUrl] -> IO a) + -> IO a +withStoredFiles r k baseurl user pass onerr a + | isJust $ chunkSize $ config r = do + let chunkcount = keyurl ++ chunkCount + maybe (onerr chunkcount) (a . listChunks keyurl . L8.toString) + =<< davGetUrlContent chunkcount user pass + | otherwise = a [keyurl] + where + keyurl = davLocation baseurl k ++ keyFile k + +davAction :: Remote -> a -> ((DavUrl, DavUser, DavPass) -> Annex a) -> Annex a +davAction r unconfigured action = do + mcreds <- getCreds (config r) (uuid r) + case (mcreds, configUrl r) of + (Just (user, pass), Just url) -> + action (url, toDavUser user, toDavPass pass) + _ -> return unconfigured + +configUrl :: Remote -> Maybe DavUrl +configUrl r = M.lookup "url" $ config r + +toDavUser :: String -> DavUser +toDavUser = B8.fromString + +toDavPass :: String -> DavPass +toDavPass = B8.fromString + +{- The directory where files(s) for a key are stored. -} +davLocation :: DavUrl -> Key -> DavUrl +davLocation baseurl k = addTrailingPathSeparator $ + davUrl baseurl $ hashDirLower k keyFile k + +{- Where we store temporary data for a key as it's being uploaded. -} +tmpLocation :: DavUrl -> Key -> DavUrl +tmpLocation baseurl k = addTrailingPathSeparator $ + davUrl baseurl $ "tmp" keyFile k + +davUrl :: DavUrl -> FilePath -> DavUrl +davUrl baseurl file = baseurl file + +davUrlExists :: DavUrl -> DavUser -> DavPass -> IO (Either String Bool) +davUrlExists url user pass = decode <$> catchHttp get + where + decode (Right _) = Right True +#if ! MIN_VERSION_http_conduit(1,9,0) + decode (Left (Left (StatusCodeException status _))) +#else + decode (Left (Left (StatusCodeException status _ _))) +#endif + | statusCode status == statusCode notFound404 = Right False + decode (Left e) = Left $ showEitherException e +#if ! MIN_VERSION_DAV(0,4,0) + get = getProps url user pass +#else + get = getProps url user pass Nothing +#endif + +davGetUrlContent :: DavUrl -> DavUser -> DavPass -> IO (Maybe L.ByteString) +davGetUrlContent url user pass = fmap (snd . snd) <$> + catchMaybeHttp (getPropsAndContent url user pass) + +{- Creates a directory in WebDAV, if not already present; also creating + - any missing parent directories. -} +davMkdir :: DavUrl -> DavUser -> DavPass -> IO () +davMkdir url user pass = go url + where + make u = makeCollection u user pass + + go u = do + r <- E.try (make u) :: IO (Either E.SomeException Bool) + case r of + {- Parent directory is missing. Recurse to create + - it, and try once more to create the directory. -} + Right False -> do + go (urlParent u) + void $ make u + {- Directory created successfully -} + Right True -> return () + {- Directory already exists, or some other error + - occurred. In the latter case, whatever wanted + - to use this directory will fail. -} + Left _ -> return () + +{- Catches HTTP and IO exceptions. -} +catchMaybeHttp :: IO a -> IO (Maybe a) +catchMaybeHttp a = (Just <$> a) `E.catches` + [ E.Handler $ \(_e :: HttpException) -> return Nothing + , E.Handler $ \(_e :: E.IOException) -> return Nothing + ] + +{- Catches HTTP and IO exceptions -} +catchHttp :: IO a -> IO (Either EitherException a) +catchHttp a = (Right <$> a) `E.catches` + [ E.Handler $ \(e :: HttpException) -> return $ Left $ Left e + , E.Handler $ \(e :: E.IOException) -> return $ Left $ Right e + ] + +type EitherException = Either HttpException E.IOException + +showEitherException :: EitherException -> String +#if ! MIN_VERSION_http_conduit(1,9,0) +showEitherException (Left (StatusCodeException status _)) = +#else +showEitherException (Left (StatusCodeException status _ _)) = +#endif + show $ statusMessage status +showEitherException (Left httpexception) = show httpexception +showEitherException (Right ioexception) = show ioexception + +throwIO :: String -> IO a +throwIO msg = ioError $ mkIOError userErrorType msg Nothing Nothing + +urlParent :: DavUrl -> DavUrl +urlParent url = dropTrailingPathSeparator $ + normalizePathSegments (dropTrailingPathSeparator url ++ "/..") + where + +{- Test if a WebDAV store is usable, by writing to a test file, and then + - deleting the file. Exits with an IO error if not. -} +testDav :: String -> Maybe CredPair -> Annex () +testDav baseurl (Just (u, p)) = do + showSideAction "testing WebDAV server" + test "make directory" $ davMkdir baseurl user pass + test "write file" $ putContent testurl user pass + (contentType, L.empty) + test "delete file" $ deleteContent testurl user pass + where + test desc a = liftIO $ + either (\e -> throwIO $ "WebDAV failed to " ++ desc ++ ": " ++ showEitherException e) + (const noop) + =<< catchHttp a + + user = toDavUser u + pass = toDavPass p + testurl = davUrl baseurl "git-annex-test" +testDav _ Nothing = error "Need to configure webdav username and password." + +{- Content-Type to use for files uploaded to WebDAV. -} +contentType :: Maybe B8.ByteString +contentType = Just $ B8.fromString "application/octet-stream" + +getCreds :: RemoteConfig -> UUID -> Annex (Maybe CredPair) +getCreds c u = getRemoteCredPairFor "webdav" c (davCreds u) + +davCreds :: UUID -> CredPairStorage +davCreds u = CredPairStorage + { credPairFile = fromUUID u + , credPairEnvironment = ("WEBDAV_USERNAME", "WEBDAV_PASSWORD") + , credPairRemoteKey = Just "davcreds" + } + +setCredsEnv :: (String, String) -> IO () +setCredsEnv creds = setEnvCredPair creds $ davCreds undefined diff --git a/Seek.hs b/Seek.hs new file mode 100644 index 0000000000..817687b453 --- /dev/null +++ b/Seek.hs @@ -0,0 +1,169 @@ +{- git-annex command seeking + - + - These functions find appropriate files or other things based on + - the values a user passes to a command, and prepare actions operating + - on them. + - + - Copyright 2010-2013 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Seek where + +import System.PosixCompat.Files + +import Common.Annex +import Types.Command +import Types.Key +import Types.FileMatcher +import qualified Annex +import qualified Git +import qualified Git.Command +import qualified Git.LsFiles as LsFiles +import qualified Limit +import qualified Option +import Config +import Logs.Location +import Logs.Unused + +seekHelper :: ([FilePath] -> Git.Repo -> IO ([FilePath], IO Bool)) -> [FilePath] -> Annex [FilePath] +seekHelper a params = do + ll <- inRepo $ \g -> + runSegmentPaths (\fs -> Git.Command.leaveZombie <$> a fs g) params + {- Show warnings only for files/directories that do not exist. -} + forM_ (map fst $ filter (null . snd) $ zip params ll) $ \p -> + unlessM (isJust <$> liftIO (catchMaybeIO $ getSymbolicLinkStatus p)) $ + fileNotFound p + return $ concat ll + +withFilesInGit :: (FilePath -> CommandStart) -> CommandSeek +withFilesInGit a params = prepFiltered a $ seekHelper LsFiles.inRepo params + +withFilesNotInGit :: (FilePath -> CommandStart) -> CommandSeek +withFilesNotInGit a params = do + {- dotfiles are not acted on unless explicitly listed -} + files <- filter (not . dotfile) <$> + seekunless (null ps && not (null params)) ps + dotfiles <- seekunless (null dotps) dotps + prepFiltered a $ return $ concat $ segmentPaths params (files++dotfiles) + where + (dotps, ps) = partition dotfile params + seekunless True _ = return [] + seekunless _ l = do + force <- Annex.getState Annex.force + g <- gitRepo + liftIO $ Git.Command.leaveZombie <$> LsFiles.notInRepo force l g + +withPathContents :: ((FilePath, FilePath) -> CommandStart) -> CommandSeek +withPathContents a params = map a . concat <$> liftIO (mapM get params) + where + get p = ifM (isDirectory <$> getFileStatus p) + ( map (\f -> (f, makeRelative p f)) <$> dirContentsRecursive p + , return [(p, takeFileName p)] + ) + +withWords :: ([String] -> CommandStart) -> CommandSeek +withWords a params = return [a params] + +withStrings :: (String -> CommandStart) -> CommandSeek +withStrings a params = return $ map a params + +withPairs :: ((String, String) -> CommandStart) -> CommandSeek +withPairs a params = return $ map a $ pairs [] params + where + pairs c [] = reverse c + pairs c (x:y:xs) = pairs ((x,y):c) xs + pairs _ _ = error "expected pairs" + +withFilesToBeCommitted :: (String -> CommandStart) -> CommandSeek +withFilesToBeCommitted a params = prepFiltered a $ + seekHelper LsFiles.stagedNotDeleted params + +withFilesUnlocked :: (FilePath -> CommandStart) -> CommandSeek +withFilesUnlocked = withFilesUnlocked' LsFiles.typeChanged + +withFilesUnlockedToBeCommitted :: (FilePath -> CommandStart) -> CommandSeek +withFilesUnlockedToBeCommitted = withFilesUnlocked' LsFiles.typeChangedStaged + +withFilesUnlocked' :: ([FilePath] -> Git.Repo -> IO ([FilePath], IO Bool)) -> (FilePath -> CommandStart) -> CommandSeek +withFilesUnlocked' typechanged a params = do + -- unlocked files have changed type from a symlink to a regular file + typechangedfiles <- seekHelper typechanged params + let unlockedfiles = liftIO $ filterM notSymlink typechangedfiles + prepFiltered a unlockedfiles + +{- Finds files that may be modified. -} +withFilesMaybeModified :: (FilePath -> CommandStart) -> CommandSeek +withFilesMaybeModified a params = + prepFiltered a $ seekHelper LsFiles.modified params + +withKeys :: (Key -> CommandStart) -> CommandSeek +withKeys a params = return $ map (a . parse) params + where + parse p = fromMaybe (error "bad key") $ file2key p + +withValue :: Annex v -> (v -> CommandSeek) -> CommandSeek +withValue v a params = do + r <- v + a r params + +{- Modifies a seek action using the value of a field option, which is fed into + - a conversion function, and then is passed into the seek action. + - This ensures that the conversion function only runs once. + -} +withField :: Option -> (Maybe String -> Annex a) -> (a -> CommandSeek) -> CommandSeek +withField option converter = withValue $ + converter <=< Annex.getField $ Option.name option + +withFlag :: Option -> (Bool -> CommandSeek) -> CommandSeek +withFlag option = withValue $ Annex.getFlag (Option.name option) + +withNothing :: CommandStart -> CommandSeek +withNothing a [] = return [a] +withNothing _ _ = error "This command takes no parameters." + +{- If --all is specified, or in a bare repo, runs an action on all + - known keys. + - + - If --unused is specified, runs an action on all keys found by + - the last git annex unused scan. + - + - Otherwise, fall back to a regular CommandSeek action on + - whatever params were passed. -} +withKeyOptions :: (Key -> CommandStart) -> CommandSeek -> CommandSeek +withKeyOptions keyop fallbackop params = do + bare <- fromRepo Git.repoIsLocalBare + allkeys <- Annex.getFlag "all" <||> pure bare + unused <- Annex.getFlag "unused" + auto <- Annex.getState Annex.auto + case (allkeys , unused, auto ) of + (True , False , False) -> go loggedKeys + (False , True , False) -> go unusedKeys + (True , True , _ ) -> error "Cannot use --all with --unused." + (False , False , _ ) -> fallbackop params + (_ , _ , True ) + | bare -> error "Cannot use --auto in a bare repository." + | otherwise -> error "Cannot use --auto with --all or --unused." + where + go a = do + unless (null params) $ + error "Cannot mix --all or --unused with file names." + map keyop <$> a + +prepFiltered :: (FilePath -> CommandStart) -> Annex [FilePath] -> Annex [CommandStart] +prepFiltered a fs = do + matcher <- Limit.getMatcher + map (process matcher) <$> fs + where + process matcher f = ifM (matcher $ FileInfo f f) + ( a f , return Nothing ) + +notSymlink :: FilePath -> IO Bool +notSymlink f = liftIO $ not . isSymbolicLink <$> getSymbolicLinkStatus f + +whenNotDirect :: CommandSeek -> CommandSeek +whenNotDirect a params = ifM isDirect ( return [] , a params ) + +whenDirect :: CommandSeek -> CommandSeek +whenDirect a params = ifM isDirect ( a params, return [] ) diff --git a/Setup.hs b/Setup.hs new file mode 100644 index 0000000000..0a187bd953 --- /dev/null +++ b/Setup.hs @@ -0,0 +1,63 @@ +{-# LANGUAGE NamedFieldPuns #-} + +{- cabal setup file -} + +import Distribution.Simple +import Distribution.Simple.LocalBuildInfo +import Distribution.Simple.Setup +import Distribution.Simple.Utils (installOrdinaryFiles, rawSystemExit) +import Distribution.PackageDescription (PackageDescription(..)) +import Distribution.Verbosity (Verbosity) +import System.FilePath +import Control.Applicative +import Control.Monad +import System.Directory + +import qualified Build.DesktopFile as DesktopFile +import qualified Build.Configure as Configure + +main = defaultMainWithHooks simpleUserHooks + { preConf = configure + , postInst = myPostInst + } + +configure _ _ = do + Configure.run Configure.tests + return (Nothing, []) + +myPostInst :: Args -> InstallFlags -> PackageDescription -> LocalBuildInfo -> IO () +myPostInst _ (InstallFlags { installVerbosity }) pkg lbi = do + installGitAnnexShell dest verbosity pkg lbi + installManpages dest verbosity pkg lbi + installDesktopFile dest verbosity pkg lbi + where + dest = NoCopyDest + verbosity = fromFlag installVerbosity + +installGitAnnexShell :: CopyDest -> Verbosity -> PackageDescription -> LocalBuildInfo -> IO () +installGitAnnexShell copyDest verbosity pkg lbi = + rawSystemExit verbosity "ln" + ["-sf", "git-annex", dstBinDir "git-annex-shell"] + where + dstBinDir = bindir $ absoluteInstallDirs pkg lbi copyDest + +{- See http://www.haskell.org/haskellwiki/Cabal/Developer-FAQ#Installing_manpages + - + - Man pages are provided prebuilt in the tarball in cabal, + - but may not be available otherwise, in which case, skip installing them. + -} +installManpages :: CopyDest -> Verbosity -> PackageDescription -> LocalBuildInfo -> IO () +installManpages copyDest verbosity pkg lbi = + installOrdinaryFiles verbosity dstManDir =<< srcManpages + where + dstManDir = mandir (absoluteInstallDirs pkg lbi copyDest) "man1" + srcManpages = zip (repeat srcManDir) + <$> filterM doesFileExist manpages + srcManDir = "" + manpages = ["git-annex.1", "git-annex-shell.1"] + +installDesktopFile :: CopyDest -> Verbosity -> PackageDescription -> LocalBuildInfo -> IO () +installDesktopFile copyDest verbosity pkg lbi = + DesktopFile.install $ dstBinDir "git-annex" + where + dstBinDir = bindir $ absoluteInstallDirs pkg lbi copyDest diff --git a/Test.hs b/Test.hs new file mode 100644 index 0000000000..ef3f4e9753 --- /dev/null +++ b/Test.hs @@ -0,0 +1,1183 @@ +{- git-annex test suite + - + - Copyright 2010-2013 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE CPP #-} + +module Test where + +import Test.HUnit +import Test.QuickCheck +import Test.QuickCheck.Test + +import System.PosixCompat.Files +import Control.Exception.Extensible +import qualified Data.Map as M +import System.IO.HVFS (SystemFS(..)) +import qualified Text.JSON +import System.Path + +import Common + +import qualified Utility.SafeCommand +import qualified Annex +import qualified Annex.UUID +import qualified Backend +import qualified Git.CurrentRepo +import qualified Git.Filename +import qualified Locations +import qualified Types.KeySource +import qualified Types.Backend +import qualified Types.TrustLevel +import qualified Types +import qualified Logs.UUIDBased +import qualified Logs.Trust +import qualified Logs.Remote +import qualified Logs.Unused +import qualified Logs.Transfer +import qualified Logs.Presence +import qualified Remote +import qualified Types.Key +import qualified Types.Messages +import qualified Config +import qualified Config.Cost +import qualified Crypto +import qualified Init +import qualified Utility.Path +import qualified Utility.FileMode +import qualified Build.SysConfig +import qualified Utility.Format +import qualified Utility.Verifiable +import qualified Utility.Process +import qualified Utility.Misc +import qualified Utility.InodeCache +import qualified Utility.Env +import qualified Utility.Gpg +import qualified Utility.Matcher +import qualified Utility.Exception +#ifndef mingw32_HOST_OS +import qualified GitAnnex +#endif + +type TestEnv = M.Map String String + +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.)" + rs <- runhunit =<< prepare False +#ifndef mingw32_HOST_OS + directrs <- runhunit =<< prepare True +#else + -- Windows is only going to use direct mode, so don't test twice. + let directrs = [] +#endif + divider + propigate (rs++directrs) qcok + where + divider = putStrLn $ replicate 70 '-' + runhunit env = do + r <- forM hunit $ \t -> do + divider + t env + cleanup tmpdir + return r + +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 :: [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_matcher_sane" Utility.Matcher.prop_matcher_sane + , check "prop_HmacSha1WithCipher_sane" Crypto.prop_HmacSha1WithCipher_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 + +hunit :: [TestEnv -> IO Counts] +hunit = + -- test order matters, later tests may rely on state from earlier + [ 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 "union merge regression" test_union_merge_regression + , check "conflict resolution" test_conflict_resolution + , 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 + , check "preferred content" test_preferred_content + ] + where + check desc t env = do + putStrLn desc + runTestTT (t env) + +test_init :: TestEnv -> Test +test_init env = "git-annex init" ~: TestCase $ innewrepo env $ do + git_annex env "init" [reponame] @? "init failed" + handleforcedirect env + where + reponame = "test repo" + +test_add :: TestEnv -> Test +test_add env = "git-annex add" ~: TestList [basic, sha1dup, subdirs] + where + -- this test case runs in the main repo, to set up a basic + -- annexed file that later tests will use + basic = TestCase $ inmainrepo env $ do + writeFile annexedfile $ content annexedfile + git_annex env "add" [annexedfile] @? "add failed" + annexed_present annexedfile + writeFile sha1annexedfile $ content sha1annexedfile + git_annex env "add" [sha1annexedfile, "--backend=SHA1"] @? "add with SHA1 failed" + annexed_present sha1annexedfile + checkbackend sha1annexedfile backendSHA1 + writeFile wormannexedfile $ content wormannexedfile + git_annex env "add" [wormannexedfile, "--backend=WORM"] @? "add with WORM failed" + annexed_present wormannexedfile + checkbackend wormannexedfile backendWORM + boolSystem "git" [Params "rm --force -q", File wormannexedfile] @? "git rm failed" + writeFile ingitfile $ content ingitfile + boolSystem "git" [Param "add", File ingitfile] @? "git add failed" + boolSystem "git" [Params "commit -q -m commit"] @? "git commit failed" + git_annex env "add" [ingitfile] @? "add ingitfile should be no-op" + unannexed ingitfile + sha1dup = TestCase $ intmpclonerepo env $ do + writeFile sha1annexedfiledup $ content sha1annexedfiledup + git_annex env "add" [sha1annexedfiledup, "--backend=SHA1"] @? "add of second file with same SHA1 failed" + annexed_present sha1annexedfiledup + annexed_present sha1annexedfile + subdirs = TestCase $ intmpclonerepo env $ do + createDirectory "dir" + writeFile ("dir" "foo") $ content annexedfile + git_annex env "add" ["dir"] @? "add of subdir failed" + createDirectory "dir2" + writeFile ("dir2" "foo") $ content annexedfile +#ifndef mingw32_HOST_OS + {- This does not work on Windows, for whatever reason. -} + setCurrentDirectory "dir" + git_annex env "add" [".." "dir2"] @? "add of ../subdir failed" +#endif + +test_reinject :: TestEnv -> Test +test_reinject env = "git-annex reinject/fromkey" ~: TestCase $ intmpclonerepoInDirect env $ do + git_annex env "drop" ["--force", sha1annexedfile] @? "drop failed" + writeFile tmp $ content sha1annexedfile + r <- annexeval $ Types.Backend.getKey backendSHA1 $ + Types.KeySource.KeySource { Types.KeySource.keyFilename = tmp, Types.KeySource.contentLocation = tmp, Types.KeySource.inodeCache = Nothing } + let key = Types.Key.key2file $ fromJust r + git_annex env "reinject" [tmp, sha1annexedfile] @? "reinject failed" + git_annex env "fromkey" [key, sha1annexedfiledup] @? "fromkey failed for dup" + annexed_present sha1annexedfiledup + where + tmp = "tmpfile" + +test_unannex :: TestEnv -> Test +test_unannex env = "git-annex unannex" ~: TestList [nocopy, withcopy] + where + nocopy = "no content" ~: intmpclonerepo env $ do + annexed_notpresent annexedfile + git_annex env "unannex" [annexedfile] @? "unannex failed with no copy" + annexed_notpresent annexedfile + withcopy = "with content" ~: intmpclonerepo env $ do + git_annex env "get" [annexedfile] @? "get failed" + annexed_present annexedfile + git_annex env "unannex" [annexedfile, sha1annexedfile] @? "unannex failed" + unannexed annexedfile + git_annex env "unannex" [annexedfile] @? "unannex failed on non-annexed file" + unannexed annexedfile + git_annex env "unannex" [ingitfile] @? "unannex ingitfile should be no-op" + unannexed ingitfile + +test_drop :: TestEnv -> Test +test_drop env = "git-annex drop" ~: TestList [noremote, withremote, untrustedremote] + where + noremote = "no remotes" ~: TestCase $ intmpclonerepo env $ do + git_annex env "get" [annexedfile] @? "get failed" + boolSystem "git" [Params "remote rm origin"] + @? "git remote rm origin failed" + not <$> git_annex env "drop" [annexedfile] @? "drop wrongly succeeded with no known copy of file" + annexed_present annexedfile + git_annex env "drop" ["--force", annexedfile] @? "drop --force failed" + annexed_notpresent annexedfile + git_annex env "drop" [annexedfile] @? "drop of dropped file failed" + git_annex env "drop" [ingitfile] @? "drop ingitfile should be no-op" + unannexed ingitfile + withremote = "with remote" ~: TestCase $ intmpclonerepo env $ do + git_annex env "get" [annexedfile] @? "get failed" + annexed_present annexedfile + git_annex env "drop" [annexedfile] @? "drop failed though origin has copy" + annexed_notpresent annexedfile + inmainrepo env $ annexed_present annexedfile + untrustedremote = "untrusted remote" ~: TestCase $ intmpclonerepo env $ do + git_annex env "untrust" ["origin"] @? "untrust of origin failed" + git_annex env "get" [annexedfile] @? "get failed" + annexed_present annexedfile + not <$> git_annex env "drop" [annexedfile] @? "drop wrongly suceeded with only an untrusted copy of the file" + annexed_present annexedfile + inmainrepo env $ annexed_present annexedfile + +test_get :: TestEnv -> Test +test_get env = "git-annex get" ~: TestCase $ intmpclonerepo env $ do + inmainrepo env $ annexed_present annexedfile + annexed_notpresent annexedfile + git_annex env "get" [annexedfile] @? "get of file failed" + inmainrepo env $ annexed_present annexedfile + annexed_present annexedfile + git_annex env "get" [annexedfile] @? "get of file already here failed" + inmainrepo env $ annexed_present annexedfile + annexed_present annexedfile + inmainrepo env $ unannexed ingitfile + unannexed ingitfile + git_annex env "get" [ingitfile] @? "get ingitfile should be no-op" + inmainrepo env $ unannexed ingitfile + unannexed ingitfile + +test_move :: TestEnv -> Test +test_move env = "git-annex move" ~: TestCase $ intmpclonerepo env $ do + annexed_notpresent annexedfile + inmainrepo env $ annexed_present annexedfile + git_annex env "move" ["--from", "origin", annexedfile] @? "move --from of file failed" + annexed_present annexedfile + inmainrepo env $ annexed_notpresent annexedfile + git_annex env "move" ["--from", "origin", annexedfile] @? "move --from of file already here failed" + annexed_present annexedfile + inmainrepo env $ annexed_notpresent annexedfile + git_annex env "move" ["--to", "origin", annexedfile] @? "move --to of file failed" + inmainrepo env $ annexed_present annexedfile + annexed_notpresent annexedfile + git_annex env "move" ["--to", "origin", annexedfile] @? "move --to of file already there failed" + inmainrepo env $ annexed_present annexedfile + annexed_notpresent annexedfile + unannexed ingitfile + inmainrepo env $ unannexed ingitfile + git_annex env "move" ["--to", "origin", ingitfile] @? "move of ingitfile should be no-op" + unannexed ingitfile + inmainrepo env $ unannexed ingitfile + git_annex env "move" ["--from", "origin", ingitfile] @? "move of ingitfile should be no-op" + unannexed ingitfile + inmainrepo env $ unannexed ingitfile + +test_copy :: TestEnv -> Test +test_copy env = "git-annex copy" ~: TestCase $ intmpclonerepo env $ do + annexed_notpresent annexedfile + inmainrepo env $ annexed_present annexedfile + git_annex env "copy" ["--from", "origin", annexedfile] @? "copy --from of file failed" + annexed_present annexedfile + inmainrepo env $ annexed_present annexedfile + git_annex env "copy" ["--from", "origin", annexedfile] @? "copy --from of file already here failed" + annexed_present annexedfile + inmainrepo env $ annexed_present annexedfile + git_annex env "copy" ["--to", "origin", annexedfile] @? "copy --to of file already there failed" + annexed_present annexedfile + inmainrepo env $ annexed_present annexedfile + git_annex env "move" ["--to", "origin", annexedfile] @? "move --to of file already there failed" + annexed_notpresent annexedfile + inmainrepo env $ annexed_present annexedfile + unannexed ingitfile + inmainrepo env $ unannexed ingitfile + git_annex env "copy" ["--to", "origin", ingitfile] @? "copy of ingitfile should be no-op" + unannexed ingitfile + inmainrepo env $ unannexed ingitfile + git_annex env "copy" ["--from", "origin", ingitfile] @? "copy of ingitfile should be no-op" + checkregularfile ingitfile + checkcontent ingitfile + +test_preferred_content :: TestEnv -> Test +test_preferred_content env = "git-annex preferred-content" ~: TestCase $ intmpclonerepo env $ do + annexed_notpresent annexedfile + -- get --auto only looks at numcopies when preferred content is not + -- set, and with 1 copy existing, does not get the file. + git_annex env "get" ["--auto", annexedfile] @? "get --auto of file failed with default preferred content" + annexed_notpresent annexedfile + + git_annex env "content" [".", "standard"] @? "set expression to standard failed" + git_annex env "group" [".", "client"] @? "set group to standard failed" + git_annex env "get" ["--auto", annexedfile] @? "get --auto of file failed for client" + annexed_present annexedfile + git_annex env "ungroup" [".", "client"] @? "ungroup failed" + + git_annex env "content" [".", "standard"] @? "set expression to standard failed" + git_annex env "group" [".", "manual"] @? "set group to manual failed" + -- drop --auto with manual leaves the file where it is + git_annex env "drop" ["--auto", annexedfile] @? "drop --auto of file failed with manual preferred content" + annexed_present annexedfile + git_annex env "drop" [annexedfile] @? "drop of file failed" + annexed_notpresent annexedfile + -- get --auto with manual does not get the file + git_annex env "get" ["--auto", annexedfile] @? "get --auto of file failed with manual preferred content" + annexed_notpresent annexedfile + git_annex env "ungroup" [".", "client"] @? "ungroup failed" + + git_annex env "content" [".", "exclude=*"] @? "set expression to exclude=* failed" + git_annex env "get" [annexedfile] @? "get of file failed" + annexed_present annexedfile + git_annex env "drop" ["--auto", annexedfile] @? "drop --auto of file failed with exclude=*" + annexed_notpresent annexedfile + git_annex env "get" ["--auto", annexedfile] @? "get --auto of file failed with exclude=*" + annexed_notpresent annexedfile + +test_lock :: TestEnv -> Test +test_lock env = "git-annex unlock/lock" ~: intmpclonerepoInDirect env $ do + -- regression test: unlock of not present file should skip it + annexed_notpresent annexedfile + not <$> git_annex env "unlock" [annexedfile] @? "unlock failed to fail with not present file" + annexed_notpresent annexedfile + + git_annex env "get" [annexedfile] @? "get of file failed" + annexed_present annexedfile + git_annex env "unlock" [annexedfile] @? "unlock failed" + unannexed annexedfile + -- write different content, to verify that lock + -- throws it away + changecontent annexedfile + writeFile annexedfile $ content annexedfile ++ "foo" + git_annex env "lock" [annexedfile] @? "lock failed" + annexed_present annexedfile + git_annex env "unlock" [annexedfile] @? "unlock failed" + unannexed annexedfile + changecontent annexedfile + git_annex env "add" [annexedfile] @? "add of modified file failed" + runchecks [checklink, checkunwritable] annexedfile + c <- readFile annexedfile + assertEqual "content of modified file" c (changedcontent annexedfile) + r' <- git_annex env "drop" [annexedfile] + not r' @? "drop wrongly succeeded with no known copy of modified file" + +test_edit :: TestEnv -> Test +test_edit env = "git-annex edit/commit" ~: TestList [t False, t True] + where t precommit = TestCase $ intmpclonerepoInDirect env $ do + git_annex env "get" [annexedfile] @? "get of file failed" + annexed_present annexedfile + git_annex env "edit" [annexedfile] @? "edit failed" + unannexed annexedfile + changecontent annexedfile + boolSystem "git" [Param "add", File annexedfile] + @? "git add of edited file failed" + if precommit + then git_annex env "pre-commit" [] + @? "pre-commit failed" + else boolSystem "git" [Params "commit -q -m contentchanged"] + @? "git commit of edited file failed" + runchecks [checklink, checkunwritable] annexedfile + c <- readFile annexedfile + assertEqual "content of modified file" c (changedcontent annexedfile) + not <$> git_annex env "drop" [annexedfile] @? "drop wrongly succeeded with no known copy of modified file" + +test_fix :: TestEnv -> Test +test_fix env = "git-annex fix" ~: intmpclonerepoInDirect env $ do + annexed_notpresent annexedfile + git_annex env "fix" [annexedfile] @? "fix of not present failed" + annexed_notpresent annexedfile + git_annex env "get" [annexedfile] @? "get of file failed" + annexed_present annexedfile + git_annex env "fix" [annexedfile] @? "fix of present file failed" + annexed_present annexedfile + createDirectory subdir + boolSystem "git" [Param "mv", File annexedfile, File subdir] + @? "git mv failed" + git_annex env "fix" [newfile] @? "fix of moved file failed" + runchecks [checklink, checkunwritable] newfile + c <- readFile newfile + assertEqual "content of moved file" c (content annexedfile) + where + subdir = "s" + newfile = subdir ++ "/" ++ annexedfile + +test_trust :: TestEnv -> Test +test_trust env = "git-annex trust/untrust/semitrust/dead" ~: intmpclonerepo env $ do + git_annex env "trust" [repo] @? "trust failed" + trustcheck Logs.Trust.Trusted "trusted 1" + git_annex env "trust" [repo] @? "trust of trusted failed" + trustcheck Logs.Trust.Trusted "trusted 2" + git_annex env "untrust" [repo] @? "untrust failed" + trustcheck Logs.Trust.UnTrusted "untrusted 1" + git_annex env "untrust" [repo] @? "untrust of untrusted failed" + trustcheck Logs.Trust.UnTrusted "untrusted 2" + git_annex env "dead" [repo] @? "dead failed" + trustcheck Logs.Trust.DeadTrusted "deadtrusted 1" + git_annex env "dead" [repo] @? "dead of dead failed" + trustcheck Logs.Trust.DeadTrusted "deadtrusted 2" + git_annex env "semitrust" [repo] @? "semitrust failed" + trustcheck Logs.Trust.SemiTrusted "semitrusted 1" + git_annex env "semitrust" [repo] @? "semitrust of semitrusted failed" + trustcheck Logs.Trust.SemiTrusted "semitrusted 2" + where + repo = "origin" + trustcheck expected msg = do + present <- annexeval $ do + l <- Logs.Trust.trustGet expected + u <- Remote.nameToUUID repo + return $ u `elem` l + assertBool msg present + +test_fsck :: TestEnv -> Test +test_fsck env = "git-annex fsck" ~: TestList [basicfsck, barefsck, withlocaluntrusted, withremoteuntrusted] + where + basicfsck = TestCase $ intmpclonerepo env $ do + git_annex env "fsck" [] @? "fsck failed" + boolSystem "git" [Params "config annex.numcopies 2"] @? "git config failed" + fsck_should_fail "numcopies unsatisfied" + boolSystem "git" [Params "config annex.numcopies 1"] @? "git config failed" + corrupt annexedfile + corrupt sha1annexedfile + barefsck = TestCase $ intmpbareclonerepo env $ do + git_annex env "fsck" [] @? "fsck failed" + withlocaluntrusted = TestCase $ intmpclonerepo env $ do + git_annex env "get" [annexedfile] @? "get failed" + git_annex env "untrust" ["origin"] @? "untrust of origin repo failed" + git_annex env "untrust" ["."] @? "untrust of current repo failed" + fsck_should_fail "content only available in untrusted (current) repository" + git_annex env "trust" ["."] @? "trust of current repo failed" + git_annex env "fsck" [annexedfile] @? "fsck failed on file present in trusted repo" + withremoteuntrusted = TestCase $ intmpclonerepo env $ do + boolSystem "git" [Params "config annex.numcopies 2"] @? "git config failed" + git_annex env "get" [annexedfile] @? "get failed" + git_annex env "get" [sha1annexedfile] @? "get failed" + git_annex env "fsck" [] @? "fsck failed with numcopies=2 and 2 copies" + git_annex env "untrust" ["origin"] @? "untrust of origin failed" + fsck_should_fail "content not replicated to enough non-untrusted repositories" + + corrupt f = do + git_annex env "get" [f] @? "get of file failed" + Utility.FileMode.allowWrite f + writeFile f (changedcontent f) + ifM (annexeval Config.isDirect) + ( git_annex env "fsck" [] @? "fsck failed in direct mode with changed file content" + , not <$> git_annex env "fsck" [] @? "fsck failed to fail with corrupted file content" + ) + git_annex env "fsck" [] @? "fsck unexpectedly failed again; previous one did not fix problem with " ++ f + fsck_should_fail m = do + not <$> git_annex env "fsck" [] @? "fsck failed to fail with " ++ m + +test_migrate :: TestEnv -> Test +test_migrate env = "git-annex migrate" ~: TestList [t False, t True] + where t usegitattributes = TestCase $ intmpclonerepoInDirect env $ do + annexed_notpresent annexedfile + annexed_notpresent sha1annexedfile + git_annex env "migrate" [annexedfile] @? "migrate of not present failed" + git_annex env "migrate" [sha1annexedfile] @? "migrate of not present failed" + git_annex env "get" [annexedfile] @? "get of file failed" + git_annex env "get" [sha1annexedfile] @? "get of file failed" + annexed_present annexedfile + annexed_present sha1annexedfile + if usegitattributes + then do + writeFile ".gitattributes" $ "* annex.backend=SHA1" + git_annex env "migrate" [sha1annexedfile] + @? "migrate sha1annexedfile failed" + git_annex env "migrate" [annexedfile] + @? "migrate annexedfile failed" + else do + git_annex env "migrate" [sha1annexedfile, "--backend", "SHA1"] + @? "migrate sha1annexedfile failed" + git_annex env "migrate" [annexedfile, "--backend", "SHA1"] + @? "migrate annexedfile failed" + annexed_present annexedfile + annexed_present sha1annexedfile + checkbackend annexedfile backendSHA1 + checkbackend sha1annexedfile backendSHA1 + + -- check that reversing a migration works + writeFile ".gitattributes" $ "* annex.backend=SHA256" + git_annex env "migrate" [sha1annexedfile] + @? "migrate sha1annexedfile failed" + git_annex env "migrate" [annexedfile] + @? "migrate annexedfile failed" + annexed_present annexedfile + annexed_present sha1annexedfile + checkbackend annexedfile backendSHA256 + checkbackend sha1annexedfile backendSHA256 + +test_unused :: TestEnv -> Test +-- This test is broken in direct mode +test_unused env = "git-annex unused/dropunused" ~: intmpclonerepoInDirect env $ do + -- keys have to be looked up before files are removed + annexedfilekey <- annexeval $ findkey annexedfile + sha1annexedfilekey <- annexeval $ findkey sha1annexedfile + git_annex env "get" [annexedfile] @? "get of file failed" + git_annex env "get" [sha1annexedfile] @? "get of file failed" + checkunused [] "after get" + boolSystem "git" [Params "rm -fq", File annexedfile] @? "git rm failed" + checkunused [] "after rm" + boolSystem "git" [Params "commit -q -m foo"] @? "git commit failed" + checkunused [] "after commit" + -- unused checks origin/master; once it's gone it is really unused + boolSystem "git" [Params "remote rm origin"] @? "git remote rm origin failed" + checkunused [annexedfilekey] "after origin branches are gone" + boolSystem "git" [Params "rm -fq", File sha1annexedfile] @? "git rm failed" + boolSystem "git" [Params "commit -q -m foo"] @? "git commit failed" + checkunused [annexedfilekey, sha1annexedfilekey] "after rm sha1annexedfile" + + -- good opportunity to test dropkey also + git_annex env "dropkey" ["--force", Types.Key.key2file annexedfilekey] + @? "dropkey failed" + checkunused [sha1annexedfilekey] ("after dropkey --force " ++ Types.Key.key2file annexedfilekey) + + not <$> git_annex env "dropunused" ["1"] @? "dropunused failed to fail without --force" + git_annex env "dropunused" ["--force", "1"] @? "dropunused failed" + checkunused [] "after dropunused" + not <$> git_annex env "dropunused" ["--force", "10", "501"] @? "dropunused failed to fail on bogus numbers" + + where + checkunused expectedkeys desc = do + git_annex env "unused" [] @? "unused failed" + unusedmap <- annexeval $ Logs.Unused.readUnusedLog "" + let unusedkeys = M.elems unusedmap + assertEqual ("unused keys differ " ++ desc) + (sort expectedkeys) (sort unusedkeys) + findkey f = do + r <- Backend.lookupFile f + return $ fst $ fromJust r + +test_describe :: TestEnv -> Test +test_describe env = "git-annex describe" ~: intmpclonerepo env $ do + git_annex env "describe" [".", "this repo"] @? "describe 1 failed" + git_annex env "describe" ["origin", "origin repo"] @? "describe 2 failed" + +test_find :: TestEnv -> Test +test_find env = "git-annex find" ~: intmpclonerepo env $ do + annexed_notpresent annexedfile + git_annex_expectoutput env "find" [] [] + git_annex env "get" [annexedfile] @? "get failed" + annexed_present annexedfile + annexed_notpresent sha1annexedfile + git_annex_expectoutput env "find" [] [annexedfile] + git_annex_expectoutput env "find" ["--exclude", annexedfile, "--and", "--exclude", sha1annexedfile] [] + git_annex_expectoutput env "find" ["--include", annexedfile] [annexedfile] + git_annex_expectoutput env "find" ["--not", "--in", "origin"] [] + git_annex_expectoutput env "find" ["--copies", "1", "--and", "--not", "--copies", "2"] [sha1annexedfile] + git_annex_expectoutput env "find" ["--inbackend", "SHA1"] [sha1annexedfile] + git_annex_expectoutput env "find" ["--inbackend", "WORM"] [] + + {- --include=* should match files in subdirectories too, + - and --exclude=* should exclude them. -} + createDirectory "dir" + writeFile "dir/subfile" "subfile" + git_annex env "add" ["dir"] @? "add of subdir failed" + git_annex_expectoutput env "find" ["--include", "*", "--exclude", annexedfile, "--exclude", sha1annexedfile] ["dir/subfile"] + git_annex_expectoutput env "find" ["--exclude", "*"] [] + +test_merge :: TestEnv -> Test +test_merge env = "git-annex merge" ~: intmpclonerepo env $ do + git_annex env "merge" [] @? "merge failed" + +test_status :: TestEnv -> Test +test_status env = "git-annex status" ~: intmpclonerepo env $ do + json <- git_annex_output env "status" ["--json"] + 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 :: TestEnv -> Test +test_version env = "git-annex version" ~: intmpclonerepo env $ do + git_annex env "version" [] @? "version failed" + +test_sync :: TestEnv -> Test +test_sync env = "git-annex sync" ~: intmpclonerepo env $ do + git_annex env "sync" [] @? "sync failed" + {- Regression test for bug fixed in + - 7b0970b340d7faeb745c666146c7f701ec71808f, where in direct mode + - sync committed the symlink standin file to the annex. -} + git_annex_expectoutput env "find" ["--in", "."] [] + +{- Regression test for union merge bug fixed in + - 0214e0fb175a608a49b812d81b4632c081f63027 -} +test_union_merge_regression :: TestEnv -> Test +test_union_merge_regression env = "union merge regression" ~: + {- We need 3 repos to see this bug. -} + withtmpclonerepo env False $ \r1 -> do + withtmpclonerepo env False $ \r2 -> do + withtmpclonerepo env False $ \r3 -> do + forM_ [r1, r2, r3] $ \r -> indir env r $ do + when (r /= r1) $ + boolSystem "git" [Params "remote add r1", File ("../../" ++ r1)] @? "remote add" + when (r /= r2) $ + boolSystem "git" [Params "remote add r2", File ("../../" ++ r2)] @? "remote add" + when (r /= r3) $ + boolSystem "git" [Params "remote add r3", File ("../../" ++ r3)] @? "remote add" + git_annex env "get" [annexedfile] @? "get failed" + boolSystem "git" [Params "remote rm origin"] @? "remote rm" + forM_ [r3, r2, r1] $ \r -> indir env r $ + git_annex env "sync" [] @? "sync failed" + forM_ [r3, r2] $ \r -> indir env r $ + git_annex env "drop" ["--force", annexedfile] @? "drop failed" + indir env r1 $ do + git_annex env "sync" [] @? "sync failed in r1" + git_annex_expectoutput env "find" ["--in", "r3"] [] + {- This was the bug. The sync + - mangled location log data and it + - thought the file was still in r2 -} + git_annex_expectoutput env "find" ["--in", "r2"] [] + +{- Regression test for the automatic conflict resolution bug fixed + - in f4ba19f2b8a76a1676da7bb5850baa40d9c388e2. -} +test_conflict_resolution :: TestEnv -> Test +test_conflict_resolution env = "automatic conflict resolution" ~: + withtmpclonerepo env False $ \r1 -> do + withtmpclonerepo env False $ \r2 -> do + let rname r = if r == r1 then "r1" else "r2" + forM_ [r1, r2] $ \r -> indir env r $ do + {- Get all files, see check below. -} + git_annex env "get" [] @? "get failed" + {- Set up repos as remotes of each other; + - remove origin since we're going to sync + - some changes to a file. -} + when (r /= r1) $ + boolSystem "git" [Params "remote add r1", File ("../../" ++ r1)] @? "remote add" + when (r /= r2) $ + boolSystem "git" [Params "remote add r2", File ("../../" ++ r2)] @? "remote add" + boolSystem "git" [Params "remote rm origin"] @? "remote rm" + + {- Set up a conflict. -} + let newcontent = content annexedfile ++ rname r + ifM (annexeval Config.isDirect) + ( writeFile annexedfile newcontent + , do + git_annex env "unlock" [annexedfile] @? "unlock failed" + writeFile annexedfile newcontent + ) + {- Sync twice in r1 so it gets the conflict resolution + - update from r2 -} + forM_ [r1, r2, r1] $ \r -> indir env r $ do + git_annex env "sync" [] @? "sync failed in " ++ rname r + {- After the sync, it should be possible to get all + - files. This includes both sides of the conflict, + - although the filenames are not easily predictable. + - + - The bug caused, in direct mode, one repo to + - be missing the content of the file that had + - been put in it. -} + forM_ [r1, r2] $ \r -> indir env r $ do + git_annex env "get" [] @? "unable to get all files after merge conflict resolution in " ++ rname r + +test_map :: TestEnv -> Test +test_map env = "git-annex map" ~: intmpclonerepo env $ do + -- set descriptions, that will be looked for in the map + git_annex env "describe" [".", "this repo"] @? "describe 1 failed" + git_annex env "describe" ["origin", "origin repo"] @? "describe 2 failed" + -- --fast avoids it running graphviz, not a build dependency + git_annex env "map" ["--fast"] @? "map failed" + +test_uninit :: TestEnv -> Test +test_uninit env = "git-annex uninit" ~: TestList [inbranch, normal] + where + inbranch = "in branch" ~: intmpclonerepoInDirect env $ do + boolSystem "git" [Params "checkout git-annex"] @? "git checkout git-annex" + not <$> git_annex env "uninit" [] @? "uninit failed to fail when git-annex branch was checked out" + normal = "normal" ~: intmpclonerepo env $ do + git_annex env "get" [] @? "get failed" + annexed_present annexedfile + _ <- git_annex env "uninit" [] -- exit status not checked; does abnormal exit + checkregularfile annexedfile + doesDirectoryExist ".git" @? ".git vanished in uninit" + not <$> doesDirectoryExist ".git/annex" @? ".git/annex still present after uninit" + +test_upgrade :: TestEnv -> Test +test_upgrade env = "git-annex upgrade" ~: intmpclonerepo env $ do + git_annex env "upgrade" [] @? "upgrade from same version failed" + +test_whereis :: TestEnv -> Test +test_whereis env = "git-annex whereis" ~: intmpclonerepo env $ do + annexed_notpresent annexedfile + git_annex env "whereis" [annexedfile] @? "whereis on non-present file failed" + git_annex env "untrust" ["origin"] @? "untrust failed" + not <$> git_annex env "whereis" [annexedfile] @? "whereis on non-present file only present in untrusted repo failed to fail" + git_annex env "get" [annexedfile] @? "get failed" + annexed_present annexedfile + git_annex env "whereis" [annexedfile] @? "whereis on present file failed" + +test_hook_remote :: TestEnv -> Test +test_hook_remote env = "git-annex hook remote" ~: intmpclonerepo env $ do +#ifndef mingw32_HOST_OS + git_annex env "initremote" (words "foo type=hook encryption=none hooktype=foo") @? "initremote failed" + createDirectory dir + git_config "annex.foo-store-hook" $ + "cp $ANNEX_FILE " ++ loc + git_config "annex.foo-retrieve-hook" $ + "cp " ++ loc ++ " $ANNEX_FILE" + git_config "annex.foo-remove-hook" $ + "rm -f " ++ loc + git_config "annex.foo-checkpresent-hook" $ + "if [ -e " ++ loc ++ " ]; then echo $ANNEX_KEY; fi" + git_annex env "get" [annexedfile] @? "get of file failed" + annexed_present annexedfile + git_annex env "copy" [annexedfile, "--to", "foo"] @? "copy --to hook remote failed" + annexed_present annexedfile + git_annex env "drop" [annexedfile, "--numcopies=2"] @? "drop failed" + annexed_notpresent annexedfile + git_annex env "move" [annexedfile, "--from", "foo"] @? "move --from hook remote failed" + annexed_present annexedfile + not <$> git_annex env "drop" [annexedfile, "--numcopies=2"] @? "drop failed to fail" + annexed_present annexedfile + where + dir = "dir" + loc = dir ++ "/$ANNEX_KEY" + git_config k v = boolSystem "git" [Param "config", Param k, Param v] + @? "git config failed" +#else + -- this test doesn't work in Windows TODO + noop +#endif + +test_directory_remote :: TestEnv -> Test +test_directory_remote env = "git-annex directory remote" ~: intmpclonerepo env $ do + createDirectory "dir" + git_annex env "initremote" (words $ "foo type=directory encryption=none directory=dir") @? "initremote failed" + git_annex env "get" [annexedfile] @? "get of file failed" + annexed_present annexedfile + git_annex env "copy" [annexedfile, "--to", "foo"] @? "copy --to directory remote failed" + annexed_present annexedfile + git_annex env "drop" [annexedfile, "--numcopies=2"] @? "drop failed" + annexed_notpresent annexedfile + git_annex env "move" [annexedfile, "--from", "foo"] @? "move --from directory remote failed" + annexed_present annexedfile + not <$> git_annex env "drop" [annexedfile, "--numcopies=2"] @? "drop failed to fail" + annexed_present annexedfile + +test_rsync_remote :: TestEnv -> Test +test_rsync_remote env = "git-annex rsync remote" ~: intmpclonerepo env $ do +#ifndef mingw32_HOST_OS + createDirectory "dir" + git_annex env "initremote" (words $ "foo type=rsync encryption=none rsyncurl=dir") @? "initremote failed" + git_annex env "get" [annexedfile] @? "get of file failed" + annexed_present annexedfile + git_annex env "copy" [annexedfile, "--to", "foo"] @? "copy --to rsync remote failed" + annexed_present annexedfile + git_annex env "drop" [annexedfile, "--numcopies=2"] @? "drop failed" + annexed_notpresent annexedfile + git_annex env "move" [annexedfile, "--from", "foo"] @? "move --from rsync remote failed" + annexed_present annexedfile + not <$> git_annex env "drop" [annexedfile, "--numcopies=2"] @? "drop failed to fail" + annexed_present annexedfile +#else + -- this test doesn't work in Windows TODO + noop +#endif + +test_bup_remote :: TestEnv -> Test +test_bup_remote env = "git-annex bup remote" ~: intmpclonerepo env $ when Build.SysConfig.bup $ do + dir <- absPath "dir" -- bup special remote needs an absolute path + createDirectory dir + git_annex env "initremote" (words $ "foo type=bup encryption=none buprepo="++dir) @? "initremote failed" + git_annex env "get" [annexedfile] @? "get of file failed" + annexed_present annexedfile + git_annex env "copy" [annexedfile, "--to", "foo"] @? "copy --to bup remote failed" + annexed_present annexedfile + git_annex env "drop" [annexedfile, "--numcopies=2"] @? "drop failed" + annexed_notpresent annexedfile + git_annex env "copy" [annexedfile, "--from", "foo"] @? "copy --from bup remote failed" + annexed_present annexedfile + not <$> git_annex env "move" [annexedfile, "--from", "foo"] @? "move --from bup remote failed to fail" + annexed_present annexedfile + +-- gpg is not a build dependency, so only test when it's available +test_crypto :: TestEnv -> Test +test_crypto env = "git-annex crypto" ~: intmpclonerepo env $ whenM (Utility.Path.inPath Utility.Gpg.gpgcmd) $ do +#ifndef mingw32_HOST_OS + Utility.Gpg.testTestHarness @? "test harness self-test failed" + Utility.Gpg.testHarness $ do + createDirectory "dir" + let a cmd = git_annex env cmd + [ "foo" + , "type=directory" + , "encryption=" ++ Utility.Gpg.testKeyId + , "directory=dir" + , "highRandomQuality=false" + ] + a "initremote" @? "initremote failed" + not <$> a "initremote" @? "initremote failed to fail when run twice in a row" + a "enableremote" @? "enableremote failed" + a "enableremote" @? "enableremote failed when run twice in a row" + git_annex env "get" [annexedfile] @? "get of file failed" + annexed_present annexedfile + git_annex env "copy" [annexedfile, "--to", "foo"] @? "copy --to encrypted remote failed" + annexed_present annexedfile + git_annex env "drop" [annexedfile, "--numcopies=2"] @? "drop failed" + annexed_notpresent annexedfile + git_annex env "move" [annexedfile, "--from", "foo"] @? "move --from encrypted remote failed" + annexed_present annexedfile + not <$> git_annex env "drop" [annexedfile, "--numcopies=2"] @? "drop failed to fail" + annexed_present annexedfile +#else + putStrLn "gpg testing not implemented on Windows" +#endif + +-- This is equivilant to running git-annex, but it's all run in-process +-- (when the OS allows) so test coverage collection works. +git_annex :: TestEnv -> String -> [String] -> IO Bool +git_annex env command params = do +#ifndef mingw32_HOST_OS + forM_ (M.toList env) $ \(var, val) -> + Utility.Env.setEnv var val True + + -- catch all errors, including normally fatal errors + r <- try (run)::IO (Either SomeException ()) + case r of + Right _ -> return True + Left _ -> return False + where + run = GitAnnex.run (command:"-q":params) +#else + Utility.SafeCommand.boolSystemEnv "git-annex" + (map Param $ command : params) + (Just $ M.toList env) +#endif + +{- Runs git-annex and returns its output. -} +git_annex_output :: TestEnv -> String -> [String] -> IO String +git_annex_output env command params = do + got <- Utility.Process.readProcessEnv "git-annex" (command:params) + (Just $ M.toList env) + -- XXX since the above is a separate process, code coverage stats are + -- not gathered for things run in it. + -- Run same command again, to get code coverage. + _ <- git_annex env command params + return got + +git_annex_expectoutput :: TestEnv -> String -> [String] -> [String] -> IO () +git_annex_expectoutput env command params expected = do + got <- lines <$> git_annex_output env command params + got == expected @? ("unexpected value running " ++ command ++ " " ++ show params ++ " -- got: " ++ show got ++ " expected: " ++ show expected) + +-- Runs an action in the current annex. Note that shutdown actions +-- are not run; this should only be used for actions that query state. +annexeval :: Types.Annex a -> IO a +annexeval a = do + s <- Annex.new =<< Git.CurrentRepo.get + Annex.eval s $ do + Annex.setOutput Types.Messages.QuietOutput + a + +innewrepo :: TestEnv -> Assertion -> Assertion +innewrepo env a = withgitrepo env $ \r -> indir env r a + +inmainrepo :: TestEnv -> Assertion -> Assertion +inmainrepo env a = indir env mainrepodir a + +intmpclonerepo :: TestEnv -> Assertion -> Assertion +intmpclonerepo env a = withtmpclonerepo env False $ \r -> indir env r a + +intmpclonerepoInDirect :: TestEnv -> Assertion -> Assertion +intmpclonerepoInDirect env a = intmpclonerepo env $ + ifM isdirect + ( putStrLn "not supported in direct mode; skipping" + , a + ) + where + isdirect = annexeval $ do + Init.initialize Nothing + Config.isDirect + +intmpbareclonerepo :: TestEnv -> Assertion -> Assertion +intmpbareclonerepo env a = withtmpclonerepo env True $ \r -> indir env r a + +withtmpclonerepo :: TestEnv -> Bool -> (FilePath -> Assertion) -> Assertion +withtmpclonerepo env bare a = do + dir <- tmprepodir + bracket (clonerepo env mainrepodir dir bare) cleanup a + +withgitrepo :: TestEnv -> (FilePath -> Assertion) -> Assertion +withgitrepo env = bracket (setuprepo env mainrepodir) return + +indir :: TestEnv -> FilePath -> Assertion -> Assertion +indir env dir a = do + cwd <- getCurrentDirectory + -- Assertion failures throw non-IO errors; catch + -- any type of error and change back to cwd before + -- rethrowing. + r <- bracket_ (changeToTmpDir env dir) (setCurrentDirectory cwd) + (try (a)::IO (Either SomeException ())) + case r of + Right () -> return () + Left e -> throw e + +setuprepo :: TestEnv -> FilePath -> IO FilePath +setuprepo env dir = do + cleanup dir + ensuretmpdir + boolSystem "git" [Params "init -q", File dir] @? "git init failed" + indir env dir $ do + boolSystem "git" [Params "config user.name", Param "Test User"] @? "git config failed" + boolSystem "git" [Params "config user.email test@example.com"] @? "git config failed" + return dir + +-- clones are always done as local clones; we cannot test ssh clones +clonerepo :: TestEnv -> FilePath -> FilePath -> Bool -> IO FilePath +clonerepo env old new bare = do + cleanup new + ensuretmpdir + let b = if bare then " --bare" else "" + boolSystem "git" [Params ("clone -q" ++ b), File old, File new] @? "git clone failed" + indir env new $ + git_annex env "init" ["-q", new] @? "git annex init failed" + when (not bare) $ + indir env new $ + handleforcedirect env + return new + +handleforcedirect :: TestEnv -> IO () +handleforcedirect env = when (M.lookup "FORCEDIRECT" env == Just "1") $ + git_annex env "direct" ["-q"] @? "git annex direct failed" + +ensuretmpdir :: IO () +ensuretmpdir = do + e <- doesDirectoryExist tmpdir + unless e $ + createDirectory tmpdir + +cleanup :: FilePath -> IO () +cleanup dir = do + e <- doesDirectoryExist dir + when e $ do + -- git-annex prevents annexed file content from being + -- removed via directory permissions; undo + recurseDir SystemFS dir >>= + filterM doesDirectoryExist >>= + mapM_ Utility.FileMode.allowWrite + -- For unknown reasons, this sometimes fails on Windows. + void $ tryIO $ removeDirectoryRecursive dir + +checklink :: FilePath -> Assertion +checklink f = do + s <- getSymbolicLinkStatus f + -- in direct mode, it may be a symlink, or not, depending + -- on whether the content is present. + unlessM (annexeval Config.isDirect) $ + isSymbolicLink s @? f ++ " is not a symlink" + +checkregularfile :: FilePath -> Assertion +checkregularfile f = do + s <- getSymbolicLinkStatus f + isRegularFile s @? f ++ " is not a normal file" + return () + +checkcontent :: FilePath -> Assertion +checkcontent f = do + c <- Utility.Exception.catchDefaultIO "could not read file" $ readFile f + assertEqual ("checkcontent " ++ f) (content f) c + +checkunwritable :: FilePath -> Assertion +checkunwritable f = unlessM (annexeval Config.isDirect) $ do + -- Look at permissions bits rather than trying to write or + -- using fileAccess because if run as root, any file can be + -- modified despite permissions. + s <- getFileStatus f + let mode = fileMode s + if (mode == mode `unionFileModes` ownerWriteMode) + then assertFailure $ "able to modify annexed file's " ++ f ++ " content" + else return () + +checkwritable :: FilePath -> Assertion +checkwritable f = do + r <- tryIO $ writeFile f $ content f + case r of + Left _ -> assertFailure $ "unable to modify " ++ f + Right _ -> return () + +checkdangling :: FilePath -> Assertion +checkdangling f = ifM (annexeval Config.crippledFileSystem) + ( return () -- probably no real symlinks to test + , do + r <- tryIO $ readFile f + case r of + Left _ -> return () -- expected; dangling link + Right _ -> assertFailure $ f ++ " was not a dangling link as expected" + ) + +checklocationlog :: FilePath -> Bool -> Assertion +checklocationlog f expected = do + thisuuid <- annexeval Annex.UUID.getUUID + r <- annexeval $ Backend.lookupFile f + case r of + Just (k, _) -> do + uuids <- annexeval $ Remote.keyLocations k + assertEqual ("bad content in location log for " ++ f ++ " key " ++ (Types.Key.key2file k) ++ " uuid " ++ show thisuuid) + expected (thisuuid `elem` uuids) + _ -> assertFailure $ f ++ " failed to look up key" + +checkbackend :: FilePath -> Types.Backend -> Assertion +checkbackend file expected = do + r <- annexeval $ Backend.lookupFile file + let b = snd $ fromJust r + assertEqual ("backend for " ++ file) expected b + +inlocationlog :: FilePath -> Assertion +inlocationlog f = checklocationlog f True + +notinlocationlog :: FilePath -> Assertion +notinlocationlog f = checklocationlog f False + +runchecks :: [FilePath -> Assertion] -> FilePath -> Assertion +runchecks [] _ = return () +runchecks (a:as) f = do + a f + runchecks as f + +annexed_notpresent :: FilePath -> Assertion +annexed_notpresent = runchecks + [checklink, checkdangling, notinlocationlog] + +annexed_present :: FilePath -> Assertion +annexed_present = runchecks + [checklink, checkcontent, checkunwritable, inlocationlog] + +unannexed :: FilePath -> Assertion +unannexed = runchecks [checkregularfile, checkcontent, checkwritable] + +prepare :: Bool -> IO TestEnv +prepare forcedirect = do + whenM (doesDirectoryExist tmpdir) $ + error $ "The temporary directory " ++ tmpdir ++ " already exists; cannot run test suite." + + cwd <- getCurrentDirectory + p <- Utility.Env.getEnvDefault "PATH" "" + + let env = + -- Ensure that the just-built git annex is used. + [ ("PATH", cwd ++ [searchPathSeparator] ++ p) + , ("TOPDIR", cwd) + -- Avoid git complaining if it cannot determine the user's + -- email address, or exploding if it doesn't know the user's + -- name. + , ("GIT_AUTHOR_EMAIL", "test@example.com") + , ("GIT_AUTHOR_NAME", "git-annex test") + , ("GIT_COMMITTER_EMAIL", "test@example.com") + , ("GIT_COMMITTER_NAME", "git-annex test") + -- force gpg into batch mode for the tests + , ("GPG_BATCH", "1") + , ("FORCEDIRECT", if forcedirect then "1" else "") + ] + + return $ M.fromList env + +changeToTmpDir :: TestEnv -> FilePath -> IO () +changeToTmpDir env t = do + let topdir = fromMaybe "" $ M.lookup "TOPDIR" env + setCurrentDirectory $ topdir ++ "/" ++ t + +tmpdir :: String +tmpdir = ".t" + +mainrepodir :: FilePath +mainrepodir = tmpdir "repo" + +tmprepodir :: IO FilePath +tmprepodir = go (0 :: Int) + where + go n = do + let d = tmpdir "tmprepo" ++ show n + ifM (doesDirectoryExist d) + ( go $ n + 1 + , return d + ) + +annexedfile :: String +annexedfile = "foo" + +wormannexedfile :: String +wormannexedfile = "apple" + +sha1annexedfile :: String +sha1annexedfile = "sha1foo" + +sha1annexedfiledup :: String +sha1annexedfiledup = "sha1foodup" + +ingitfile :: String +ingitfile = "bar" + +content :: FilePath -> String +content f + | f == annexedfile = "annexed file content" + | f == ingitfile = "normal file content" + | f == sha1annexedfile ="sha1 annexed file content" + | f == sha1annexedfiledup = content sha1annexedfile + | f == wormannexedfile = "worm annexed file content" + | otherwise = "unknown file " ++ f + +changecontent :: FilePath -> IO () +changecontent f = writeFile f $ changedcontent f + +changedcontent :: FilePath -> String +changedcontent f = (content f) ++ " (modified)" + +backendSHA1 :: Types.Backend +backendSHA1 = backend_ "SHA1" + +backendSHA256 :: Types.Backend +backendSHA256 = backend_ "SHA256" + +backendWORM :: Types.Backend +backendWORM = backend_ "WORM" + +backend_ :: String -> Types.Backend +backend_ name = Backend.lookupBackendName name diff --git a/Types.hs b/Types.hs new file mode 100644 index 0000000000..8768ed1fe6 --- /dev/null +++ b/Types.hs @@ -0,0 +1,31 @@ +{- git-annex abstract data types + - + - Copyright 2010 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Types ( + Annex, + Backend, + Key, + AssociatedFile, + UUID(..), + GitConfig(..), + RemoteGitConfig(..), + Remote, + RemoteType, + Option +) where + +import Annex +import Types.Backend +import Types.GitConfig +import Types.Key +import Types.UUID +import Types.Remote +import Types.Option + +type Backend = BackendA Annex +type Remote = RemoteA Annex +type RemoteType = RemoteTypeA Annex diff --git a/Types/Backend.hs b/Types/Backend.hs new file mode 100644 index 0000000000..c7d962db06 --- /dev/null +++ b/Types/Backend.hs @@ -0,0 +1,26 @@ +{- git-annex key/value backend data type + - + - Most things should not need this, using Types instead + - + - Copyright 2010,2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Types.Backend where + +import Types.Key +import Types.KeySource + +data BackendA a = Backend + { name :: String + , getKey :: KeySource -> a (Maybe Key) + , fsckKey :: Maybe (Key -> FilePath -> a Bool) + , canUpgradeKey :: Maybe (Key -> Bool) + } + +instance Show (BackendA a) where + show backend = "Backend { name =\"" ++ name backend ++ "\" }" + +instance Eq (BackendA a) where + a == b = name a == name b diff --git a/Types/BranchState.hs b/Types/BranchState.hs new file mode 100644 index 0000000000..2f7948ebbf --- /dev/null +++ b/Types/BranchState.hs @@ -0,0 +1,16 @@ +{- git-annex BranchState data type + - + - Copyright 2011 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Types.BranchState where + +data BranchState = BranchState + { branchUpdated :: Bool -- has the branch been updated this run? + , indexChecked :: Bool -- has the index file been checked to exist? + } + +startBranchState :: BranchState +startBranchState = BranchState False False diff --git a/Types/Command.hs b/Types/Command.hs new file mode 100644 index 0000000000..3187efd171 --- /dev/null +++ b/Types/Command.hs @@ -0,0 +1,77 @@ +{- git-annex command data types + - + - Copyright 2010-2011 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Types.Command where + +import Data.Ord + +import Types + +{- A command runs in these stages. + - + - a. The check stage runs checks, that error out if + - anything prevents the command from running. -} +data CommandCheck = CommandCheck { idCheck :: Int, runCheck :: Annex () } +{- b. The seek stage takes the parameters passed to the command, + - looks through the repo to find the ones that are relevant + - to that command (ie, new files to add), and generates + - a list of start stage actions. -} +type CommandSeek = [String] -> Annex [CommandStart] +{- c. The start stage is run before anything is printed about the + - command, is passed some input, and can early abort it + - if the input does not make sense. It should run quickly and + - should not modify Annex state. -} +type CommandStart = Annex (Maybe CommandPerform) +{- d. The perform stage is run after a message is printed about the command + - being run, and it should be where the bulk of the work happens. -} +type CommandPerform = Annex (Maybe CommandCleanup) +{- e. The cleanup stage is run only if the perform stage succeeds, and it + - returns the overall success/fail of the command. -} +type CommandCleanup = Annex Bool + +{- A command is defined by specifying these things. -} +data Command = Command + { cmdoptions :: [Option] -- command-specific options + , cmdnorepo :: Maybe (IO ()) -- an action to run when not in a repo + , cmdcheck :: [CommandCheck] -- check stage + , cmdnocommit :: Bool -- don't commit journalled state changes + , cmdnomessages :: Bool -- don't output normal messages + , cmdname :: String + , cmdparamdesc :: String -- description of params for usage + , cmdseek :: [CommandSeek] -- seek stage + , cmdsection :: CommandSection + , cmddesc :: String -- description of command for usage + } + +{- CommandCheck functions can be compared using their unique id. -} +instance Eq CommandCheck where + a == b = idCheck a == idCheck b + +instance Eq Command where + a == b = cmdname a == cmdname b + +{- Order commands by name. -} +instance Ord Command where + compare = comparing cmdname + +{- The same sections are listed in doc/git-annex.mdwn -} +data CommandSection + = SectionCommon + | SectionSetup + | SectionMaintenance + | SectionQuery + | SectionUtility + | SectionPlumbing + deriving (Eq, Ord, Enum, Bounded) + +descSection :: CommandSection -> String +descSection SectionCommon = "Commonly used commands" +descSection SectionSetup = "Repository setup commands" +descSection SectionMaintenance = "Repository maintenance commands" +descSection SectionQuery = "Query commands" +descSection SectionUtility = "Utility commands" +descSection SectionPlumbing = "Plumbing commands" diff --git a/Types/Crypto.hs b/Types/Crypto.hs new file mode 100644 index 0000000000..e97d02ba8e --- /dev/null +++ b/Types/Crypto.hs @@ -0,0 +1,69 @@ +{- git-annex crypto types + - + - Copyright 2011-2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Types.Crypto ( + Cipher(..), + StorableCipher(..), + KeyIds(..), + Mac(..), + readMac, + showMac, + defaultMac, + calcMac, +) where + +import qualified Data.ByteString.Lazy as L +import Data.Digest.Pure.SHA + +import Utility.Gpg (KeyIds(..)) + +-- XXX ideally, this would be a locked memory region +newtype Cipher = Cipher String + +data StorableCipher = EncryptedCipher String KeyIds | SharedCipher String + deriving (Ord, Eq) + +{- File names are (client-side) MAC'ed on special remotes. + - The chosen MAC algorithm needs to be same for all files stored on the + - remote. + -} +data Mac = HmacSha1 | HmacSha224 | HmacSha256 | HmacSha384 | HmacSha512 + deriving (Eq) + +defaultMac :: Mac +defaultMac = HmacSha1 + +-- MAC algorithms are shown as follows in the file names. +showMac :: Mac -> String +showMac HmacSha1 = "HMACSHA1" +showMac HmacSha224 = "HMACSHA224" +showMac HmacSha256 = "HMACSHA256" +showMac HmacSha384 = "HMACSHA384" +showMac HmacSha512 = "HMACSHA512" + +-- Read the MAC algorithm from the remote config. +readMac :: String -> Maybe Mac +readMac "HMACSHA1" = Just HmacSha1 +readMac "HMACSHA224" = Just HmacSha224 +readMac "HMACSHA256" = Just HmacSha256 +readMac "HMACSHA384" = Just HmacSha384 +readMac "HMACSHA512" = Just HmacSha512 +readMac _ = Nothing + +calcMac + :: Mac -- ^ MAC + -> L.ByteString -- ^ secret key + -> L.ByteString -- ^ message + -> String -- ^ MAC'ed message, in hexadecimals +calcMac mac = case mac of + HmacSha1 -> showDigest $* hmacSha1 + HmacSha224 -> showDigest $* hmacSha224 + HmacSha256 -> showDigest $* hmacSha256 + HmacSha384 -> showDigest $* hmacSha384 + HmacSha512 -> showDigest $* hmacSha512 + where + ($*) g f x y = g $ f x y diff --git a/Types/FileMatcher.hs b/Types/FileMatcher.hs new file mode 100644 index 0000000000..fc442b6041 --- /dev/null +++ b/Types/FileMatcher.hs @@ -0,0 +1,13 @@ +{- git-annex file matcher types + - + - Copyright 2013 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Types.FileMatcher where + +data FileInfo = FileInfo + { relFile :: FilePath -- may be relative to cwd + , matchFile :: FilePath -- filepath to match on; may be relative to top + } diff --git a/Types/GitConfig.hs b/Types/GitConfig.hs new file mode 100644 index 0000000000..d5d234ca93 --- /dev/null +++ b/Types/GitConfig.hs @@ -0,0 +1,146 @@ +{- git-annex configuration + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Types.GitConfig ( + GitConfig(..), + extractGitConfig, + RemoteGitConfig(..), + extractRemoteGitConfig, +) where + +import Common +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 -} +data GitConfig = GitConfig + { annexVersion :: Maybe String + , annexNumCopies :: Int + , annexDiskReserve :: Integer + , annexDirect :: Bool + , annexBackends :: [String] + , annexQueueSize :: Maybe Int + , annexBloomCapacity :: Maybe Int + , annexBloomAccuracy :: Maybe Int + , annexSshCaching :: Maybe Bool + , annexAlwaysCommit :: Bool + , annexDelayAdd :: Maybe Int + , annexHttpHeaders :: [String] + , annexHttpHeadersCommand :: Maybe String + , annexAutoCommit :: Bool + , annexDebug :: Bool + , annexWebOptions :: [String] + , annexWebDownloadCommand :: Maybe String + , annexCrippledFileSystem :: Bool + , annexLargeFiles :: Maybe String + , coreSymlinks :: Bool + } + +extractGitConfig :: Git.Repo -> GitConfig +extractGitConfig r = GitConfig + { annexVersion = notempty $ getmaybe (annex "version") + , annexNumCopies = get (annex "numcopies") 1 + , annexDiskReserve = fromMaybe onemegabyte $ + readSize dataUnits =<< getmaybe (annex "diskreserve") + , annexDirect = getbool (annex "direct") False + , annexBackends = getwords (annex "backends") + , annexQueueSize = getmayberead (annex "queuesize") + , annexBloomCapacity = getmayberead (annex "bloomcapacity") + , annexBloomAccuracy = getmayberead (annex "bloomaccuracy") + , annexSshCaching = getmaybebool (annex "sshcaching") + , annexAlwaysCommit = getbool (annex "alwayscommit") True + , annexDelayAdd = getmayberead (annex "delayadd") + , annexHttpHeaders = getlist (annex "http-headers") + , annexHttpHeadersCommand = getmaybe (annex "http-headers-command") + , annexAutoCommit = getbool (annex "autocommit") True + , annexDebug = getbool (annex "debug") False + , annexWebOptions = getwords (annex "web-options") + , annexWebDownloadCommand = getmaybe (annex "web-download-command") + , annexCrippledFileSystem = getbool (annex "crippledfilesystem") False + , annexLargeFiles = getmaybe (annex "largefiles") + , coreSymlinks = getbool "core.symlinks" True + } + where + get k def = fromMaybe def $ getmayberead k + getbool k def = fromMaybe def $ getmaybebool k + getmaybebool k = Git.Config.isTrue =<< getmaybe k + getmayberead k = readish =<< getmaybe k + getmaybe k = Git.Config.getMaybe k r + getlist k = Git.Config.getList k r + getwords k = fromMaybe [] $ words <$> getmaybe k + + annex k = "annex." ++ k + + onemegabyte = 1000000 + +{- Per-remote git-annex settings. Each setting corresponds to a git-config + - key such as .annex-foo, or if that is not set, a default from + - annex.foo -} +data RemoteGitConfig = RemoteGitConfig + { remoteAnnexCost :: Maybe Cost + , remoteAnnexCostCommand :: Maybe String + , remoteAnnexIgnore :: Bool + , remoteAnnexSync :: Bool + , remoteAnnexTrustLevel :: Maybe String + , remoteAnnexStartCommand :: Maybe String + , remoteAnnexStopCommand :: Maybe String + + {- These settings are specific to particular types of remotes + - including special remotes. -} + , remoteAnnexSshOptions :: [String] + , remoteAnnexRsyncOptions :: [String] + , remoteAnnexRsyncTransport :: [String] + , remoteAnnexGnupgOptions :: [String] + , remoteAnnexRsyncUrl :: Maybe String + , remoteAnnexBupRepo :: Maybe String + , remoteAnnexBupSplitOptions :: [String] + , remoteAnnexDirectory :: Maybe FilePath + , remoteAnnexHookType :: Maybe String + {- A regular git remote's git repository config. -} + , remoteGitConfig :: Maybe GitConfig + } + +extractRemoteGitConfig :: Git.Repo -> String -> RemoteGitConfig +extractRemoteGitConfig r remotename = RemoteGitConfig + { remoteAnnexCost = getmayberead "cost" + , remoteAnnexCostCommand = notempty $ getmaybe "cost-command" + , remoteAnnexIgnore = getbool "ignore" False + , remoteAnnexSync = getbool "sync" True + , remoteAnnexTrustLevel = notempty $ getmaybe "trustlevel" + , remoteAnnexStartCommand = notempty $ getmaybe "start-command" + , remoteAnnexStopCommand = notempty $ getmaybe "stop-command" + + , remoteAnnexSshOptions = getoptions "ssh-options" + , remoteAnnexRsyncOptions = getoptions "rsync-options" + , remoteAnnexRsyncTransport = getoptions "rsync-transport" + , remoteAnnexGnupgOptions = getoptions "gnupg-options" + , remoteAnnexRsyncUrl = notempty $ getmaybe "rsyncurl" + , remoteAnnexBupRepo = getmaybe "buprepo" + , remoteAnnexBupSplitOptions = getoptions "bup-split-options" + , remoteAnnexDirectory = notempty $ getmaybe "directory" + , remoteAnnexHookType = notempty $ getmaybe "hooktype" + , remoteGitConfig = Nothing + } + where + getbool k def = fromMaybe def $ getmaybebool k + getmaybebool k = Git.Config.isTrue =<< getmaybe k + getmayberead k = readish =<< getmaybe k + getmaybe k = mplus (Git.Config.getMaybe (key k) r) + (Git.Config.getMaybe (remotekey k) r) + getoptions k = fromMaybe [] $ words <$> getmaybe k + + key k = "annex." ++ k + remotekey k = "remote." ++ remotename ++ ".annex-" ++ k + +notempty :: Maybe String -> Maybe String +notempty Nothing = Nothing +notempty (Just "") = Nothing +notempty (Just s) = Just s + diff --git a/Types/Group.hs b/Types/Group.hs new file mode 100644 index 0000000000..88bc352077 --- /dev/null +++ b/Types/Group.hs @@ -0,0 +1,27 @@ +{- git-annex repo groups + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Types.Group ( + Group, + GroupMap(..), + emptyGroupMap +) where + +import Types.UUID + +import qualified Data.Map as M +import qualified Data.Set as S + +type Group = String + +data GroupMap = GroupMap + { groupsByUUID :: M.Map UUID (S.Set Group) + , uuidsByGroup :: M.Map Group (S.Set UUID) + } + +emptyGroupMap :: GroupMap +emptyGroupMap = GroupMap M.empty M.empty diff --git a/Types/Key.hs b/Types/Key.hs new file mode 100644 index 0000000000..a0c6d83bc6 --- /dev/null +++ b/Types/Key.hs @@ -0,0 +1,90 @@ +{- git-annex Key data type + - + - Most things should not need this, using Types instead + - + - Copyright 2011 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Types.Key ( + Key(..), + AssociatedFile, + stubKey, + key2file, + file2key, + + prop_idempotent_key_encode +) where + +import System.Posix.Types + +import Common +import Utility.QuickCheck + +{- A Key has a unique name, which is derived from a particular backend, + - and may contain other optional metadata. -} +data Key = Key + { keyName :: String + , keyBackendName :: String + , keySize :: Maybe Integer + , keyMtime :: Maybe EpochTime + } deriving (Eq, Ord, Read, Show) + +{- A filename may be associated with a Key. -} +type AssociatedFile = Maybe FilePath + +stubKey :: Key +stubKey = Key + { keyName = "" + , keyBackendName = "" + , keySize = Nothing + , keyMtime = Nothing + } + +fieldSep :: Char +fieldSep = '-' + +{- Converts a key to a string that is suitable for use as a filename. + - The name field is always shown last, separated by doubled fieldSeps, + - and is the only field allowed to contain the fieldSep. -} +key2file :: Key -> FilePath +key2file Key { keyBackendName = b, keySize = s, keyMtime = m, keyName = n } = + b +++ ('s' ?: s) +++ ('m' ?: m) +++ (fieldSep : n) + where + "" +++ y = y + x +++ "" = x + x +++ y = x ++ fieldSep:y + c ?: (Just v) = c : show v + _ ?: _ = "" + +file2key :: FilePath -> Maybe Key +file2key s = if key == Just stubKey then Nothing else key + where + key = startbackend stubKey s + + startbackend k v = sepfield k v addbackend + + sepfield k v a = case span (/= fieldSep) v of + (v', _:r) -> findfields r $ a k v' + _ -> Nothing + + findfields (c:v) (Just k) + | c == fieldSep = Just $ k { keyName = v } + | otherwise = sepfield k v $ addfield c + findfields _ v = v + + addbackend k v = Just k { keyBackendName = v } + 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/KeySource.hs b/Types/KeySource.hs new file mode 100644 index 0000000000..fd4af07a68 --- /dev/null +++ b/Types/KeySource.hs @@ -0,0 +1,29 @@ +{- KeySource data type + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Types.KeySource where + +import Utility.InodeCache + +{- When content is in the process of being added to the annex, + - and a Key generated from it, this data type is used. + - + - The contentLocation may be different from the filename + - associated with the key. For example, the add command + - may temporarily hard link the content into a lockdown directory + - for checking. The migrate command uses the content + - of a different Key. + - + - The inodeCache can be used to detect some types of modifications to + - files that may be made while they're in the process of being added. + -} +data KeySource = KeySource + { keyFilename :: FilePath + , contentLocation :: FilePath + , inodeCache :: Maybe InodeCache + } + deriving (Show) diff --git a/Types/Messages.hs b/Types/Messages.hs new file mode 100644 index 0000000000..4fcce79f80 --- /dev/null +++ b/Types/Messages.hs @@ -0,0 +1,24 @@ +{- git-annex Messages data types + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Types.Messages where + +import qualified Data.Set as S + +data OutputType = NormalOutput | QuietOutput | JSONOutput + +data SideActionBlock = NoBlock | StartBlock | InBlock + deriving (Eq) + +data MessageState = MessageState + { outputType :: OutputType + , sideActionBlock :: SideActionBlock + , fileNotFoundShown :: S.Set FilePath + } + +defaultMessageState :: MessageState +defaultMessageState = MessageState NormalOutput NoBlock S.empty diff --git a/Types/Option.hs b/Types/Option.hs new file mode 100644 index 0000000000..0362578388 --- /dev/null +++ b/Types/Option.hs @@ -0,0 +1,17 @@ +{- git-annex command options + - + - Copyright 2011 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Types.Option where + +import System.Console.GetOpt + +import Annex + +{- Each dashed command-line option results in generation of an action + - in the Annex monad that performs the necessary setting. + -} +type Option = OptDescr (Annex ()) diff --git a/Types/Remote.hs b/Types/Remote.hs new file mode 100644 index 0000000000..8492be06d3 --- /dev/null +++ b/Types/Remote.hs @@ -0,0 +1,90 @@ +{- git-annex remotes types + - + - Most things should not need this, using Types instead + - + - Copyright 2011 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Types.Remote where + +import Data.Map as M +import Data.Ord + +import qualified Git +import Types.Key +import Types.UUID +import Types.GitConfig +import Config.Cost +import Utility.Metered + +type RemoteConfigKey = String +type RemoteConfig = M.Map RemoteConfigKey String + +{- There are different types of remotes. -} +data RemoteTypeA a = RemoteType { + -- human visible type name + typename :: String, + -- enumerates remotes of this type + enumerate :: a [Git.Repo], + -- generates a remote of this type + generate :: Git.Repo -> UUID -> RemoteConfig -> RemoteGitConfig -> a (RemoteA a), + -- initializes or changes a remote + setup :: UUID -> RemoteConfig -> a RemoteConfig +} + +instance Eq (RemoteTypeA a) where + x == y = typename x == typename y + +{- An individual remote. -} +data RemoteA a = Remote { + -- each Remote has a unique uuid + uuid :: UUID, + -- each Remote has a human visible name + name :: String, + -- Remotes have a use cost; higher is more expensive + cost :: Cost, + -- Transfers a key to the remote. + storeKey :: Key -> AssociatedFile -> MeterUpdate -> a Bool, + -- Retrieves a key's contents to a file. + -- (The MeterUpdate does not need to be used if it retrieves + -- directly to the file, and not to an intermediate file.) + retrieveKeyFile :: Key -> AssociatedFile -> FilePath -> MeterUpdate -> a Bool, + -- retrieves a key's contents to a tmp file, if it can be done cheaply + retrieveKeyFileCheap :: Key -> FilePath -> a Bool, + -- removes a key's contents + removeKey :: Key -> a Bool, + -- Checks if a key is present in the remote; if the remote + -- cannot be accessed returns a Left error message. + hasKey :: Key -> a (Either String Bool), + -- Some remotes can check hasKey without an expensive network + -- operation. + hasKeyCheap :: Bool, + -- Some remotes can provide additional details for whereis. + whereisKey :: Maybe (Key -> a [String]), + -- a Remote has a persistent configuration store + config :: RemoteConfig, + -- git repo for the Remote + repo :: Git.Repo, + -- a Remote's configuration from git + gitconfig :: RemoteGitConfig, + -- a Remote can be assocated with a specific local filesystem path + localpath :: Maybe FilePath, + -- a Remote can be known to be readonly + readonly :: Bool, + -- a Remote can be globally available. (Ie, "in the cloud".) + globallyAvailable :: Bool, + -- the type of the remote + remotetype :: RemoteTypeA a +} + +instance Show (RemoteA a) where + show remote = "Remote { name =\"" ++ name remote ++ "\" }" + +-- two remotes are the same if they have the same uuid +instance Eq (RemoteA a) where + x == y = uuid x == uuid y + +instance Ord (RemoteA a) where + compare = comparing uuid diff --git a/Types/StandardGroups.hs b/Types/StandardGroups.hs new file mode 100644 index 0000000000..30b8822820 --- /dev/null +++ b/Types/StandardGroups.hs @@ -0,0 +1,96 @@ +{- git-annex standard repository groups + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Types.StandardGroups where + +import Types.Remote (RemoteConfig) + +import qualified Data.Map as M +import Data.Maybe + +data StandardGroup + = ClientGroup + | TransferGroup + | BackupGroup + | IncrementalBackupGroup + | SmallArchiveGroup + | FullArchiveGroup + | SourceGroup + | ManualGroup + | PublicGroup + | UnwantedGroup + deriving (Eq, Ord, Enum, Bounded, Show) + +fromStandardGroup :: StandardGroup -> String +fromStandardGroup ClientGroup = "client" +fromStandardGroup TransferGroup = "transfer" +fromStandardGroup BackupGroup = "backup" +fromStandardGroup IncrementalBackupGroup = "incrementalbackup" +fromStandardGroup SmallArchiveGroup = "smallarchive" +fromStandardGroup FullArchiveGroup = "archive" +fromStandardGroup SourceGroup = "source" +fromStandardGroup ManualGroup = "manual" +fromStandardGroup PublicGroup = "public" +fromStandardGroup UnwantedGroup = "unwanted" + +toStandardGroup :: String -> Maybe StandardGroup +toStandardGroup "client" = Just ClientGroup +toStandardGroup "transfer" = Just TransferGroup +toStandardGroup "backup" = Just BackupGroup +toStandardGroup "incrementalbackup" = Just IncrementalBackupGroup +toStandardGroup "smallarchive" = Just SmallArchiveGroup +toStandardGroup "archive" = Just FullArchiveGroup +toStandardGroup "source" = Just SourceGroup +toStandardGroup "manual" = Just ManualGroup +toStandardGroup "public" = Just PublicGroup +toStandardGroup "unwanted" = Just UnwantedGroup +toStandardGroup _ = Nothing + +descStandardGroup :: StandardGroup -> String +descStandardGroup ClientGroup = "client: a repository on your computer" +descStandardGroup TransferGroup = "transfer: distributes files to clients" +descStandardGroup BackupGroup = "full backup: backs up all files" +descStandardGroup IncrementalBackupGroup = "incremental backup: backs up files not backed up elsewhere" +descStandardGroup SmallArchiveGroup = "small archive: archives files located in \"archive\" directories" +descStandardGroup FullArchiveGroup = "full archive: archives all files not archived elsewhere" +descStandardGroup SourceGroup = "file source: moves files on to other repositories" +descStandardGroup ManualGroup = "manual mode: only stores files you manually choose" +descStandardGroup UnwantedGroup = "unwanted: remove content from this repository" +descStandardGroup PublicGroup = "public: publishes files located in an associated directory" + +associatedDirectory :: Maybe RemoteConfig -> StandardGroup -> Maybe FilePath +associatedDirectory _ SmallArchiveGroup = Just "archive" +associatedDirectory _ FullArchiveGroup = Just "archive" +associatedDirectory (Just c) PublicGroup = Just $ + fromMaybe "public" $ M.lookup "preferreddir" c +associatedDirectory Nothing PublicGroup = Just "public" +associatedDirectory _ _ = Nothing + +{- See doc/preferred_content.mdwn for explanations of these expressions. -} +preferredContent :: StandardGroup -> String +preferredContent ClientGroup = lastResort $ + "(exclude=*/archive/* and exclude=archive/*) or (" ++ notArchived ++ ")" +preferredContent TransferGroup = lastResort $ + "not (inallgroup=client and copies=client:2) and (" ++ preferredContent ClientGroup ++ ")" +preferredContent BackupGroup = "include=*" +preferredContent IncrementalBackupGroup = lastResort $ + "include=* and (not copies=incrementalbackup:1)" +preferredContent SmallArchiveGroup = lastResort $ + "(include=*/archive/* or include=archive/*) and (" ++ preferredContent FullArchiveGroup ++ ")" +preferredContent FullArchiveGroup = lastResort notArchived +preferredContent SourceGroup = "not (copies=1)" +preferredContent ManualGroup = "present and (" ++ preferredContent ClientGroup ++ ")" +preferredContent PublicGroup = "inpreferreddir" +preferredContent UnwantedGroup = "exclude=*" + +notArchived :: String +notArchived = "not (copies=archive:1 or copies=smallarchive:1)" + +{- Most repositories want any content that is only on untrusted + - or dead repositories. -} +lastResort :: String -> String +lastResort s = "(" ++ s ++ ") or (not copies=semitrusted+:1)" diff --git a/Types/TrustLevel.hs b/Types/TrustLevel.hs new file mode 100644 index 0000000000..27325cd2bf --- /dev/null +++ b/Types/TrustLevel.hs @@ -0,0 +1,41 @@ +{- git-annex trust levels + - + - Copyright 2010 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Types.TrustLevel ( + TrustLevel(..), + TrustMap, + readTrustLevel, + showTrustLevel, + prop_read_show_TrustLevel +) where + +import qualified Data.Map as M + +import Types.UUID + +data TrustLevel = Trusted | SemiTrusted | UnTrusted | DeadTrusted + deriving (Eq, Enum, Ord, Bounded) + +type TrustMap = M.Map UUID TrustLevel + +readTrustLevel :: String -> Maybe TrustLevel +readTrustLevel "trusted" = Just Trusted +readTrustLevel "untrusted" = Just UnTrusted +readTrustLevel "semitrusted" = Just SemiTrusted +readTrustLevel "dead" = Just DeadTrusted +readTrustLevel _ = Nothing + +showTrustLevel :: TrustLevel -> String +showTrustLevel Trusted = "trusted" +showTrustLevel UnTrusted = "untrusted" +showTrustLevel SemiTrusted = "semitrusted" +showTrustLevel DeadTrusted = "dead" + +prop_read_show_TrustLevel :: Bool +prop_read_show_TrustLevel = all check [minBound .. maxBound] + where + check l = readTrustLevel (showTrustLevel l) == Just l diff --git a/Types/UUID.hs b/Types/UUID.hs new file mode 100644 index 0000000000..8a304dffab --- /dev/null +++ b/Types/UUID.hs @@ -0,0 +1,24 @@ +{- git-annex UUID type + - + - Copyright 2011 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Types.UUID where + +import qualified Data.Map as M + +-- A UUID is either an arbitrary opaque string, or UUID info may be missing. +data UUID = NoUUID | UUID String + deriving (Eq, Ord, Show, Read) + +fromUUID :: UUID -> String +fromUUID (UUID u) = u +fromUUID NoUUID = "" + +toUUID :: String -> UUID +toUUID [] = NoUUID +toUUID s = UUID s + +type UUIDMap = M.Map UUID String diff --git a/Upgrade.hs b/Upgrade.hs new file mode 100644 index 0000000000..f0166bf8ef --- /dev/null +++ b/Upgrade.hs @@ -0,0 +1,31 @@ +{- git-annex upgrade support + - + - Copyright 2010 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE CPP #-} + +module Upgrade where + +import Common.Annex +import Annex.Version +#ifndef mingw32_HOST_OS +import qualified Upgrade.V0 +import qualified Upgrade.V1 +#endif +import qualified Upgrade.V2 + +upgrade :: Annex Bool +upgrade = go =<< getVersion + where +#ifndef mingw32_HOST_OS + go (Just "0") = Upgrade.V0.upgrade + go (Just "1") = Upgrade.V1.upgrade +#else + go (Just "0") = error "upgrade from v0 on Windows not supported" + go (Just "1") = error "upgrade from v1 on Windows not supported" +#endif + go (Just "2") = Upgrade.V2.upgrade + go _ = return True diff --git a/Upgrade/V0.hs b/Upgrade/V0.hs new file mode 100644 index 0000000000..00a08cb458 --- /dev/null +++ b/Upgrade/V0.hs @@ -0,0 +1,49 @@ +{- git-annex v0 -> v1 upgrade support + - + - Copyright 2010 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Upgrade.V0 where + +import Common.Annex +import Annex.Content +import qualified Upgrade.V1 + +upgrade :: Annex Bool +upgrade = do + showAction "v0 to v1" + + -- do the reorganisation of the key files + olddir <- fromRepo gitAnnexDir + keys <- getKeysPresent0 olddir + forM_ keys $ \k -> moveAnnex k $ olddir keyFile0 k + + -- update the symlinks to the key files + -- No longer needed here; V1.upgrade does the same thing + + -- Few people had v0 repos, so go the long way around from 0 -> 1 -> 2 + Upgrade.V1.upgrade + +-- these stayed unchanged between v0 and v1 +keyFile0 :: Key -> FilePath +keyFile0 = Upgrade.V1.keyFile1 +fileKey0 :: FilePath -> Key +fileKey0 = Upgrade.V1.fileKey1 +lookupFile0 :: FilePath -> Annex (Maybe (Key, Backend)) +lookupFile0 = Upgrade.V1.lookupFile1 + +getKeysPresent0 :: FilePath -> Annex [Key] +getKeysPresent0 dir = ifM (liftIO $ doesDirectoryExist dir) + ( liftIO $ map fileKey0 + <$> (filterM present =<< getDirectoryContents dir) + , return [] + ) + where + present d = do + result <- tryIO $ + getFileStatus $ dir ++ "/" ++ takeFileName d + case result of + Right s -> return $ isRegularFile s + Left _ -> return False diff --git a/Upgrade/V1.hs b/Upgrade/V1.hs new file mode 100644 index 0000000000..9793f04e8a --- /dev/null +++ b/Upgrade/V1.hs @@ -0,0 +1,241 @@ +{- git-annex v1 -> v2 upgrade support + - + - Copyright 2011 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Upgrade.V1 where + +import System.Posix.Types +import Data.Char + +import Common.Annex +import Types.Key +import Annex.Content +import Logs.Presence +import qualified Annex.Queue +import qualified Git +import qualified Git.LsFiles as LsFiles +import Backend +import Annex.Version +import Utility.FileMode +import Utility.Tmp +import qualified Upgrade.V2 + +-- v2 adds hashing of filenames of content and location log files. +-- Key information is encoded in filenames differently, so +-- both content and location log files move around, and symlinks +-- to content need to be changed. +-- +-- When upgrading a v1 key to v2, file size metadata ought to be +-- added to the key (unless it is a WORM key, which encoded +-- mtime:size in v1). This can only be done when the file content +-- is present. Since upgrades need to happen consistently, +-- (so that two repos get changed the same way by the upgrade, and +-- will merge), that metadata cannot be added on upgrade. +-- +-- Note that file size metadata +-- will only be used for detecting situations where git-annex +-- would run out of disk space, so if some keys don't have it, +-- the impact is minor. At least initially. It could be used in the +-- future by smart auto-repo balancing code, etc. +-- +-- Anyway, since v2 plans ahead for other metadata being included +-- in keys, there should probably be a way to update a key. +-- Something similar to the migrate subcommand could be used, +-- and users could then run that at their leisure. + +upgrade :: Annex Bool +upgrade = do + showAction "v1 to v2" + + ifM (fromRepo Git.repoIsLocalBare) + ( do + moveContent + setVersion defaultVersion + , do + moveContent + updateSymlinks + moveLocationLogs + + Annex.Queue.flush + setVersion defaultVersion + ) + + Upgrade.V2.upgrade + +moveContent :: Annex () +moveContent = do + showAction "moving content" + files <- getKeyFilesPresent1 + forM_ files move + where + move f = do + let k = fileKey1 (takeFileName f) + let d = parentDir f + liftIO $ allowWrite d + liftIO $ allowWrite f + moveAnnex k f + liftIO $ removeDirectory d + +updateSymlinks :: Annex () +updateSymlinks = do + showAction "updating symlinks" + top <- fromRepo Git.repoPath + (files, cleanup) <- inRepo $ LsFiles.inRepo [top] + forM_ files fixlink + void $ liftIO cleanup + where + fixlink f = do + r <- lookupFile1 f + case r of + Nothing -> noop + Just (k, _) -> do + link <- inRepo $ gitAnnexLink f k + liftIO $ removeFile f + liftIO $ createSymbolicLink link f + Annex.Queue.addCommand "add" [Param "--"] [f] + +moveLocationLogs :: Annex () +moveLocationLogs = do + showAction "moving location logs" + logkeys <- oldlocationlogs + forM_ logkeys move + where + oldlocationlogs = do + dir <- fromRepo Upgrade.V2.gitStateDir + ifM (liftIO $ doesDirectoryExist dir) + ( mapMaybe oldlog2key + <$> (liftIO $ getDirectoryContents dir) + , return [] + ) + move (l, k) = do + dest <- fromRepo $ logFile2 k + dir <- fromRepo Upgrade.V2.gitStateDir + let f = dir l + liftIO $ createDirectoryIfMissing True (parentDir dest) + -- could just git mv, but this way deals with + -- log files that are not checked into git, + -- as well as merging with already upgraded + -- logs that have been pulled from elsewhere + old <- liftIO $ readLog1 f + new <- liftIO $ readLog1 dest + liftIO $ writeLog1 dest (old++new) + Annex.Queue.addCommand "add" [Param "--"] [dest] + Annex.Queue.addCommand "add" [Param "--"] [f] + Annex.Queue.addCommand "rm" [Param "--quiet", Param "-f", Param "--"] [f] + +oldlog2key :: FilePath -> Maybe (FilePath, Key) +oldlog2key l + | drop len l == ".log" && sane = Just (l, k) + | otherwise = Nothing + where + len = length l - 4 + k = readKey1 (take len l) + sane = (not . null $ keyName k) && (not . null $ keyBackendName k) + +-- WORM backend keys: "WORM:mtime:size:filename" +-- all the rest: "backend:key" +-- +-- If the file looks like "WORM:XXX-...", then it was created by mixing +-- v2 and v1; that infelicity is worked around by treating the value +-- as the v2 key that it is. +readKey1 :: String -> Key +readKey1 v + | mixup = fromJust $ file2key $ intercalate ":" $ Prelude.tail bits + | otherwise = Key + { keyName = n + , keyBackendName = b + , keySize = s + , keyMtime = t + } + where + bits = split ":" v + b = Prelude.head bits + n = intercalate ":" $ drop (if wormy then 3 else 1) bits + t = if wormy + then Just (Prelude.read (bits !! 1) :: EpochTime) + else Nothing + s = if wormy + then Just (Prelude.read (bits !! 2) :: Integer) + else Nothing + wormy = Prelude.head bits == "WORM" + mixup = wormy && isUpper (Prelude.head $ bits !! 1) + +showKey1 :: Key -> String +showKey1 Key { keyName = n , keyBackendName = b, keySize = s, keyMtime = t } = + intercalate ":" $ filter (not . null) [b, showifhere t, showifhere s, n] + where + showifhere Nothing = "" + showifhere (Just v) = show v + +keyFile1 :: Key -> FilePath +keyFile1 key = replace "/" "%" $ replace "%" "&s" $ replace "&" "&a" $ showKey1 key + +fileKey1 :: FilePath -> Key +fileKey1 file = readKey1 $ + replace "&a" "&" $ replace "&s" "%" $ replace "%" "/" file + +writeLog1 :: FilePath -> [LogLine] -> IO () +writeLog1 file ls = viaTmp writeFile file (showLog ls) + +readLog1 :: FilePath -> IO [LogLine] +readLog1 file = catchDefaultIO [] $ + parseLog <$> readFileStrict file + +lookupFile1 :: FilePath -> Annex (Maybe (Key, Backend)) +lookupFile1 file = do + tl <- liftIO $ tryIO getsymlink + case tl of + Left _ -> return Nothing + Right l -> makekey l + where + getsymlink = takeFileName <$> readSymbolicLink file + makekey l = case maybeLookupBackendName bname of + Nothing -> do + unless (null kname || null bname || + not (isLinkToAnnex l)) $ + warning skip + return Nothing + Just backend -> return $ Just (k, backend) + where + k = fileKey1 l + bname = keyBackendName k + kname = keyName k + skip = "skipping " ++ file ++ + " (unknown backend " ++ bname ++ ")" + +getKeyFilesPresent1 :: Annex [FilePath] +getKeyFilesPresent1 = getKeyFilesPresent1' =<< fromRepo gitAnnexObjectDir +getKeyFilesPresent1' :: FilePath -> Annex [FilePath] +getKeyFilesPresent1' dir = + ifM (liftIO $ doesDirectoryExist dir) + ( do + dirs <- liftIO $ getDirectoryContents dir + let files = map (\d -> dir ++ "/" ++ d ++ "/" ++ takeFileName d) dirs + liftIO $ filterM present files + , return [] + ) + where + present f = do + result <- tryIO $ getFileStatus f + case result of + Right s -> return $ isRegularFile s + Left _ -> return False + +logFile1 :: Git.Repo -> Key -> String +logFile1 repo key = Upgrade.V2.gitStateDir repo ++ keyFile1 key ++ ".log" + +logFile2 :: Key -> Git.Repo -> String +logFile2 = logFile' hashDirLower + +logFile' :: (Key -> FilePath) -> Key -> Git.Repo -> String +logFile' hasher key repo = + gitStateDir repo ++ hasher key ++ keyFile key ++ ".log" + +stateDir :: FilePath +stateDir = addTrailingPathSeparator ".git-annex" + +gitStateDir :: Git.Repo -> FilePath +gitStateDir repo = addTrailingPathSeparator $ Git.repoPath repo stateDir diff --git a/Upgrade/V2.hs b/Upgrade/V2.hs new file mode 100644 index 0000000000..b5de6c8c04 --- /dev/null +++ b/Upgrade/V2.hs @@ -0,0 +1,137 @@ +{- git-annex v2 -> v3 upgrade support + - + - Copyright 2011 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Upgrade.V2 where + +import Common.Annex +import qualified Git +import qualified Git.Command +import qualified Git.Ref +import qualified Annex.Branch +import Logs.Location +import Annex.Content +import Utility.Tmp + +olddir :: Git.Repo -> FilePath +olddir g + | Git.repoIsLocalBare g = "" + | otherwise = ".git-annex" + +{- .git-annex/ moved to a git-annex branch. + - + - Strategy: + - + - * Create the git-annex branch. + - * Find each location log file in .git-annex/, and inject its content + - into the git-annex branch, unioning with any content already in + - there. (in passing, this deals with the semi transition that left + - some location logs hashed two different ways; both are found and + - merged). + - * Also inject remote.log, trust.log, and uuid.log. + - * git rm -rf .git-annex + - * Remove stuff that used to be needed in .gitattributes. + - * Commit changes. + -} +upgrade :: Annex Bool +upgrade = do + showAction "v2 to v3" + bare <- fromRepo Git.repoIsLocalBare + old <- fromRepo olddir + + Annex.Branch.create + showProgress + + e <- liftIO $ doesDirectoryExist old + when e $ do + mapM_ (\(k, f) -> inject f $ logFile k) =<< locationLogs + mapM_ (\f -> inject f f) =<< logFiles old + + saveState False + showProgress + + when e $ do + inRepo $ Git.Command.run [Param "rm", Param "-r", Param "-f", Param "-q", File old] + unless bare $ inRepo gitAttributesUnWrite + showProgress + + unless bare push + + return True + +locationLogs :: Annex [(Key, FilePath)] +locationLogs = do + dir <- fromRepo gitStateDir + liftIO $ do + levela <- dirContents dir + levelb <- mapM tryDirContents levela + files <- mapM tryDirContents (concat levelb) + return $ mapMaybe islogfile (concat files) + where + tryDirContents d = catchDefaultIO [] $ dirContents d + islogfile f = maybe Nothing (\k -> Just (k, f)) $ + logFileKey $ takeFileName f + +inject :: FilePath -> FilePath -> Annex () +inject source dest = do + old <- fromRepo olddir + new <- liftIO (readFile $ old source) + Annex.Branch.change dest $ \prev -> + unlines $ nub $ lines prev ++ lines new + +logFiles :: FilePath -> Annex [FilePath] +logFiles dir = return . filter (".log" `isSuffixOf`) + <=< liftIO $ getDirectoryContents dir + +push :: Annex () +push = do + origin_master <- inRepo $ Git.Ref.exists $ Git.Ref "origin/master" + origin_gitannex <- Annex.Branch.hasOrigin + case (origin_master, origin_gitannex) of + (_, True) -> do + -- Merge in the origin's git-annex branch, + -- so that pushing the git-annex branch + -- will immediately work. Not pushed here, + -- because it's less obnoxious to let the user + -- push. + Annex.Branch.update + (True, False) -> do + -- push git-annex to origin, so that + -- "git push" will from then on + -- automatically push it + Annex.Branch.update -- just in case + showAction "pushing new git-annex branch to origin" + showOutput + 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 + Annex.Branch.update + showLongNote $ + "git-annex branch created\n" ++ + "Be sure to push this branch when pushing to remotes.\n" + +{- Old .gitattributes contents, not needed anymore. -} +attrLines :: [String] +attrLines = + [ stateDir "*.log merge=union" + , stateDir "*/*/*.log merge=union" + ] + +gitAttributesUnWrite :: Git.Repo -> IO () +gitAttributesUnWrite repo = do + let attributes = Git.attributes repo + whenM (doesFileExist attributes) $ do + c <- readFileStrict attributes + liftIO $ viaTmp writeFile attributes $ unlines $ + filter (`notElem` attrLines) $ lines c + Git.Command.run [Param "add", File attributes] repo + +stateDir :: FilePath +stateDir = addTrailingPathSeparator ".git-annex" +gitStateDir :: Git.Repo -> FilePath +gitStateDir repo = addTrailingPathSeparator $ Git.repoPath repo stateDir diff --git a/Usage.hs b/Usage.hs new file mode 100644 index 0000000000..9a48a09086 --- /dev/null +++ b/Usage.hs @@ -0,0 +1,111 @@ +{- git-annex usage messages + - + - Copyright 2010-2011 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Usage where + +import Common.Annex + +import Types.Command + +import System.Console.GetOpt + +usageMessage :: String -> String +usageMessage s = "Usage: " ++ s + +{- Usage message with lists of commands by section. -} +usage :: String -> [Command] -> String +usage header cmds = unlines $ usageMessage header : concatMap go [minBound..] + where + go section + | null cs = [] + | otherwise = + [ "" + , descSection section ++ ":" + , "" + ] ++ map cmdline cs + where + cs = filter (\c -> cmdsection c == section) scmds + cmdline c = concat + [ cmdname c + , namepad (cmdname c) + , cmdparamdesc c + , descpad (cmdparamdesc c) + , cmddesc c + ] + pad n s = replicate (n - length s) ' ' + namepad = pad $ longest cmdname + 1 + descpad = pad $ longest cmdparamdesc + 2 + longest f = foldl max 0 $ map (length . f) cmds + scmds = sort cmds + +{- Usage message for a single command. -} +commandUsage :: Command -> String +commandUsage cmd = unlines + [ usageInfo header (cmdoptions cmd) + , "To see additional options common to all commands, run: git annex help options" + ] + where + header = usageMessage $ unwords + [ "git-annex" + , cmdname cmd + , cmdparamdesc cmd + , "[option ...]" + ] + +{- Descriptions of params used in usage messages. -} +paramPaths :: String +paramPaths = paramOptional $ paramRepeating paramPath -- most often used +paramPath :: String +paramPath = "PATH" +paramKey :: String +paramKey = "KEY" +paramDesc :: String +paramDesc = "DESC" +paramUrl :: String +paramUrl = "URL" +paramNumber :: String +paramNumber = "NUMBER" +paramNumRange :: String +paramNumRange = "NUM|RANGE" +paramRemote :: String +paramRemote = "REMOTE" +paramGlob :: String +paramGlob = "GLOB" +paramName :: String +paramName = "NAME" +paramValue :: String +paramValue = "VALUE" +paramUUID :: String +paramUUID = "UUID" +paramType :: String +paramType = "TYPE" +paramDate :: String +paramDate = "DATE" +paramTime :: String +paramTime = "TIME" +paramFormat :: String +paramFormat = "FORMAT" +paramFile :: String +paramFile = "FILE" +paramGroup :: String +paramGroup = "GROUP" +paramExpression :: String +paramExpression = "EXPR" +paramSize :: String +paramSize = "SIZE" +paramAddress :: String +paramAddress = "ADDRESS" +paramKeyValue :: String +paramKeyValue = "K=V" +paramNothing :: String +paramNothing = "" +paramRepeating :: String -> String +paramRepeating s = s ++ " ..." +paramOptional :: String -> String +paramOptional s = "[" ++ s ++ "]" +paramPair :: String -> String -> String +paramPair a b = a ++ " " ++ b diff --git a/Utility/Applicative.hs b/Utility/Applicative.hs new file mode 100644 index 0000000000..64400c8012 --- /dev/null +++ b/Utility/Applicative.hs @@ -0,0 +1,16 @@ +{- applicative stuff + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Utility.Applicative where + +{- Like <$> , but supports one level of currying. + - + - foo v = bar <$> action v == foo = bar <$$> action + -} +(<$$>) :: Functor f => (a -> b) -> (c -> f a) -> c -> f b +f <$$> v = fmap f . v +infixr 4 <$$> diff --git a/Utility/Base64.hs b/Utility/Base64.hs new file mode 100644 index 0000000000..ec660108a6 --- /dev/null +++ b/Utility/Base64.hs @@ -0,0 +1,24 @@ +{- Simple Base64 access + - + - Copyright 2011 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +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 = fromMaybe bad . fromB64Maybe + where + bad = error "bad base64 encoded data" diff --git a/Utility/Batch.hs b/Utility/Batch.hs new file mode 100644 index 0000000000..c3c34bf270 --- /dev/null +++ b/Utility/Batch.hs @@ -0,0 +1,40 @@ +{- Running a long or expensive batch operation niced. + - + - Copyright 2013 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE CPP #-} + +module Utility.Batch where + +#if defined(linux_HOST_OS) || defined(__ANDROID__) +import Control.Concurrent.Async +import System.Posix.Process +#endif + +{- Runs an operation, at batch priority. + - + - This is done by running it in a bound thread, which on Linux can be set + - to have a different nice level than the rest of the program. Note that + - due to running in a bound thread, some operations may be more expensive + - to perform. Also note that if the action calls forkIO or forkOS itself, + - that will make a new thread that does not have the batch priority. + - + - POSIX threads do not support separate nice levels, so on other operating + - systems, the action is simply ran. + -} +batch :: IO a -> IO a +#if defined(linux_HOST_OS) || defined(__ANDROID__) +batch a = wait =<< batchthread + where + batchthread = asyncBound $ do + setProcessPriority 0 maxNice + a +#else +batch a = a +#endif + +maxNice :: Int +maxNice = 19 diff --git a/Utility/CoProcess.hs b/Utility/CoProcess.hs new file mode 100644 index 0000000000..710d2af136 --- /dev/null +++ b/Utility/CoProcess.hs @@ -0,0 +1,93 @@ +{- Interface for running a shell command as a coprocess, + - sending it queries and getting back results. + - + - Copyright 2012-2013 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE CPP #-} + +module Utility.CoProcess ( + CoProcessHandle, + start, + stop, + query, + rawMode +) where + +import Common + +import Control.Concurrent.MVar + +type CoProcessHandle = MVar CoProcessState + +data CoProcessState = CoProcessState + { coProcessPid :: ProcessHandle + , coProcessTo :: Handle + , coProcessFrom :: Handle + , coProcessSpec :: CoProcessSpec + } + +data CoProcessSpec = CoProcessSpec + { coProcessRestartable :: Bool + , coProcessCmd :: FilePath + , coProcessParams :: [String] + , coProcessEnv :: Maybe [(String, String)] + } + +start :: Bool -> FilePath -> [String] -> Maybe [(String, String)] -> IO CoProcessHandle +start restartable cmd params env = do + s <- start' $ CoProcessSpec restartable cmd params env + newMVar s + +start' :: CoProcessSpec -> IO CoProcessState +start' s = do + (pid, from, to) <- startInteractiveProcess (coProcessCmd s) (coProcessParams s) (coProcessEnv s) + return $ CoProcessState pid to from s + +stop :: CoProcessHandle -> IO () +stop ch = do + s <- readMVar ch + hClose $ coProcessTo s + hClose $ coProcessFrom s + let p = proc (coProcessCmd $ coProcessSpec s) (coProcessParams $ coProcessSpec s) + forceSuccessProcess p (coProcessPid s) + +{- To handle a restartable process, any IO exception thrown by the send and + - receive actions are assumed to mean communication with the process + - failed, and the failed action is re-run with a new process. -} +query :: CoProcessHandle -> (Handle -> IO a) -> (Handle -> IO b) -> IO b +query ch send receive = do + s <- readMVar ch + restartable s (send $ coProcessTo s) $ const $ + restartable s (hFlush $ coProcessTo s) $ const $ + restartable s (receive $ coProcessFrom s) $ + return + where + restartable s a cont + | coProcessRestartable (coProcessSpec s) = + maybe restart cont =<< catchMaybeIO a + | otherwise = cont =<< a + restart = do + s <- takeMVar ch + void $ catchMaybeIO $ do + hClose $ coProcessTo s + hClose $ coProcessFrom s + void $ waitForProcess $ coProcessPid s + s' <- start' (coProcessSpec s) + putMVar ch s' + query ch send receive + +rawMode :: CoProcessHandle -> IO CoProcessHandle +rawMode ch = do + s <- readMVar ch + raw $ coProcessFrom s + raw $ coProcessTo s + return ch + where + raw h = do + fileEncoding h +#ifdef mingw32_HOST_OS + hSetNewlineMode h noNewlineTranslation +#endif diff --git a/Utility/CopyFile.hs b/Utility/CopyFile.hs new file mode 100644 index 0000000000..4a609fd162 --- /dev/null +++ b/Utility/CopyFile.hs @@ -0,0 +1,48 @@ +{- file copying + - + - Copyright 2010-2013 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE CPP #-} + +module Utility.CopyFile ( + copyFileExternal, + createLinkOrCopy +) where + +import Common +import qualified Build.SysConfig as SysConfig + +{- The cp command is used, because I hate reinventing the wheel, + - and because this allows easy access to features like cp --reflink. -} +copyFileExternal :: FilePath -> FilePath -> IO Bool +copyFileExternal src dest = do + whenM (doesFileExist dest) $ + removeFile dest + boolSystem "cp" $ params ++ [File src, File dest] + where +#ifndef __ANDROID__ + params = map snd $ filter fst + [ (SysConfig.cp_reflink_auto, Param "--reflink=auto") + , (SysConfig.cp_a, Param "-a") + , (SysConfig.cp_p && not SysConfig.cp_a, Param "-p") + ] +#else + params = [] +#endif + +{- Create a hard link if the filesystem allows it, and fall back to copying + - the file. -} +createLinkOrCopy :: FilePath -> FilePath -> IO Bool +#ifndef mingw32_HOST_OS +createLinkOrCopy src dest = go `catchIO` const fallback + where + go = do + createLink src dest + return True + fallback = copyFileExternal src dest +#else +createLinkOrCopy = copyFileExternal +#endif diff --git a/Utility/DBus.hs b/Utility/DBus.hs new file mode 100644 index 0000000000..3523a3aa35 --- /dev/null +++ b/Utility/DBus.hs @@ -0,0 +1,84 @@ +{- DBus utilities + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE OverloadedStrings, ScopedTypeVariables #-} + +module Utility.DBus where + +import Utility.Exception + +import DBus.Client +import DBus +import Data.Maybe +import Control.Concurrent +import Control.Exception as E + +type ServiceName = String + +listServiceNames :: Client -> IO [ServiceName] +listServiceNames client = do + reply <- callDBus client "ListNames" [] + return $ fromMaybe [] $ fromVariant (methodReturnBody reply !! 0) + +callDBus :: Client -> MemberName -> [Variant] -> IO MethodReturn +callDBus client name params = call_ client $ + (methodCall "/org/freedesktop/DBus" "org.freedesktop.DBus" name) + { methodCallDestination = Just "org.freedesktop.DBus" + , methodCallBody = params + } + +{- Connects to the bus, and runs the client action. + - + - Throws a ClientError, and closes the connection if it fails to + - process an incoming message, or if the connection is lost. + - Unlike DBus's usual interface, this error is thrown at the top level, + - rather than inside the clientThreadRunner, so it can be caught, and + - runClient re-run as needed. -} +runClient :: IO (Maybe Address) -> (Client -> IO ()) -> IO () +runClient getaddr clientaction = do + env <- getaddr + case env of + Nothing -> throwIO (clientError "runClient: unable to determine DBUS address") + Just addr -> do + {- The clientaction will set up listeners, which + - run in a different thread. We block while + - they're running, until our threadrunner catches + - a ClientError, which it will put into the MVar + - to be rethrown here. -} + mv <- newEmptyMVar + let tr = threadrunner (putMVar mv) + let opts = defaultClientOptions { clientThreadRunner = tr } + client <- connectWith opts addr + clientaction client + e <- takeMVar mv + disconnect client + throw e + where + threadrunner storeerr io = loop + where + loop = catchClientError (io >> loop) storeerr + +{- Connects to the bus, and runs the client action. + - + - If the connection is lost, runs onretry, which can do something like + - a delay, or printing a warning, and has a state value (useful for + - exponential backoff). Once onretry returns, the connection is retried. + -} +persistentClient :: IO (Maybe Address) -> v -> (SomeException -> v -> IO v) -> (Client -> IO ()) -> IO () +persistentClient getaddr v onretry clientaction = + {- runClient can fail with not just ClientError, but also other + - things, if dbus is not running. Let async exceptions through. -} + runClient getaddr clientaction `catchNonAsync` retry + where + retry e = do + v' <- onretry e v + persistentClient getaddr v' onretry clientaction + +{- Catches only ClientError -} +catchClientError :: IO () -> (ClientError -> IO ()) -> IO () +catchClientError io handler = + either handler return =<< (E.try io :: IO (Either ClientError ())) diff --git a/Utility/Daemon.hs b/Utility/Daemon.hs new file mode 100644 index 0000000000..2f942769a8 --- /dev/null +++ b/Utility/Daemon.hs @@ -0,0 +1,121 @@ +{- daemon support + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE CPP #-} + +module Utility.Daemon where + +import Common +#ifndef mingw32_HOST_OS +import Utility.LogFile +#endif + +#ifndef mingw32_HOST_OS +import System.Posix +#else +import System.PosixCompat +#endif + +{- Run an action as a daemon, with all output sent to a file descriptor. + - + - Can write its pid to a file, to guard against multiple instances + - running and allow easy termination. + - + - When successful, does not return. -} +daemonize :: Fd -> Maybe FilePath -> Bool -> IO () -> IO () +#ifndef mingw32_HOST_OS +daemonize logfd pidfile changedirectory a = do + maybe noop checkalreadyrunning pidfile + _ <- forkProcess child1 + out + where + checkalreadyrunning f = maybe noop (const $ alreadyRunning) + =<< checkDaemon f + child1 = do + _ <- createSession + _ <- forkProcess child2 + out + child2 = do + maybe noop lockPidFile pidfile + when changedirectory $ + setCurrentDirectory "/" + nullfd <- openFd "/dev/null" ReadOnly Nothing defaultFileFlags + redir nullfd stdInput + redirLog logfd + a + out + out = exitImmediately ExitSuccess +#else +daemonize = error "daemonize is not implemented on Windows" -- TODO +#endif + +{- Locks the pid file, with an exclusive, non-blocking lock. + - Writes the pid to the file, fully atomically. + - Fails if the pid file is already locked by another process. -} +lockPidFile :: FilePath -> IO () +lockPidFile file = do + createDirectoryIfMissing True (parentDir file) +#ifndef mingw32_HOST_OS + fd <- openFd file ReadWrite (Just stdFileMode) defaultFileFlags + locked <- catchMaybeIO $ setLock fd (WriteLock, AbsoluteSeek, 0, 0) + fd' <- openFd newfile ReadWrite (Just stdFileMode) defaultFileFlags + { trunc = True } + locked' <- catchMaybeIO $ setLock fd' (WriteLock, AbsoluteSeek, 0, 0) + case (locked, locked') of + (Nothing, _) -> alreadyRunning + (_, Nothing) -> alreadyRunning + _ -> do + _ <- fdWrite fd' =<< show <$> getProcessID + closeFd fd +#else + writeFile newfile "-1" +#endif + renameFile newfile file + where + newfile = file ++ ".new" + +alreadyRunning :: IO () +alreadyRunning = error "Daemon is already running." + +{- Checks if the daemon is running, by checking that the pid file + - is locked by the same process that is listed in the pid file. + - + - If it's running, returns its pid. -} +checkDaemon :: FilePath -> IO (Maybe ProcessID) +#ifndef mingw32_HOST_OS +checkDaemon pidfile = do + v <- catchMaybeIO $ + openFd pidfile ReadOnly (Just stdFileMode) defaultFileFlags + case v of + Just fd -> do + locked <- getLock fd (ReadLock, AbsoluteSeek, 0, 0) + p <- readish <$> readFile pidfile + closeFd fd `after` return (check locked p) + Nothing -> return Nothing + where + check Nothing _ = Nothing + check _ Nothing = Nothing + check (Just (pid, _)) (Just pid') + | pid == pid' = Just pid + | otherwise = error $ + "stale pid in " ++ pidfile ++ + " (got " ++ show pid' ++ + "; expected " ++ show pid ++ " )" +#else +checkDaemon pidfile = maybe Nothing readish <$> catchMaybeIO (readFile pidfile) +#endif + +{- Stops the daemon, safely. -} +stopDaemon :: FilePath -> IO () +#ifndef mingw32_HOST_OS +stopDaemon pidfile = go =<< checkDaemon pidfile + where + go Nothing = noop + go (Just pid) = signalProcess sigTERM pid +#else +stopDaemon = error "stopDaemon is not implemented on Windows" -- TODO +#endif diff --git a/Utility/DataUnits.hs b/Utility/DataUnits.hs new file mode 100644 index 0000000000..2a936f1fda --- /dev/null +++ b/Utility/DataUnits.hs @@ -0,0 +1,160 @@ +{- data size display and parsing + - + - Copyright 2011 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + - + - + - And now a rant: + - + - In the beginning, we had powers of two, and they were good. + - + - Disk drive manufacturers noticed that some powers of two were + - sorta close to some powers of ten, and that rounding down to the nearest + - power of ten allowed them to advertise their drives were bigger. This + - was sorta annoying. + - + - Then drives got big. Really, really big. This was good. + - + - Except that the small rounding error perpretrated by the drive + - manufacturers suffered the fate of a small error, and became a large + - error. This was bad. + - + - So, a committee was formed. And it arrived at a committee-like decision, + - which satisfied noone, confused everyone, and made the world an uglier + - place. As with all committees, this was meh. + - + - And the drive manufacturers happily continued selling drives that are + - increasingly smaller than you'd expect, if you don't count on your + - fingers. But that are increasingly too big for anyone to much notice. + - This caused me to need git-annex. + - + - Thus, I use units here that I loathe. Because if I didn't, people would + - be confused that their drives seem the wrong size, and other people would + - complain at me for not being standards compliant. And we call this + - progress? + -} + +module Utility.DataUnits ( + dataUnits, + storageUnits, + memoryUnits, + bandwidthUnits, + oldSchoolUnits, + + roughSize, + compareSizes, + readSize +) where + +import Data.List +import Data.Char + +import Utility.HumanNumber + +type ByteSize = Integer +type Name = String +type Abbrev = String +data Unit = Unit ByteSize Abbrev Name + deriving (Ord, Show, Eq) + +dataUnits :: [Unit] +dataUnits = storageUnits ++ memoryUnits + +{- Storage units are (stupidly) powers of ten. -} +storageUnits :: [Unit] +storageUnits = + [ Unit (p 8) "YB" "yottabyte" + , Unit (p 7) "ZB" "zettabyte" + , Unit (p 6) "EB" "exabyte" + , Unit (p 5) "PB" "petabyte" + , Unit (p 4) "TB" "terabyte" + , Unit (p 3) "GB" "gigabyte" + , Unit (p 2) "MB" "megabyte" + , Unit (p 1) "kB" "kilobyte" -- weird capitalization thanks to committe + , Unit (p 0) "B" "byte" + ] + where + p :: Integer -> Integer + p n = 1000^n + +{- Memory units are (stupidly named) powers of 2. -} +memoryUnits :: [Unit] +memoryUnits = + [ Unit (p 8) "YiB" "yobibyte" + , Unit (p 7) "ZiB" "zebibyte" + , Unit (p 6) "EiB" "exbibyte" + , Unit (p 5) "PiB" "pebibyte" + , Unit (p 4) "TiB" "tebibyte" + , Unit (p 3) "GiB" "gibibyte" + , Unit (p 2) "MiB" "mebibyte" + , Unit (p 1) "KiB" "kibibyte" + , Unit (p 0) "B" "byte" + ] + where + p :: Integer -> Integer + p n = 2^(n*10) + +{- Bandwidth units are only measured in bits if you're some crazy telco. -} +bandwidthUnits :: [Unit] +bandwidthUnits = error "stop trying to rip people off" + +{- Do you yearn for the days when men were men and megabytes were megabytes? -} +oldSchoolUnits :: [Unit] +oldSchoolUnits = zipWith (curry mingle) storageUnits memoryUnits + where + mingle (Unit _ a n, Unit s' _ _) = Unit s' a n + +{- approximate display of a particular number of bytes -} +roughSize :: [Unit] -> Bool -> ByteSize -> String +roughSize units short i + | i < 0 = '-' : findUnit units' (negate i) + | otherwise = findUnit units' i + where + units' = reverse $ sort units -- largest first + + findUnit (u@(Unit s _ _):us) i' + | i' >= s = showUnit i' u + | otherwise = findUnit us i' + findUnit [] i' = showUnit i' (last units') -- bytes + + showUnit x (Unit size abbrev name) = s ++ " " ++ unit + where + v = (fromInteger x :: Double) / fromInteger size + s = showImprecise 2 v + unit + | short = abbrev + | s == "1" = name + | otherwise = name ++ "s" + +{- displays comparison of two sizes -} +compareSizes :: [Unit] -> Bool -> ByteSize -> ByteSize -> String +compareSizes units abbrev old new + | old > new = roughSize units abbrev (old - new) ++ " smaller" + | old < new = roughSize units abbrev (new - old) ++ " larger" + | otherwise = "same" + +{- Parses strings like "10 kilobytes" or "0.5tb". -} +readSize :: [Unit] -> String -> Maybe ByteSize +readSize units input + | null parsednum || null parsedunit = Nothing + | otherwise = Just $ round $ number * fromIntegral multiplier + where + (number, rest) = head parsednum + multiplier = head parsedunit + unitname = takeWhile isAlpha $ dropWhile isSpace rest + + parsednum = reads input :: [(Double, String)] + parsedunit = lookupUnit units unitname + + lookupUnit _ [] = [1] -- no unit given, assume bytes + lookupUnit [] _ = [] + lookupUnit (Unit s a n:us) v + | a ~~ v || n ~~ v = [s] + | plural n ~~ v || a ~~ byteabbrev v = [s] + | otherwise = lookupUnit us v + + a ~~ b = map toLower a == map toLower b + + plural n = n ++ "s" + byteabbrev a = a ++ "b" diff --git a/Utility/DirWatcher.hs b/Utility/DirWatcher.hs new file mode 100644 index 0000000000..d28381fae9 --- /dev/null +++ b/Utility/DirWatcher.hs @@ -0,0 +1,145 @@ +{- generic directory watching interface + - + - Uses inotify, or kqueue, or fsevents to watch a directory + - (and subdirectories) for changes, and runs hooks for different + - sorts of events as they occur. + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE CPP #-} + +module Utility.DirWatcher where + +import Utility.DirWatcher.Types + +#if WITH_INOTIFY +import qualified Utility.INotify as INotify +import qualified System.INotify as INotify +#endif +#if WITH_KQUEUE +import qualified Utility.Kqueue as Kqueue +import Control.Concurrent +#endif +#if WITH_FSEVENTS +import qualified Utility.FSEvents as FSEvents +import qualified System.OSX.FSEvents as FSEvents +#endif + +type Pruner = FilePath -> Bool + +canWatch :: Bool +#if (WITH_INOTIFY || WITH_KQUEUE || WITH_FSEVENTS) +canWatch = True +#else +#if defined linux_HOST_OS +#warning "Building without inotify support" +#endif +canWatch = False +#endif + +{- With inotify, discrete events will be received when making multiple changes + - to the same filename. For example, adding it, deleting it, and adding it + - again will be three events. + - + - OTOH, with kqueue, often only one event is received, indicating the most + - recent state of the file. -} +eventsCoalesce :: Bool +#if WITH_INOTIFY +eventsCoalesce = False +#else +#if (WITH_KQUEUE || WITH_FSEVENTS) +eventsCoalesce = True +#else +eventsCoalesce = undefined +#endif +#endif + +{- With inotify, file closing is tracked to some extent, so an add event + - will always be received for a file once its writer closes it, and + - (typically) not before. This may mean multiple add events for the same file. + - + - fsevents behaves similarly, although different event types are used for + - creating and modification of the file. + - + - OTOH, with kqueue, add events will often be received while a file is + - still being written to, and then no add event will be received once the + - writer closes it. -} +closingTracked :: Bool +#if (WITH_INOTIFY || WITH_FSEVENTS) +closingTracked = True +#else +#if WITH_KQUEUE +closingTracked = False +#else +closingTracked = undefined +#endif +#endif + +{- With inotify, modifications to existing files can be tracked. + - Kqueue does not support this. + - Fsevents generates events when an existing file is reopened and rewritten, + - but not necessarily when it's opened once and modified repeatedly. -} +modifyTracked :: Bool +#if (WITH_INOTIFY || WITH_FSEVENTS) +modifyTracked = True +#else +#if WITH_KQUEUE +modifyTracked = False +#else +modifyTracked = undefined +#endif +#endif + +{- Starts a watcher thread. The runstartup action is passed a scanner action + - to run, that will return once the initial directory scan is complete. + - Once runstartup returns, the watcher thread continues running, + - and processing events. Returns a DirWatcherHandle that can be used + - to shutdown later. -} +#if WITH_INOTIFY +type DirWatcherHandle = INotify.INotify +watchDir :: FilePath -> Pruner -> WatchHooks -> (IO () -> IO ()) -> IO DirWatcherHandle +watchDir dir prune hooks runstartup = do + i <- INotify.initINotify + runstartup $ INotify.watchDir i dir prune hooks + return i +#else +#if WITH_KQUEUE +type DirWatcherHandle = ThreadId +watchDir :: FilePath -> Pruner -> WatchHooks -> (IO Kqueue.Kqueue -> IO Kqueue.Kqueue) -> IO DirWatcherHandle +watchDir dir prune hooks runstartup = do + kq <- runstartup $ Kqueue.initKqueue dir prune + forkIO $ Kqueue.runHooks kq hooks +#else +#if WITH_FSEVENTS +type DirWatcherHandle = FSEvents.EventStream +watchDir :: FilePath -> Pruner -> WatchHooks -> (IO FSEvents.EventStream -> IO FSEvents.EventStream) -> IO DirWatcherHandle +watchDir dir prune hooks runstartup = + runstartup $ FSEvents.watchDir dir prune hooks +#else +type DirWatcherHandle = () +watchDir :: FilePath -> Pruner -> WatchHooks -> (IO () -> IO ()) -> IO DirWatcherHandle +watchDir = undefined +#endif +#endif +#endif + +#if WITH_INOTIFY +stopWatchDir :: DirWatcherHandle -> IO () +stopWatchDir = INotify.killINotify +#else +#if WITH_KQUEUE +stopWatchDir :: DirWatcherHandle -> IO () +stopWatchDir = killThread +#else +#if WITH_FSEVENTS +stopWatchDir :: DirWatcherHandle -> IO () +stopWatchDir = FSEvents.eventStreamDestroy +#else +stopWatchDir :: DirWatcherHandle -> IO () +stopWatchDir = undefined +#endif +#endif +#endif diff --git a/Utility/DirWatcher/Types.hs b/Utility/DirWatcher/Types.hs new file mode 100644 index 0000000000..8cfa69d340 --- /dev/null +++ b/Utility/DirWatcher/Types.hs @@ -0,0 +1,24 @@ +{- generic directory watching types + - + - Copyright 2012 Joey Hess + - + - 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/Directory.hs b/Utility/Directory.hs new file mode 100644 index 0000000000..13e6168cb8 --- /dev/null +++ b/Utility/Directory.hs @@ -0,0 +1,102 @@ +{- directory manipulation + - + - Copyright 2011 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE CPP #-} + +module Utility.Directory where + +import System.IO.Error +import System.PosixCompat.Files +import System.Directory +import Control.Exception (throw) +import Control.Monad +import Control.Monad.IfElse +import System.FilePath +import Control.Applicative +import System.IO.Unsafe (unsafeInterleaveIO) + +import Utility.SafeCommand +import Utility.Tmp +import Utility.Exception +import Utility.Monad + +dirCruft :: FilePath -> Bool +dirCruft "." = True +dirCruft ".." = True +dirCruft _ = False + +{- Lists the contents of a directory. + - Unlike getDirectoryContents, paths are not relative to the directory. -} +dirContents :: FilePath -> IO [FilePath] +dirContents d = map (d ) . filter (not . dirCruft) <$> getDirectoryContents d + +{- Gets files in a directory, and then its subdirectories, recursively, + - and lazily. If the directory does not exist, no exception is thrown, + - instead, [] is returned. -} +dirContentsRecursive :: FilePath -> IO [FilePath] +dirContentsRecursive topdir = dirContentsRecursive' [topdir] + +dirContentsRecursive' :: [FilePath] -> IO [FilePath] +dirContentsRecursive' [] = return [] +dirContentsRecursive' (dir:dirs) = unsafeInterleaveIO $ do + (files, dirs') <- collect [] [] =<< catchDefaultIO [] (dirContents dir) + files' <- dirContentsRecursive' (dirs' ++ dirs) + return (files ++ files') + where + collect files dirs' [] = return (reverse files, reverse dirs') + collect files dirs' (entry:entries) + | dirCruft entry = collect files dirs' entries + | otherwise = do + ifM (doesDirectoryExist entry) + ( collect files (entry:dirs') entries + , collect (entry:files) dirs' entries + ) + +{- Moves one filename to another. + - First tries a rename, but falls back to moving across devices if needed. -} +moveFile :: FilePath -> FilePath -> IO () +moveFile src dest = tryIO (rename src dest) >>= onrename + where + onrename (Right _) = noop + onrename (Left e) + | isPermissionError e = rethrow + | isDoesNotExistError e = rethrow + | otherwise = do + -- copyFile is likely not as optimised as + -- the mv command, so we'll use the latter. + -- But, mv will move into a directory if + -- dest is one, which is not desired. + whenM (isdir dest) rethrow + viaTmp mv dest undefined + where + rethrow = throw e + mv tmp _ = do + ok <- boolSystem "mv" [Param "-f", Param src, Param tmp] + unless ok $ do + -- delete any partial + _ <- tryIO $ removeFile tmp + rethrow + + isdir f = do + r <- tryIO $ getFileStatus f + case r of + (Left _) -> return False + (Right s) -> return $ isDirectory s + +{- Removes a file, which may or may not exist, and does not have to + - be a regular file. + - + - Note that an exception is thrown if the file exists but + - cannot be removed. -} +nukeFile :: FilePath -> IO () +nukeFile file = void $ tryWhenExists go + where +#ifndef mingw32_HOST_OS + go = removeLink file +#else + go = removeFile file +#endif diff --git a/Utility/DiskFree.hs b/Utility/DiskFree.hs new file mode 100644 index 0000000000..aa1bfeedb3 --- /dev/null +++ b/Utility/DiskFree.hs @@ -0,0 +1,38 @@ +{- disk free space checking + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE ForeignFunctionInterface, CPP #-} + +module Utility.DiskFree ( getDiskFree ) where + +#ifdef WITH_CLIBS + +import Common + +import Foreign.C.Types +import Foreign.C.String +import Foreign.C.Error + +foreign import ccall safe "libdiskfree.h diskfree" c_diskfree + :: CString -> IO CULLong + +getDiskFree :: FilePath -> IO (Maybe Integer) +getDiskFree path = withFilePath path $ \c_path -> do + free <- c_diskfree c_path + ifM (safeErrno <$> getErrno) + ( return $ Just $ toInteger free + , return Nothing + ) + where + safeErrno (Errno v) = v == 0 + +#else + +getDiskFree :: FilePath -> IO (Maybe Integer) +getDiskFree _ = return Nothing + +#endif diff --git a/Utility/Dot.hs b/Utility/Dot.hs new file mode 100644 index 0000000000..e57bf009f6 --- /dev/null +++ b/Utility/Dot.hs @@ -0,0 +1,63 @@ +{- a simple graphviz / dot(1) digraph description generator library + - + - Copyright 2010 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Utility.Dot where -- import qualified + +{- generates a graph description from a list of lines -} +graph :: [String] -> String +graph s = unlines $ [header] ++ map indent s ++ [footer] + where + header = "digraph map {" + footer= "}" + +{- a node in the graph -} +graphNode :: String -> String -> String +graphNode nodeid desc = label desc $ quote nodeid + +{- an edge between two nodes -} +graphEdge :: String -> String -> Maybe String -> String +graphEdge fromid toid desc = indent $ maybe edge (`label` edge) desc + where + edge = quote fromid ++ " -> " ++ quote toid + +{- adds a label to a node or edge -} +label :: String -> String -> String +label = attr "label" + +{- adds an attribute to a node or edge + - (can be called multiple times for multiple attributes) -} +attr :: String -> String -> String -> String +attr a v s = s ++ " [ " ++ a ++ "=" ++ quote v ++ " ]" + +{- fills a node with a color -} +fillColor :: String -> String -> String +fillColor color s = attr "fillcolor" color $ attr "style" "filled" s + +{- apply to graphNode to put the node in a labeled box -} +subGraph :: String -> String -> String -> String -> String +subGraph subid l color s = + "subgraph " ++ name ++ " {\n" ++ + ii setlabel ++ + ii setfilled ++ + ii setcolor ++ + ii s ++ + indent "}" + where + -- the "cluster_" makes dot draw a box + name = quote ("cluster_" ++ subid) + setlabel = "label=" ++ quote l + setfilled = "style=" ++ quote "filled" + setcolor = "fillcolor=" ++ quote color + ii x = indent (indent x) ++ "\n" + +indent ::String -> String +indent s = '\t' : s + +quote :: String -> String +quote s = "\"" ++ s' ++ "\"" + where + s' = filter (/= '"') s diff --git a/Utility/Env.hs b/Utility/Env.hs new file mode 100644 index 0000000000..cb738732f9 --- /dev/null +++ b/Utility/Env.hs @@ -0,0 +1,63 @@ +{- portable environment variables + - + - Copyright 2013 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE CPP #-} + +module Utility.Env where + +#ifdef mingw32_HOST_OS +import Utility.Exception +import Control.Applicative +import Data.Maybe +import qualified System.Environment as E +#else +import qualified System.Posix.Env as PE +#endif + +getEnv :: String -> IO (Maybe String) +#ifndef mingw32_HOST_OS +getEnv = PE.getEnv +#else +getEnv = catchMaybeIO . E.getEnv +#endif + +getEnvDefault :: String -> String -> IO String +#ifndef mingw32_HOST_OS +getEnvDefault = PE.getEnvDefault +#else +getEnvDefault var fallback = fromMaybe fallback <$> getEnv var +#endif + +getEnvironment :: IO [(String, String)] +#ifndef mingw32_HOST_OS +getEnvironment = PE.getEnvironment +#else +getEnvironment = E.getEnvironment +#endif + +{- Returns True if it could successfully set the environment variable. + - + - There is, apparently, no way to do this in Windows. Instead, + - environment varuables must be provided when running a new process. -} +setEnv :: String -> String -> Bool -> IO Bool +#ifndef mingw32_HOST_OS +setEnv var val overwrite = do + PE.setEnv var val overwrite + return True +#else +setEnv _ _ _ = return False +#endif + +{- Returns True if it could successfully unset the environment variable. -} +unsetEnv :: String -> IO Bool +#ifndef mingw32_HOST_OS +unsetEnv var = do + PE.unsetEnv var + return True +#else +unsetEnv _ = return False +#endif diff --git a/Utility/Exception.hs b/Utility/Exception.hs new file mode 100644 index 0000000000..3835d741dd --- /dev/null +++ b/Utility/Exception.hs @@ -0,0 +1,58 @@ +{- Simple IO exception handling (and some more) + - + - Copyright 2011-2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE ScopedTypeVariables #-} + +module Utility.Exception where + +import Control.Exception +import qualified Control.Exception as E +import Control.Applicative +import Control.Monad +import System.IO.Error (isDoesNotExistError) + +{- Catches IO errors and returns a Bool -} +catchBoolIO :: IO Bool -> IO Bool +catchBoolIO a = catchDefaultIO False a + +{- Catches IO errors and returns a Maybe -} +catchMaybeIO :: IO a -> IO (Maybe a) +catchMaybeIO a = catchDefaultIO Nothing $ Just <$> a + +{- Catches IO errors and returns a default value. -} +catchDefaultIO :: a -> IO a -> IO a +catchDefaultIO def a = catchIO a (const $ return def) + +{- Catches IO errors and returns the error message. -} +catchMsgIO :: IO a -> IO (Either String a) +catchMsgIO a = either (Left . show) Right <$> tryIO a + +{- catch specialized for IO errors only -} +catchIO :: IO a -> (IOException -> IO a) -> IO a +catchIO = E.catch + +{- try specialized for IO errors only -} +tryIO :: IO a -> IO (Either IOException a) +tryIO = try + +{- Catches all exceptions except for async exceptions. + - This is often better to use than catching them all, so that + - ThreadKilled and UserInterrupt get through. + -} +catchNonAsync :: IO a -> (SomeException -> IO a) -> IO a +catchNonAsync a onerr = a `catches` + [ Handler (\ (e :: AsyncException) -> throw e) + , Handler (\ (e :: SomeException) -> onerr e) + ] + +tryNonAsync :: IO a -> IO (Either SomeException a) +tryNonAsync a = (Right <$> a) `catchNonAsync` (return . Left) + +{- Catches only DoesNotExist exceptions, and lets all others through. -} +tryWhenExists :: IO a -> IO (Maybe a) +tryWhenExists a = either (const Nothing) Just <$> + tryJust (guard . isDoesNotExistError) a diff --git a/Utility/ExternalSHA.hs b/Utility/ExternalSHA.hs new file mode 100644 index 0000000000..21241d302b --- /dev/null +++ b/Utility/ExternalSHA.hs @@ -0,0 +1,67 @@ +{- Calculating a SHA checksum with an external command. + - + - This is often faster than using Haskell libraries. + - + - Copyright 2011-2013 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Utility.ExternalSHA (externalSHA) where + +import Utility.SafeCommand +import Utility.Process +import Utility.FileSystemEncoding +import Utility.Misc + +import System.Process +import Data.List +import Data.Char +import Control.Applicative +import System.IO + +externalSHA :: String -> Int -> FilePath -> IO (Either String String) +externalSHA command shasize file = do + ls <- lines <$> readsha (toCommand [File file]) + return $ sanitycheck =<< parse ls + where + {- sha commands output the filename, so need to set fileEncoding -} + readsha args = + withHandle StdoutHandle (createProcessChecked checkSuccessProcess) p $ \h -> do + fileEncoding h + output <- hGetContentsStrict h + hClose h + return output + where + p = (proc command args) { std_out = CreatePipe } + + {- The first word of the output is taken to be the sha. -} + parse [] = bad + parse (l:_) + | null sha = bad + -- sha is prefixed with \ when filename contains certian chars + | "\\" `isPrefixOf` sha = Right $ drop 1 sha + | otherwise = Right sha + where + sha = fst $ separate (== ' ') l + bad = Left $ command ++ " parse error" + + {- Check that we've correctly parsing the output of the command, + - by making sure the sha we read is of the expected length + - and contains only the right characters. -} + sanitycheck sha + | length sha /= expectedSHALength shasize = + Left $ "Failed to parse the output of " ++ command + | any (`notElem` "0123456789abcdef") sha' = + Left $ "Unexpected character in output of " ++ command ++ "\"" ++ sha ++ "\"" + | otherwise = Right sha' + where + sha' = map toLower sha + +expectedSHALength :: Int -> Int +expectedSHALength 1 = 40 +expectedSHALength 256 = 64 +expectedSHALength 512 = 128 +expectedSHALength 224 = 56 +expectedSHALength 384 = 96 +expectedSHALength _ = 0 diff --git a/Utility/FSEvents.hs b/Utility/FSEvents.hs new file mode 100644 index 0000000000..d6663e9d76 --- /dev/null +++ b/Utility/FSEvents.hs @@ -0,0 +1,92 @@ +{- FSEvents interface + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Utility.FSEvents where + +import Common hiding (isDirectory) +import Utility.DirWatcher.Types + +import System.OSX.FSEvents +import qualified System.Posix.Files as Files +import Data.Bits ((.&.)) + +watchDir :: FilePath -> (FilePath -> Bool) -> WatchHooks -> IO EventStream +watchDir dir ignored hooks = do + unlessM fileLevelEventsSupported $ + error "Need at least OSX 10.7.0 for file-level FSEvents" + scan dir + eventStreamCreate [dir] 1.0 True True True handle + where + handle evt + | ignoredPath ignored (eventPath evt) = noop + | otherwise = do + {- More than one flag may be set, if events occurred + - close together. + - + - Order is important.. + - If a file is added and then deleted, we'll see it's + - not present, and addHook won't run. + - OTOH, if a file is deleted and then re-added, + - the delHook will run first, followed by the addHook. + -} + + when (hasflag eventFlagItemRemoved) $ + if hasflag eventFlagItemIsDir + then runhook delDirHook Nothing + else runhook delHook Nothing + when (hasflag eventFlagItemCreated) $ + maybe noop handleadd =<< getstatus (eventPath evt) + {- When a file or dir is renamed, a rename event is + - received for both its old and its new name. -} + when (hasflag eventFlagItemRenamed) $ + if hasflag eventFlagItemIsDir + then ifM (doesDirectoryExist $ eventPath evt) + ( scan $ eventPath evt + , runhook delDirHook Nothing + ) + else maybe (runhook delHook Nothing) handleadd + =<< getstatus (eventPath evt) + {- Add hooks are run when a file is modified for + - compatability with INotify, which calls the add + - hook when a file is closed, and so tends to call + - both add and modify for file modifications. -} + when (hasflag eventFlagItemModified && not (hasflag eventFlagItemIsDir)) $ do + ms <- getstatus $ eventPath evt + maybe noop handleadd ms + runhook modifyHook ms + where + hasflag f = eventFlags evt .&. f /= 0 + runhook h s = maybe noop (\a -> a (eventPath evt) s) (h hooks) + handleadd s + | Files.isSymbolicLink s = runhook addSymlinkHook $ Just s + | Files.isRegularFile s = runhook addHook $ Just s + | otherwise = noop + + scan d = unless (ignoredPath ignored d) $ + mapM_ go =<< dirContentsRecursive d + where + go f + | ignoredPath ignored f = noop + | otherwise = do + ms <- getstatus f + case ms of + Nothing -> noop + Just s + | Files.isSymbolicLink s -> + runhook addSymlinkHook ms + | Files.isRegularFile s -> + runhook addHook ms + | otherwise -> + noop + where + runhook h s = maybe noop (\a -> a f s) (h hooks) + + getstatus = catchMaybeIO . getSymbolicLinkStatus + +{- Check each component of the path to see if it's ignored. -} +ignoredPath :: (FilePath -> Bool) -> FilePath -> Bool +ignoredPath ignored = any ignored . map dropTrailingPathSeparator . splitPath diff --git a/Utility/FileMode.hs b/Utility/FileMode.hs new file mode 100644 index 0000000000..d76fb5703c --- /dev/null +++ b/Utility/FileMode.hs @@ -0,0 +1,135 @@ +{- File mode utilities. + - + - Copyright 2010-2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE CPP #-} + +module Utility.FileMode where + +import Common + +import Control.Exception (bracket) +import System.PosixCompat.Types +#ifndef mingw32_HOST_OS +import System.Posix.Files +#endif +import Foreign (complement) + +{- Applies a conversion function to a file's mode. -} +modifyFileMode :: FilePath -> (FileMode -> FileMode) -> IO () +modifyFileMode f convert = void $ modifyFileMode' f convert +modifyFileMode' :: FilePath -> (FileMode -> FileMode) -> IO FileMode +modifyFileMode' f convert = do + s <- getFileStatus f + let old = fileMode s + let new = convert old + when (new /= old) $ + setFileMode f new + return old + +{- Adds the specified FileModes to the input mode, leaving the rest + - unchanged. -} +addModes :: [FileMode] -> FileMode -> FileMode +addModes ms m = combineModes (m:ms) + +{- Removes the specified FileModes from the input mode. -} +removeModes :: [FileMode] -> FileMode -> FileMode +removeModes ms m = m `intersectFileModes` complement (combineModes ms) + +{- Runs an action after changing a file's mode, then restores the old mode. -} +withModifiedFileMode :: FilePath -> (FileMode -> FileMode) -> IO a -> IO a +withModifiedFileMode file convert a = bracket setup cleanup go + where + setup = modifyFileMode' file convert + cleanup oldmode = modifyFileMode file (const oldmode) + go _ = a + +writeModes :: [FileMode] +writeModes = [ownerWriteMode, groupWriteMode, otherWriteMode] + +readModes :: [FileMode] +readModes = [ownerReadMode, groupReadMode, otherReadMode] + +executeModes :: [FileMode] +executeModes = [ownerExecuteMode, groupExecuteMode, otherExecuteMode] + +{- Removes the write bits from a file. -} +preventWrite :: FilePath -> IO () +preventWrite f = modifyFileMode f $ removeModes writeModes + +{- Turns a file's owner write bit back on. -} +allowWrite :: FilePath -> IO () +allowWrite f = modifyFileMode f $ addModes [ownerWriteMode] + +{- Allows owner and group to read and write to a file. -} +groupWriteRead :: FilePath -> IO () +groupWriteRead f = modifyFileMode f $ addModes + [ ownerWriteMode, groupWriteMode + , ownerReadMode, groupReadMode + ] + +checkMode :: FileMode -> FileMode -> Bool +checkMode checkfor mode = checkfor `intersectFileModes` mode == checkfor + +{- Checks if a file mode indicates it's a symlink. -} +isSymLink :: FileMode -> Bool +#ifdef mingw32_HOST_OS +isSymLink _ = False +#else +isSymLink = checkMode symbolicLinkMode +#endif + +{- Checks if a file has any executable bits set. -} +isExecutable :: FileMode -> Bool +isExecutable mode = combineModes executeModes `intersectFileModes` mode /= 0 + +{- Runs an action without that pesky umask influencing it, unless the + - passed FileMode is the standard one. -} +noUmask :: FileMode -> IO a -> IO a +#ifndef mingw32_HOST_OS +noUmask mode a + | mode == stdFileMode = a + | otherwise = bracket setup cleanup go + where + setup = setFileCreationMask nullFileMode + cleanup = setFileCreationMask + go _ = a +#else +noUmask _ a = a +#endif + +combineModes :: [FileMode] -> FileMode +combineModes [] = undefined +combineModes [m] = m +combineModes (m:ms) = foldl unionFileModes m ms + +isSticky :: FileMode -> Bool +#ifdef mingw32_HOST_OS +isSticky _ = False +#else +isSticky = checkMode stickyMode + +stickyMode :: FileMode +stickyMode = 512 + +setSticky :: FilePath -> IO () +setSticky f = modifyFileMode f $ addModes [stickyMode] +#endif + +{- Writes a file, ensuring that its modes do not allow it to be read + - by anyone other than the current user, before any content is written. + - + - On a filesystem that does not support file permissions, this is the same + - as writeFile. + -} +writeFileProtected :: FilePath -> String -> IO () +writeFileProtected file content = do + h <- openFile file WriteMode + void $ tryIO $ + modifyFileMode file $ + removeModes [groupReadMode, otherReadMode] + hPutStr h content + hClose h diff --git a/Utility/FileSystemEncoding.hs b/Utility/FileSystemEncoding.hs new file mode 100644 index 0000000000..ac105e73d2 --- /dev/null +++ b/Utility/FileSystemEncoding.hs @@ -0,0 +1,93 @@ +{- GHC File system encoding handling. + - + - Copyright 2012-2013 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Utility.FileSystemEncoding ( + fileEncoding, + withFilePath, + md5FilePath, + decodeW8, + encodeW8, + truncateFilePath, +) where + +import qualified GHC.Foreign as GHC +import qualified GHC.IO.Encoding as Encoding +import Foreign.C +import System.IO +import System.IO.Unsafe +import qualified Data.Hash.MD5 as MD5 +import Data.Word +import Data.Bits.Utils + +{- Sets a Handle to use the filesystem encoding. This causes data + - written or read from it to be encoded/decoded the same + - as ghc 7.4 does to filenames etc. This special encoding + - allows "arbitrary undecodable bytes to be round-tripped through it". -} +fileEncoding :: Handle -> IO () +fileEncoding h = hSetEncoding h =<< Encoding.getFileSystemEncoding + +{- Marshal a Haskell FilePath into a NUL terminated C string using temporary + - storage. The FilePath is encoded using the filesystem encoding, + - reversing the decoding that should have been done when the FilePath + - was obtained. -} +withFilePath :: FilePath -> (CString -> IO a) -> IO a +withFilePath fp f = Encoding.getFileSystemEncoding + >>= \enc -> GHC.withCString enc fp f + +{- Encodes a FilePath into a String, applying the filesystem encoding. + - + - There are very few things it makes sense to do with such an encoded + - string. It's not a legal filename; it should not be displayed. + - So this function is not exported, but instead used by the few functions + - that can usefully consume it. + - + - This use of unsafePerformIO is belived to be safe; GHC's interface + - only allows doing this conversion with CStrings, and the CString buffer + - is allocated, used, and deallocated within the call, with no side + - effects. + -} +{-# NOINLINE _encodeFilePath #-} +_encodeFilePath :: FilePath -> String +_encodeFilePath fp = unsafePerformIO $ do + enc <- Encoding.getFileSystemEncoding + GHC.withCString enc fp $ GHC.peekCString Encoding.char8 + +{- Encodes a FilePath into a Md5.Str, applying the filesystem encoding. -} +md5FilePath :: FilePath -> MD5.Str +md5FilePath = MD5.Str . _encodeFilePath + +{- Converts a [Word8] to a FilePath, encoding using the filesystem encoding. + - + - w82c produces a String, which may contain Chars that are invalid + - unicode. From there, this is really a simple matter of applying the + - file system encoding, only complicated by GHC's interface to doing so. + -} +{-# NOINLINE encodeW8 #-} +encodeW8 :: [Word8] -> FilePath +encodeW8 w8 = unsafePerformIO $ do + enc <- Encoding.getFileSystemEncoding + GHC.withCString Encoding.char8 (w82s w8) $ GHC.peekCString enc + +{- Useful when you want the actual number of bytes that will be used to + - represent the FilePath on disk. -} +decodeW8 :: FilePath -> [Word8] +decodeW8 = s2w8 . _encodeFilePath + +{- Truncates a FilePath to the given number of bytes (or less), + - as represented on disk. + - + - Avoids returning an invalid part of a unicode byte sequence, at the + - cost of efficiency when running on a large FilePath. + -} +truncateFilePath :: Int -> FilePath -> FilePath +truncateFilePath n = go . reverse + where + go f = + let bytes = decodeW8 f + in if length bytes <= n + then reverse f + else go (drop 1 f) diff --git a/Utility/Format.hs b/Utility/Format.hs new file mode 100644 index 0000000000..97a966ac1a --- /dev/null +++ b/Utility/Format.hs @@ -0,0 +1,173 @@ +{- Formatted string handling. + - + - Copyright 2010, 2011 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Utility.Format ( + Format, + gen, + format, + decode_c, + encode_c, + prop_idempotent_deencode +) where + +import Text.Printf (printf) +import Data.Char (isAlphaNum, isOctDigit, isSpace, chr, ord) +import Data.Maybe (fromMaybe) +import Data.Word (Word8) +import Data.List (isPrefixOf) +import qualified Codec.Binary.UTF8.String +import qualified Data.Map as M + +import Utility.PartialPrelude + +type FormatString = String + +{- A format consists of a list of fragments. -} +type Format = [Frag] + +{- A fragment is either a constant string, + - or a variable, with a justification. -} +data Frag = Const String | Var String Justify + deriving (Show) + +data Justify = LeftJustified Int | RightJustified Int | UnJustified + deriving (Show) + +type Variables = M.Map String String + +{- Expands a Format using some variables, generating a formatted string. + - This can be repeatedly called, efficiently. -} +format :: Format -> Variables -> String +format f vars = concatMap expand f + where + expand (Const s) = s + expand (Var name j) + | "escaped_" `isPrefixOf` name = + justify j $ encode_c_strict $ + getvar $ drop (length "escaped_") name + | otherwise = justify j $ getvar name + getvar name = fromMaybe "" $ M.lookup name vars + justify UnJustified s = s + justify (LeftJustified i) s = s ++ pad i s + justify (RightJustified i) s = pad i s ++ s + pad i s = take (i - length s) spaces + spaces = repeat ' ' + +{- Generates a Format that can be used to expand variables in a + - format string, such as "${foo} ${bar;10} ${baz;-10}\n" + - + - (This is the same type of format string used by dpkg-query.) + -} +gen :: FormatString -> Format +gen = filter (not . empty) . fuse [] . scan [] . decode_c + where + -- The Format is built up in reverse, for efficiency, + -- and can have many adjacent Consts. Fusing it fixes both + -- problems. + fuse f [] = f + fuse f (Const c1:Const c2:vs) = fuse f $ Const (c2++c1) : vs + fuse f (v:vs) = fuse (v:f) vs + + scan f (a:b:cs) + | a == '$' && b == '{' = invar f [] cs + | otherwise = scan (Const [a] : f ) (b:cs) + scan f v = Const v : f + + invar f var [] = Const (novar var) : f + invar f var (c:cs) + | c == '}' = foundvar f var UnJustified cs + | isAlphaNum c || c == '_' = invar f (c:var) cs + | c == ';' = inpad "" f var cs + | otherwise = scan ((Const $ novar $ c:var):f) cs + + inpad p f var (c:cs) + | c == '}' = foundvar f var (readjustify $ reverse p) cs + | otherwise = inpad (c:p) f var cs + inpad p f var [] = Const (novar $ p++";"++var) : f + readjustify = getjustify . fromMaybe 0 . readish + getjustify i + | i == 0 = UnJustified + | i < 0 = LeftJustified (-1 * i) + | otherwise = RightJustified i + novar v = "${" ++ reverse v + foundvar f v p = scan (Var (reverse v) p : f) + +empty :: Frag -> Bool +empty (Const "") = True +empty _ = False + +{- Decodes a C-style encoding, where \n is a newline, \NNN is an octal + - encoded character, etc. + -} +decode_c :: FormatString -> FormatString +decode_c [] = [] +decode_c s = unescape ("", s) + where + e = '\\' + unescape (b, []) = b + -- look for escapes starting with '\' + unescape (b, v) = b ++ fst pair ++ unescape (handle $ snd pair) + where + pair = span (/= e) v + isescape x = x == e + -- \NNN is an octal encoded character + handle (x:n1:n2:n3:rest) + | isescape x && alloctal = (fromoctal, rest) + where + alloctal = isOctDigit n1 && isOctDigit n2 && isOctDigit n3 + fromoctal = [chr $ readoctal [n1, n2, n3]] + readoctal o = Prelude.read $ "0o" ++ o :: Int + -- \C is used for a few special characters + handle (x:nc:rest) + | isescape x = ([echar nc], rest) + where + echar 'a' = '\a' + echar 'b' = '\b' + echar 'f' = '\f' + echar 'n' = '\n' + echar 'r' = '\r' + echar 't' = '\t' + echar 'v' = '\v' + echar a = a + handle n = ("", n) + +{- Inverse of decode_c. -} +encode_c :: FormatString -> FormatString +encode_c = encode_c' (const False) + +{- Encodes more strictly, including whitespace. -} +encode_c_strict :: FormatString -> FormatString +encode_c_strict = encode_c' isSpace + +encode_c' :: (Char -> Bool) -> FormatString -> FormatString +encode_c' p = concatMap echar + where + e c = '\\' : [c] + echar '\a' = e 'a' + echar '\b' = e 'b' + echar '\f' = e 'f' + echar '\n' = e 'n' + echar '\r' = e 'r' + echar '\t' = e 't' + echar '\v' = e 'v' + echar '\\' = e '\\' + echar '"' = e '"' + echar c + | ord c < 0x20 = e_asc c -- low ascii + | ord c >= 256 = e_utf c -- unicode + | ord c > 0x7E = e_asc c -- high ascii + | p c = e_asc c -- unprintable ascii + | otherwise = [c] -- printable ascii + -- unicode character is decomposed to individual Word8s, + -- and each is shown in octal + e_utf c = showoctal =<< (Codec.Binary.UTF8.String.encode [c] :: [Word8]) + e_asc c = showoctal $ ord c + showoctal i = '\\' : printf "%03o" i + +{- for quickcheck -} +prop_idempotent_deencode :: String -> Bool +prop_idempotent_deencode s = s == decode_c (encode_c s) diff --git a/Utility/FreeDesktop.hs b/Utility/FreeDesktop.hs new file mode 100644 index 0000000000..da9d7b6185 --- /dev/null +++ b/Utility/FreeDesktop.hs @@ -0,0 +1,144 @@ +{- Freedesktop.org specifications + - + - http://standards.freedesktop.org/basedir-spec/latest/ + - http://standards.freedesktop.org/desktop-entry-spec/latest/ + - http://standards.freedesktop.org/menu-spec/latest/ + - http://standards.freedesktop.org/icon-theme-spec/latest/ + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Utility.FreeDesktop ( + DesktopEntry, + genDesktopEntry, + buildDesktopMenuFile, + writeDesktopMenuFile, + desktopMenuFilePath, + autoStartPath, + iconDir, + iconFilePath, + systemDataDir, + systemConfigDir, + userDataDir, + userConfigDir, + userDesktopDir +) where + +import Utility.Exception +import Utility.Path +import Utility.UserInfo +import Utility.Process +import Utility.PartialPrelude + +import System.Environment +import System.Directory +import System.FilePath +import Data.List +import Data.String.Utils +import Data.Maybe +import Control.Applicative + +type DesktopEntry = [(Key, Value)] + +type Key = String + +data Value = StringV String | BoolV Bool | NumericV Float | ListV [Value] + +toString :: Value -> String +toString (StringV s) = s +toString (BoolV b) + | b = "true" + | otherwise = "false" +toString(NumericV f) = show f +toString (ListV l) + | null l = "" + | otherwise = (intercalate ";" $ map (escapesemi . toString) l) ++ ";" + where + escapesemi = join "\\;" . split ";" + +genDesktopEntry :: String -> String -> Bool -> FilePath -> Maybe String -> [String] -> DesktopEntry +genDesktopEntry name comment terminal program icon categories = catMaybes + [ item "Type" StringV "Application" + , item "Version" NumericV 1.0 + , item "Name" StringV name + , item "Comment" StringV comment + , item "Terminal" BoolV terminal + , item "Exec" StringV program + , maybe Nothing (item "Icon" StringV) icon + , item "Categories" ListV (map StringV categories) + ] + where + item x c y = Just (x, c y) + +buildDesktopMenuFile :: DesktopEntry -> String +buildDesktopMenuFile d = unlines ("[Desktop Entry]" : map keyvalue d) ++ "\n" + where + keyvalue (k, v) = k ++ "=" ++ toString v + +writeDesktopMenuFile :: DesktopEntry -> String -> IO () +writeDesktopMenuFile d file = do + createDirectoryIfMissing True (parentDir file) + writeFile file $ buildDesktopMenuFile d + +{- Path to use for a desktop menu file, in either the systemDataDir or + - the userDataDir -} +desktopMenuFilePath :: String -> FilePath -> FilePath +desktopMenuFilePath basename datadir = + datadir "applications" desktopfile basename + +{- Path to use for a desktop autostart file, in either the systemDataDir + - or the userDataDir -} +autoStartPath :: String -> FilePath -> FilePath +autoStartPath basename configdir = + configdir "autostart" desktopfile basename + +{- Base directory to install an icon file, in either the systemDataDir + - or the userDatadir. -} +iconDir :: FilePath -> FilePath +iconDir datadir = datadir "icons" "hicolor" + +{- Filename of an icon, given the iconDir to use. + - + - The resolution is something like "48x48" or "scalable". -} +iconFilePath :: FilePath -> String -> FilePath -> FilePath +iconFilePath file resolution icondir = + icondir resolution "apps" file + +desktopfile :: FilePath -> FilePath +desktopfile f = f ++ ".desktop" + +{- Directory used for installation of system wide data files.. -} +systemDataDir :: FilePath +systemDataDir = "/usr/share" + +{- Directory used for installation of system wide config files. -} +systemConfigDir :: FilePath +systemConfigDir = "/etc/xdg" + +{- Directory for user data files. -} +userDataDir :: IO FilePath +userDataDir = xdgEnvHome "DATA_HOME" ".local/share" + +{- Directory for user config files. -} +userConfigDir :: IO FilePath +userConfigDir = xdgEnvHome "CONFIG_HOME" ".config" + +{- Directory for the user's Desktop, may be localized. + - + - This is not looked up very fast; the config file is in a shell format + - that is best parsed by shell, so xdg-user-dir is used, with a fallback + - to ~/Desktop. -} +userDesktopDir :: IO FilePath +userDesktopDir = maybe fallback return =<< (parse <$> xdg_user_dir) + where + parse = maybe Nothing (headMaybe . lines) + xdg_user_dir = catchMaybeIO $ readProcess "xdg-user-dir" ["DESKTOP"] + fallback = xdgEnvHome "DESKTOP_DIR" "Desktop" + +xdgEnvHome :: String -> String -> IO String +xdgEnvHome envbase homedef = do + home <- myHomeDir + catchDefaultIO (home homedef) $ + getEnv $ "XDG_" ++ envbase diff --git a/Utility/Gpg.hs b/Utility/Gpg.hs new file mode 100644 index 0000000000..81180148e5 --- /dev/null +++ b/Utility/Gpg.hs @@ -0,0 +1,262 @@ +{- gpg interface + - + - Copyright 2011 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE CPP #-} + +module Utility.Gpg where + +import Control.Applicative +import Control.Concurrent + +import Common +import qualified Build.SysConfig as SysConfig + +#ifndef mingw32_HOST_OS +import System.Posix.Types +import Control.Exception (bracket) +import System.Path +import Utility.Env +#else +import Utility.Tmp +#endif + +newtype KeyIds = KeyIds [String] + deriving (Ord, Eq) + +{- If a specific gpg command was found at configure time, use it. + - Otherwise, try to run gpg. -} +gpgcmd :: FilePath +gpgcmd = fromMaybe "gpg" SysConfig.gpg + +stdParams :: [CommandParam] -> IO [String] +stdParams params = do +#ifndef mingw32_HOST_OS + -- Enable batch mode if GPG_AGENT_INFO is set, to avoid extraneous + -- gpg output about password prompts. GPG_BATCH is set by the test + -- suite for a similar reason. + e <- getEnv "GPG_AGENT_INFO" + b <- getEnv "GPG_BATCH" + let batch = if isNothing e && isNothing b + then [] + else ["--batch", "--no-tty", "--use-agent"] + return $ batch ++ defaults ++ toCommand params +#else + return $ defaults ++ toCommand params +#endif + where + -- be quiet, even about checking the trustdb + defaults = ["--quiet", "--trust-model", "always"] + +{- Runs gpg with some params and returns its stdout, strictly. -} +readStrict :: [CommandParam] -> IO String +readStrict params = do + params' <- stdParams params + withHandle StdoutHandle createProcessSuccess (proc gpgcmd params') $ \h -> do + hSetBinaryMode h True + hGetContentsStrict h + +{- Runs gpg, piping an input value to it, and returning its stdout, + - strictly. -} +pipeStrict :: [CommandParam] -> String -> IO String +pipeStrict params input = do + params' <- stdParams params + withBothHandles createProcessSuccess (proc gpgcmd params') $ \(to, from) -> do + hSetBinaryMode to True + hSetBinaryMode from True + hPutStr to input + hClose to + hGetContentsStrict from + +{- Runs gpg with some parameters. First sends it a passphrase via + - --passphrase-fd. Then runs a feeder action that is passed a handle and + - should write to it all the data to input to gpg. Finally, runs + - a reader action that is passed a handle to gpg's output. + - + - Runs gpg in batch mode; this is necessary to avoid gpg 2.x prompting for + - the passphrase. + - + - Note that to avoid deadlock with the cleanup stage, + - the reader must fully consume gpg's input before returning. -} +feedRead :: [CommandParam] -> String -> (Handle -> IO ()) -> (Handle -> IO a) -> IO a +feedRead params passphrase feeder reader = do +#ifndef mingw32_HOST_OS + -- pipe the passphrase into gpg on a fd + (frompipe, topipe) <- createPipe + void $ forkIO $ do + toh <- fdToHandle topipe + hPutStrLn toh passphrase + hClose toh + let Fd pfd = frompipe + let passphrasefd = [Param "--passphrase-fd", Param $ show pfd] + + params' <- stdParams $ [Param "--batch"] ++ passphrasefd ++ params + closeFd frompipe `after` go params' +#else + -- store the passphrase in a temp file for gpg + withTmpFile "gpg" $ \tmpfile h -> do + hPutStr h passphrase + hClose h + let passphrasefile = [Param "--passphrase-file", File tmpfile] + params' <- stdParams $ [Param "--batch"] ++ passphrasefile ++ params + go params' +#endif + where + go params' = withBothHandles createProcessSuccess (proc gpgcmd params') + $ \(to, from) -> do + void $ forkIO $ do + feeder to + hClose to + reader from + +{- Finds gpg public keys matching some string. (Could be an email address, + - a key id, or a name; See the section 'HOW TO SPECIFY A USER ID' of + - GnuPG's manpage.) -} +findPubKeys :: String -> IO KeyIds +findPubKeys for = KeyIds . parse <$> readStrict params + where + params = [Params "--with-colons --list-public-keys", Param for] + parse = catMaybes . map (keyIdField . split ":") . lines + keyIdField ("pub":_:_:_:f:_) = Just f + keyIdField _ = Nothing + +{- 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 :: Bool -> Int -> IO String +genRandom highQuality size = checksize <$> readStrict + [ Params params + , Param $ show randomquality + , Param $ show size + ] + where + params = "--gen-random --armor" + + -- See http://www.gnupg.org/documentation/manuals/gcrypt/Quality-of-random-numbers.html + -- for the meaning of random quality levels. + -- The highest available is 2, which is the default for OpenPGP + -- key generation; Note that it uses the blocking PRNG /dev/random + -- on the Linux kernel, hence the running time may take a while. + randomquality :: Int + randomquality = if highQuality then 2 else 1 + + {- 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 + - do. + - + - This key was generated with no exipiration date, and a small keysize. + - It has an empty passphrase. -} +testKeyId :: String +testKeyId = "129D6E0AC537B9C7" +testKey :: String +testKey = keyBlock True + [ "mI0ETvFAZgEEAKnqwWgZqznMhi1RQExem2H8t3OyKDxaNN3rBN8T6LWGGqAYV4wT" + , "r8In5tfsnz64bKpE1Qi68JURFwYmthgUL9N48tbODU8t3xzijdjLOSaTyqkH1ik6" + , "EyulfKN63xLne9i4F9XqNwpiZzukXYbNfHkDA2yb0M6g4UFKLY/fNzGXABEBAAG0" + , "W2luc2VjdXJlIHRlc3Qga2V5ICh0aGlzIGlzIGEgdGVzdCBrZXksIGRvIG5vdCB1" + , "c2UgZm9yIGFjdHVhbCBlbmNyeXB0aW9uKSA8dGVzdEBleGFtcGxlLmNvbT6IuAQT" + , "AQgAIgUCTvFAZgIbAwYLCQgHAwIGFQgCCQoLBBYCAwECHgECF4AACgkQEp1uCsU3" + , "uceQ9wP/YMd1f0+/eLLcwGXNBvGqyVhUOfAKknO1bMzGbqTsq9g60qegy/cldqee" + , "xVxNfy0VN//JeMfgdcb8+RgJYLoaMrTy9CcsUcFPxtwN9tcLmsM0V2/fNmmFBO9t" + , "v75iH+zeFbNg0/FbPkHiN6Mjw7P2gXYKQXgTvQZBWaphk8oQlBm4jQRO8UBmAQQA" + , "vdi50M/WRCkOLt2RsUve8V8brMWYTJBJTTWoHUeRr82v4NCdX7OE1BsoVK8cy/1Q" + , "Y+gLOH9PqinuGGNWRmPV2Ju/RYn5H7sdewXA8E80xWhc4phHRMJ8Jjhg/GVPamkJ" + , "8B5zeKF0jcLFl7cuVdOyQakhoeDWJd0CyfW837nmPtMAEQEAAYifBBgBCAAJBQJO" + , "8UBmAhsMAAoJEBKdbgrFN7nHclAEAKBShuP/toH03atDUQTbGE34CA4yEC9BVghi" + , "7kviOZlOz2s8xAfp/8AYsrECx1kgbXcA7JD902eNyp7NzXsdJX0zJwHqiuZW0XlD" + , "T8ZJu4qrYRYgl/790WPESZ+ValvHD/fqkR38RF4tfxvyoMhhp0roGmJY33GASIG/" + , "+gQkDF9/" + , "=1k11" + ] +testSecretKey :: String +testSecretKey = keyBlock False + [ "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" + , "=LDsg" + ] +keyBlock :: Bool -> [String] -> String +keyBlock public ls = unlines + [ "-----BEGIN PGP "++t++" KEY BLOCK-----" + , "Version: GnuPG v1.4.11 (GNU/Linux)" + , "" + , unlines ls + , "-----END PGP "++t++" KEY BLOCK-----" + ] + where + t + | public = "PUBLIC" + | otherwise = "PRIVATE" + +#ifndef mingw32_HOST_OS +{- Runs an action using gpg in a test harness, in which gpg does + - not use ~/.gpg/, but a directory with the test key set up to be used. -} +testHarness :: IO a -> IO a +testHarness a = do + orig <- getEnv var + bracket setup (cleanup orig) (const a) + where + var = "GNUPGHOME" + + setup = do + base <- getTemporaryDirectory + dir <- mktmpdir $ base "gpgtmpXXXXXX" + void $ setEnv var dir True + _ <- pipeStrict [Params "--import -q"] $ unlines + [testSecretKey, testKey] + return dir + + cleanup orig tmpdir = removeDirectoryRecursive tmpdir >> reset orig + reset (Just v) = setEnv var v True + reset _ = unsetEnv var + +{- Tests the test harness. -} +testTestHarness :: IO Bool +testTestHarness = do + keys <- testHarness $ findPubKeys testKeyId + return $ KeyIds [testKeyId] == keys +#endif diff --git a/Utility/Gpg/Types.hs b/Utility/Gpg/Types.hs new file mode 100644 index 0000000000..d457072074 --- /dev/null +++ b/Utility/Gpg/Types.hs @@ -0,0 +1,30 @@ +{- gpg data types + - + - Copyright 2013 guilhem + - + - 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/HumanNumber.hs b/Utility/HumanNumber.hs new file mode 100644 index 0000000000..904135987e --- /dev/null +++ b/Utility/HumanNumber.hs @@ -0,0 +1,21 @@ +{- numbers for humans + - + - Copyright 2012-2013 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Utility.HumanNumber where + +{- Displays a fractional value as a string with a limited number + - of decimal digits. -} +showImprecise :: RealFrac a => Int -> a -> String +showImprecise precision n + | precision == 0 || remainder == 0 = show (round n :: Integer) + | otherwise = show int ++ "." ++ striptrailing0s (pad0s $ show remainder) + where + int :: Integer + (int, frac) = properFraction n + remainder = round (frac * 10 ^ precision) :: Integer + pad0s s = (take (precision - length s) (repeat '0')) ++ s + striptrailing0s = reverse . dropWhile (== '0') . reverse diff --git a/Utility/HumanTime.hs b/Utility/HumanTime.hs new file mode 100644 index 0000000000..038d1228eb --- /dev/null +++ b/Utility/HumanTime.hs @@ -0,0 +1,26 @@ +{- Time for humans. + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Utility.HumanTime where + +import Utility.PartialPrelude + +import Data.Time.Clock.POSIX (POSIXTime) + +{- Parses a human-input time duration, of the form "5h" or "1m". -} +parseDuration :: String -> Maybe POSIXTime +parseDuration s = do + num <- readish s :: Maybe Integer + units <- findUnits =<< lastMaybe s + return $ fromIntegral num * units + where + findUnits 's' = Just 1 + findUnits 'm' = Just 60 + findUnits 'h' = Just $ 60 * 60 + findUnits 'd' = Just $ 60 * 60 * 24 + findUnits 'y' = Just $ 60 * 60 * 24 * 365 + findUnits _ = Nothing diff --git a/Utility/INotify.hs b/Utility/INotify.hs new file mode 100644 index 0000000000..e9071d906f --- /dev/null +++ b/Utility/INotify.hs @@ -0,0 +1,182 @@ +{- higher-level inotify interface + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Utility.INotify where + +import Common hiding (isDirectory) +import Utility.ThreadLock +import Utility.DirWatcher.Types + +import System.INotify +import qualified System.Posix.Files as Files +import System.IO.Error +import Control.Exception (throw) + +{- Watches for changes to files in a directory, and all its subdirectories + - that are not ignored, using inotify. This function returns after + - its initial scan is complete, leaving a thread running. Callbacks are + - made for different events. + - + - Inotify is weak at recursive directory watching; the whole directory + - tree must be scanned and watches set explicitly for each subdirectory. + - + - To notice newly created subdirectories, inotify is used, and + - watches are registered for those directories. There is a race there; + - things can be added to a directory before the watch gets registered. + - + - To close the inotify race, each time a new directory is found, it also + - recursively scans it, assuming all files in it were just added, + - and registering each subdirectory. + - + - Note: Due to the race amelioration, multiple add events may occur + - for the same file. + - + - Note: Moving a file will cause events deleting it from its old location + - and adding it to the new location. + - + - Note: It's assumed that when a file that was open for write is closed, + - it's finished being written to, and can be added. + - + - Note: inotify has a limit to the number of watches allowed, + - /proc/sys/fs/inotify/max_user_watches (default 8192). + - So this will fail if there are too many subdirectories. The + - errHook is called when this happens. + -} +watchDir :: INotify -> FilePath -> (FilePath -> Bool) -> WatchHooks -> IO () +watchDir i dir ignored hooks + | ignored dir = noop + | otherwise = do + -- Use a lock to make sure events generated during initial + -- scan come before real inotify events. + lock <- newLock + let handler event = withLock lock (void $ go event) + void (addWatch i watchevents dir handler) + `catchIO` failedaddwatch + withLock lock $ + mapM_ scan =<< filter (not . dirCruft) <$> + getDirectoryContents dir + where + recurse d = watchDir i d ignored hooks + + -- Select only inotify events required by the enabled + -- hooks, but always include Create so new directories can + -- be scanned. + watchevents = Create : addevents ++ delevents ++ modifyevents + addevents + | hashook addHook || hashook addSymlinkHook = [MoveIn, CloseWrite] + | otherwise = [] + delevents + | hashook delHook || hashook delDirHook = [MoveOut, Delete] + | otherwise = [] + modifyevents + | hashook modifyHook = [Modify] + | otherwise = [] + + scan f = unless (ignored f) $ do + ms <- getstatus f + case ms of + Nothing -> return () + Just s + | Files.isDirectory s -> + recurse $ indir f + | Files.isSymbolicLink s -> + runhook addSymlinkHook f ms + | Files.isRegularFile s -> + runhook addHook f ms + | otherwise -> + noop + + go (Created { isDirectory = isd, filePath = f }) + | isd = recurse $ indir f + | otherwise = do + ms <- getstatus f + case ms of + Just s + | Files.isSymbolicLink s -> + when (hashook addSymlinkHook) $ + runhook addSymlinkHook f ms + | Files.isRegularFile s -> + when (hashook addHook) $ + runhook addHook f ms + _ -> noop + -- Closing a file is assumed to mean it's done being written, + -- so a new add event is sent. + go (Closed { isDirectory = False, maybeFilePath = Just f }) = + checkfiletype Files.isRegularFile addHook f + -- When a file or directory is moved in, scan it to add new + -- stuff. + go (MovedIn { filePath = f }) = scan f + go (MovedOut { isDirectory = isd, filePath = f }) + | isd = runhook delDirHook f Nothing + | otherwise = runhook delHook f Nothing + -- Verify that the deleted item really doesn't exist, + -- since there can be spurious deletion events for items + -- in a directory that has been moved out, but is still + -- being watched. + go (Deleted { isDirectory = isd, filePath = f }) + | isd = guarded $ runhook delDirHook f Nothing + | otherwise = guarded $ runhook delHook f Nothing + where + guarded = unlessM (filetype (const True) f) + go (Modified { isDirectory = isd, maybeFilePath = Just f }) + | isd = noop + | otherwise = runhook modifyHook f Nothing + go _ = noop + + hashook h = isJust $ h hooks + + runhook h f s + | ignored f = noop + | otherwise = maybe noop (\a -> a (indir f) s) (h hooks) + + indir f = dir f + + getstatus f = catchMaybeIO $ getSymbolicLinkStatus $ indir f + checkfiletype check h f = do + ms <- getstatus f + case ms of + Just s + | check s -> runhook h f ms + _ -> noop + filetype t f = catchBoolIO $ t <$> getSymbolicLinkStatus (indir f) + + failedaddwatch e + -- Inotify fails when there are too many watches with a + -- disk full error. + | isFullError e = + case errHook hooks of + Nothing -> throw e + Just hook -> tooManyWatches hook dir + -- The directory could have been deleted. + | isDoesNotExistError e = return () + | otherwise = throw e + +tooManyWatches :: (String -> Maybe FileStatus -> IO ()) -> FilePath -> IO () +tooManyWatches hook dir = do + sysctlval <- querySysctl [Param maxwatches] :: IO (Maybe Integer) + hook (unlines $ basewarning : maybe withoutsysctl withsysctl sysctlval) Nothing + where + maxwatches = "fs.inotify.max_user_watches" + basewarning = "Too many directories to watch! (Not watching " ++ dir ++")" + withoutsysctl = ["Increase the value in /proc/sys/fs/inotify/max_user_watches"] + withsysctl n = let new = n * 10 in + [ "Increase the limit permanently by running:" + , " echo " ++ maxwatches ++ "=" ++ show new ++ + " | sudo tee -a /etc/sysctl.conf; sudo sysctl -p" + , "Or temporarily by running:" + , " sudo sysctl -w " ++ maxwatches ++ "=" ++ show new + ] + +querySysctl :: Read a => [CommandParam] -> IO (Maybe a) +querySysctl ps = getM go ["sysctl", "/sbin/sysctl", "/usr/sbin/sysctl"] + where + go p = do + v <- catchMaybeIO $ readProcess p (toCommand ps) + case v of + Nothing -> return Nothing + Just s -> return $ parsesysctl s + parsesysctl s = readish =<< lastMaybe (words s) diff --git a/Utility/InodeCache.hs b/Utility/InodeCache.hs new file mode 100644 index 0000000000..8037c61c86 --- /dev/null +++ b/Utility/InodeCache.hs @@ -0,0 +1,91 @@ +{- Caching a file's inode, size, and modification time to see when it's changed. + - + - Copyright 2013 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Utility.InodeCache where + +import Common +import System.PosixCompat.Types +import Utility.QuickCheck + +data InodeCachePrim = InodeCachePrim FileID FileOffset EpochTime + deriving (Show, Eq, Ord) + +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 (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 (InodeCachePrim inode size mtime)) = unwords + [ show inode + , show size + , show mtime + ] + +readInodeCache :: String -> Maybe InodeCache +readInodeCache s = case words s of + (inode:size:mtime:_) -> + let prim = InodeCachePrim + <$> readish inode + <*> readish size + <*> readish mtime + in InodeCache <$> prim + _ -> Nothing + +genInodeCache :: FilePath -> IO (Maybe InodeCache) +genInodeCache f = catchDefaultIO Nothing $ toInodeCache <$> getFileStatus f + +toInodeCache :: FileStatus -> Maybe InodeCache +toInodeCache s + | 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/JSONStream.hs b/Utility/JSONStream.hs new file mode 100644 index 0000000000..f3e93c3dac --- /dev/null +++ b/Utility/JSONStream.hs @@ -0,0 +1,44 @@ +{- Streaming JSON output. + - + - Copyright 2011 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Utility.JSONStream ( + start, + add, + end +) where + +import Text.JSON + +{- Text.JSON does not support building up a larger JSON document piece by + - piece as a stream. To support streaming, a hack. The JSObject is converted + - to a string with its final "}" is left off, allowing it to be added to + - later. -} +start :: JSON a => [(String, a)] -> String +start l + | last s == endchar = init s + | otherwise = bad s + where + s = encodeStrict $ toJSObject l + +add :: JSON a => [(String, a)] -> String +add l + | head s == startchar = ',' : drop 1 s + | otherwise = bad s + where + s = start l + +end :: String +end = [endchar, '\n'] + +startchar :: Char +startchar = '{' + +endchar :: Char +endchar = '}' + +bad :: String -> a +bad s = error $ "Text.JSON returned unexpected string: " ++ s diff --git a/Utility/Kqueue.hs b/Utility/Kqueue.hs new file mode 100644 index 0000000000..eb5feab007 --- /dev/null +++ b/Utility/Kqueue.hs @@ -0,0 +1,267 @@ +{- BSD kqueue file modification notification interface + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE ForeignFunctionInterface #-} + +module Utility.Kqueue ( + Kqueue, + initKqueue, + stopKqueue, + waitChange, + Change(..), + changedFile, + runHooks, +) where + +import Common +import Utility.DirWatcher.Types + +import System.Posix.Types +import Foreign.C.Types +import Foreign.C.Error +import Foreign.Ptr +import Foreign.Marshal +import qualified Data.Map as M +import qualified Data.Set as S +import qualified System.Posix.Files as Files +import Control.Concurrent + +data Change + = Deleted FilePath + | DeletedDir FilePath + | Added FilePath + deriving (Show) + +isAdd :: Change -> Bool +isAdd (Added _) = True +isAdd (Deleted _) = False +isAdd (DeletedDir _) = False + +changedFile :: Change -> FilePath +changedFile (Added f) = f +changedFile (Deleted f) = f +changedFile (DeletedDir f) = f + +data Kqueue = Kqueue + { kqueueFd :: Fd + , kqueueTop :: FilePath + , kqueueMap :: DirMap + , _kqueuePruner :: Pruner + } + +type Pruner = FilePath -> Bool + +type DirMap = M.Map Fd DirInfo + +{- Enough information to uniquely identify a file in a directory, + - but not too much. -} +data DirEnt = DirEnt + { dirEnt :: FilePath -- relative to the parent directory + , _dirInode :: FileID -- included to notice file replacements + , isSubDir :: Bool + } + deriving (Eq, Ord, Show) + +{- A directory, and its last known contents. -} +data DirInfo = DirInfo + { dirName :: FilePath + , dirCache :: S.Set DirEnt + } + deriving (Show) + +getDirInfo :: FilePath -> IO DirInfo +getDirInfo dir = do + l <- filter (not . dirCruft) <$> getDirectoryContents dir + contents <- S.fromList . catMaybes <$> mapM getDirEnt l + return $ DirInfo dir contents + where + getDirEnt f = catchMaybeIO $ do + s <- getSymbolicLinkStatus (dir f) + return $ DirEnt f (fileID s) (isDirectory s) + +{- Difference between the dirCaches of two DirInfos. -} +(//) :: DirInfo -> DirInfo -> [Change] +oldc // newc = deleted ++ added + where + deleted = calc gendel oldc newc + added = calc genadd newc oldc + gendel x = (if isSubDir x then DeletedDir else Deleted) $ + dirName oldc dirEnt x + genadd x = Added $ dirName newc dirEnt x + calc a x y = map a $ S.toList $ + S.difference (dirCache x) (dirCache y) + +{- Builds a map of directories in a tree, possibly pruning some. + - Opens each directory in the tree, and records its current contents. -} +scanRecursive :: FilePath -> Pruner -> IO DirMap +scanRecursive topdir prune = M.fromList <$> walk [] [topdir] + where + walk c [] = return c + walk c (dir:rest) + | prune dir = walk c rest + | otherwise = do + minfo <- catchMaybeIO $ getDirInfo dir + case minfo of + Nothing -> walk c rest + Just info -> do + mfd <- catchMaybeIO $ + openFd dir ReadOnly Nothing defaultFileFlags + case mfd of + Nothing -> walk c rest + Just fd -> do + let subdirs = map (dir ) . map dirEnt $ + S.toList $ dirCache info + walk ((fd, info):c) (subdirs ++ rest) + +{- Adds a list of subdirectories (and all their children), unless pruned to a + - directory map. Adding a subdirectory that's already in the map will + - cause its contents to be refreshed. -} +addSubDirs :: DirMap -> Pruner -> [FilePath] -> IO DirMap +addSubDirs dirmap prune dirs = do + newmap <- foldr M.union M.empty <$> + mapM (\d -> scanRecursive d prune) dirs + return $ M.union newmap dirmap -- prefer newmap + +{- Removes a subdirectory (and all its children) from a directory map. -} +removeSubDir :: DirMap -> FilePath -> IO DirMap +removeSubDir dirmap dir = do + mapM_ closeFd $ M.keys toremove + return rest + where + (toremove, rest) = M.partition (dirContains dir . dirName) dirmap + +findDirContents :: DirMap -> FilePath -> [FilePath] +findDirContents dirmap dir = concatMap absolutecontents $ search + where + absolutecontents i = map (dirName i ) + (map dirEnt $ S.toList $ dirCache i) + search = map snd $ M.toList $ + M.filter (\i -> dirName i == dir) dirmap + +foreign import ccall safe "libkqueue.h init_kqueue" c_init_kqueue + :: IO Fd +foreign import ccall safe "libkqueue.h addfds_kqueue" c_addfds_kqueue + :: Fd -> CInt -> Ptr Fd -> IO () +foreign import ccall safe "libkqueue.h waitchange_kqueue" c_waitchange_kqueue + :: Fd -> IO Fd + +{- Initializes a Kqueue to watch a directory, and all its subdirectories. -} +initKqueue :: FilePath -> Pruner -> IO Kqueue +initKqueue dir pruned = do + dirmap <- scanRecursive dir pruned + h <- c_init_kqueue + let kq = Kqueue h dir dirmap pruned + updateKqueue kq + return kq + +{- Updates a Kqueue, adding watches for its map. -} +updateKqueue :: Kqueue -> IO () +updateKqueue (Kqueue h _ dirmap _) = + withArrayLen (M.keys dirmap) $ \fdcnt c_fds -> do + c_addfds_kqueue h (fromIntegral fdcnt) c_fds + +{- Stops a Kqueue. Note: Does not directly close the Fds in the dirmap, + - so it can be reused. -} +stopKqueue :: Kqueue -> IO () +stopKqueue = closeFd . kqueueFd + +{- Waits for a change on a Kqueue. + - May update the Kqueue. + -} +waitChange :: Kqueue -> IO (Kqueue, [Change]) +waitChange kq@(Kqueue h _ dirmap _) = do + changedfd <- c_waitchange_kqueue h + if changedfd == -1 + then ifM ((==) eINTR <$> getErrno) + (yield >> waitChange kq, nochange) + else case M.lookup changedfd dirmap of + Nothing -> nochange + Just info -> handleChange kq changedfd info + where + nochange = return (kq, []) + +{- The kqueue interface does not tell what type of change took place in + - the directory; it could be an added file, a deleted file, a renamed + - file, a new subdirectory, or a deleted subdirectory, or a moved + - subdirectory. + - + - So to determine this, the contents of the directory are compared + - with its last cached contents. The Kqueue is updated to watch new + - directories as necessary. + -} +handleChange :: Kqueue -> Fd -> DirInfo -> IO (Kqueue, [Change]) +handleChange kq@(Kqueue _ _ dirmap pruner) fd olddirinfo = + go =<< catchMaybeIO (getDirInfo $ dirName olddirinfo) + where + go (Just newdirinfo) = do + let changes = filter (not . pruner . changedFile) $ + olddirinfo // newdirinfo + let (added, deleted) = partition isAdd changes + + -- Scan newly added directories to add to the map. + -- (Newly added files will fail getDirInfo.) + newdirinfos <- catMaybes <$> + mapM (catchMaybeIO . getDirInfo . changedFile) added + newmap <- addSubDirs dirmap pruner $ map dirName newdirinfos + + -- Remove deleted directories from the map. + newmap' <- foldM removeSubDir newmap (map changedFile deleted) + + -- Update the cached dirinfo just looked up. + let newmap'' = M.insertWith' const fd newdirinfo newmap' + + -- When new directories were added, need to update + -- the kqueue to watch them. + let kq' = kq { kqueueMap = newmap'' } + unless (null newdirinfos) $ + updateKqueue kq' + + return (kq', changes) + go Nothing = do + -- The directory has been moved or deleted, so + -- remove it from our map. + newmap <- removeSubDir dirmap (dirName olddirinfo) + return (kq { kqueueMap = newmap }, []) + +{- Processes changes on the Kqueue, calling the hooks as appropriate. + - Never returns. -} +runHooks :: Kqueue -> WatchHooks -> IO () +runHooks kq hooks = do + -- First, synthetic add events for the whole directory tree contents, + -- to catch any files created beforehand. + recursiveadd (kqueueMap kq) (Added $ kqueueTop kq) + loop kq + where + loop q = do + (q', changes) <- waitChange q + forM_ changes $ dispatch (kqueueMap q') + loop q' + + dispatch _ change@(Deleted _) = + callhook delHook Nothing change + dispatch _ change@(DeletedDir _) = + callhook delDirHook Nothing change + dispatch dirmap change@(Added _) = + withstatus change $ dispatchadd dirmap + + dispatchadd dirmap change s + | Files.isSymbolicLink s = callhook addSymlinkHook (Just s) change + | Files.isDirectory s = recursiveadd dirmap change + | Files.isRegularFile s = callhook addHook (Just s) change + | otherwise = noop + + recursiveadd dirmap change = do + let contents = findDirContents dirmap $ changedFile change + forM_ contents $ \f -> + withstatus (Added f) $ dispatchadd dirmap + + callhook h s change = case h hooks of + Nothing -> noop + Just a -> a (changedFile change) s + + withstatus change a = maybe noop (a change) =<< + (catchMaybeIO (getSymbolicLinkStatus (changedFile change))) diff --git a/Utility/LogFile.hs b/Utility/LogFile.hs new file mode 100644 index 0000000000..090ac60d0e --- /dev/null +++ b/Utility/LogFile.hs @@ -0,0 +1,68 @@ +{- log files + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE CPP #-} + +module Utility.LogFile where + +import Common + +import System.Posix.Types + +openLog :: FilePath -> IO Fd +#ifndef mingw32_HOST_OS +openLog logfile = do + rotateLog logfile + openFd logfile WriteOnly (Just stdFileMode) + defaultFileFlags { append = True } +#else +openLog = error "openLog TODO" +#endif + +rotateLog :: FilePath -> IO () +rotateLog logfile = go 0 + where + 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 + +{- Lists most recent logs last. -} +listLogs :: FilePath -> IO [FilePath] +listLogs logfile = filterM doesFileExist $ reverse $ + logfile : map (rotatedLog logfile) [1..maxLogs] + +maxLogs :: Int +maxLogs = 9 + +redirLog :: Fd -> IO () +#ifndef mingw32_HOST_OS +redirLog logfd = do + mapM_ (redir logfd) [stdOutput, stdError] + closeFd logfd +#else +redirLog _ = error "redirLog TODO" +#endif + +redir :: Fd -> Fd -> IO () +#ifndef mingw32_HOST_OS +redir newh h = do + closeFd h + void $ dupTo newh h +#else +redir _ _ = error "redir TODO" +#endif diff --git a/Utility/Lsof.hs b/Utility/Lsof.hs new file mode 100644 index 0000000000..6d6b353f26 --- /dev/null +++ b/Utility/Lsof.hs @@ -0,0 +1,120 @@ +{- lsof interface + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE BangPatterns, CPP #-} + +module Utility.Lsof where + +import Common +import Build.SysConfig as SysConfig +import Utility.Env + +import System.Posix.Types + +data LsofOpenMode = OpenReadWrite | OpenReadOnly | OpenWriteOnly | OpenUnknown + deriving (Show, Eq) + +type CmdLine = String + +data ProcessInfo = ProcessInfo ProcessID CmdLine + deriving (Show) + +{- lsof is not in PATH on all systems, so SysConfig may have the absolute + - path where the program was found. Make sure at runtime that lsof is + - available, and if it's not in PATH, adjust PATH to contain it. -} +setupLsof :: IO () +setupLsof = do + let cmd = fromMaybe "lsof" SysConfig.lsof + when (isAbsolute cmd) $ do + path <- getSearchPath + let path' = takeDirectory cmd : path + void $ setEnv "PATH" (intercalate [searchPathSeparator] path') True + +{- Checks each of the files in a directory to find open files. + - Note that this will find hard links to files elsewhere that are open. -} +queryDir :: FilePath -> IO [(FilePath, LsofOpenMode, ProcessInfo)] +queryDir path = query ["+d", path] + +{- Runs lsof with some parameters. + - + - Ignores nonzero exit code; lsof returns that when no files are open. + - + - Note: If lsof is not available, this always returns [] ! + -} +query :: [String] -> IO [(FilePath, LsofOpenMode, ProcessInfo)] +query opts = + withHandle StdoutHandle (createProcessChecked checkSuccessProcess) p $ \h -> do + fileEncoding h + parse <$> hGetContentsStrict h + where + p = proc "lsof" ("-F0can" : opts) + +type LsofParser = String -> [(FilePath, LsofOpenMode, ProcessInfo)] + +parse :: LsofParser +#ifdef __ANDROID__ +parse = parseDefault +#else +parse = parseFormatted +#endif + +{- Parsing null-delimited output like: + - + - pPID\0cCMDLINE\0 + - aMODE\0nFILE\0 + - aMODE\0nFILE\0 + - pPID\0[...] + - + - Where each new process block is started by a pid, and a process can + - have multiple files open. + -} +parseFormatted :: LsofParser +parseFormatted s = bundle $ go [] $ lines s + where + bundle = concatMap (\(fs, p) -> map (\(f, m) -> (f, m, p)) fs) + + go c [] = c + go c ((t:r):ls) + | t == 'p' = + let (fs, ls') = parsefiles [] ls + in go ((fs, parseprocess r):c) ls' + | otherwise = parsefail + go _ _ = parsefail + + parseprocess l = case splitnull l of + [pid, 'c':cmdline, ""] -> + case readish pid of + (Just n) -> ProcessInfo n cmdline + Nothing -> parsefail + _ -> parsefail + + parsefiles c [] = (c, []) + parsefiles c (l:ls) = case splitnull l of + ['a':mode, 'n':file, ""] -> + parsefiles ((file, parsemode mode):c) ls + (('p':_):_) -> (c, l:ls) + _ -> parsefail + + parsemode ('r':_) = OpenReadOnly + parsemode ('w':_) = OpenWriteOnly + parsemode ('u':_) = OpenReadWrite + parsemode _ = OpenUnknown + + splitnull = split "\0" + + parsefail = error $ "failed to parse lsof output: " ++ show s + +{- Parses lsof's default output format. -} +parseDefault :: LsofParser +parseDefault = catMaybes . map parseline . drop 1 . lines + where + parseline l = case words l of + (command : spid : _user : _fd : _type : _device : _size : _node : rest) -> + case readish spid of + Nothing -> Nothing + Just pid -> Just (unwords rest, OpenUnknown, ProcessInfo pid command) + _ -> Nothing diff --git a/Utility/Matcher.hs b/Utility/Matcher.hs new file mode 100644 index 0000000000..e0a51ff6ab --- /dev/null +++ b/Utility/Matcher.hs @@ -0,0 +1,169 @@ +{- A generic matcher. + - + - Can be used to check if a user-supplied condition, + - like "foo and ( bar or not baz )" matches. The condition must already + - be tokenized, and can contain arbitrary operations. + - + - If operations are not separated by and/or, they are defaulted to being + - anded together, so "foo bar baz" all must match. + - + - Is forgiving about misplaced closing parens, so "foo and (bar or baz" + - will be handled, as will "foo and ( bar or baz ) )" + - + - Copyright 2011-2013 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE Rank2Types, KindSignatures #-} + +module Utility.Matcher ( + Token(..), + Matcher, + token, + tokens, + generate, + match, + matchM, + matchMrun, + isEmpty, + + prop_matcher_sane +) where + +import Common + +{- A Token can be an Operation of an arbitrary type, or one of a few + - predefined peices of syntax. -} +data Token op = Operation op | And | Or | Not | Open | Close + deriving (Show, Eq) + +data Matcher op = MAny + | MAnd (Matcher op) (Matcher op) + | MOr (Matcher op) (Matcher op) + | MNot (Matcher op) + | MOp op + deriving (Show, Eq) + +{- Converts a word of syntax into a token. Doesn't handle operations. -} +token :: String -> Token op +token "and" = And +token "or" = Or +token "not" = Not +token "(" = Open +token ")" = Close +token t = error $ "unknown token " ++ t + +tokens :: [String] +tokens = words "and or not ( )" + +{- Converts a list of Tokens into a Matcher. -} +generate :: [Token op] -> Matcher op +generate = simplify . process MAny . tokenGroups + where + process m [] = m + process m ts = uncurry process $ consume m ts + + consume m ((One And):rest) = term (m `MAnd`) rest + consume m ((One Or):rest) = term (m `MOr`) rest + consume m ((One Not):rest) = term (\p -> m `MAnd` (MNot p)) rest + consume m ((One (Operation o)):rest) = (m `MAnd` MOp o, rest) + consume m (Group g:rest) = (process m g, rest) + consume m (_:rest) = consume m rest + consume m [] = (m, []) + + term a l = + let (p, l') = consume MAny l + in (a p, l') + + simplify (MAnd MAny x) = simplify x + simplify (MAnd x MAny) = simplify x + simplify (MAnd x y) = MAnd (simplify x) (simplify y) + simplify (MOr x y) = MOr (simplify x) (simplify y) + simplify (MNot x) = MNot (simplify x) + simplify x = x + +data TokenGroup op = One (Token op) | Group [TokenGroup op] + deriving (Show, Eq) + +tokenGroups :: [Token op] -> [TokenGroup op] +tokenGroups [] = [] +tokenGroups (t:ts) = go t + where + go Open = + let (gr, rest) = findClose ts + in gr : tokenGroups rest + go Close = tokenGroups ts -- not picky about missing Close + go _ = One t : tokenGroups ts + +findClose :: [Token op] -> (TokenGroup op, [Token op]) +findClose l = + let (g, rest) = go [] l + in (Group (reverse g), rest) + where + go c [] = (c, []) -- not picky about extra Close + go c (t:ts) = handle t + where + handle Close = (c, ts) + handle Open = + let (c', ts') = go [] ts + in go (Group (reverse c') : c) ts' + handle _ = go (One t:c) ts + +{- Checks if a Matcher matches, using a supplied function to check + - the value of Operations. -} +match :: (op -> v -> Bool) -> Matcher op -> v -> Bool +match a m v = go m + where + go MAny = True + go (MAnd m1 m2) = go m1 && go m2 + go (MOr m1 m2) = go m1 || go m2 + go (MNot m1) = not $ go m1 + go (MOp o) = a o v + +{- Runs a monadic Matcher, where Operations are actions in the monad. -} +matchM :: Monad m => Matcher (v -> m Bool) -> v -> m Bool +matchM m v = matchMrun m $ \o -> o v + +{- More generic running of a monadic Matcher, with full control over running + - of Operations. Mostly useful in order to match on more than one + - parameter. -} +matchMrun :: forall o (m :: * -> *). Monad m => Matcher o -> (o -> m Bool) -> m Bool +matchMrun m run = go m + where + go MAny = return True + go (MAnd m1 m2) = go m1 <&&> go m2 + go (MOr m1 m2) = go m1 <||> go m2 + go (MNot m1) = liftM not (go m1) + go (MOp o) = run o + +{- Checks if a matcher contains no limits. -} +isEmpty :: Matcher a -> Bool +isEmpty MAny = True +isEmpty _ = False + +prop_matcher_sane :: Bool +prop_matcher_sane = all (\m -> match dummy m ()) $ map generate + [ [Operation True] + , [] + , [Operation False, Or, Operation True, Or, Operation False] + , [Operation True, Or, Operation True] + , [Operation True, And, Operation True] + , [Not, Open, Operation True, And, Operation False, Close] + , [Not, Open, Not, Open, Not, Operation False, Close, Close] + , [Not, Open, Not, Open, Not, Open, Not, Operation True, Close, Close] + , [Operation True, And, Not, Operation False] + , [Operation True, Not, Operation False] + , [Operation True, Not, Not, Not, Operation False] + , [Operation True, Not, Not, Not, Operation False, And, Operation True] + , [Operation True, Not, Not, Not, Operation False, Operation True] + , [Not, Open, Operation True, And, Operation False, Close, + And, Open, + Open, Operation True, And, Operation False, Close, + Or, + Open, Operation True, And, Open, Not, Operation False, Close, Close, + Close, And, + Open, Not, Operation False, Close] + ] + where + dummy b _ = b diff --git a/Utility/Metered.hs b/Utility/Metered.hs new file mode 100644 index 0000000000..f33ad443ac --- /dev/null +++ b/Utility/Metered.hs @@ -0,0 +1,116 @@ +{- Metered IO + - + - Copyright 2012, 2013 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE TypeSynonymInstances #-} + +module Utility.Metered where + +import Common + +import qualified Data.ByteString.Lazy as L +import qualified Data.ByteString as S +import System.IO.Unsafe +import Foreign.Storable (Storable(sizeOf)) +import System.Posix.Types + +{- An action that can be run repeatedly, updating it on the bytes processed. + - + - Note that each call receives the total number of bytes processed, so + - far, *not* an incremental amount since the last call. -} +type MeterUpdate = (BytesProcessed -> IO ()) + +{- Total number of bytes processed so far. -} +newtype BytesProcessed = BytesProcessed Integer + deriving (Eq, Ord) + +class AsBytesProcessed a where + toBytesProcessed :: a -> BytesProcessed + fromBytesProcessed :: BytesProcessed -> a + +instance AsBytesProcessed Integer where + toBytesProcessed i = BytesProcessed i + fromBytesProcessed (BytesProcessed i) = i + +instance AsBytesProcessed Int where + toBytesProcessed i = BytesProcessed $ toInteger i + fromBytesProcessed (BytesProcessed i) = fromInteger i + +instance AsBytesProcessed FileOffset where + toBytesProcessed sz = BytesProcessed $ toInteger sz + fromBytesProcessed (BytesProcessed sz) = fromInteger sz + +addBytesProcessed :: AsBytesProcessed v => BytesProcessed -> v -> BytesProcessed +addBytesProcessed (BytesProcessed i) v = + let (BytesProcessed n) = toBytesProcessed v + in BytesProcessed $! i + n + +zeroBytesProcessed :: BytesProcessed +zeroBytesProcessed = BytesProcessed 0 + +{- Sends the content of a file to an action, updating the meter as it's + - consumed. -} +withMeteredFile :: FilePath -> MeterUpdate -> (L.ByteString -> IO a) -> IO a +withMeteredFile f meterupdate a = withBinaryFile f ReadMode $ \h -> + hGetContentsMetered h meterupdate >>= a + +{- Sends the content of a file to a Handle, updating the meter as it's + - written. -} +streamMeteredFile :: FilePath -> MeterUpdate -> Handle -> IO () +streamMeteredFile f meterupdate h = withMeteredFile f meterupdate $ L.hPut h + +{- Writes a ByteString to a Handle, updating a meter as it's written. -} +meteredWrite :: MeterUpdate -> Handle -> L.ByteString -> IO () +meteredWrite meterupdate h = go zeroBytesProcessed . L.toChunks + where + go _ [] = return () + go sofar (c:cs) = do + S.hPut h c + let sofar' = addBytesProcessed sofar $ S.length c + meterupdate sofar' + go sofar' cs + +meteredWriteFile :: MeterUpdate -> FilePath -> L.ByteString -> IO () +meteredWriteFile meterupdate f b = withBinaryFile f WriteMode $ \h -> + meteredWrite meterupdate h b + +{- This is like L.hGetContents, but after each chunk is read, a meter + - is updated based on the size of the chunk. + - + - Note that the meter update is run in unsafeInterleaveIO, which means that + - it can be run at any time. It's even possible for updates to run out + - of order, as different parts of the ByteString are consumed. + - + - All the usual caveats about using unsafeInterleaveIO apply to the + - meter updates, so use caution. + -} +hGetContentsMetered :: Handle -> MeterUpdate -> IO L.ByteString +hGetContentsMetered h meterupdate = lazyRead zeroBytesProcessed + where + lazyRead sofar = unsafeInterleaveIO $ loop sofar + + loop sofar = do + c <- S.hGetSome h defaultChunkSize + if S.null c + then do + hClose h + return $ L.empty + else do + let sofar' = addBytesProcessed sofar $ + S.length c + meterupdate sofar' + {- unsafeInterleaveIO causes this to be + - deferred until the data is read from the + - ByteString. -} + cs <- lazyRead sofar' + return $ L.append (L.fromChunks [c]) cs + +{- Same default chunk size Lazy ByteStrings use. -} +defaultChunkSize :: Int +defaultChunkSize = 32 * k - chunkOverhead + where + k = 1024 + chunkOverhead = 2 * sizeOf (undefined :: Int) -- GHC specific diff --git a/Utility/Misc.hs b/Utility/Misc.hs new file mode 100644 index 0000000000..804a9e4872 --- /dev/null +++ b/Utility/Misc.hs @@ -0,0 +1,138 @@ +{- misc utility functions + - + - Copyright 2010-2011 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE CPP #-} + +module Utility.Misc where + +import System.IO +import Control.Monad +import Foreign +import Data.Char +import Data.List +import Control.Applicative +#ifndef mingw32_HOST_OS +import System.Posix.Process (getAnyProcessStatus) +import Utility.Exception +#endif + +{- A version of hgetContents that is not lazy. Ensures file is + - all read before it gets closed. -} +hGetContentsStrict :: Handle -> IO String +hGetContentsStrict = hGetContents >=> \s -> length s `seq` return s + +{- A version of readFile that is not lazy. -} +readFileStrict :: FilePath -> IO String +readFileStrict = readFile >=> \s -> length s `seq` return s + +{- Like break, but the character matching the condition is not included + - in the second result list. + - + - separate (== ':') "foo:bar" = ("foo", "bar") + - separate (== ':') "foobar" = ("foobar", "") + -} +separate :: (a -> Bool) -> [a] -> ([a], [a]) +separate c l = unbreak $ break c l + where + unbreak r@(a, b) + | null b = r + | otherwise = (a, tail b) + +{- Breaks out the first line. -} +firstLine :: String -> String +firstLine = takeWhile (/= '\n') + +{- Splits a list into segments that are delimited by items matching + - a predicate. (The delimiters are not included in the segments.) + - Segments may be empty. -} +segment :: (a -> Bool) -> [a] -> [[a]] +segment p l = map reverse $ go [] [] l + where + go c r [] = reverse $ c:r + go c r (i:is) + | p i = go [] (c:r) is + | otherwise = go (i:c) r is + +prop_segment_regressionTest :: Bool +prop_segment_regressionTest = all id + -- Even an empty list is a segment. + [ segment (== "--") [] == [[]] + -- There are two segements in this list, even though the first is empty. + , segment (== "--") ["--", "foo", "bar"] == [[],["foo","bar"]] + ] + +{- Includes the delimiters as segments of their own. -} +segmentDelim :: (a -> Bool) -> [a] -> [[a]] +segmentDelim p l = map reverse $ go [] [] l + where + go c r [] = reverse $ c:r + go c r (i:is) + | p i = go [] ([i]:c:r) is + | otherwise = go (i:c) r is + +{- Replaces multiple values in a string. + - + - Takes care to skip over just-replaced values, so that they are not + - mangled. For example, massReplace [("foo", "new foo")] does not + - replace the "new foo" with "new new foo". + -} +massReplace :: [(String, String)] -> String -> String +massReplace vs = go [] vs + where + + go acc _ [] = concat $ reverse acc + go acc [] (c:cs) = go ([c]:acc) vs cs + go acc ((val, replacement):rest) s + | val `isPrefixOf` s = + go (replacement:acc) vs (drop (length val) s) + | otherwise = go acc rest s + +{- Given two orderings, returns the second if the first is EQ and returns + - the first otherwise. + - + - Example use: + - + - compare lname1 lname2 `thenOrd` compare fname1 fname2 + -} +thenOrd :: Ordering -> Ordering -> Ordering +thenOrd EQ x = x +thenOrd x _ = x +{-# INLINE thenOrd #-} + +{- Wrapper around hGetBufSome that returns a String. + - + - The null string is returned on eof, otherwise returns whatever + - data is currently available to read from the handle, or waits for + - data to be written to it if none is currently available. + - + - Note on encodings: The normal encoding of the Handle is ignored; + - each byte is converted to a Char. Not unicode clean! + -} +hGetSomeString :: Handle -> Int -> IO String +hGetSomeString h sz = do + fp <- mallocForeignPtrBytes sz + len <- withForeignPtr fp $ \buf -> hGetBufSome h buf sz + map (chr . fromIntegral) <$> withForeignPtr fp (peekbytes len) + where + peekbytes :: Int -> Ptr Word8 -> IO [Word8] + peekbytes len buf = mapM (peekElemOff buf) [0..pred len] + +{- Reaps any zombie git processes. + - + - Warning: Not thread safe. Anything that was expecting to wait + - on a process and get back an exit status is going to be confused + - if this reap gets there first. -} +reapZombies :: IO () +#ifndef mingw32_HOST_OS +reapZombies = do + -- throws an exception when there are no child processes + catchDefaultIO Nothing (getAnyProcessStatus False True) + >>= maybe (return ()) (const reapZombies) + +#else +reapZombies = return () +#endif diff --git a/Utility/Monad.hs b/Utility/Monad.hs new file mode 100644 index 0000000000..b66419f76a --- /dev/null +++ b/Utility/Monad.hs @@ -0,0 +1,69 @@ +{- monadic stuff + - + - Copyright 2010-2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Utility.Monad where + +import Data.Maybe +import Control.Monad (liftM) + +{- Return the first value from a list, if any, satisfying the given + - predicate -} +firstM :: Monad m => (a -> m Bool) -> [a] -> m (Maybe a) +firstM _ [] = return Nothing +firstM p (x:xs) = ifM (p x) (return $ Just x , firstM p xs) + +{- Runs the action on values from the list until it succeeds, returning + - its result. -} +getM :: Monad m => (a -> m (Maybe b)) -> [a] -> m (Maybe b) +getM _ [] = return Nothing +getM p (x:xs) = maybe (getM p xs) (return . Just) =<< p x + +{- Returns true if any value in the list satisfies the predicate, + - stopping once one is found. -} +anyM :: Monad m => (a -> m Bool) -> [a] -> m Bool +anyM p = liftM isJust . firstM p + +allM :: Monad m => (a -> m Bool) -> [a] -> m Bool +allM _ [] = return True +allM p (x:xs) = p x <&&> allM p xs + +{- Runs an action on values from a list until it succeeds. -} +untilTrue :: Monad m => [a] -> (a -> m Bool) -> m Bool +untilTrue = flip anyM + +{- if with a monadic conditional. -} +ifM :: Monad m => m Bool -> (m a, m a) -> m a +ifM cond (thenclause, elseclause) = do + c <- cond + if c then thenclause else elseclause + +{- short-circuiting monadic || -} +(<||>) :: Monad m => m Bool -> m Bool -> m Bool +ma <||> mb = ifM ma ( return True , mb ) + +{- short-circuiting monadic && -} +(<&&>) :: Monad m => m Bool -> m Bool -> m Bool +ma <&&> mb = ifM ma ( mb , return False ) + +{- Same fixity as && and || -} +infixr 3 <&&> +infixr 2 <||> + +{- Runs an action, passing its value to an observer before returning it. -} +observe :: Monad m => (a -> m b) -> m a -> m a +observe observer a = do + r <- a + _ <- observer r + return r + +{- b `after` a runs first a, then b, and returns the value of a -} +after :: Monad m => m b -> m a -> m a +after = observe . const + +{- do nothing -} +noop :: Monad m => m () +noop = return () diff --git a/Utility/Mounts.hsc b/Utility/Mounts.hsc new file mode 100644 index 0000000000..b6defda43d --- /dev/null +++ b/Utility/Mounts.hsc @@ -0,0 +1,93 @@ +{- Interface to mtab (and fstab) + - + - Derived from hsshellscript, originally written by + - Volker Wysk + - + - Modified to support BSD, Mac OS X, and Android by + - Joey Hess + - + - Licensed under the GNU LGPL version 2.1 or higher. + -} + +{-# LANGUAGE ForeignFunctionInterface #-} + +module Utility.Mounts ( + Mntent(..), + getMounts +) where + +#ifndef __ANDROID__ +import Control.Monad +import Foreign +import Foreign.C +#include "libmounts.h" +#else +import Utility.Exception +import Data.Maybe +import Control.Applicative +#endif + +{- This is a stripped down mntent, containing only + - fields available everywhere. -} +data Mntent = Mntent + { mnt_fsname :: String + , mnt_dir :: FilePath + , mnt_type :: String + } deriving (Read, Show, Eq, Ord) + +#ifndef __ANDROID__ + +getMounts :: IO [Mntent] +getMounts = do + h <- c_mounts_start + when (h == nullPtr) $ + throwErrno "getMounts" + mntent <- getmntent h [] + _ <- c_mounts_end h + return mntent + + where + getmntent h c = do + ptr <- c_mounts_next h + if (ptr == nullPtr) + then return $ reverse c + else do + mnt_fsname_str <- #{peek struct mntent, mnt_fsname} ptr >>= peekCString + mnt_dir_str <- #{peek struct mntent, mnt_dir} ptr >>= peekCString + mnt_type_str <- #{peek struct mntent, mnt_type} ptr >>= peekCString + let ent = Mntent + { mnt_fsname = mnt_fsname_str + , mnt_dir = mnt_dir_str + , mnt_type = mnt_type_str + } + getmntent h (ent:c) + +{- Using unsafe imports because the C functions are belived to never block. + - Note that getmntinfo is called with MNT_NOWAIT to avoid possibly blocking; + - while getmntent only accesses a file in /etc (or /proc) that should not + - block. -} +foreign import ccall unsafe "libmounts.h mounts_start" c_mounts_start + :: IO (Ptr ()) +foreign import ccall unsafe "libmounts.h mounts_next" c_mounts_next + :: Ptr () -> IO (Ptr ()) +foreign import ccall unsafe "libmounts.h mounts_end" c_mounts_end + :: Ptr () -> IO CInt + +#else + +{- Android does not support getmntent (well, it's a no-op stub in Bionic). + - + - But, the linux kernel's /proc/mounts is available to be parsed. + -} +getMounts :: IO [Mntent] +getMounts = catchDefaultIO [] $ + mapMaybe (parse . words) . lines <$> readFile "/proc/mounts" + where + parse (device:mountpoint:fstype:_rest) = Just $ Mntent + { mnt_fsname = device + , mnt_dir = mountpoint + , mnt_type = fstype + } + parse _ = Nothing + +#endif diff --git a/Utility/Network.hs b/Utility/Network.hs new file mode 100644 index 0000000000..62523c9e98 --- /dev/null +++ b/Utility/Network.hs @@ -0,0 +1,21 @@ +{- network functions + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Utility.Network where + +import Utility.Process +import Utility.Exception + +import Control.Applicative + +{- Haskell lacks uname(2) bindings, except in the + - Bindings.Uname addon. Rather than depend on that, + - use uname -n when available. -} +getHostname :: IO (Maybe String) +getHostname = catchMaybeIO uname_node + where + uname_node = takeWhile (/= '\n') <$> readProcess "uname" ["-n"] diff --git a/Utility/NotificationBroadcaster.hs b/Utility/NotificationBroadcaster.hs new file mode 100644 index 0000000000..b873df655f --- /dev/null +++ b/Utility/NotificationBroadcaster.hs @@ -0,0 +1,86 @@ +{- notification broadcaster + - + - This is used to allow clients to block until there is a new notification + - that some thing occurred. It does not communicate what the change is, + - it only provides blocking reads to wait on notifications. + - + - Multiple clients are supported. Each has a unique id. + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Utility.NotificationBroadcaster ( + NotificationBroadcaster, + NotificationHandle, + NotificationId, + newNotificationBroadcaster, + newNotificationHandle, + notificationHandleToId, + notificationHandleFromId, + sendNotification, + waitNotification, +) where + +import Common + +import Control.Concurrent.STM +import Control.Concurrent.MSampleVar + +{- One MSampleVar per client. The TMVar is never empty, so never blocks. -} +type NotificationBroadcaster = TMVar [MSampleVar ()] + +newtype NotificationId = NotificationId Int + deriving (Read, Show, Eq, Ord) + +{- Handle given out to an individual client. -} +data NotificationHandle = NotificationHandle NotificationBroadcaster NotificationId + +newNotificationBroadcaster :: IO NotificationBroadcaster +newNotificationBroadcaster = atomically $ newTMVar [] + +{- Allocates a notification handle for a client to use. + - + - An immediate notification can be forced the first time waitNotification + - is called on the handle. This is useful in cases where a notification + - may be sent while the new handle is being constructed. Normally, + - such a notification would be missed. Forcing causes extra work, + - but ensures such notifications get seen. + -} +newNotificationHandle :: Bool -> NotificationBroadcaster -> IO NotificationHandle +newNotificationHandle force b = NotificationHandle + <$> pure b + <*> addclient + where + addclient = do + s <- if force + then newSV () + else newEmptySV + atomically $ do + l <- takeTMVar b + putTMVar b $ l ++ [s] + return $ NotificationId $ length l + +{- Extracts the identifier from a notification handle. + - This can be used to eg, pass the identifier through to a WebApp. -} +notificationHandleToId :: NotificationHandle -> NotificationId +notificationHandleToId (NotificationHandle _ i) = i + +notificationHandleFromId :: NotificationBroadcaster -> NotificationId -> NotificationHandle +notificationHandleFromId = NotificationHandle + +{- Sends a notification to all clients. -} +sendNotification :: NotificationBroadcaster -> IO () +sendNotification b = do + l <- atomically $ readTMVar b + mapM_ notify l + where + notify s = writeSV s () + +{- Used by a client to block until a new notification is available since + - the last time it tried. -} +waitNotification :: NotificationHandle -> IO () +waitNotification (NotificationHandle b (NotificationId i)) = do + l <- atomically $ readTMVar b + readSV (l !! i) diff --git a/Utility/OSX.hs b/Utility/OSX.hs new file mode 100644 index 0000000000..f9d992575a --- /dev/null +++ b/Utility/OSX.hs @@ -0,0 +1,44 @@ +{- OSX stuff + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Utility.OSX where + +import Utility.UserInfo + +import System.FilePath + +autoStartBase :: String -> FilePath +autoStartBase label = "Library" "LaunchAgents" label ++ ".plist" + +systemAutoStart :: String -> FilePath +systemAutoStart label = "/" autoStartBase label + +userAutoStart :: String -> IO FilePath +userAutoStart label = do + home <- myHomeDir + return $ home autoStartBase label + +{- Generates an OSX autostart plist file with a given label, command, and + - params to run at boot or login. -} +genOSXAutoStartFile :: String -> String -> [String] -> String +genOSXAutoStartFile label command params = unlines + [ "" + , "" + , "" + , "" + , "Label" + , "" ++ label ++ "" + , "ProgramArguments" + , "" + , unlines $ map (\v -> "" ++ v ++ "") (command:params) + , "" + , "RunAtLoad" + , "" + , "" + , "" + ] + diff --git a/Utility/Parallel.hs b/Utility/Parallel.hs new file mode 100644 index 0000000000..b398803557 --- /dev/null +++ b/Utility/Parallel.hs @@ -0,0 +1,35 @@ +{- parallel processing via threads + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Utility.Parallel where + +import Common + +import Control.Concurrent +import Control.Exception + +{- Runs an action in parallel with a set of values, in a set of threads. + - In order for the actions to truely run in parallel, requires GHC's + - threaded runtime, + - + - Returns the values partitioned into ones with which the action succeeded, + - and ones with which it failed. -} +inParallel :: (v -> IO Bool) -> [v] -> IO ([v], [v]) +inParallel a l = do + mvars <- mapM thread l + statuses <- mapM takeMVar mvars + return $ reduce $ partition snd $ zip l statuses + where + reduce (x,y) = (map fst x, map fst y) + thread v = do + mvar <- newEmptyMVar + _ <- forkIO $ do + r <- try (a v) :: IO (Either SomeException Bool) + case r of + Left _ -> putMVar mvar False + Right b -> putMVar mvar b + return mvar diff --git a/Utility/PartialPrelude.hs b/Utility/PartialPrelude.hs new file mode 100644 index 0000000000..6efa093fd3 --- /dev/null +++ b/Utility/PartialPrelude.hs @@ -0,0 +1,68 @@ +{- Parts of the Prelude are partial functions, which are a common source of + - bugs. + - + - This exports functions that conflict with the prelude, which avoids + - them being accidentially used. + -} + +module Utility.PartialPrelude where + +import qualified Data.Maybe + +{- read should be avoided, as it throws an error + - Instead, use: readish -} +read :: Read a => String -> a +read = Prelude.read + +{- head is a partial function; head [] is an error + - Instead, use: take 1 or headMaybe -} +head :: [a] -> a +head = Prelude.head + +{- tail is also partial + - Instead, use: drop 1 -} +tail :: [a] -> [a] +tail = Prelude.tail + +{- init too + - Instead, use: beginning -} +init :: [a] -> [a] +init = Prelude.init + +{- last too + - Instead, use: end or lastMaybe -} +last :: [a] -> a +last = Prelude.last + +{- Attempts to read a value from a String. + - + - Ignores leading/trailing whitespace, and throws away any trailing + - text after the part that can be read. + - + - readMaybe is available in Text.Read in new versions of GHC, + - but that one requires the entire string to be consumed. + -} +readish :: Read a => String -> Maybe a +readish s = case reads s of + ((x,_):_) -> Just x + _ -> Nothing + +{- Like head but Nothing on empty list. -} +headMaybe :: [a] -> Maybe a +headMaybe = Data.Maybe.listToMaybe + +{- Like last but Nothing on empty list. -} +lastMaybe :: [a] -> Maybe a +lastMaybe [] = Nothing +lastMaybe v = Just $ Prelude.last v + +{- All but the last element of a list. + - (Like init, but no error on an empty list.) -} +beginning :: [a] -> [a] +beginning [] = [] +beginning l = Prelude.init l + +{- Like last, but no error on an empty list. -} +end :: [a] -> [a] +end [] = [] +end l = [Prelude.last l] diff --git a/Utility/Path.hs b/Utility/Path.hs new file mode 100644 index 0000000000..79e8e80895 --- /dev/null +++ b/Utility/Path.hs @@ -0,0 +1,238 @@ +{- path manipulation + - + - Copyright 2010-2013 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE PackageImports, CPP #-} + +module Utility.Path where + +import Data.String.Utils +import System.FilePath +import System.Directory +import Data.List +import Data.Maybe +import Control.Applicative + +#ifdef mingw32_HOST_OS +import Data.Char +import qualified System.FilePath.Posix as Posix +#else +import qualified "MissingH" System.Path as MissingH +import System.Posix.Files +#endif + +import Utility.Monad +import Utility.UserInfo + +{- Makes a path absolute if it's not already. + - The first parameter is a base directory (ie, the cwd) to use if the path + - is not already absolute. + - + - On Unix, collapses and normalizes ".." etc in the path. May return Nothing + - if the path cannot be normalized. + - + - MissingH's absNormPath does not work on Windows, so on Windows + - no normalization is done. + -} +absNormPath :: FilePath -> FilePath -> Maybe FilePath +#ifndef mingw32_HOST_OS +absNormPath dir path = MissingH.absNormPath dir path +#else +absNormPath dir path = Just $ combine dir path +#endif + +{- Returns the parent directory of a path. + - + - To allow this to be easily used in loops, which terminate upon reaching the + - top, the parent of / is "" -} +parentDir :: FilePath -> FilePath +parentDir dir + | null dirs = "" + | otherwise = joinDrive drive (join s $ init dirs) + where + -- on Unix, the drive will be "/" when the dir is absolute, otherwise "" + (drive, path) = splitDrive dir + dirs = filter (not . null) $ split s path + s = [pathSeparator] + +prop_parentDir_basics :: FilePath -> Bool +prop_parentDir_basics dir + | null dir = True + | dir == "/" = parentDir dir == "" + | otherwise = p /= dir + where + p = parentDir dir + +{- Checks if the first FilePath is, or could be said to contain the second. + - For example, "foo/" contains "foo/bar". Also, "foo", "./foo", "foo/" etc + - are all equivilant. + -} +dirContains :: FilePath -> FilePath -> Bool +dirContains a b = a == b || a' == b' || (a'++[pathSeparator]) `isPrefixOf` b' + where + norm p = fromMaybe "" $ absNormPath p "." + a' = norm a + b' = norm b + +{- Converts a filename into a normalized, absolute path. + - + - Unlike Directory.canonicalizePath, this does not require the path + - already exists. -} +absPath :: FilePath -> IO FilePath +absPath file = do + cwd <- getCurrentDirectory + return $ absPathFrom cwd file + +{- Converts a filename into a normalized, absolute path + - from the specified cwd. -} +absPathFrom :: FilePath -> FilePath -> FilePath +absPathFrom cwd file = fromMaybe bad $ absNormPath cwd file + where + bad = error $ "unable to normalize " ++ file + +{- Constructs a relative path from the CWD to a file. + - + - For example, assuming CWD is /tmp/foo/bar: + - relPathCwdToFile "/tmp/foo" == ".." + - relPathCwdToFile "/tmp/foo/bar" == "" + -} +relPathCwdToFile :: FilePath -> IO FilePath +relPathCwdToFile f = relPathDirToFile <$> getCurrentDirectory <*> absPath f + +{- Constructs a relative path from a directory to a file. + - + - Both must be absolute, and normalized (eg with absNormpath). + -} +relPathDirToFile :: FilePath -> FilePath -> FilePath +relPathDirToFile from to = join s $ dotdots ++ uncommon + where + s = [pathSeparator] + pfrom = split s from + pto = split s to + common = map fst $ takeWhile same $ zip pfrom pto + same (c,d) = c == d + uncommon = drop numcommon pto + dotdots = replicate (length pfrom - numcommon) ".." + numcommon = length common + +prop_relPathDirToFile_basics :: FilePath -> FilePath -> Bool +prop_relPathDirToFile_basics from to + | from == to = null r + | otherwise = not (null r) + where + r = relPathDirToFile from to + +prop_relPathDirToFile_regressionTest :: Bool +prop_relPathDirToFile_regressionTest = same_dir_shortcurcuits_at_difference + where + {- Two paths have the same directory component at the same + - location, but it's not really the same directory. + - Code used to get this wrong. -} + same_dir_shortcurcuits_at_difference = + relPathDirToFile (joinPath [pathSeparator : "tmp", "r", "lll", "xxx", "yyy", "18"]) + (joinPath [pathSeparator : "tmp", "r", ".git", "annex", "objects", "18", "gk", "SHA256-foo", "SHA256-foo"]) + == joinPath ["..", "..", "..", "..", ".git", "annex", "objects", "18", "gk", "SHA256-foo", "SHA256-foo"] + +{- Given an original list of paths, and an expanded list derived from it, + - generates a list of lists, where each sublist corresponds to one of the + - original paths. When the original path is a directory, any items + - in the expanded list that are contained in that directory will appear in + - its segment. + -} +segmentPaths :: [FilePath] -> [FilePath] -> [[FilePath]] +segmentPaths [] new = [new] +segmentPaths [_] new = [new] -- optimisation +segmentPaths (l:ls) new = [found] ++ segmentPaths ls rest + where + (found, rest)=partition (l `dirContains`) new + +{- This assumes that it's cheaper to call segmentPaths on the result, + - than it would be to run the action separately with each path. In + - the case of git file list commands, that assumption tends to hold. + -} +runSegmentPaths :: ([FilePath] -> IO [FilePath]) -> [FilePath] -> IO [[FilePath]] +runSegmentPaths a paths = segmentPaths paths <$> a paths + +{- Converts paths in the home directory to use ~/ -} +relHome :: FilePath -> IO String +relHome path = do + home <- myHomeDir + return $ if dirContains home path + then "~/" ++ relPathDirToFile home path + else path + +{- Checks if a command is available in PATH. + - + - The command may be fully-qualified, in which case, this succeeds as + - long as it exists. -} +inPath :: String -> IO Bool +inPath command = isJust <$> searchPath command + +{- Finds a command in PATH and returns the full path to it. + - + - The command may be fully qualified already, in which case it will + - be returned if it exists. + -} +searchPath :: String -> IO (Maybe FilePath) +searchPath command + | isAbsolute command = check command + | otherwise = getSearchPath >>= getM indir + where + indir d = check $ d command + check f = firstM doesFileExist +#ifdef mingw32_HOST_OS + [f, f ++ ".exe"] +#else + [f] +#endif + +{- Checks if a filename is a unix dotfile. All files inside dotdirs + - count as dotfiles. -} +dotfile :: FilePath -> Bool +dotfile file + | f == "." = False + | f == ".." = False + | f == "" = False + | otherwise = "." `isPrefixOf` f || dotfile (takeDirectory file) + where + f = takeFileName file + +{- Converts a DOS style path to a Cygwin style path. Only on Windows. + - Any trailing '\' is preserved as a trailing '/' -} +toCygPath :: FilePath -> FilePath +#ifndef mingw32_HOST_OS +toCygPath = id +#else +toCygPath p + | null drive = recombine parts + | otherwise = recombine $ "/cygdrive" : driveletter drive : parts + where + (drive, p') = splitDrive p + parts = splitDirectories p' + driveletter = map toLower . takeWhile (/= ':') + recombine = fixtrailing . Posix.joinPath + fixtrailing s + | hasTrailingPathSeparator p = Posix.addTrailingPathSeparator s + | otherwise = s +#endif + +{- Maximum size to use for a file in a specified directory. + - + - Many systems have a 255 byte limit to the name of a file, + - so that's taken as the max if the system has a larger limit, or has no + - limit. + -} +fileNameLengthLimit :: FilePath -> IO Int +#ifdef mingw32_HOST_OS +fileNameLengthLimit _ = return 255 +#else +fileNameLengthLimit dir = do + l <- fromIntegral <$> getPathVar dir FileNameLimit + if l <= 0 + then return 255 + else return $ minimum [l, 255] + where +#endif diff --git a/Utility/Percentage.hs b/Utility/Percentage.hs new file mode 100644 index 0000000000..d4b2da4299 --- /dev/null +++ b/Utility/Percentage.hs @@ -0,0 +1,33 @@ +{- percentages + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Utility.Percentage ( + Percentage, + percentage, + showPercentage +) where + +import Data.Ratio + +import Utility.HumanNumber + +newtype Percentage = Percentage (Ratio Integer) + +instance Show Percentage where + show = showPercentage 0 + +{- Normally the big number comes first. But 110% is allowed if desired. :) -} +percentage :: Integer -> Integer -> Percentage +percentage 0 _ = Percentage 0 +percentage full have = Percentage $ have * 100 % full + +{- Pretty-print a Percentage, with a specified level of precision. -} +showPercentage :: Int -> Percentage -> String +showPercentage precision (Percentage p) = v ++ "%" + where + v = showImprecise precision n + n = fromRational p :: Double diff --git a/Utility/Process.hs b/Utility/Process.hs new file mode 100644 index 0000000000..8ea6321203 --- /dev/null +++ b/Utility/Process.hs @@ -0,0 +1,324 @@ +{- System.Process enhancements, including additional ways of running + - processes, and logging. + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE CPP, Rank2Types #-} + +module Utility.Process ( + module X, + CreateProcess, + StdHandle(..), + readProcess, + readProcessEnv, + writeReadProcessEnv, + forceSuccessProcess, + checkSuccessProcess, + ignoreFailureProcess, + createProcessSuccess, + createProcessChecked, + createBackgroundProcess, + processTranscript, + withHandle, + withBothHandles, + withQuietOutput, + withNullHandle, + createProcess, + startInteractiveProcess, + stdinHandle, + stdoutHandle, + stderrHandle, +) where + +import qualified System.Process +import System.Process as X hiding (CreateProcess(..), createProcess, runInteractiveProcess, readProcess, readProcessWithExitCode, system, rawSystem, runInteractiveCommand, runProcess) +import System.Process hiding (createProcess, readProcess) +import System.Exit +import System.IO +import System.Log.Logger +import Control.Concurrent +import qualified Control.Exception as E +import Control.Monad +#ifndef mingw32_HOST_OS +import System.Posix.IO +import Data.Maybe +#endif + +import Utility.Misc +import Utility.Exception + +type CreateProcessRunner = forall a. CreateProcess -> ((Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle) -> IO a) -> IO a + +data StdHandle = StdinHandle | StdoutHandle | StderrHandle + deriving (Eq) + +{- Normally, when reading from a process, it does not need to be fed any + - standard input. -} +readProcess :: FilePath -> [String] -> IO String +readProcess cmd args = readProcessEnv cmd args Nothing + +readProcessEnv :: FilePath -> [String] -> Maybe [(String, String)] -> IO String +readProcessEnv cmd args environ = + withHandle StdoutHandle createProcessSuccess p $ \h -> do + output <- hGetContentsStrict h + hClose h + return output + where + p = (proc cmd args) + { std_out = CreatePipe + , env = environ + } + +{- Writes a string to a process on its stdin, + - returns its output, and also allows specifying the environment. + -} +writeReadProcessEnv + :: FilePath + -> [String] + -> Maybe [(String, String)] + -> String + -> (Maybe (Handle -> IO ())) + -> IO String +writeReadProcessEnv cmd args environ input adjusthandle = do + (Just inh, Just outh, _, pid) <- createProcess p + + maybe (return ()) (\a -> a inh) adjusthandle + maybe (return ()) (\a -> a outh) adjusthandle + + -- fork off a thread to start consuming the output + output <- hGetContents outh + outMVar <- newEmptyMVar + _ <- forkIO $ E.evaluate (length output) >> putMVar outMVar () + + -- now write and flush any input + when (not (null input)) $ do hPutStr inh input; hFlush inh + hClose inh -- done with stdin + + -- wait on the output + takeMVar outMVar + hClose outh + + -- wait on the process + forceSuccessProcess p pid + + return output + + where + p = (proc cmd args) + { std_in = CreatePipe + , std_out = CreatePipe + , std_err = Inherit + , env = environ + } + +{- Waits for a ProcessHandle, and throws an IOError if the process + - did not exit successfully. -} +forceSuccessProcess :: CreateProcess -> ProcessHandle -> IO () +forceSuccessProcess p pid = do + code <- waitForProcess pid + case code of + ExitSuccess -> return () + ExitFailure n -> fail $ showCmd p ++ " exited " ++ show n + +{- Waits for a ProcessHandle and returns True if it exited successfully. + - Note that using this with createProcessChecked will throw away + - the Bool, and is only useful to ignore the exit code of a process, + - while still waiting for it. -} +checkSuccessProcess :: ProcessHandle -> IO Bool +checkSuccessProcess pid = do + code <- waitForProcess pid + return $ code == ExitSuccess + +ignoreFailureProcess :: ProcessHandle -> IO Bool +ignoreFailureProcess pid = do + void $ waitForProcess pid + return True + +{- Runs createProcess, then an action on its handles, and then + - forceSuccessProcess. -} +createProcessSuccess :: CreateProcessRunner +createProcessSuccess p a = createProcessChecked (forceSuccessProcess p) p a + +{- Runs createProcess, then an action on its handles, and then + - a checker action on its exit code, which must wait for the process. -} +createProcessChecked :: (ProcessHandle -> IO b) -> CreateProcessRunner +createProcessChecked checker p a = do + t@(_, _, _, pid) <- createProcess p + r <- tryNonAsync $ a t + _ <- checker pid + either E.throw return r + +{- Leaves the process running, suitable for lazy streaming. + - Note: Zombies will result, and must be waited on. -} +createBackgroundProcess :: CreateProcessRunner +createBackgroundProcess p a = a =<< createProcess p + +{- Runs a process, optionally feeding it some input, and + - returns a transcript combining its stdout and stderr, and + - whether it succeeded or failed. -} +processTranscript :: String -> [String] -> (Maybe String) -> IO (String, Bool) +#ifndef mingw32_HOST_OS +processTranscript cmd opts input = do + (readf, writef) <- createPipe + readh <- fdToHandle readf + writeh <- fdToHandle writef + p@(_, _, _, pid) <- createProcess $ + (proc cmd opts) + { std_in = if isJust input then CreatePipe else Inherit + , std_out = UseHandle writeh + , std_err = UseHandle writeh + } + hClose writeh + + -- fork off a thread to start consuming the output + transcript <- hGetContents readh + outMVar <- newEmptyMVar + _ <- forkIO $ E.evaluate (length transcript) >> putMVar outMVar () + + -- now write and flush any input + case input of + Just s -> do + let inh = stdinHandle p + unless (null s) $ do + hPutStr inh s + hFlush inh + hClose inh + Nothing -> return () + + -- wait on the output + takeMVar outMVar + hClose readh + + ok <- checkSuccessProcess pid + return (transcript, ok) +#else +processTranscript = error "processTranscript TODO" +#endif + +{- Runs a CreateProcessRunner, on a CreateProcess structure, that + - is adjusted to pipe only from/to a single StdHandle, and passes + - the resulting Handle to an action. -} +withHandle + :: StdHandle + -> CreateProcessRunner + -> CreateProcess + -> (Handle -> IO a) + -> IO a +withHandle h creator p a = creator p' $ a . select + where + base = p + { std_in = Inherit + , std_out = Inherit + , std_err = Inherit + } + (select, p') + | h == StdinHandle = + (stdinHandle, base { std_in = CreatePipe }) + | h == StdoutHandle = + (stdoutHandle, base { std_out = CreatePipe }) + | h == StderrHandle = + (stderrHandle, base { std_err = CreatePipe }) + +{- Like withHandle, but passes (stdin, stdout) handles to the action. -} +withBothHandles + :: CreateProcessRunner + -> CreateProcess + -> ((Handle, Handle) -> IO a) + -> IO a +withBothHandles creator p a = creator p' $ a . bothHandles + where + p' = p + { std_in = CreatePipe + , std_out = CreatePipe + , std_err = Inherit + } + +{- Forces the CreateProcessRunner to run quietly; + - both stdout and stderr are discarded. -} +withQuietOutput + :: CreateProcessRunner + -> CreateProcess + -> IO () +withQuietOutput creator p = withNullHandle $ \nullh -> do + let p' = p + { std_out = UseHandle nullh + , std_err = UseHandle nullh + } + creator p' $ const $ return () + +withNullHandle :: (Handle -> IO a) -> IO a +withNullHandle = withFile devnull WriteMode + where +#ifndef mingw32_HOST_OS + devnull = "/dev/null" +#else + devnull = "NUL" +#endif + +{- Extract a desired handle from createProcess's tuple. + - These partial functions are safe as long as createProcess is run + - with appropriate parameters to set up the desired handle. + - Get it wrong and the runtime crash will always happen, so should be + - easily noticed. -} +type HandleExtractor = (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle) -> Handle +stdinHandle :: HandleExtractor +stdinHandle (Just h, _, _, _) = h +stdinHandle _ = error "expected stdinHandle" +stdoutHandle :: HandleExtractor +stdoutHandle (_, Just h, _, _) = h +stdoutHandle _ = error "expected stdoutHandle" +stderrHandle :: HandleExtractor +stderrHandle (_, _, Just h, _) = h +stderrHandle _ = error "expected stderrHandle" +bothHandles :: (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle) -> (Handle, Handle) +bothHandles (Just hin, Just hout, _, _) = (hin, hout) +bothHandles _ = error "expected bothHandles" + +{- Debugging trace for a CreateProcess. -} +debugProcess :: CreateProcess -> IO () +debugProcess p = do + debugM "Utility.Process" $ unwords + [ action ++ ":" + , showCmd p + ] + where + action + | piped (std_in p) && piped (std_out p) = "chat" + | piped (std_in p) = "feed" + | piped (std_out p) = "read" + | otherwise = "call" + piped Inherit = False + piped _ = True + +{- Shows the command that a CreateProcess will run. -} +showCmd :: CreateProcess -> String +showCmd = go . cmdspec + where + go (ShellCommand s) = s + go (RawCommand c ps) = c ++ " " ++ show ps + +{- Starts an interactive process. Unlike runInteractiveProcess in + - System.Process, stderr is inherited. -} +startInteractiveProcess + :: FilePath + -> [String] + -> Maybe [(String, String)] + -> IO (ProcessHandle, Handle, Handle) +startInteractiveProcess cmd args environ = do + let p = (proc cmd args) + { std_in = CreatePipe + , std_out = CreatePipe + , std_err = Inherit + , env = environ + } + (Just from, Just to, _, pid) <- createProcess p + return (pid, to, from) + +{- Wrapper around System.Process function that does debug logging. -} +createProcess :: CreateProcess -> IO (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle) +createProcess p = do + debugProcess p + System.Process.createProcess p diff --git a/Utility/QuickCheck.hs b/Utility/QuickCheck.hs new file mode 100644 index 0000000000..078b10c8bc --- /dev/null +++ b/Utility/QuickCheck.hs @@ -0,0 +1,45 @@ +{- QuickCheck with additional instances + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# OPTIONS_GHC -fno-warn-orphans #-} +{-# LANGUAGE TypeSynonymInstances #-} + +module Utility.QuickCheck + ( module X + , module Utility.QuickCheck + ) where + +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 + arbitrary = nonNegative arbitrarySizedIntegral + +instance Arbitrary EpochTime where + arbitrary = nonNegative arbitrarySizedIntegral + +{- Pids are never negative, or 0. -} +instance Arbitrary ProcessID where + arbitrary = arbitrarySizedBoundedIntegral `suchThat` (> 0) + +{- Inodes are never negative. -} +instance Arbitrary FileID where + arbitrary = nonNegative arbitrarySizedIntegral + +{- File sizes are never negative. -} +instance Arbitrary FileOffset where + arbitrary = nonNegative arbitrarySizedIntegral + +nonNegative :: (Num a, Ord a) => Gen a -> Gen a +nonNegative g = g `suchThat` (>= 0) diff --git a/Utility/Rsync.hs b/Utility/Rsync.hs new file mode 100644 index 0000000000..5f322a0cb7 --- /dev/null +++ b/Utility/Rsync.hs @@ -0,0 +1,152 @@ +{- various rsync stuff + - + - Copyright 2010-2013 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Utility.Rsync where + +import Common +import Utility.Metered + +import Data.Char +import System.Console.GetOpt +import Data.Tuple.Utils + +{- Generates parameters to make rsync use a specified command as its remote + - shell. -} +rsyncShell :: [CommandParam] -> [CommandParam] +rsyncShell command = [Param "-e", Param $ unwords $ map escape (toCommand command)] + where + {- rsync requires some weird, non-shell like quoting in + - here. A doubled single quote inside the single quoted + - string is a single quote. -} + escape s = "'" ++ intercalate "''" (split "'" s) ++ "'" + +{- Runs rsync in server mode to send a file. -} +rsyncServerSend :: [CommandParam] -> FilePath -> IO Bool +rsyncServerSend options file = rsync $ + rsyncServerParams ++ Param "--sender" : options ++ [File file] + +{- Runs rsync in server mode to receive a file. -} +rsyncServerReceive :: [CommandParam] -> FilePath -> IO Bool +rsyncServerReceive options file = rsync $ + rsyncServerParams ++ options ++ [File file] + +rsyncServerParams :: [CommandParam] +rsyncServerParams = + [ Param "--server" + -- preserve timestamps + , Param "-t" + -- allow resuming of transfers of big files + , Param "--inplace" + -- other options rsync normally uses in server mode + , Params "-e.Lsf ." + ] + +rsyncUseDestinationPermissions :: CommandParam +rsyncUseDestinationPermissions = Param "--chmod=ugo=rwX" + +rsync :: [CommandParam] -> IO Bool +rsync = boolSystem "rsync" . rsyncParamsFixup + +{- On Windows, rsync is from Cygwin, and expects to get Cygwin formatted + - paths to files. (It thinks that C:foo refers to a host named "C"). + - Fix up all Files in the Params appropriately. -} +rsyncParamsFixup :: [CommandParam] -> [CommandParam] +rsyncParamsFixup = map fixup + where + fixup (File f) = File (toCygPath f) + fixup p = p + +{- Runs rsync, but intercepts its progress output and updates a meter. + - The progress output is also output to stdout. + - + - The params must enable rsync's --progress mode for this to work. + -} +rsyncProgress :: MeterUpdate -> [CommandParam] -> IO Bool +rsyncProgress meterupdate params = do + r <- withHandle StdoutHandle createProcessSuccess p (feedprogress 0 []) + {- For an unknown reason, piping rsync's output like this does + - causes it to run a second ssh process, which it neglects to wait + - on. Reap the resulting zombie. -} + reapZombies + return r + where + p = proc "rsync" (toCommand $ rsyncParamsFixup params) + feedprogress prev buf h = do + s <- hGetSomeString h 80 + if null s + then return True + else do + putStr s + hFlush stdout + let (mbytes, buf') = parseRsyncProgress (buf++s) + case mbytes of + Nothing -> feedprogress prev buf' h + (Just bytes) -> do + when (bytes /= prev) $ + meterupdate $ toBytesProcessed bytes + feedprogress bytes buf' h + +{- Checks if an rsync url involves the remote shell (ssh or rsh). + - Use of such urls with rsync requires additional shell + - escaping. -} +rsyncUrlIsShell :: String -> Bool +rsyncUrlIsShell s + | "rsync://" `isPrefixOf` s = False + | otherwise = go s + where + -- host::dir is rsync protocol, while host:dir is ssh/rsh + go [] = False + go (c:cs) + | c == '/' = False -- got to directory with no colon + | c == ':' = not $ ":" `isPrefixOf` cs + | otherwise = go cs + +{- Checks if a rsync url is really just a local path. -} +rsyncUrlIsPath :: String -> Bool +rsyncUrlIsPath s + | rsyncUrlIsShell s = False + | otherwise = ':' `notElem` s + +{- Parses the String looking for rsync progress output, and returns + - Maybe the number of bytes rsynced so far, and any any remainder of the + - string that could be an incomplete progress output. That remainder + - should be prepended to future output, and fed back in. This interface + - allows the output to be read in any desired size chunk, or even one + - character at a time. + - + - Strategy: Look for chunks prefixed with \r (rsync writes a \r before + - the first progress output, and each thereafter). The first number + - after the \r is the number of bytes processed. After the number, + - there must appear some whitespace, or we didn't get the whole number, + - and return the \r and part we did get, for later processing. + -} +parseRsyncProgress :: String -> (Maybe Integer, String) +parseRsyncProgress = go [] . reverse . progresschunks + where + go remainder [] = (Nothing, remainder) + go remainder (x:xs) = case parsebytes (findbytesstart x) of + Nothing -> go (delim:x++remainder) xs + Just b -> (Just b, remainder) + + delim = '\r' + {- Find chunks that each start with delim. + - The first chunk doesn't start with it + - (it's empty when delim is at the start of the string). -} + progresschunks = drop 1 . split [delim] + findbytesstart s = dropWhile isSpace s + parsebytes s = case break isSpace s of + ([], _) -> Nothing + (_, []) -> Nothing + (b, _) -> readish b + +{- Filters options to those that are safe to pass to rsync in server mode, + - without causing it to eg, expose files. -} +filterRsyncSafeOptions :: [String] -> [String] +filterRsyncSafeOptions = fst3 . getOpt Permute + [ Option [] ["bwlimit"] (reqArgLong "bwlimit") "" ] + where + reqArgLong x = ReqArg (\v -> "--" ++ x ++ "=" ++ v) "" diff --git a/Utility/SRV.hs b/Utility/SRV.hs new file mode 100644 index 0000000000..0a77191c42 --- /dev/null +++ b/Utility/SRV.hs @@ -0,0 +1,106 @@ +{- SRV record lookup + - + - Uses either the ADNS Haskell library, or the standalone Haskell DNS + - package, or the host command. + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE CPP #-} + +module Utility.SRV ( + mkSRVTcp, + mkSRV, + lookupSRV, + lookupSRVHost, + HostPort, +) where + +import Utility.Process +import Utility.Exception +import Utility.PartialPrelude + +import Network +import Data.Function +import Data.List +import Control.Applicative +import Data.Maybe + +#ifdef WITH_ADNS +import ADNS.Resolver +import Data.Either +#else +#ifdef WITH_DNS +import qualified Network.DNS.Lookup as DNS +import Network.DNS.Resolver +import qualified Data.ByteString.UTF8 as B8 +#endif +#endif + +newtype SRV = SRV String + deriving (Show, Eq) + +type HostPort = (HostName, PortID) + +type PriorityWeight = (Int, Int) -- sort by priority first, then weight + +mkSRV :: String -> String -> HostName -> SRV +mkSRV transport protocol host = SRV $ concat + ["_", protocol, "._", transport, ".", host] + +mkSRVTcp :: String -> HostName -> SRV +mkSRVTcp = mkSRV "tcp" + +{- Returns an ordered list, with highest priority hosts first. + - + - On error, returns an empty list. -} +lookupSRV :: SRV -> IO [HostPort] +#ifdef WITH_ADNS +lookupSRV (SRV srv) = initResolver [] $ \resolver -> do + r <- catchDefaultIO (Right []) $ + resolveSRV resolver srv + return $ either (\_ -> []) id r +#else +#ifdef WITH_DNS +lookupSRV (SRV srv) = do + seed <- makeResolvSeed defaultResolvConf + r <- withResolver seed $ flip DNS.lookupSRV $ B8.fromString srv + return $ maybe [] (orderHosts . map tohosts) r + where + tohosts (priority, weight, port, hostname) = + ( (priority, weight) + , (B8.toString hostname, PortNumber $ fromIntegral port) + ) +#else +lookupSRV = lookupSRVHost +#endif +#endif + +lookupSRVHost :: SRV -> IO [HostPort] +lookupSRVHost (SRV srv) = catchDefaultIO [] $ + parseSrvHost <$> readProcessEnv "host" ["-t", "SRV", "--", srv] + -- clear environment, to avoid LANG affecting output + (Just []) + +parseSrvHost :: String -> [HostPort] +parseSrvHost = orderHosts . catMaybes . map parse . lines + where + parse l = case words l of + [_, _, _, _, spriority, sweight, sport, hostname] -> do + let v = + ( readish sport :: Maybe Int + , readish spriority :: Maybe Int + , readish sweight :: Maybe Int + ) + case v of + (Just port, Just priority, Just weight) -> Just + ( (priority, weight) + , (hostname, PortNumber $ fromIntegral port) + ) + _ -> Nothing + _ -> Nothing + +orderHosts :: [(PriorityWeight, HostPort)] -> [HostPort] +orderHosts = map snd . sortBy (compare `on` fst) diff --git a/Utility/SafeCommand.hs b/Utility/SafeCommand.hs new file mode 100644 index 0000000000..c8318ec2e5 --- /dev/null +++ b/Utility/SafeCommand.hs @@ -0,0 +1,120 @@ +{- safely running shell commands + - + - Copyright 2010-2013 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Utility.SafeCommand where + +import System.Exit +import Utility.Process +import System.Process (env) +import Data.String.Utils +import Control.Applicative +import System.FilePath +import Data.Char + +{- A type for parameters passed to a shell command. A command can + - be passed either some Params (multiple parameters can be included, + - whitespace-separated, or a single Param (for when parameters contain + - whitespace), or a File. + -} +data CommandParam = Params String | Param String | File FilePath + deriving (Eq, Show, Ord) + +{- Used to pass a list of CommandParams to a function that runs + - a command and expects Strings. -} +toCommand :: [CommandParam] -> [String] +toCommand = concatMap unwrap + where + unwrap (Param s) = [s] + unwrap (Params s) = filter (not . null) (split " " s) + -- Files that start with a non-alphanumeric that is not a path + -- separator are modified to avoid the command interpreting them as + -- options or other special constructs. + unwrap (File s@(h:_)) + | isAlphaNum h || h `elem` pathseps = [s] + | otherwise = ["./" ++ s] + unwrap (File s) = [s] + -- '/' is explicitly included because it's an alternative + -- path separator on Windows. + pathseps = pathSeparator:"./" + +{- Run a system command, and returns True or False + - if it succeeded or failed. + -} +boolSystem :: FilePath -> [CommandParam] -> IO Bool +boolSystem command params = boolSystemEnv command params Nothing + +boolSystemEnv :: FilePath -> [CommandParam] -> Maybe [(String, String)] -> IO Bool +boolSystemEnv command params environ = dispatch <$> safeSystemEnv command params environ + where + dispatch ExitSuccess = True + dispatch _ = False + +{- Runs a system command, returning the exit status. -} +safeSystem :: FilePath -> [CommandParam] -> IO ExitCode +safeSystem command params = safeSystemEnv command params Nothing + +safeSystemEnv :: FilePath -> [CommandParam] -> Maybe [(String, String)] -> IO ExitCode +safeSystemEnv command params environ = do + (_, _, _, pid) <- createProcess (proc command $ toCommand params) + { env = environ } + waitForProcess pid + +{- Wraps a shell command line inside sh -c, allowing it to be run in a + - login shell that may not support POSIX shell, eg csh. -} +shellWrap :: String -> String +shellWrap cmdline = "sh -c " ++ shellEscape cmdline + +{- Escapes a filename or other parameter to be safely able to be exposed to + - the shell. + - + - This method works for POSIX shells, as well as other shells like csh. + -} +shellEscape :: String -> String +shellEscape f = "'" ++ escaped ++ "'" + where + -- replace ' with '"'"' + escaped = join "'\"'\"'" $ split "'" f + +{- Unescapes a set of shellEscaped words or filenames. -} +shellUnEscape :: String -> [String] +shellUnEscape [] = [] +shellUnEscape s = word : shellUnEscape rest + where + (word, rest) = findword "" s + findword w [] = (w, "") + findword w (c:cs) + | c == ' ' = (w, cs) + | c == '\'' = inquote c w cs + | c == '"' = inquote c w cs + | otherwise = findword (w++[c]) cs + inquote _ w [] = (w, "") + inquote q w (c:cs) + | c == q = findword w cs + | otherwise = inquote q (w++[c]) cs + +{- For quickcheck. -} +prop_idempotent_shellEscape :: String -> Bool +prop_idempotent_shellEscape s = [s] == (shellUnEscape . shellEscape) s +prop_idempotent_shellEscape_multiword :: [String] -> Bool +prop_idempotent_shellEscape_multiword s = s == (shellUnEscape . unwords . map shellEscape) s + +{- Segements a list of filenames into groups that are all below the manximum + - command-line length limit. Does not preserve order. -} +segmentXargs :: [FilePath] -> [[FilePath]] +segmentXargs l = go l [] 0 [] + where + go [] c _ r = c:r + go (f:fs) c accumlen r + | len < maxlen && newlen > maxlen = go (f:fs) [] 0 (c:r) + | otherwise = go fs (f:c) newlen r + where + len = length f + newlen = accumlen + len + + {- 10k of filenames per command, well under Linux's 20k limit; + - allows room for other parameters etc. -} + maxlen = 10240 diff --git a/Utility/Shell.hs b/Utility/Shell.hs new file mode 100644 index 0000000000..2227dc767b --- /dev/null +++ b/Utility/Shell.hs @@ -0,0 +1,26 @@ +{- /bin/sh handling + - + - Copyright 2013 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE CPP #-} + +module Utility.Shell where + +shellPath_portable :: FilePath +shellPath_portable = "/bin/sh" + +shellPath_local :: FilePath +#ifndef __ANDROID__ +shellPath_local = shellPath_portable +#else +shellPath_local = "/system/bin/sh" +#endif + +shebang_portable :: String +shebang_portable = "#!" ++ shellPath_portable + +shebang_local :: String +shebang_local = "#!" ++ shellPath_local diff --git a/Utility/TList.hs b/Utility/TList.hs new file mode 100644 index 0000000000..e4bb95498d --- /dev/null +++ b/Utility/TList.hs @@ -0,0 +1,66 @@ +{- Transactional lists + - + - Based on DLists, a transactional list can quickly and efficiently + - have items inserted at either end, or a whole list appended to it. + - + - Copyright 2013 Joey Hess + -} + +{-# LANGUAGE BangPatterns #-} + +module Utility.TList where + +import Common + +import Control.Concurrent.STM +import qualified Data.DList as D + +type TList a = TMVar (D.DList a) + +newTList :: STM (TList a) +newTList = newEmptyTMVar + +{- Gets the contents of the TList. Blocks when empty. + - TList is left empty. -} +getTList :: TList a -> STM [a] +getTList tlist = D.toList <$> getTDList tlist + +getTDList :: TList a -> STM (D.DList a) +getTDList = takeTMVar + +{- Replaces the contents of the TList. -} +setTList :: TList a -> [a] -> STM () +setTList tlist = setTDList tlist . D.fromList + +setTDList :: TList a -> D.DList a -> STM () +setTDList tlist = modifyTList tlist . const + +{- Takes anything currently in the TList, without blocking. + - TList is left empty. -} +takeTList :: TList a -> STM [a] +takeTList tlist = maybe [] D.toList <$> tryTakeTMVar tlist + +{- Reads anything in the list, without modifying it, or blocking. -} +readTList :: TList a -> STM [a] +readTList tlist = maybe [] D.toList <$> tryReadTMVar tlist + +{- Mutates a TList. -} +modifyTList :: TList a -> (D.DList a -> D.DList a) -> STM () +modifyTList tlist a = do + dl <- fromMaybe D.empty <$> tryTakeTMVar tlist + let !dl' = a dl + {- The TMVar is left empty when the list is empty. + - Thus attempts to read it automatically block. -} + unless (emptyDList dl') $ + putTMVar tlist dl' + where + emptyDList = D.list True (\_ _ -> False) + +consTList :: TList a -> a -> STM () +consTList tlist v = modifyTList tlist $ \dl -> D.cons v dl + +snocTList :: TList a -> a -> STM () +snocTList tlist v = modifyTList tlist $ \dl -> D.snoc dl v + +appendTList :: TList a -> [a] -> STM () +appendTList tlist l = modifyTList tlist $ \dl -> D.append dl (D.fromList l) diff --git a/Utility/Tense.hs b/Utility/Tense.hs new file mode 100644 index 0000000000..60b3fa513d --- /dev/null +++ b/Utility/Tense.hs @@ -0,0 +1,57 @@ +{- Past and present tense text. + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE OverloadedStrings #-} + +module Utility.Tense where + +import qualified Data.Text as T +import Data.Text (Text) +import GHC.Exts( IsString(..) ) + +data Tense = Present | Past + deriving (Eq) + +data TenseChunk = Tensed Text Text | UnTensed Text + deriving (Eq, Ord, Show) + +newtype TenseText = TenseText [TenseChunk] + deriving (Eq, Ord) + +{- Allows OverloadedStrings to be used, to build UnTensed chunks. -} +instance IsString TenseChunk where + fromString = UnTensed . T.pack + +{- Allows OverloadedStrings to be used, to provide UnTensed TenseText. -} +instance IsString TenseText where + fromString s = TenseText [fromString s] + +renderTense :: Tense -> TenseText -> Text +renderTense tense (TenseText chunks) = T.concat $ map render chunks + where + render (Tensed present past) + | tense == Present = present + | otherwise = past + render (UnTensed s) = s + +{- Builds up a TenseText, separating chunks with spaces. + - + - However, rather than just intersperse new chunks for the spaces, + - the spaces are appended to the end of the chunks. + -} +tenseWords :: [TenseChunk] -> TenseText +tenseWords = TenseText . go [] + where + go c [] = reverse c + go c (w:[]) = reverse (w:c) + go c ((UnTensed w):ws) = go (UnTensed (addspace w) : c) ws + go c ((Tensed w1 w2):ws) = + go (Tensed (addspace w1) (addspace w2) : c) ws + addspace w = T.append w " " + +unTensed :: Text -> TenseText +unTensed t = TenseText [UnTensed t] diff --git a/Utility/ThreadLock.hs b/Utility/ThreadLock.hs new file mode 100644 index 0000000000..c029a2b0c8 --- /dev/null +++ b/Utility/ThreadLock.hs @@ -0,0 +1,19 @@ +{- locking between threads + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Utility.ThreadLock where + +import Control.Concurrent.MVar + +type Lock = MVar () + +newLock :: IO Lock +newLock = newMVar () + +{- Runs an action with a lock held, so only one thread at a time can run it. -} +withLock :: Lock -> IO a -> IO a +withLock lock = withMVar lock . const diff --git a/Utility/ThreadScheduler.hs b/Utility/ThreadScheduler.hs new file mode 100644 index 0000000000..c3e871cde7 --- /dev/null +++ b/Utility/ThreadScheduler.hs @@ -0,0 +1,69 @@ +{- thread scheduling + - + - Copyright 2012, 2013 Joey Hess + - Copyright 2011 Bas van Dijk & Roel van Dijk + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE CPP #-} + +module Utility.ThreadScheduler where + +import Common + +import Control.Concurrent +#ifndef mingw32_HOST_OS +import System.Posix.Signals +#ifndef __ANDROID__ +import System.Posix.Terminal +#endif +#endif + +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 +runEvery n a = forever $ do + threadDelaySeconds n + a + +threadDelaySeconds :: Seconds -> IO () +threadDelaySeconds (Seconds n) = unboundDelay (fromIntegral n * oneSecond) + +{- Like threadDelay, but not bounded by an Int. + - + - There is no guarantee that the thread will be rescheduled promptly when the + - delay has expired, but the thread will never continue to run earlier than + - specified. + - + - Taken from the unbounded-delay package to avoid a dependency for 4 lines + - of code. + -} +unboundDelay :: Microseconds -> IO () +unboundDelay time = do + let maxWait = min time $ toInteger (maxBound :: Int) + threadDelay $ fromInteger maxWait + when (maxWait /= time) $ unboundDelay (time - maxWait) + +{- Pauses the main thread, letting children run until program termination. -} +waitForTermination :: IO () +waitForTermination = do + lock <- newEmptyMVar +#ifndef mingw32_HOST_OS + let check sig = void $ + installHandler sig (CatchOnce $ putMVar lock ()) Nothing + check softwareTermination +#ifndef __ANDROID__ + whenM (queryTerminal stdInput) $ + check keyboardSignal +#endif +#endif + takeMVar lock + +oneSecond :: Microseconds +oneSecond = 1000000 diff --git a/Utility/Tmp.hs b/Utility/Tmp.hs new file mode 100644 index 0000000000..186cd121a6 --- /dev/null +++ b/Utility/Tmp.hs @@ -0,0 +1,88 @@ +{- Temporary files and directories. + - + - Copyright 2010-2013 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Utility.Tmp where + +import Control.Exception (bracket) +import System.IO +import System.Directory +import Control.Monad.IfElse + +import Utility.Exception +import System.FilePath +import Utility.FileSystemEncoding + +type Template = String + +{- Runs an action like writeFile, writing to a temp file first and + - then moving it into place. The temp file is stored in the same + - directory as the final file to avoid cross-device renames. -} +viaTmp :: (FilePath -> String -> IO ()) -> FilePath -> String -> IO () +viaTmp a file content = do + let (dir, base) = splitFileName file + createDirectoryIfMissing True dir + (tmpfile, handle) <- openTempFile dir (base ++ ".tmp") + hClose handle + a tmpfile content + renameFile tmpfile file + +{- Runs an action with a tmp file located in the system's tmp directory + - (or in "." if there is none) then removes the file. -} +withTmpFile :: Template -> (FilePath -> Handle -> IO a) -> IO a +withTmpFile template a = do + tmpdir <- catchDefaultIO "." getTemporaryDirectory + withTmpFileIn tmpdir template a + +{- Runs an action with a tmp file located in the specified directory, + - then removes the file. -} +withTmpFileIn :: FilePath -> Template -> (FilePath -> Handle -> IO a) -> IO a +withTmpFileIn tmpdir template a = bracket create remove use + where + create = openTempFile tmpdir template + remove (name, handle) = do + hClose handle + catchBoolIO (removeFile name >> return True) + use (name, handle) = a name handle + +{- Runs an action with a tmp directory located within the system's tmp + - directory (or within "." if there is none), then removes the tmp + - directory and all its contents. -} +withTmpDir :: Template -> (FilePath -> IO a) -> IO a +withTmpDir template a = do + tmpdir <- catchDefaultIO "." getTemporaryDirectory + withTmpDirIn tmpdir template a + +{- Runs an action with a tmp directory located within a specified directory, + - then removes the tmp directory and all its contents. -} +withTmpDirIn :: FilePath -> Template -> (FilePath -> IO a) -> IO a +withTmpDirIn tmpdir template = bracket create remove + where + remove d = whenM (doesDirectoryExist d) $ + removeDirectoryRecursive d + create = do + createDirectoryIfMissing True tmpdir + makenewdir (tmpdir template) (0 :: Int) + makenewdir t n = do + let dir = t ++ "." ++ show n + either (const $ makenewdir t $ n + 1) (const $ return dir) + =<< tryIO (createDirectory dir) + +{- It's not safe to use a FilePath of an existing file as the template + - for openTempFile, because if the FilePath is really long, the tmpfile + - will be longer, and may exceed the maximum filename length. + - + - This generates a template that is never too long. + - (Well, it allocates 20 characters for use in making a unique temp file, + - anyway, which is enough for the current implementation and any + - likely implementation.) + -} +relatedTemplate :: FilePath -> FilePath +relatedTemplate f + | len > 20 = truncateFilePath (len - 20) f + | otherwise = f + where + len = length f diff --git a/Utility/Touch.hsc b/Utility/Touch.hsc new file mode 100644 index 0000000000..53dd719fb4 --- /dev/null +++ b/Utility/Touch.hsc @@ -0,0 +1,120 @@ +{- More control over touching a file. + - + - Copyright 2011 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE ForeignFunctionInterface #-} + +module Utility.Touch ( + TimeSpec(..), + touchBoth, + touch +) where + +import Utility.FileSystemEncoding + +import Foreign +import Foreign.C +import Control.Monad (when) + +newtype TimeSpec = TimeSpec CTime + +{- Changes the access and modification times of an existing file. + Can follow symlinks, or not. Throws IO error on failure. -} +touchBoth :: FilePath -> TimeSpec -> TimeSpec -> Bool -> IO () + +touch :: FilePath -> TimeSpec -> Bool -> IO () +touch file mtime = touchBoth file mtime mtime + +#include +#include +#include +#include + +#ifndef _BSD_SOURCE +#define _BSD_SOURCE +#endif + +#if (defined UTIME_OMIT && defined UTIME_NOW && defined AT_FDCWD && defined AT_SYMLINK_NOFOLLOW) + +at_fdcwd :: CInt +at_fdcwd = #const AT_FDCWD + +at_symlink_nofollow :: CInt +at_symlink_nofollow = #const AT_SYMLINK_NOFOLLOW + +instance Storable TimeSpec where + -- use the larger alignment of the two types in the struct + alignment _ = max sec_alignment nsec_alignment + where + sec_alignment = alignment (undefined::CTime) + nsec_alignment = alignment (undefined::CLong) + sizeOf _ = #{size struct timespec} + peek ptr = do + sec <- #{peek struct timespec, tv_sec} ptr + return $ TimeSpec sec + poke ptr (TimeSpec sec) = do + #{poke struct timespec, tv_sec} ptr sec + #{poke struct timespec, tv_nsec} ptr (0 :: CLong) + +{- While its interface is beastly, utimensat is in recent + POSIX standards, unlike lutimes. -} +foreign import ccall "utimensat" + c_utimensat :: CInt -> CString -> Ptr TimeSpec -> CInt -> IO CInt + +touchBoth file atime mtime follow = + allocaArray 2 $ \ptr -> + withFilePath file $ \f -> do + pokeArray ptr [atime, mtime] + r <- c_utimensat at_fdcwd f ptr flags + when (r /= 0) $ throwErrno "touchBoth" + where + flags + | follow = 0 + | otherwise = at_symlink_nofollow + +#else +#if 0 +{- Using lutimes is needed for BSD. + - + - TODO: test if lutimes is available. May have to do it in configure. + - TODO: TimeSpec uses a CTime, while tv_sec is a CLong. It is implementation + - dependent whether these are the same; need to find a cast that works. + - (Without the cast it works on linux i386, but + - maybe not elsewhere.) + -} + +instance Storable TimeSpec where + alignment _ = alignment (undefined::CLong) + sizeOf _ = #{size struct timeval} + peek ptr = do + sec <- #{peek struct timeval, tv_sec} ptr + return $ TimeSpec sec + poke ptr (TimeSpec sec) = do + #{poke struct timeval, tv_sec} ptr sec + #{poke struct timeval, tv_usec} ptr (0 :: CLong) + +foreign import ccall "utimes" + c_utimes :: CString -> Ptr TimeSpec -> IO CInt +foreign import ccall "lutimes" + c_lutimes :: CString -> Ptr TimeSpec -> IO CInt + +touchBoth file atime mtime follow = + allocaArray 2 $ \ptr -> + withFilePath file $ \f -> do + pokeArray ptr [atime, mtime] + r <- syscall f ptr + when (r /= 0) $ + throwErrno "touchBoth" + where + syscall + | follow = c_lutimes + | otherwise = c_utimes + +#else +#warning "utimensat and lutimes not available; building without symlink timestamp preservation support" +touchBoth _ _ _ _ = return () +#endif +#endif diff --git a/Utility/Url.hs b/Utility/Url.hs new file mode 100644 index 0000000000..508b9eeb44 --- /dev/null +++ b/Utility/Url.hs @@ -0,0 +1,174 @@ +{- Url downloading. + - + - Copyright 2011,2013 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE CPP #-} + +module Utility.Url ( + URLString, + check, + exists, + download, + downloadQuiet +) where + +import Common +import Network.URI +import qualified Network.Browser as Browser +import Network.HTTP +import Data.Either + +import qualified Build.SysConfig + +type URLString = String + +type Headers = [String] + +{- Checks that an url exists and could be successfully downloaded, + - also checking that its size, if available, matches a specified size. -} +check :: URLString -> Headers -> Maybe Integer -> IO Bool +check url headers expected_size = handle <$> exists url headers + where + handle (False, _) = False + handle (True, Nothing) = True + handle (True, s) = expected_size == s + +{- Checks that an url exists and could be successfully downloaded, + - also returning its size if available. + - + - For a file: url, check it directly. + - + - Uses curl otherwise, when available, since curl handles https better + - than does Haskell's Network.Browser. + -} +exists :: URLString -> Headers -> IO (Bool, Maybe Integer) +exists url headers = case parseURIRelaxed url of + Just u + | uriScheme u == "file:" -> do + s <- catchMaybeIO $ getFileStatus (unEscapeString $ uriPath u) + case s of + Just stat -> return (True, Just $ fromIntegral $ fileSize stat) + Nothing -> dne + | otherwise -> if Build.SysConfig.curl + then do + output <- readProcess "curl" curlparams + case lastMaybe (lines output) of + Just ('2':_:_) -> return (True, extractsize output) + _ -> dne + else do + r <- request u headers HEAD + case rspCode r of + (2,_,_) -> return (True, size r) + _ -> return (False, Nothing) + Nothing -> dne + where + dne = return (False, Nothing) + + curlparams = + [ "-s" + , "--head" + , "-L" + , url + , "-w", "%{http_code}" + ] ++ concatMap (\h -> ["-H", h]) headers + + extractsize s = case lastMaybe $ filter ("Content-Length:" `isPrefixOf`) (lines s) of + Just l -> case lastMaybe $ words l of + Just sz -> readish sz + _ -> Nothing + _ -> Nothing + + size = liftM Prelude.read . lookupHeader HdrContentLength . rspHeaders + +{- Used to download large files, such as the contents of keys. + - + - Uses wget or curl program for its progress bar. (Wget has a better one, + - 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. + -} +download :: URLString -> Headers -> [CommandParam] -> FilePath -> IO Bool +download = download' False + +{- No output, even on error. -} +downloadQuiet :: URLString -> Headers -> [CommandParam] -> FilePath -> IO Bool +downloadQuiet = download' True + +download' :: Bool -> URLString -> Headers -> [CommandParam] -> FilePath -> IO Bool +download' quiet 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 ++ quietopt "-q" ++ [Params "-c -O"] + {- Uses the -# progress display, because the normal + - one is very confusing when resuming, showing + - the remainder to download as the whole file, + - and not indicating how much percent was + - downloaded before the resume. -} + curl = go "curl" $ headerparams ++ quietopt "-s" ++ + [Params "-f -L -C - -# -o"] + go cmd opts = boolSystem cmd $ + options++opts++[File file, File url] + quietopt s + | quiet = [Param s] + | otherwise = [] + +{- Uses Network.Browser to make a http request of an url. + - For example, HEAD can be used to check if the url exists, + - or GET used to get the url content (best for small urls). + - + - This does its own redirect following because Browser's is buggy for HEAD + - requests. + - + - Unfortunately, does not handle https, so should only be used + - when curl is not available. + -} +request :: URI -> Headers -> RequestMethod -> IO (Response String) +request url headers requesttype = go 5 url + where + go :: Int -> URI -> IO (Response String) + go 0 _ = error "Too many redirects " + go n u = do + rsp <- Browser.browse $ do + Browser.setErrHandler ignore + Browser.setOutHandler ignore + Browser.setAllowRedirects False + let req = mkRequest requesttype u :: Request_String + snd <$> Browser.request (addheaders req) + case rspCode rsp of + (3,0,x) | x /= 5 -> redir (n - 1) u rsp + _ -> return rsp + addheaders req = setHeaders req (rqHeaders req ++ userheaders) + userheaders = rights $ map parseHeader headers + ignore = const noop + redir n u rsp = case retrieveHeaders HdrLocation rsp of + [] -> return rsp + (Header _ newu:_) -> + case parseURIReference newu of + Nothing -> return rsp + Just newURI -> go n $ +#if defined VERSION_network +#if ! MIN_VERSION_network(2,4,0) +#define WITH_OLD_URI +#endif +#endif +#ifdef WITH_OLD_URI + fromMaybe newURI (newURI `relativeTo` u) +#else + newURI `relativeTo` u +#endif + +{- Allows for spaces and other stuff in urls, properly escaping them. -} +parseURIRelaxed :: URLString -> Maybe URI +parseURIRelaxed = parseURI . escapeURIString isAllowedInURI diff --git a/Utility/UserInfo.hs b/Utility/UserInfo.hs new file mode 100644 index 0000000000..9c3bfd42fa --- /dev/null +++ b/Utility/UserInfo.hs @@ -0,0 +1,55 @@ +{- user info + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE CPP #-} + +module Utility.UserInfo ( + myHomeDir, + myUserName, + myUserGecos, +) where + +import Control.Applicative +import System.PosixCompat + +import Utility.Env + +{- Current user's home directory. + - + - getpwent will fail on LDAP or NIS, so use HOME if set. -} +myHomeDir :: IO FilePath +myHomeDir = myVal env homeDirectory + where +#ifndef mingw32_HOST_OS + env = ["HOME"] +#else + env = ["USERPROFILE", "HOME"] -- HOME is used in Cygwin +#endif + +{- Current user's user name. -} +myUserName :: IO String +myUserName = myVal env userName + where +#ifndef mingw32_HOST_OS + env = ["USER", "LOGNAME"] +#else + env = ["USERNAME", "USER", "LOGNAME"] +#endif + +myUserGecos :: IO String +#ifdef __ANDROID__ +myUserGecos = return "" -- userGecos crashes on Android +#else +myUserGecos = myVal [] userGecos +#endif + +myVal :: [String] -> (UserEntry -> String) -> IO String +myVal envvars extract = maybe (extract <$> getpwent) return =<< check envvars + where + check [] = return Nothing + check (v:vs) = maybe (check vs) (return . Just) =<< getEnv v + getpwent = getUserEntryForID =<< getEffectiveUserID diff --git a/Utility/Verifiable.hs b/Utility/Verifiable.hs new file mode 100644 index 0000000000..4f88cb9f29 --- /dev/null +++ b/Utility/Verifiable.hs @@ -0,0 +1,37 @@ +{- values verified using a shared secret + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Utility.Verifiable where + +import Data.Digest.Pure.SHA +import Data.ByteString.Lazy.UTF8 (fromString) +import qualified Data.ByteString.Lazy as L + +type Secret = L.ByteString +type HMACDigest = String + +{- A value, verifiable using a HMAC digest and a secret. -} +data Verifiable a = Verifiable + { verifiableVal :: a + , verifiableDigest :: HMACDigest + } + deriving (Eq, Read, Show) + +mkVerifiable :: Show a => a -> Secret -> Verifiable a +mkVerifiable a secret = Verifiable a (calcDigest (show a) secret) + +verify :: (Eq a, Show a) => Verifiable a -> Secret -> Bool +verify v secret = v == mkVerifiable (verifiableVal v) secret + +calcDigest :: String -> Secret -> HMACDigest +calcDigest v secret = showDigest $ hmacSha1 secret $ fromString v + +{- for quickcheck -} +prop_verifiable_sane :: String -> String -> Bool +prop_verifiable_sane a s = verify (mkVerifiable a secret) secret + where + secret = fromString s diff --git a/Utility/WebApp.hs b/Utility/WebApp.hs new file mode 100644 index 0000000000..f3c0d3a6b3 --- /dev/null +++ b/Utility/WebApp.hs @@ -0,0 +1,281 @@ +{- Yesod webapp + - + - Copyright 2012 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE OverloadedStrings, CPP, RankNTypes #-} + +module Utility.WebApp where + +import Common +import Utility.Tmp +import Utility.FileMode + +import qualified Yesod +import qualified Network.Wai as Wai +import Network.Wai.Handler.Warp +import Network.Wai.Logger +import Control.Monad.IO.Class +import Network.HTTP.Types +import System.Log.Logger +import qualified Data.CaseInsensitive as CI +import Network.Socket +import Control.Exception +import Crypto.Random +import Data.Digest.Pure.SHA +import qualified Web.ClientSession as CS +import qualified Data.ByteString.Lazy as L +import qualified Data.ByteString.Lazy.UTF8 as L8 +import qualified Data.ByteString as B +import qualified Data.Text as T +import qualified Data.Text.Encoding as TE +import Blaze.ByteString.Builder.Char.Utf8 (fromText) +import Blaze.ByteString.Builder (Builder) +import Data.Monoid +import Control.Arrow ((***)) +import Control.Concurrent +#ifdef __ANDROID__ +import Data.Endian +#endif + +localhost :: HostName +localhost = "localhost" + +{- Builds a command to use to start or open a web browser showing an url. -} +browserProc :: String -> CreateProcess +#ifdef darwin_HOST_OS +browserProc url = proc "open" [url] +#else +#ifdef __ANDROID__ +-- Warning: The `am` command does not work very reliably on Android. +browserProc url = proc "am" + ["start", "-a", "android.intent.action.VIEW", "-d", url] +#else +browserProc url = proc "xdg-open" [url] +#endif +#endif + +{- Binds to a socket on localhost, or possibly a different specified + - hostname or address, and runs a webapp on it. + - + - An IO action can also be run, to do something with the address, + - such as start a web browser to view the webapp. + -} +runWebApp :: Maybe HostName -> Wai.Application -> (SockAddr -> IO ()) -> IO () +runWebApp h app observer = do + sock <- getSocket h + void $ forkIO $ runSettingsSocket webAppSettings sock app + sockaddr <- fixSockAddr <$> getSocketName sock + observer sockaddr + +fixSockAddr :: SockAddr -> SockAddr +#ifdef __ANDROID__ +{- On Android, the port is currently incorrectly returned in network + - byte order, which is wrong on little endian systems. -} +fixSockAddr (SockAddrInet (PortNum port) addr) = SockAddrInet (PortNum $ swapEndian port) addr +#endif +fixSockAddr addr = addr + +webAppSettings :: Settings +webAppSettings = defaultSettings + -- disable buggy sloworis attack prevention code + { settingsTimeout = 30 * 60 + } + +{- Binds to a local socket, or if specified, to a socket on the specified + - hostname or address. Selects any free port, unless the hostname ends with + - ":port" + - + - Prefers to bind to the ipv4 address rather than the ipv6 address + - of localhost, if it's available. + -} +getSocket :: Maybe HostName -> IO Socket +getSocket h = do +#ifdef __ANDROID__ + -- getAddrInfo currently segfaults on Android. + -- The HostName is ignored by this code. + when (isJust h) $ + error "getSocket with HostName not supported on Android" + addr <- inet_addr "127.0.0.1" + sock <- socket AF_INET Stream defaultProtocol + preparesocket sock + bindSocket sock (SockAddrInet aNY_PORT addr) + use sock + where +#else + addrs <- getAddrInfo (Just hints) (Just hostname) port + case (partition (\a -> addrFamily a == AF_INET) addrs) of + (v4addr:_, _) -> go v4addr + (_, v6addr:_) -> go v6addr + _ -> error "unable to bind to a local socket" + where + (hostname, port) = maybe (localhost, Nothing) splitHostPort h + hints = defaultHints { addrSocketType = Stream } + {- Repeated attempts because bind sometimes fails for an + - unknown reason on OSX. -} + go addr = go' 100 addr + go' :: Int -> AddrInfo -> IO Socket + go' 0 _ = error "unable to bind to local socket" + go' n addr = do + r <- tryIO $ bracketOnError (open addr) sClose (useaddr addr) + either (const $ go' (pred n) addr) return r + open addr = socket (addrFamily addr) (addrSocketType addr) (addrProtocol addr) + useaddr addr sock = do + preparesocket sock + bindSocket sock (addrAddress addr) + use sock +#endif + preparesocket sock = setSocketOption sock ReuseAddr 1 + use sock = do + listen sock maxListenQueue + return sock + +{- Splits address:port. For IPv6, use [address]:port. The port is optional. -} +splitHostPort :: String -> (HostName, Maybe ServiceName) +splitHostPort s + | "[" `isPrefixOf` s = let (h, p) = break (== ']') (drop 1 s) + in if "]:" `isPrefixOf` p + then (h, Just $ drop 2 p) + else (h, Nothing) + | otherwise = let (h, p) = separate (== ':') s + in if null p + then (h, Nothing) + else (h, Just p) + +{- Checks if debugging is actually enabled. -} +debugEnabled :: IO Bool +debugEnabled = do + l <- getRootLogger + return $ getLevel l <= Just DEBUG + +{- WAI middleware that logs using System.Log.Logger at debug level. + - + - Recommend only inserting this middleware when debugging is actually + - enabled, as it's not optimised at all. + -} +httpDebugLogger :: Wai.Middleware +httpDebugLogger waiApp req = do + logRequest req + waiApp req + +logRequest :: MonadIO m => Wai.Request -> m () +logRequest req = do + liftIO $ debugM "WebApp" $ unwords + [ showSockAddr $ Wai.remoteHost req + , frombs $ Wai.requestMethod req + , frombs $ Wai.rawPathInfo req + --, show $ Wai.httpVersion req + --, frombs $ lookupRequestField "referer" req + , frombs $ lookupRequestField "user-agent" req + ] + where + frombs v = L8.toString $ L.fromChunks [v] + +lookupRequestField :: CI.CI B.ByteString -> Wai.Request -> B.ByteString +lookupRequestField k req = fromMaybe "" . lookup k $ Wai.requestHeaders req + +{- Rather than storing a session key on disk, use a random key + - that will only be valid for this run of the webapp. -} +#if MIN_VERSION_yesod(1,2,0) +webAppSessionBackend :: Yesod.Yesod y => y -> IO (Maybe Yesod.SessionBackend) +#else +webAppSessionBackend :: Yesod.Yesod y => y -> IO (Maybe (Yesod.SessionBackend y)) +#endif +webAppSessionBackend _ = do + g <- newGenIO :: IO SystemRandom + case genBytes 96 g of + 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 -> use key + where + timeout = 120 * 60 -- 120 minutes + use key = +#if MIN_VERSION_yesod(1,2,0) + Just . Yesod.clientSessionBackend key . fst + <$> Yesod.clientSessionDateCacher timeout +#else +#if MIN_VERSION_yesod(1,1,7) + Just . Yesod.clientSessionBackend2 key . fst + <$> Yesod.clientSessionDateCacher timeout +#else + return $ Just $ + Yesod.clientSessionBackend key timeout +#endif +#endif + +{- Generates a random sha512 string, suitable to be used for an + - authentication secret. -} +genRandomToken :: IO String +genRandomToken = do + g <- newGenIO :: IO SystemRandom + return $ + case genBytes 512 g of + Left e -> error $ "failed to generate secret token: " ++ show e + Right (s, _) -> showDigest $ sha512 $ L.fromChunks [s] + +{- A Yesod isAuthorized method, which checks the auth cgi parameter + - against a token extracted from the Yesod application. + - + - Note that the usual Yesod error page is bypassed on error, to avoid + - possibly leaking the auth token in urls on that page! + -} +#if MIN_VERSION_yesod(1,2,0) +checkAuthToken :: (Monad m, Yesod.MonadHandler m) => (Yesod.HandlerSite m -> T.Text) -> m Yesod.AuthResult +#else +checkAuthToken :: forall t sub. (t -> T.Text) -> Yesod.GHandler sub t Yesod.AuthResult +#endif +checkAuthToken extractToken = do + webapp <- Yesod.getYesod + req <- Yesod.getRequest + let params = Yesod.reqGetParams req + if lookup "auth" params == Just (extractToken webapp) + then return Yesod.Authorized + else Yesod.sendResponseStatus unauthorized401 () + +{- A Yesod joinPath method, which adds an auth cgi parameter to every + - url matching a predicate, containing a token extracted from the + - Yesod application. + - + - A typical predicate would exclude files under /static. + -} +insertAuthToken :: forall y. (y -> T.Text) + -> ([T.Text] -> Bool) + -> y + -> T.Text + -> [T.Text] + -> [(T.Text, T.Text)] + -> Builder +insertAuthToken extractToken predicate webapp root pathbits params = + fromText root `mappend` encodePath pathbits' encodedparams + where + pathbits' = if null pathbits then [T.empty] else pathbits + encodedparams = map (TE.encodeUtf8 *** go) params' + go "" = Nothing + go x = Just $ TE.encodeUtf8 x + authparam = (T.pack "auth", extractToken webapp) + params' + | predicate pathbits = authparam:params + | otherwise = params + +{- Creates a html shim file that's used to redirect into the webapp, + - to avoid exposing the secret token when launching the web browser. -} +writeHtmlShim :: String -> String -> FilePath -> IO () +writeHtmlShim title url file = viaTmp writeFileProtected file $ genHtmlShim title url + +{- TODO: generate this static file using Yesod. -} +genHtmlShim :: String -> String -> String +genHtmlShim title url = unlines + [ "" + , "" + , ""++ title ++ "" + , "" + , "" + , "

" + , "" ++ title ++ "" + , "

" + , "" + , "" + ] diff --git a/Utility/Yesod.hs b/Utility/Yesod.hs new file mode 100644 index 0000000000..00424d1914 --- /dev/null +++ b/Utility/Yesod.hs @@ -0,0 +1,71 @@ +{- Yesod stuff, that's typically found in the scaffolded site. + - + - Also a bit of a compatability layer to make it easier to support yesod + - 1.1 and 1.2 in the same code base. + - + - Copyright 2012, 2013 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE CPP, RankNTypes, FlexibleContexts #-} + +module Utility.Yesod + ( module Y + , liftH +#ifndef __ANDROID__ + , widgetFile + , hamletTemplate +#endif +#if ! MIN_VERSION_yesod(1,2,0) + , giveUrlRenderer + , Html +#endif + ) where + +#if MIN_VERSION_yesod(1,2,0) +import Yesod as Y +#else +import Yesod as Y hiding (Html) +#endif +#ifndef __ANDROID__ +import Yesod.Default.Util +import Language.Haskell.TH.Syntax (Q, Exp) +#if MIN_VERSION_yesod_default(1,1,0) +import Data.Default (def) +import Text.Hamlet hiding (Html) +#endif +#endif + +#ifndef __ANDROID__ +widgetFile :: String -> Q Exp +#if ! MIN_VERSION_yesod_default(1,1,0) +widgetFile = widgetFileNoReload +#else +widgetFile = widgetFileNoReload $ def + { wfsHamletSettings = defaultHamletSettings + { hamletNewlines = AlwaysNewlines + } + } +#endif + +hamletTemplate :: FilePath -> FilePath +hamletTemplate f = globFile "hamlet" f +#endif + +{- Lift Handler to Widget -} +#if MIN_VERSION_yesod(1,2,0) +liftH :: Monad m => HandlerT site m a -> WidgetT site m a +liftH = handlerToWidget +#else +liftH :: MonadLift base m => base a -> m a +liftH = lift +#endif + +{- Misc new names for stuff. -} +#if ! MIN_VERSION_yesod(1,2,0) +giveUrlRenderer :: forall master sub. HtmlUrl (Route master) -> GHandler sub master RepHtml +giveUrlRenderer = hamletToRepHtml + +type Html = RepHtml +#endif diff --git a/Utility/libdiskfree.c b/Utility/libdiskfree.c new file mode 100644 index 0000000000..d2843ed203 --- /dev/null +++ b/Utility/libdiskfree.c @@ -0,0 +1,73 @@ +/* disk free space checking, C mini-library + * + * Copyright 2012 Joey Hess + * + * Licensed under the GNU GPL version 3 or higher. + */ + +/* Include appropriate headers for the OS, and define what will be used to + * check the free space. */ +#if defined(__APPLE__) +# include +# include +/* In newer OSX versions, statfs64 is deprecated, in favor of statfs, + * which is 64 bit only with a build option -- but statfs64 still works, + * and this keeps older OSX also supported. */ +# define STATCALL statfs64 +# define STATSTRUCT statfs64 +#else +#if defined (__FreeBSD__) +# include +# include +# define STATCALL statfs /* statfs64 not yet tested on a real FreeBSD machine */ +# define STATSTRUCT statfs +#else +#if defined __ANDROID__ +# warning free space checking code not available for Android +# define UNKNOWN +#else +#if defined (__linux__) || defined (__FreeBSD_kernel__) +/* Linux or Debian kFreeBSD */ +/* This is a POSIX standard, so might also work elsewhere too. */ +# include +# define STATCALL statvfs +# define STATSTRUCT statvfs +#else +# warning free space checking code not available for this OS +# define UNKNOWN +#endif +#endif +#endif +#endif + +#include +#include + +/* Checks the amount of disk that is available to regular (non-root) users. + * (If there's an error, or this is not supported, + * returns 0 and sets errno to nonzero.) + */ +unsigned long long int diskfree(const char *path) { +#ifdef UNKNOWN + errno = 1; + return 0; +#else + unsigned long long int available, blocksize; + struct STATSTRUCT buf; + + if (STATCALL(path, &buf) != 0) + return 0; /* errno is set */ + else + errno = 0; + + available = buf.f_bavail; + blocksize = buf.f_bsize; + return available * blocksize; +#endif +} + +/* +main () { + printf("%lli\n", diskfree(".")); +} +*/ diff --git a/Utility/libdiskfree.h b/Utility/libdiskfree.h new file mode 100644 index 0000000000..e5b84754fe --- /dev/null +++ b/Utility/libdiskfree.h @@ -0,0 +1 @@ +unsigned long long int diskfree(const char *path); diff --git a/Utility/libkqueue.c b/Utility/libkqueue.c new file mode 100644 index 0000000000..a87f65102b --- /dev/null +++ b/Utility/libkqueue.c @@ -0,0 +1,74 @@ +/* kqueue interface, C mini-library + * + * Copyright 2012 Joey Hess + * + * Licensed under the GNU GPL version 3 or higher. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* The specified fds are added to the set of fds being watched for changes. + * Fds passed to prior calls still take effect, so it's most efficient to + * not pass the same fds repeatedly. + * + * Returns the fd that changed, or -1 on error. + */ +signed int helper(const int kq, const int fdcnt, const int *fdlist, int nodelay) { + int i, nev; + struct kevent evlist[1]; + struct kevent chlist[fdcnt]; + struct timespec avoiddelay = {0, 0}; + struct timespec *timeout = nodelay ? &avoiddelay : NULL; + + for (i = 0; i < fdcnt; i++) { + EV_SET(&chlist[i], fdlist[i], EVFILT_VNODE, + EV_ADD | EV_ENABLE | EV_CLEAR, + NOTE_WRITE, + 0, 0); + } + + nev = kevent(kq, chlist, fdcnt, evlist, 1, timeout); + if (nev == 1) + return evlist[0].ident; + else + return -1; +} + +/* Initializes a new, empty kqueue. */ +int init_kqueue() { + int kq; + if ((kq = kqueue()) == -1) { + perror("kqueue"); + exit(1); + } + return kq; +} + +/* Adds fds to the set that should be watched. */ +void addfds_kqueue(const int kq, const int fdcnt, const int *fdlist) { + helper(kq, fdcnt, fdlist, 1); +} + +/* Waits for a change event on a kqueue. */ +signed int waitchange_kqueue(const int kq) { + return helper(kq, 0, NULL, 0); +} + +/* +main () { + int list[1]; + int kq; + list[0]=open(".", O_RDONLY); + kq = init_kqueue(); + addfds_kqueue(kq, 1, list) + printf("change: %i\n", waitchange_kqueue(kq)); +} +*/ diff --git a/Utility/libkqueue.h b/Utility/libkqueue.h new file mode 100644 index 0000000000..692b47f14e --- /dev/null +++ b/Utility/libkqueue.h @@ -0,0 +1,3 @@ +int init_kqueue(); +void addfds_kqueue(const int kq, const int fdcnt, const int *fdlist); +signed int waitchange_kqueue(const int kq); diff --git a/Utility/libmounts.c b/Utility/libmounts.c new file mode 100644 index 0000000000..8669f33ea9 --- /dev/null +++ b/Utility/libmounts.c @@ -0,0 +1,103 @@ +/* mounted filesystems, C mini-library + * + * Copyright (c) 1980, 1989, 1993, 1994 + * The Regents of the University of California. All rights reserved. + * Copyright (c) 2001 + * David Rufino + * Copyright 2012 + * Joey Hess + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the University nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +#include "libmounts.h" + +#ifdef GETMNTENT +/* direct passthrough the getmntent */ +FILE *mounts_start (void) { + return setmntent("/etc/mtab", "r"); +} +int mounts_end (FILE *fp) { + return endmntent(fp); +} +struct mntent *mounts_next (FILE *fp) { + return getmntent(fp); +} +#endif + +#ifdef GETMNTINFO +/* getmntent emulation using getmntinfo */ +FILE *mounts_start (void) { + return ((FILE *)0x1); /* dummy non-NULL FILE pointer, not used */ +} +int mounts_end (FILE *fp) { + return 1; +} + +static struct mntent _mntent; + +static struct mntent *statfs_to_mntent (struct statfs *mntbuf) { + _mntent.mnt_fsname = mntbuf->f_mntfromname; + _mntent.mnt_dir = mntbuf->f_mntonname; + _mntent.mnt_type = mntbuf->f_fstypename; + + _mntent.mnt_opts = '\0'; + _mntent.mnt_freq = 0; + _mntent.mnt_passno = 0; + + return (&_mntent); +} + +static int pos = -1; +static int mntsize = -1; +struct statfs *mntbuf = NULL; + +struct mntent *mounts_next (FILE *fp) { + + if (pos == -1 || mntsize == -1) + mntsize = getmntinfo(&mntbuf, MNT_NOWAIT); + ++pos; + if (pos == mntsize) { + pos = mntsize = -1; + mntbuf = NULL; + return NULL; + } + + return (statfs_to_mntent(&mntbuf[pos])); +} +#endif + +#ifdef UNKNOWN +/* dummy, do-nothing version */ +FILE *mounts_start (void) { + return ((FILE *)0x1); +} +int mounts_end (FILE *fp) { + return 1; +} +struct mntent *mounts_next (FILE *fp) { + return NULL; +} +#endif diff --git a/Utility/libmounts.h b/Utility/libmounts.h new file mode 100644 index 0000000000..24df55f310 --- /dev/null +++ b/Utility/libmounts.h @@ -0,0 +1,38 @@ +/* Include appropriate headers for the OS, and define what will be used. */ +#if defined (__FreeBSD__) || defined (__APPLE__) +# include +# include +# include +# define GETMNTINFO +#else +#if defined __ANDROID__ +/* Android is handled by the Haskell code, not here. */ +# define UNKNOWN +#else +#if defined (__linux__) || defined (__FreeBSD_kernel__) +/* Linux or Debian kFreeBSD */ +#include +# define GETMNTENT +#else +# warning mounts listing code not available for this OS +# define UNKNOWN +#endif +#endif +#endif + +#include + +#ifndef GETMNTENT +struct mntent { + char *mnt_fsname; + char *mnt_dir; + char *mnt_type; + char *mnt_opts; /* not filled in */ + int mnt_freq; /* not filled in */ + int mnt_passno; /* not filled in */ +}; +#endif + +FILE *mounts_start (void); +int mounts_end (FILE *fp); +struct mntent *mounts_next (FILE *fp); diff --git a/configure.hs b/configure.hs new file mode 100644 index 0000000000..15833e62a7 --- /dev/null +++ b/configure.hs @@ -0,0 +1,6 @@ +{- configure program -} + +import Build.Configure + +main :: IO () +main = run tests diff --git a/debian/NEWS b/debian/NEWS new file mode 100644 index 0000000000..1c95146912 --- /dev/null +++ b/debian/NEWS @@ -0,0 +1,36 @@ +git-annex (3.20120123) unstable; urgency=low + + There was a bug in the handling of directory special remotes that + could cause partial file contents to be stored in them. If you use + a directory special remote, you should fsck it, to avoid potential + data loss. + + Example: git annex fsck --from mydirectory + + -- Joey Hess Thu, 19 Jan 2012 15:24:23 -0400 + +git-annex (3.20110624) experimental; urgency=low + + There has been another change to the git-annex data store. + Use `git annex upgrade` to migrate your repositories to the new + layout. See or + /usr/share/doc/git-annex/html/upgrades.html + + The significant change this time is that the .git-annex/ directory + is gone; instead there is a git-annex branch that is automatically + maintained by git-annex, and encapsulates all its state nicely out + of your way. + + You should make sure you include the git-annex branch when + git pushing and pulling. + + -- Joey Hess Tue, 21 Jun 2011 20:18:00 -0400 + +git-annex (0.20110316) experimental; urgency=low + + This version reorganises the layout of git-annex's files in your repository. + There is an upgrade process to convert a repository from the old git-annex + to this version. See or + /usr/share/doc/git-annex/html/upgrades.html + + -- Joey Hess Wed, 16 Mar 2011 15:49:15 -0400 diff --git a/debian/changelog b/debian/changelog new file mode 100644 index 0000000000..4e498a0a1f --- /dev/null +++ b/debian/changelog @@ -0,0 +1,2218 @@ +git-annex (4.20130815) unstable; urgency=low + + * assistant, watcher: .gitignore files and other git ignores are now + honored, when git 1.8.4 or newer is installed. + (Thanks, Adam Spiers, for getting the necessary support into git for this.) + * importfeed: Ignores transient problems with feeds. Only exits nonzero + when a feed has repeatedly had a problems for at least 1 day. + * importfeed: Fix handling of dots in extensions. + * Windows: Added support for encrypted special remotes. + * Windows: Fixed permissions problem that prevented removing files + from directory special remote. Directory special remotes now fully usable. + + -- Joey Hess Thu, 15 Aug 2013 10:14:33 +0200 + +git-annex (4.20130802) unstable; urgency=low + + * dropunused behavior change: Now refuses to drop the last copy of a + file, unless you use the --force. + This was the last place in git-annex that could remove data referred + to by the git history, without being forced. + Like drop, dropunused checks remotes, and honors the global + annex.numcopies setting. (However, .gitattributes settings cannot + apply to unused files.) + * Fix inverted logic in last release's fix for data loss bug, + that caused git-annex sync on FAT or other crippled filesystems to add + symlink standin files to the annex. + * importfeed can be used to import files from podcast feeds. + * webapp: When setting up a dedicated ssh key to access the annex + on a host, set IdentitiesOnly to prevent the ssh-agent from forcing + use of a different ssh key. That could result in unncessary password + prompts, or prevent git-annex-shell from being run on the remote host. + * webapp: Improve handling of remotes whose setup has stalled. + * Add status message to XMPP presence tag, to identify to others that + the client is a git-annex client. Closes: #717652 + * webapp: When creating a repository on a removable drive, set + core.fsyncobjectfiles, to help prevent data loss when the drive is yanked. + * Always build with -threaded, to avoid a deadlock when communicating with + gpg. + * unused: No longer shows as unused tmp files that are actively being + transferred. + * assistant: Fix NetWatcher to not sync with remotes that have + remote..annex-sync set to false. + * assistant: Fix deadlock that could occur when adding a lot of files + at once in indirect mode. + * assistant: Fix bug that caused it to stall when adding a very large + number of files at once (around 5 thousand). + * OSX: Make git-annex-webapp run in the background, so that the app icon + can be clicked on the open a new webapp when the assistant is already + running. + * Improve test suite on Windows; now tests git annex sync. + * Fix a few bugs involving filenames that are at or near the filesystem's + maximum filename length limit. + * find: Avoid polluting stdout with progress messages. Closes: #718186 + * Escape ':' in file/directory names to avoid it being treated + as a pathspec by some git commands. Closes: #718185 + * Slow and ugly work around for bug #718517 in git 1.8.4~rc0, which broke + git-cat-file --batch for filenames containing spaces. + (Will be reverted after next git pre-release fixes the problem.) + + -- Joey Hess Fri, 02 Aug 2013 11:35:16 -0400 + +git-annex (4.20130723) unstable; urgency=low + + * Fix data loss bug when adding an (uncompressed) tarball of a + git-annex repository, or other file that begins with something + that can be mistaken for a git-annex link. Closes: #717456 + * New improved version of the git-annex logo, contributed by + John Lawrence. + * Rsync.net have committed to support git-annex and offer a special + discounted rate for git-annex users. Updated the webapp to reflect this. + http://www.rsync.net/products/git-annex-pricing.html + * Install XDG desktop icon files. + * Support unannex and uninit in direct mode. + * Support import in direct mode. + * webapp: Better display of added files. + * fix: Preserve the original mtime of fixed symlinks. + * uninit: Preserve .git/annex/objects at the end, if it still + has content, so that old versions of files and deleted files + are not deleted. Print a message with some suggested actions. + * When a transfer is already being run by another process, + proceed on to the next file, rather than dying. + * Fix checking when content is present in a non-bare repository + accessed via http. + * Display byte sizes with more precision. + * watcher: Fixed a crash that could occur when a directory was renamed + or deleted before it could be scanned. + * watcher: Partially worked around a bug in hinotify, no longer crashes + if hinotify cannot process a directory (but can't detect changes in it) + * directory special remote: Fix checking that there is enough disk space + to hold an object, was broken when using encryption. + * webapp: Differentiate between creating a new S3/Glacier/WebDav remote, + and initializing an existing remote. When creating a new remote, avoid + conflicts with other existing (or deleted) remotes with the same name. + * When an XMPP server has SRV records, try them, but don't then fall + back to the regular host if they all fail. + * For long hostnames, use a hash of the hostname to generate the socket + file for ssh connection caching. + + -- Joey Hess Tue, 23 Jul 2013 10:46:05 -0400 + +git-annex (4.20130709) unstable; urgency=low + + * --all: New switch that makes git-annex operate on all data stored + in the git annex, including old versions of files. Supported by + fsck, get, move, copy. + * --unused: New switch that makes git-annex operate on all data found + by the last run of git annex unused. Supported by fsck, move, copy. + * get, move, copy: Can now be run in a bare repository, + like fsck already could. --all is enabled automatically in this case. + * merge: Now also merges synced/master or similar branches, which + makes it useful to put in a post-receive hook to make a repository + automatically update its working copy when git annex sync or the assistant + sync with it. + * webapp: Fix ssh setup with nonstandard port, broken in last release. + * init: Detect systems on which git commit fails due to not being able to + determine the FQDN, and put in a workaround so committing to the git-annex + branch works. + * addurl --pathdepth: Fix failure when the pathdepth specified is deeper + than the urls's path. + * Windows: Look for .exe extension when searching for a command in path. + * Pass -f to curl when downloading a file with it, so it propigates failure. + * Windows: Fix url to object when using a http remote. + * webapp: Fix authorized_keys line added when setting up a rsync remote + on a server that also supports git-annex, to not force running + git-annex-shell. + * OSX Mountain Lion: Fixed gpg bundled in dmg to not fail due to a missing + gpg-agent. + * Android: gpg is built without --enable-minimal, so it interoperates + better with other gpg builds that may default to using other algorithms + for encryption. + * dropunused, addunused: Complain when asked to operate on a number that + does not correspond to any unused key. + * fsck: Don't claim to fix direct mode when run on a symlink whose content + is not present. + * Make --numcopies override annex.numcopies set in .gitattributes. + + -- Joey Hess Tue, 09 Jul 2013 13:55:39 -0400 + +git-annex (4.20130627) unstable; urgency=low + + * assistant --autostart: Automatically ionices the daemons it starts. + * assistant: Daily sanity check thread is run niced. + * bup: Handle /~/ in bup remote paths. + Thanks, Oliver Matthews + * fsck: Ensures that direct mode is used for files when it's enabled. + * webapp: Fix bug when setting up a remote ssh repo repeatedly on the same + server. + * webapp: Ensure that ssh keys generated for different directories + on a server are always different. + * webapp: Fix bug setting up ssh repo if the user enters "~/" at the start + of the path. + * assistant: Fix bug that prevented adding files written by gnucash, + and more generally support adding hard links to files. However, + other operations on hard links are still unsupported. + * webapp: Fix bug that caused the webapp to hang when built with yesod 1.2. + + -- Joey Hess Thu, 27 Jun 2013 14:21:55 -0400 + +git-annex (4.20130621) unstable; urgency=low + + * Supports indirect mode on encfs in paranoia mode, and other + filesystems that do not support hard links, but do support + symlinks and other POSIX filesystem features. + * Android: Add .thumbnails to .gitignore when setting up a camera + repository. + * Android: Make the "Open webapp" menu item open the just created + repository when a new repo is made. + * webapp: When the user switches to display a different repository, + that repository becomes the default repository to be displayed next time + the webapp gets started. + * glacier: Better handling of the glacier inventory, which avoids + duplicate uploads to the same glacier repository by `git annex copy`. + * Direct mode: No longer temporarily remove write permission bit of files + when adding them. + * sync: Better support for bare git remotes. Now pushes directly to the + master branch on such a remote, instead of to synced/master. This + makes it easier to clone from a bare git remote that has been populated + with git annex sync or by the assistant. + * Android: Fix use of cp command to not try to use features present + only on build system. + * Windows: Fix hang when adding several files at once. + * assistant: In direct mode, objects are now only dropped when all + associated files are unwanted. This avoids a repreated drop/get loop + of a file that has a copy in an archive directory, and a copy not in an + archive directory. (Indirect mode still has some buggy behavior in this + area, since it does not keep track of associated files.) + Closes: #712060 + * status: No longer shows dead repositories. + * annex.debug can now be set to enable debug logging by default. + The webapp's debugging check box does this. + * fsck: Avoid getting confused by Windows path separators + * Windows: Multiple bug fixes, including fixing the data written to the + git-annex branch. + * Windows: The test suite now passes on Windows (a few broken parts are + disabled). + * assistant: On Linux, the expensive transfer scan is run niced. + * Enable assistant and WebDAV support on powerpc and sparc architectures, + which now have the necessary dependencies built. + + -- Joey Hess Fri, 21 Jun 2013 10:18:41 -0400 + +git-annex (4.20130601) unstable; urgency=medium + + * XMPP: Git push over xmpp made much more robust. + * XMPP: Avoid redundant and unncessary pushes. Note that this breaks + compatibility with previous versions of git-annex, which will refuse + to accept any XMPP pushes from this version. + * XMPP: Send pings and use them to detect when contact with the server + is lost. + * hook special remote: Added combined hook program support. + * Android app: Avoid using hard links to app's lib directory, which + is sometimes on a different filesystem than the data directory. + * Fix bug in parsing of parens in some preferred content expressions. + This fixes the behavior of the manual mode group. + * assistant: Work around git-cat-file's not reloading the index after files + are staged. + * Improve error handling when getting uuid of http remotes to auto-ignore, + like with ssh remotes. + * content: New command line way to view and configure a repository's + preferred content settings. + * sync: Fix double merge conflict resolution handling. + * XMPP: Fix a file descriptor leak. + * Android: Added an "Open WebApp" item to the terminal's menu. + * Android: Work around Android devices where the `am` command doesn't work. + * Can now restart certain long-running git processes if they crash, and + continue working. + + -- Joey Hess Sat, 01 Jun 2013 19:16:04 -0400 + +git-annex (4.20130521) unstable; urgency=low + + * Sanitize debian changelog version before putting it into cabal file. + Closes: #708619 + * Switch to MonadCatchIO-transformers for better handling of state while + catching exceptions. + * Fix a zombie that could result when running a process like gpg to + read and write to it. + * Allow building with gpg2. + * Disable building with the haskell threaded runtime when the webapp + is not built. This may fix builds on mips, s390x and sparc, which are + failing to link -lHSrts_thr + * Temporarily build without webapp on kfreebsd-i386, until yesod is + installable there again. + * Direct mode bug fix: After a conflicted merge was automatically resolved, + the content of a file that was already present could incorrectly + be replaced with a symlink. + * Fix a bug in the git-annex branch handling code that could + cause info from a remote to not be merged and take effect immediately. + * Direct mode is now fully tested by the test suite. + * Detect bad content in ~/.config/git-annex/program and look in PATH instead. + * OSX: Fixed gpg included in dmg. + * Linux standalone: Back to being built with glibc 2.13 for maximum + portability. + + -- Joey Hess Tue, 21 May 2013 13:10:26 -0400 + +git-annex (4.20130516) unstable; urgency=low + + * Android: The webapp is ported and working. + * Windows: There is a very rough Windows port. Do not trust it with + important data. + * git-annex-shell: Ensure that received files can be read. Files + transferred from some Android devices may have very broken permissions + as received. + * direct mode: Direct mode commands now work on files staged in the index, + they do not need to be committed to git. + * Temporarily add an upper bound to the version of yesod that can be built + with, since yesod 1.2 has a great many changes that will require extensive + work on the webapp. + * Disable building with the haskell threaded runtime when the assistant + is not built. This may fix builds on s390x and sparc, which are failing + to link -lHSrts_thr + * Avoid depending on regex-tdfa on mips, mipsel, and s390, where it fails + to build. + * direct: Fix a bug that could cause some files to be left in indirect mode. + * When initializing a directory special remote with a relative path, + the path is made absolute. + * SHA: Add a runtime sanity check that sha commands output something + that appears to be a real sha. + * configure: Better checking that sha commands output in the desired format. + * rsync special remotes: When sending from a crippled filesystem, use + the destination's default file permissions, as the local ones can + be arbitrarily broken. (Ie, ----rwxr-x for files on Android) + * migrate: Detect if a file gets corrupted while it's being migrated. + * Debian: Add a menu file. + + -- Joey Hess Thu, 16 May 2013 11:03:35 -0400 + +git-annex (4.20130501) unstable; urgency=low + + * sync, assistant: Behavior changes: Sync with remotes that have + annex-ignore set, so that git remotes on servers without git-annex + installed can be used to keep clients' git repos in sync. + * assistant: Work around misfeature in git 1.8.2 that makes + `git commit --alow-empty -m ""` run an editor. + * sync: Bug fix, avoid adding to the annex the + dummy symlinks used on crippled filesystems. + * Add public repository group. + (And inpreferreddir to preferred content expressions.) + * webapp: Can now set up Internet Archive repositories. + * S3: Dropping content from the Internet Archive doesn't work, but + their API indicates it does. Always refuse to drop from there. + * Automatically register public urls for files uploaded to the + Internet Archive. + * To enable an existing special remote, the new enableremote command + must be used. The initremote command now is used only to create + new special remotes. + * initremote: If two existing remotes have the same name, + prefer the one with a higher trust level. + * assistant: Improved XMPP protocol to better support multiple repositories + using the same XMPP account. Fixes bad behavior when sharing with a friend + when you or the friend have multiple reposotories on an XMPP account. + Note that XMPP pairing with your own devices still pairs with all + repositories using your XMPP account. + * assistant: Fix bug that could cause incoming pushes to not get + merged into the local tree. Particularly affected XMPP pushes. + * webapp: Display some additional information about a repository on + its edit page. + * webapp: Install FDO desktop menu file when started in standalone mode. + * webapp: Don't default to making repository in cwd when started + from within a directory containing a git-annex file (eg, standalone + tarball directory). + * Detect systems that have no user name set in GECOS, and also + don't have user.name set in git config, and put in a workaround + so that commits to the git-annex branch (and the assistant) + will still succeed despite git not liking the system configuration. + * webapp: When told to add a git repository on a remote server, and + the repository already exists as a non-bare repository, use it, + rather than initializing a bare repository in the same directory. + * direct, indirect: Refuse to do anything when the assistant + or git-annex watch daemon is running. + * assistant: When built with git before 1.8.0, use `git remote rm` + to delete a remote. Newer git uses `git remote remove`. + * rmurl: New command, removes one of the recorded urls for a file. + * Detect when the remote is broken like bitbucket is, and exits 0 when + it fails to run git-annex-shell. + * assistant: Several improvements to performance and behavior when + performing bulk adds of a large number of files (tens to hundreds + of thousands). + * assistant: Sanitize XMPP presence information logged for debugging. + * webapp: Now automatically fills in any creds used by an existing remote + when creating a new remote of the same type. Done for Internet Archive, + S3, Glacier, and Box.com remotes. + * Store an annex-uuid file in the bucket when setting up a new S3 remote. + * Support building with DAV 0.4. + + -- Joey Hess Wed, 01 May 2013 01:42:46 -0400 + +git-annex (4.20130417) unstable; urgency=low + + * initremote: Generates encryption keys with high quality entropy. + This can be disabled using --fast to get the old behavior. + The assistant still uses low-quality entropy when creating encrypted + remotes, to avoid delays. (Thanks, guilhem for the patch.) + * Bugfix: Direct mode no longer repeatedly checksums duplicated files. + * assistant: Work around horrible, terrible, very bad behavior of + gnome-keyring, by not storing special-purpose ssh keys in ~/.ssh/*.pub. + Apparently gnome-keyring apparently will load and indiscriminately use + such keys in some cases, even if they are not using any of the standard + ssh key names. Instead store the keys in ~/.ssh/annex/, + which gnome-keyring will not check. + * addurl: Bugfix: Did not properly add file in direct mode. + * assistant: Bug fix to avoid annexing the files that git uses + to stand in for symlinks on FAT and other filesystem not supporting + symlinks. + * Adjust preferred content expressions so that content in archive + directories is preferred until it has reached an archive or smallarchive + repository. + * webapp: New --listen= option allows running the webapp on one computer + and connecting to it from another. (Note: Does not yet use HTTPS.) + * Added annex.web-download-command setting. + * Added per-remote annex-rsync-transport option. (guilhem again) + * Ssh connection caching is now also used by rsync special remotes. + (guilhem yet again) + * The version number is now derived from git, unless built with + VERSION_FROM_CHANGELOG. + * assistant: Stop any transfers the assistant initiated on shutdown. + * assistant: Added sequence numbers to XMPP git push packets. (Not yet used.) + * addurl: Register transfer so the webapp can see it. + * addurl: Automatically retry downloads that fail, as long as some + additional content was downloaded. + * webapp: Much improved progress bar display for downloads from encrypted + remotes. + * Avoid using runghc, as that needs ghci. + * webapp: When a repository's group is changed, rescan for transfers. + * webapp: Added animations. + * webapp: Include the repository directory in the mangled hostname and + ssh key name, so that a locked down ssh key for one repository is not + re-used when setting up additional repositories on the same server. + * Fall back to internal url downloader when built without curl. + * fsck: Check content of direct mode files (only when the inode cache + thinks they are unmodified). + + -- Joey Hess Wed, 17 Apr 2013 09:07:38 -0400 + +git-annex (4.20130405) unstable; urgency=low + + * Group subcommands into sections in usage. Closes: #703797 + * Per-command usage messages. + * webapp: Fix a race that sometimes caused alerts or other notifications + to be missed if they occurred while a page was loading. + * webapp: Progess bar fixes for many types of special remotes. + * Build debian package without using cabal, which writes to HOME. + Closes: #704205 + * webapp: Run ssh server probes in a way that will work when the + login shell is a monstrosity that should have died 25 years ago, + such as csh. + * New annex.largefiles setting, which configures which files + `git annex add` and the assistant add to the annex. + * assistant: Check small files into git directly. + * Remotes can be configured to use other MAC algorithms than HMACSHA1 + to encrypt filenames. + Thanks, guilhem for the patch. + * git-annex-shell: Passes rsync --bwlimit options on rsync. + Thanks, guilhem for the patch. + * webapp: Added UI to delete repositories. Closes: #689847 + * Adjust built-in preferred content expressions to make most types + of repositories want content that is only located on untrusted, dead, + and unwanted repositories. + * drop --auto: Fix bug that prevented dropping files from untrusted + repositories. + * assistant: Fix bug that could cause direct mode files to be unstaged + from git. + * Update working tree files fully atomically. + * webapp: Improved transfer queue management. + * init: Probe whether the filesystem supports fifos, and if not, + disable ssh connection caching. + * Use lower case hash directories for storing files on crippled filesystems, + same as is already done for bare repositories. + + -- Joey Hess Fri, 05 Apr 2013 10:42:18 -0400 + +git-annex (4.20130323) unstable; urgency=low + + * webapp: Repository list is now included in the dashboard, and other + UI tweaks. + * webapp: Improved UI for pairing your own devices together using XMPP. + * webapp: Display an alert when there are XMPP remotes, and a cloud + transfer repository needs to be configured. + * Add incrementalbackup repository group. + * webapp: Encourage user to install git-annex on a server when adding + a ssh server, rather than just funneling them through to rsync. + * xmpp: --debug now enables a sanitized dump of the XMPP protocol + * xmpp: Try harder to detect presence of clients when there's a git push + to send. + * xmpp: Re-enable XA flag, since disabling it did not turn out to help + with the problems Google Talk has with not always sending presence + messages to clients. + * map: Combine duplicate repositories, for a nicer looking map. + * Fix several bugs caused by a bad Ord instance for Remote. + * webapp: Switch all forms to POST. + * assistant: Avoid syncing with annex-ignored remotes when reconnecting + to the network, or connecting a drive. + * assistant: Fix OSX bug that prevented committing changed files to a + repository when in indirect mode. + * webapp: Improved alerts displayed when syncing with remotes, and + when syncing with a remote fails. + * webapp: Force wrap long filenames in transfer display. + * assistant: The ConfigMonitor left one zombie behind each time + it checked for changes, now fixed. + * get, copy, move: Display an error message when an identical transfer + is already in progress, rather than failing with no indication why. + * assistant: Several optimisations to file transfers. + * OSX app and standalone Linux tarball now both support being added to + PATH; no need to use runshell to start git-annex. + * webapp: When adding a removable drive, you can now specify the + directory inside it to use. + * webapp: Confirm whether user wants to combine repositories when + adding a removable drive that already has a repository on it. + + -- Joey Hess Fri, 22 Mar 2013 18:54:05 -0400 + +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..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 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. + * Should now fully support git repositories with core.symlinks=false; + always using git's pseudosymlink files in such repositories. + * webapp: Allow creating repositories on filesystems that lack support for + symlinks. + * webapp: Can now add a new local repository, and make it sync with + the main local repository. + * Android: Bundle now includes openssh. + * Android: Support ssh connection caching. + * Android: Assistant is fully working. (But no webapp yet.) + * Direct mode: Support filesystems like FAT which can change their inodes + each time they are mounted. + * Direct mode: Fix support for adding a modified file. + * Avoid passing -p to rsync, to interoperate with crippled filesystems. + Closes: #700282 + * Additional GIT_DIR support bugfixes. May actually work now. + * webapp: Display any error message from git init if it fails to create + a repository. + * Fix a reversion in matching globs introduced in the last release, + where "*" did not match files inside subdirectories. No longer uses + the Glob library. + * copy: Update location log when no copy was performed, if the location + log was out of date. + * Makefile now builds using cabal, taking advantage of cabal's automatic + detection of appropriate build flags. + * test: The test suite is now built into the git-annex binary, and can + be run at any time. + + -- Joey Hess Wed, 27 Feb 2013 14:07:24 -0400 + +git-annex (3.20130216) unstable; urgency=low + + * Now uses the Haskell uuid library, rather than needing a uuid program. + * Now uses the Haskell Glob library, rather than pcre-light, avoiding + the need to install libpcre. Currently done only for Cabal or when + the Makefile is made to use -DWITH_GLOB + * Android port now available (command-line only). + * New annex.crippledfilesystem setting, allows use of git-annex + repositories on FAT and even worse filesystems; avoiding use of + hard links and locked down permissions settings. (Support is incomplete.) + * init: Detect when the repository is on a filesystem that does not + support hard links, or symlinks, or unix permissions, and set + annex.crippledfilesystem, as well as annex.direct. + * add: Improved detection of files that are modified while being added. + * Fix a bug in direct mode, introduced in the previous release, where + if a file was dropped and then got back, it would be stored in indirect + mode. + + -- Joey Hess Sat, 16 Feb 2013 10:03:26 -0400 + +git-annex (3.20130207) unstable; urgency=low + + * webapp: Now allows restarting any threads that crash. + * Adjust debian package to only build-depend on DAV on architectures + where it is available. + * addurl --fast: Use curl, rather than haskell HTTP library, to support https. + * annex.autocommit: New setting, can be used to disable autocommit + of changed files by the assistant, while it still does data syncing + and other tasks. + * assistant: Ignore .DS_Store on OSX. + * assistant: Fix location log when adding new file in direct mode. + * Deal with stale mappings for deleted file in direct mode. + * pre-commit: Update direct mode mappings. + * uninit, unannex --fast: If hard link creation fails, fall back to slow + mode. + * Clean up direct mode cache and mapping info when dropping keys. + * dropunused: Clean up stale direct mode cache and mapping info not + removed before. + + -- Joey Hess Thu, 07 Feb 2013 12:45:25 -0400 + +git-annex (3.20130124) unstable; urgency=low + + * Added source repository group, that only retains files until they've + been transferred to another repository. Useful for things like + repositories on cameras. + * Added manual repository group. Use to prevent the assistant from + downloading any file contents to keep things in sync. Instead + `git annex get`, `git annex drop` etc can be used manually as desired. + * webapp: More adjustments to longpoll code to deal with changes in + variable quoting in different versions of shakespeare-js. + * webapp: Avoid an error if a transfer is stopped just as it finishes. + Closes: #698184 + * webapp: Now always logs to .git/annex/daemon.log + * webapp: Has a page to view the log, accessed from the control menu. + * webapp: Fix crash adding removable drive that has an annex directory + in it that is not a git repository. + * Deal with incompatability in gpg2, which caused prompts for encryption + passphrases rather than using the supplied --passphrase-fd. + * bugfix: Union merges involving two or more repositories could sometimes + result in data from one repository getting lost. This could result + in the location log data becoming wrong, and fsck being needed to fix it. + * sync: Automatic merge conflict resolution now stages deleted files. + * Depend on git 1.7.7.6 for --no-edit. Closes: #698399 + * Fix direct mode mapping code to always store direct mode filenames + relative to the top of the repository, even when operating inside a + subdirectory. + * fsck: Detect and fix consistency errors in direct mode mapping files. + * Avoid filename encoding errors when writing direct mode mappings. + + -- Joey Hess Tue, 22 Jan 2013 07:11:59 +1100 + +git-annex (3.20130114) unstable; urgency=low + + * 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. + + -- Joey Hess Mon, 14 Jan 2013 15:25:18 -0400 + +git-annex (3.20130107) unstable; urgency=low + + * webapp: Add UI to stop and restart assistant. + * committer: Fix a file handle leak. + * assistant: Make expensive transfer scan work fully in direct mode. + * More commands work in direct mode repositories: find, whereis, move, copy, + drop, log, fsck, add, addurl. + * sync: No longer automatically adds files in direct mode. + * assistant: Detect when system is not configured with a user name, + and set environment to prevent git from failing. + * direct: Avoid hardlinking symlinks that point to the same content + when the content is not present. + * Fix transferring files to special remotes in direct mode. + + -- Joey Hess Mon, 07 Jan 2013 01:01:41 -0400 + +git-annex (3.20130102) unstable; urgency=low + + * direct, indirect: New commands, that switch a repository to and from + direct mode. In direct mode, files are accessed directly, rather than + via symlinks. Note that direct mode is currently experimental. Many + git-annex commands do not work in direct mode. Some git commands can + cause data loss when used in direct mode repositories. + * assistant: Now uses direct mode by default when setting up a new + local repository. + * OSX assistant: Uses the FSEvents API to detect file changes. + This avoids issues with running out of file descriptors on large trees, + as well as allowing detection of modification of files in direct mode. + Other BSD systems still use kqueue. + * kqueue: Fix bug that made broken symlinks not be noticed. + * vicfg: Quote filename. Closes: #696193 + * Bugfix: Fixed bug parsing transfer info files, where the newline after + the filename was included in it. This was generally benign, but in + the assistant, it caused unexpected dropping of preferred content. + * Bugfix: Remove leading \ from checksums output by sha*sum commands, + when the filename contains \ or a newline. Closes: #696384 + * fsck: Still accept checksums with a leading \ as valid, now that + above bug is fixed. + * SHA*E backends: Exclude non-alphanumeric characters from extensions. + * migrate: Remove leading \ in SHA* checksums, and non-alphanumerics + from extensions of SHA*E keys. + + -- Joey Hess Wed, 02 Jan 2013 13:21:34 -0400 + +git-annex (3.20121211) unstable; urgency=low + + * webapp: Defaults to sharing box.com account info with friends, allowing + one-click enabling of the repository. + * Fix broken .config/git-annex/program installed by standalone tarball. + * assistant: Retrival from glacier now handled. + * Include ssh in standalone tarball and OSX app. + * watch: Avoid leaving hard links to files behind in .git/annex/tmp + if a file is deleted or moved while it's being quarantined in preparation + to being added to the annex. + * Allow `git annex drop --from web`; of course this does not remove + any file from the web, but it does make git-annex remove all urls + associated with a file. + * webapp: S3 and Glacier forms now have a select list of all + currently-supported AWS regions. + * webdav: Avoid trying to set props, avoiding incompatability with + livedrive.com. Needs DAV version 0.3. + * webapp: Prettify error display. + * webapp: Fix bad interaction between required fields and modals. + * webapp: Added help buttons and links next to fields that require + explanations. + * webapp: Encryption can be disabled when setting up remotes. + * assistant: Avoid trying to drop content from remotes that don't have it. + * assistant: Allow periods in ssh key comments. + * get/copy --auto: Transfer data even if it would exceed numcopies, + when preferred content settings want it. + * drop --auto: Fix dropping content when there are no preferred content + settings. + * webapp: Allow user to specify the port when setting up a ssh or rsync + remote. + * assistant: Fix syncing to just created ssh remotes. + * Enable WebDAV support in Debian package. Closes: #695532 + + -- Joey Hess Tue, 11 Dec 2012 11:25:03 -0400 + +git-annex (3.20121127) unstable; urgency=low + + * Fix dirContentsRecursive, which had missed some files in deeply nested + subdirectories. Could affect various parts of git-annex. + * rsync: Fix bug introduced in last release that broke encrypted rsync + special remotes. + * The standalone builds now unset their special path and library path + variables before running the system web browser. + + -- Joey Hess Tue, 27 Nov 2012 17:07:32 -0400 + +git-annex (3.20121126) unstable; urgency=low + + * New webdav and Amazon glacier special remotes. + * Display a warning when a non-existing file or directory is specified. + * webapp: Added configurator for Box.com. + * webapp: Show error messages to user when testing XMPP creds. + * Fix build of assistant without yesod. + * webapp: The list of repositiories refreshes when new repositories are + added, including when new repository configurations are pushed in from + remotes. + * OSX: Fix RunAtLoad value in plist file. + * Getting a file from chunked directory special remotes no longer buffers + it all in memory. + * S3: Added progress display for uploading and downloading. + * directory special remote: Made more efficient and robust. + * Bugfix: directory special remote could loop forever storing a key + when a too small chunksize was configured. + * Allow controlling whether login credentials for S3 and webdav are + committed to the repository, by setting embedcreds=yes|no when running + initremote. + * Added smallarchive repository group, that only archives files that are + in archive directories. Used by default for glacier when set up in the + webapp. + * assistant: Fixed handling of toplevel archive directory and + client repository group. + * assistant: Apply preferred content settings when a new symlink + is created, or a symlink gets renamed. Made archive directories work. + + -- Joey Hess Mon, 26 Nov 2012 11:37:49 -0400 + +git-annex (3.20121112) unstable; urgency=low + + * assistant: Can use XMPP to notify other nodes about pushes made to other + repositories, as well as pushing to them directly over XMPP. + * wepapp: Added an XMPP configuration interface. + * webapp: Supports pairing over XMPP, with both friends, and other repos + using the same account. + * assistant: Drops non-preferred content when possible. + * assistant: Notices, and applies config changes as they are made to + the git-annex branch, including config changes pushed in from remotes. + * git-annex-shell: GIT_ANNEX_SHELL_DIRECTORY can be set to limit it + to operating on a specified directory. + * webapp: When setting up authorized_keys, use GIT_ANNEX_SHELL_DIRECTORY. + * Preferred content path matching bugfix. + * Preferred content expressions cannot use "in=". + * Preferred content expressions can use "present". + * Fix handling of GIT_DIR when it refers to a git submodule. + * Depend on and use the Haskell SafeSemaphore library, which provides + exception-safe versions of SampleVar and QSemN. + Thanks, Ben Gamari for an excellent patch set. + * file:/// URLs can now be used with the web special remote. + * webapp: Allow dashes in ssh key comments when pairing. + * uninit: Check and abort if there are symlinks to annexed content that + are not checked into git. + * webapp: Switched to using the same multicast IP address that avahi uses. + * bup: Don't pass - to bup-split to make it read stdin; bup 0.25 + does not accept that. + * bugfix: Don't fail transferring content from read-only repos. + Closes: #691341 + * configure: Check that checksum programs produce correct checksums. + * Re-enable dbus, using a new version of the library that fixes the memory + leak. + * NetWatcher: When dbus connection is lost, try to reconnect. + * Use USER and HOME environment when set, and only fall back to getpwent, + which doesn't work with LDAP or NIS. + * rsync special remote: Include annex-rsync-options when running rsync + to test a key's presence. + * The standalone tarball's runshell now takes care of installing a + ~/.ssh/git-annex-shell wrapper the first time it's run. + * webapp: Make an initial, empty commit so there is a master branch + * assistant: Fix syncing local drives. + * webapp: Fix creation of rsync.net repositories. + * webapp: Fix renaming of special remotes. + * webapp: Generate better git remote names. + * webapp: Ensure that rsync special remotes are enabled using the same + name they were originally created using. + * Bugfix: Fix hang in webapp when setting up a ssh remote with an absolute + path. + + -- Joey Hess Mon, 12 Nov 2012 10:39:47 -0400 + +git-annex (3.20121017) unstable; urgency=low + + * Fix zombie cleanup reversion introduced in 3.20121009. + * Additional fix to support git submodules. + + -- Joey Hess Tue, 16 Oct 2012 21:10:14 -0400 + +git-annex (3.20121016) unstable; urgency=low + + * vicfg: New file format, avoids ambiguity with repos that have the same + description, or no description. + * Bug fix: A recent change caused git-annex-shell to crash. + * Better preferred content expression for transfer repos. + * webapp: Repository edit form can now edit the name of a repository. + * webapp: Make bare repositories on removable drives, as there is nothing + to ensure non-bare repos get updated when syncing. + * webapp: Better behavior when pausing syncing to a remote when a transfer + scan is running and queueing new transfers for that remote. + * The standalone binaries are now built to not use ssh connection caching, + in order to work with old versions of ssh. + * A relative core.worktree is relative to the gitdir. Now that this is + handled correctly, git-annex can be used in git submodules. + * Temporarily disable use of dbus, as the haskell dbus library blows up + when losing connection, which will need to be fixed upstream. + + -- Joey Hess Tue, 16 Oct 2012 15:25:22 -0400 + +git-annex (3.20121010) unstable; urgency=low + + * Renamed --ingroup to --inallgroup. + * Standard groups changed to client, transfer, archive, and backup. + Each of these has its own standard preferred content setting. + * dead: Remove dead repository from all groups. + * Avoid unsetting HOME when running certian git commands. Closes: #690193 + * test: Fix threaded runtime hang. + * Makefile: Avoid building with -threaded if the ghc threaded runtime does + not exist. + * webapp: Improve wording of intro display. Closes: #689848 + * webapp: Repositories can now be configured, to change their description, + their group, or even to disable syncing to them. + * git config remote.name.annex-sync can be used to control whether + a remote gets synced. + * Fix a crash when merging files in the git-annex branch that contain + invalid utf8. + * Automatically detect when a ssh remote does not have git-annex-shell + installed, and set annex-ignore. + + -- Joey Hess Fri, 12 Oct 2012 13:45:21 -0400 + +git-annex (3.20121009) unstable; urgency=low + + * watch, assistant: It's now safe to git annex unlock files while + the watcher is running, as well as modify files checked into git + as normal files. Additionally, .gitignore settings are now honored. + Closes: #689979 + * group, ungroup: New commands to indicate groups of repositories. + * webapp: Adds newly created repositories to one of these groups: + clients, drives, servers + * vicfg: New command, allows editing (or simply viewing) most + of the repository configuration settings stored in the git-annex branch. + * Added preferred content expressions, configurable using vicfg. + * get --auto: If the local repository has preferred content + configured, only get that content. + * drop --auto: If the repository the content is dropped from has + preferred content configured, drop only content that is not preferred. + * copy --auto: Only transfer content that the destination repository prefers. + * assistant: Now honors preferred content settings when deciding what to + transfer. + * --copies=group:number can now be used to match files that are present + in a specified number of repositories in a group. + * Added --smallerthan, --largerthan, and --inall limits. + * Only build-depend on libghc-clientsession-dev on arches that will have + the webapp. + * uninit: Unset annex.version. Closes: #689852 + + -- Joey Hess Tue, 09 Oct 2012 15:13:23 -0400 + +git-annex (3.20121001) unstable; urgency=low + + * fsck: Now has an incremental mode. Start a new incremental fsck pass + with git annex fsck --incremental. Now the fsck can be interrupted + as desired, and resumed with git annex fsck --more. + Thanks, Justin Azoff + * New --time-limit option, makes long git-annex commands stop after + a specified amount of time. + * fsck: New --incremental-schedule option which is nice for scheduling + eg, monthly incremental fsck runs in cron jobs. + * Fix fallback to ~/Desktop when xdg-user-dir is not available. + Closes: #688833 + * S3: When using a shared cipher, S3 credentials are not stored encrypted + in the git repository, as that would allow anyone with access to + the repository access to the S3 account. Instead, they're stored + in a 600 mode file in the local git repo. + * webapp: Avoid crashing when ssh-keygen -F chokes on an invalid known_hosts + file. + * Always do a system wide installation when DESTDIR is set. Closes: #689052 + * The Makefile now builds with the new yesod by default. + Systems like Debian that have the old yesod 1.0.1 should set + GIT_ANNEX_LOCAL_FEATURES=-DWITH_OLD_YESOD + * copy: Avoid updating the location log when no copy is performed. + * configure: Test that uuid -m works, falling back to plain uuid if not. + * Avoid building the webapp on Debian architectures that do not yet + have template haskell and thus yesod. (Should be available for arm soonish + I hope). + + -- Joey Hess Mon, 01 Oct 2012 13:56:55 -0400 + +git-annex (3.20120924) unstable; urgency=low + + * assistant: New command, a daemon which does everything watch does, + as well as automatically syncing file contents between repositories. + * webapp: An interface for managing and configuring the assistant. + * The default backend used when adding files to the annex is changed + from SHA256 to SHA256E, to simplify interoperability with OSX, media + players, and various programs that needlessly look at symlink targets. + To get old behavior, add a .gitattributes containing: * annex.backend=SHA256 + * init: If no description is provided for a new repository, one will + automatically be generated, like "joey@gnu:~/foo" + * test: Set a lot of git environment variables so testing works in strange + environments that normally need git config to set names, etc. + Closes: #682351 Thanks, gregor herrmann + * Disable ssh connection caching if the path to the control socket would be + too long (and use relative path to minimise path to the control socket). + * migrate: Check content before generating the new key, to avoid generating + a key for corrupt data. + * Support repositories created with --separate-git-dir. Closes: #684405 + * reinject: When the provided file doesn't match, leave it where it is, + rather than moving to .git/annex/bad/ + * Avoid crashing on encoding errors in filenames when writing transfer info + files and reading from checksum commands. + * sync: Pushes the git-annex branch to remote/synced/git-annex, rather + than directly to remote/git-annex. + * Now supports matching files that are present on a number of remotes + with a specified trust level. Example: --copies=trusted:2 + Thanks, Nicolas Pouillard + + -- Joey Hess Mon, 24 Sep 2012 13:47:48 -0400 + +git-annex (3.20120825) unstable; urgency=low + + * S3: Add fileprefix setting. + * Pass --use-agent to gpg when in no tty mode. Thanks, Eskild Hustvedt. + * Bugfix: Fix fsck in SHA*E backends, when the key contains composite + extensions, as added in 3.20120721. + + -- Joey Hess Sat, 25 Aug 2012 10:00:10 -0400 + +git-annex (3.20120807) unstable; urgency=low + + * initremote: Avoid recording remote's description before checking + that its config is valid. + * unused, status: Avoid crashing when ran in bare repo. + * Avoid crashing when "git annex get" fails to download from one + location, and falls back to downloading from a second location. + + -- Joey Hess Tue, 07 Aug 2012 13:35:07 -0400 + +git-annex (3.20120721) unstable; urgency=low + + * get, move, copy: Now refuse to do anything when the requested file + transfer is already in progress by another process. + * status: Lists transfers that are currently in progress. + * Fix passing --uuid to git-annex-shell. + * When shaNsum commands cannot be found, use the Haskell SHA library + (already a dependency) to do the checksumming. This may be slower, + but avoids portability problems. + * Use SHA library for files less than 50 kb in size, at which point it's + faster than forking the more optimised external program. + * SHAnE backends are now smarter about composite extensions, such as + .tar.gz Closes: #680450 + * map: Write map.dot to .git/annex, which avoids watch trying to annex it. + + -- Joey Hess Sat, 21 Jul 2012 16:52:48 -0400 + +git-annex (3.20120629) unstable; urgency=low + + * cabal: Only try to use inotify on Linux. + * Version build dependency on STM, and allow building without it, + which disables the watch command. + * Avoid ugly failure mode when moving content from a local repository + that is not available. + * Got rid of the last place that did utf8 decoding. + * Accept arbitrarily encoded repository filepaths etc when reading + git config output. This fixes support for remotes with unusual characters + in their names. + * sync: Automatically resolves merge conflicts. + + -- Joey Hess Fri, 29 Jun 2012 10:17:49 -0400 + +git-annex (3.20120624) unstable; urgency=low + + * watch: New subcommand, a daemon which notices changes to + files and automatically annexes new files, etc, so you don't + need to manually run git commands when manipulating files. + Available on Linux, BSDs, and OSX! + * Enable diskfree on kfreebsd, using kqueue. + * unused: Fix crash when key names contain invalid utf8. + * sync: Avoid recent git's interactive merge. + + -- Joey Hess Sun, 24 Jun 2012 12:36:50 -0400 + +git-annex (3.20120614) unstable; urgency=medium + + * addurl: Was broken by a typo introduced 2 released ago, now fixed. + Closes: #677576 + * Install man page when run by cabal, in a location where man will + find it, even when installing under $HOME. Thanks, Nathan Collins + + -- Joey Hess Thu, 14 Jun 2012 20:21:29 -0400 + +git-annex (3.20120611) unstable; urgency=medium + + * add: Prevent (most) modifications from being made to a file while it + is being added to the annex. + * initremote: Automatically describe a remote when creating it. + * uninit: Refuse to run in a subdirectory. Closes: #677076 + + -- Joey Hess Mon, 11 Jun 2012 10:32:01 -0400 + +git-annex (3.20120605) unstable; urgency=low + + * sync: Show a nicer message if a user tries to sync to a special remote. + * lock: Reset unlocked file to index, rather than to branch head. + * import: New subcommand, pulls files from a directory outside the annex + and adds them. + * Fix display of warning message when encountering a file that uses an + unsupported backend. + * Require that the SHA256 backend can be used when building, since it's the + default. + * Preserve parent environment when running hooks of the hook special remote. + + -- Joey Hess Tue, 05 Jun 2012 14:03:39 -0400 + +git-annex (3.20120522) unstable; urgency=low + + * Pass -a to cp even when it supports --reflink=auto, to preserve + permissions. + * Clean up handling of git directory and git worktree. + * Add support for core.worktree, and fix support for GIT_WORK_TREE and + GIT_DIR. + + -- Joey Hess Tue, 22 May 2012 11:16:13 -0400 + +git-annex (3.20120511) unstable; urgency=low + + * Rsync special remotes can be configured with shellescape=no + to avoid shell quoting that is normally done when using rsync over ssh. + This is known to be needed for certian rsync hosting providers + (specificially hidrive.strato.com) that use rsync over ssh but do not + pass it through the shell. + * dropunused: Allow specifying ranges to drop. + * addunused: New command, the opposite of dropunused, it relinks unused + content into the git repository. + * Fix use of several config settings: annex.ssh-options, + annex.rsync-options, annex.bup-split-options. (And adjust types to avoid + the bugs that broke several config settings.) + + -- Joey Hess Fri, 11 May 2012 12:29:30 -0400 + +git-annex (3.20120430) unstable; urgency=low + + * Fix use of annex.diskreserve config setting. + * Directory special remotes now check annex.diskreserve. + * Support git's core.sharedRepository configuration. + * Add annex.http-headers and annex.http-headers-command config + settings, to allow custom headers to be sent with all HTTP requests. + (Requested by the Internet Archive) + * uninit: Clear annex.uuid from .git/config. Closes: #670639 + * Added shared cipher mode to encryptable special remotes. This option + avoids gpg key distribution, at the expense of flexability, and with + the requirement that all clones of the git repository be equally trusted. + + -- Joey Hess Mon, 30 Apr 2012 13:16:10 -0400 + +git-annex (3.20120418) unstable; urgency=low + + * bugfix: Adding a dotfile also caused all non-dotfiles to be added. + * bup: Properly handle key names with spaces or other things that are + not legal git refs. + * git-annex (but not git-annex-shell) supports the git help.autocorrect + configuration setting, doing fuzzy matching using the restricted + Damerau-Levenshtein edit distance, just as git does. This adds a build + dependency on the haskell edit-distance library. + * Renamed diskfree.c to avoid OSX case insensativity bug. + * cabal now installs git-annex-shell as a symlink to git-annex. + * cabal file now autodetects whether S3 support is available. + + -- Joey Hess Wed, 18 Apr 2012 12:11:32 -0400 + +git-annex (3.20120406) unstable; urgency=low + + * Disable diskfree on kfreebsd, as I have a build failure on kfreebsd-i386 + that is quite likely caused by it. + + -- Joey Hess Sat, 07 Apr 2012 15:50:36 -0400 + +git-annex (3.20120405) unstable; urgency=low + + * Rewrote free disk space checking code, moving the portability + handling into a small C library. + * status: Display amount of free disk space. + + -- Joey Hess Thu, 05 Apr 2012 16:19:10 -0400 + +git-annex (3.20120315) unstable; urgency=low + + * fsck: Fix up any broken links and misplaced content caused by the + directory hash calculation bug fixed in the last release. + * sync: Sync to lower cost remotes first. + * status: Fixed to run in constant space. + * status: More accurate display of sizes of tmp and bad keys. + * unused: Now uses a bloom filter, and runs in constant space. + Use of a bloom filter does mean it will not notice a small + number of unused keys. For repos with up to half a million keys, + it will miss one key in 1000. + * Added annex.bloomcapacity and annex.bloomaccuracy, which can be + adjusted as desired to tune the bloom filter. + * status: Display amount of memory used by bloom filter, and + detect when it's too small for the number of keys in a repository. + * git-annex-shell: Runs hooks/annex-content after content is received + or dropped. + * Work around a bug in rsync (IMHO) introduced by openSUSE's SIP patch. + * git-annex now behaves as git-annex-shell if symlinked to and run by that + name. The Makefile sets this up, saving some 8 mb of installed size. + * git-union-merge is a demo program, so it is no longer built by default. + + -- Joey Hess Thu, 15 Mar 2012 11:05:28 -0400 + +git-annex (3.20120309) unstable; urgency=low + + * Fix key directory hash calculation code to behave as it did before + version 3.20120227 when a key contains non-ascii characters (only + WORM backend is likely to have been affected). + + -- Joey Hess Fri, 09 Mar 2012 20:05:09 -0400 + +git-annex (3.20120230) unstable; urgency=low + + * "here" can be used to refer to the current repository, + which can read better than the old "." (which still works too). + * Directory special remotes now support chunking files written to them, + avoiding writing files larger than a specified size. + * Add progress bar display to the directory special remote. + * Add configurable hooks that are run when git-annex starts and stops + using a remote: remote.name.annex-start-command and + remote.name.annex-stop-command + * Fix a bug in symlink calculation code, that triggered in rare + cases where an annexed file is in a subdirectory that nearly + matched to the .git/annex/object/xx/yy subdirectories. + + -- Joey Hess Mon, 05 Mar 2012 13:38:13 -0400 + +git-annex (3.20120229) unstable; urgency=low + + * Fix test suite to not require a unicode locale. + * Fix cabal build failure. Thanks, Sergei Trofimovich + + -- Joey Hess Wed, 29 Feb 2012 02:31:31 -0400 + +git-annex (3.20120227) unstable; urgency=low + + * Modifications to support ghc 7.4's handling of filenames. + This version can only be built with ghc 7.4 or newer. See the ghc7.0 + branch for older ghcs. + * S3: Fix irrefutable pattern failure when accessing encrypted S3 + credentials. + * Use the haskell IfElse library. + * Fix teardown of stale cached ssh connections. + * Fixed to use the strict state monad, to avoid leaking all kinds of memory + due to lazy state update thunks when adding/fixing many files. + * Fixed some memory leaks that occurred when committing journal files. + * Added a annex.queuesize setting, useful when adding hundreds of thousands + of files on a system with plenty of memory. + * whereis: Prints the urls of files that the web special remote knows about. + * addurl --fast: Verifies that the url can be downloaded (only getting + its head), and records the size in the key. + * When checking that an url has a key, verify that the Content-Length, + if available, matches the size of the key. + * addurl: Added a --file option, which can be used to specify what + file the url is added to. This can be used to override the default + filename that is used when adding an url, which is based on the url. + Or, when the file already exists, the url is recorded as another + location of the file. + * addurl: Normalize badly encoded urls. + * addurl: Add --pathdepth option. + * rekey: New plumbing level command, can be used to change the keys used + for files en masse. + * Store web special remote url info in a more efficient location. + (Urls stored with this version will not be visible to older versions.) + * Deal with NFS problem that caused a failure to remove a directory + when removing content from the annex. + * Make a single location log commit after a remote has received or + dropped files. Uses a new "git-annex-shell commit" command when available. + * To avoid commits of data to the git-annex branch after each command + is run, set annex.alwayscommit=false. Its data will then be committed + less frequently, when a merge or sync is done. + * configure: Check if ssh connection caching is supported by the installed + version of ssh and default annex.sshcaching accordingly. + * move --from, copy --from: Now 10 times faster when scanning to find + files in a remote on a local disk; rather than go through the location log + to see which files are present on the remote, it simply looks at the + disk contents directly. + + -- Joey Hess Mon, 27 Feb 2012 12:58:21 -0400 + +git-annex (3.20120123) unstable; urgency=low + + * fsck --from: Fscking a remote is now supported. It's done by retrieving + the contents of the specified files from the remote, and checking them, + so can be an expensive operation. Still, if the remote is a special + remote, or a git repository that you cannot run fsck in locally, it's + nice to have the ability to fsck it. + * If you have any directory special remotes, now would be a good time to + fsck them, in case you were hit by the data loss bug fixed in the + previous release! + * fsck --from remote --fast: Avoids expensive file transfers, at the + expense of not checking file size and/or contents. + * Ssh connection caching is now enabled automatically by git-annex. + Only one ssh connection is made to each host per git-annex run, which + can speed some things up a lot, as well as avoiding repeated password + prompts. Concurrent git-annex processes also share ssh connections. + Cached ssh connections are shut down when git-annex exits. + * To disable the ssh caching (if for example you have your own broader + ssh caching configuration), set annex.sshcaching=false. + + -- Joey Hess Mon, 23 Jan 2012 13:48:48 -0400 + +git-annex (3.20120116) unstable; urgency=medium + + * Fix data loss bug in directory special remote, when moving a file + to the remote failed, and partially transferred content was left + behind in the directory, re-running the same move would think it + succeeded and delete the local copy. + + -- Joey Hess Mon, 16 Jan 2012 16:43:45 -0400 + +git-annex (3.20120115) unstable; urgency=low + + * Add a sanity check for bad StatFS results. On architectures + where StatFS does not currently work (s390, mips, powerpc, sparc), + this disables the diskreserve checking code, and attempting to + configure an annex.diskreserve will result in an error. + * Fix QuickCheck dependency in cabal file. + * Minor optimisations. + + -- Joey Hess Sun, 15 Jan 2012 13:54:20 -0400 + +git-annex (3.20120113) unstable; urgency=low + + * log: Add --gource mode, which generates output usable by gource. + * map: Fix display of remote repos + * Add annex-trustlevel configuration settings, which can be used to + override the trust level of a remote. + * git-annex, git-union-merge: Support GIT_DIR and GIT_WORK_TREE. + * Add libghc-testpack-dev to build depends on all arches. + + -- Joey Hess Fri, 13 Jan 2012 15:35:17 -0400 + +git-annex (3.20120106) unstable; urgency=low + + * Support unescaped repository urls, like git does. + * log: New command that displays the location log for files, + showing each repository they were added to and removed from. + * Fix overbroad gpg --no-tty fix from last release. + + -- Joey Hess Sat, 07 Jan 2012 13:16:23 -0400 + +git-annex (3.20120105) unstable; urgency=low + + * Added annex-web-options configuration settings, which can be + used to provide parameters to whichever of wget or curl git-annex uses + (depends on which is available, but most of their important options + suitable for use here are the same). + * Dotfiles, and files inside dotdirs are not added by "git annex add" + unless the dotfile or directory is explicitly listed. So "git annex add ." + will add all untracked files in the current directory except for those in + dotdirs. + * Added quickcheck to build dependencies, and fail if test suite cannot be + built. + * fsck: Do backend-specific check before checking numcopies is satisfied. + * Run gpg with --no-tty. Closes: #654721 + + -- Joey Hess Thu, 05 Jan 2012 13:44:12 -0400 + +git-annex (3.20111231) unstable; urgency=low + + * sync: Improved to work well without a central bare repository. + Thanks to Joachim Breitner. + * Rather than manually committing, pushing, pulling, merging, and git annex + merging, we encourage you to give "git annex sync" a try. + * sync --fast: Selects some of the remotes with the lowest annex.cost + and syncs those, in addition to any specified at the command line. + * Union merge now finds the least expensive way to represent the merge. + * reinject: Add a sanity check for using an annexed file as the source file. + * Properly handle multiline git config values. + * Fix the hook special remote, which bitrotted a while ago. + * map: --fast disables use of dot to display map + * Test suite improvements. Current top-level test coverage: 75% + * Improve deletion of files from rsync special remotes. Closes: #652849 + * Add --include, which is the same as --not --exclude. + * Format strings can be specified using the new --format option, to control + what is output by git annex find. + * Support git annex find --json + * Fixed behavior when multiple insteadOf configs are provided for the + same url base. + * Can now be built with older git versions (before 1.7.7); the resulting + binary should only be used with old git. + * Updated to build with monad-control 0.3. + + -- Joey Hess Sat, 31 Dec 2011 14:55:29 -0400 + +git-annex (3.20111211) unstable; urgency=medium + + * Fix bug in last version in getting contents from bare repositories. + * Ensure that git-annex branch changes are merged into git-annex's index, + which fixes a bug that could cause changes that were pushed to the + git-annex branch to get reverted. As a side effect, it's now safe + for users to check out and commit changes directly to the git-annex + branch. + * map: Fix a failure to detect a loop when both repositories are local + and refer to each other with relative paths. + * Prevent key names from containing newlines. + * add: If interrupted, add can leave files converted to symlinks but not + yet added to git. Running the add again will now clean up this situtation. + * Fix caching of decrypted ciphers, which failed when drop had to check + multiple different encrypted special remotes. + * unannex: Can be run on files that have been added to the annex, but not + yet committed. + * sync: New command that synchronises the local repository and default + remote, by running git commit, pull, and push for you. + * Version monad-control dependency in cabal file. + + -- Joey Hess Sun, 11 Dec 2011 21:24:39 -0400 + +git-annex (3.20111203) unstable; urgency=low + + * The VFAT filesystem on recent versions of Linux, when mounted with + shortname=mixed, does not get along well with git-annex's mixed case + .git/annex/objects hash directories. To avoid this problem, new content + is now stored in all-lowercase hash directories. Except for non-bare + repositories which would be a pain to transition and cannot be put on FAT. + (Old mixed-case hash directories are still tried for backwards + compatibility.) + * Flush json output, avoiding a buffering problem that could result in + doubled output. + * Avoid needing haskell98 and other fixes for new ghc. Thanks, Mark Wright. + * Bugfix: dropunused did not drop keys with two spaces in their name. + * Support for storing .git/annex on a different device than the rest of the + git repository. + * --inbackend can be used to make git-annex only operate on files + whose content is stored using a specified key-value backend. + * dead: A command which says that a repository is gone for good + and you don't want git-annex to mention it again. + + -- Joey Hess Sat, 03 Dec 2011 21:01:45 -0400 + +git-annex (3.20111122) unstable; urgency=low + + * merge: Improve commit messages to mention what was merged. + * Avoid doing auto-merging in commands that don't need fully current + information from the git-annex branch. In particular, git annex add + no longer needs to auto-merge. + * init: When run in an already initalized repository, and without + a description specified, don't delete the old description. + * Optimised union merging; now only runs git cat-file once, and runs + in constant space. + * status: Now displays trusted, untrusted, and semitrusted repositories + separately. + * status: Include all special remotes in the list of repositories. + * status: Fix --json mode. + * status: --fast is back + * Fix support for insteadOf url remapping. Closes: #644278 + * When not run in a git repository, git-annex can still display a usage + message, and "git annex version" even works. + * migrate: Don't fall over a stale temp file. + * Avoid excessive escaping for rsync special remotes that are not accessed + over ssh. + * find: Support --print0 + + -- Joey Hess Tue, 22 Nov 2011 14:31:45 -0400 + +git-annex (3.20111111) unstable; urgency=low + + * Handle a case where an annexed file is moved into a gitignored directory, + by having fix --force add its change. + * Avoid cyclic drop problems. + * Optimized copy --from and get --from to avoid checking the location log + for files that are already present. + * Automatically fix up badly formatted uuid.log entries produced by + 3.20111105, whenever the uuid.log is changed (ie, by init or describe). + * map: Support remotes with /~/ and /~user/ + + -- Joey Hess Fri, 11 Nov 2011 13:44:18 -0400 + +git-annex (3.20111107) unstable; urgency=low + + * merge: Use fast-forward merges when possible. + Thanks Valentin Haenel for a test case showing how non-fast-forward + merges could result in an ongoing pull/merge/push cycle. + * Don't try to read config from repos with annex-ignore set. + * Bugfix: In the past two releases, git-annex init has written the uuid.log + in the wrong format, with the UUID and description flipped. + + -- Joey Hess Mon, 07 Nov 2011 12:47:44 -0400 + +git-annex (3.20111105) unstable; urgency=low + + * The default backend used when adding files to the annex is changed + from WORM to SHA256. + To get old behavior, add a .gitattributes containing: * annex.backend=WORM + * Sped up some operations on remotes that are on the same host. + * copy --to: Fixed leak when copying many files to a remote on the same + host. + * uninit: Add guard against being run with the git-annex branch checked out. + * Fail if --from or --to is passed to commands that do not support them. + * drop --from is now supported to remove file content from a remote. + * status: Now always shows the current repository, even when it does not + appear in uuid.log. + * fsck: Now works in bare repositories. Checks location log information, + and file contents. Does not check that numcopies is satisfied, as + .gitattributes information about numcopies is not available in a bare + repository. + * unused, dropunused: Now work in bare repositories. + * Removed the setkey command, and added a reinject command with a more + useful interface. + * The fromkey command now takes the key as its first parameter. The --key + option is no longer used. + * Built without any filename containing .git being excluded. Closes: #647215 + * Record uuid when auto-initializing a remote so it shows in status. + * Bugfix: Fixed git-annex init crash in a bare repository when there was + already an existing git-annex branch. + * Pass -t to rsync to preserve timestamps. + + -- Joey Hess Sat, 05 Nov 2011 15:47:52 -0400 + +git-annex (3.20111025) unstable; urgency=low + + * A remote can have a annexUrl configured, that is used by git-annex + instead of its usual url. (Similar to pushUrl.) + * migrate: Copy url logs for keys when migrating. + * git-annex-shell: GIT_ANNEX_SHELL_READONLY and GIT_ANNEX_SHELL_LIMITED + environment variables can be set to limit what commands can be run. + This is used by gitolite's new git-annex support! + + -- Joey Hess Tue, 25 Oct 2011 13:03:08 -0700 + +git-annex (3.20111011) unstable; urgency=low + + * This version of git-annex only works with git 1.7.7 and newer. + The breakage with old versions is subtle, and affects the + annex.numcopies settings in .gitattributes, so be sure to upgrade git + to 1.7.7. (Debian package now depends on that version.) + * Don't pass absolute paths to git show-attr, as it started following + symlinks when that's done in 1.7.7. Instead, use relative paths, + which show-attr only handles 100% correctly in 1.7.7. Closes: #645046 + * Fix referring to remotes by uuid. + * New or changed repository descriptions in uuid.log now have a timestamp, + which is used to ensure the newest description is used when the uuid.log + has been merged. + * Note that older versions of git-annex will display the timestamp as part + of the repository description, which is ugly but otherwise harmless. + * Add timestamps to trust.log and remote.log too. + * git-annex-shell: Added the --uuid option. + * git-annex now asks git-annex-shell to verify that it's operating in + the expected repository. + * Note that this git-annex will not interoperate with remotes using + older versions of git-annex-shell. + * Now supports git's insteadOf configuration, to modify the url + used to access a remote. Note that pushInsteadOf is not used; + that and pushurl are reserved for actual git pushes. Closes: #644278 + * status: List all known repositories. + * When displaying a list of repositories, show git remote names + in addition to their descriptions. + * Add locking to avoid races when changing the git-annex branch. + * Various speed improvements gained by using ByteStrings. + * Contain the zombie hordes. + + -- Joey Hess Tue, 11 Oct 2011 23:00:02 -0400 + +git-annex (3.20110928) unstable; urgency=low + + * --in can be used to make git-annex only operate on files + believed to be present in a given repository. + * Arbitrarily complex expressions can be built to limit the files git-annex + operates on, by combining the options --not --and --or -( and -) + Example: git annex get --exclude '*.mp3' --and --not -( --in usbdrive --or --in archive -) + * --copies=N can be used to make git-annex only operate on files with + the specified number of copies. (And --not --copies=N for the inverse.) + * find: Rather than only showing files whose contents are present, + when used with --exclude --copies or --in, displays all files that + match the specified conditions. + * Note that this is a behavior change for git-annex find! Old behavior + can be gotten by using: git-annex find --in . + * status: Massively sped up; remove --fast mode. + * unused: File contents used by branches and tags are no longer + considered unused, even when not used by the current branch. This is + the final piece of the puzzle needed for git-annex to to play nicely + with branches. + + -- Joey Hess Wed, 28 Sep 2011 18:14:02 -0400 + +git-annex (3.20110915) unstable; urgency=low + + * whereis: Show untrusted locations separately and do not include in + location count. + * Fix build without S3. + * addurl: Always use whole url as destination filename, rather than + only its file component. + * get, drop, copy: Added --auto option, which decides whether + to get/drop content as needed to work toward the configured numcopies. + * bugfix: drop and fsck did not honor --exclude + + -- Joey Hess Thu, 15 Sep 2011 22:25:46 -0400 + +git-annex (3.20110906) unstable; urgency=low + + * Improve display of newlines around error and warning messages. + * Fix Makefile to work with cabal again. + + -- Joey Hess Tue, 06 Sep 2011 13:45:16 -0400 + +git-annex (3.20110902) unstable; urgency=low + + * Set EMAIL when running test suite so that git does not need to be + configured first. Closes: #638998 + * The wget command will now be used in preference to curl, if available. + * init: Make description an optional parameter. + * unused, status: Sped up by avoiding unnecessary stats of annexed files. + * unused --remote: Reduced memory use to 1/4th what was used before. + * Add --json switch, to produce machine-consumable output. + + -- Joey Hess Fri, 02 Sep 2011 21:20:37 -0400 + +git-annex (3.20110819) unstable; urgency=low + + * Now "git annex init" only has to be run once, when a git repository + is first being created. Clones will automatically notice that git-annex + is in use and automatically perform a basic initalization. It's + still recommended to run "git annex init" in any clones, to describe them. + * Added annex-cost-command configuration, which can be used to vary the + cost of a remote based on the output of a shell command. + * Fix broken upgrade from V1 repository. Closes: #638584 + + -- Joey Hess Fri, 19 Aug 2011 20:34:09 -0400 + +git-annex (3.20110817) unstable; urgency=low + + * Fix shell escaping in rsync special remote. + * addurl: --fast can be used to avoid immediately downloading the url. + * Added support for getting content from git remotes using http (and https). + * Added curl to Debian package dependencies. + + -- Joey Hess Wed, 17 Aug 2011 01:29:02 -0400 + +git-annex (3.20110719) unstable; urgency=low + + * add: Be even more robust to avoid ever leaving the file seemingly deleted. + Closes: #634233 + * Bugfix: Make add ../ work. + * Support the standard git -c name=value + * unannex: Clean up use of git commit -a. + + -- Joey Hess Tue, 19 Jul 2011 23:39:53 -0400 + +git-annex (3.20110707) unstable; urgency=low + + * Fix sign bug in disk free space checking. + * Bugfix: Forgot to de-escape keys when upgrading. Could result in + bad location log data for keys that contain [&:%] in their names. + (A workaround for this problem is to run git annex fsck.) + * add: Avoid a failure mode that resulted in the file seemingly being + deleted (content put in the annex but no symlink present). + + -- Joey Hess Thu, 07 Jul 2011 19:29:39 -0400 + +git-annex (3.20110705) unstable; urgency=low + + * uninit: Delete the git-annex branch and .git/annex/ + * unannex: In --fast mode, file content is left in the annex, and a + hard link made to it. + * uninit: Use unannex in --fast mode, to support unannexing multiple + files that link to the same content. + * Drop the dependency on the haskell curl bindings, use regular haskell HTTP. + * Fix a pipeline stall when upgrading (caused by #624389). + + -- Joey Hess Tue, 05 Jul 2011 14:37:39 -0400 + +git-annex (3.20110702) unstable; urgency=low + + * Now the web can be used as a special remote. + This feature replaces the old URL backend. + * addurl: New command to download an url and store it in the annex. + * Sped back up fsck, copy --from, and other commands that often + have to read a lot of information from the git-annex branch. Such + commands are now faster than they were before introduction of the + git-annex branch. + * Always ensure git-annex branch exists. + * Modify location log parser to allow future expansion. + * --force will cause add, etc, to operate on ignored files. + * Avoid mangling encoding when storing the description of repository + and other content. + * cabal can now be used to build git-annex. This is substantially + slower than using make, does not build or install documentation, + does not run the test suite, and is not particularly recommended, + but could be useful to some. + + -- Joey Hess Sat, 02 Jul 2011 15:00:18 -0400 + +git-annex (3.20110624) experimental; urgency=low + + * New repository format, annex.version=3. Use `git annex upgrade` to migrate. + * git-annex now stores its logs in a git-annex branch. + * merge: New subcommand. Auto-merges the new git-annex branch. + * Improved handling of bare git repos with annexes. Many more commands will + work in them. + * git-annex is now more robust; it will never leave state files + uncommitted when some other git process comes along and locks the index + at an inconvenient time. + * rsync is now used when copying files from repos on other filesystems. + cp is still used when copying file from repos on the same filesystem, + since --reflink=auto can make it significantly faster on filesystems + such as btrfs. + * Allow --trust etc to specify a repository by name, for temporarily + trusting repositories that are not configured remotes. + * unlock: Made atomic. + * git-union-merge: New git subcommand, that does a generic union merge + operation, and operates efficiently without touching the working tree. + + -- Joey Hess Fri, 24 Jun 2011 14:32:18 -0400 + +git-annex (0.20110610) unstable; urgency=low + + * Add --numcopies option. + * Add --trust, --untrust, and --semitrust options. + * get --from is the same as copy --from + * Bugfix: Fix fsck to not think all SHAnE keys are bad. + + -- Joey Hess Fri, 10 Jun 2011 11:48:40 -0400 + +git-annex (0.20110601) unstable; urgency=low + + * Minor bugfixes and error message improvements. + * Massively sped up `git annex lock` by avoiding use of the uber-slow + `git reset`, and only running `git checkout` once, even when many files + are being locked. + * Fix locking of files with staged changes. + * Somewhat sped up `git commit` of modifications to unlocked files. + * Build fix for older ghc. + + -- Joey Hess Wed, 01 Jun 2011 11:50:47 -0400 + +git-annex (0.20110522) unstable; urgency=low + + * Closer emulation of git's behavior when told to use "foo/.git" as a + git repository instead of just "foo". Closes: #627563 + * Fix bug in --exclude introduced in 0.20110516. + + -- Joey Hess Fri, 27 May 2011 20:20:41 -0400 + +git-annex (0.20110521) unstable; urgency=low + + * status: New subcommand to show info about an annex, including its size. + * --backend now overrides any backend configured in .gitattributes files. + * Add --debug option. Closes: #627499 + + -- Joey Hess Sat, 21 May 2011 11:52:53 -0400 + +git-annex (0.20110516) unstable; urgency=low + + * Add a few tweaks to make it easy to use the Internet Archive's variant + of S3. In particular, munge key filenames to comply with the IA's filename + limits, disable encryption, support their nonstandard way of creating + buckets, and allow x-archive-* headers to be specified in initremote to + set item metadata. + * Added filename extension preserving variant backends SHA1E, SHA256E, etc. + * migrate: Use current filename when generating new key, for backends + where the filename affects the key name. + * Work around a bug in Network.URI's handling of bracketed ipv6 addresses. + + -- Joey Hess Mon, 16 May 2011 14:16:52 -0400 + +git-annex (0.20110503) unstable; urgency=low + + * Fix hasKeyCheap setting for bup and rsync special remotes. + * Add hook special remotes. + * Avoid crashing when an existing key is readded to the annex. + * unused: Now also lists files fsck places in .git/annex/bad/ + * S3: When encryption is enabled, the Amazon S3 login credentials + are stored, encrypted, in .git-annex/remotes.log, so environment + variables need not be set after the remote is initialized. + + -- Joey Hess Tue, 03 May 2011 20:56:01 -0400 + +git-annex (0.20110427) unstable; urgency=low + + * Switch back to haskell SHA library, so git-annex remains buildable on + Debian stable. + * Added rsync special remotes. This could be used, for example, to + store annexed content on rsync.net (encrypted naturally). Or anywhere else. + * Bugfix: Avoid pipeline stall when running git annex drop or fsck on a + lot of files. Possibly only occured with ghc 7. + + -- Joey Hess Wed, 27 Apr 2011 22:50:26 -0400 + +git-annex (0.20110425) unstable; urgency=low + + * Use haskell Crypto library instead of haskell SHA library. + * Remove testpack from build depends for non x86 architectures where it + is not available. The test suite will not be run if it cannot be compiled. + * Avoid using absolute paths when staging location log, as that can + confuse git when a remote's path contains a symlink. Closes: #621386 + + -- Joey Hess Mon, 25 Apr 2011 15:47:00 -0400 + +git-annex (0.20110420) unstable; urgency=low + + * Update Debian build dependencies for ghc 7. + * Debian package is now built with S3 support. + Thanks Joachim Breitner for making this possible. + * Somewhat improved memory usage of S3, still work to do. + Thanks Greg Heartsfield for ongoing work to improve the hS3 library + for git-annex. + + -- Joey Hess Thu, 21 Apr 2011 15:00:48 -0400 + +git-annex (0.20110419) unstable; urgency=low + + * Don't run gpg in batch mode, so it can prompt for passphrase when + there is no agent. + * Add missing build dep on dataenc. + * S3: Fix stalls when transferring encrypted data. + * bup: Avoid memory leak when transferring encrypted data. + + -- Joey Hess Tue, 19 Apr 2011 21:26:51 -0400 + +git-annex (0.20110417) unstable; urgency=low + + * bup is now supported as a special type of remote. + * The data sent to special remotes (Amazon S3, bup, etc) can be encrypted + using GPG for privacy. + * Use lowercase hash directories for locationlog files, to avoid + some issues with git on OSX with the mixed-case directories. + No migration is needed; the old mixed case hash directories are still + read; new information is written to the new directories. + * Unused files on remotes, particulary special remotes, can now be + identified and dropped, by using "--from remote" with git annex unused + and git annex dropunused. + * Clear up short option confusion between --from and --force (-f is now + --from, and there is no short option for --force). + * Add build depend on perlmagick so docs are consistently built. + Closes: #621410 + * Add doc-base file. Closes: #621408 + * Periodically flush git command queue, to avoid boating memory usage + too much. + * Support "sha1" and "sha512" commands on FreeBSD, and allow building + if any/all SHA commands are not available. Thanks, Fraser Tweedale + + -- Joey Hess Sun, 17 Apr 2011 12:00:24 -0400 + +git-annex (0.20110401) experimental; urgency=low + + * Amazon S3 is now supported as a special type of remote. + Warning: Encrypting data before sending it to S3 is not yet supported. + * Note that Amazon S3 support is not built in by default on Debian yet, + as hS3 is not packaged. + * fsck: Ensure that files and directories in .git/annex/objects + have proper permissions. + * Added a special type of remote called a directory remote, which + simply stores files in an arbitrary local directory. + * Bugfix: copy --to --fast never really copied, fixed. + + -- Joey Hess Fri, 01 Apr 2011 21:27:22 -0400 + +git-annex (0.20110328) experimental; urgency=low + + * annex.diskreserve can be given in arbitrary units (ie "0.5 gigabytes") + * Generalized remotes handling, laying groundwork for remotes that are + not regular git remotes. (Think Amazon S3.) + * Provide a less expensive version of `git annex copy --to`, enabled + via --fast. This assumes that location tracking information is correct, + rather than contacting the remote for every file. + * Bugfix: Keys could be received into v1 annexes from v2 annexes, via + v1 git-annex-shell. This results in some oddly named keys in the v1 + annex. Recognise and fix those keys when upgrading, instead of crashing. + + -- Joey Hess Mon, 28 Mar 2011 10:47:29 -0400 + +git-annex (0.20110325) experimental; urgency=low + + * Free space checking is now done, for transfers of data for keys + that have free space metadata. (Notably, not for SHA* keys generated + with git-annex 0.2x or earlier.) The code is believed to work on + Linux, FreeBSD, and OSX; check compile-time messages to see if it + is not enabled for your OS. + * Add annex.diskreserve config setting, to control how much free space + to reserve for other purposes and avoid using (defaults to 1 mb). + * Add --fast flag, that can enable less expensive, but also less thorough + versions of some commands. + * fsck: In fast mode, avoid checking checksums. + * unused: In fast mode, just show all existing temp files as unused, + and avoid expensive scan for other unused content. + * migrate: Support migrating v1 SHA keys to v2 SHA keys with + size information that can be used for free space checking. + * Fix space leak in fsck and drop commands. + * migrate: Bugfix for case when migrating a file results in a key that + is already present in .git/annex/objects. + * dropunused: Significantly sped up; only read unused log file once. + + -- Joey Hess Fri, 25 Mar 2011 00:47:37 -0400 + +git-annex (0.20110320) experimental; urgency=low + + * Fix dropping of files using the URL backend. + * Fix support for remotes with '.' in their names. + * Add version command to show git-annex version as well as repository + version information. + * No longer auto-upgrade to repository format 2, to avoid accidental + upgrades, etc. Use git-annex upgrade when you're ready to run this + version. + + -- Joey Hess Sun, 20 Mar 2011 16:36:33 -0400 + +git-annex (0.20110316) experimental; urgency=low + + * New repository format, annex.version=2. + * The first time git-annex is run in an old format repository, it + will automatically upgrade it to the new format, staging all + necessary changes to git. Also added a "git annex upgrade" command. + * Colons are now avoided in filenames, so bare clones of git repos + can be put on USB thumb drives formatted with vFAT or similar + filesystems. + * Added two levels of hashing to object directory and .git-annex logs, + to improve scalability with enormous numbers of annexed + objects. (With one hundred million annexed objects, each + directory would contain fewer than 1024 files.) + * The setkey, fromkey, and dropkey subcommands have changed how + the key is specified. --backend is no longer used with these. + + -- Joey Hess Wed, 16 Mar 2011 16:20:23 -0400 + +git-annex (0.24) unstable; urgency=low + + Branched the 0.24 series, which will be maintained for a while to + support v1 git-annex repos, while main development moves to the 0.2011 + series, with v2 git-annex repos. + + * Add Suggests on graphviz. Closes: #618039 + * When adding files to the annex, the symlinks pointing at the annexed + content are made to have the same mtime as the original file. + While git does not preserve that information, this allows a tool + like metastore to be used with annexed files. + (Currently this is only done on systems supporting POSIX 200809.) + + -- Joey Hess Wed, 16 Mar 2011 18:35:13 -0400 + +git-annex (0.23) unstable; urgency=low + + * Support ssh remotes with a port specified. + * whereis: New subcommand to show where a file's content has gotten to. + * Rethink filename encoding handling for display. Since filename encoding + may or may not match locale settings, any attempt to decode filenames + will fail for some files. So instead, do all output in binary mode. + + -- Joey Hess Sat, 12 Mar 2011 15:02:49 -0400 + +git-annex (0.22) unstable; urgency=low + + * Git annexes can now be attached to bare git repositories. + (Both the local and remote host must have this version of git-annex + installed for it to work.) + * Support filenames that start with a dash; when such a file is passed + to a utility it will be escaped to avoid it being interpreted as an + option. (I went a little overboard and got the type checker involved + in this, so such files are rather comprehensively supported now.) + * New backends: SHA512 SHA384 SHA256 SHA224 + (Supported on systems where corresponding shaNsum commands are available.) + * describe: New subcommand that can set or change the description of + a repository. + * Fix test suite to reap zombies. + (Zombies can be particularly annoying on OSX; thanks to Jimmy Tang + for his help eliminating the infestation... for now.) + * Make test suite not rely on a working cp -pr. + (The Unix wars are still ON!) + * Look for dir.git directories the same as git does. + * Support remote urls specified as relative paths. + * Support non-ssh remote paths that contain tilde expansions. + * fsck: Check for and repair location log damage. + * Bugfix: When fsck detected and moved away corrupt file content, it did + not update the location log. + + -- Joey Hess Fri, 04 Mar 2011 15:10:57 -0400 + +git-annex (0.21) unstable; urgency=low + + * test: Don't rely on chmod -R working. + * unannex: Fix recently introduced bug when attempting to unannex more + than one file at a time. + * test: Set git user name and email in case git can't guess values. + * Fix display of unicode filenames. + + -- Joey Hess Fri, 11 Feb 2011 23:21:08 -0400 + +git-annex (0.20) unstable; urgency=low + + * Preserve specified file ordering when instructed to act on multiple + files or directories. For example, "git annex get a b" will now always + get "a" before "b". Previously it could operate in either order. + * unannex: Commit staged changes at end, to avoid some confusing behavior + with the pre-commit hook, which would see some types of commits after + an unannex as checking in of an unlocked file. + * map: New subcommand that uses graphviz to display a nice map of + the git repository network. + * Deal with the mtl/monads-fd conflict. + * configure: Check for sha1sum. + + -- Joey Hess Tue, 08 Feb 2011 18:57:24 -0400 + +git-annex (0.19) unstable; urgency=low + + * configure: Support using the uuidgen command if the uuid command is + not available. + * Allow --exclude to be specified more than once. + * There are now three levels of repository trust. + * untrust: Now marks the current repository as untrusted. + * semitrust: Now restores the default trust level. (What untrust used to do.) + * fsck, drop: Take untrusted repositories into account. + * Bugfix: Files were copied from trusted remotes first even if their + annex.cost was higher than other remotes. + * Improved temp file handling. Transfers of content can now be resumed + from temp files later; the resume does not have to be the immediate + next git-annex run. + * unused: Include partially transferred content in the list. + * Bugfix: Running a second git-annex while a first has a transfer in + progress no longer deletes the first processes's temp file. + + -- Joey Hess Fri, 28 Jan 2011 14:31:37 -0400 + +git-annex (0.18) unstable; urgency=low + + * Bugfix: `copy --to` and `move --to` forgot to stage location log changes + after transferring the file to the remote repository. + (Did not affect ssh remotes.) + * fsck: Fix bug in moving of corrupted files to .git/annex/bad/ + * migrate: Fix support for --backend option. + * unlock: Fix behavior when file content is not present. + * Test suite improvements. Current top-level test coverage: 80% + + -- Joey Hess Fri, 14 Jan 2011 14:17:44 -0400 + +git-annex (0.17) unstable; urgency=low + + * unannex: Now skips files whose content is not present, rather than + it being an error. + * New migrate subcommand can be used to switch files to using a different + backend, safely and with no duplication of content. + * bugfix: Fix crash caused by empty key name. (Thanks Henrik for reporting.) + + -- Joey Hess Sun, 09 Jan 2011 10:04:11 -0400 + +git-annex (0.16) unstable; urgency=low + + * git-annex-shell: Avoid exposing any git repo config except for the + annex.uuid when doing configlist. + * bugfix: Running `move --to` with a remote whose UUID was not yet known + could result in git-annex not recording on the local side where the + file was moved to. This could not result in data loss, or even a + significant problem, since the remote *did* record that it had the file. + * Also, add a general guard to detect attempts to record information + about repositories with missing UUIDs. + * bugfix: Running `move --to` with a non-ssh remote failed. + * bugfix: Running `copy --to` with a non-ssh remote actually did a move. + * Many test suite improvements. Current top-level test coverage: 65% + + -- Joey Hess Fri, 07 Jan 2011 14:33:13 -0400 + +git-annex (0.15) unstable; urgency=low + + * Support scp-style urls for remotes (host:path). + * Support ssh urls containing "~". + * Add trust and untrust subcommands, to allow configuring repositories + that are trusted to retain files without explicit checking. + * Fix bug in numcopies handling when multiple remotes pointed to the + same repository. + * Introduce the git-annex-shell command. It's now possible to make + a user have it as a restricted login shell, similar to git-shell. + * Note that git-annex will always use git-annex-shell when accessing + a ssh remote, so all of your remotes need to be upgraded to this + version of git-annex at the same time. + * Now rsync is exclusively used for copying files to and from remotes. + scp is not longer supported. + + -- Joey Hess Fri, 31 Dec 2010 22:00:52 -0400 + +git-annex (0.14) unstable; urgency=low + + * Bugfix to git annex unused in a repository with nothing yet annexed. + * Support upgrading from a v0 annex with nothing in it. + * Avoid multiple calls to git ls-files when passed eg, "*". + + -- Joey Hess Fri, 24 Dec 2010 17:38:48 -0400 + +git-annex (0.13) unstable; urgency=low + + * Makefile: Install man page and html (when built). + * Makefile: Add GHCFLAGS variable. + * Fix upgrade from 0.03. + * Support remotes using git+ssh and ssh+git as protocol. + Closes: #607056 + + -- Joey Hess Tue, 14 Dec 2010 13:05:10 -0400 + +git-annex (0.12) unstable; urgency=low + + * Add --exclude option to exclude files from processing. + * mwdn2man: Fix a bug in newline supression. Closes: #606578 + * Bugfix to git annex add of an unlocked file in a subdir. Closes: #606579 + * Makefile: Add PREFIX variable. + + -- Joey Hess Sat, 11 Dec 2010 17:32:00 -0400 + +git-annex (0.11) unstable; urgency=low + + * If available, rsync will be used for file transfers from remote + repositories. This allows resuming interrupted transfers. + * Added remote.annex-rsync-options. + * Avoid deleting temp files when rsync fails. + * Improve detection of version 0 repos. + * Add uninit subcommand. Closes: #605749 + + -- Joey Hess Sat, 04 Dec 2010 17:27:42 -0400 + +git-annex (0.10) unstable; urgency=low + + * In .gitattributes, the annex.numcopies attribute can be used + to control the number of copies to retain of different types of files. + * Bugfix: Always correctly handle gitattributes when in a subdirectory of + the repository. (Had worked ok for ones like "*.mp3", but failed for + ones like "dir/*".) + * fsck: Fix warning about not enough copies of a file, when locations + are known, but are not available in currently configured remotes. + * precommit: Optimise to avoid calling git-check-attr more than once. + * The git-annex-backend attribute has been renamed to annex.backend. + + -- Joey Hess Sun, 28 Nov 2010 19:28:05 -0400 + +git-annex (0.09) unstable; urgency=low + + * Add copy subcommand. + * Fix bug in setkey subcommand triggered by move --to. + + -- Joey Hess Sat, 27 Nov 2010 17:14:59 -0400 + +git-annex (0.08) unstable; urgency=low + + * Fix `git annex add ../foo` (when ran in a subdir of the repo). + * Add configure step to build process. + * Only use cp -a if it is supported, falling back to cp -p or plain cp + as needed for portability. + * cp --reflink=auto is used if supported, and will make git annex unlock + much faster on filesystems like btrfs that support copy on write. + + -- Joey Hess Sun, 21 Nov 2010 13:45:44 -0400 + +git-annex (0.07) unstable; urgency=low + + * find: New subcommand. + * unused: New subcommand, finds unused data. (Split out from fsck.) + * dropunused: New subcommand, provides for easy dropping of unused keys + by number, as listed by the unused subcommand. + * fsck: Print warnings to stderr; --quiet can now be used to only see + problems. + + -- Joey Hess Mon, 15 Nov 2010 18:41:50 -0400 + +git-annex (0.06) unstable; urgency=low + + * fsck: Check if annex.numcopies is satisfied. + * fsck: Verify the sha1 of files when the SHA1 backend is used. + * fsck: Verify the size of files when the WORM backend is used. + * fsck: Allow specifying individual files if fscking everything + is not desired. + * fsck: Fix bug, introduced in 0.04, in detection of unused data. + + -- Joey Hess Sat, 13 Nov 2010 16:24:29 -0400 + +git-annex (0.05) unstable; urgency=low + + * Optimize both pre-commit and lock subcommands to not call git diff + on every file being committed/locked. + (This actually also works around a bug in ghc, that caused + git-annex 0.04 pre-commit to sometimes corrupt filename being read + from git ls-files and fail. + See + The excessive number of calls made by pre-commit exposed the ghc bug. + Thanks Josh Triplett for the debugging.) + * Build with -O2. + + -- Joey Hess Thu, 11 Nov 2010 18:31:09 -0400 + +git-annex (0.04) unstable; urgency=low + + * Add unlock subcommand, which replaces the symlink with a copy of + the file's content in preparation of changing it. The "edit" subcommand + is an alias for unlock. + * Add lock subcommand. + * Unlocked files will now automatically be added back into the annex when + committed (and the updated symlink committed), by some magic in the + pre-commit hook. + * The SHA1 backend is now fully usable. + * Add annex.version, which will be used to automate upgrades + between incompatible versions. + * Reorganised the layout of .git/annex/ + * The new layout will be automatically upgraded to the first time + git-annex is used in a repository with the old layout. + * Note that git-annex 0.04 cannot transfer content from old repositories + that have not yet been upgraded. + * Annexed file contents are now made unwritable and put in unwriteable + directories, to avoid them accidentially being removed or modified. + (Thanks Josh Triplett for the idea.) + * Add build dep on libghc6-testpack-dev. Closes: #603016 + * Avoid using runghc to run test suite as it is not available on all + architectures. Closes: #603006 + + -- Joey Hess Wed, 10 Nov 2010 14:23:23 -0400 + +git-annex (0.03) unstable; urgency=low + + * Fix support for file:// remotes. + * Add --verbose + * Fix SIGINT handling. + * Fix handling of files with unusual characters in their name. + * Fixed memory leak; git-annex no longer reads the whole file list + from git before starting, and will be much faster with large repos. + * Fix crash on unknown symlinks. + * Added remote.annex-scp-options and remote.annex-ssh-options. + * The backends to use when adding different sets of files can be configured + via gitattributes. + * In .gitattributes, the git-annex-backend attribute can be set to the + names of backends to use when adding different types of files. + * Add fsck subcommand. (For now it only finds unused key contents in the + annex.) + + -- Joey Hess Sun, 07 Nov 2010 18:26:04 -0400 + +git-annex (0.02) unstable; urgency=low + + * Can scp annexed files from remote hosts, and check remote hosts for + file content when dropping files. + * New move subcommand, that makes it easy to move file contents from + or to a remote. + * New fromkey subcommand, for registering urls, etc. + * git-annex init will now set up a pre-commit hook that fixes up symlinks + before they are committed, to ensure that moving symlinks around does not + break them. + * More intelligent and fast staging of modified files; git add coalescing. + * Add remote.annex-ignore git config setting to allow completly disabling + a given remote. + * --from/--to can be used to control the remote repository that git-annex + uses. + * --quiet can be used to avoid verbose output + * New plumbing-level dropkey and addkey subcommands. + * Lots of bug fixes. + + -- Joey Hess Wed, 27 Oct 2010 16:39:29 -0400 + +git-annex (0.01) unstable; urgency=low + + * First prerelease. + + -- Joey Hess Wed, 20 Oct 2010 12:54:24 -0400 diff --git a/debian/compat b/debian/compat new file mode 100644 index 0000000000..ec635144f6 --- /dev/null +++ b/debian/compat @@ -0,0 +1 @@ +9 diff --git a/debian/control b/debian/control new file mode 100644 index 0000000000..49c0bde930 --- /dev/null +++ b/debian/control @@ -0,0 +1,87 @@ +Source: git-annex +Section: utils +Priority: optional +Build-Depends: + debhelper (>= 9), + ghc (>= 7.4), + libghc-mtl-dev (>= 2.1.1), + libghc-missingh-dev, + libghc-hslogger-dev, + libghc-pcre-light-dev, + libghc-sha-dev, + libghc-regex-tdfa-dev [!mips !mipsel !s390], + libghc-dataenc-dev, + libghc-utf8-string-dev, + libghc-hs3-dev (>= 0.5.6), + libghc-dav-dev (>= 0.3) [amd64 i386 kfreebsd-amd64 kfreebsd-i386 powerpc sparc], + libghc-quickcheck2-dev, + libghc-monad-control-dev (>= 0.3), + libghc-monadcatchio-transformers-dev, + libghc-unix-compat-dev, + libghc-dlist-dev, + libghc-uuid-dev, + libghc-json-dev, + libghc-ifelse-dev, + libghc-bloomfilter-dev, + libghc-edit-distance-dev, + libghc-extensible-exceptions-dev, + libghc-hinotify-dev [linux-any], + libghc-stm-dev (>= 2.3), + libghc-dbus-dev (>= 0.10.3) [linux-any], + libghc-yesod-dev [i386 amd64 kfreebsd-i386 kfreebsd-amd64 powerpc sparc], + libghc-yesod-static-dev [i386 amd64 kfreebsd-i386 kfreebsd-amd64 powerpc sparc], + libghc-yesod-default-dev [i386 amd64 kfreebsd-amd64 powerpc sparc], + libghc-hamlet-dev [i386 amd64 kfreebsd-i386 kfreebsd-amd64 powerpc sparc], + libghc-clientsession-dev [i386 amd64 kfreebsd-i386 kfreebsd-amd64 powerpc sparc], + libghc-warp-dev [i386 amd64 kfreebsd-i386 kfreebsd-amd64 powerpc sparc], + libghc-wai-dev [i386 amd64 kfreebsd-i386 kfreebsd-amd64 powerpc sparc], + libghc-wai-logger-dev [i386 amd64 kfreebsd-i386 kfreebsd-amd64 powerpc sparc], + libghc-case-insensitive-dev, + libghc-http-types-dev, + libghc-blaze-builder-dev, + libghc-crypto-api-dev, + libghc-network-multicast-dev, + libghc-network-info-dev [linux-any kfreebsd-any], + libghc-safesemaphore-dev, + libghc-network-protocol-xmpp-dev (>= 0.4.3-1+b1), + libghc-gnutls-dev (>= 0.1.4), + libghc-xml-types-dev, + libghc-async-dev, + libghc-http-dev, + libghc-feed-dev, + ikiwiki, + perlmagick, + git, + rsync, + wget, + curl, + openssh-client, +Maintainer: Joey Hess +Standards-Version: 3.9.4 +Vcs-Git: git://git.kitenet.net/git-annex +Homepage: http://git-annex.branchable.com/ + +Package: git-annex +Architecture: any +Section: utils +Depends: ${misc:Depends}, ${shlibs:Depends}, + git (>= 1:1.7.7.6), + rsync, + wget, + curl, + openssh-client (>= 1:5.6p1) +Recommends: lsof, gnupg, bind9-host +Suggests: graphviz, bup, libnss-mdns +Description: manage files with git, without checking their contents into git + git-annex allows managing files with git, without checking the file + contents into git. While that may seem paradoxical, it is useful when + dealing with files larger than git can currently easily handle, whether due + to limitations in memory, time, or disk space. + . + Even without file content tracking, being able to manage files with git, + move files around and delete files with versioned directory trees, and use + branches and distributed clones, are all very handy reasons to use git. And + annexed files can co-exist in the same git repository with regularly + versioned files, which is convenient for maintaining documents, Makefiles, + etc that are associated with annexed files but that benefit from full + revision control. diff --git a/debian/copyright b/debian/copyright new file mode 100644 index 0000000000..5a667adf74 --- /dev/null +++ b/debian/copyright @@ -0,0 +1,784 @@ +Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Source: native package + +Files: * +Copyright: © 2010-2013 Joey Hess +License: GPL-3+ + +Files: Assistant/WebApp.hs Assistant/WebApp/* templates/* static/* +Copyright: © 2012-2013 Joey Hess +License: AGPL-3+ + +Files: Utility/ThreadScheduler.hs +Copyright: 2011 Bas van Dijk & Roel van Dijk + 2012 Joey Hess +License: GPL-3+ + +Files: Utility/Gpg/Types.hs +Copyright: 2013 guilhem +License: GPL-3+ + +Files: doc/logo* */favicon.ico standalone/osx/git-annex.app/Contents/Resources/git-annex.icns standalone/android/icons/* +Copyright: 2007 Henrik Nyh + 2010 Joey Hess + 2013 John Lawrence +License: other + Free to modify and redistribute with due credit, and obviously free to use. + +Files: Utility/Mounts.hsc +Copyright: Volker Wysk +License: LGPL-2.1+ + +Files: Utility/libmounts.c +Copyright: 1980, 1989, 1993, 1994 The Regents of the University of California + 2001 David Rufino + 2012 Joey Hess +License: BSD-3-clause + * Copyright (c) 1980, 1989, 1993, 1994 + * The Regents of the University of California. All rights reserved. + * Copyright (c) 2001 + * David Rufino + * Copyright 2012 + * Joey Hess + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the University nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + +Files: static/jquery* +Copyright: © 2005-2011 by John Resig, Branden Aaron & Jörn Zaefferer + © 2011 The Dojo Foundation +License: MIT or GPL-2 + The full text of version 2 of the GPL is distributed in + /usr/share/common-licenses/GPL-2 on Debian systems. The text of the MIT + license follows: + . + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + . + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + . + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Files: static/*/bootstrap* static/img/glyphicons-halflings* +Copyright: 2012 Twitter, Inc. +License: Apache-2.0 + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + . + http://www.apache.org/licenses/LICENSE-2.0 + . + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + . + The complete text of the Apache License is distributed in + /usr/share/common-licenses/Apache-2.0 on Debian systems. + +License: GPL-3+ + The full text of version 3 of the GPL is distributed as doc/license/GPL in + this package's source, or in /usr/share/common-licenses/GPL-3 on + Debian systems. + +License: LGPL-2.1+ + The full text of version 2.1 of the LGPL is distributed as doc/license/LGPL + in this package's source, or in /usr/share/common-licenses/LGPL-2.1 + on Debian systems. + +License: AGPL-3+ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + . + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + . + Preamble + . + The GNU Affero General Public License is a free, copyleft license for + software and other kinds of works, specifically designed to ensure + cooperation with the community in the case of network server software. + . + The licenses for most software and other practical works are designed + to take away your freedom to share and change the works. By contrast, + our General Public Licenses are intended to guarantee your freedom to + share and change all versions of a program--to make sure it remains free + software for all its users. + . + When we speak of free software, we are referring to freedom, not + price. Our General Public Licenses are designed to make sure that you + have the freedom to distribute copies of free software (and charge for + them if you wish), that you receive source code or can get it if you + want it, that you can change the software or use pieces of it in new + free programs, and that you know you can do these things. + . + Developers that use our General Public Licenses protect your rights + with two steps: (1) assert copyright on the software, and (2) offer + you this License which gives you legal permission to copy, distribute + and/or modify the software. + . + A secondary benefit of defending all users' freedom is that + improvements made in alternate versions of the program, if they + receive widespread use, become available for other developers to + incorporate. Many developers of free software are heartened and + encouraged by the resulting cooperation. However, in the case of + software used on network servers, this result may fail to come about. + The GNU General Public License permits making a modified version and + letting the public access it on a server without ever releasing its + source code to the public. + . + The GNU Affero General Public License is designed specifically to + ensure that, in such cases, the modified source code becomes available + to the community. It requires the operator of a network server to + provide the source code of the modified version running there to the + users of that server. Therefore, public use of a modified version, on + a publicly accessible server, gives the public access to the source + code of the modified version. + . + An older license, called the Affero General Public License and + published by Affero, was designed to accomplish similar goals. This is + a different license, not a version of the Affero GPL, but Affero has + released a new version of the Affero GPL which permits relicensing under + this license. + . + The precise terms and conditions for copying, distribution and + modification follow. + . + TERMS AND CONDITIONS + . + 0. Definitions. + . + "This License" refers to version 3 of the GNU Affero General Public License. + . + "Copyright" also means copyright-like laws that apply to other kinds of + works, such as semiconductor masks. + . + "The Program" refers to any copyrightable work licensed under this + License. Each licensee is addressed as "you". "Licensees" and + "recipients" may be individuals or organizations. + . + To "modify" a work means to copy from or adapt all or part of the work + in a fashion requiring copyright permission, other than the making of an + exact copy. The resulting work is called a "modified version" of the + earlier work or a work "based on" the earlier work. + . + A "covered work" means either the unmodified Program or a work based + on the Program. + . + To "propagate" a work means to do anything with it that, without + permission, would make you directly or secondarily liable for + infringement under applicable copyright law, except executing it on a + computer or modifying a private copy. Propagation includes copying, + distribution (with or without modification), making available to the + public, and in some countries other activities as well. + . + To "convey" a work means any kind of propagation that enables other + parties to make or receive copies. Mere interaction with a user through + a computer network, with no transfer of a copy, is not conveying. + . + An interactive user interface displays "Appropriate Legal Notices" + to the extent that it includes a convenient and prominently visible + feature that (1) displays an appropriate copyright notice, and (2) + tells the user that there is no warranty for the work (except to the + extent that warranties are provided), that licensees may convey the + work under this License, and how to view a copy of this License. If + the interface presents a list of user commands or options, such as a + menu, a prominent item in the list meets this criterion. + . + 1. Source Code. + . + The "source code" for a work means the preferred form of the work + for making modifications to it. "Object code" means any non-source + form of a work. + . + A "Standard Interface" means an interface that either is an official + standard defined by a recognized standards body, or, in the case of + interfaces specified for a particular programming language, one that + is widely used among developers working in that language. + . + The "System Libraries" of an executable work include anything, other + than the work as a whole, that (a) is included in the normal form of + packaging a Major Component, but which is not part of that Major + Component, and (b) serves only to enable use of the work with that + Major Component, or to implement a Standard Interface for which an + implementation is available to the public in source code form. A + "Major Component", in this context, means a major essential component + (kernel, window system, and so on) of the specific operating system + (if any) on which the executable work runs, or a compiler used to + produce the work, or an object code interpreter used to run it. + . + The "Corresponding Source" for a work in object code form means all + the source code needed to generate, install, and (for an executable + work) run the object code and to modify the work, including scripts to + control those activities. However, it does not include the work's + System Libraries, or general-purpose tools or generally available free + programs which are used unmodified in performing those activities but + which are not part of the work. For example, Corresponding Source + includes interface definition files associated with source files for + the work, and the source code for shared libraries and dynamically + linked subprograms that the work is specifically designed to require, + such as by intimate data communication or control flow between those + subprograms and other parts of the work. + . + The Corresponding Source need not include anything that users + can regenerate automatically from other parts of the Corresponding + Source. + . + The Corresponding Source for a work in source code form is that + same work. + . + 2. Basic Permissions. + . + All rights granted under this License are granted for the term of + copyright on the Program, and are irrevocable provided the stated + conditions are met. This License explicitly affirms your unlimited + permission to run the unmodified Program. The output from running a + covered work is covered by this License only if the output, given its + content, constitutes a covered work. This License acknowledges your + rights of fair use or other equivalent, as provided by copyright law. + . + You may make, run and propagate covered works that you do not + convey, without conditions so long as your license otherwise remains + in force. You may convey covered works to others for the sole purpose + of having them make modifications exclusively for you, or provide you + with facilities for running those works, provided that you comply with + the terms of this License in conveying all material for which you do + not control copyright. Those thus making or running the covered works + for you must do so exclusively on your behalf, under your direction + and control, on terms that prohibit them from making any copies of + your copyrighted material outside their relationship with you. + . + Conveying under any other circumstances is permitted solely under + the conditions stated below. Sublicensing is not allowed; section 10 + makes it unnecessary. + . + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + . + No covered work shall be deemed part of an effective technological + measure under any applicable law fulfilling obligations under article + 11 of the WIPO copyright treaty adopted on 20 December 1996, or + similar laws prohibiting or restricting circumvention of such + measures. + . + When you convey a covered work, you waive any legal power to forbid + circumvention of technological measures to the extent such circumvention + is effected by exercising rights under this License with respect to + the covered work, and you disclaim any intention to limit operation or + modification of the work as a means of enforcing, against the work's + users, your or third parties' legal rights to forbid circumvention of + technological measures. + . + 4. Conveying Verbatim Copies. + . + You may convey verbatim copies of the Program's source code as you + receive it, in any medium, provided that you conspicuously and + appropriately publish on each copy an appropriate copyright notice; + keep intact all notices stating that this License and any + non-permissive terms added in accord with section 7 apply to the code; + keep intact all notices of the absence of any warranty; and give all + recipients a copy of this License along with the Program. + . + You may charge any price or no price for each copy that you convey, + and you may offer support or warranty protection for a fee. + . + 5. Conveying Modified Source Versions. + . + You may convey a work based on the Program, or the modifications to + produce it from the Program, in the form of source code under the + terms of section 4, provided that you also meet all of these conditions: + . + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + . + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + . + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + . + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + . + A compilation of a covered work with other separate and independent + works, which are not by their nature extensions of the covered work, + and which are not combined with it such as to form a larger program, + in or on a volume of a storage or distribution medium, is called an + "aggregate" if the compilation and its resulting copyright are not + used to limit the access or legal rights of the compilation's users + beyond what the individual works permit. Inclusion of a covered work + in an aggregate does not cause this License to apply to the other + parts of the aggregate. + . + 6. Conveying Non-Source Forms. + . + You may convey a covered work in object code form under the terms + of sections 4 and 5, provided that you also convey the + machine-readable Corresponding Source under the terms of this License, + in one of these ways: + . + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + . + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + . + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + . + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + . + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + . + A separable portion of the object code, whose source code is excluded + from the Corresponding Source as a System Library, need not be + included in conveying the object code work. + . + A "User Product" is either (1) a "consumer product", which means any + tangible personal property which is normally used for personal, family, + or household purposes, or (2) anything designed or sold for incorporation + into a dwelling. In determining whether a product is a consumer product, + doubtful cases shall be resolved in favor of coverage. For a particular + product received by a particular user, "normally used" refers to a + typical or common use of that class of product, regardless of the status + of the particular user or of the way in which the particular user + actually uses, or expects or is expected to use, the product. A product + is a consumer product regardless of whether the product has substantial + commercial, industrial or non-consumer uses, unless such uses represent + the only significant mode of use of the product. + . + "Installation Information" for a User Product means any methods, + procedures, authorization keys, or other information required to install + and execute modified versions of a covered work in that User Product from + a modified version of its Corresponding Source. The information must + suffice to ensure that the continued functioning of the modified object + code is in no case prevented or interfered with solely because + modification has been made. + . + If you convey an object code work under this section in, or with, or + specifically for use in, a User Product, and the conveying occurs as + part of a transaction in which the right of possession and use of the + User Product is transferred to the recipient in perpetuity or for a + fixed term (regardless of how the transaction is characterized), the + Corresponding Source conveyed under this section must be accompanied + by the Installation Information. But this requirement does not apply + if neither you nor any third party retains the ability to install + modified object code on the User Product (for example, the work has + been installed in ROM). + . + The requirement to provide Installation Information does not include a + requirement to continue to provide support service, warranty, or updates + for a work that has been modified or installed by the recipient, or for + the User Product in which it has been modified or installed. Access to a + network may be denied when the modification itself materially and + adversely affects the operation of the network or violates the rules and + protocols for communication across the network. + . + Corresponding Source conveyed, and Installation Information provided, + in accord with this section must be in a format that is publicly + documented (and with an implementation available to the public in + source code form), and must require no special password or key for + unpacking, reading or copying. + . + 7. Additional Terms. + . + "Additional permissions" are terms that supplement the terms of this + License by making exceptions from one or more of its conditions. + Additional permissions that are applicable to the entire Program shall + be treated as though they were included in this License, to the extent + that they are valid under applicable law. If additional permissions + apply only to part of the Program, that part may be used separately + under those permissions, but the entire Program remains governed by + this License without regard to the additional permissions. + . + When you convey a copy of a covered work, you may at your option + remove any additional permissions from that copy, or from any part of + it. (Additional permissions may be written to require their own + removal in certain cases when you modify the work.) You may place + additional permissions on material, added by you to a covered work, + for which you have or can give appropriate copyright permission. + . + Notwithstanding any other provision of this License, for material you + add to a covered work, you may (if authorized by the copyright holders of + that material) supplement the terms of this License with terms: + . + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + . + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + . + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + . + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + . + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + . + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + . + All other non-permissive additional terms are considered "further + restrictions" within the meaning of section 10. If the Program as you + received it, or any part of it, contains a notice stating that it is + governed by this License along with a term that is a further + restriction, you may remove that term. If a license document contains + a further restriction but permits relicensing or conveying under this + License, you may add to a covered work material governed by the terms + of that license document, provided that the further restriction does + not survive such relicensing or conveying. + . + If you add terms to a covered work in accord with this section, you + must place, in the relevant source files, a statement of the + additional terms that apply to those files, or a notice indicating + where to find the applicable terms. + . + Additional terms, permissive or non-permissive, may be stated in the + form of a separately written license, or stated as exceptions; + the above requirements apply either way. + . + 8. Termination. + . + You may not propagate or modify a covered work except as expressly + provided under this License. Any attempt otherwise to propagate or + modify it is void, and will automatically terminate your rights under + this License (including any patent licenses granted under the third + paragraph of section 11). + . + However, if you cease all violation of this License, then your + license from a particular copyright holder is reinstated (a) + provisionally, unless and until the copyright holder explicitly and + finally terminates your license, and (b) permanently, if the copyright + holder fails to notify you of the violation by some reasonable means + prior to 60 days after the cessation. + . + Moreover, your license from a particular copyright holder is + reinstated permanently if the copyright holder notifies you of the + violation by some reasonable means, this is the first time you have + received notice of violation of this License (for any work) from that + copyright holder, and you cure the violation prior to 30 days after + your receipt of the notice. + . + Termination of your rights under this section does not terminate the + licenses of parties who have received copies or rights from you under + this License. If your rights have been terminated and not permanently + reinstated, you do not qualify to receive new licenses for the same + material under section 10. + . + 9. Acceptance Not Required for Having Copies. + . + You are not required to accept this License in order to receive or + run a copy of the Program. Ancillary propagation of a covered work + occurring solely as a consequence of using peer-to-peer transmission + to receive a copy likewise does not require acceptance. However, + nothing other than this License grants you permission to propagate or + modify any covered work. These actions infringe copyright if you do + not accept this License. Therefore, by modifying or propagating a + covered work, you indicate your acceptance of this License to do so. + . + 10. Automatic Licensing of Downstream Recipients. + . + Each time you convey a covered work, the recipient automatically + receives a license from the original licensors, to run, modify and + propagate that work, subject to this License. You are not responsible + for enforcing compliance by third parties with this License. + . + An "entity transaction" is a transaction transferring control of an + organization, or substantially all assets of one, or subdividing an + organization, or merging organizations. If propagation of a covered + work results from an entity transaction, each party to that + transaction who receives a copy of the work also receives whatever + licenses to the work the party's predecessor in interest had or could + give under the previous paragraph, plus a right to possession of the + Corresponding Source of the work from the predecessor in interest, if + the predecessor has it or can get it with reasonable efforts. + . + You may not impose any further restrictions on the exercise of the + rights granted or affirmed under this License. For example, you may + not impose a license fee, royalty, or other charge for exercise of + rights granted under this License, and you may not initiate litigation + (including a cross-claim or counterclaim in a lawsuit) alleging that + any patent claim is infringed by making, using, selling, offering for + sale, or importing the Program or any portion of it. + . + 11. Patents. + . + A "contributor" is a copyright holder who authorizes use under this + License of the Program or a work on which the Program is based. The + work thus licensed is called the contributor's "contributor version". + . + A contributor's "essential patent claims" are all patent claims + owned or controlled by the contributor, whether already acquired or + hereafter acquired, that would be infringed by some manner, permitted + by this License, of making, using, or selling its contributor version, + but do not include claims that would be infringed only as a + consequence of further modification of the contributor version. For + purposes of this definition, "control" includes the right to grant + patent sublicenses in a manner consistent with the requirements of + this License. + . + Each contributor grants you a non-exclusive, worldwide, royalty-free + patent license under the contributor's essential patent claims, to + make, use, sell, offer for sale, import and otherwise run, modify and + propagate the contents of its contributor version. + . + In the following three paragraphs, a "patent license" is any express + agreement or commitment, however denominated, not to enforce a patent + (such as an express permission to practice a patent or covenant not to + sue for patent infringement). To "grant" such a patent license to a + party means to make such an agreement or commitment not to enforce a + patent against the party. + . + If you convey a covered work, knowingly relying on a patent license, + and the Corresponding Source of the work is not available for anyone + to copy, free of charge and under the terms of this License, through a + publicly available network server or other readily accessible means, + then you must either (1) cause the Corresponding Source to be so + available, or (2) arrange to deprive yourself of the benefit of the + patent license for this particular work, or (3) arrange, in a manner + consistent with the requirements of this License, to extend the patent + license to downstream recipients. "Knowingly relying" means you have + actual knowledge that, but for the patent license, your conveying the + covered work in a country, or your recipient's use of the covered work + in a country, would infringe one or more identifiable patents in that + country that you have reason to believe are valid. + . + If, pursuant to or in connection with a single transaction or + arrangement, you convey, or propagate by procuring conveyance of, a + covered work, and grant a patent license to some of the parties + receiving the covered work authorizing them to use, propagate, modify + or convey a specific copy of the covered work, then the patent license + you grant is automatically extended to all recipients of the covered + work and works based on it. + . + A patent license is "discriminatory" if it does not include within + the scope of its coverage, prohibits the exercise of, or is + conditioned on the non-exercise of one or more of the rights that are + specifically granted under this License. You may not convey a covered + work if you are a party to an arrangement with a third party that is + in the business of distributing software, under which you make payment + to the third party based on the extent of your activity of conveying + the work, and under which the third party grants, to any of the + parties who would receive the covered work from you, a discriminatory + patent license (a) in connection with copies of the covered work + conveyed by you (or copies made from those copies), or (b) primarily + for and in connection with specific products or compilations that + contain the covered work, unless you entered into that arrangement, + or that patent license was granted, prior to 28 March 2007. + . + Nothing in this License shall be construed as excluding or limiting + any implied license or other defenses to infringement that may + otherwise be available to you under applicable patent law. + . + 12. No Surrender of Others' Freedom. + . + If conditions are imposed on you (whether by court order, agreement or + otherwise) that contradict the conditions of this License, they do not + excuse you from the conditions of this License. If you cannot convey a + covered work so as to satisfy simultaneously your obligations under this + License and any other pertinent obligations, then as a consequence you may + not convey it at all. For example, if you agree to terms that obligate you + to collect a royalty for further conveying from those to whom you convey + the Program, the only way you could satisfy both those terms and this + License would be to refrain entirely from conveying the Program. + . + 13. Remote Network Interaction; Use with the GNU General Public License. + . + Notwithstanding any other provision of this License, if you modify the + Program, your modified version must prominently offer all users + interacting with it remotely through a computer network (if your version + supports such interaction) an opportunity to receive the Corresponding + Source of your version by providing access to the Corresponding Source + from a network server at no charge, through some standard or customary + means of facilitating copying of software. This Corresponding Source + shall include the Corresponding Source for any work covered by version 3 + of the GNU General Public License that is incorporated pursuant to the + following paragraph. + . + Notwithstanding any other provision of this License, you have + permission to link or combine any covered work with a work licensed + under version 3 of the GNU General Public License into a single + combined work, and to convey the resulting work. The terms of this + License will continue to apply to the part which is the covered work, + but the work with which it is combined will remain governed by version + 3 of the GNU General Public License. + . + 14. Revised Versions of this License. + . + The Free Software Foundation may publish revised and/or new versions of + the GNU Affero General Public License from time to time. Such new versions + will be similar in spirit to the present version, but may differ in detail to + address new problems or concerns. + . + Each version is given a distinguishing version number. If the + Program specifies that a certain numbered version of the GNU Affero General + Public License "or any later version" applies to it, you have the + option of following the terms and conditions either of that numbered + version or of any later version published by the Free Software + Foundation. If the Program does not specify a version number of the + GNU Affero General Public License, you may choose any version ever published + by the Free Software Foundation. + . + If the Program specifies that a proxy can decide which future + versions of the GNU Affero General Public License can be used, that proxy's + public statement of acceptance of a version permanently authorizes you + to choose that version for the Program. + . + Later license versions may give you additional or different + permissions. However, no additional obligations are imposed on any + author or copyright holder as a result of your choosing to follow a + later version. + . + 15. Disclaimer of Warranty. + . + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY + APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT + HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY + OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, + THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM + IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF + ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + . + 16. Limitation of Liability. + . + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING + WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS + THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY + GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE + USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF + DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD + PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), + EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF + SUCH DAMAGES. + . + 17. Interpretation of Sections 15 and 16. + . + If the disclaimer of warranty and limitation of liability provided + above cannot be given local legal effect according to their terms, + reviewing courts shall apply local law that most closely approximates + an absolute waiver of all civil liability in connection with the + Program, unless a warranty or assumption of liability accompanies a + copy of the Program in return for a fee. + . + END OF TERMS AND CONDITIONS + . + How to Apply These Terms to Your New Programs + . + If you develop a new program, and you want it to be of the greatest + possible use to the public, the best way to achieve this is to make it + free software which everyone can redistribute and change under these terms. + . + To do so, attach the following notices to the program. It is safest + to attach them to the start of each source file to most effectively + state the exclusion of warranty; and each file should have at least + the "copyright" line and a pointer to where the full notice is found. + . + + Copyright (C) + . + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + . + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + . + Also add information on how to contact you by electronic and paper mail. + . + If your software can interact with users remotely through a computer + network, you should also make sure that it provides a way for users to + get its source. For example, if your program is a web application, its + interface could display a "Source" link that leads users to an archive + of the code. There are many ways you could offer source, and different + solutions will be better for different programs; see section 13 for the + specific requirements. + . + You should also get your employer (if you work as a programmer) or school, + if any, to sign a "copyright disclaimer" for the program, if necessary. + For more information on this, and how to apply and follow the GNU AGPL, see + . diff --git a/debian/doc-base b/debian/doc-base new file mode 100644 index 0000000000..f71a233333 --- /dev/null +++ b/debian/doc-base @@ -0,0 +1,9 @@ +Document: git-annex +Title: git-annex documentation +Author: Joey Hess +Abstract: All the documentation from git-annex's website. +Section: File Management + +Format: HTML +Index: /usr/share/doc/git-annex/html/index.html +Files: /usr/share/doc/git-annex/html/*.html diff --git a/debian/menu b/debian/menu new file mode 100644 index 0000000000..20790a94c3 --- /dev/null +++ b/debian/menu @@ -0,0 +1,2 @@ +?package(git-annex):needs="X11" section="Applications/Network/File Transfer" \ + title="git-annex assistant" command="git-annex webapp" diff --git a/debian/rules b/debian/rules new file mode 100755 index 0000000000..3a0511fa67 --- /dev/null +++ b/debian/rules @@ -0,0 +1,14 @@ +#!/usr/bin/make -f + +# Avoid using cabal, as it writes to $HOME +export CABAL=./Setup + +# Do use the changelog's version number, rather than making one up. +export RELEASE_BUILD=1 + +%: + dh $@ + +# Not intended for use by anyone except the author. +announcedir: + @echo ${HOME}/src/git-annex/doc/news diff --git a/doc/Android.mdwn b/doc/Android.mdwn new file mode 100644 index 0000000000..71263ea8dd --- /dev/null +++ b/doc/Android.mdwn @@ -0,0 +1,53 @@ +git-annex is now available for Android. This includes the +[[git-annex assistant|/assistant]], for easy syncing between your Android +and other devices. You do not need to root your Android to use git-annex. + +[[Android installation instructions|/install/android]] + +When you run the git-annex Android app, two windows will open. The first is +a terminal window, and the second is a web browser showing the git-annex +webapp. + +[[!img apps.png alt="two windows"]] + +[[!toc ]] + +## closing and reopening the webapp + +The webapp does not need to be left open after you've set up your +repository. As long as the terminal window is left open, git-annex will +remain running and sync your files. To re-open the webapp after closing it, +use the [[!img newwindow.png alt="New Window"]] icon in the terminal window. + +## starting git-annex + +The app is not currently automatically started on boot, so you will need to +manually open it to keep your files in sync. You do not need to leave the +app running all the time, though. It will sync back up automatically when +started. + +## stopping git-annex + +Simply close the terminal window to stop git-annex from running. + +## using the command line + +[[!img terminal.png alt="Android terminal"]] + +If you prefer to use `git-annex` at the command line, you can do so using the +terminal. A fairly full set of tools is provided, including `git`, `ssh`, +`rsync`, and `gpg`. + +To prevent the webapp from being automatically started +when a terminal window opens, go into the terminal preferences, to "Inital +Command", and clear out the default `git annex webapp` setting. + +Or, if you'd like to run the assistant automatically, but not open the +webapp, change the "Initial Command" to: `git annex assistant --autostart` + +## using from adb shell + +To set up the git-annex environment from within `adb shell`, run: +`/data/data/ga.androidterm/runshell` + +This will launch a shell that has git-annex, git, etc in PATH. diff --git a/doc/Android/comment_15_77bafc01b47d4cf8f96bde2b6704ed71._comment b/doc/Android/comment_15_77bafc01b47d4cf8f96bde2b6704ed71._comment new file mode 100644 index 0000000000..0ecd59fe66 --- /dev/null +++ b/doc/Android/comment_15_77bafc01b47d4cf8f96bde2b6704ed71._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="http://yarikoptic.myopenid.com/" + nickname="site-myopenid" + subject="asking for ssh password in the terminal (not in web ui)" + date="2013-05-24T23:49:40Z" + content=""" +not sure if that is a known issue: whenever \"remote server\" is added, password needs to be typed back in the original terminal... is a bit challenging to do on android and not straightforward user-wise +"""]] diff --git a/doc/Android/comment_19_dc7b428f525a082834cb87221fc627ff._comment b/doc/Android/comment_19_dc7b428f525a082834cb87221fc627ff._comment new file mode 100644 index 0000000000..009ada0038 --- /dev/null +++ b/doc/Android/comment_19_dc7b428f525a082834cb87221fc627ff._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="http://afoolishmanifesto.com/" + nickname="frioux" + subject="SSH Keys?" + date="2013-07-17T16:50:46Z" + content=""" +Is there a way I can use an SSH Key to connect to a remote server? What would be really cool, though maybe not feasible, would be to use connectbot as an ssh-agent. +"""]] diff --git a/doc/Android/comment_20_81940ea56ace3dcd5fa84dfccd88ad96._comment b/doc/Android/comment_20_81940ea56ace3dcd5fa84dfccd88ad96._comment new file mode 100644 index 0000000000..ed4d6e0b0b --- /dev/null +++ b/doc/Android/comment_20_81940ea56ace3dcd5fa84dfccd88ad96._comment @@ -0,0 +1,10 @@ +[[!comment format=mdwn + username="http://joeyh.name/" + ip="4.154.4.90" + subject="comment 20" + date="2013-07-17T19:06:31Z" + content=""" +@frioux the webapp has a \"ssh server\" option that will set up a ssh key and use it for passwordless data transfer to a ssh server. You have to enter your password twice in the git-annex terminal app, and then it's set up. + +The openssh included in the git-annex app fully supports everything you can usually do with ssh keys, so you can also set this up by hand. +"""]] diff --git a/doc/Android/comment_29_37aa87a451d4390ed367402eec740855._comment b/doc/Android/comment_29_37aa87a451d4390ed367402eec740855._comment new file mode 100644 index 0000000000..448050d12f --- /dev/null +++ b/doc/Android/comment_29_37aa87a451d4390ed367402eec740855._comment @@ -0,0 +1,12 @@ +[[!comment format=mdwn + username="http://joeyh.name/" + ip="4.154.0.21" + subject="comment 29" + date="2013-07-30T17:45:27Z" + content=""" +If you are experiencing a problem using git-annex on Android, please examine the list of [[bugs]] and add a new, detailed bug report if no-one has reported the problem. If you are not sure if you have a bug, or need help in filing a good bug report, ask for help in the [[forum]]. + +I have moved to [[oldcomments]] a lot of old comments about problems that may be fixed or +not (hard to tell without a bug report!) " This page cannot +scale to handle every bug report that someone wants to paste into it. +"""]] diff --git a/doc/Android/oldcomments.mdwn b/doc/Android/oldcomments.mdwn new file mode 100644 index 0000000000..3566712cd4 --- /dev/null +++ b/doc/Android/oldcomments.mdwn @@ -0,0 +1,2 @@ +If one of these comments is yours, and you are still experiencing the +problem, please file a proper [[bug_report|bugs]]. --[[Joey]] diff --git a/doc/Android/oldcomments/comment_10_20e3d513b8b97496d76aca4619026cd6._comment b/doc/Android/oldcomments/comment_10_20e3d513b8b97496d76aca4619026cd6._comment new file mode 100644 index 0000000000..cf7a4fdb5b --- /dev/null +++ b/doc/Android/oldcomments/comment_10_20e3d513b8b97496d76aca4619026cd6._comment @@ -0,0 +1,16 @@ +[[!comment format=mdwn + username="http://yarikoptic.myopenid.com/" + nickname="site-myopenid" + subject="comment 10" + date="2013-05-24T03:11:50Z" + content=""" +>you said before the error was \"Read-only file system\". Now you're saying it's \"Cross-device link\". I'm slightly confused. + +;-) Sorry for confusion, here are the details: + +\"Read-only file system\" -- that error appeared when I started \"stock git annex\", i.e. from running /data/data/ga.androidterm/lib/lib.start.so . +Since you have suggested that it might be coming from hard linking command, I have ran that one manually, and that is when I got \"Cross-device link\" error, which suggests that hard linking is not the one at fault here. + +I will try fresh build now +Cheers, +"""]] diff --git a/doc/Android/oldcomments/comment_11_c96b8f1cc1583a74eb2483f48357f023._comment b/doc/Android/oldcomments/comment_11_c96b8f1cc1583a74eb2483f48357f023._comment new file mode 100644 index 0000000000..2e1ba65606 --- /dev/null +++ b/doc/Android/oldcomments/comment_11_c96b8f1cc1583a74eb2483f48357f023._comment @@ -0,0 +1,15 @@ +[[!comment format=mdwn + username="http://yarikoptic.myopenid.com/" + nickname="site-myopenid" + subject="fresh build" + date="2013-05-24T03:21:29Z" + content=""" +With fresh build got: + +u0_a39@android:/ $ git annex webapp +/system/bin/sh: git: not found + +the PATH is /sbin:/system/bin:/system/xbin + +where should git (and ga) reside now ? (/data somehow is not accessible now to u0_a39) +"""]] diff --git a/doc/Android/oldcomments/comment_12_6551f5fa081494b079c10a33c9b0d8ad._comment b/doc/Android/oldcomments/comment_12_6551f5fa081494b079c10a33c9b0d8ad._comment new file mode 100644 index 0000000000..39ce3e0588 --- /dev/null +++ b/doc/Android/oldcomments/comment_12_6551f5fa081494b079c10a33c9b0d8ad._comment @@ -0,0 +1,10 @@ +[[!comment format=mdwn + username="http://joeyh.name/" + nickname="joey" + subject="comment 12" + date="2013-05-24T03:26:33Z" + content=""" +You should be able to run /data/data/ga.androidterm/runshell even if you cannot ls /data. This adds /data/data/ga.androidterm/bin to PATH + +However, the shell that the app starts is started by runshell anyway, so I don't understand how this could happen. +"""]] diff --git a/doc/Android/oldcomments/comment_13_7c633d245651ec08f63194fe1fc194ae._comment b/doc/Android/oldcomments/comment_13_7c633d245651ec08f63194fe1fc194ae._comment new file mode 100644 index 0000000000..dae84414b1 --- /dev/null +++ b/doc/Android/oldcomments/comment_13_7c633d245651ec08f63194fe1fc194ae._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawkwjBDXkP9HAQKhjTgThGOxUa1B99y_WRA" + nickname="Franck" + subject="Still problems with my old N1/CM7" + date="2013-05-24T06:01:18Z" + content=""" +Hi, thank you for addressing this issue! I installed the new release but now it fails in another way: the message is just \"In mgmain NJI_OnLoad\" then the terminal says that the session is closed. +"""]] diff --git a/doc/Android/oldcomments/comment_14_60c2403140085f9caf48a33b59a36ab4._comment b/doc/Android/oldcomments/comment_14_60c2403140085f9caf48a33b59a36ab4._comment new file mode 100644 index 0000000000..a6f4598f6e --- /dev/null +++ b/doc/Android/oldcomments/comment_14_60c2403140085f9caf48a33b59a36ab4._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="http://yarikoptic.myopenid.com/" + nickname="site-myopenid" + subject="It starts after uninstall/install" + date="2013-05-24T23:29:52Z" + content=""" +Hi Joey -- there is success here... previous installation was \"updated\" by installing the new package without uninstalling previous one, and that apparently didn't work correctly (I didn't even have bin/ directory you mentioned). So I have removed previous installation and reinstalled it again -- it starts now! Thanks ;) +"""]] diff --git a/doc/Android/oldcomments/comment_16_9af73451be09f03cfff81fdf9481ffc4._comment b/doc/Android/oldcomments/comment_16_9af73451be09f03cfff81fdf9481ffc4._comment new file mode 100644 index 0000000000..07923c1722 --- /dev/null +++ b/doc/Android/oldcomments/comment_16_9af73451be09f03cfff81fdf9481ffc4._comment @@ -0,0 +1,27 @@ +[[!comment format=mdwn + username="http://yarikoptic.myopenid.com/" + nickname="site-myopenid" + subject="Few other issues" + date="2013-05-25T15:35:46Z" + content=""" +Hi again. + +talking about 4.20130523-gcfe07a2 version: + +- because working in the terminal to interact with git-annex probably should not be a common case on Android, may be it is worth making default type of new added repository to become a full backup? I have initiated a new one, attached a remote one, it said \"synced\" but all the files were just containing symlinks and were not usable. I had to switch to \"full backup\" (or whatever that name) to finally get directory synced + +- log file might grow too large simply because of containing numerous entries for attempting connect remote repository while offline, e.g. + +Please make sure you have the correct access rights +and the repository exists. +ssh: Could not resolve hostname onerussian.com: No address associated with hostname +fatal: Could not read from remote repository. + +IMHO those should not be there at all, e.g. if it is known that ATM there is no network connectivity + +- In addition to two existing repositories (1 local /sdcard/annex, which is also avail at/storage/sdcard0/annex + 1 remote) I have added one more local (and said to keep it in sync with original local). But it didn't work -- it \"Synced with onerussian.com_annex but not with Annex\" and claimed that the /external/extSdCard/Annex doesn't exist, although it is there (and with .git generated etc). When I restarted the deamon I got into a \"new\" Repository: /storage/extSdCard/Annex which also listed the 1st local but with \"Failed to sync with localhost\" message -- no remote one listed. Whenever I try to \"Switch repository\" to /sdcard/annex (the original local) -- it starts loading a new page but gets stuck right there. The only way to revive webui is to go back to Dashboard. Log there says (retyping from the screen so typos might be there): + +error: cannot run git-receive-pack '/storage/sdcard0/annex': No such file or directory +fatal: unable to fork + +"""]] diff --git a/doc/Android/oldcomments/comment_17_f76561a654b534df3a807b1c045710b2._comment b/doc/Android/oldcomments/comment_17_f76561a654b534df3a807b1c045710b2._comment new file mode 100644 index 0000000000..bc4a648101 --- /dev/null +++ b/doc/Android/oldcomments/comment_17_f76561a654b534df3a807b1c045710b2._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="http://yarikoptic.myopenid.com/" + nickname="site-myopenid" + subject="comment 17" + date="2013-05-29T02:43:29Z" + content=""" +joey -- any additional information could I provide to troubleshoot the issue? original repository seems to sync ok, but I can't \"administer\" it if I can't even switch to it... +"""]] diff --git a/doc/Android/oldcomments/comment_18_1b46cdf154ddadfe17e4b6e4054dc619._comment b/doc/Android/oldcomments/comment_18_1b46cdf154ddadfe17e4b6e4054dc619._comment new file mode 100644 index 0000000000..bf9d79060b --- /dev/null +++ b/doc/Android/oldcomments/comment_18_1b46cdf154ddadfe17e4b6e4054dc619._comment @@ -0,0 +1,17 @@ +[[!comment format=mdwn + username="http://aap.liquidid.net/" + nickname="AAP" + subject="comment 18" + date="2013-05-30T11:23:58Z" + content=""" +I too get the 'link busybox: Read-only file system' message. Here is my phone info: + +Phone: Samsung Galaxy Y GT-S5360 (rooted) +Android: 2.3.6 Gingerbread +BusyBox path: /system/xbin/ + + +Androids own terminal seems not to understand the d argument (-ld: No such file or directory) but over ssh 'ls -ld /data/data/ga.androidterm' returns + + drwxr-x--x 1 app_97 app_97 0 May 30 12:57 /data/data/ga.androidterm/ +"""]] diff --git a/doc/Android/oldcomments/comment_1_cc9caa5dd22dd67e5c1d22d697096dd2._comment b/doc/Android/oldcomments/comment_1_cc9caa5dd22dd67e5c1d22d697096dd2._comment new file mode 100644 index 0000000000..7fb38058c5 --- /dev/null +++ b/doc/Android/oldcomments/comment_1_cc9caa5dd22dd67e5c1d22d697096dd2._comment @@ -0,0 +1,15 @@ +[[!comment format=txt + username="http://yarikoptic.myopenid.com/" + nickname="site-myopenid" + subject="Does it require the device to be rooted?" + date="2013-05-16T20:55:45Z" + content=""" +Following your news on kickstarter downloaded the .apk, and installed it. Upn start I just got a terminal window with + + link busybox: Read-only file system + + [Terminal session finished] + +That is on Galaxy Note + +"""]] diff --git a/doc/Android/oldcomments/comment_21_5903f6a4a81a6534fa8cfafb3b6c37bb._comment b/doc/Android/oldcomments/comment_21_5903f6a4a81a6534fa8cfafb3b6c37bb._comment new file mode 100644 index 0000000000..1e247b96a5 --- /dev/null +++ b/doc/Android/oldcomments/comment_21_5903f6a4a81a6534fa8cfafb3b6c37bb._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="http://afoolishmanifesto.com/" + nickname="frioux" + subject="SSH Keys - 2" + date="2013-07-17T22:56:37Z" + content=""" +@joey should I be using the nightlies to see that? Under \"Adding a remote server using ssh\" I only see Host name, user name, directory, and port. Will it only be an option after I type in a password? +"""]] diff --git a/doc/Android/oldcomments/comment_22_36afd354f9669a154d7b6b2c4d43ded9._comment b/doc/Android/oldcomments/comment_22_36afd354f9669a154d7b6b2c4d43ded9._comment new file mode 100644 index 0000000000..e5b5f964aa --- /dev/null +++ b/doc/Android/oldcomments/comment_22_36afd354f9669a154d7b6b2c4d43ded9._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="http://joeyh.name/" + ip="4.154.6.48" + subject="comment 22" + date="2013-07-17T23:25:21Z" + content=""" +@frioux it will automatically generate a new ssh key and configure the server to use it, once you submit the form and enter the password to let it into the server. +"""]] diff --git a/doc/Android/oldcomments/comment_23_de98154792e8611a134429f06d82bcb1._comment b/doc/Android/oldcomments/comment_23_de98154792e8611a134429f06d82bcb1._comment new file mode 100644 index 0000000000..1f34a775e7 --- /dev/null +++ b/doc/Android/oldcomments/comment_23_de98154792e8611a134429f06d82bcb1._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="http://afoolishmanifesto.com/" + nickname="frioux" + subject="comment 23" + date="2013-07-18T02:01:28Z" + content=""" +@joey: ok, I got it to connect and it indeed sent over a key etc. For some reason now though git-annex (on android) \"crashes\" shortly after starting. To be clear, the web app says that the program crashed, the console is still there. I suspect that it may have something to do with my largish remote repo and the time required to sync just the metadata, but I can't tell. Any ideas what I should do next? (Note that I *did* change it to manual mode because my phone doesn't have 30G of storage :) +"""]] diff --git a/doc/Android/oldcomments/comment_24_7ab509c25243009bfbffd796ec64e77b._comment b/doc/Android/oldcomments/comment_24_7ab509c25243009bfbffd796ec64e77b._comment new file mode 100644 index 0000000000..fdec15ac63 --- /dev/null +++ b/doc/Android/oldcomments/comment_24_7ab509c25243009bfbffd796ec64e77b._comment @@ -0,0 +1,10 @@ +[[!comment format=mdwn + username="http://afoolishmanifesto.com/" + nickname="frioux" + subject="comment 24" + date="2013-07-18T11:35:06Z" + content=""" +ok, it eventually got the details from the remote server, but now I'm getting some other oddities. here is some of my log that shows what I am running into + +Watcher crashed: addWatch: does not exist (No such file or directory) [2013-07-18 06:22:46 CDT] Watcher: warning Watcher crashed: addWatch: does not exist (No such file or directory) (scanning...) [2013-07-18 06:23:19 CDT] Watcher: Performing startup scan Watcher crashed: addWatch: does not exist (No such file or directory) [2013-07-18 06:24:28 CDT] Watcher: warning Watcher crashed: addWatch: does not exist (No such file or directory) (scanning...) [2013-07-18 06:24:31 CDT] Watcher: Performing startup scan Watcher crashed: addWatch: does not exist (No such file or directory) [2013-07-18 06:25:44 CDT] Watcher: warning Watcher crashed: addWatch: does not exist (No such file or directory) +"""]] diff --git a/doc/Android/oldcomments/comment_25_026d1a01d5753d71ac3dfc002f2a5eec._comment b/doc/Android/oldcomments/comment_25_026d1a01d5753d71ac3dfc002f2a5eec._comment new file mode 100644 index 0000000000..aa74230dc4 --- /dev/null +++ b/doc/Android/oldcomments/comment_25_026d1a01d5753d71ac3dfc002f2a5eec._comment @@ -0,0 +1,10 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawnRfQArYOmDd7r2DC7DkIJFOQgqXCVcAeU" + nickname="Frew" + subject="comment 25" + date="2013-07-18T13:14:46Z" + content=""" +frioux here (something messed up with myopenid or something) + +So I deleted the repo on my phone (via the CLI since the web app seemed hung) and recreated it; this time making sure that I set things to manual mode ASAP. It didn't have the problem it was having before, but now what seems to have happened is that it fetches from the remote, commits to the local repo, and then immediately fetches and commits again. It looks like it's about a 4s repeat loop. Any ideas what I should do next? +"""]] diff --git a/doc/Android/oldcomments/comment_26_f0a044fb649d43e32c96b08edbc336c3._comment b/doc/Android/oldcomments/comment_26_f0a044fb649d43e32c96b08edbc336c3._comment new file mode 100644 index 0000000000..c7c0e623f9 --- /dev/null +++ b/doc/Android/oldcomments/comment_26_f0a044fb649d43e32c96b08edbc336c3._comment @@ -0,0 +1,12 @@ +[[!comment format=mdwn + username="http://joeyh.name/" + ip="4.154.0.140" + subject="comment 26" + date="2013-07-18T17:07:27Z" + content=""" +@Frew, you should file bug reports when you have a bug. + +One problem you mentioned had already had a bug report filed by someone +else: + So you can post your details there. +"""]] diff --git a/doc/Android/oldcomments/comment_27_6b9ae35b1ceeba14cd7a74e142870705._comment b/doc/Android/oldcomments/comment_27_6b9ae35b1ceeba14cd7a74e142870705._comment new file mode 100644 index 0000000000..b77e0873fa --- /dev/null +++ b/doc/Android/oldcomments/comment_27_6b9ae35b1ceeba14cd7a74e142870705._comment @@ -0,0 +1,34 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawnaH44G3QbxBAYyDwy0PbvL0ls60XoaR3Y" + nickname="Nigel" + subject="Watcher crashed in Android on /storage/sdcard1 - bug?" + date="2013-07-29T11:50:46Z" + content=""" +In webapp UI, added on first install, the location for repository: /storage/sdcard1 + +!warning + +Watcher crashed: addWatch: + +permission denied (Permission denied) + +[Restart Thread] + +:Performing startup scan + + +In terminal Window 1: + +nex webapp < + + Detected a crippled filesystem. + + Enabling direct mode. + + Detected a filesystem without fifo support. + + Disabling ssh connection caching. + + +Android 4.1.1 Huawei Y300 Annex.apk v1.0.52 version 4.20130723 +"""]] diff --git a/doc/Android/oldcomments/comment_28_c91db1215f529aa68bfb0576c3c5eddc._comment b/doc/Android/oldcomments/comment_28_c91db1215f529aa68bfb0576c3c5eddc._comment new file mode 100644 index 0000000000..cf315c0d9c --- /dev/null +++ b/doc/Android/oldcomments/comment_28_c91db1215f529aa68bfb0576c3c5eddc._comment @@ -0,0 +1,10 @@ +[[!comment format=mdwn + username="Jonathan" + ip="63.131.117.194" + subject="link busybox: Read-only file system" + date="2013-07-29T20:08:12Z" + content=""" +Phone: HTC EVO 3d 4g +Model Number: pg86100 +Android Version: 4.0.3 +"""]] diff --git a/doc/Android/oldcomments/comment_2_c2422b7dd9d526b3616e49f48cf178c2._comment b/doc/Android/oldcomments/comment_2_c2422b7dd9d526b3616e49f48cf178c2._comment new file mode 100644 index 0000000000..bfa4decc41 --- /dev/null +++ b/doc/Android/oldcomments/comment_2_c2422b7dd9d526b3616e49f48cf178c2._comment @@ -0,0 +1,10 @@ +[[!comment format=mdwn + username="http://joeyh.name/" + nickname="joey" + subject="comment 2" + date="2013-05-17T22:28:34Z" + content=""" +The Android app works on many non-rooted Android systems. + +The \"link busybox: Read-only file system\" means that `/data/data/ga.androidterm/lib/lib.busybox.so` cannot be hard linked to `/data/data/ga.androidterm/busybox`. That's not normal. I'd appreciate if you could provide more information on your Android device, like Android version and model number. +"""]] diff --git a/doc/Android/oldcomments/comment_3_0e4980c27b13dbc28477c02a82898248._comment b/doc/Android/oldcomments/comment_3_0e4980c27b13dbc28477c02a82898248._comment new file mode 100644 index 0000000000..fdbfac1c6f --- /dev/null +++ b/doc/Android/oldcomments/comment_3_0e4980c27b13dbc28477c02a82898248._comment @@ -0,0 +1,14 @@ +[[!comment format=mdwn + username="http://yarikoptic.myopenid.com/" + nickname="site-myopenid" + subject="Follow-up information on my system" + date="2013-05-18T01:23:28Z" + content=""" +Sorry for the delay: my android is stock Samsung-tuned Jelly beans. +Android 4.1.2 +Baseband version N7000XXLSO + +not sure if that would be of any use :-/ nothing in the logs (aLogcat) if I filter by annex -- should there any debug output? what should be a key to search by? + + +"""]] diff --git a/doc/Android/oldcomments/comment_4_86f7b5444e2eaea7f8f7b9160f671a1d._comment b/doc/Android/oldcomments/comment_4_86f7b5444e2eaea7f8f7b9160f671a1d._comment new file mode 100644 index 0000000000..ad46a26be8 --- /dev/null +++ b/doc/Android/oldcomments/comment_4_86f7b5444e2eaea7f8f7b9160f671a1d._comment @@ -0,0 +1,10 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawnu1NYw8UF-NoDbKu8YKVGxi8FoZLH7JPs" + nickname="Chris" + subject="Not starting browser on Nexus 7, Android 4.2.2" + date="2013-05-19T14:04:28Z" + content=""" +I just tried to run this on my Nexus 7 which has Android 4.2.2, and I received the following: + +In spite of that, though, the URL provided still worked. +"""]] diff --git a/doc/Android/oldcomments/comment_5_9d78009435736a178d5a3f5a9bc0ed6a._comment b/doc/Android/oldcomments/comment_5_9d78009435736a178d5a3f5a9bc0ed6a._comment new file mode 100644 index 0000000000..d29a59036b --- /dev/null +++ b/doc/Android/oldcomments/comment_5_9d78009435736a178d5a3f5a9bc0ed6a._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="http://joeyh.name/" + nickname="joey" + subject="comment 5" + date="2013-05-19T19:46:14Z" + content=""" +@Chris, that is a known bug: [[bugs/Android_app_permission_denial_on_startup]] +"""]] diff --git a/doc/Android/oldcomments/comment_6_7b9523ddb20dc4a929e556c3ed0c7406._comment b/doc/Android/oldcomments/comment_6_7b9523ddb20dc4a929e556c3ed0c7406._comment new file mode 100644 index 0000000000..1c4aceaefc --- /dev/null +++ b/doc/Android/oldcomments/comment_6_7b9523ddb20dc4a929e556c3ed0c7406._comment @@ -0,0 +1,18 @@ +[[!comment format=mdwn + username="http://joeyh.name/" + nickname="joey" + subject="comment 6" + date="2013-05-19T20:06:56Z" + content=""" +@yarikoptic, there is a process you can perform that will help me determine what's going on. + +You should be able to get the git-annex app to let you into a shell. You can do this by starting the app, and then going into its configuration menu, to Preferences, selecting \"Command Line\", and changing it to run \"/system/bin/sh\" + +Then when you open a new window in the git-annex app, you'll be at a shell prompt. From there, you can run: + +ls -ld /data/data/ga.androidterm + +I'm interested to know a) whether the directory exists and b) what permissions and owner it has. On my tablet, I get back \"drwxr-x--x app_39 app_39\" .. and if I run `id` in the shell, it tells me it's running as `app_39`. + +My guess is the directory probably does exist, but cannot be written to by the app. If you're able to verify that, the next step will be to investigate if there is some other directory that the app can write to. It needs to be able to write to someplace that is not on the `/sdcard` to install itself. +"""]] diff --git a/doc/Android/oldcomments/comment_7_a56628a622da752806c42c5b8b54ceef._comment b/doc/Android/oldcomments/comment_7_a56628a622da752806c42c5b8b54ceef._comment new file mode 100644 index 0000000000..df0c0a2de2 --- /dev/null +++ b/doc/Android/oldcomments/comment_7_a56628a622da752806c42c5b8b54ceef._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawkwjBDXkP9HAQKhjTgThGOxUa1B99y_WRA" + nickname="Franck" + subject="Link issue" + date="2013-05-22T12:01:38Z" + content=""" +Hi, I have exactly the same problem with the link that fails on my phone. However, I checked the permissions and they are as you describe on your tablet (except for the app number). At the same time, everything is fine on my tablet... The phone runs an old Cyanogenmod 7.2.0 (Android 2.3.7) while the tablet is a more recent Asus TF700T (Android 4.1.1). Let me know if you want me to run tests. +"""]] diff --git a/doc/Android/oldcomments/comment_8_19656ec99b8f6aa64c1d01a3c9ae9bd0._comment b/doc/Android/oldcomments/comment_8_19656ec99b8f6aa64c1d01a3c9ae9bd0._comment new file mode 100644 index 0000000000..43ff8d64bd --- /dev/null +++ b/doc/Android/oldcomments/comment_8_19656ec99b8f6aa64c1d01a3c9ae9bd0._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="http://yarikoptic.myopenid.com/" + nickname="site-myopenid" + subject="why ln failed" + date="2013-05-23T13:27:39Z" + content=""" +Finally got to check it out: so indeed hardlinking fails but not because of permissions but \"link failed Cross-device link\" that lib is -> /mnt/asec/ga.androidterm-1/lib which resides on a different partition (vfat, /dev/block/dm-2, ro) from /data (ext4, /dev/block/mmcblk0p10) +"""]] diff --git a/doc/Android/oldcomments/comment_9_55e703ae105d0c0ee9ac50df8cc59dfb._comment b/doc/Android/oldcomments/comment_9_55e703ae105d0c0ee9ac50df8cc59dfb._comment new file mode 100644 index 0000000000..0970412a4d --- /dev/null +++ b/doc/Android/oldcomments/comment_9_55e703ae105d0c0ee9ac50df8cc59dfb._comment @@ -0,0 +1,10 @@ +[[!comment format=mdwn + username="http://joeyh.name/" + nickname="joey" + subject="comment 9" + date="2013-05-23T18:44:46Z" + content=""" +@yarikoptic you said before the error was \"Read-only file system\". Now you're saying it's \"Cross-device link\". I'm slightly confused. + +I've reworked the android app to not need any hard links. Try the current autobuild: +"""]] diff --git a/doc/android/DCIM.png b/doc/android/DCIM.png new file mode 100644 index 0000000000000000000000000000000000000000..3ac0933234e6eee1e0736584bf6547fae9553aea GIT binary patch literal 95786 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

V9z-2hl;_m7wwhn3*&P!bF8|5ez2w2yfK=wdW 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%0AkSt1I-SRy%CS9XS4O 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 literal 0 HcmV?d00001 diff --git a/doc/android/appinstalled.png b/doc/android/appinstalled.png new file mode 100644 index 0000000000000000000000000000000000000000..166120d48204c4358250ce113376c329b2f07895 GIT binary patch literal 16805 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�rLGi{bQNr-yn! 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≷x0gFWc}*$g`jr4}y-PJ2WAD3*@9_WH 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 literal 0 HcmV?d00001 diff --git a/doc/android/install.png b/doc/android/install.png new file mode 100644 index 0000000000000000000000000000000000000000..882455df16d92bfff2b422f439a3d965daa2e73f GIT binary patch literal 55106 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%0DxEoVemUq7M(HBd@hn}iEfQ-%no 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 Mn)(`f>K1|j55dTaJOBUy literal 0 HcmV?d00001 diff --git a/doc/android/newwindow.png b/doc/android/newwindow.png new file mode 100644 index 0000000000000000000000000000000000000000..4cca6ae09bf686692ec2a04abdc7964c48fa6256 GIT binary patch literal 1009 zcmVzfbB=6vuDN4^fjU zO%x*#qYwub2ZIBVrP`Q=C`RKVF>!EbH}Nm!VcWO`WJ)hfiPR~66wA4~d|58{3dpbHg+-|qi=_E-K0Py_$ z%yC>goi3Nl|AdWUn6B$2NzTp9`FuXJ*$e=XWf{XT^sFcf%d*GE$Av-xE&e-f2NrFKA+d?g%IxV@5kfupWy4WaU9n)Ef@?20)dBz zhn=0Bd_MolHF~w#Y-?+4E|=@%pDeIOG`^E%SIxR*Voqy?;{uh zAd2GUIB zjkVb%NeY4hAp`)JOeTh5+S}VTO~WuuRaKHCA%qJH3nr6E*L57nOQq83=_!N|LMR9V zYJ3f{aU54w_4iVvk)kMqARvSvLmdbNP~uG{Q$C+RJ3B*mRaL9w@dxZusWdP!fKDJu z(#FO{CE$vpY;JDKvb?pmWwBULFf>hje}6}ZCJ5s3@v-rI=W;o#)rvx=>$)ttCN#Zz85Ci~#)oOJ*ofeCw zSS%hK9K_@CpA1ZY%+__it*vcrY;0m;!r^dqb#(y%ip3(waj8`5=H>=Xa6e=>B^<|V fcg>bs`akpwh<(%rwkcA;00000NkvXXu0mjfI0)9; literal 0 HcmV?d00001 diff --git a/doc/android/terminal.png b/doc/android/terminal.png new file mode 100644 index 0000000000000000000000000000000000000000..9afa2720fa1f1b1c68839b4b272db626a62b6ad6 GIT binary patch literal 20565 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 literal 0 HcmV?d00001 diff --git a/doc/android/webapp.png b/doc/android/webapp.png new file mode 100644 index 0000000000000000000000000000000000000000..edb5c5ccd906fd44372d448ce9455eac226cac84 GIT binary patch literal 64097 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%0DCfdfCN@#VkYhklIbq{Y32B?DqbP=a}Z?T%CnH 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 Snd}yU&R6>_ar!KWSlXgBt0}cNAI5Fp zpAeB=P^`XqD3A2`e3|d@LU5~u0H3PTU6$Jrx+}Vd14SWoI_xCkP%D{UjZLN)kZ83D z<28->=WF2duT&omgpJR^Q;qyk^gF&hmUV06cH4P^`->dXn!tyu2bNV-i7!}DJB@S8 zBxQb2#TlfHflo)be{&yAvAXpbg~?Df{Rzpa(rphztUo#2J1cF;U zL9Xv2N%gr>OUKKkAUNHLfkCx1Tcc_W;H>zh*v^KAI-=Pg7o^cs&1g9)F7IV5bei>g z0rf%TX!T$5WYUCmot(Id@WZJXErR?dQYD2z1S_ny6i9c>i*PR#saQ#WJmuwW!%i3W zxW~wnFrO4aDC+1!z7`eLQ?#KzoV-MiY7@P#P%MNZ#1OIjz1kA>R3*QB-zAEQ9~}nr z#%*|X-gcgAwwu^Y_s3tsa& zasTqh;ro0b_|>o9z#cs0-rqe17x7V*13kvN$?~ z@&yU2ShIiGewI4MN#yK@EfceDaaoY(UzZHesQmUbq1iZ)@Wz3|S^U0~#r=d_1BImT zRV4XiwD==|CM`yTkq={|nH0A}T3j-g!83omnTvI|5&o?y@Z8Yh=B8let(xpW8jUkV2=dhtY$|O&v(S#}_3Mz{|_aW!}DU7$5vJADoLihX}x0 zpgv3l{x;f@1cZf*BYZf;Uu&+wDcA^`lJ;-P7*N#BBY$oU-l%)=VjNU#6AgBoo8;}z zE{eXT%^Ty4{7YVYHx1y zpXSGA8_R{Vh7U+%d5*kXMq~pRa%gaFrpF5*!tKGe_26gs9NMHPsj&TOo{3zLd57bB zYqXJi?{~UsRKdrleDk|(oIIg16XMg(&07+&OO1fiiDvbrnn}~S>OUVrom3=5^&I|> zw`UeF_w$d3*Jp#YFW0cHTr!WMH>Mh~eV?dJ!KVO9OMQphQOTJTAG=|&*Mh)rUs!m^ zCCtF}NOO+X$!|R4th&cidKpCNJ@yHI5=4pG z*Af0phM{*tN;L0kIV?2T*ss5{{A>oc*=BL?F0Hvjyo1AcVQQyp_(Zs`n}OeA4w?Fz7$xNIU;@$t6qKRFxXJP$T~UW2&4}m& zcTLSTjGXkiFdqG3=aN?wA&-f6;983-uTmU19@{O+kseK#HC7{ii*yE`s~JfUIh5o0 z#T$LPmI?e!PXalqaWg6^y>^FJV7)l0Bwt(5hy(|akw%5>MaiQsC`Rb(6QcTa7c~Ib znP=8=XN@5OY^U(@e)>hDyQr5n(#|IW&D|R((fQQOtLtEz<{$Slo6AwbTMD}^{uDGB zM7*vI9Z@tm`J~VUk%*c}^LU$-AvL=|F=@!?WzQM_c&5G0f~1P4S|Ik6WCQ zLFIQ-EMe75eI{r%KyV~;_R~q>rbT@$VZLD@P4?4(?MQv)h+Nz;haXTzzr3sZTI|<` z6qGkEfcgaJx>plCX8Q0(jBljtERsF%)oqmudN?+v-dp5#=^xRzooXlMz9BOa-k4ZZ z3%7dGTl!&b2Kq5|GDs`qC21^*D(43`UpO_^+y%;IfG@-Pe@pmr8Ay7}PVzqglvpRo zDuqrtkh=Pgvc%I$J%^;`eyS#Zox#!bYi=ShV5nA-PjnhrYA@e49V=K!axnd{ZN3^Ma>XO)Vysz3^7arcrX

%1eJ6sIR0al-H;JH7F@Vnk1}>gGD6T%&dIGS@)G<`diK&#|+6s(XAh zP;=q#gAq(*F}>;NAF%`eTEj$EQ{Z~u1#GUF4&4r95@hZA@NXOJd)9UBKugd{@z`>4 zt9u_BG*I}F*x3iY%2K@Y{c+>9;PJ-y7ei69iK}?-;PzQFiK=*o(D!F$4yxn9_ZFju zH5baeX z3_+2RBVMIQmX59^JtSnyIE@%?GI^{9dUl z+|rj8U%Er5hum|^+7|TWhd1oLP-_|fYh$4^uuWIA+MnRjMnVe&I`|dvy-E@q~Z8*&k;SaMqtA;t?>#|Mu-$Ha3;FNw;@6u%Cs6zwm$} zVYsge2WKo_DtO=vd1z?p)?fN>Zx@`JdU|?HV9rA}yzvfUctV6z{6|ssrs-f87mIqE zA)B;6$jH{|GCVLJu-9{pd3B?3_LuE8b%3) z+7>Z$3Pw?rZ@?JnB@|RQrU|9HCwxkQOvWFhEXh7w{>CN@;0Ey?7t_Bf0}UN>^xPmy zBAv8VGqt3zKMX@V_l@g>j5e-58S{oCuc=mF1r^l+KoT@=k93t#Lk>r%CKEW= zR&d256f%cY;^y$}AP1)v(k>BbsfLw;{%mM1dfj3Dm?Dl+d9}W^K-}ABI5IV3Rd=eK zbBK!|prfb$%pJ?Cdd@f81Wec6+4`2PI!MJXRTu_Jb()Kxy0|-f7W?90N!UXRQXA~_ z`4XI5$};aql^kCt5_*=EmG!PbPuFm{MDp9k@gH>ZQT}quwtY6PG_GQq1DPrsk@>5F zRE8r|&EZ42MN{0LV9=UvwOj#JYK|C&G-W8d=6KY>KZFp}Kn%c7#*lNRAajy01?mM> z2rUT#i>M|-h`@sMb2CDU-j&n`!%k|xP{FWdq&o2jRT-g#kI}Wus#D8=Ov1)IDSAok zqHIe(2G_>Y6Xk1dAHQ<}e9?R5uR-4C(~|G+Ax@LL*nHyRPEcb`5>*$Mla)3fjXZHk zSHhSJx9a@>KLo%SPE{VQ&?Pd8Pj1iN?%EX zsg~*Pl%2bZl`_KqX*29QhZI}NB?#)nzRz1c^@0z$IhrPM*g-gvLPJ314+0{E>=Q|ytG zl~Fph-b8P>_#7hui9^tgmhStWTs;r`u#B^{@n&K!cRLQh2kF z*(}7Co0%xb2H}c8Lr?nK0OrD#wbLl%YlO0oNp$t9&QoYkCsBcRcO}c-WdlW>6*AK)Y(D#9*&H z&3MJX7$E(9HB7F)@{%n#oBh}wL7`ykvlN*DD1IM%(LYXg^=6waL@3CBdQ^k%= zxUr*ev`y`+b?gUmBZDeFR-ye0#iz;&mZOhDX5ysWNT+?ln(R(kFEM&@(ZPdG&5M!v>}08 z?auY_eqJ5zfwJ>nU5>^Xicn&5e5zcbmSCZ&+CCi)tUKJyWD8!H)cyX{W9$p+-j6-lYyRT{Htc9Ao zbnFu=tf4=s&2-LTtzIS${aO8kXqW@YAz-8ni8jP)B~O(_RZ*5m!v$9pc6(zR7nO##8Z%GAL#&NFvIUq_kUESFK*QXl z;xo12Ubk)HEs~D2NBZK{#!AVW0A3+lnjgFLhvW0mw4H8G^c~S}Y)d2O49RmIQT*ga z{j_@m_#;{HxXi)>XqW&7Xge*kKh~g+G^8+n+klGzz&D z0*hat+3`_KNcm{6ZpDQt8x)(`l;vaXh1w_ao2dBRZ?)LVMH&ItVR^`a|wJ2&YWv6A1sv% z#tJHwITtcb8qM^Rv;r01Y0U3HE-xK_Ey-?-%YwhFnHzz>sLvmdogKG>P0T+AxkXf( zv^3D6WVpMt;mvNNy5N~IU*_l)=cII24zb}V#qPH&n_SfX*Kjs2OOZEzh&h(xq> zO?vN!4SDcT8Q$ur^%~jyvnKJ+E;&#wDLV?%vUmcnS%vWUxR<>>_tnS(soTPqqN1X} zf+jjT)2iGZo?UCaYEBw3Ec;TKC215{nb2TA!AV1PjK+ge&4f!(p(Q!1je1DZ2joP$ znUuAiaF-tRZ}z`g=krRLFZmHei-}qRNMC1DCtinJ%fNZ@wiL3Y_||Id(9n>&y1F05 zck%DEgRS6bbRwGh7b1x7AJYc0uF3~b;1IG3JcU`~9Wyf;Dk>^6vQn{jj6AQO#V_63 z^mR+5LAb*+;eyjoc8*HD6N#)gQIeElW7a!)T0E9gNc-}aVX?#^EL@;QbbZ1&4PsBK z<;h<7w78YII_q}lk&>aRYp~`>iGSbh3=svh1|pO1rL>{ye+SPM+1m6`Pz^Jm0?(Lm zUFw!GqBcD~KK>lxf08)65J6z+^o2(d8P`+wGg?$Hspp7+&xp?NqBr72m1)>`totgq z%WZqfzeRECKg!LdOGiLuBQpEroQFnJ|e0;C_$i0@M2`KCGC(P!S-wb##> zNRq&-8mgHt_e70lbn8Mqy{Y)E#SP8@>Y)aNo6<8?S(!+LA3$qN39->IM$Z`v0WADb zOeA8|)4WA)3{(NE`&y~(}WNQl(ubgC7Ch8-;n_=wLj2x$rz7) z8hnL!F8hcx-uMrTwXcxq`}h$e3?JN`(0H7Gt83~Bj&hwwk%26Sp+jwWj^uCHu-}Ql zzt2joMVZ>pCJDaRcar^s_#!I=igDY{-lW8`ViT0eOk_lpR+cXn>X+)^eV&F{Nv$wi zw47t?MiCRAxwtS2I>+9@V8oVMNdPwUnCPU<0GZJm-ErDH@!Z2det*gY{PoW$W4>lv z#ja^;^8GTso_E}&6zXqI>M`RgK+oZd4))7@yewIWRgWtqm49%@?w%gf&~GPRGsM^F z+#70^-&N-oTaCZ*UNTwPR-P{TnXncc9&y7!%69t~w)ul~(0UlYo+LjqQUB=edZI4V zPhIf?PspqBz3}o(9TOxK+DiH(14@>#XWXb|`ywkDUsk-452X>FB=k5w8Pv8zDj?UV zjtI28iGOP8zt7w_)yN&%39)sC;}jliaPET!0<*{ftqk)Bhj((SW=}q*`GE~0Rr(oo zoimUFH6hP!&I9M=KP3Z*fM4a|wL`0rbq!AE>gJISVh3r?{=A>hzk(f=om#xtBke8X z5rnT~zg^iZg(7jD^HmMbjHhAvK+jxFqyzg>{zyT8yR4dJi{Jm9pPw5|S!?&tu%*?R z<3->bFs{JCKwR=u4tq`IR3JQQ^(b?E<4^5b> zafW@y+~-JL=aheR66`l&EIu39@d1EF^`{fOX2>S}jlQR7!1Zbm!dBjaMjYL?m)CU9y2_r3|+IB4TIA$-Kg3@px-lFbg(zAF%!=%jeACGJZssk3>eo0{7( zK-i;bQ{m>Y&x4=w)O!78N}O{dI@b?~dTERH$Z{9n4FuBq=q8`n-o_mCRwF#5ZD%oNN)TGyHm!WU>m#ndi z=bEbd<#+yiuOX|ed&u8>T8r2N{Q){g_Z1b$;7X8Gyk2`Jo$cbLMiD_vU|r1WF8Ty` zquF&O_GOT|nZSGn5XL88jBPzZvzseO%!j z2?P9-1WC2``8`*;UirLR$j#7|`Mw0Ne!gD3lG(Mu)5><9%k2uJ&o)swQ+2d6G4arW z)t)?>5Im5qE@}NWWj($->B=8V&6&8W^4e_TYC3q0d9gd>wCuahh+R(I>lqR_NWjLs zh^tL_n@{)(X$M`ehZhAd@6?m*EPCrEs(lxB+c+!7FCtKJb2_pqLnZ9gE(;qh#Hz(c zES%%>P6xmdb@aEZ>hC@cOje@YSY_PZe-KKOt)Mq%lbODC~BCyo*G{-ZGu{s8Tn3QVOVsXXx$wLbR z{=+1TKB+QY^QBtNgKscJKOj^vMkoFF72oiMZ?SVnl2eA^VyEhOLwUcx*}4L;7w_@O z()uW-*oXEX1pSRIXU}a3o%pKy`-h?(U7ooEAT4 z9X;ILIY^{@4tzrAek}ux@xFcN-)TL=RTD~Bv3s0Dr(vJl+1@6{Mvav1A!Q@N4DU(I z$Y8FtE-xQ?QP8qg4*IazG@3k|L*>zMvE6?+;X=FH%#;naz9#Py2pf!szLrb{#G`ii z|GV*BE=qV4GaXYQvLC2iJbW&COCfoft9n2M5Ew_GVP$ElV&*|&wxm|_a(0?>EV0Qgl z4TkNVZyD8TjwBMqgp((=`BW$l>D{$~GmDqB5K`Jdx}%cp_cBqiy4_3jSV>KWvb6E2?@`>2<$10?O=0KTce=8t&`dAu56 zuZ82!H9?_~pFg(_hnJMRn`eg;VJg_$+nbnV!}N1@R*qY>J=D42D$)lCd@uc+{|{i? zW%_iwME(#5urZ)E&C1;{5+ORkUThOk^b-C1?1 z+z0$Sa6ABqj(I@=mpD+=7B=k<0>&Sf7ew|^d+;Klyo+Sqn7r92d2L#)VscUKUlQv1 z1bs+>$8)6H1t5MV(w#CLpsxY-lJkXwH!BLjk$lU%2B*BkQ!%?(EsdTz@RFpUeBgSe zg|&>=z99{I?+wVR{E`KC1PIJR-O3Nk_l*GpP-s>qwvdqaQ;W>0QoW#{;G9(*88)I& z1|>FXTz_(6B3%K>D{8C2EpbdZ(Wk29$gEfyGcg_l0(dqu3>N_{Ul8dMsGyb`7%hQc zJ=0n{r_kYsOZFTbqZMHO5WzViifO%lWw-G44Iuc#2h@;c!caSAciHOj$r55tq*NmJ z?S*{Gnx@iADm9aUn>iin^&KZwe+ zlC4;@3EyJq1^ug3*U&IvA@b{xl#*IYbHhQE3kvEquX%Ot?d`1^w_SLFi$!>-!&-1l zhOg@%ql>PBAj0#yH)7vF6eB7SStjsIkLlkjA=&S91|&VE(}rg6jg?Dn1oVksGq*un zrYa=NGd4EF!T+Huz+{nK`%RL0N|HkKxu#|LaW^YF`^d=1^ud*gh{*Tv-vQ0sq_2I( z$SAWwCCu9=$j!{jE0E62rSPqqawNQ?S_1i5gDDrRC$mQh|I?xy=+q+^Av!Wzwf(Th z!hz99?kP|f@R!o(m;-LkR`b)W)q@hTEmvXxe?cC%Joo<}J7vuVEz^Aq`f@OSo?-6i zU^Q3qn##-JaJUVxk*sW(aTUpEJ(m;3tsDS=i1XiG08fM?etXKF(kv{^bDmy1Q$jth zaDLYqQ~*;Xs^4XQ6_}qx_JrEG#PY9bcAZ~e76H#xqRx7kVYbWHH4@M8PqD7&TSI7m zG`$f|0Dm`gbi6%DKT1+eI6z1@$9v2h>`yh)8IXxBNB9y z`MSs-fc?sYX7bsERlcqWcJlsZEye0(04)%Jh?>`6fw zMi}psI=M=ei(jc$(R(>^^H#4q)BUwXLymMM#>dBD9&Ag_9-=z^P28Z?DJ3CHR^^G394x)pLtGu_|Y0b&fUW4|rI3KT08cPQ=)6t@C}F76IR zU!M2g|L$ax%$y`MIg^u5zH<_;rJ)GIrNjjQ0HmxWr}NbBJylO^)Tb7V9#s9*;Xsv) z-2s5$X@Fo4#dS0&`N@dop{y>CwTwcEN$_H4@)Y`HqVSM6@{o0LcDDQA0m!=9z4frO z1$#q09Kedo>RN_D1msV1#Fgb_^n4Z%vSE6rbJ+tOE)$8gZ?L%|t40V=xqN!Ms%fnM zev3wOcxLdciznu&`A+g@t7Uo>O?gb_xGjETR!_~V~9L*lCC5?z_4y^RadV=$n%ZN9LBJf$iexWZD zl8^AoQnRZBk;zt5$8)_s^z7z05@1odT%@lZU>9D^6OO5S_V#c%4Y>}te}f@=8o!dt zJ?CpZBvc)Y6Z#KRn>&alO&W~*is(x)j$9Wy>XSj5ObtwgLHcAs&$mKF0pzsF&|k9Y z5}|98g$LuDNw+&6kT3o+WmKljg+QD-xMhiJ=X(1ZS0vq54;X&_Of^3w)^&X_Hx*oN zwwvM*%Cy1+Dx$C*X5$%+Q=Z8XJuOvH`^2w~sPSt@=7X`R2b2>=#-ziL22GK&qk3;9 z>xNsG8DgbQdM?k%3r^Mh{&fWIr1#1lystNzs$fPb_puct9ybGn^9u@kkSB~xJGgjm zTt+Qf8sxvu`uh65Lxv4cgcp*S<~Sn0uv-ELTU?S4y@dAFLuM?{Jh+m5%vbBTywCPL zM2n|9FU>Nib|!Z`n3!J8M1{a9Sg(IH#)fVF6C@3{Tj=ga4Qt12jk5Q^qy9p)UAThK#sNGBG!!s zVxD19!K05DojR>kGcI&u;&s1gykR>%zafg|N1tyGya1)yo~9kux_}T4j1gZiUU8=u zNJwD?C)ny~Qiw<)m5|5m4D9FlPJAtd>PIDD<$vG}OwulJs;2egC;WIExE5>Q-Zg|}2$pRFVqh|Ue+SwW1y$mT0ca>< zV|K>_k@6ZZjFgY=OB#f(TyRc zE)$puf!^se$6$<-Nx!i4p?d2;YMutkIhpzZ?HU!D0=P|@jgHeGqZt+m`MIH7&|)^J zptX2yPUIGfLu%k$Q`_}a|7K@1`Ip6t`>Hr_U2fy`J?gKsPoFUGUz0z7URPi5E54F4 z8$+UtA{4Tm&Zh%4Fi>#$V>EKy9Vup`$e8bT9%<0jA_cwhZoL%+UbxIJo>?HZys1(h z(c`Sm@TG-(zP2915g?J3&z!~`3YE0pHc2U5v!#mXMdh)Y)(M?#73qdVC>+M|1@9C* z*l{(p89ugNvfz?oP!BvKC&#d{3^Ry`j5IO*?T}Ffc{h8r+S>45blFGH&*#(!1h1!R zP@t51rfi(Hj3#``iGNiO=UcZeKIc{r@!ulF;DUeqgaMz{G2DZqaRWGTrW@LEg)+Tj z6hMH%d`+sq_nqT>5TgJmX}vk&wBwj9FKa};b?KK0oMBtu3I4vC>GHlV?+*)P3y6y4 zDgmALqQ7_ZFpxhz*1MpBjs=MKE+SOquz6kUKop=2glPT@11c1G!r@$esurOV`Z&d1? z=+~^YlKZm?^5ELxCkpJ6)W6bw=pU|69AVpYM<|y213Ki-0i!tM`Ol~Tt4LW0CSvf-q9Ry5*e4K_ybR_i{&Gx@(%QX#pN((lkE^Gk{BKU- z2%Tq!>BCd9e8162`zzit;LbldE%b% zJs#k&=q;SKU9wC%7Nz#pa8jqNfqeQ#xN#92CKG`Q34W=ZS5xJW(T zzm`UgEskQB!?cqlY#Ien;{=q<^XeSgF#B&C0QfYILx&k-T9++QQO!?!2Nq+bN>c+1 z{B=JAdj=a*yAyjU34(E=c-pINKdqzpgbY#5?WuBdm#*n>0@!kv8fNyvp51swgU@E@ z68%eatAk$`@9NC@*||29voKi2!`$O7IZ*BZ+pC>J*~-?obkUWR+W(*`z?gQ`W@c3V z{cI$T1GJgECG)0u0RzJAVrTI;q0)Q-sA|7#ty64hoDHCk8?QTp_9uX1+GC)iW#YPC znLiXS@mu*4Nu3vRZ*i~tYC<>txu_yld`122e)qG_|F)Jmk66wgau0zC=~sod7DRnI zoNJ*PVdzH9sou~_f}A#@9M>J}OWIthDx37DxKFW-T}xN-YW}80q~|HQ*kq%C7h_Ec z4#=xYO6Y}l{~#hhhX+fUVh~>ZPIVau#j=#yYRkuJG#E>YZ}SGsG<__rKgrU1jDVau zazmA>gg=bTw|uOcTX8$T%}5yF3)!*x)A1Ik;yux#SG?yy2Y#E|rr39-}06tgx-5t|okFy#x}sKLMZ z!kT-9hT-&P=Zi=vJ0&naB?&C7_((q#N=gj=VmPG5F%A=-}L_YY>Oo>4!Ig`c+ZTasQ1 zR`7+~UryRJQ|u4sGZn^<^^T>JR`}B4ALsubenp9l%q##LU~D=8CY%6V+ZHgHG5WZR zY1me}_;}?{85acSZ@_$gnEl)SgecDJT?WzsaQRa;1MH|+U zHTgX@t2UuYcQ)%7z6w6vPIof6Tn_zBrX9vvn7%Bz(-flp`7pM03-!mQHM`2_O};1j zw<^`c{bLWYiU78iw zH4X6k9S&X@O86b|XTP=U(r_=$pHn#(LTJm)e!qQf-x3;=x+($k&c2=I5{-?*^lEeM z`TFk}b}`my<#9h~ZpF1ouAu{K%|kCk$UxiPQYA5#$?Q7%?5APnS4$iH$I|8FoJ;ei zg}XV1YD~%du)f7c(CMuTA^@!HHI3r%xVQIknW~FApjz(|)5#6HuO2JWF$AT1%L+?O z-=R=Y-^8HD# z|EK5EFy1wA(z{WHuRVb?KEzc+DfK0+>)1+X;Q3&GwjVBMbhJ4G)fi4{#F>mtFuOlW z|BIt?Py&rs5KqjgTkKhGv_&5tV1Z4u>Sn{1bv^m&G?yFe*;1Xoz0a~x5Bl@%gLJB+ zLQ-~MFedEb<~}558vf0iNJ+WQzQXiZ(H|OpHZ`c|cgU}?lB?p+?%jni#xirZ)hdlN zi~rCFlHOX>3h*y3mjXJZ2kCok^XJtYxNoiwRY5;EUnT|%@hztZObH&_Y3GbTk$xhl z{)E#+N)YSNq3oc%5>aZDTeBVn-CR!N*f(qaP(N}sk5BQq#fo(o(vlC8TMHplynRLb-c#7st+oT3C09qSJ~7~Yw&czv4Z{6|X0ZZ1B* z`~GJiA{6p+6^?fC7CC-3Fpp2I%dacg=lDjHj+3gt5J#jk@NfBfP8Amv8ofXyjNuZR zQm$}2R{&Nbp2BZuLJ%r19$`2oj1dZ;I+W>K`j*pXnd<35oO%@18mMr+2^5$EA~k_tynB#_6oL z_4V053f?5jh4?qvQAKu7AtUnF(@wkXnHQ9JR{|@~Mts!<0|AN69th${7ZDV-;cd=) z#E#AC^D$K+4T($OLYtD#Xt_%sA!SKxmf2ALy{)Xwwxl`4Gi&zm1&0l|@b8M%IfXGB zFLusz6GyO4Z%iT z6ve)f(B=v&qyu#+w;I$rgDdUVRL~@<`d4V7sRM8>je|0y< z(fY&c4H8W)VZULL*i^nhJKKjrc|NfsR&q!%%S6khH}vkvMT|okvh_!p|09(W~G9M;*8(^ zOTu6wWK>{ryc@Qn#WU>%{&`n@yptDMZz6mBUsNbaOsv$k{p&2PtBZ}J#D9jGnxV4pk==CbY2eB;wT68pFx6wF5J!vKfT z?ZsG2Dz2c{WSh2^hmqv_5u@9XZJppL|LU>H{%3(}F{|n}X0?58tFYIPZ{9p$0_}&$ z6h55?a#~Q%sEKFC#jF$k6tNio|RfIx>vijPk7ot4}u_=d6LG*P#*VoNGeL*sxkc+>O@B7D7Q* zjjR_lvP-L}j`7DQ-nMG4BZp_is-5Pq@0x=0i)o`_7&5k}FZ+VjhA`T~|yk*mX zC6BwN%@GLD-a9<7E#=Zn+EDQ@8x+oAen^#pMVh2H~|> zn=D$93w7#_GHB9l{{faRTOwgsB^!M z*U^{0{MdnpxUblKvCpWUc=?p@;MN?Vms9??F_Gt;K{LSRqok|x3G@lan`7cpQ6Co( zXQ(lHX%xx@-fDwN%H$^G&y);Z@d(@{@UEWTnD;FIVVx}*lugYin6JQTG zI!nJ@#PSOWzr!3)(&C#&wZN#q{C$MK-z1iuKbS40svVloLT9vuld(=qZmchyEoo%H ziGdpFfkS5$_KYc-#Yga&UqEM>hDNB`B6RV*RsOL`h&O;?q(3nWKM?MRWw=0^C>E%} zlI92EbCl@z(}GS*@(^fQ%I6AX>$pZEKNwD-a7ugAn0VsOkYN034^C$F1KpF%V@nJP zZj*KvsUL-i(=wLW+lrI5s7*+1YT}M_y}bX}+pJrX(rbB^Px#>u`2#yj!wWyMgl78v zV-It{%bE@{YthXTwa24equcAptca0t)(u?G60;~CfdAp$g@Z~m(WU)NtO@0#&))Z9 zjZYsP%+;J*Sj8yPOY!toZ&v?Rt|oVeNIByafyCRjll3lK%8st(r%JTKvcBr%;e(kV zdYlZ)*xLYzjQJYi$6L#5+}x&ZtX)MBSZzhG#MPrm)!1fqzz4WFf_6V8;D2ay{j&8& za{pc#HcvNbgwr__-H9q4`kiVq%e9_KbJf<2t``NLRf|b_s?m)Nl?gY8OVjOEJnzLc zvJzI`cqY`>rQ;a$<6+2Lf2h5n$Lyj~>XaR*`Wq&o=|2%SfMxR2>>}5<8FUXC^SD@w742Kbw7Uea+1X|*$75r(i_^M&S=1~Wh6 z^?#M+2xIxupG>$U_?`wkXYoOky}_&T=*F&R7LR7;v5hg!_*SXGWvh#3X5f?{!--ZU zEXthA68(57)-;B;37+@FIRW*xBb!rLkk=1UQcF3= z3RrdD)w{iw+nNH$%)udH$-()HILJo&ScYZ2-qydO3E|M@2SzOZLnBaUvC8#$c1g%SesFGv8Ah-$XoLtot6qzS7qfiPe(8d{!BT0aSFO|D|c&S5q@Ui;^!VZX)JVT;3F-TKi^ zL$mpPJ&D+aevFYu73f738nil}B{i8Ccansv zC88mk-yp1^u^RHNDC}M*SfM9g`1**QAO(d&oniiDYs~zyr2*kOO@Q+Tt3wNm} z%x>mtI3&GNvUN<$;@_s#Uj}lu!lb~u1h{Fg3BD$Y?fSw#VZ&y-xWoB!4JOev4qidQ zZ){{~ZdUls*%?o>? zI|OKc68hYBmn^hyX^Ma8rzvD^@0)82kWBlxbou?2eynSC>AT`HK05J1b@|YVYaBdHr0eZ`5(TbACHhD2cu#*Og0Q*xYTNz<6Qv zL#e5Ie=!xdPj%bnydA_w>k{UsVpOh|#|3X0Z}1X;GT?>a`lDnK$sVPm3)YO6K*#X} zs?t9b3h&C(MkcfEpD6^Y%wI=5$kB#PX$wAic=7VOd5CTLW3{Ir=;Q?d+LF`mMVJzF z@t5VCZR1ZyX79hgV zY+MeGD@xm7Zaz+xp^WE_jAO?6zsdUlP3GS*u%*3|el?HnP9b0P;vy8cNaQCy?zd!Y zcf(F=xDJHbfIAmQSx*iHcfnsK&HZO3$+0X6M3RqJ%*#B6-q$%oIR8t)q(f*kTHkk8 y1J$$iuDN;jr?>L$gX9n~_WynpaO*$N$?=f<#(w{L>FKitP?pz_t9WA-{Qm%w^{Hh5 literal 0 HcmV?d00001 diff --git a/doc/assistant/dashboard.png b/doc/assistant/dashboard.png new file mode 100644 index 0000000000000000000000000000000000000000..c71558d7927ab85bfe22be77c89377356582a45c GIT binary patch literal 41061 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

&@2C1Jqw@zC0J6>C9CgYHf~{ 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 JiIkD=e*uHp_9p-U literal 0 HcmV?d00001 diff --git a/doc/assistant/deleterepository.png b/doc/assistant/deleterepository.png new file mode 100644 index 0000000000000000000000000000000000000000..20db674caa2470ab4502ea42a4ec44a6320d0aa3 GIT binary patch literal 22780 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 UZvK9oB1(X|iYB<|vE{4(09u5ohX4Qo literal 0 HcmV?d00001 diff --git a/doc/assistant/example.png b/doc/assistant/example.png new file mode 100644 index 0000000000000000000000000000000000000000..bfcb9afa66bbd6143e96ad47dde8f67c041deb5b GIT binary patch literal 110994 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

b} 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%0DBFR!4FtPrZ5j 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 literal 0 HcmV?d00001 diff --git a/doc/assistant/inotify_max_limit_alert.png b/doc/assistant/inotify_max_limit_alert.png new file mode 100644 index 0000000000000000000000000000000000000000..d8d334152e80428d509b036baf8a2d9eba70f4db GIT binary patch literal 12583 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 literal 0 HcmV?d00001 diff --git a/doc/assistant/local_pairing_walkthrough.mdwn b/doc/assistant/local_pairing_walkthrough.mdwn new file mode 100644 index 0000000000..07b6399104 --- /dev/null +++ b/doc/assistant/local_pairing_walkthrough.mdwn @@ -0,0 +1,60 @@ +So you have two computers in the same building, and you want them to share +the same synchronised folder, communicating directly with each other. + +This is incredibly easy to set up with the git annex assistant. + +Let's say the two computers are your computer and your friend's computer. +We'll start on your computer, where you open up your git annex dashboard. + +[[!img addrepository.png alt="Add another repository button"]] + +`*click*` + +[[!img pairing.png alt="Pair with another computer"]] + +`*click*` + +Now the hard bit. You have to think up a secret phrase, and type it in, +(and perhaps get the spelling correct). + +[[!img secret.png alt="Enter secret phrase"]] + +Now your computer is in pairing mode. When your friend looks at her git +annex dashboard, she sees something like this. + +[[!img pairrequest.png alt="Pair request"]] + +`*click*` + +[[!img secretempty.png alt="Enter same secret phrase"]] + +Now it's up to you to let her know what the secret is. As soon as she +enters it, both your computers will be paired, and will begin to sync their +git-annex folders. Just like that you can share files. + +---- + +Something to keep in mind, especially if pairing doesn't seem to be +working, is that the two computers need to be on the same network for this +pairing process to work. Sometimes a building will have more than one +network inside it, and you'll need to connect them both to the same one. +Make sure the wireless network name is the same, or that they're both +plugged into the same router. + +Also, the file sharing set up by this pairing only works when both +computers are on the same network. If you go on a trip, any files you +edit will not be visible to your friend until you get back. + +To get around this, you'll often also want to set up +[[jabber_pairing|share_with_a_friend_walkthrough]], and a server +in the cloud, which they can use to exchange files while away. + +And also, you can pair with as many other computers as you like, not just +one! + +## What does pairing actually do behind the scenes? + +It ensures that both repositories have correctly configured +[[remotes|walkthrough/adding_a_remote]] pointing to each other. +If you have already configured this manually, you do not need to +perform pairing. diff --git a/doc/assistant/local_pairing_walkthrough/addrepository.png b/doc/assistant/local_pairing_walkthrough/addrepository.png new file mode 100644 index 0000000000000000000000000000000000000000..b82efdbea23cf86359f3f5ff5f794701479dc898 GIT binary patch literal 2259 zcmV;^2rT!BP) zK~#9!?OT0J8|NAS`Qy0rM~n#&UqbjuQWh`W3Y2O%Pmrqe{q+3dd*0{c_dNIB`@Uz4XnK0u>kpiKf4t|ed)V!PfaJgqmArJj>g=Iy zvJ|Q4o^9&U#sMdxgjg!HCb_W4r~~uYYYQaz_dN5_Gz&?J(37NtNV34<{UI3}PbBrs#Gm}@x06-?IJi1H2V-aARh6&4Cw-N9GxNv57%{x35p?COD`x}G^TV5-ajGM2*f>A7gHrWP#LmK%NmwWSK(_qAmw?*Y)s zp)U2b|9-5?i<)B6eDn)NCr1JNxH?Dg_@wdenFYXbAT{r3cKx}VO%21=X+SD-6I2q9INikP#E&C;a&@L8L3tt2-F+um*;zQ)G2

ZD<%<)174@1rk^Z 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 literal 0 HcmV?d00001 diff --git a/doc/assistant/local_pairing_walkthrough/pairrequest.png b/doc/assistant/local_pairing_walkthrough/pairrequest.png new file mode 100644 index 0000000000000000000000000000000000000000..8d3f603bf47c7b849188d6cb4bada0a2d774c57a GIT binary patch literal 5383 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 literal 0 HcmV?d00001 diff --git a/doc/assistant/local_pairing_walkthrough/secret.png b/doc/assistant/local_pairing_walkthrough/secret.png new file mode 100644 index 0000000000000000000000000000000000000000..1eb8051222869ffe4f9336e991c0de46fad3d52b GIT binary patch literal 5132 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 literal 0 HcmV?d00001 diff --git a/doc/assistant/local_pairing_walkthrough/secretempty.png b/doc/assistant/local_pairing_walkthrough/secretempty.png new file mode 100644 index 0000000000000000000000000000000000000000..491878784504437ef073cce23cfdc9050f1d67e1 GIT binary patch literal 9575 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 QB?=%TAunDfY8dqY0Cmw+jsO4v literal 0 HcmV?d00001 diff --git a/doc/assistant/logs.png b/doc/assistant/logs.png new file mode 100644 index 0000000000000000000000000000000000000000..8e9e5b1ec80be50269f9c79c06e374009cfa521f GIT binary patch literal 33631 zcmb5V1ymf(x-LApOK^wa79hB5fZ)O1-QC@T2MBHfg1gJ$8rMj|dD^G($s85;l~0Z4oiQgT~7&hS=JRBe81Z^@{oFsEa}tkNszBw?YohG2m* z31K>cJY${5^Psa6kC^da%=&dFJlERuq}c~dWO6lcR(dQMoJn28GNLt;N;jVz#j%00 zsxZ>%q!G@G8W+QHmq*ta^Z&6%4EJ$6>U>jbX|G1Byqn)s;AvsZr#kEE*wF#){0~`- z9|>##$@eo-ejgM*2?wl zH7Ot2jjs&J!054<8__VB52>F`j5bz2CYhGPa4^TfHA&LJ3!!qL@Mpc90?$@G&sysR zDsN&BRS&;l773RNXvTLs?CxjdR!l~R^_*L!q|P0 zx(!Rxg{HTebE+?q-4oukBwf0gXPkZB3cayiem)WkAZaV8t*>wXxeU50hG06CQoEw~ zJH`e7+}2SFXP9V+`;j;I+Ec-dsmVR-rYjZBEgS(f)EA?}tOzo3(Q5&h{?#h?a1NE$nBuNWSn7yER2BJ`ujR_MZqi8iD+;k>EPR zn+-GlknOKBk5BCtX|ge^)8|x_tE*N%hyw!JE~-yJnwUbCPn(=5HY+!+7{ibz6ZwkwxrO0A3U<*XQ3@({=3FnaJ_{;>wfJq8o6#Q zI(vpOi`7;}Uw_#f(WAk_!#&e7kDhK9GQ`5jpnKq()aQqqq$#cWln%pQCO7?hvYjFHnNLoI5S*y1MKWnJmY> z!E(E$&)2M*o0dj2v#Ew4s}c)QuOXqKqSfiiz&^Kh=*TsM+vu|sqkRB!-iIH)j9q3) zAx1|MzIe{{=**({KK#B1Lnn2*iE4bL0Sv7~h+483id6V=d7Xt)yIo$;}$lG_d@BE&np|i z&djr&hqZ9Yoh!qM0CHiM&G%*cd_U%l+y1^RY^%6|*9`xCYcopoq@oB!AXJp^0gJcC z;N2##KCaC%!pa%~5&WzhM=wiS&6@Aql&(ooidez-4@6NWER7NTM5!?-Ts!@)-h8y2 z&V}66=OJWdzMBtJZ+msBTnMu^vV3iCpXOi%E~9!M0rX@ahuI*ZVxk}yY=t7#i6kV= zp0+Fj;D$t=#}~w6=hCX6HngpA&6Zu6af}6L(sx6uXL>N~@X1V{s}jRu6}%i)!MvR> z+S@SXKG$-@_7O>9c^AKpLk<-@qQuAfa{HV>01mFAuvgvaa$csl;<6T{RPx^npI)wt z4Xakp3IsBD8=;-rh#>8Y6b*vTC)tM zJ{N9}mpax@__nY6bSGpnZ(S;lE<2l_Z;tkK%QEMlcUfXqSe`yUcXvI#J}!L08w3-< za?{k%@7x6bV7+a)DsG@8^qigz=ylyl61YFUGPI4#;IHELzW5n&6=V$V>(qXadn=iS zqSymT##j9mU~!1EF=0j&7#zC0)orEN+S~qHxA9nB{5<^OVC=O0cZ2A7JCV)9UrWR`(AXneUH_yrNj^=Vd zN9~m!DV{w^b$AOI&RLt@WI5f`%+h_*-KD%(tI+@>?^(W^B}NoUzmbgC$YSTe=ycN^ z&Cn>DWfTYd6_HnQ3z{6z6(1Vbd^B?>?#p-E$jevKH#vQd5*n5*7m*Vgk$t`2{;Wsg zbT3pb@9j2dlmYI$lScdB^7niNn(WHynqLU%On~OT*mlPA%&*CT{s! z0Q`Qvd-Nl6Mn>S9>CSXS%?Pt$CjvN(02~Y3oBB(WOeDt!aos&8SqtunHnHu;KK5aJ z;<`NknvF6QZM`2e%zbil8qb|aXA8rVy&gEDj`qaf*QcfkP{VRlK2Dz;x$Y(oJW4P- zCPRtarrlR(&wW?j`a6Q7)N{GVV6fjTy8bY@T&e6&FuzT4CBJR8GmNR@`|MO8Y0nYX z`|M@70*;rPSV%}ZR2(*Y!rq>I-_B~gpKC1`H4?p4&un3t?dPF3qSdN z98HXn?Pe}&m%d+*fMINKwcCnW@I*f~jXYm7TNRG9PF|rLVl1y+Yv3GlDzcxD^Cddp zp<1ETCG%{3t*P*r3*i*HqY{99+>V0+^wIlE3}I)KKjfB;?gYnO_ivG%Hjj1}I%>wa zI5J+F;Ub*xu>`7*L1sO_{EC3w`zla_BhYH)Gi>l;uArrO;){apP#;(6nev;;_vt~m z&R+-&Q0fn^R_^}+kh^7Is}^|Ly^k8O2gX6*tu7`G9te(#%68fqYrw3=i&O{m!uPeaX&N!`va?*VirE5>@1A+Qh9}m2c}gm*`mvUz~PWK3Cu; zLFv9ZJqL70oD1^P&wXGOjXXDDm3wqU{u~{c7^9(L{B}D7k(Ec)@2vz}Gj}2)G9xAu zXhhZ`M%lb$)WG6ib~GQ)eBI-3>iW$!Z_XOC-bUpEoUp(Z(yLSjx>H~4rF@p^$Z@)` z>nFx8F!j1HG1VI<_w{@_X(&@UlJmKYf4h%)OHLV+-dcU;o1oj(WN5f{$a1WFu{_x8 zY8tu|TWSwLyNb_RsiY6{+7Gi#;yEEQH`Be3r?OE?q<1}WOP_*|~GQScUrQH*^bN8*O0UTL^_rc~}@#>&2= zmquthR0~-1myfk2)yJd~J-HhRdYwl3<#D*7tfbv8kJb2YTAF7>*Jq|!*b`N+KYN~) zc`&uuCP|KA0x%)sr|xFAvJSplVp@Ol4qbVCom9_W>~T-a zR^9h9aQp8MjIUz&(Iy@wBBWl%mNeps!uVbiFmrYLZaH3R&y!GowO<&;muXC8y^(q! zP8`t@HMurkwZ;=K!6V5Wn!bHLSu|@o=oX7ytgwGo@O*AMc4FV}uX}spDienk8s<8h zElaDG_4EWT#knmEkseSt5VKmOWoo_L++fBm^Kj3X*H!rY%TQ%L?c@fwOSaiM6^eLz z-7l^-!Td-7iw~!gK|k=RsBRI#sD*Kw|AXH@7mO zz#?@0f)d%H5b6jNssto`a$|x`>Z22f{)lrwDLnK+2-R^cl-OtA?Ja|2aBy(d=gE2x zZE0{fI(NCLtF;3qfQi=~DH2iGj44E%3Uf5hy`vRWjVisKH$&OzVN+zUwx(?YAHyia zWPl%(5W4enuOc7ItT8@Z_ z_-&f1MV~#5*lpdPD$}j2Urg50@(705>pYP7xkFP61qH=--5(|(Ai&DE`8)jn$q9uB zsQZVNBO64oE81Tl(`?(^&pTnxn)Zl7^#3sT$OLRPzHc95Y{!6wWS2vLg|&;m2BRN} zO96HFE^Xh^<-JrA0|hzx@aqFjz&T7r@8s%go7qIp`ue(`{jq*#ecXFNroNb{DDQ`z zBz=ATyA@aBl;B2VEuf3FogL_G#GngzioU+~AOPLLrpOCJLqoCL3y^Z8tU+yv1D>@l znXxYNj&J<<+hMz$^Gmbp&BSuc1~?)1(HGH|ddwh--jn|OG8#vKon6f2qYWSXT8;G_ z4=G=;`+JpexAmg4232Yxxe9B4RP~_ahyF{(6SRj*dY=%OZ-l|)Dk-Gx&AUg!T8lT$BHUpww8dxBi z^^A*FfCS7e6T_Mz9of7|2d8=)oK;N(X*^r{a#9U-eRsoarugAvQJMUtL^Hu4q#j zzXb6^zF=^aaj;auc&`Q;$?tMA1dIi~#sbgc_~UFV=3`pP)nzzyN%7U?xNSQc^#J{> z>R<$O>#11doiVy{QkLpZ^*pcU`QH`S+3i$e@jT9R@YI9!?CjK1D#{5J8nzZCx7lG; z*#=W%SW#YwHD}Dtr(x3ot1YIi%)tm_Rd!{S*3Q zbTslE@9M+6d}nZK>fCQAD0#oqU%$kZx2RQB3b`GJDEgIY1OK>Yv*bx$2`;2nL(g=W zC|51F`P%hGN(O#$(CVe*QOL2X#cb1(FT$kzjYON#sHH9sMPivnAZXLwxznUx&wZ;B z9;TAO^V1?$c=4A)1V(WZEtp@Z7)vqK!X$J8$5!JVl%R5UDSCnwzEa1ogc4m)T#;HsuV;sp$=*^Map6m zH6AxXADq|{AZQ-IS4#=UBtcWuFvil{o|u@J&2l4X&It_(d3=17&ERI!=29q9B1a4R zLVBlIe|dfFgfdd@`%yyT;c(FW=w?b+n=2zT&Y;z&(}hx${BiRUsbuD{Hvu2HiU=XY zyh*=KqrGy~tl8(KHaFlaXn*sJMcrzp24V1lY|dnc1$W6w(~RnwMn%@^W{GPajqGK` z@;Rp+u{Rfd|IBS(yZA=5Y^%BR#^8IkGdK7k+W2Wc;oiis!e@|Fz{Q-a7=`7GM&0 zLZDKt+w-&0@TjRPEfDAwVqGaeT+)A+%x6Olr7V)?&0|{nRfJ(O=**UYY8;W{R^$5V z@66GVRa#2ls&fVm(8MkAkO4l(L@`O~J@NFKDPWq0Ukw3>Iw~p2C=BT z*7|hbx{~KmgGD|jlz0^nn?516zF0fu=2Jy69iEEIeXArOY+`gj^4VSlWv0f z%>+`V=D#Xh_NF58`u||#ay=A^KZdeK%PWkRQ8teX7A+}GlQOZY4H$X2yuqCzwKrBa z?EKK1-iY;gVr_i+nm5_l$l0F->d?_5FD5eo?2pTvx)2zpOR#EeYmqJ1W8 ze&?K3ZL-MmyOAE7A+7yg63kmh@cP4h|)4C!Qs>i1P=uf7*%d}Bbcs|n# z_uwfpnYYx1OC>Qo=NXu7I^|jCF+P|R#CZ4S?Hv|xGnq%GqPgqLINjWd(|KpWQ%33J zsZ~lJi^N~vU{XeD^_bU-7jakpQErZx;l&>b8+P;mEL%*^z|NRt+=sB+Qx^al@cR&A zsF0so($B_YqTiDPfhlu}2}yBr@!*!~-qz-3aWOA%aJd3L`9c#za9oR5f(DgbZ;i3> z<)oy3l#9DMTr${_I%tsm{E4xRsS<<${lz#<(T<<7qFf^hhEt%zRDgk&P5}ch$k{^V zr&1Nqn96;H_-z@Z0<%_uWc)$Z%x*QQyZO&bS!-1KKPnha>CWA%x@kj~J9z=A^f$)- z--@ZvHGLah4@K$ru`rbQ%8tKi4YZLGr~cB@&8(JGlNTUSBVX|u7j3JN;pNCG)e-VJ zmC8G;4t!v-`xrmqbaSX2q8||WHw6TMLqI@4!@wK_f}Z}*q57{|F#WPXte2DEna$#Xg;<#$>hmjJ zloo{>y=RLpBL>MYHuy&dzfJXK2Pz}xfN~`Y+jq{8!!o%3WB8tOkHsslB)!ds?1C(SlX|%fQ~1Gj|5@$z!Jucz?i# ze*@rr%Xy!Y5Yhmkx749|syeb1{8o(im)|Z|kyB8F^%{tSNZiZs0XH)ft8onePr7;Z z!^O1fd8I=`EOpAV3#$4mQ73}xrO7!aVh1N|5&!$}rr~1_xgAkV!p%VUrKC^>z`nj*jFL>7d6%N5oA@gtl*$-7e;A+8}G1?a(1c7Tlqmj_jb^W9`<*4Fej-X>Z z6GVlYRg04^V2)_8Otnx+2-nmYjKGH;rXDVi-Ic1j4Bodbr%O0O6wY3E z@E4{<#azg>Ux^;1C7T}-6&(s%7#!F@r%35_U!W=GR?K6-d>NVPy<`#7m;CwBHVf?o zDL^!(*VveOXDDN1pMJ4by^YkJjG~dUgiGX*D+<38Mm5#Ep<>w{{N|@&?R#I+g!6#$B?EIXrWw zL>ZY7gf}1_u(pIF_m|LVN*lW;!b7RwG^zJOTq^H)xD!1(1IQQD@yhW0#LBdPW-k4j z1s^x7i#w(lU)tdXpdyfLgZUL-D7d##D*}v7lsaP_#Ordv?ehUkz4A$%W84@Lip)@e zZ?3xv?hI*g+U^XR1w3bk=TQH>W0%?&}f@Gn4q zN5MP{N6{JhJq9hMoB8Ez2_Yeu1WANst0J#FN!=feFZ?`ke^r4koxC3@OU{N~{$|Ey zW1#aORQRO<6u`W>@gfqeRLHaMo08HmzA3obakoAa8Ej8};v=7TH6UM}>gMCLk=WJ*T`oP=I5^4-X7ZGW^5S)?V{$RhCz%O=E z)g%%>O|8@Zj@z|)mw!Qf&6FzJ^PTUnY0R^qZ$hqzxuL>+9MP@4WUC?g9s}4^U>$^C zt;VSO(H|i-M@@&Cghsp}qQbef)*0~~hEkx81F7q(Wa#5}aJ9-1I6@idOu;{?hydS~ z$gx{|w-3VgzBx>uLTO)U4viF*H4OD9iVt_u&IsMjPNh3{y&fJTEn*w1i>ubZZ+f1R z{hd&}G-()>oRxMv)sW(m`J{TkkHbA$$>|XMzNFqLwCcnqoGfO3sDj3n z#skh>kxXV*;Ug-34i1X|06siU-p!N(`YLuyzI9pi_8&pO9ix=Eo^b=R8*O+KHnA`?) z1712Pu;^ik0y&A&73WN_ig;1_-+}OkTP?ZYwhtc+BEpVjRZ(VdD-4ytXyy!VGHL2(aLk0VC&MQEcG>KQp;Z;04)iU&^jPl(sI-3l)Vu5Uvh-T#zXAD8a-zn^STG&3;T~d~^D=zrP z?&0z@bv6HAu-l}pe!A||TsHL2kJ~eU^ZN%Ehd*y>zNUN9LfsFH|Fv{r1OER5=0EV6 zj6QAQtgoM~Mn5_@Nc-uNa>)z;12hYTB;#IV@q!?k2fSuM>)W^Sg#|djvAr~D$_&|p zsRNg@RcrP%K5>tDo6ThX4s`G--&md{=+(=nQmaWJl6<(lzmP4h7jZ!vTlo7oIRgW- zAV_@c^V|RVuUG((5MQvmo&C*}i{bTT+s&lcD;%gK--T`>Q=)5Jm9wq4LZHGhOwGqR zMZ8S*7v;TRz znLQG)832V_VZQ@f?MN$$*fEXl`bjOa%1=T11rUiuq0H#2W753^8DctAO{yUEpYZxJ zvQweV>XM2@zljS!*z>^vO6{i+)}C=e;h{9b=2ki#Pl^6vVF`UXfnVisE7Ek&AG@Me zIm4UPXa_@#_-eNf@)OK}1Z4<4Mr_Pa=q7Yvm15d5vBCl*7{T}s3C6to`DMxyvA?Sa z_qF2f{E0e2RIBj?CrXSz%XX< zP{5c!cp&9FlP8JpSL*EmXhMQX+A{&8R!)T(T>Z&b2+dv+zGDGc&z^|15UQ^g+EG}* zH4ZLjY0z@e%43#(?r4EA*TLX145?O3_xUTyl_m;VjU=K=+>FH~e@4~AGpZ_C^un75 zN>VYXswi=3DC3bcu)z(!hgqC*N>j#H{M4q$f`zP)Tnp|h#RD)MEi^62S&QUNRe3jT4O1w)hkx_C z*OGr7lyZYHFtVvd@l>S<^;95{w9cmz@CeThTNIy{+_!taFea z%dbBr8~**^NnD;G+AfvVh1Faw-#+b@T<)l&CNzH4{LNTVI_{Dhw7l)0--FT6$|Xp> z-ZXtpJ(6nxuuA&C+h8b87wWZFfVph>ui?j8U0r?c(W+47^98*FS!Y2S&3=PXckW`nqgHeX-QA7p}ca8w;LgKgl*+3P8yEZQK;Kw z!A+*879UIX-QbBQKWQ0H}pWS- zxto@f%5Q2k*rVwX>LLTzRi(1;aNr<`_cx@7rQ4zov}{~e^%-P;bHArS`{jOO-xiaR z)D$?Sk{Ki2 zbN?>azMoN8)Zqz&kXJz=Zc8rU@4kzZJ}dUUogEqs_&wo&4BaPD4aV!hneJ&s6%-t* za2al29+M^Bq$fsw_T^Naf3Bq=31qpCj*f!tj=iqN#zxRj(ET5?jzmiBD%dIc8LX~M zQS#H--95ivaw(-NQbFKJ8HJ>czdYy+L zsF)Qwv;tjgEn$-0q(56E)g7VU%@!H~P6svMB!fNd=u{COt8b zzGc--O<-hoo>s zs5XjVDt`)moWqhQ_}~NhD(;oroH4pK&sJ5ELZ17tV;S{83%#0RHJi2O!sTqG2n>+X ztnvF>t5G|#eIKH60q09&xT?$V!W?T>qF#@NClo}ou z1`xR}_T)6D;4i4hoZ_W)?p9yTTOt{sQ!BXIt2fQ+sIBlk)qv66@Ya;HOd8vP=3G)d5gIdH$*(K^8%q9W5Ud=@Hed_J7 zQ8HFFmzL2-zy|dB;y$7IY(@Y$qv#iCnLInYvXwVXSc(q8g;3uS6+PxZBd zN5VZA{2CE!!^0#oJ*s{7wv&_aFj1fz1E7A8@pclvo(&JOFFUOBoRQ(nUT64@{-ddk zKz{&mI7bWk-FipF`Lk3dXavE%$}88UqP~a&a%gG>E6+1+uadf85xVaZA@89xl6vqMI3l~aox@2Y2?pF z;stF~mf=O$T2b53!`dFu|y-tny(I|@Z{Dfh3RL@r; zXQR8GIWKk)r<>UVXI!(OPVajB^TLOl<})BQ&&!LJDTGkS{Yq~O|4KDYwwnvo8<)Bi(Kw1IWIa&24Hkn#=d6K&7>~P0B}Ogp;8~kZs%OZ z6WeLMb0=NB0ne36K5Ddns`Y1a95Hrjw-RfGyh$n=UKab;bin*PK^$UlxVYSAgmvf) z3lyRmoqegFK^Fzt4~HCdp=f!bPTEYaa%g!#yAvw)u@>`o6mn0IlqSQHt>@d;z2*jj zA1(3=RLaLTz1iAN+IIrDwcyR!u^FCAo(;$GE%IFC%ExU?@-@JN{g7_$#jg^5{t&Hs zF!f1UHbl3T30*-K#c>0p}=N2;*80od`_N(5%1p7h}1nPxs#n9P=Cnq*y#z7xyzP= ze9Pw)`ZZH;lmcLpPxnUoRIHp)iwEEmVhQC&rl=UW(sh5n+buGnARd{O!t*e@#&&aS z4!=~;EhlijW7S6GLCawmY1aGNE%A2t4fE=r@*!)O@GK-m?_x?$&NX&;a1i{Ll1HPy z6>Q|K%F( z+3{T~XldBaYklG>ALP?HRbl~=ZQh0rJ)iEjm9y89wOuFW`=?=oVE3iK3Wm3IPLIYm znE7h+GJ3S?uT~3{9>i?&8HN%BN3@2FV;;T4xMu3;<*Sszoh#$ z(JY9ZhTX_0P8u`dSheBt-|y+{_I8gTUdX4T-vFHo+uQ!5{B(6<_PfrBxf$<{ zd?aRd^50tqF+_YDV@41BN17XRM|A+5d6I;Xk5teDT-7wnH)b5R{4HXXX?Z*khIRrn zpYxDM?_Jn^DHoU2_db+G_v}7Xh5F6Km;;`LIZwt(6C^>4KGz=JpfCr}sy$f@BR&1} z(G3W)Psi54sQ(XF%}bWArlW8PEPhuyVd|BF%m9ftnHBKgLLxPcm*?NjvTEz#|BDN^ zr^k0|5E)Ms@??mdT=trnA>^H*J=R+uMlIOhXi$App^{A-f@Uuh|1Y zPBA|wsfb?tRKVfAN>nO?f+$&wHuAuFPB1XifwAbd=V|}Dl*;s0ATMIWq?x?N0H8@oj!4lg#3>Q~Guj7IVtBbC%wTt2&ic`GbL|(pe)}~h zfR(S#NaR{Fmu=G^6!RIF^PUJE*^^~ut)=RmdaY*g0j{FwZEQZ9iF?#PFmdF} zCq=D{$U0x<(UO634Yw40Rlx$@Be;U}@dwCBX~3~d!?>o-h-L(lI2lbZUIDDWdrKo* zHtRu7r|(gSX{Xoj@hw3IC;XHG`KRU_V|z?7X>EqX($#NM+->)l8B|@LdD`|yvr=w+ zU9@;dJ`>glw&}N14&AA*!2@fNu0)(;T+;kep12Ntdm6Sw_=q0*x!(R~AdF&jPvZ5UF?5sn!$`J=G*}G?+d)1@)eTOOP$sFJ7z53F>Q{Vd9vJ#Yb=eKOi@a}DYnZEiESq`ftwA!eMZ*gOdAR-AH_)M`CL>eJ- zj~vCC{jvN8O;rUkVf%)`WDpumXnCQzw=Pde2S(&(qW}0j1LP}$m zK{L29v;9y}s|>eus@0Kth6m)2fgYAAuPAa6SGT14Hl2F5CJ9e)SZ%VVytRGfj**1I zDNmQgtV1~UDhU;ah@C)vO6}$q-hrtM6{^Mqj|nI5OBGoxmY0p;X{1KY-EJ0^#1L({O~CYb%LaKkFzVOA8cmm-e3-PDjjP`PVPk(axU1P}p@ zhK-_fi&W%N0H=&PfnAeRo5dUHi-ip1n$ije+LZ?-8Vd0Y=s7nfmEG^{sU%B`U`%54iKYhMqR65%Bm59rss13g;Mm7 zm+&`R7P#OX&PRdT5T;&&FElmbkSyn#wF;-_GufO1_cJ`+f4miV3z4lLaT&=5Ln0B~ z(~`G@(qQl!9tm0C8UAqsFubSJ`O5^^I*nW#M$p@g5qf61S9KaM3ng4eJk(u^0>Dt* z(Cy`MetnxOlU*Ma19Y=W?7sN)-adKXQn;0Vra~2BA1J>s^_^KwX`RE52{JZnFkj|~ zS7TN^u4hY7&R}wk@n;`c)Z4}$RXCOFU0TYZSNabv$7G9&Ie-tB8|sZypk^agWJ-q8 zEW3>+)+ySyT>}M>M6OVd09w{>=%U!0$ojWww{*{32?PUHoTa=i4F^pIgb?KsjM~i- zw>k8aX+Me%m)z~8OlS-Soew=Snql;_H&>SG-ClQReu5j`f5)`7n$3vDH0@zZuG+2S z>meVQp3!j=PTXp7r&?7gU-G?ws4jQc?@q8a@~{AQ#Td@TMNI}Jo_bT&-jt28ePp-3 zsgL?(>Aae6q`_@9UvJz%T7N&jFmKqr0XdLwZQg&#qG1`sueM+&m`$f3h-d8RJT0yLwD`jlB+>Q`Q8I1@_yKIwMSOZ zhC&-C$N@BsRt%|4PS(8zJ$^ZI)Ci&|ov(&ia@C*bg#bRBZ-7*37O}-!i$}04q1MHT z)zkxy2hl_}pYKAYC^mTgQKoMHn~KKuksTrmp@4JKH=&E*Mlx6&0)ljzC(0V{+~F+6 zKtU83`)QbK%Bm{kX6Qu7(e-D7(23ov4-n})9rikynUglFUT*BC+k2rNl8TMrZ#F^|xzpZ=MQq9NDm8%W zbUf@Xwm*y~_TLjzf2;GZVQ-Bkbq8ug^5WtmC;~cJKN%F93QE6z{p#rGSTMQb&#ZD4 zz}(#_p;-+Idbo(9?7L<2cq4MZ_{7ww=$93?yX$D-d=);EcY2gtQ+#TjA=N?%OhU96 zga$UFaV+Y>SOPP+7-x@o)L-Vn1j$hkSnc?K47*{mX;mm`gzyLu2 zI9K_ek8M0@w?S8%^Ff`NB0J)q_eGb^DGm8!C!aPEnz0@#c@4WY?7D9}QNXg}>e9^E zPX)c?W(hwhG<1iy$LkQD>v}l2ZDKh}?+;*_Hkr$DDqbrspfcfnTCM)X!g7h_yf`A{ zvu}_kOE`36w|*Byk#B=)D}|7IcD?L>pIOSIPiq^2;#<jEnh1TERm_x1iz+Z7Wx;?TD4Ae;fJ7stHi`UnIMt7hHq7ubJZjRo^V zK)P$cBzE+z+%-kehM#-3+6Y^I0PwH!!o+;?X>tB61OGROx5vuf=&+my=!&@K*+AJl z%MJ~gy{PspgS+#+)W`N)BeZ`^==ufx;Ds$_+i{763c1>f&c%JkUpbfVaChX_J8_yV z@dU~Kk$_%S;4^BExE<>J(fW#f!J16N3+>scvmov z?|=H$Z0bE0S5{vm?KX*iyzYhXEt{BA@{;(m6KLIQ;&ICj3FKwqX5@&;r}nW}d8S4fz`% z`&;%HVt;h)Sr{{)WW4yoGe&Y6z%NTo(V^y842$wRPcp96BE?$>1{dm;MwcHh!bW#Yf;uVPb_U;=)g zwTde;BLHu@mUlk=h!2L_lr^E0TZBkGngzb{|Ge)$cg%X>y9nK2LT1x(mFYi&K`yN` zeU9xk0TkbM#v5#S=k7c+?M*nwu~oy9#$+U_c(g=S7`L!`QKmftJ>>cEjtjTiTl&*MLsWx2)Wl&c4S(H>QJwUvZ)+-=S(U;`hCIV^L1f1& zw`It>;cZ^(c`@;K49jg}EQ8$yjt#S)fDzmW08_b^f!w!6dQ(??>7Ky|D-f#ji7hMB zNy(_U&((e!$}oQMClKxEjQmAU1RVooAwGA7=gFsz#H~2Zp(^~erPha@hZ7GWHc#lj zsxZ=YzIazILxQ8ZcT2x6gSHSD19(_*}3_UfMU_OME&u~>qIou+preusA55Iev60H?@{}l_sv#zDs z>Nt0hUvAz0CXzfgCg4ags6uz2?PlM92&w@H-6=kYXMP)4t>m69B%o6u&GxWPOD0GS~_WxfPxxg2F_kF6YNMSh2qCnBU)Q!h*!S0j-SJ!m1NpLV_5mGzdkp0T z5O;TsXpy4gB$bgy8~72uW8) zssOs@eaxuU?Mb3yK=$RtOy(l7r;|jVSctxc|Btp0`E5MS&oezt04CjOUR{r3Mvaz( z4tD7Il6hz8a&PA~cpqU(ztj9&d}P;;D%GMF1`R1oIy#fg$NNKpth%$62kfj}MGr+l zI!~uP*<;3&_%mW@HL{h~GC9|B7vCcHvm?!fl&R`8Yr0Iht)bO#_2sXafCYR>;`Z4l z@3W@;PcY=M!$;z2E?M8Mj~CmkNZreKCvs&pTK5t4x+p!DazPg87v{S7I?!#w2SEzV zult@-OWQdahfjE`CG5>Tg2u**``)JAaJgclHSi#QfB%SgD|Ky#Sn#&CMwDwL+8;L9A$+?4?BD(L{Xu zCC`slCIC=2QEW4qLOPkXKaULCysb=MgdW{AL_dNYOz;ZTVOEmR_fX@>#DoL-D}MW? z1jtPEOqSDW*Z>^@O&p$X1UO90JNmymg1 zuV(8}3XLZ?nkr{$tX$Q;4axSn-NDzDnhuQa?9mq*QyK93`3_;~+=tFL1d?S+>Uue^ z)5>hRvZN)QdFsQebvXs%gEvwj!u+x|dUo1Z_+O>HWmH|s8ZEkkAOQj-1P`7-aCdiy z;O?^V9o!`WLV~+Xkl^l4f(LiE;BFh&w@7!N?mq9lJMI{df14#$^;NA^bIxy8EmZw2 zZmpDvN@pnzqpP2UP)}(YxhkJGUp-|I_=Kv5k%&Cs}l-HD&0IM(3Z-CZV#n7C2) zlKnMBdg`=|Ex!wlprujK{!q3*E@)t1w0gy*`R1IViPG*^1;kaAMTu^2^2$V&0Kf5M;`{@XOxe zLepE8nZs4l#Za=sA~+m;rL%^rfL0ZTf$|mNs`O?5D};A&`sMqS(9qK(^Mp#)rOC1% zV9z@)md2~_+PmBO)NVwllivg-lOr3d9rQ!L>6 zg)qK$Sco3FFgqGjHT%@;El91nM}XiwQF|Hm)9eO9mw<=}L(xYJc}Vh=nEVhEaWpIS?8z-mQQdUl%eR` z#_Os#p3}Z5TdkGY1Wlr;iQGgvr5Hh8Cv!yj&S^nukcFMLA zY`ZzKE{zQ0FmJWp;?GU~q_shD6Tb&ol0MJ9*x00t1=Zy(qCvNRjLn6!2h1vg4_25! zNeR^r&shoS0*I{d@@~iWr%`fqqhS-^s!1ZRma4mnXxIP2_N9xs&@?{0yT1)(;MXq^KisA_>9Exz zmq~VwOM~f%lGWW0pfur9<}YJ|tcKZfS(Fn@_Kv(pZGtx_-pd6#ddKEVVR2- z%-QS7%_NNE8uz)bxaz^5TR|3@?Zquq0(aGOYicWW>kW5-=?uF`IP5xC;yK|-2X7o$ z!qePmPFKXQ&dmd~YE=pezsN^7)ybNE|=l(iF`hvBi7U zoyt42{(}G!5$sV~^Q<*aZMOVdhgu>Uf6M_YlBgygt z;Zf35S#)d++A9rjq9V(15(1*(&3>5}R!UYfO72&&x>5a%AD#cPlkA$qz*XE)?YkYd z9pFYTME6w92!DSzg)T^7fu@yf2tqy~SDVUos_o69TxI+R} zz9kC%H?p83l+ZTC+SW+cs9VbXv%XG=@8E~C))pe{xzD3pC<52&XX}&2XUQzFKNL0* z9p)(O3*2APMpGOq=yu)+yspZ2P8g}jSJQuTe1CMX(CBiwcn8>CflX2FT$a3mg{DhV zQKyf)X!zVs`P@P7pAn);%)ffLE`7KT2UhO;9OL+8jF=xRHhWwgEGP)voerk*s4s6o zK2JVe-Y%8i-TLST-$Sy{w0G*RXrwBye2O_9w$u9`^1aHe?l>NXqGLA&9{O6Zn2B;I z#4!9blON9KAP+}@0-o2~xjtv=K4O^eD`uDXH-|pZ1Xv_?+K>AD}0glC2K{dbi-^+=o6)1~#gX zBnVe)Aw$4T<{+(i0}yX$u>S1|rNGq}aS;g-jH!UvQa-n<#6CB3OZPy5KuGpk+vI(< zmp8mx?-^R*l3F!%=dN5fj(2O->yrtPZX3Zs8#W*ITVLI_KhS-+D?Sjo8|Z4i3xB{| zNEgTGsJHUDIM8+JzWzy>K40&!3B<_fdMw?(e!lg#ueFIM%j21EP*|OY2f-_~`>XVa z`X2@cxEsJ(3;XZxudr!nA6j34Zoi37>}RXDB-6k&`7cTNiW_bM{8L!3>fsl#lLyc0 zV_SgO@$_8^+#bOV#$DvseJ(t?sK9A;*pvr-MXB-^IC-jq*eXlC*{dQ>NbD<)`wT&;k4p1*(yb3G)zGV=f*Jgi8m>(svf{t?ZuEY6)u>Ip& zf~oawo5Z>mTvMWw*fHwt`9$aLoaXbVH+zRE6DVhQy!62sOPCY>Pxok(=-FP_76&T?K;`s40Ve|Cx zs?fqE?{RH8(uzpH{O?6q5s&M&f#ZMt!dUyg)!E12?F5!!J@zJW*fu#C$KFyTC^ueu zD{4oCEWp(sCFo#hpC6{<=?T>XIloy+DPEpi6+{z?Nt&i(w`6bp=7`^Ud~L%=MJ-N= zTIApfS-cc*X^;xb$iC$I9YX^Rm`5q!EYlKNkE^38eYU1XE7BuEdx20S)DKsEe=ncm zQ6St?xQjO4&6Qodk3yA})>wEPi7F%cE*?gVMRkr}qbJ(b`#Y1%u5;nVn{k{y+Ol~B zHEh4t7X#t(viIiU3Ku~VEPewXu)RMgz~PxgaW87i8(qb|1={6lMJ$E0;8^&| z-}S&OpL43(A=h)f@fJJKsXEUf?c~Kp>39KRWB~!I#Vdv!xNvDoh8Z)QIQHp+p!ponPX* z?)}a66&uKWP5Hz77TXk#qxngSQwPwnWPCVtxv0hF{+#WZ$y(kxGcKD`d^dA-8567F zgdLiqI#PWle!dFpaM`}T{v4%JJeOSei;*nv`iNcI#2~Y<3NJygvIxo^&st-1to5Q2 zT7(~3gTX7+go5N3o&svi+6d%*jdvGkP^-;@Y}GygB{!Q*G-uEgoWoFgM;odpx1*qu zogrF_BD#QD{noT1Fs|!L&^}egtz-Y0Kyt|QgdIF|CbQ3P1YubOiXsBTC^b+>5@#Cv zz!P)naw!oJPzvdTg@_LmsB2@O1o8Y}N)U<}z7bvs%&?xZi(O)vd)@xs&VuRUmFA#! z_cRJ8RVRPqd{dh!6_mDK|Ccs5P&==SGhk_N=w6q1W=E}~S)9;dZqpeBsR-BbAxrLEM8pXOg>%MljY9*LOcc=2Xh+6`)ll)8 z13jOwsUF;l8(0BBV!#dF=3;)(z%*-Jdi9jp)(3hnZJj>hS8Z3{p1p7;E9dH=P6eOk zG+DV@C2tyL^AdQZG4l6*Lo8`p4dYcx6Hl*;@2N+-zfY(N2p4oHRa}&e^{rPyEX>=hmLw-@ z-@Kmra@=+`+{BLM4Pg;{&>-UvHx%w1X*QDNdT)13HdoI)L)D4<;#Ivc8?MJm zr=|esS8w=i7WmvHHg@Pwb6TFtgV_o&O0@xPIn=(Qp9UE#Sf0s*HffF6+P>sUl1w=Q zt#tSnK9DU{az+=Ers0Bp0~b`Lr5&tvW1D?yM|IlE9h~yR^ch3ox?AAk@)fF|{`tl% zc_B(Y9@S09(R&s6{4nL`oe{-qV_j-F-$ktU64mpS5hJ8TH+6SAmydlt%0k%w?x*%w zm4x+aOCY_`ef&l9YIN(c{2-fR1(|PFhh65ZCr&M@o5sKL7}g*{#;o{(e9S@NHKUk_ zpplZ?)%7+6U)R@m)|xJj`#z10_XyLf@s$gvquVgB@(NJbjoqEqKhyU1t1lIx}~(mZNRI z61dB4bV#6nZHAvvKY1z_9NK8gYT@eCw5htgX@EmUq@l1u4*L^7y6niz&a7*KN1(2#2wjdWks4z2U(3}rEYX3 z&hW67m_=i?FD6;MA9I54rk0zbReODQ*C#C;x=k>>EfMo5d=N8W+ewnrO4-o7*|?fL z6k{5ahpsa-NWJ;$YM+@nng|9<@G|>7_CxNi<16e--BuIf!MzyRzHfsOs<1k?gG_8W zVd61SPmcEbeUOUvNiu|j(NJy=8YZ#>W=zlckxN2UE@osUe1~kcI@p|X;t%swXZam# zmMi3}44pOO9!mHv_yemk?4i=D2P@9J=Q*RS(hH&1E;JZ4TTOTb$&Oct#@;pw@7&O` zxwDOSKsnWd9x^-bk)UR0&kb!~slHREJ4{o(W-&j~Z$!UQ!Vj^OX+uX#LqT! zrbOniIKO=8A>3o!(OceZ3Ca!cz1k|^vps|gE+jMWtg5IGn?MT*>s6Ekb7Plx&f}xq zdPP}x*5*Tav$qzuRQF~P23}F9yd!?QCS%sZ!mvk$k-I%z6HU#Y`DI+q%PA>Xzd~1z zVBjiRr*PtlBV}w*hG~lS`w&-?m`_H?3MngNBd}zBRL@vnyo+}?+fjqYB&~-Ue#?e& z)RNU_C4~<@MFmBtS)7H|AdZ`kl{Plkc}f(Io@XS+qnPM0WVa|#aFoiDxaMgKyw}Ll zKn7k ztV*6?GCH8q6`?;1E}xuzkIpKT60{SvTb_h@<@ILQc-6>g#cps3XJA8#KGy=B*8S5< zuYe-+4UD1=a^_#D`mn!*GJj3hxub3f zw%W>51?xS*Rm?j-7f=@JX3DETRZwAnW#0|SmV?;DJ@N0ySU zF;RU4sisgPMD=BwPu$pLzRsACeTe3|anKIf z?!65OrJ-oYsOY|V9vn(wxY1>w8RvySsx5$Qg*tiAqIy3isNnHKsLQb8+YUx*-i=uRx{t!-Uv1|IqARm$ z{J&KeZ}l0BTJ09AHN)4=&BP2_v&*^f*Gc4Tj;MLA#00)n3zI|K@4*j;%l$E}dWIUW zkmn<8?V%z0iPQw;=jW00Win`-j0=v}ypBXM)42U(vqTl@v7(#f^J|7V|JzNobY}!} z&4Bvi1g^$HAH%1durwDFTJmv;uP0&zg~k~Xa>*O7Hf8jr6E7}xe~~2X^p0zi(YV{(A)X3R*useTS!2wo#X$3Z2j zkNFikKN;uXpUAxw$I$@!oZ~jR$4{dm1^`=oyjIHByE8gr2L#3zdf*FdezkIx;jBfr zkh0SOU=fw(NdNPii+mdFitGOLKojtM#-Lh->3fLYDsT-&=!jz~n*iFCM^?E`8Q9RK zNRuTu&~~HzwG=rqUVw!5@fT%aF@(1|V-G$$3$K-T-2qXugA<>haDo)(PV~*KLOYC? zJykmawy^fRq*LGb2lX-CQ@iaj_dEcuun)y?L@}CIAfyvZY=WIka_?>wGO1Jq}wwasT?WwE*uSI5t_gPS}gBK2D`dscQ_p_YFNfU$sE+MyXGK#tz zD*gzYCqKp=+T3^-1ZWD-jo&FoR`@QDmbUV01UftWCxR?N48l{%RlWV_m}uCkN|Miw zj;r2Ozm`((FQQ;8ZFS|Jm91{;+Fs)S(!%L#p``c@P#-*Q&M1bx8*Mb2`@Wf&3<}V+ z2%>C@07G}cKy(LTKS6y$f|6Au+wVo}P-N!y8y26mcT)(^?1>RbWCYxM2-BtWJcmUt z8K|H6ZE%FIrkM&P>(U&Z->%&qp%d(&f&MiIjElVy^;GqPJ4A+PBna5Yy5PXb9Hsu9 z1YZMV@w~)Zueu=)`QZ>*^ zZP%N)q#xZAx;k7J2gUOyG-MnPisa#}OPeli1VF@;I6f#3eff+hzF(`H`qN)mMs8y@ zAnC`=5StUu)JU9d1y=K>?L9r)yj6hPVQcp=ocqQJG0O!@}S(9yvDfk4M@J9R5vr0Y#uF8K)291fx$kikC!~_k$~}Q*G$ujv!C$ zU`j3;ue3Ha?OWSw9mV5ZZVq<$tw57R@VxYW-qIgCJUrFL+7MLX9&b9|Fw?XpXU>|T zJ}=aCu=q?A&D0_zz+iW`x$f{uS^W3+NbWm_Ck^B8V|2tfUQmiE21+wuH-xR`rwN|FDYg!= z<22pl3IBIodvPEbLtz}Aw^@f9vjC;OkmdLZMn2zTe2G(&OU*LpB7>45cs|H1XmI3T z_Zl4)SkH&46ePZ_rUb&VLhb++2IwmNW&L731W%aAs2*Z_m(wBAb0%AX1u*!1ELbK1 zaYDAwZeJ(U8(d+(x;m-RU)H@Z0%b_c1pyIv^{G2oKB@?$TJW;1*{y06cQzJKT+!xc z(RR5xwAxgkb=du;qTJT2g#3NbipTfAJ9|IBlB0(U{NhW)lY#TyE5jZ0HKh-de=uG( zq-!^jFjp#4M-t-=?o`*gGP$oylh`*j4;&Foq2L{)kKW_nRJ6HSdqD*(;<$I?N@-J1UUcX4v z%8MwCZz>qE<cq9vM}5z z>h^JojnjIB#nDMGf!;oTUhnpF_+n9Eb#4K<1DPAr)kOh_015g8Iaai`-Nq1Vxh&Q# zq1I>BtrR$bd(HpPB>RoP_o3gp2nkYAoX1 zd6aE@FGreWM0N2>Ku zb0U8|$A_Oa3$!&9e|Z6}dK=xZ-M-#w&bH%tEWS;1R~W^=2!UKc*9YV6dl#z|7B*c1 zf(LVD@D5w>(&RL^POjRipktIX(V7CRe8(?iGgTRAO<2?d{|x*6kyNGIac!4tT6$U< z5n&E3N!{r#1Nvk}Dj!aex%99FBVXCMH?^>zgWKPaF)C=yKTUmKJZ}o5=a0tsG&Lx~ zmkYrS2C94fR=iMcn)7asas&sFK~Bq3%^VD!am8rY$b1bNY~s5DdqsJfBLw9K=UN6FirC)xpK3gRa-7w872CPo zt0dj>^u7q44p7qecvM>un9#?s8??WL6(N(BmXNCv=aRvZc8)o?u<=J3K7G2w%-Am* zyAxDX$j|`OJ#XXcf^cnF$uO;HQ2*?08fK9w+=>B8y`%D;tv`702^n<&$7bIUQA)Y6 zSTImg?)eTxCou!)9-1qtP0CZiUc~~hpZ4%%X{wK7ob4oEkW-&ykigZ}3iFx^WS-q8 zk*JYb1wFT^b!WC$vBV2WYUndg)SAXUpc?Pu64fPL8??+;8WC)-@$s@rm(<;0C6o~j zzg};i`W3>c9NLh%kNHx!7R3TA?OSNRsA8<%zZ-GkhT{N3{$I?{4y*vWSbENX#s_CV}u zMfe)zo6OngyUw|GaUT4#(|8rr9)S`xGWj{<_g3KH91>m`uf^{U+JN$uX8&*HX$Q9} zS*76WyHAXCecH@W0momj`4WmO5|t|ReZ}vq6??g_C7ZgzY;@L+g*SH_l9%sqk~8p> zX&Ve{fZgk9E}!fDa5%K#b&=(&LekX6lA1c0376TWFNEW2?MtGLG06b5or78H^acrF zN+9n{`V^Se6tKgIrHDFX$!25S8J41}O}JnC=}pX|vP1NZ4Lum2MYVutwxnf?JFI?4 zoX0-Zr@}wp(Yu`>Pz{MI$FtFj5D3u?`0RB!uvJX3NHAUQ&)W5m;C`9Ev1D#`kc_)Ui?Tki zI5!fvvr8W=f`Sg_yJ(SaGsM2~3WMjH#F6zZ)L+=I2qj)Xd%3_k2W*6nHGgLpg=KYR z&dJeCv@*zt4HwBXZTPiU^}T-Qn8doL;{=g_zIm+A)*^#Cac*YAM~*v7iG%sSgpqks zXUa@P%1yJiAEJln#YXW9;0@88c57lHPD4`AJUNg}+0c$e9>GQ6dJNoNC~Q$g?h*O^?3k(R_Y6a1%14?$do;N{`ghO!uzL|Hk3#!cbgI#RYRh%ln&>c zHOOPtlKjT2K|dM&_l~_gj8+G4dI$T@XxIKJEZkWoEO<-)4_!gxd=+^oS^u!Eb#~kO z?wpqL)cfr>^Bi|4vAIC31(KoWFIp5xq}uWqpI-f;EUb=WwY}hRcth7=ET3Gq~Pt$w#)WcRi+pLg-A z&#Jo0kSLM6Q0LhQOq*9k_OnFmdw2q#oOC zoIW72h}++N6?@loTgdz7%(cz{^z$1kWUZzqWxu7OIl^|^dsB0B?nM0J*$C@U(gNwl zXaDi;{CQ=X0299Cz7a2D+JW7@w1Ms{Zud85C5HarAZGJGzh)-xOkmll>pO0mRj?dH z6h@n0sLxi2;SX)tvs#4Y>=s?w^(<|cGE55drUe+_=6^#4Zl1jFdHqi&uLF4zx48Dw zpL}Pb20&K$aD^UM@SmP~{YwV0TAfxuQcBFBS>q}dJrJVyG;;Z|4h{5sYD{f)eq}ek znELJ|T6qt10>?jO4)aqE3S=>kd0Kt7aNOuRb7Ca@ z4JqRCqR~-t5W@hzpcnwa?9@J73);+9^F4%PgyjL!J^txttHRrOuznmuuvb-+h+YS8 z7A+SRDU1W^3G7DoX5uy`wbpp*!ORd@$BKoFrBqZ&f~K6Mp3kwTXcplg^UKHq+|uFA z2Bd4CR71nMC9b7+NzRd_W2Vma+ zM@qFLaDM|!;Sk>~d&;$gnM6+)TkJC>PWehyz)==?nobk$lENdr>KRyK3<7@)<9q8^ z75D`)^P{UN1WWyQ@)TB0rvk&lH!_FQxqVUw)fl=0j5Z9pVz%J07}QH#+k1^ zNO9$+e#8F!I6JjONML97Y|l{IB|Uln ze!0;u3Jt-bX{2XzVHds1VZW;P=XY=E(MPb53ZTTKVT`jHbp`ln%JZ{LDC%&J?H;AH zrwrI^*Y?3{|2BwMW|51Bss>tFkr;3GA5vMV_-n8~W$ZY-b~2IbhVe13pV6(^8Qd}P z<+iX~FiIO;;37pd@&7p`t0;0(C{ZBf)&~M;L9JCduAzMbZ!=5bFTrURtD`&rQ4)PcHIcQ(wLj9r6>Yt-45C2JO zC%@BP-GaqfO3j210cIL7Vz?41+Evl=9%D#r3Uj0b>5uyM53pqTg~rY_=tiT@TuSfu z)5ZQdkGdQPVFh5KQRClOEJELxXm3;|zWxs8aFRdeqdiJaSrepRiy7_yHli4JnbSG% zz-xX|_rhMAPz!)p^)4LT_N_zZU%RhA<{Rf23Ac|2M5n&*!kAs-?hc*O>=T~cHK*Vx zq8nPQ9JzYM3Y9J!#0`#RQY6B@jOg1LCR?i@!n%C^$FLYWqn{-R20j^FJyA z*`}a`(|+?`qA9N^P*KwI^=#atTRcXZ;YH(=@y%a!EZqwNQ^Bc7G_A)cah`YTCdW&R*&lTq-UKxO_p1=y zGeHhx8X`c?4v*f{UJYt{e(F^bdvlYJ2;EYBn^gQN8ErO_jR;ZRuYBZ#lbKBQD0HRu zpSr&NC=eQ+)1%@>*pVoWzTPMsh)S(}Iv%BG{=Fge$pNMHR zfKz`q{n*ECdNIX(BmhXrbz7bdF~A%JA?+qN4{1@r(`DL9(2N89@ew~~Q6)`3>%n_!NqyJ6Y9UPxu-71iG z2IR^lO4|L{^xd$XB3I%04XyKl}4T@+j;{NMxKU5Qg70}-EbQ32z2*hCY zor-@m%MtQlkCXVAlT($DpgCB1M=f<}9v8#KlQj$xd!+e9hgV+Ese=s&&k)PRwr9}XFt|hg`t40-(EW;zFYgf-AH9$r6G#D1E z0`B?WV(Ooo;0oM(sBp<$_+^3{anc8)ot1p8Uv$(veF<)GPN$3K2vdrOJB5BEw)x5W zny4kT-vC#Qu3Qr;DSG+g@4HY;`-Oz9o4dIUs zsHiN;-vAQ56Ko4uW(XEjKeWv5p*Uk}*un+n?of~Ta!w1=FT89d_=U&w;j{EFwj=(e z{5tLg@NEE@ zd?bg-!$UBFn*w2YT9nJG@gzXGUER?8b=w`3=axY?w<{h!zfD{un32gW&{r zC%$O?j=2h;j!Mo=_fH{8n84V=%qh{3fU`p*os8}&J(zR(&ywIxSehOYBkw;(qyD8gqtrGlp{QhQ_Z3x7!Hqg|*1 z1W%M{&91q#+0~FSi9QDnP*Wd$CL0_JX&!lrz((DV`0@YbGI4F(A%_5gcJ5l8cg$W= zX}{^7tHAk7qy2NL>sYlJfYgn*ISx-a9bpAwpzW8gTSE+!i`e7EYiVA5+ zDIIVz0h(@&{>(W7*QP9)V*J+`Z$5z{Zrk9*pdG7lSfY4Sfj2@ceEfwLhH!rY1@Bg_ z7yR@0W_9KN^y>`c4a7u$xQoe-1gt&O;ha{9;GcKMOQU$2i~EfXOE#QBJJL?$|26OT zZrSaZxgVCGtv~;$6h~123{nbjfAynz4}u{=Wi&eBNi{J`ecdE}`r^hSw6$A$tAck? z0WE%;jaGLqeeNbsYrIND7OtEH1geC&WI?3b#W|tH(~*@{g;{r#W&X@xwwn5~sx}N2 zjdP>{HZp;u+s$+_V=hqwRfs!^8ics64kMFx$3R?)v)<4+KNO{{*^N{h*qN8FvtHHc zR4@`rnb7W{ARZSv@jaL8Y5NzP2*Lo^|>=Udw*F` z{DwD(w{f8d$k!4o-8M|hip|5qRXbIOoxkRubuIR^X|QU#m2JCX{Z)%8 zOv=o&t{uYqSGkTvx&KbDP|E3+hc*4;K-%X$3L+`Zw5 zT=-%>{<9v>O>(5stMIb_B9SZdnsE*>Sf0->I$Z9*-5OGGh9u{qxLQCq0K~YRcp#M+ z2Qzy!YV04M@gE>{)<0gnk#Bf+SqJ2rdo|N0Sp^~+myW2e5Vfb8+$=Khr$8x2rW zFCt$$P$N+rb_2ePw0*MFeLI_dR-!@Zm&btd@e*miNkcA>+|A={6{FMrcqKxHV(qS+ za51d&8X;)qX#!Uu_GI9vP7N!YI>aQB$Bs9w+Yt3e@423?ONgLgyE_avbD)YJt3t%) z0}KN3H8K4ug1RH;q++=$J-G(`AULF~W*qV~?$b)N!29u?bNc%ef&*my&*I>h1z5#^ z>y7jT43RH4%Ie*cGs;5UvTYc#bez|dY?p^2d+j|uMNNwq>QTMp#p)3C;=?;XXX(7q zJ4+Et?Qx3_?QQV?OoMRR0WXUVJZkis{Wm|-cuPtU(t9AVav!gE-szJ(vCFze@_;5v zE}IPA&#M_WwDcvF+jRoM!MUq|QMuiwmnnF|9oycROR?lK|8DVClD!v5HQPQ); zgWf`_@lkSrGn(!kKWCHB?hj)I*0(DWLFiqJA}kUG7Y(+RXKQa^Nj;jQH@--tVGzL09>mR`JokO68!iT(4v~}3TI&P#Dzl8q_ zS>uiB|8H629hh>KxsIds*qUAIltNWCB8Iu2YCL; ze>PXYn&YTalAF9FIW@BWhC1G8K{-hMjyO-Qv1aq(dE4^|J0MMUe|zzC{ba9i)Sx^(u5}Z@ zBQA6H=aoq1uCq~EQQ0(RYSBqk`n<$9xX#J>FTr!?ai292$lkqV8j_7`Fe1!{K!d)q zY7k1l1#iPn2+fNxLjRUPB=o~Qn~H8N;RVYBY9aY}^R`c(kG#*(?3Gdqo*M8F4o~IS zD)OEmhseYMK$l}@gGWT`+IP+PQ__z>QMhF>N$R+2{-!?<1E1vKP*~s=8d!!CLElQG z;d=Ay{F}FSg(wEIEevzDNq zkc}Y{d-M-%Zv@ygs_7g#ED*VvQdL*P%QCQ3AEX@`Ctd57lj_4jIH6iwjJdpDEt&{ap~v{fRqG!7 z$+Triwv#h(1FA&(XC_^N97r<9I_xhEfYFs3Zsx70IL3fvnE|3;&A7%h-s{nFnEw(( z&_Vx1Lx6jEmeBss%*<@JD8$1r#epI+PiEMc>Scr}zJwJPaFLwIG0Y1*3$8bCz>@@I!F zi!JQ5+4dho5*!N4%zGGRe*jI2g}Paoof+8Z957nA@MR%J0i6&xurZeSacsl~FNu(g z$DW8vZL0d=W%%pSxU&F1*RG1RkZQ=Tw7Bth;^XgyN`jCH9#SAP0{ob4Nc)_okKm+Y z@yX%SRdalp`aaE&@rcR#kDOIiUNtAjk9NNyQ8|Y|0Cwi1ih{Oe%Xprnz!#NH7t&-u zN#N}_TmmIoJl%&kDn6*vHRHDwIO0?A!5rslyel{D?C~oLW2nf))Lm;SUGa{!DAM?5#sg#Q`=y zs&Kc}IqIwf+7;O+A9WO-T}SSs8qD3R7!~#pswM;HJ5mZ+fy-?HAqV`rgeZ+G=`;-qrkXt3cRrwhdwe506;IUwMl& zHx#If{%A$RD{s~@EzmG^{*ZjAomI<1v0%|xsF}6(=z44TRka~$Xy?A`p4>N!n67n+ zazP5C;Y>rU?9u*Rky6vw?%05MX`7Xd$clkKymLbl@bF_(Ml5~$u<`pAj9}L4xiG#n zG6v}}7{=-MObsGQ^G$>Ul~ug6j)Bp2shahMl>C`DSP$N4dh~G;K*d1+vVxwRBxl(2 zWAKg_6LV%z)h>ZRu=!G=LMpV%wEinuosnN?uzT24BdA#8B6%lZfp}`uaC*E6sI4@{ngH;L)D^|f)vH=ooBe&Q4V;j#FS z`VabF)<|lg2^eDI+SyGnGfXxjII&KslvSjKo68~wGzee)ZiD(@^%93ZHipjiSv=zI z@L+FL%CL8!%oAkTqS^EHP&Qwj3^>5#w}=&>`jx(|=c@-z3#JW6F4=2?Fd@C~O*Mo? z1dTQN7kl9Umy@qo_R%3Ir@+tSHte|Gid-ey&y4XQ^Rt_oxun<}qM}VCS+5y&7Z+`M zd(=A`>y-Qb=&fMCc#4MnIN)=pJs#dmx7Ceqpg%PkjYhEviqD@vu?#55M`<g7VB4hg6Eu{1~S3EAc`<=5rQQ+#TxQ zCq4V^&Uk<0$qVB93O@RAlMbA~b#er}L8iJM(BETc?OL5pZG(V6DKUA`5@Can{||2x BL013( literal 0 HcmV?d00001 diff --git a/doc/assistant/makerepo.png b/doc/assistant/makerepo.png new file mode 100644 index 0000000000000000000000000000000000000000..e8f1b26216e590daa30575385662b2df77605281 GIT binary patch literal 32061 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

j6oV4hQR>=;#r(bHeYQD|b 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 E06v>uYXATM literal 0 HcmV?d00001 diff --git a/doc/assistant/menu.png b/doc/assistant/menu.png new file mode 100644 index 0000000000000000000000000000000000000000..29a7c9d0f842a7b6fc58d571b5caefd8f8ac8c64 GIT binary patch literal 22921 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 XHrVUcpj$9#@ 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 O00006d literal 0 HcmV?d00001 diff --git a/doc/assistant/preferences.png b/doc/assistant/preferences.png new file mode 100644 index 0000000000000000000000000000000000000000..74fb814183c30006f71383fd22a3661a3989babd GIT binary patch literal 22815 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%0AjMh>dopY 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 + +## version 4.20130709 + +This release is mostly bug fixes. + +One of the bugs involved setting up rsync remotes on servers other than +rsync.net. The wrong `.ssh/authorized_keys` line was deployed to the +remote server. If you set up a rsync remote with a past release, and it does +not work, you will need to manually edit the `.ssh/authorized_keys` file, +and remove the `command=` forced command. + +## version 4.20130621, 4.20130627 + +These releases mostly consist of bug fixes. + +## version 4.20130601 + +This is a bugfix release, featuring significant XMPP improvements and +more robustness thanks to automated fuzz testing. Recommended upgrade. + +This version changes its XMPP protocol, so it will fail to sync with older +git-annex versions over XMPP. + +## version 4.20130521 + +This is a bugfix release. Recommended upgrade. + +## version 4.20130516 + +This version contains numerous bug fixes, and improvements. + +This is the first release with a fully usable Android app. No command-line +typing needed to set up syncing to your Android phone or tablet! +A few of the more advanced features may not work (or not work reliably) +on Android. The Android app is still beta quality. + +This is also the first release with a Windows port! The Windows port +is in an alpha quality state, and is missing many features. +It does not yet include the assistant. + +## version 4.20130501 + +This version contains numerous bug fixes, and improvements. + +## version 4.20130417 + +This version contains numerous bug fixes, and improvements. + +One bug that was fixed can affect users of gnome-keyring who +have set up remote repositories on ssh servers using the webapp. +The gnome-keyring may load the restricted key that is set up +for that, and make it be used for regular logins to the server; +with the result that you'll get an error message about "git-annex-shell" +when sshing to the server. + +If you experience this problem you can fix it by +moving `.ssh/key.git-annex*` to `.ssh/git-annex/` (creating +that directory first), and edit `.ssh/config` to reflect the new +location of the key. You will also need to restart gnome-keyring. + +Another change relates to files in `archive/` directories. Client repositories +now sync these files between themselves like any other files, until +the files reach an archive repository. Only then are they removed from +the client repositories. So you need to ensure you have at least one +archive repository if you want to use the `archive/` directory feature. + +## version 4.20130323, 4.20130405 + +These versions continue fixing bugs and adding features. + +## 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. +So, it is a recommended upgrade from the previous release, which introduced +that bug. + +In this release, the assistant is fully working on Android, although +it must be set up using the command line. + +Repositories can now be placed on filesystems that lack support for symbolic +links; FAT support is complete. + +## version 3.20130216 + +This adds a port to Android. Only usable at the command line so far; +beta qualitty. + +Also a bugfix release, and improves support for FAT. + +The following are known limitations of this release of the git-annex +assistant: + +* No Android app yet. +* On BSD operating systems (but not on OS X), the assistant uses kqueue to + watch files. Kqueue has to open every directory it watches, so too many + directories will run it out of the max number of open files (typically + 1024), and fail. See [[this_bug|bugs/Issue_on_OSX_with_some_system_limits]] + for a workaround. +* Also on systems with kqueue, modifications to existing files in direct + mode will not be noticed. + +## version 3.20130107, 3.20130114, 3.20130124, 3.20130207 + +These are bugfix releases. + +## version 3.20130102 + +This release makes several significant improvements to the git-annex +assistant, which is still in beta. + +The main improvement is direct mode. This allows you to directly edit files +in the repository, and the assistant will automatically commit and sync +your changes. Direct mode is the default for new repositories created +by the assistant. To convert your existing repository to use direct mode, +manually run `git annex direct` inside the repository. + +## version 3.20121211 + +This release of the git-annex assistant (which is still in beta) +consists of mostly bugfixes, user interface improvements, and improvements +to existing features. + +In general, anything you can configure with the assistant's web app +will work. Some examples of use cases supported by this release include: + +* Using Box.com's 5 gigabytes of free storage space as a cloud transfer + point between between repositories that cannot directly contact + one-another. (Many other cloud providers are also supported, from Rsync.net + to Amazon S3, to your own ssh server.) +* Archiving or backing up files to Amazon Glacier. See [[archival_walkthrough]]. +* [[Sharing repositories with friends|share_with_a_friend_walkthrough]] + contacted through a Jabber server (such as Google Talk). +* [[Pairing|local_pairing_walkthrough]] two computers that are on the same local + network (or VPN) and automatically keeping the files in the annex in + sync as changes are made to them. +* Cloning your repository to removable drives, USB keys, etc. The assistant + will notice when the drive is mounted and keep it in sync. + Such a drive can be stored as an offline backup, or transported between + computers to keep them in sync. + +The following are known limitations of this release of the git-annex +assistant: + +* The Max OSX standalone app may not work on all versions of Max OSX. + Please test! +* On Mac OSX and BSD operating systems, the assistant uses kqueue to watch + files. Kqueue has to open every directory it watches, so too many + directories will run it out of the max number of open files (typically + 1024), and fail. See [[bugs/Issue_on_OSX_with_some_system_limits]] + for a workaround. + +## version 3.20121126 + +This adds several features to the git-annex assistant, which is still in beta. + +In general, anything you can configure with the assistant's web app +will work. Some examples of use cases supported by this release include: + +* Using Box.com's 5 gigabytes of free storage space as a cloud transfer + point between between repositories that cannot directly contact + one-another. (Many other cloud providers are also supported, from Rsync.net + to Amazon S3, to your own ssh server.) +* Archiving or backing up files to Amazon Glacier. +* [[Sharing repositories with friends|share_with_a_friend_walkthrough]] + contacted through a Jabber server (such as Google Talk). +* [[Pairing|local_pairing_walkthrough]] two computers that are on the same local + network (or VPN) and automatically keeping the files in the annex in + sync as changes are made to them. +* Cloning your repository to removable drives, USB keys, etc. The assistant + will notice when the drive is mounted and keep it in sync. + Such a drive can be stored as an offline backup, or transported between + computers to keep them in sync. + +The following are known limitations of this release of the git-annex +assistant: + +* The Max OSX standalone app does not work on all versions of Max OSX. +* On Mac OSX and BSD operating systems, the assistant uses kqueue to watch + files. Kqueue has to open every directory it watches, so too many + directories will run it out of the max number of open files (typically + 1024), and fail. See [[bugs/Issue_on_OSX_with_some_system_limits]] + for a workaround. +* Retrieval of files from Amazon Glacier is not fully automated; the + assistant does not automatically retry in the 4 to 5 hours period + when Glacier makes the files available. + +## version 3.20121112 + +This is a major upgrade of the git-annex assistant, which is still in beta. + +In general, anything you can configure with the assistant's web app +will work. Some examples of use cases supported by this release include: + +* [[Sharing repositories with friends|share_with_a_friend_walkthrough]] + contacted through a Jabber server (such as Google Talk). +* Setting up cloud repositories, that are used as backups, archives, + or transfer points between repositories that cannot directly contact + one-another. +* [[Pairing|local_pairing_walkthrough]] two computers that are on the same local + network (or VPN) and automatically keeping the files in the annex in + sync as changes are made to them. +* Cloning your repository to removable drives, USB keys, etc. The assistant + will notice when the drive is mounted and keep it in sync. + Such a drive can be stored as an offline backup, or transported between + computers to keep them in sync. + +The following upgrade notes apply if you're upgrading from a previous version: + +* For best results, edit the configuration of repositories you set + up with older versions, and place them in a repository group. + This lets the assistant know how you want to use the repository; for backup, + archival, as a transfer point for clients, etc. Go to Configuration -> + Manage Repositories, and click in the "configure" link to edit a repository's + configuration. +* If you set up a cloud repository with an older version, and have multiple + clients using it, you are recommended to configure an Jabber account, + so that clients can use it to communicate when sending data to the + cloud repository. Configure Jabber by opening the webapp, and going to + Configuration -> Configure jabber account +* When setting up local pairing, the assistant did not limit the paired + computer to accessing a single git repository. This new version does, + by setting GIT_ANNEX_SHELL_DIRECTORY in `~/.ssh/authorized_keys`. + +The following are known limitations of this release of the git-annex +assistant: + +* On Mac OSX and BSD operating systems, the assistant uses kqueue to watch + files. Kqueue has to open every directory it watches, so too many + directories will run it out of the max number of open files (typically + 1024), and fail. See [[bugs/Issue_on_OSX_with_some_system_limits]] + for a workaround. + +## version 3.20121009 + +This is a maintenance release of the git-annex assistant, which is still in +beta. + +In general, anything you can configure with the assistant's web app +will work. Some examples of use cases supported by this release include: + +* [[Pairing|local_pairing_walkthrough]] two computers that are on the same local + network (or VPN) and automatically keeping the files in the annex in + sync as changes are made to them. +* Cloning your repository to removable drives, USB keys, etc. The assistant + will notice when the drive is mounted and keep it in sync. + Such a drive can be stored as an offline backup, or transported between + computers to keep them in sync. +* Cloning your repository to a remote server, running ssh, and uploading + changes made to your files to the server. There is special support + for using the rsync.net cloud provider this way, or any shell account + on a typical unix server, such as a Linode VPS can be used. + +The following are known limitations of this release of the git-annex +assistant: + +* On Mac OSX and BSD operating systems, the assistant uses kqueue to watch + files. Kqueue has to open every directory it watches, so too many + directories will run it out of the max number of open files (typically + 1024), and fail. See [[bugs/Issue_on_OSX_with_some_system_limits]] + for a workaround. +* In order to ensure that all multiple repositories are kept in sync, + each computer with a repository must be running the git-annex assistant. +* The assistant does not yet always manage to keep repositories in sync + when some are hidden from others behind firewalls. + +## version 3.20120924 + +This is the first beta release of the git-annex assistant. + +In general, anything you can configure with the assistant's web app +will work. Some examples of use cases supported by this release include: + +* [[Pairing|local_pairing_walkthrough]] two computers that are on the same local + network (or VPN) and automatically keeping the files in the annex in + sync as changes are made to them. +* Cloning your repository to removable drives, USB keys, etc. The assistant + will notice when the drive is mounted and keep it in sync. + Such a drive can be stored as an offline backup, or transported between + computers to keep them in sync. +* Cloning your repository to a remote server, running ssh, and uploading + changes made to your files to the server. There is special support + for using the rsync.net cloud provider this way, or any shell account + on a typical unix server, such as a Linode VPS can be used. + +The following are known limitations of this release of the git-annex +assistant: + +* On Mac OSX and BSD operating systems, the assistant uses kqueue to watch + files. Kqueue has to open every directory it watches, so too many + directories will run it out of the max number of open files (typically + 1024), and fail. See [[bugs/Issue_on_OSX_with_some_system_limits]] + for a workaround. +* In order to ensure that all multiple repositories are kept in sync, + each computer with a repository must be running the git-annex assistant. +* The assistant does not yet always manage to keep repositories in sync + when some are hidden from others behind firewalls. +* If a file is checked into git as a normal file and gets modified + (or merged, etc), it will be converted into an annexed file. So you + should not mix use of the assistant with normal git files in the same + repository yet. +* If you `git annex unlock` a file, it will immediately be re-locked. + See [[bugs/watcher_commits_unlocked_files]]. diff --git a/doc/assistant/release_notes/comment_1_bd8f376c9d0c1d5ed07fb013907a60ee._comment b/doc/assistant/release_notes/comment_1_bd8f376c9d0c1d5ed07fb013907a60ee._comment new file mode 100644 index 0000000000..04cdf4039d --- /dev/null +++ b/doc/assistant/release_notes/comment_1_bd8f376c9d0c1d5ed07fb013907a60ee._comment @@ -0,0 +1,14 @@ +[[!comment format=mdwn + username="http://wiggy.net/" + nickname="Wichert" + subject="OSX 10.8's gatekeeper does not like git-annex" + date="2012-11-13T14:47:52Z" + content=""" +Trying to run this release on OSX results in an error message from Gatekeeper: + +> \"git-annex\" can't be opened because it is from an unidentified developer. +> +> Your security preferences allow installation of only apps from the Mac App Store and identified developers. + +It would be nice if the binary could be signed to make Gatekeeper happy. Until then a note in the installation instructions might be useful. +"""]] diff --git a/doc/assistant/release_notes/comment_2_75e0774ad042717fbd059a8a9ec2db1e._comment b/doc/assistant/release_notes/comment_2_75e0774ad042717fbd059a8a9ec2db1e._comment new file mode 100644 index 0000000000..9033b1af6d --- /dev/null +++ b/doc/assistant/release_notes/comment_2_75e0774ad042717fbd059a8a9ec2db1e._comment @@ -0,0 +1,12 @@ +[[!comment format=mdwn + username="http://wiggy.net/" + nickname="Wichert" + subject="git-annex not runnable on OSX 10.8" + date="2012-11-13T14:49:35Z" + content=""" +After telling Gatekeeper that I really want to run git-annex it still fails: + +[fog;~]-131> open Applications/git-annex.app +LSOpenURLsWithRole() failed with error -10810 for the file /Users/wichert/Applications/git-annex.app. + +"""]] diff --git a/doc/assistant/release_notes/comment_3_b3bfd8e547e20c51f7c32c6c9424e936._comment b/doc/assistant/release_notes/comment_3_b3bfd8e547e20c51f7c32c6c9424e936._comment new file mode 100644 index 0000000000..73377c714f --- /dev/null +++ b/doc/assistant/release_notes/comment_3_b3bfd8e547e20c51f7c32c6c9424e936._comment @@ -0,0 +1,10 @@ +[[!comment format=mdwn + username="http://joeyh.name/" + ip="4.252.11.120" + subject="comment 3" + date="2012-11-13T17:11:51Z" + content=""" +This has been previously reported: [[bugs/OSX_git-annex.app_error:__LSOpenURLsWithRole()]] + +No clue what that error is supposed to mean. +"""]] diff --git a/doc/assistant/release_notes/comment_4_c6caa2b521b456bb4ce594d64919cffe._comment b/doc/assistant/release_notes/comment_4_c6caa2b521b456bb4ce594d64919cffe._comment new file mode 100644 index 0000000000..1ebaf504f8 --- /dev/null +++ b/doc/assistant/release_notes/comment_4_c6caa2b521b456bb4ce594d64919cffe._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawkSq2FDpK2n66QRUxtqqdbyDuwgbQmUWus" + nickname="Jimmy" + subject="comment 4" + date="2012-11-16T13:02:40Z" + content=""" +sadly i only have a 10.7 machine to create the builds, so I have no experience with 10.8. I haven't had a 10.6 machine in a while to create the builds. Anyone else want to work together in setting up another 10.6 or 10.8 builder for others? +"""]] diff --git a/doc/assistant/remote_sharing_walkthrough.mdwn b/doc/assistant/remote_sharing_walkthrough.mdwn new file mode 100644 index 0000000000..ec8f39d531 --- /dev/null +++ b/doc/assistant/remote_sharing_walkthrough.mdwn @@ -0,0 +1,12 @@ +So you have two computers that are not in the same place, and you want them +to share the same synchronised folder, communicating directly with each other. + +[[!inline feeds=no template=bare pages=videos/git-annex_assistant_remote_sharing]] + +You can add even more computers using the same method shown here. + +---- + +If you have a laptop that is sometimes near another computer, you can +speed up file transfers when it is by also connecting it using the +[[local_pairing_walkthrough]]. diff --git a/doc/assistant/remote_sharing_walkthrough/comment_1_e0187b0a926904b363065ab0f850f0b2._comment b/doc/assistant/remote_sharing_walkthrough/comment_1_e0187b0a926904b363065ab0f850f0b2._comment new file mode 100644 index 0000000000..cac5295a78 --- /dev/null +++ b/doc/assistant/remote_sharing_walkthrough/comment_1_e0187b0a926904b363065ab0f850f0b2._comment @@ -0,0 +1,10 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawmG4rlD9k1ezNkYZ8jDbITrycUmHV-P8Qs" + nickname="Jeroen" + subject="Synced vs. unsynced" + date="2013-07-29T18:07:45Z" + content=""" +I've noticed that it is also possible to add an existing annex folder on a remote server without using syncing. Are there any dangers in doing this? + +Could you explain what syncing does and when it is needed? Thanks. +"""]] diff --git a/doc/assistant/remote_sharing_walkthrough/comment_2_dabcbc9aaf0bdb82716f5a5d55807a21._comment b/doc/assistant/remote_sharing_walkthrough/comment_2_dabcbc9aaf0bdb82716f5a5d55807a21._comment new file mode 100644 index 0000000000..b99d9e22b7 --- /dev/null +++ b/doc/assistant/remote_sharing_walkthrough/comment_2_dabcbc9aaf0bdb82716f5a5d55807a21._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="http://joeyh.name/" + ip="4.154.0.21" + subject="comment 2" + date="2013-07-30T18:08:38Z" + content=""" +I'm afraid I don't quite understand the question. +"""]] diff --git a/doc/assistant/repogroups.png b/doc/assistant/repogroups.png new file mode 100644 index 0000000000000000000000000000000000000000..06300ce66a1a6dabdbbbb1bf91ca1abfd303189c GIT binary patch literal 15636 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 M5|bCL5jF_=Kh-V{uK)l5 literal 0 HcmV?d00001 diff --git a/doc/assistant/repositories.png b/doc/assistant/repositories.png new file mode 100644 index 0000000000000000000000000000000000000000..2853683dca0090d5daa9df3e2a1faf4e59e24ffc GIT binary patch literal 63405 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


ELHY@-RG_z>hP(q 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 z zka6?sZb}Lmqndj`!^A$4!D-ZL=1Wt}Ur!;omc+*J_2R2soGJT7D85bUyZJ?<#+s86 zVyS6PsP^$2=Tntj58HS_er#r!EK42KLu8tAThJ0H-LSr@I<7`W+;Mj&oE`0P*C+9+ zIs?2$z0ld->OY$*&8?D5t7l5(($Nn9_lJZI;Vk*q_&jxKiG9&~^gW!&OB1E@Y$XYt zae}sXI@&txPzgFmtaQ7&RwRO7Wiu|PL_2>XXRMo&ZV_Bf$ z<|k*)vgzDUcEpiHPTtIGXbljCZN~9C?+?b(M@nWjDeib}SM+ehqZ|&+5S9@1bpkMm z$fXp9I$(z5K(le?i4lhI=DcMLO&^6m5Yv8A8RGluvg2p#!ogvRQXZjcSAkArgZu{~Dh+? zlhVPP4C*Fr5o}e6=xkpvePf|F<6ue7h4CjL4i`t%jIZ;w>YHpbrUqNR2dBk`ru!(L%iXu{EARK?vk}zNt~vr=sf9iUYudJ%e+reI)TDoH&m;m&o;Zxk+En3dO%cnU_mm79!&mZ{Kceh`F` znnsNK4Q9?~ET2RsjUAMa*ht5*7`GK&X0lB%U`_?Mu0v7r#9{+fnYcUOjJo~T-VkYVKhebpT3o`r| z)iD4za|PT#+E-2C$#^j%tQ*XUu(XWO5!5AxB0*Pe>MnC zyDap&9nn7|47-kU(Yk@=Xkp%2H@Qh!v%{VNvuxd8Y?*p;k6;y#VXoI>X6@J6;PbYV z>;#U4;MJx-2ss0445Bs9>n$IwPwe!sF@Iz~xrGB7ZLY#OnrYI(%b=N<>&^X4W6XcJ z3ZVV(2VX1_uSh3jm~MgOx(Bs8k8;`eZVg}FCwRd$@nW}7+Dd5SqLWvAt~c4-*0KTI zti}zX^$g@cz?U1C&1yyv_XkquZq81bb8zowW`O>{SXfj=OMe*s(R=#2<-nn#B!_}8 zhOch@pT^y1*h?;&IdR7!v@K`My_X1q=Ix4TdAu&(MVgsqk!h%OJ;W);L+)Fk@1$#{ z3D4?f=Jk;fI2FW@Oy7zX!kt_heb}UKKXK34QNL*b7fk;A%*~X{l0V9 z5JRj^&j*ZsbxjWQu9cI@rNk9D(>hGrO}ns#zi_N~+u4~DA+ok1PbMmjbiQbqnGqP2 z8!XpC>C^>_hDd1b_$r^+5a=Q2<=dsDV?q=Ddgc|bg>BvydX-V@U->unIGZ3Zf?h~! zI%f*JLztyW5r5J7A=Y;`_GtrwA{_?b4EZLK{fmn44Cy7o
+ +
+What excites me about GIT ANNEX is how it fundamentally tracks the +backup and availability of any data you own, and allows you to share +data with a large or small audience, ensuring that the data survives. +
+-- Jason Scott + +Seen on IRC: +
+oh my god, git-annex is amazing
+this is the revolution in fucking with gigantic piles of files that I've been waiting for
+
+ +And then my own story: I have a ton of drives. I have a lot of servers. I +live in a cabin on **dialup** and often have 1 hour on broadband in a week +to get everything I need. Without git-annex, managing all this would not be +possible. It works perfectly for me, not a surprise since I wrote it, but +still, it's a different level of "perfect" than anything I could put +together before. --[[Joey]] + +See also: [[design/assistant/blog/day_288__success_stories]] diff --git a/doc/tips.mdwn b/doc/tips.mdwn new file mode 100644 index 0000000000..eda84c8672 --- /dev/null +++ b/doc/tips.mdwn @@ -0,0 +1,4 @@ +This page is a place to document tips and techniques for using git-annex. + +[[!inline pages="tips/* and !tips/*/*" archive="yes" +rootpage="tips" postformtext="Add a new tip about:" show=0]] diff --git a/doc/tips/Building_git-annex_on_Debian_OR___37____164____35____34____164____37____38____34____35___Haskell__33__.mdwn b/doc/tips/Building_git-annex_on_Debian_OR___37____164____35____34____164____37____38____34____35___Haskell__33__.mdwn new file mode 100644 index 0000000000..c32eb966f7 --- /dev/null +++ b/doc/tips/Building_git-annex_on_Debian_OR___37____164____35____34____164____37____38____34____35___Haskell__33__.mdwn @@ -0,0 +1,111 @@ +I've been wrestling with git-annex to try to make it build on Debian, or more specifically, wrestling with Haskell dependencies. + +After a fair amount of futzing around, and pestering a bunch of people in the process (thanks for the help! :) ) I finally managed to make it build. + +I figured I would post the steps here, since it's not completely trivial, and I expect that a few others might be interested in building newer versions as well. + +There appears to currently be two methods: + +* Debian packages on Wheezy plus Sid + * Starting out on Wheezy, and then picking the rest from Sid (it seems at least libghc-safesemaphore-dev from Sid is critical for newer git-annex) + * WebDAV suport will not be available with this method +* Cabal packages + + +#Debian packages on Wheezy plus Sid + +##Start off with a clean wheezy chroot + + sudo debootstrap wheezy debian-wheezy + sudo chroot debian-wheezy + +##Install some build tools + + apt-get update + apt-get install devscripts git + +##Get git-annex (either by cloning or simply moving the source into the chroot) + + mkdir /src + cd /src + git clone git://git-annex.branchable.com/source.git git-annex + cd git-annex + +##Remove WebDAV dependency which can't be satisfied anywhere + + sed '/libghc-dav-dev/d' -i debian/control + +##Create dummy build-depends package and install all available Wheezy dependencies using it + + mk-build-deps + dpkg -i git-annex-build-deps*.deb + apt-get install -f + +(this will remove the build-depends package) + +##Add Sid sources and install all available Sid dependencies + + echo "deb http://http.debian.net/debian sid main" >>/etc/apt/sources.list + apt-get update + dpkg -i git-annex-build-deps*.deb + apt-get install -f + +(the build-depends package should now be fully installed) + +##Disable the 'make test' that fails due to missing hothasktags + + echo >>debian/rules + echo "override_dh_auto_test:" >>debian/rules + +##Build! + + debuild -us -uc -Igit + + +#Cabal packages + +##Start off with a clean Sid(/Wheezy) chroot + + sudo debootstrap sid debian-sid + sudo chroot debian-sid + +##Install a smaller set of tools and build-depends from Debian (cabal needs these to compile the Haskell stuff) + + apt-get update + apt-get install ghc cabal-install devscripts libz-dev pkg-config c2hs libgsasl7-dev libxml2-dev libgnutls-dev c2hs git debhelper ikiwiki perlmagick uuid rsync openssh-client fakeroot + +##Get git-annex (either by cloning or simply moving the source into the chroot) + + mkdir /src + cd /src + git clone git://git-annex.branchable.com/source.git git-annex + cd git-annex + +##Install the Haskell build-dependencies from cabal + + cabal update + cabal install --only-dependencies + +##Optional step which doesn't work (might in the future) +If we want to run the 'make test' after build we need hothasktags, which is only available via cabal + + apt-get install happy + cabal install hothasktags + export PATH=$PATH:~/.cabal/bin + +But this currently fails silently inside make test->fast->tags, and if you dig a bit (manually edit the makefile to be more verbose) you see + + hothasktags: ./Command/AddUnused.hs: hGetContents: invalid argument (invalid byte sequence) + +##Disable the 'make test' that fails + + echo >>debian/rules + echo "override_dh_auto_test:" >>debian/rules + +##Remove all Debian package haskell depends (taken care of by cabal instead) + + sed '/\tlibghc/d' -i debian/control + +## Build! + + debuild -us -uc -Igit diff --git a/doc/tips/Building_git-annex_on_Debian_OR___37____164____35____34____164____37____38____34____35___Haskell__33__/comment_1_835a3608df3e9d044cabe822d0f3e7e4._comment b/doc/tips/Building_git-annex_on_Debian_OR___37____164____35____34____164____37____38____34____35___Haskell__33__/comment_1_835a3608df3e9d044cabe822d0f3e7e4._comment new file mode 100644 index 0000000000..55cf0b97b9 --- /dev/null +++ b/doc/tips/Building_git-annex_on_Debian_OR___37____164____35____34____164____37____38____34____35___Haskell__33__/comment_1_835a3608df3e9d044cabe822d0f3e7e4._comment @@ -0,0 +1,27 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawkCw26IdxXXPBoLcZsQFslM67OJSJynb1w" + nickname="Alexander" + subject="can't install git-annex on OS X Mountain Lion without disabling WebDAV support" + date="2013-04-29T17:57:03Z" + content=""" +possibly related to this Debian issue: + +trying to install git-annex with cabal on OS X 10.8.3, the build fails with + + + Loading package DAV-0.4 ... linking ... ghc: + lookupSymbol failed in relocateSection (relocate external) + ~/.cabal/lib/DAV-0.4/ghc-7.4.2/HSDAV-0.4.o: unknown symbol `_DAVzm0zi4_PathszuDAV_version1_closure' + ghc: unable to load package `DAV-0.4' + Failed to install git-annex-4.20130417 + cabal: Error: some packages failed to install: + git-annex-4.20130417 failed during the building phase. The exception was: + ExitFailure 1 + + +This was after following all of the instructions for the Homebrew install at [http://git-annex.branchable.com/install/OSX/](http://git-annex.branchable.com/install/OSX/) +I was able to work around this issue by installing with the WebDAV flag disabled (ie, added the option --flags=\"-WebDAV\" to last command in the OS X install instructions): + + cabal install git-annex --bindir=$HOME/bin --flags=\"-WebDAV\" + +"""]] diff --git a/doc/tips/Building_git-annex_on_Debian_OR___37____164____35____34____164____37____38____34____35___Haskell__33__/comment_2_080b30cba72a718e73ea715e259e1cfb._comment b/doc/tips/Building_git-annex_on_Debian_OR___37____164____35____34____164____37____38____34____35___Haskell__33__/comment_2_080b30cba72a718e73ea715e259e1cfb._comment new file mode 100644 index 0000000000..7e8e295fdc --- /dev/null +++ b/doc/tips/Building_git-annex_on_Debian_OR___37____164____35____34____164____37____38____34____35___Haskell__33__/comment_2_080b30cba72a718e73ea715e259e1cfb._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="http://joeyh.name/" + nickname="joey" + subject="comment 2" + date="2013-04-30T21:51:50Z" + content=""" +@Alexander that DAV-0.4 problem is a bug in DAV, not git-annex. I've informed its author and it should be fixed soon, in a new version of DAV. +"""]] diff --git a/doc/tips/Decentralized_repository_behind_a_Firewall.mdwn b/doc/tips/Decentralized_repository_behind_a_Firewall.mdwn new file mode 100644 index 0000000000..6a9eb241b1 --- /dev/null +++ b/doc/tips/Decentralized_repository_behind_a_Firewall.mdwn @@ -0,0 +1,59 @@ +If you're anything like me¹, you have a copy of your annex on a computer running at home², set up so you can access it from anywhere like this: + + ssh myhome.no-ip.org + +This is totally great! Except, there is no way for your home computer to pull your changes, because there is no *on-the-go.no-ip.org*. You can get clunky and use a *bare git repository and git push*, but there is a better way. + +First, install *openssh-server* on your *on-the-go* computer + + sudo apt-get install openssh-server # Adjust to your flavor of unix + +Then, log into your *home* computer, with *port forwarding*: + + ssh me@myhome.no-ip.org -R 2201:localhost:22 + +Your *home* computer can now ssh into your *on-the-go* computer, as long as you keep the above shell running. + +You can now add your *on-the-go* computer as a remote on your *home* computer. Use the port forwarding shell you just connected with the command above, if you like. + + ssh-keygen -t rsa + ssh-copy-id "me@localhost -p 2201" + cd ~/annex + git remote add on-the-go ssh://me@localhost:2201/home/myuser/annex + +Now you can run normal annex operations, as long as the port forwarding shell is running³. + + git annex sync + git annex get on-the-go some/big/file + git annex status + +You can add more computers by repeating with a different port, e.g. 2202 or 2203 (or any other). + +If you're security paranoid (like me), read on. If you're not, that's it! Thanks for reading! + +--- +Paranoid Area + +Note you're granting passwordless access to your on-the-go computer to your home computer. I believe that's all right, as long as: + +* Your home computer is really in your home, and not at a friend's house or some datacenter +* Your home computer can be accessed only by ssh, and not HTTP or Samba or NTP or (shoot me now!) FTP +* Only you (and perhaps trustworthy family) have access to your home computer +* You have reasonably strong passwords or key-only logins on both your home and on-the-go computers. +* You regularly install security updates on both computers (sudo apt-get update && sudo apt-get upgrade) + +In any case, the setup is much, much, much more secure than Dropbox. With Dropbox, you have exactly the same setup, but: + +* Your data is stored in some datacenter. It's supposed to be encrypted. It might not be. +* Lot's of people have routine access to your files, and plausible reason to. Bored employees might regularly be doing some 'maintenance work' involving your pictures. +* The dropbox software can do anything it likes on your computer, and it's closed source so you don't know if it does. A disgruntled employee could put a trojan into it. +* Dropbox might have a backdoor for employee access to any file on your computer. This might be done with the best of intentions, but a mal-intentioned or careless employee might still erase things or send sensitive files from your computer by email. +* A truly huge amount of eyes connected to incredibly smart brains have looked at openssh and found it secure. Everybody trusts openssh. With dropbox, there is, well, dropbox. Whoever that is. + +----- + +¹ Me=Carlo, not Joey. I'm pretty sure doing what I wrote here is a good idea, but in case it turns out to be catastrophically dumb, it's my fault, not his. + +² My always-on computer at home is a raspberry pi with a 32GB USB stick. Best self-hosted dropbox you could imagine. + +³ You can just forward the port, but not open a shell, by adding the -N command. This could be useful for connecting on startup, e.g. in /etc/rc.local. I prefer to open the shell to forward the ports, maybe use it, and close it to stop it. diff --git a/doc/tips/Decentralized_repository_behind_a_Firewall/comment_1_78b9035234a690ca5a7c9f3cc78fa092._comment b/doc/tips/Decentralized_repository_behind_a_Firewall/comment_1_78b9035234a690ca5a7c9f3cc78fa092._comment new file mode 100644 index 0000000000..71a1db9c88 --- /dev/null +++ b/doc/tips/Decentralized_repository_behind_a_Firewall/comment_1_78b9035234a690ca5a7c9f3cc78fa092._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="http://joeyh.name/" + ip="4.154.6.49" + subject="comment 1" + date="2012-11-30T16:25:58Z" + content=""" +If you don't trust your home computer with shell access, you can lock it down in `.ssh/authorized_keys` to only be able to run git-annex-shell. See [[forum/Restricting_git-annex-shell_to_a_specific_repository]] +"""]] diff --git a/doc/tips/Delay_Assistant_Startup_on_Login.mdwn b/doc/tips/Delay_Assistant_Startup_on_Login.mdwn new file mode 100644 index 0000000000..74652308ae --- /dev/null +++ b/doc/tips/Delay_Assistant_Startup_on_Login.mdwn @@ -0,0 +1,13 @@ +# Problem +I noticed that after installing git-annex assistant, my start up times greatly increased because the assistant does a startup scan while everything else is loading. +# Solution (for people using Gnome) +The solution I came up with is to delay the assistant's startup, as well as setting its IO priority as idle. To do this in Gnome 3, run: + + gnome-session-properties +Find the "Git Annex Assistant" entry in the Startup Programs tab, then click edit. Change this: + + /usr/local/bin/git-annex assistant --autostart (your location of git-annex may be different) +to this: + + bash -c "sleep 30; ionice -c3 /usr/local/bin/git-annex assistant --autostart" (replace /usr/local/bin to wherever git-annex is installed) +The "sleep 30" command delays the startup of the assistant by 30 seconds, and "ionice -c3" sets git-annex's IO priority to "idle," the lowest level. diff --git a/doc/tips/Delay_Assistant_Startup_on_Login/comment_1_c63917150527efab4b1106183b3aa7ef._comment b/doc/tips/Delay_Assistant_Startup_on_Login/comment_1_c63917150527efab4b1106183b3aa7ef._comment new file mode 100644 index 0000000000..fe8cb80ba3 --- /dev/null +++ b/doc/tips/Delay_Assistant_Startup_on_Login/comment_1_c63917150527efab4b1106183b3aa7ef._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="https://launchpad.net/~alphapapa" + nickname="alphapapa" + subject="ionice not supported by deadline scheduler" + date="2013-06-28T17:43:47Z" + content=""" +Linux's deadline I/O scheduler does not support ionice. It is now the default on some distros, including Ubuntu. CFQ does support ionice. +"""]] diff --git a/doc/tips/Git_annex_and_Calibre.mdwn b/doc/tips/Git_annex_and_Calibre.mdwn new file mode 100644 index 0000000000..70663dce96 --- /dev/null +++ b/doc/tips/Git_annex_and_Calibre.mdwn @@ -0,0 +1,118 @@ +The problem +=========== + +[Calibre](http://calibre-ebook.com/) is a ebook manager that is +available in [debian](http://packages.debian.org/sid/calibre). I use +it to maintain my library, but also to dowload every day an epub +version of a French newspaper and then put it on my kobo. + +Configuring git annex for this +============================== + +I wanted to use git-annex, so + + $ git init + $ git annex init "some useful name" + +But I don't want every thing in annex, because Calibre use some text +file to save some metadata, so I used: + + $ git config annex.largefiles "include=* exclude=*.opf exclude=*.json" + +then lets add everything + + $ git annex add * + $ git add * + $ git commit -m "first commit" + +Calibre need read and write access on the its database, so let unlock it: + + $ git annex unlock metadata.db + +On my other computer I only need to do + + $ git clone $user@$host:Calibre\ library + $ cd Calibre\ library + $ git annex init "another useful name" + $ git annex get . + $ git annex unlock metadata.db + +The problem is that every time you will `git annex sync`, git annex +will lock again the metadata.db, so lets unlock it automatically. I +use git hooks, in `.git/hooks/post-commit` I have + + #!/bin/bash + + git annex edit metadata.db + +don't forget to make this file executable + + $ chmod a+x .git/hooks/post-commit + +Day to day operation +==================== + + $ git annex add . + +Will put new file into the annex + + $ git add . + +Will take care of the files that should no go into annex + + $ git annex sync + +Will make the repositories exchange informations about all this, and +make remote change local + + $ git annex get . + +Will make remote book locally available + +Merge conflict +-------------- +You should not run calibre on the two computer simultaneously, or +without syncing before it. If you do, you will have a conflict that +git-annex will automatically *solve* by rename both of the file. + +You can then either: + + - Choose one. If no books have been changed or added on one of the + computer, to use the other `metadata.db` will not make you loose + any information + - rebuild it. `calibredb restore_database` won't do it, but will tell + you how to do it. + +Checking the library +-------------------- +You can use `calibredb check_library` to check you library is +correct. If you use git for it, it will always tell you that it is not +correct: there is this author ".git" it doesn't know about. Just don't +care about it. + +Maybe this can be solved by using `vcsh` but apparently +`vcsh`+`git annex` it not well tested yet. + +Automatic stuff +--------------- +I use `mr` to automatically run all this, but some config could be +done (I believe) to have `git annex copy --auto` do what it should. + +There are also the git annex assistant for this kind of automatic +synchronizations of contents, but I don't know if my automatic +unlocking of one file will break this. + +It might be interesting to find someway to unlock and lock the library +only when running calibre, a simple script to launch calibre will do +that. Note that each time you will lock and unlock, you will have a +new commit in git. + +Another solution +=================== +You could also use direct mode in place of the auto unlock feature + + git annex indirect + +The remove the `post-commit` git hook (or do not add it). Its a +simpler solution, but remember that interaction between git annex direct +repositories and plain git are complex diff --git a/doc/tips/How_to_retroactively_annex_a_file_already_in_a_git_repo.mdwn b/doc/tips/How_to_retroactively_annex_a_file_already_in_a_git_repo.mdwn new file mode 100644 index 0000000000..97f5828d39 --- /dev/null +++ b/doc/tips/How_to_retroactively_annex_a_file_already_in_a_git_repo.mdwn @@ -0,0 +1,19 @@ +I worked out how to retroactively annex a large file that had been checked into a git repo some time ago. I thought this might be useful for others, so I am posting it here. + +Suppose you have a git repo where somebody had checked in a large file you would like to have annexed, but there are a bunch of commits after it and you don't want to loose history, but you also don't want everybody to have to retrieve the large file when they clone the repo. This will re-write history as if the file had been annexed when it was originally added. + +This command works for me, it relies on the current behavior of git which is to use a directory named .git-rewrite/t/ at the top of the git tree for the extracted tree. This will not be fast and it will rewrite history, so be sure that everybody who has a copy of your repo is OK with accepting the new history. If the behavior of git changes, you can specify the directory to use with the -d option. Currently, the t/ directory is created inside the directory you specify, so "-d ./.git-rewrite/" should be roughly equivalent to the default. + +Enough with the explanation, on to the command: +
+git filter-branch --tree-filter 'for FILE in file1 file2 file3;do if [ -f "$FILE" ] && [ ! -L "$FILE" ];then git rm --cached "$FILE";git annex add "$FILE";ln -sf `readlink "$FILE"|sed -e "s:^../../::"` "$FILE";fi;done' --tag-name-filter cat -- --all
+
+ +replace file1 file2 file3... with whatever paths you want retroactively annexed. If you wanted bigfile1.bin in the top dir and subdir1/bigfile2.bin to be retroactively annexed try: +
+git filter-branch --tree-filter 'for FILE in bigfile1.bin subdir1/bigfile2.bin;do if [ -f "$FILE" ] && [ ! -L "$FILE" ];then git rm --cached "$FILE";git annex add "$FILE";ln -sf `readlink "$FILE"|sed -e "s:^../../::"` "$FILE";fi;done' --tag-name-filter cat -- --all
+
+ +**If your repo has tags** then you should take a look at the git-filter-branch man page about the --tag-name-filter option and decide what you want to do. By default this will re-write the tags "nearly properly". + +You'll probably also want to look at the git-filter-branch man page's section titled "CHECKLIST FOR SHRINKING A REPOSITORY" if you want to free up the space in the existing repo that you just changed history on. diff --git a/doc/tips/How_to_retroactively_annex_a_file_already_in_a_git_repo/comment_1_7eaf73fb3355bd706ab18a43790b3c10._comment b/doc/tips/How_to_retroactively_annex_a_file_already_in_a_git_repo/comment_1_7eaf73fb3355bd706ab18a43790b3c10._comment new file mode 100644 index 0000000000..d4e34e8cda --- /dev/null +++ b/doc/tips/How_to_retroactively_annex_a_file_already_in_a_git_repo/comment_1_7eaf73fb3355bd706ab18a43790b3c10._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="http://edheil.wordpress.com/" + ip="173.162.44.162" + subject="comment 1" + date="2012-12-16T00:11:38Z" + content=""" +Man, I wish you'd written this a couple weeks ago. :) I was never able to figure that incantation out and ended up unannexing and re-annexing the whole thing to get rid of the file I inadvertently checked into git instead of the annex. +"""]] diff --git a/doc/tips/How_to_retroactively_annex_a_file_already_in_a_git_repo/comment_2_dac1a171204f30d7c906e878eb6bd461._comment b/doc/tips/How_to_retroactively_annex_a_file_already_in_a_git_repo/comment_2_dac1a171204f30d7c906e878eb6bd461._comment new file mode 100644 index 0000000000..a3ea62385e --- /dev/null +++ b/doc/tips/How_to_retroactively_annex_a_file_already_in_a_git_repo/comment_2_dac1a171204f30d7c906e878eb6bd461._comment @@ -0,0 +1,45 @@ +[[!comment format=mdwn + username="https://launchpad.net/~arand" + nickname="arand" + subject="comment 2" + date="2013-03-13T12:05:49Z" + content=""" +Based on the hints given here I've worked on a filter to both annex and add urls via filter-branch: + +[https://gitorious.org/arand-scripts/arand-scripts/blobs/master/annex-filter](https://gitorious.org/arand-scripts/arand-scripts/blobs/master/annex-filter) + +The script above is very specific but I think there are a few ideas that can be used in general, the general structure is + + #!/bin/bash + + # links that already exist + links=$(mktemp) + find . -type l >\"$links\" + + # remove from staging area first to not block and then annex + git rm --cached --ignore-unmatch -r bin* + git annex add -c annex.alwayscommit=false bin* + + # compare links before and after annexing, remove links that existed before + newlinks=$(mktemp -u) + mkfifo \"$newlinks\" + comm -13 <(sort \"$links\") <(find . -type l | sort) > \"$newlinks\" & + + # rewrite links + while IFS= read -r file + do + # link is created below .git-rewrite/t/ during filter-branch, strip two parents for correct target + ln -sf \"$(readlink \"$file\" | sed -e 's%^\.\./\.\./%%')\" \"$file\" + done < \"$newlinks\" + + git annex merge + +which would be run using + + git filter-branch --tree-filter path/annex-filter --tag-filter cat -- --all + +or similar. + +* I'm using `find` to make sure the only rewritten symlinks are for the newly annexed files, this way it is possible to annex an unknown set of filenames +* If doing several git annex commands using `-c annex.alwayscommit=false` and doing a `git annex merge` at the end instead might be faster. +"""]] diff --git a/doc/tips/How_to_retroactively_annex_a_file_already_in_a_git_repo/comment_3_b62ec0b848d2487d68d7032682622193._comment b/doc/tips/How_to_retroactively_annex_a_file_already_in_a_git_repo/comment_3_b62ec0b848d2487d68d7032682622193._comment new file mode 100644 index 0000000000..9b8aa58f8e --- /dev/null +++ b/doc/tips/How_to_retroactively_annex_a_file_already_in_a_git_repo/comment_3_b62ec0b848d2487d68d7032682622193._comment @@ -0,0 +1,36 @@ +[[!comment format=mdwn + username="arand" + ip="130.238.245.202" + subject="comment 3" + date="2013-03-18T14:39:52Z" + content=""" +One thing I noticed is that git-annex needs to checksum each file even if they were previously annexed (rather obviously since there is no general way to tell if the file is the same as the old one without checksumming), but in the specific case that we are replacing files that are already in git, we do actually have the sha1 checksum for each file in question, which could be used. + +So, trying to work with this, I wrote a filter script that starts out annexing stuff in the first commit, and continously writes out sha1<->filename<->git-annex-object triplets to a global file, when it then starts with the next commit, it compares the sha1s of the index with those of the global file, and any matches are manually symlinked directly to the corresponding git-annex-object without checksumming. + +I've done a few tests and this seems to be considerably faster than letting git-annex checksum everything. + +This is from a git-svn import of the (free software) Red Eclipse game project, there are approximately 3500 files (images, maps, models, etc.) being annexed in each commit (and around 5300 commits, hence why I really, really care about speed): + +10 commits: ~7min + +100 commits: ~38min + +For comparison, the old and new method (the difference should increase with the amount of commits): + +old, 20 commits ~32min + +new, 20 commits: ~11min + +The script itself is a bit of a monstrosity in bash(/grep/sed/awk/git), and the files that are annexed are hardcoded (removed in forming $oldindexfiles), but should be fairly easy to adapt: + +[https://gitorious.org/arand-scripts/arand-scripts/blobs/master/annex-ffilter](https://gitorious.org/arand-scripts/arand-scripts/blobs/master/annex-ffilter) + +The usage would be something like: + + rm /tmp/annex-ffilter.log; git filter-branch --tree-filter 'ANNEX_FFILTER_LOG=/tmp/annex-ffilter.log ~/utv/scripts/annex-ffilter' --tag-name-filter cat -- branchname + +I suggest you use it with at least two orders of magnitude more caution than normal filter-branch. + +Hope it might be useful for someone else wrestling with filter-branch and git-annex :) +"""]] diff --git a/doc/tips/How_to_retroactively_annex_a_file_already_in_a_git_repo/comment_4_2423904e41a86cd1c6bc155d7b733642._comment b/doc/tips/How_to_retroactively_annex_a_file_already_in_a_git_repo/comment_4_2423904e41a86cd1c6bc155d7b733642._comment new file mode 100644 index 0000000000..ab1d4e0064 --- /dev/null +++ b/doc/tips/How_to_retroactively_annex_a_file_already_in_a_git_repo/comment_4_2423904e41a86cd1c6bc155d7b733642._comment @@ -0,0 +1,9 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawknOATcOkmzX4jKuET5Z2RsaFUNnLKnQsU" + nickname="Stephen" + subject="comment 4" + date="2013-06-22T07:43:09Z" + content=""" +Thanks for the tip :) One question though: how do I push this new history out throughout my other Annexes? +All I managed to make it do was revert the rewrite so the raw file appeared again... +"""]] diff --git a/doc/tips/Internet_Archive_via_S3.mdwn b/doc/tips/Internet_Archive_via_S3.mdwn new file mode 100644 index 0000000000..d7893693f9 --- /dev/null +++ b/doc/tips/Internet_Archive_via_S3.mdwn @@ -0,0 +1,58 @@ +[The Internet Archive](http://www.archive.org/) allows members to upload +collections using an Amazon S3 +[compatible API](http://www.archive.org/help/abouts3.txt), and this can +be used with git-annex's [[special_remotes/S3]] support. + +So, you can locally archive things with git-annex, define remotes that +correspond to "items" at the Internet Archive, and use git-annex to upload +your files to there. Of course, your use of the Internet Archive must +comply with their [terms of service](http://www.archive.org/about/terms.php). + +A nice added feature is that whenever git-annex sends a file to the +Internet Archive, it records its url, the same as if you'd run `git annex +addurl`. So any users who can clone your repository can download the files +from archive.org, without needing any login or password info. This makes +the Internet Archive a nice way to publish the large files associated with +a public git repository. + +---- + +Sign up for an account, and get your access keys here: + + + # export AWS_ACCESS_KEY_ID=blahblah + # export AWS_SECRET_ACCESS_KEY=xxxxxxx + +Specify `host=s3.us.archive.org` when doing `initremote` to set up +a remote at the Archive. This will enable a special Internet Archive mode: +Encryption is not allowed; you are required to specify a bucket name +rather than having git-annex pick a random one; and you can optionally +specify `x-archive-meta*` headers to add metadata as explained in their +[documentation](http://www.archive.org/help/abouts3.txt). + +[[!template id=note text=""" +/!\ There seems to be a bug in either hS3 or the archive that breaks +authentication when the bucket name contains spaces or upper-case letters.. +use all lowercase and no spaces when making the bucket with `initremote`. +"""]] + + # git annex initremote archive-panama type=S3 \ + host=s3.us.archive.org bucket=panama-canal-lock-blueprints \ + x-archive-meta-mediatype=texts x-archive-meta-language=eng \ + x-archive-meta-title="original Panama Canal lock design blueprints" + initremote archive-panama (Internet Archive mode) ok + # git annex describe archive-panama "a man, a plan, a canal: panama" + describe archive-panama ok + +Then you can annex files and copy them to the remote as usual: + + # git annex add photo1.jpeg --backend=SHA1E + add photo1.jpeg (checksum...) ok + # git annex copy photo1.jpeg --fast --to archive-panama + copy (to archive-panama...) ok + +Note the use of the SHA1E [[backend|backends]]. It makes most sense +to use the WORM or SHA1E backend for files that will be stored in +the Internet Archive, since the key name will be exposed as the filename +there, and since the Archive does special processing of files based on +their extension. diff --git a/doc/tips/Using_Git-annex_as_a_web_browsing_assistant.mdwn b/doc/tips/Using_Git-annex_as_a_web_browsing_assistant.mdwn new file mode 100644 index 0000000000..4ee023de3e --- /dev/null +++ b/doc/tips/Using_Git-annex_as_a_web_browsing_assistant.mdwn @@ -0,0 +1,46 @@ +[[todo/wishlist: an "assistant" for web-browsing -- tracking the sources of the downloads]] suggests using git-annex as a tool to store downloads tied +to their URLs. This also enables people to have their files stored offline, +while being able to git annex drop them at any time and redownload them +with git annex get. Additionally, a clone of the repo can be used to +download whatever files are desired from online. + +This tip explains how to implement a similar system to the one described in +the linked wishlist with existing software and features of git-annex. + +The first step is to install the Firefox plugin +[FlashGot](http://flashgot.net/). We will use it to provide the Firefox +shortcuts to add things to our annex. + +We also need a normal download manager, if we want to get status updates as +the download is done. We'll need to configure git-annex to use it by +setting `annex.web-download-command` as Joey describes in his comment on +[[todo/wishlist: allow configuration of downloader for addurl]]. See the +manpage [[git-annex]] for more information on setting configuration. + +Once we have installed all that, we need a script that has an interface +which FlashGot can treat as a downloader, but which calls git-annex to do +the actual downloading. Such a script is available from +. Download it and store it +somewhere it can live, or cut and paste: + +[[!format sh """ +#!/bin/bash +# $1=folder to cd to (must be a git annex repo) +# $2=URL to download + +cd "$1" +git-annex addurl "$2" +"""]] + +Finally, we need to configure FlashGot to use the script as a downloader. +Go to Tools > Add-ons in Firefox. Click "Preferences" on FlashGot. Click +the Add button next to the list of download managers. Enter a name for the +git-annex downloader. Choose the script that was downloaded from the +"Locate executable file" dialog that appears. Now set the command line +arguments template to be "[FOLDER] [URL]" (you can find more substitution +expressions in the Placeholders dropdown above the Command line arguments +template field). You're done! + +Go ahead and test it by trying to download a file using FlashGot. It should +offer as one of its available download managers the new manager you created +just above. Select it and have fun! diff --git a/doc/tips/Using_Git-annex_as_a_web_browsing_assistant/comment_1_74167f9fff400f148916003468c77de4._comment b/doc/tips/Using_Git-annex_as_a_web_browsing_assistant/comment_1_74167f9fff400f148916003468c77de4._comment new file mode 100644 index 0000000000..a091b8e488 --- /dev/null +++ b/doc/tips/Using_Git-annex_as_a_web_browsing_assistant/comment_1_74167f9fff400f148916003468c77de4._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="http://joeyh.name/" + nickname="joey" + subject="comment 1" + date="2013-04-11T20:16:02Z" + content=""" +As of my last commit, you don't really need a separate download manager. The webapp will now display urls that `git annex addurl` is downloading in amoung the other transfers. +"""]] diff --git a/doc/tips/assume-unstaged.mdwn b/doc/tips/assume-unstaged.mdwn new file mode 100644 index 0000000000..536772c893 --- /dev/null +++ b/doc/tips/assume-unstaged.mdwn @@ -0,0 +1,31 @@ +[[!meta title="using assume-unstages to speed up git with large trees of annexed files"]] + +Git update-index's assume-unstaged feature can be used to speed +up `git status` and stuff by not statting the whole tree looking for changed +files. + +This feature works quite well with git-annex. Especially because git +annex's files are immutable, so aren't going to change out from under it, +this is a nice fit. If you have a very large tree and `git status` is +annoyingly slow, you can turn it on: + + git config core.ignoreStat true + +When git mv and git rm are used, those changes *do* get noticed, even +on assume-unchanged files. When new files are added, eg by `git annex add`, +they are also noticed. + +There are two gotchas. Both occur because `git add` does not stage +assume-unchanged files. + +1. When an annexed file is moved to a different directory, it updates + the symlink, and runs `git add` on it. So the file will move, + but the changed symlink will not be noticed by git and it will commit a + dangling symlink. +2. When using `git annex migrate`, it changes the symlink and `git adds` + it. Again this won't be committed. + +These can be worked around by running `git update-index --really-refresh` +after performing such operations. I hope that `git add` will be changed +to stage changes to assume-unchanged files, which would remove this +only complication. --[[Joey]] diff --git a/doc/tips/assume-unstaged/comment_1_44abd811ef79a85e557418e17a3927be._comment b/doc/tips/assume-unstaged/comment_1_44abd811ef79a85e557418e17a3927be._comment new file mode 100644 index 0000000000..d253feb5b8 --- /dev/null +++ b/doc/tips/assume-unstaged/comment_1_44abd811ef79a85e557418e17a3927be._comment @@ -0,0 +1,10 @@ +[[!comment format=mdwn + username="https://me.yahoo.com/a/2djv2EYwk43rfJIAQXjYt_vfuOU-#a11a6" + nickname="Olivier R" + subject="It doesn't work 100%" + date="2012-05-03T21:42:54Z" + content=""" +When you remove tracked files... it doesn't show the new status. it's like if the file was ignored. + + +"""]] diff --git a/doc/tips/automatically_getting_files_on_checkout.mdwn b/doc/tips/automatically_getting_files_on_checkout.mdwn new file mode 100644 index 0000000000..bbb3b302eb --- /dev/null +++ b/doc/tips/automatically_getting_files_on_checkout.mdwn @@ -0,0 +1,15 @@ +Normally git-annex does not retrieve file contents when checking out a +tree. In some use cases, it makes sense to always have the contents of +files available after a `git checkout` or `git update`. This can be +accomplished by installing the following as `.git/hooks/post-checkout` + + #!/bin/sh + # Uses git-annex to get all files in the specified directories + # (relative to the top of the repository) on checkout. + dirs=. + top="$(git rev-parse --show-toplevel)" + for dir in "$dirs"; do git annex get $top/$dir"; done + +By default, all files in the whole repository will be made available. The +`dirs` setting can be configured if you only want to get files in certian +directories. diff --git a/doc/tips/beware_of_SSD_wear_when_doing_fsck_on_large_special_remotes.mdwn b/doc/tips/beware_of_SSD_wear_when_doing_fsck_on_large_special_remotes.mdwn new file mode 100644 index 0000000000..0983c7d31b --- /dev/null +++ b/doc/tips/beware_of_SSD_wear_when_doing_fsck_on_large_special_remotes.mdwn @@ -0,0 +1,2 @@ +When git annex does fsck on (for example) a GPG-encrypted special directory remote, it first transfers the whole file into .git/annex/tmp directory. +If your annex is on an SSD, it's a good idea to make .git/annex/tmp a symlink to say /var/tmp so SSD isn't worn down. This actually may be a better default. diff --git a/doc/tips/beware_of_SSD_wear_when_doing_fsck_on_large_special_remotes/comment_1_e7c5c46112a2406b873d08bbf53c40d8._comment b/doc/tips/beware_of_SSD_wear_when_doing_fsck_on_large_special_remotes/comment_1_e7c5c46112a2406b873d08bbf53c40d8._comment new file mode 100644 index 0000000000..9c7bc2ed17 --- /dev/null +++ b/doc/tips/beware_of_SSD_wear_when_doing_fsck_on_large_special_remotes/comment_1_e7c5c46112a2406b873d08bbf53c40d8._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-07-31T15:15:41Z" + content=""" +Of course, this only works when /var/tmp isn't on SSD itself. Perhaps tmpfs (e.g. a /tmp on many distros) is good -- after checking that there's enough space to transfer a particular file. +"""]] diff --git a/doc/tips/beware_of_SSD_wear_when_doing_fsck_on_large_special_remotes/comment_2_daf45ce29fed986fa9aa8b173760d0b7._comment b/doc/tips/beware_of_SSD_wear_when_doing_fsck_on_large_special_remotes/comment_2_daf45ce29fed986fa9aa8b173760d0b7._comment new file mode 100644 index 0000000000..929019705b --- /dev/null +++ b/doc/tips/beware_of_SSD_wear_when_doing_fsck_on_large_special_remotes/comment_2_daf45ce29fed986fa9aa8b173760d0b7._comment @@ -0,0 +1,14 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawln3ckqKx0x_xDZMYwa9Q1bn4I06oWjkog" + nickname="Michael" + subject="there's a problem" + date="2013-08-04T17:15:05Z" + content=""" +If .git/annex/tmp is a symlink to another fs, then adding doesn't work: + + add file1.jpg (checksum...) + git-annex: /path/to/.git/annex/tmp/tmp30148: rename: unsupported operation (Invalid cross-device link) + +It looks like it would be good to have two types of tmp directories here, one for adding, another one for verifying (and that one could be redirected off SSD). + +"""]] diff --git a/doc/tips/centralised_repository:_starting_from_nothing.mdwn b/doc/tips/centralised_repository:_starting_from_nothing.mdwn new file mode 100644 index 0000000000..b12246d368 --- /dev/null +++ b/doc/tips/centralised_repository:_starting_from_nothing.mdwn @@ -0,0 +1,75 @@ +If you are starting from nothing (no existing `git` or `git-annex` repository) and want to use a server as a centralised repository, try the following steps. + +On the server where you'll hold the "master" repository: + + server$ cd /one/git + server$ mkdir m + server$ cd m + server$ git init --bare + Initialized empty Git repository in /one/git/m/ + server$ git annex init origin + init origin ok + server$ + +Clone that to the laptop: + + laptop$ cd /other + laptop$ git clone ssh://server//one/git/m + Cloning into 'm'... + Warning: No xauth data; using fake authentication data for X11 forwarding. + remote: Counting objects: 5, done. + remote: Compressing objects: 100% (3/3), done. + remote: Total 5 (delta 0), reused 0 (delta 0) + Receiving objects: 100% (5/5), done. + warning: remote HEAD refers to nonexistent ref, unable to checkout. + + laptop$ cd m + laptop$ git annex init laptop + init laptop ok + laptop$ + +Merge the `git-annex` repository (this is the bit that is often +overlooked!): + + laptop$ git annex merge + merge . (merging "origin/git-annex" into git-annex...) + ok + laptop$ + +Add some content: + + laptop$ git annex addurl http://kitenet.net/~joey/screencasts/git-annex_coding_in_haskell.ogg + "kitenet.net_~joey_screencasts_git-annex_coding_in_haskell.ogg" + addurl kitenet.net_~joey_screencasts_git-annex_coding_in_haskell.ogg (downloading http://kitenet.net/~joey/screencasts/git-annex_coding_in_haskell.ogg ...) --2011-12-15 08:13:10-- http://kitenet.net/~joey/screencasts/git-annex_coding_in_haskell.ogg + Resolving kitenet.net (kitenet.net)... 2001:41c8:125:49::10, 80.68.85.49 + Connecting to kitenet.net (kitenet.net)|2001:41c8:125:49::10|:80... connected. + HTTP request sent, awaiting response... 200 OK + Length: 39362757 (38M) [audio/ogg] + Saving to: `/other/m/.git/annex/tmp/URL--http&c%%kitenet.net%~joey%screencasts%git-annex_coding_in_haskell.ogg' + + 100%[======================================>] 39,362,757 2.31M/s in 17s + + 2011-12-15 08:13:27 (2.21 MB/s) - `/other/m/.git/annex/tmp/URL--http&c%%kitenet.net%~joey%screencasts%git-annex_coding_in_haskell.ogg' saved [39362757/39362757] + + (checksum...) ok + (Recording state in git...) + laptop$ git commit -m 'See Joey play.' + [master (root-commit) 106e923] See Joey play. + 1 files changed, 1 insertions(+), 0 deletions(-) + create mode 120000 kitenet.net_~joey_screencasts_git-annex_coding_in_haskell.ogg + laptop$ + +All fine, now push it back to the centralised master: + + laptop$ git push + Counting objects: 20, done. + Delta compression using up to 4 threads. + Compressing objects: 100% (11/11), done. + Writing objects: 100% (18/18), 1.50 KiB, done. + Total 18 (delta 1), reused 1 (delta 0) + To ssh://server//one/git/m + 3ba1386..ad3bc9e git-annex -> git-annex + laptop$ + +You can add more "client" repositories by following the `laptop` +sequence of operations. diff --git a/doc/tips/centralised_repository:_starting_from_nothing/comment_1_b0d22822017646775869ce1292e676f4._comment b/doc/tips/centralised_repository:_starting_from_nothing/comment_1_b0d22822017646775869ce1292e676f4._comment new file mode 100644 index 0000000000..22857af3e8 --- /dev/null +++ b/doc/tips/centralised_repository:_starting_from_nothing/comment_1_b0d22822017646775869ce1292e676f4._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="http://joey.kitenet.net/" + nickname="joey" + subject="comment 1" + date="2011-12-23T19:19:53Z" + content=""" +See also: [[centralized_git_repository_tutorial]] +"""]] diff --git a/doc/tips/centralized_git_repository_tutorial.mdwn b/doc/tips/centralized_git_repository_tutorial.mdwn new file mode 100644 index 0000000000..00283829fd --- /dev/null +++ b/doc/tips/centralized_git_repository_tutorial.mdwn @@ -0,0 +1,140 @@ +The [[walkthrough]] builds up a decentralized git repository setup, but +git-annex can also be used with a centralized bare repository, just like +git can. This tutorial shows how to set up a centralized repository hosted on +GitHub. + +## set up the repository, and make a checkout + +I've created a repository for technical talk videos, which you can +[fork on Github](https://github.com/joeyh/techtalks). +Or make your own repository on GitHub (or elsewhere) now. + +On your laptop, [[install]] git-annex, and clone the repository: + + # git clone git@github.com:joeyh/techtalks.git + # cd techtalks + +Tell git-annex to use the repository, and describe where this clone is +located: + + # git annex init 'my laptop' + init my laptop ok + +Let's tell git-annex that GitHub doesn't support running git-annex-shell there. +This means you can't store annexed file *contents* on GitHub; it would +really be better to host the bare repository on your own server, which +would not have this limitation. (If you want to do that, check out +[[using_gitolite_with_git-annex]].) + + # git config remote.origin.annex-ignore true + +## add files to the repository + +Add some files, obtained however. + + # youtube-dl -t 'http://www.youtube.com/watch?v=b9FagOVqxmI' + # git annex add *.mp4 + add Haskell_Amuse_Bouche-b9FagOVqxmI.mp4 (checksum) ok + (Recording state in git...) + # git commit -m "added a video. I have not watched it yet but it sounds interesting" + +This file is available directly from the web; so git-annex can download it: + + # git annex addurl http://kitenet.net/~joey/screencasts/git-annex_coding_in_haskell.ogg + addurl kitenet.net_~joey_screencasts_git-annex_coding_in_haskell.ogg + (downloading http://kitenet.net/~joey/screencasts/git-annex_coding_in_haskell.ogg ...) + (checksum...) ok + (Recording state in git...) + # git commit -a -m 'added a screencast I made' + +Feel free the rename the files, etc, using normal git commands: + + # git mv Haskell_Amuse_Bouche-b9FagOVqxmI.mp4 Haskell_Amuse_Bouche.mp4 + # git mv kitenet.net_~joey_screencasts_git-annex_coding_in_haskell.ogg git-annex_coding_in_haskell.ogg + # git commit -m 'better filenames' + +Now push your changes back to the central repository. This first time, +remember to push the git-annex branch, which is used to track the file +contents. + + # git push origin master git-annex + To git@github.com:joeyh/techtalks.git + * [new branch] master -> master + * [new branch] git-annex -> git-annex + +That push went fast, because it didn't upload large videos to GitHub. +To check this, you can ask git-annex where the contents of the videos are: + + # git annex whereis + whereis Haskell_Amuse_Bouche.mp4 (1 copy) + 767e8558-0955-11e1-be83-cbbeaab7fff8 -- here + ok + whereis git-annex_coding_in_haskell.ogg (2 copies) + 00000000-0000-0000-0000-000000000001 -- web + 767e8558-0955-11e1-be83-cbbeaab7fff8 -- here + ok + +## make more checkouts + +So far you have a central repository, and a checkout on a laptop. +Let's make another checkout that's used as a backup. You can put it anywhere +you like, just make it be somewhere your laptop can access. A few options: + +* Put it on a USB drive that you can plug into the laptop. +* Put it on a desktop. +* Put it on some server in the local network. +* Put it on a remote VPS. + +I'll use the VPS option, but these instructions should work for +any of the above. + + # ssh server + server# sudo apt-get install git-annex + +Clone the central repository as before. (If the clone fails, you need +to add your server's ssh public key to github -- see +[this page](http://help.github.com/ssh-issues/).) + + server# git clone git@github.com:joeyh/techtalks.git + server# cd techtalks + server# git config remote.origin.annex-ignore true + server# git annex init 'backup' + init backup (merging origin/git-annex into git-annex...) ok + +Notice that the server does not have the contents of any of the files yet. +If you run `ls`, you'll see broken symlinks. We want to populate this +backup with the file contents, by copying them from your laptop. + +Back on your laptop, you need to configure a git remote for the backup. +Adjust the ssh url as needed to point to wherever the backup is. (If it +was on a local USB drive, you'd use the path to the repository instead.) + + # git remote add backup ssh://server/~/techtalks + +Now git-annex on your laptop knows how to reach the backup repository, +and can do things like copy files to it: + + # git annex copy --to backup git-annex_coding_in_haskell.ogg + copy git-annex_coding_in_haskell.ogg (checking backup...) + 12877824 2% 255.11kB/s 00:00 + ok + +You can also `git annex move` files to it, to free up space on your laptop. +And then you can `git annex get` files back to your laptop later on, as +desired. + +After you use git-annex to move files around, remember to push, +which will broadcast its updated location information. + + # git push + +## take it farther + +Of course you can create as many checkouts as you desire. If you have a +desktop machine too, you can make a checkout there, and use `git remote +add` to also let your desktop access the backup repository. + +You can add remotes for each direct connection between machines you find you +need -- so make the laptop have the desktop as a remote, and the desktop +have the laptop as a remote, and then on either machine git-annex can +access files stored on the other. diff --git a/doc/tips/downloading_podcasts.mdwn b/doc/tips/downloading_podcasts.mdwn new file mode 100644 index 0000000000..2e0ec0e300 --- /dev/null +++ b/doc/tips/downloading_podcasts.mdwn @@ -0,0 +1,63 @@ +You can use git-annex as a podcatcher, to download podcast contents. +No additional software is required, but your git-annex must be built +with the Feeds feature (run `git annex version` to check). + +All you need to do is put something like this in a cron job: + +`cd somerepo && git annex importfeed http://url/to/podcast http://other/podcast/url` + +This downloads the urls, and parses them as RSS, Atom, or RDF feeds. +All enclosures are downloaded and added to the repository, the same as if you +had manually run `git annex addurl` on each of them. + +git-annex will avoid downloading a file from a feed if its url has already +been stored in the repository before. So once a file is downloaded, +you can move it around, delete it, `git annex drop` its content, etc, +and it will not be downloaded again by repeated runs of +`git annex importfeed`. Just how a podcatcher should behave. + +## templates + +To control the filenames used for items downloaded from a feed, +there's a --template option. The default is +`--template='${feedtitle}/${itemtitle}${extension}'` + +Other available template variables: +feedauthor, itemauthor, itemsummary, itemdescription, itemrights, itemid + +## catching up + +To catch up on a feed without downloading its contents, +use `git annex importfeed --relaxed`, and delete the symlinks it creates. +Next time you run `git annex addurl` it will only fetch any new items. + +## fast mode + +To add a feed without downloading its contents right now, +use `git annex importfeed --fast`. Then you can use `git annex get` as +usual to download the content of an item. + +## storing the podcast list in git + +You can check the list of podcast urls into git right next to the +files it downloads. Just make a file named feeds and add one podcast url +per line. + +Then you can run git-annex on all the feeds: + +`xargs git-annex importfeed < feeds` + +## distributed podcatching + +A nice benefit of using git-annex as a podcatcher is that you can +run `git annex importfeed` on the same url in different clones +of a repository, and `git annex sync` will sync it all up. + +## centralized podcatching + +You can also have a designated machine which always fetches all podcstas +to local disk and stores them. That way, you can archive podcasts with +time-delayed deletion of upstream content. You can also work around slow +downloads upstream by podcatching to a server with ample bandwidth or work +around a slow local Internet connection by podcatching to your home server +and transferring to your laptop on demand. diff --git a/doc/tips/downloading_podcasts/comment_10_4d4f6c22070b58918ee8d34c5e7290ad._comment b/doc/tips/downloading_podcasts/comment_10_4d4f6c22070b58918ee8d34c5e7290ad._comment new file mode 100644 index 0000000000..3bf5afe681 --- /dev/null +++ b/doc/tips/downloading_podcasts/comment_10_4d4f6c22070b58918ee8d34c5e7290ad._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="http://joeyh.name/" + ip="2001:4978:f:21a::2" + subject="comment 10" + date="2013-08-05T16:47:30Z" + content=""" +`cabal install feed` should get the necessary library installed so that git-annex will build with feeds support. +"""]] diff --git a/doc/tips/downloading_podcasts/comment_11_d8d77048c7e2524968c188e1ad517873._comment b/doc/tips/downloading_podcasts/comment_11_d8d77048c7e2524968c188e1ad517873._comment new file mode 100644 index 0000000000..fd34599265 --- /dev/null +++ b/doc/tips/downloading_podcasts/comment_11_d8d77048c7e2524968c188e1ad517873._comment @@ -0,0 +1,24 @@ +[[!comment format=mdwn + username="http://a-or-b.myopenid.com/" + ip="220.244.41.108" + subject="comment 11" + date="2013-08-06T04:20:16Z" + content=""" + $ cabal install feed + Resolving dependencies... + All the requested packages are already installed: + feed-0.3.9.1 + Use --reinstall if you want to reinstall anyway. + +Then I reinstalled `git-annex` but it still doesn't find the feeds flag. + + $ git annex version + git-annex version: 4.20130802 + build flags: Assistant Webapp Pairing Testsuite S3 WebDAV FsEvents XMPP DNS + +Do I need to do something like: + + cabal install git-annex --bindir=$HOME/bin -f\"-assistant -webapp -webdav -pairing -xmpp -dns -feed\" + +...but what are the default flags to include in addition to `-feed` +"""]] diff --git a/doc/tips/downloading_podcasts/comment_12_0859317471b43c88744dd3df95c879f7._comment b/doc/tips/downloading_podcasts/comment_12_0859317471b43c88744dd3df95c879f7._comment new file mode 100644 index 0000000000..e75a44a8cd --- /dev/null +++ b/doc/tips/downloading_podcasts/comment_12_0859317471b43c88744dd3df95c879f7._comment @@ -0,0 +1,10 @@ +[[!comment format=mdwn + username="http://joeyh.name/" + ip="2001:4978:f:21a::2" + subject="comment 12" + date="2013-08-06T04:24:10Z" + content=""" +-f-Feed will disable the feature. -fFeed will try to force it on. + +You can probably work out what's going wrong using cabal install -v3 +"""]] diff --git a/doc/tips/downloading_podcasts/comment_13_e8c3c97282d17e2a1d47fb9d5e2b2f7b._comment b/doc/tips/downloading_podcasts/comment_13_e8c3c97282d17e2a1d47fb9d5e2b2f7b._comment new file mode 100644 index 0000000000..8d12428185 --- /dev/null +++ b/doc/tips/downloading_podcasts/comment_13_e8c3c97282d17e2a1d47fb9d5e2b2f7b._comment @@ -0,0 +1,18 @@ +[[!comment format=mdwn + username="http://a-or-b.myopenid.com/" + ip="220.244.41.108" + subject="comment 13" + date="2013-08-06T05:42:45Z" + content=""" +So I ran `cabal install -v3` and looked at the output, + + Flags chosen: feed=True, tdfa=True, testsuite=True, android=False, + production=True, dns=True, xmpp=True, pairing=True, webapp=True, + assistant=True, dbus=True, inotify=True, webdav=True, s3=True + +This looks like feed should be on. + +There doesn't appear to be any errors in the compile either. + +Is it as simple as a bug where this flag just doesn't show in the `git annex version` command? +"""]] diff --git a/doc/tips/downloading_podcasts/comment_14_05a3694052de36848fbbad6eeeada895._comment b/doc/tips/downloading_podcasts/comment_14_05a3694052de36848fbbad6eeeada895._comment new file mode 100644 index 0000000000..4bc831f7f1 --- /dev/null +++ b/doc/tips/downloading_podcasts/comment_14_05a3694052de36848fbbad6eeeada895._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="http://joeyh.name/" + ip="2001:4978:f:21a::2" + subject="comment 14" + date="2013-08-07T16:03:12Z" + content=""" +Yes, it did turn out to be as simple as my having forgotten that I have to manually add features to the version list. +"""]] diff --git a/doc/tips/downloading_podcasts/comment_15_21028bed8858c2dae1ac9c2d014fd2a1._comment b/doc/tips/downloading_podcasts/comment_15_21028bed8858c2dae1ac9c2d014fd2a1._comment new file mode 100644 index 0000000000..0f998d0669 --- /dev/null +++ b/doc/tips/downloading_podcasts/comment_15_21028bed8858c2dae1ac9c2d014fd2a1._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="http://23.gs/" + ip="46.165.197.5" + subject="No file extension?" + date="2013-08-12T13:21:50Z" + content=""" +It seems git-annex is a bit overzealous when sanitizing the file extension, currently I get: \"Nerdkunde/Let_s_go_to_the_D_M_C_A_m4a\" from http://www.nerdkunde.de/episodes.m4a.rss with the default template and only \"Nerdkunde/Let_s_go_to_the_D_M_C_A._m4a\" if I add the \".\" in the template myself... +"""]] diff --git a/doc/tips/downloading_podcasts/comment_16_4869fb5c9f896acc477c44de06c36ca7._comment b/doc/tips/downloading_podcasts/comment_16_4869fb5c9f896acc477c44de06c36ca7._comment new file mode 100644 index 0000000000..4419d02a81 --- /dev/null +++ b/doc/tips/downloading_podcasts/comment_16_4869fb5c9f896acc477c44de06c36ca7._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="arand" + ip="130.243.226.21" + subject="comment 16" + date="2013-08-12T13:32:46Z" + content=""" +The filename extension is a known issue and already fixed in the development version, see +"""]] diff --git a/doc/tips/downloading_podcasts/comment_17_2e278ff200c1c15efd27c46a3e0aed40._comment b/doc/tips/downloading_podcasts/comment_17_2e278ff200c1c15efd27c46a3e0aed40._comment new file mode 100644 index 0000000000..bc49e5dd07 --- /dev/null +++ b/doc/tips/downloading_podcasts/comment_17_2e278ff200c1c15efd27c46a3e0aed40._comment @@ -0,0 +1,9 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawlpKmTa1OPwy5Jk24pOoD8Vlo2jahzTPnw" + nickname="Stephen" + subject="rss authentication" + date="2013-08-13T13:32:52Z" + content=""" +If a podcast requires authentication, is there a way to pass credentials through? I tried `http://user:pass@site.com/rss.xml` but it didn't work. + +"""]] diff --git a/doc/tips/downloading_podcasts/comment_1_f04bc32a34baeeffcd691e9f7cce0230._comment b/doc/tips/downloading_podcasts/comment_1_f04bc32a34baeeffcd691e9f7cce0230._comment new file mode 100644 index 0000000000..014fe3f50e --- /dev/null +++ b/doc/tips/downloading_podcasts/comment_1_f04bc32a34baeeffcd691e9f7cce0230._comment @@ -0,0 +1,13 @@ +[[!comment format=mdwn + username="ckeen" + ip="79.249.110.228" + subject="Filename too long" + date="2013-07-30T14:39:44Z" + content=""" +It seems that some of my feeds get stored into keys that generate a too long filename: + + podcasts/.git/annex/tmp/b1f_325_URL-s143660317--http&c%%feedproxy.google.com%~r%mixotic%~5%urTIRWQK2OQ%Mixotic__258__-__Michael__Miller__-__Galactic__Technolgies.mp3.log.web: + openBinaryFile: invalid argument (File name too long) + +Is there a way to work around this? +"""]] diff --git a/doc/tips/downloading_podcasts/comment_2_a9a98cad7358d16792853a2ee413fe6c._comment b/doc/tips/downloading_podcasts/comment_2_a9a98cad7358d16792853a2ee413fe6c._comment new file mode 100644 index 0000000000..f8ba1155cb --- /dev/null +++ b/doc/tips/downloading_podcasts/comment_2_a9a98cad7358d16792853a2ee413fe6c._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="http://joeyh.name/" + ip="4.154.0.21" + subject="comment 2" + date="2013-07-30T17:16:07Z" + content=""" +@ckeen You seem to be using a filesystem that does not support filenames 150 characters long. This is unusual -- even windows and android can support a filename up to 255 characters in length. `git-annex addurl` already deals with this sort of problem by limiting the filename to 255 characters. If you'd like to file a bug report with details about your system, I can try to make git-annex support its limitations, I suppose. +"""]] diff --git a/doc/tips/downloading_podcasts/comment_3_5a8068a5cb0fd864581157a3aa5d1113._comment b/doc/tips/downloading_podcasts/comment_3_5a8068a5cb0fd864581157a3aa5d1113._comment new file mode 100644 index 0000000000..7e5633865a --- /dev/null +++ b/doc/tips/downloading_podcasts/comment_3_5a8068a5cb0fd864581157a3aa5d1113._comment @@ -0,0 +1,10 @@ +[[!comment format=mdwn + username="http://www.joachim-breitner.de/" + nickname="nomeata" + subject="Great stuff!" + date="2013-07-30T21:21:57Z" + content=""" +Looking forward to seeing it in Debian unstable; where it will definitely replace my hpodder setup. + +I guess there is no easy way to re-use the files already downloaded with hpodder? At first I thought that `git annex importfeed --relaxed` followed by adding the files to the git annex would work, but `importfeed` stores URLs, not content-based hashes, so it wouldn’t match up. +"""]] diff --git a/doc/tips/downloading_podcasts/comment_4_e7072a9da30b4c4b4c526013144238d4._comment b/doc/tips/downloading_podcasts/comment_4_e7072a9da30b4c4b4c526013144238d4._comment new file mode 100644 index 0000000000..1693c4bdcf --- /dev/null +++ b/doc/tips/downloading_podcasts/comment_4_e7072a9da30b4c4b4c526013144238d4._comment @@ -0,0 +1,10 @@ +[[!comment format=mdwn + username="http://joeyh.name/" + ip="4.154.0.21" + subject="comment 4" + date="2013-07-30T21:29:50Z" + content=""" +@nomeata, well, you can, but it has to download the files again. + +When run without --fast, `importfeed` does use content based hashes, so if you run it in a temporary directory, it will download the content redundantly, hash it and see it's the same, and add the url to that hash. You can then delete the temporary directory, and the files hpodder had downloaded will have the url attached to them now. I don't know if this really buys you anything over deleting the hpodder files and starting over though. +"""]] diff --git a/doc/tips/downloading_podcasts/comment_5_79b3f8d678ac9f67df4c0cd649657283._comment b/doc/tips/downloading_podcasts/comment_5_79b3f8d678ac9f67df4c0cd649657283._comment new file mode 100644 index 0000000000..f5df9910f3 --- /dev/null +++ b/doc/tips/downloading_podcasts/comment_5_79b3f8d678ac9f67df4c0cd649657283._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="ckeen" + ip="79.249.110.228" + subject="Force a reload of a feed?" + date="2013-07-31T10:35:50Z" + content=""" +Currently I have my podcasts imported with --fast. For some reason there are podcast episodes missing. This has been done propably during my period of toying with the feature. If I retry on a clean annex I see all episodes. My suspicion is that git-annex has been interrupted during downloading a feed but now somehow thinks it's already there. How can I debug this situation and/or force git annex to retry all the links in a feed? +"""]] diff --git a/doc/tips/downloading_podcasts/comment_6_35106fee5458bdd5c21868fbc49d3616._comment b/doc/tips/downloading_podcasts/comment_6_35106fee5458bdd5c21868fbc49d3616._comment new file mode 100644 index 0000000000..caeca01511 --- /dev/null +++ b/doc/tips/downloading_podcasts/comment_6_35106fee5458bdd5c21868fbc49d3616._comment @@ -0,0 +1,10 @@ +[[!comment format=mdwn + username="http://joeyh.name/" + ip="4.154.0.21" + subject="use the force" + date="2013-07-31T16:20:39Z" + content=""" +The only way it can skip downloading a file is if its url has already been seen before. Perhaps you deleted them? + +I've made `importfeed --force` re-download files it's seen before. +"""]] diff --git a/doc/tips/downloading_podcasts/comment_7_ceb16498b7aadbf04a27acd5d6561d46._comment b/doc/tips/downloading_podcasts/comment_7_ceb16498b7aadbf04a27acd5d6561d46._comment new file mode 100644 index 0000000000..ac2c89a366 --- /dev/null +++ b/doc/tips/downloading_podcasts/comment_7_ceb16498b7aadbf04a27acd5d6561d46._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="ckeen" + ip="78.108.63.46" + subject="--force reload all URLs" + date="2013-08-01T09:47:34Z" + content=""" +Is it intentionally saving URLs with a prefixed 2_? I have sorted out all missing URLs and renamed it, so no harm done, but it has been a bit of a hassle to get there. +"""]] diff --git a/doc/tips/downloading_podcasts/comment_8_147397603f0b3fdb42ca387d1da7c5ef._comment b/doc/tips/downloading_podcasts/comment_8_147397603f0b3fdb42ca387d1da7c5ef._comment new file mode 100644 index 0000000000..0995d80752 --- /dev/null +++ b/doc/tips/downloading_podcasts/comment_8_147397603f0b3fdb42ca387d1da7c5ef._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="http://joeyh.name/" + ip="4.152.108.145" + subject="comment 8" + date="2013-08-01T16:05:10Z" + content=""" +I've now made importfeed --force a bit smarter about reusing existing files. +"""]] diff --git a/doc/tips/downloading_podcasts/comment_9_6a26a6cc7683d38fae0f23c5a52d1e23._comment b/doc/tips/downloading_podcasts/comment_9_6a26a6cc7683d38fae0f23c5a52d1e23._comment new file mode 100644 index 0000000000..3045c98949 --- /dev/null +++ b/doc/tips/downloading_podcasts/comment_9_6a26a6cc7683d38fae0f23c5a52d1e23._comment @@ -0,0 +1,24 @@ +[[!comment format=mdwn + username="http://a-or-b.myopenid.com/" + ip="220.244.41.108" + subject="How do I switch on the 'feeds' feature?" + date="2013-08-05T04:52:41Z" + content=""" +Joey - your initial post said: + + git-annex must be built with the Feeds feature (run git annex version to check). + +...but how do I actually switch on the feeds feature? + +I install git-annex from cabal, so I do + + cabal update + cabal install git-annex + +which I did this morning and now `git annex version` gives me: + + git-annex version: 4.20130802 + build flags: Assistant Webapp Pairing Testsuite S3 WebDAV FsEvents XMPP DNS + +So it is the latest version, but without Feeds. :-( +"""]] diff --git a/doc/tips/dropboxannex.mdwn b/doc/tips/dropboxannex.mdwn new file mode 100644 index 0000000000..926e142ca9 --- /dev/null +++ b/doc/tips/dropboxannex.mdwn @@ -0,0 +1,28 @@ +dropboxannex +========= + +Hook program for gitannex to use dropbox as backend + +# Requirements: + + python2 + +Credit for the Dropbox api interface goes to Dropbox. + +# Install +Clone the git repository in your home folder. + + git clone git://github.com/TobiasTheViking/dropboxannex.git + +This should make a ~/dropboxannex folder + +# Setup +Run the program once to set it up. + + cd ~/dropboxannex; python2 dropboxannex.py + +# Commands for gitannex: + + git config annex.dropbox-hook '/usr/bin/python2 ~/dropboxannex/dropboxannex.py' + git annex initremote dropbox type=hook hooktype=dropbox encryption=shared + git annex describe dropbox "the dropbox library" diff --git a/doc/tips/emacs_integration.mdwn b/doc/tips/emacs_integration.mdwn new file mode 100644 index 0000000000..12f16888a6 --- /dev/null +++ b/doc/tips/emacs_integration.mdwn @@ -0,0 +1,20 @@ +bergey has developed an emacs mode for browsing git-annex repositories, +dired style. + + + +Locally available files are colored differently, and pressing g runs +`git annex get` on the file at point. + +---- + +John Wiegley has developed a brand new git-annex interaction mode for +Emacs, which aims to integrate with the standard facilities +(C-x C-q, M-x dired, etc) rather than invent its own interface. + + + +He has also added support to org-attach; if +`org-attach-git-annex-cutoff' is non-nil and smaller than the size + of the file you're attaching then org-attach will `git annex add the +file`; otherwise it will "git add" it. diff --git a/doc/tips/finding_duplicate_files.mdwn b/doc/tips/finding_duplicate_files.mdwn new file mode 100644 index 0000000000..94fc85400e --- /dev/null +++ b/doc/tips/finding_duplicate_files.mdwn @@ -0,0 +1,21 @@ +Maybe you had a lot of files scattered around on different drives, and you +added them all into a single git-annex repository. Some of the files are +surely duplicates of others. + +While git-annex stores the file contents efficiently, it would still +help in cleaning up this mess if you could find, and perhaps remove +the duplicate files. + +Here's a command line that will show duplicate sets of files grouped together: + + git annex find --include '*' --format='${file} ${escaped_key}\n' | \ + sort -k2 | uniq --all-repeated=separate -f1 | \ + sed 's/ [^ ]*$//' + +Here's a command line that will remove one of each duplicate set of files: + + git annex find --include '*' --format='${file} ${escaped_key}\n' | \ + sort -k2 | uniq --repeated -f1 | sed 's/ [^ ]*$//' | \ + xargs -d '\n' git rm + +--[[Joey]] diff --git a/doc/tips/finding_duplicate_files/comment_1_ddb477ca242ffeb21e0df394d8fdf5d2._comment b/doc/tips/finding_duplicate_files/comment_1_ddb477ca242ffeb21e0df394d8fdf5d2._comment new file mode 100644 index 0000000000..d1bd4475e5 --- /dev/null +++ b/doc/tips/finding_duplicate_files/comment_1_ddb477ca242ffeb21e0df394d8fdf5d2._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="http://adamspiers.myopenid.com/" + nickname="Adam" + subject="Cool" + date="2011-12-23T19:16:50Z" + content=""" +Very nice :) Just for reference, here's [my Perl implementation](https://github.com/aspiers/git-config/blob/master/bin/git-annex-finddups). As per [this discussion](http://git-annex.branchable.com/todo/wishlist:_Provide_a___34__git_annex__34___command_that_will_skip_duplicates/#comment-fb15d5829a52cd05bcbd5dc53edaffb2) it would be interesting to benchmark these two approaches and see if one is substantially more efficient than the other w.r.t. CPU and memory usage. +"""]] diff --git a/doc/tips/finding_duplicate_files/comment_2_900eafe0a781018ff44b35ac232e3ad3._comment b/doc/tips/finding_duplicate_files/comment_2_900eafe0a781018ff44b35ac232e3ad3._comment new file mode 100644 index 0000000000..605c804dd8 --- /dev/null +++ b/doc/tips/finding_duplicate_files/comment_2_900eafe0a781018ff44b35ac232e3ad3._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="bremner" + ip="156.34.89.108" + subject="problems with spaces in filenames" + date="2012-09-05T02:12:18Z" + content=""" +note that the sort -k2 doesn't work right for filenames with spaces in them. On the other hand, git-rm doesn't seem to like the escaped names from escaped_file. +"""]] diff --git a/doc/tips/finding_duplicate_files/comment_3._comment b/doc/tips/finding_duplicate_files/comment_3._comment new file mode 100644 index 0000000000..44eeb50759 --- /dev/null +++ b/doc/tips/finding_duplicate_files/comment_3._comment @@ -0,0 +1,39 @@ +[[!comment format=mdwn + username="mhameed" + ip="82.32.202.53" + subject="problems with spaces in filenames" + date="Wed Sep 5 09:38:56 BST 2012" + content=""" + +Spaces, and other special chars can make filename handeling ugly. +If you don't have a restriction on keeping the exact filenames, then +it might be easiest just to get rid of the problematic chars. + + #!/bin/bash + + function process() { + dir="$1" + echo "processing $dir" + pushd $dir >/dev/null 2>&1 + + for fileOrDir in *; do + nfileOrDir=`echo "$fileOrDir" | sed -e 's/\[//g' -e 's/\]//g' -e 's/ /_/g' -e "s/'//g" ` + if [ "$fileOrDir" != "$nfileOrDir" ]; then + echo renaming $fileOrDir to $nfileOrDir + git mv "$fileOrDir" "$nfileOrDir" + else + echo "skipping $fileOrDir, no need to rename." + fi + done + + find ./ -mindepth 1 -maxdepth 1 -type d | while read d; do + process "$d" + done + popd >/dev/null 2>&1 + } + + process . + +Maybe you can run something like this before checking for duplicates. + +"""]] diff --git a/doc/tips/finding_duplicate_files/comment_4_1494143a74cc1e9fbe4720c14b73d42b._comment b/doc/tips/finding_duplicate_files/comment_4_1494143a74cc1e9fbe4720c14b73d42b._comment new file mode 100644 index 0000000000..f1a86f43ce --- /dev/null +++ b/doc/tips/finding_duplicate_files/comment_4_1494143a74cc1e9fbe4720c14b73d42b._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="bremner" + ip="156.34.89.108" + subject="more about spaces..." + date="2012-09-09T19:33:01Z" + content=""" +Ironically, previous renaming to remove spaces, plus some synching is how I ended up with these duplicates. For what it is worth, aspiers perl script worked out for me with a small modification. I just only printed out the duplicates with spaces in them (quoted). +"""]] diff --git a/doc/tips/finding_duplicate_files/comment_5_1a35ca360468bcb84a67ad8d62a2ef7d._comment b/doc/tips/finding_duplicate_files/comment_5_1a35ca360468bcb84a67ad8d62a2ef7d._comment new file mode 100644 index 0000000000..23beb779f8 --- /dev/null +++ b/doc/tips/finding_duplicate_files/comment_5_1a35ca360468bcb84a67ad8d62a2ef7d._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawkaBh9VNJ-RZ26wJZ4BEhMN1IlPT-DK6JA" + nickname="Alex" + subject="printing keys first is the easiest workaround" + date="2013-04-01T23:32:23Z" + content=""" +Since the keys are sure to have nos paces in them, putting them first makes working with the output with tools like sort, uniq, and awk simpler. +"""]] diff --git a/doc/tips/finding_duplicate_files/comment_6_a6e88c93b31f67c933523725ff61b287._comment b/doc/tips/finding_duplicate_files/comment_6_a6e88c93b31f67c933523725ff61b287._comment new file mode 100644 index 0000000000..31601a9893 --- /dev/null +++ b/doc/tips/finding_duplicate_files/comment_6_a6e88c93b31f67c933523725ff61b287._comment @@ -0,0 +1,16 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawnkBYpLu_NOj7Uq0-acvLgWhxF8AUEIJbo" + nickname="Chris" + subject="Find files by key" + date="2013-05-03T04:14:55Z" + content=""" +Is there any simple way to search for files with a given key? + +At the moment, the best I've come up with is this: + +```` +git annex find --include '*' --format='${key} ${file}' | grep +```` + +where `` is the key. This seems like an awfully longwinded approach, but I don't see anything in the docs indicating a simpler way to do it. Am I missing something? +"""]] diff --git a/doc/tips/finding_duplicate_files/comment_7_347b0186755a809594bd42feda6363e2._comment b/doc/tips/finding_duplicate_files/comment_7_347b0186755a809594bd42feda6363e2._comment new file mode 100644 index 0000000000..d97b0d500b --- /dev/null +++ b/doc/tips/finding_duplicate_files/comment_7_347b0186755a809594bd42feda6363e2._comment @@ -0,0 +1,10 @@ +[[!comment format=mdwn + username="http://joeyh.name/" + nickname="joey" + subject="comment 7" + date="2013-05-13T18:42:14Z" + content=""" +@Chris I guess there's no really easy way because searching for a given key is not something many people need to do. + +However, git does provide a way. Try `git log --stat -S $KEY` +"""]] diff --git a/doc/tips/flickrannex.mdwn b/doc/tips/flickrannex.mdwn new file mode 100644 index 0000000000..47d834177d --- /dev/null +++ b/doc/tips/flickrannex.mdwn @@ -0,0 +1,49 @@ +Hook program for gitannex to use flickr as backend. + +This allows storing any type of file on flickr, not only images and movies. + +# Requirements: + + python2 + +Credit for the flickr api interface goes to: +Credit for the png library goes to: +Credit for the png tEXt patch goes to: + +## Install + +Clone the git repository in your home folder. + + git clone git://github.com/TobiasTheViking/flickrannex.git + +This should make a ~/flickrannex folder + +## Setup + +Run the program once to set it up. + + cd ~/flickrannex; python2 flickrannex.py + +After the setup has finished, it will print the git-annex configure lines. + +## Configuring git-annex + + git config annex.flickr-hook '/usr/bin/python2 ~/flickrannex/flickrannex.py' + git annex initremote flickr type=hook hooktype=flickr encryption=shared + git annex describe flickr "the flickr library" + +## Notes + +### Unencrypted mode + +The photo name on flickr is currently the [[key|backends]] used by git-annex. + +### Encrypted mode + +The current version base64 encodes all the data, which results in ~35% +larger filesize. + +I might look into yyenc instead. I'm not sure if it will work in the tEXt +field. + +-- Tobias diff --git a/doc/tips/flickrannex/comment_10_50707f259abe5829ce075dfbecd5a4ba._comment b/doc/tips/flickrannex/comment_10_50707f259abe5829ce075dfbecd5a4ba._comment new file mode 100644 index 0000000000..7bda45e5ca --- /dev/null +++ b/doc/tips/flickrannex/comment_10_50707f259abe5829ce075dfbecd5a4ba._comment @@ -0,0 +1,13 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawmkBwMWvNKZZCge_YqobCSILPMeK6xbFw8" + nickname="develop" + subject="comment 10" + date="2013-06-07T09:39:59Z" + content=""" +I'm not even sure if chunksize is exposed to the hooks at all. + +As it is, the hook will check the filesize, and if the filesize is more than 30mbyte it will exit 1. + +Chunking may be implemented down the road. I do believe joeyh might have some plans that will touch this issue, so I'd rather wait. Than re-invent the wheel yet again. + +"""]] diff --git a/doc/tips/flickrannex/comment_11_ab5bcb025381b3da4d7c6dfd0c7310dd._comment b/doc/tips/flickrannex/comment_11_ab5bcb025381b3da4d7c6dfd0c7310dd._comment new file mode 100644 index 0000000000..e59aa65003 --- /dev/null +++ b/doc/tips/flickrannex/comment_11_ab5bcb025381b3da4d7c6dfd0c7310dd._comment @@ -0,0 +1,46 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawnaH44G3QbxBAYyDwy0PbvL0ls60XoaR3Y" + nickname="Nigel" + subject="git annex get failed" + date="2013-08-02T14:29:30Z" + content=""" +Hi, I am coming back to this and testing Flickr as a repository for moving files about and have run into what may be my very basic misunderstanding with vanilla annex. + +I copied one file to Flickr and dropped it elsewhere (--force). I assumed that the file was on Flickr ok but that the numcopies setting required the force because of the semi-trust level of the Flickr remote. + +Then I find I can't get the file back, even though there is a record of it from whereis. + +Can you help enlighten me as to what am I missing? I assumed whereis would only report files that exist and can be copied back. If not my error, I can raise bug or search for logs. Thanks in advance for any help. + +[[!format perl \"\"\" + + +nrb@nrb-ThinkPad-T61:~/tmp$ git annex whereis +whereis libpeerconnection.log (3 copies) + 31124688-0792-4214-9e00-7ed115aa6b8e -- flickr (the flickr library) + 3e3d40d7-de8f-4591-a4ab-747d74a3b278 -- origin (my laptop) + ec2d64fc-30d6-48b4-99bf-7b1bc22d420d -- portable USB drive +ok +whereis test.cgi (1 copy) + 31124688-0792-4214-9e00-7ed115aa6b8e -- flickr (the flickr library) +ok +whereis walkthrough.sh (3 copies) + 31124688-0792-4214-9e00-7ed115aa6b8e -- flickr (the flickr library) + 3e3d40d7-de8f-4591-a4ab-747d74a3b278 -- origin (my laptop) + ec2d64fc-30d6-48b4-99bf-7b1bc22d420d -- portable USB drive +ok +whereis walkthrough.sh~ (3 copies) + 31124688-0792-4214-9e00-7ed115aa6b8e -- flickr (the flickr library) + 3e3d40d7-de8f-4591-a4ab-747d74a3b278 -- origin (my laptop) + ec2d64fc-30d6-48b4-99bf-7b1bc22d420d -- portable USB drive +ok +nrb@nrb-ThinkPad-T61:~/tmp$ git annex get test.cgi +get test.cgi (from flickr...) + +git-annex: /home/nrb/tmp/.git/annex/tmp/SHA256E-s48--a01eedbee949120aeda41e566f9ae8faef1c2bacaa6d7bb8e45050fb8df6d09d.cgi: rename: does not exist (No such file or directory) +failed +git-annex: get: 1 failed +nrb@nrb-ThinkPad-T61:~/tmp$ + +\"\"\"]] +"""]] diff --git a/doc/tips/flickrannex/comment_12_90a331275d888221bc695003c8acbe46._comment b/doc/tips/flickrannex/comment_12_90a331275d888221bc695003c8acbe46._comment new file mode 100644 index 0000000000..003755f301 --- /dev/null +++ b/doc/tips/flickrannex/comment_12_90a331275d888221bc695003c8acbe46._comment @@ -0,0 +1,58 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawnaH44G3QbxBAYyDwy0PbvL0ls60XoaR3Y" + nickname="Nigel" + subject="re: git annex get failed" + date="2013-08-02T15:02:14Z" + content=""" +Another try - this time a slightly simpler setup using my version of the walkthrough commands + +[[!format bash \"\"\" + +nrb@nrb-ThinkPad-T61:~/repos/annex/laptop-annex$ git annex drop walkthrough.sh --from usbdrive +drop usbdrive walkthrough.sh ok +(Recording state in git...) +nrb@nrb-ThinkPad-T61:~/repos/annex/laptop-annex$ git annex move walkthrough.sh --to flickr +move walkthrough.sh (gpg) (checking flickr...) (to flickr...) +/home/nrb/repos/gits/flickrannex/flickrannex.py:92: FutureWarning: The behavior of this method will change in future versions. Use specific 'len(elem)' or 'elem is not None' test instead. + if res: +/home/nrb/repos/gits/flickrannex/flickrannex.py:100: FutureWarning: The behavior of this method will change in future versions. Use specific 'len(elem)' or 'elem is not None' test instead. + if res: +ok +(Recording state in git...) +nrb@nrb-ThinkPad-T61:~/repos/annex/laptop-annex$ git annex whereis +whereis walkthrough.sh (1 copy) + 161b7af0-2075-4314-9767-308a49b86018 -- flickr (the flickr library) +ok +whereis walkthrough.sh~ (3 copies) + 161b7af0-2075-4314-9767-308a49b86018 -- flickr (the flickr library) + 7803d853-d231-4bb4-b696-f12a950fb96b -- here (my laptop) + d60d75f9-d878-4214-af20-fa055134ae77 -- usbdrive (portable USB drive) +ok +nrb@nrb-ThinkPad-T61:~/repos/annex/laptop-annex$ git annex get walkthrough.sh +get walkthrough.sh (from flickr...) (gpg) +git-annex: /home/nrb/repos/annex/laptop-annex/.git/annex/tmp/GPGHMACSHA1--02f600d7e8b071d2945270fd5e7fc26dd066ff31: openBinaryFile: does not exist (No such file or directory) +gpg: decrypt_message failed: eof + + Unable to access these remotes: flickr + + Try making some of these repositories available: + 161b7af0-2075-4314-9767-308a49b86018 -- flickr (the flickr library) +failed +git-annex: get: 1 failed +nrb@nrb-ThinkPad-T61:~/repos/annex/laptop-annex$ git annex fsck --from flickr +fsck walkthrough.sh (gpg) (checking flickr...) (fixing location log) + ** Based on the location log, walkthrough.sh + ** was expected to be present, but its content is missing. + + ** No known copies exist of walkthrough.sh +failed +fsck walkthrough.sh~ (checking flickr...) (fixing location log) + ** Based on the location log, walkthrough.sh~ + ** was expected to be present, but its content is missing. +failed +(Recording state in git...) +git-annex: fsck: 2 failed +nrb@nrb-ThinkPad-T61:~/repos/annex/laptop-annex$ + +\"\"\" ]] +"""]] diff --git a/doc/tips/flickrannex/comment_13_cf9dad91ee7d334c720adb3310aa0003._comment b/doc/tips/flickrannex/comment_13_cf9dad91ee7d334c720adb3310aa0003._comment new file mode 100644 index 0000000000..71d44aff56 --- /dev/null +++ b/doc/tips/flickrannex/comment_13_cf9dad91ee7d334c720adb3310aa0003._comment @@ -0,0 +1,130 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawnaH44G3QbxBAYyDwy0PbvL0ls60XoaR3Y" + nickname="Nigel" + subject="re: git annex get failed -- debug" + date="2013-08-02T15:28:41Z" + content=""" +With debug turned on. + +[[!format bash \"\"\" + +initremote flickr (encryption setup) (shared cipher) ok +(Recording state in git...) +describe flickr ok +(Recording state in git...) +/home/nrb/repos/annex/laptop-annex +fsck walkthrough.sh (checksum...) ok +fsck walkthrough.sh~ (checksum...) ok +/home/nrb/repos/annex/laptop-annex +copy walkthrough.sh (gpg) (checking flickr...) 16:18:52 [flickrannex-0.1.5] : 'START' +16:18:52 [flickrannex-0.1.5] main : 'ARGS: 'ANNEX_ACTION=checkpresent ANNEX_KEY=GPGHMACSHA1--280dd2d5003ad3962b1ecaa52ba45fdd44381fd0 ANNEX_HASH_1=kQ ANNEX_HASH_2=0P /home/nrb/repos/gits/flickrannex/flickrannex.py --dbglevel 1 --stderr'' +16:18:52 [flickrannex-0.1.5] readFile : ''/home/nrb/repos/gits/flickrannex/flickrannex.conf' - 'r'' +16:18:52 [flickrannex-0.1.5] readFile : 'Done' +16:18:52 [flickrannex-0.1.5] login : 'nrbray@yahoo.com' +16:18:54 [flickrannex-0.1.5] login : 'Done: '72157633920418017-5c0274bd421d7bb1' - None - '8086216@N08'' +16:18:54 [flickrannex-0.1.5] main : 'Trying page: 1' +16:18:55 [flickrannex-0.1.5] main : 'Error. found nothing:{'pages': '1', 'cancreate': '1', 'total': '0', 'page': '1', 'perpage': '0'}' +16:18:55 [flickrannex-0.1.5] checkFile : 'GPGHMACSHA1--280dd2d5003ad3962b1ecaa52ba45fdd44381fd0 - u'gitannex' - '8086216@N08'' +16:18:55 [flickrannex-0.1.5] checkFile : 'No set exists, thus no files exists' +16:18:55 [flickrannex-0.1.5] : 'STOP: 2s' +(to flickr...) +16:18:55 [flickrannex-0.1.5] : 'START' +16:18:55 [flickrannex-0.1.5] main : 'ARGS: 'ANNEX_ACTION=store ANNEX_KEY=GPGHMACSHA1--280dd2d5003ad3962b1ecaa52ba45fdd44381fd0 ANNEX_HASH_1=kQ ANNEX_HASH_2=0P ANNEX_FILE=/home/nrb/repos/annex/laptop-annex/.git/annex/tmp/GPGHMACSHA1--280dd2d5003ad3962b1ecaa52ba45fdd44381fd0 /home/nrb/repos/gits/flickrannex/flickrannex.py --dbglevel 1 --stderr'' +16:18:55 [flickrannex-0.1.5] readFile : ''/home/nrb/repos/gits/flickrannex/flickrannex.conf' - 'r'' +16:18:55 [flickrannex-0.1.5] readFile : 'Done' +16:18:55 [flickrannex-0.1.5] login : 'nrbray@yahoo.com' +16:18:57 [flickrannex-0.1.5] login : 'Done: '72157633920418017-5c0274bd421d7bb1' - None - '8086216@N08'' +16:18:58 [flickrannex-0.1.5] main : 'Trying page: 1' +16:18:58 [flickrannex-0.1.5] main : 'Error. found nothing:{'pages': '1', 'cancreate': '1', 'total': '0', 'page': '1', 'perpage': '0'}' +16:18:58 [flickrannex-0.1.5] postFile : '/home/nrb/repos/annex/laptop-annex/.git/annex/tmp/GPGHMACSHA1--280dd2d5003ad3962b1ecaa52ba45fdd44381fd0 to u'gitannex' - GPGHMACSHA1--280dd2d5003ad3962b1ecaa52ba45fdd44381fd0' +16:18:58 [flickrannex-0.1.5] postFile : 'pre /home/nrb/repos/annex/laptop-annex/.git/annex/tmp/GPGHMACSHA1--280dd2d5003ad3962b1ecaa52ba45fdd44381fd0 size: 1047 more than 40234050.' +16:18:58 [flickrannex-0.1.5] readFile : ''/home/nrb/repos/annex/laptop-annex/.git/annex/tmp/GPGHMACSHA1--280dd2d5003ad3962b1ecaa52ba45fdd44381fd0' - 'rb'' +16:18:58 [flickrannex-0.1.5] readFile : 'Done' +16:18:58 [flickrannex-0.1.5] postFile : 'Uploading: /home/nrb/repos/gits/flickrannex/temp/encoded-GPGHMACSHA1--280dd2d5003ad3962b1ecaa52ba45fdd44381fd0' +/home/nrb/repos/gits/flickrannex/flickrannex.py:92: FutureWarning: The behavior of this method will change in future versions. Use specific 'len(elem)' or 'elem is not None' test instead. + if res: +/home/nrb/repos/gits/flickrannex/flickrannex.py:100: FutureWarning: The behavior of this method will change in future versions. Use specific 'len(elem)' or 'elem is not None' test instead. + if res: +16:19:01 [flickrannex-0.1.5] postFile : 'Done: ' +16:19:01 [flickrannex-0.1.5] : 'STOP: 5s' +ok +copy walkthrough.sh~ (checking flickr...) 16:19:01 [flickrannex-0.1.5] : 'START' +16:19:01 [flickrannex-0.1.5] main : 'ARGS: 'ANNEX_ACTION=checkpresent ANNEX_KEY=GPGHMACSHA1--131f95d3bc932d23ef6af47cf49db3c04be4f0f9 ANNEX_HASH_1=m5 ANNEX_HASH_2=kz /home/nrb/repos/gits/flickrannex/flickrannex.py --dbglevel 1 --stderr'' +16:19:01 [flickrannex-0.1.5] readFile : ''/home/nrb/repos/gits/flickrannex/flickrannex.conf' - 'r'' +16:19:01 [flickrannex-0.1.5] readFile : 'Done' +16:19:01 [flickrannex-0.1.5] login : 'nrbray@yahoo.com' +16:19:03 [flickrannex-0.1.5] login : 'Done: '72157633920418017-5c0274bd421d7bb1' - None - '8086216@N08'' +16:19:03 [flickrannex-0.1.5] main : 'Photoset gitannex found: ' +16:19:03 [flickrannex-0.1.5] main : 'Trying page: 1' +16:19:03 [flickrannex-0.1.5] checkFile : 'GPGHMACSHA1--131f95d3bc932d23ef6af47cf49db3c04be4f0f9 - 72157634897264995L - '8086216@N08'' +16:19:03 [flickrannex-0.1.5] checkFile : 'No set exists, thus no files exists' +16:19:03 [flickrannex-0.1.5] : 'STOP: 1s' +(to flickr...) +16:19:03 [flickrannex-0.1.5] : 'START' +16:19:03 [flickrannex-0.1.5] main : 'ARGS: 'ANNEX_ACTION=store ANNEX_KEY=GPGHMACSHA1--131f95d3bc932d23ef6af47cf49db3c04be4f0f9 ANNEX_HASH_1=m5 ANNEX_HASH_2=kz ANNEX_FILE=/home/nrb/repos/annex/laptop-annex/.git/annex/tmp/GPGHMACSHA1--131f95d3bc932d23ef6af47cf49db3c04be4f0f9 /home/nrb/repos/gits/flickrannex/flickrannex.py --dbglevel 1 --stderr'' +16:19:03 [flickrannex-0.1.5] readFile : ''/home/nrb/repos/gits/flickrannex/flickrannex.conf' - 'r'' +16:19:03 [flickrannex-0.1.5] readFile : 'Done' +16:19:03 [flickrannex-0.1.5] login : 'nrbray@yahoo.com' +16:19:05 [flickrannex-0.1.5] login : 'Done: '72157633920418017-5c0274bd421d7bb1' - None - '8086216@N08'' +16:19:05 [flickrannex-0.1.5] main : 'Photoset gitannex found: ' +16:19:05 [flickrannex-0.1.5] main : 'Trying page: 1' +16:19:05 [flickrannex-0.1.5] postFile : '/home/nrb/repos/annex/laptop-annex/.git/annex/tmp/GPGHMACSHA1--131f95d3bc932d23ef6af47cf49db3c04be4f0f9 to 72157634897264995L - GPGHMACSHA1--131f95d3bc932d23ef6af47cf49db3c04be4f0f9' +16:19:05 [flickrannex-0.1.5] postFile : 'pre /home/nrb/repos/annex/laptop-annex/.git/annex/tmp/GPGHMACSHA1--131f95d3bc932d23ef6af47cf49db3c04be4f0f9 size: 1044 more than 40234050.' +16:19:05 [flickrannex-0.1.5] readFile : ''/home/nrb/repos/annex/laptop-annex/.git/annex/tmp/GPGHMACSHA1--131f95d3bc932d23ef6af47cf49db3c04be4f0f9' - 'rb'' +16:19:05 [flickrannex-0.1.5] readFile : 'Done' +16:19:05 [flickrannex-0.1.5] postFile : 'Uploading: /home/nrb/repos/gits/flickrannex/temp/encoded-GPGHMACSHA1--131f95d3bc932d23ef6af47cf49db3c04be4f0f9' +/home/nrb/repos/gits/flickrannex/flickrannex.py:92: FutureWarning: The behavior of this method will change in future versions. Use specific 'len(elem)' or 'elem is not None' test instead. + if res: +/home/nrb/repos/gits/flickrannex/flickrannex.py:100: FutureWarning: The behavior of this method will change in future versions. Use specific 'len(elem)' or 'elem is not None' test instead. + if res: +16:19:08 [flickrannex-0.1.5] postFile : 'Done: ' +16:19:08 [flickrannex-0.1.5] : 'STOP: 4s' +ok +(Recording state in git...) +nrb@nrb-ThinkPad-T61:~/repos/annex/laptop-annex$ git annex whereis +whereis walkthrough.sh (3 copies) + 86491ded-899c-425d-9470-bf446cb06db1 -- flickr (the flickr library) + 8e766014-7154-4f4f-a04b-9d1b3d333db1 -- here (my laptop) + eed7055b-743b-4ab6-a390-29cfd326005d -- usbdrive (portable USB drive) +ok +whereis walkthrough.sh~ (3 copies) + 86491ded-899c-425d-9470-bf446cb06db1 -- flickr (the flickr library) + 8e766014-7154-4f4f-a04b-9d1b3d333db1 -- here (my laptop) + eed7055b-743b-4ab6-a390-29cfd326005d -- usbdrive (portable USB drive) +ok +nrb@nrb-ThinkPad-T61:~/repos/annex/laptop-annex$ git annex fsck --from flickr +fsck walkthrough.sh (gpg) (checking flickr...) 16:22:57 [flickrannex-0.1.5] : 'START' +16:22:57 [flickrannex-0.1.5] main : 'ARGS: 'ANNEX_ACTION=checkpresent ANNEX_KEY=GPGHMACSHA1--280dd2d5003ad3962b1ecaa52ba45fdd44381fd0 ANNEX_HASH_1=kQ ANNEX_HASH_2=0P /home/nrb/repos/gits/flickrannex/flickrannex.py --dbglevel 1 --stderr'' +16:22:57 [flickrannex-0.1.5] readFile : ''/home/nrb/repos/gits/flickrannex/flickrannex.conf' - 'r'' +16:22:57 [flickrannex-0.1.5] readFile : 'Done' +16:22:57 [flickrannex-0.1.5] login : 'nrbray@yahoo.com' +16:22:58 [flickrannex-0.1.5] login : 'Done: '72157633920418017-5c0274bd421d7bb1' - None - '8086216@N08'' +16:22:59 [flickrannex-0.1.5] main : 'Photoset gitannex found: ' +16:22:59 [flickrannex-0.1.5] main : 'Trying page: 1' +16:22:59 [flickrannex-0.1.5] checkFile : 'GPGHMACSHA1--280dd2d5003ad3962b1ecaa52ba45fdd44381fd0 - 72157634897264995L - '8086216@N08'' +16:22:59 [flickrannex-0.1.5] checkFile : 'No set exists, thus no files exists' +16:22:59 [flickrannex-0.1.5] : 'STOP: 2s' +(fixing location log) + ** Based on the location log, walkthrough.sh + ** was expected to be present, but its content is missing. +failed +fsck walkthrough.sh~ (checking flickr...) 16:22:59 [flickrannex-0.1.5] : 'START' +16:22:59 [flickrannex-0.1.5] main : 'ARGS: 'ANNEX_ACTION=checkpresent ANNEX_KEY=GPGHMACSHA1--131f95d3bc932d23ef6af47cf49db3c04be4f0f9 ANNEX_HASH_1=m5 ANNEX_HASH_2=kz /home/nrb/repos/gits/flickrannex/flickrannex.py --dbglevel 1 --stderr'' +16:22:59 [flickrannex-0.1.5] readFile : ''/home/nrb/repos/gits/flickrannex/flickrannex.conf' - 'r'' +16:22:59 [flickrannex-0.1.5] readFile : 'Done' +16:22:59 [flickrannex-0.1.5] login : 'nrbray@yahoo.com' +16:23:01 [flickrannex-0.1.5] login : 'Done: '72157633920418017-5c0274bd421d7bb1' - None - '8086216@N08'' +16:23:01 [flickrannex-0.1.5] main : 'Photoset gitannex found: ' +16:23:01 [flickrannex-0.1.5] main : 'Trying page: 1' +16:23:01 [flickrannex-0.1.5] checkFile : 'GPGHMACSHA1--131f95d3bc932d23ef6af47cf49db3c04be4f0f9 - 72157634897264995L - '8086216@N08'' +16:23:01 [flickrannex-0.1.5] checkFile : 'No set exists, thus no files exists' +16:23:01 [flickrannex-0.1.5] : 'STOP: 1s' +(fixing location log) + ** Based on the location log, walkthrough.sh~ + ** was expected to be present, but its content is missing. +failed +(Recording state in git...) +git-annex: fsck: 2 failed + +\"\"\" ]] +"""]] diff --git a/doc/tips/flickrannex/comment_2_d74c4fc7edf8e47f7482564ce0ef4d12._comment b/doc/tips/flickrannex/comment_2_d74c4fc7edf8e47f7482564ce0ef4d12._comment new file mode 100644 index 0000000000..d015dc1955 --- /dev/null +++ b/doc/tips/flickrannex/comment_2_d74c4fc7edf8e47f7482564ce0ef4d12._comment @@ -0,0 +1,10 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawmkBwMWvNKZZCge_YqobCSILPMeK6xbFw8" + nickname="develop" + subject="comment 2" + date="2013-06-05T21:33:42Z" + content=""" +Get the statically linked version from here http://git-annex.branchable.com/install/Linux_standalone/ + +I believe the new hook format was introduced in version 4.20130521 +"""]] diff --git a/doc/tips/flickrannex/comment_2_f53d0d5520e2835e9705bea4e75556f0._comment b/doc/tips/flickrannex/comment_2_f53d0d5520e2835e9705bea4e75556f0._comment new file mode 100644 index 0000000000..14d7a1b7c4 --- /dev/null +++ b/doc/tips/flickrannex/comment_2_f53d0d5520e2835e9705bea4e75556f0._comment @@ -0,0 +1,30 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawnaH44G3QbxBAYyDwy0PbvL0ls60XoaR3Y" + nickname="Nigel" + subject="missing configuration for flickr-checkpresent-hook" + date="2013-06-05T20:44:25Z" + content=""" + + +9 days ago: [the annex] \"hook format a few versions ago, and this is using the new hook format\". + +Looks very handy. I am just starting with this, but can't seem to get it working as a remote after following the simple walkthrough. All goes well until: + + $ git annex copy . --to flickr + copy walkthrough.sh (checking flickr...) + missing configuration for flickr-checkpresent-hook + git-annex: checkpresent hook misconfigured + +my Ubuntu 12.04: + + $ git annex version + git-annex version: 4.20130516.1 + build flags: Assistant Webapp Pairing Testsuite S3 WebDAV Inotify DBus XMPP + local repository version: 3 + default repository version: 3 + supported repository versions: 3 4 + upgrade supported from repository versions: 0 1 2 + +I guess my \"git-annex version is still too old\"? Any idea what version is needed? Even better if I can figure out which Linux distribution/release has the most up to date version of annex. + +"""]] diff --git a/doc/tips/flickrannex/comment_4_9ebba4d61140f6c2071e988c9328cf7e._comment b/doc/tips/flickrannex/comment_4_9ebba4d61140f6c2071e988c9328cf7e._comment new file mode 100644 index 0000000000..741b0c5bac --- /dev/null +++ b/doc/tips/flickrannex/comment_4_9ebba4d61140f6c2071e988c9328cf7e._comment @@ -0,0 +1,14 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawmkBwMWvNKZZCge_YqobCSILPMeK6xbFw8" + nickname="develop" + subject="comment 4" + date="2013-06-05T22:02:29Z" + content=""" +The path for the binary \"/usr/bin/python2\" is wrong. + +It could be any of /usr/bin/python /usr/bin/python2.6 /usr/bin/python2.7 + +Or maybe in /usr/local/bin + +you can try running \"which python\" or \"which python2\" to get the real path. +"""]] diff --git a/doc/tips/flickrannex/comment_5_4470dae270613dd8712623474bc80ab0._comment b/doc/tips/flickrannex/comment_5_4470dae270613dd8712623474bc80ab0._comment new file mode 100644 index 0000000000..1c19711df9 --- /dev/null +++ b/doc/tips/flickrannex/comment_5_4470dae270613dd8712623474bc80ab0._comment @@ -0,0 +1,24 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawnaH44G3QbxBAYyDwy0PbvL0ls60XoaR3Y" + nickname="Nigel" + subject="missing configuration for flickr-checkpresent-hook" + date="2013-06-05T22:00:48Z" + content=""" +Many thanks. + +I used gitannex-install and was left with a slight anomaly: + + Installing...........done + git-annex version 4.20130601 has been installed + $ git-annex version + git-annex version: 4.20130531-g5df09b5 + +But I guess this includes the new hook format. I get a bit further: + + $ git annex copy . --to flickr + copy walkthrough.sh (checking flickr...) (user error (sh [\"-c\",\"/usr/bin/python2 /home/nrb/repos/gits/flickrannex/flickrannex.py\"] exited 1)) failed + copy walkthrough.sh~ (checking flickr...) (user error (sh [\"-c\",\"/usr/bin/python2 /home/nrb/repos/gits/flickrannex/flickrannex.py\"] exited 1)) failed + git-annex: copy: 2 failed + + +"""]] diff --git a/doc/tips/flickrannex/comment_5_d395cdcf815cb430e374ff05c1a63ff4._comment b/doc/tips/flickrannex/comment_5_d395cdcf815cb430e374ff05c1a63ff4._comment new file mode 100644 index 0000000000..dbeaafb734 --- /dev/null +++ b/doc/tips/flickrannex/comment_5_d395cdcf815cb430e374ff05c1a63ff4._comment @@ -0,0 +1,17 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawnaH44G3QbxBAYyDwy0PbvL0ls60XoaR3Y" + nickname="Nigel" + subject="comment 5" + date="2013-06-05T22:11:14Z" + content=""" +Thanks, but on my machine I get: + + $ which python2 + /usr/bin/python2 + +I have scripted all my walkthrough commands, blowing away the test repositories and flickr settings first each time. This re-runs the flickr scripts and git config annex.flickr-hook etc. + +I can't spot anything here. + + +"""]] diff --git a/doc/tips/flickrannex/comment_6_8cf730097001ffe106f2c743edce9d0a._comment b/doc/tips/flickrannex/comment_6_8cf730097001ffe106f2c743edce9d0a._comment new file mode 100644 index 0000000000..8e7b15ed0a --- /dev/null +++ b/doc/tips/flickrannex/comment_6_8cf730097001ffe106f2c743edce9d0a._comment @@ -0,0 +1,12 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawmWg4VvDTer9f49Y3z-R0AH16P4d1ygotA" + nickname="Tobias" + subject="comment 6" + date="2013-06-06T09:44:11Z" + content=""" +That's weird... + +You could try adding \"--dbglevel 1 --stderr\" arguments to the hook command and give me the output. But the way i read the log it seems like it doesn't even launch the python intrepreter. I might be wrong though. + + +"""]] diff --git a/doc/tips/flickrannex/comment_7_a80c8087c4e1562a4c98a24edc182e5a._comment b/doc/tips/flickrannex/comment_7_a80c8087c4e1562a4c98a24edc182e5a._comment new file mode 100644 index 0000000000..9e0eb0a73f --- /dev/null +++ b/doc/tips/flickrannex/comment_7_a80c8087c4e1562a4c98a24edc182e5a._comment @@ -0,0 +1,12 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawnaH44G3QbxBAYyDwy0PbvL0ls60XoaR3Y" + nickname="Nigel" + subject="Unencrypted flickr can only accept picture and video files" + date="2013-06-06T10:24:58Z" + content=""" +Thanks and sorry to trouble you, it is my error, I picked unencrypted option (thinking it would be less of an issue) and am using a text file for test, gave an error line: + + 10:53:07 [flickrannex-0.1.5] main : 'Unencrypted flickr can only accept picture and video files' + +I've not looked through your code yet, but could that message be printed when not in debug mode? +"""]] diff --git a/doc/tips/flickrannex/comment_8_94f84254c32cf0f7dd1441b7da5d2bc6._comment b/doc/tips/flickrannex/comment_8_94f84254c32cf0f7dd1441b7da5d2bc6._comment new file mode 100644 index 0000000000..ff11a618a5 --- /dev/null +++ b/doc/tips/flickrannex/comment_8_94f84254c32cf0f7dd1441b7da5d2bc6._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawmWg4VvDTer9f49Y3z-R0AH16P4d1ygotA" + nickname="Tobias" + subject="comment 8" + date="2013-06-06T10:51:39Z" + content=""" +I'll make it so, in the next version i push. +"""]] diff --git a/doc/tips/flickrannex/comment_9_5299b4cab4a4cb8e8fd4d2b39f0ea59c._comment b/doc/tips/flickrannex/comment_9_5299b4cab4a4cb8e8fd4d2b39f0ea59c._comment new file mode 100644 index 0000000000..f25cd04c14 --- /dev/null +++ b/doc/tips/flickrannex/comment_9_5299b4cab4a4cb8e8fd4d2b39f0ea59c._comment @@ -0,0 +1,9 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawleVyKk2kQsB_HgEdS7w1s0BmgRGy1aay0" + nickname="Milan" + subject="chunksize" + date="2013-06-07T09:09:56Z" + content=""" +Hi! Does this backend support chunksize option? If yes, is it possible to set it after the remote has been added to the repository? +Thanks, Milan. +"""]] diff --git a/doc/tips/googledriveannex.mdwn b/doc/tips/googledriveannex.mdwn new file mode 100644 index 0000000000..abecf10cf3 --- /dev/null +++ b/doc/tips/googledriveannex.mdwn @@ -0,0 +1,28 @@ +googledriveannex +========= + +Hook program for gitannex to use Google Drive as backend + +# Requirements: + + python2 + +Credit for the googledrive api interface goes to google + +## Install +Clone the git repository in your home folder. + + git clone git://github.com/TobiasTheViking/googledriveannex.git + +This should make a ~/googledriveannex folder + +## Setup +Run the program once to make an empty config file + + cd ~/googledriveannex; python2 googledriveannex.py + +## Commands for gitannex: + + git config annex.googledrive-hook '/usr/bin/python2 ~/googledriveannex/googledriveannex.py' + git annex initremote googledrive type=hook hooktype=googledrive encryption=shared + git annex describe googledrive "the googledrive library" diff --git a/doc/tips/megaannex.mdwn b/doc/tips/megaannex.mdwn new file mode 100644 index 0000000000..8edbf4421c --- /dev/null +++ b/doc/tips/megaannex.mdwn @@ -0,0 +1,41 @@ +[Megaannex](https://github.com/TobiasTheViking/megaannex) +is a hook program for git-annex to use mega.co.nz as backend + +# Requirements: + + python2 + requests>=0.10 + pycrypto + +Credit for the mega api interface goes to: + + +## Install + +Clone the git repository in your home folder. + + git clone git://github.com/TobiasTheViking/megaannex.git + +This should make a ~/megannex folder + +## Setup + +Run the program once to make an empty config file. + + cd ~/megaannex; python2 megaannex.py + +Edit the megaannex.conf file. Add your mega.co.nz username, password, and folder name. + +## Configuring git-annex + + git config annex.mega-hook '/usr/bin/python2 ~/megaannex/megaannex.py' + + git annex initremote mega type=hook hooktype=mega encryption=shared + git annex describe mega "the mega.co.nz library" + +## Notes + +You may need to use a different command than "python2", depending +on your python installation. + +-- Tobias diff --git a/doc/tips/migrating_data_to_a_new_backend.mdwn b/doc/tips/migrating_data_to_a_new_backend.mdwn new file mode 100644 index 0000000000..b9acb8bd15 --- /dev/null +++ b/doc/tips/migrating_data_to_a_new_backend.mdwn @@ -0,0 +1,16 @@ +Maybe you started out using the WORM backend, and have now configured +git-annex to use SHA1. But files you added to the annex before still +use the WORM backend. There is a simple command that can migrate that +data: + + # git annex migrate my_cool_big_file + migrate my_cool_big_file (checksum...) ok + +You can only migrate files whose content is currently available. Other +files will be skipped. + +After migrating a file to a new backend, the old content in the old backend +will still be present. That is necessary because multiple files +can point to the same content. The `git annex unused` subcommand can be +used to clear up that detritus later. Note that hard links are used, +to avoid wasting disk space. diff --git a/doc/tips/owncloudannex.mdwn b/doc/tips/owncloudannex.mdwn new file mode 100644 index 0000000000..ad40c67e3e --- /dev/null +++ b/doc/tips/owncloudannex.mdwn @@ -0,0 +1,28 @@ +owncloudannex +========= + +Hook program for gitannex to use owncloud as backend + +# Requirements: + + python2 + +Credit for the Owncloud api interface goes to me. + +# Install +Clone the git repository in your home folder. + + git clone git://github.com/TobiasTheViking/owncloudannex.git + +This should make a ~/owncloudannex folder + +# Setup +Run the program once to set it up. + + cd ~/owncloudannex; python2 owncloudannex.py + +# Commands for gitannex: + + git config annex.owncloud-hook '/usr/bin/python2 ~/owncloudannex/owncloudannex.py' + git annex initremote owncloud type=hook hooktype=owncloud encryption=shared + git annex describe owncloud "the owncloud library" diff --git a/doc/tips/owncloudannex/comment_1_129652308c3c499462828dcaf8e747a4._comment b/doc/tips/owncloudannex/comment_1_129652308c3c499462828dcaf8e747a4._comment new file mode 100644 index 0000000000..47e33042ec --- /dev/null +++ b/doc/tips/owncloudannex/comment_1_129652308c3c499462828dcaf8e747a4._comment @@ -0,0 +1,40 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawkQvRLq7nMMEGoEKuYx9oaf67IC0nZfmVI" + nickname="chung yan" + subject="owncloud hook exited nonzero" + date="2013-07-10T05:03:47Z" + content=""" +hi + +I got the above message, and cannot sync to my owncloud. Here is the log file i had: + +>[2013-07-10 12:30:31 HKT] main: starting assistant version 4.20130627 +(scanning...) [2013-07-10 12:30:31 HKT] Watcher: Performing startup scan +(started...) git-annex: Daemon is already running. +git-annex: Daemon is already running. +[2013-07-10 12:44:04 HKT] Committer: Adding sync2Servers.sh + +>add syncByRsync/sync2Servers.sh (checksum...) [2013-07-10 12:44:04 HKT] Committer: Committing changes to git +(gpg) + +>owncloud hook exited nonzero! +>git-annex: Daemon is already running. +[2013-07-10 12:51:07 HKT] main: Syncing with owncloud + +>owncloud hook exited nonzero! +>[2013-07-10 12:53:10 HKT] Committer: Adding 2 files +ok +(Recording state in git...) +(Recording state in git...) +add syncByRsync/DSCN1810.JPG (checksum...) ok +add syncByRsync/DSCN1810.JPG (checksum...) [2013-07-10 12:53:10 HKT] Committer: Committing changes to git + +>owncloud hook exited nonzero! +[2013-07-10 12:53:50 HKT] main: Syncing with owncloud + owncloud hook exited nonzero! + owncloud hook exited nonzero! + +thanks + +yan +"""]] diff --git a/doc/tips/owncloudannex/comment_2_38604990368666f654d41891ba99ac61._comment b/doc/tips/owncloudannex/comment_2_38604990368666f654d41891ba99ac61._comment new file mode 100644 index 0000000000..6ea0b033c0 --- /dev/null +++ b/doc/tips/owncloudannex/comment_2_38604990368666f654d41891ba99ac61._comment @@ -0,0 +1,15 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawmLB39PC89rfGaA8SwrsnB6tbumezj-aC0" + nickname="Tobias" + subject="comment 2" + date="2013-07-10T08:21:39Z" + content=""" +Personally i've only seen that when the server ran out of space. But lets see what is going on. + +Please run this command + +git config annex.owncloud-hook '/usr/bin/python2 ~/owncloudannex/owncloudannex.py --dbglevel 1 --stderr' + +And then replicate the error. It should give me some debug information to work with. + +"""]] diff --git a/doc/tips/owncloudannex/comment_3_1bfd290d00d6536da7d31818db46f8ec._comment b/doc/tips/owncloudannex/comment_3_1bfd290d00d6536da7d31818db46f8ec._comment new file mode 100644 index 0000000000..f7e3f0ee4d --- /dev/null +++ b/doc/tips/owncloudannex/comment_3_1bfd290d00d6536da7d31818db46f8ec._comment @@ -0,0 +1,87 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawkQvRLq7nMMEGoEKuYx9oaf67IC0nZfmVI" + nickname="chung yan" + subject="comment 3" + date="2013-07-10T08:46:27Z" + content=""" +hi Tobias + +Thanks your suggestion for list of my error log, here it is: + +[2013-07-10 16:27:58 HKT] main: starting assistant version 4.20130627 +(scanning...) [2013-07-10 16:27:58 HKT] Watcher: Performing startup scan +(started...) [2013-07-10 16:28:14 HKT] Committer: Adding 2 files + +add syncByRsync/sync2Servers.sh (checksum...) ok +add syncByRsync/sync2Servers.sh (checksum...) [2013-07-10 16:28:14 HKT] Committer: Committing changes to git +[2013-07-10 16:31:12 HKT] Watcher: add direct DSCN1810.JPG +[2013-07-10 16:31:12 HKT] read: lsof [\"-F0can\",\"+d\",\"/home/yan/annex_testing/.git/annex/tmp/\"] +[2013-07-10 16:31:12 HKT] Committer: Adding DSCN1810.JPG +ok +(Recording state in git...) +(Recording state in git...) +add DSCN1810.JPG [2013-07-10 16:31:12 HKT](checksum...) Watcher: add direct DSCN1810.JPG +[2013-07-10 16:31:12 HKT] read: sha256sum [\"/home/yan/annex_testing/.git/annex/tmp/DSCN181012023.JPG\"] + + DSCN1810.JPG changed while it was being added +[2013-07-10 16:31:12 HKT] Committer: delaying commit of 1 changes +[2013-07-10 16:31:13 HKT] read: lsof [\"-F0can\",\"+d\",\"/home/yan/annex_testing/.git/annex/tmp/\"] +[2013-07-10 16:31:13 HKT] Committer: Adding 2 files +failed +add DSCN1810.JPG (checksum...) [2013-07-10 16:31:13 HKT] read: sha256sum [\"/home/yan/annex_testing/.git/annex/tmp/DSCN181012023.JPG\"] +[2013-07-10 16:31:13 HKT] chat: git [\"--git-dir=/home/yan/annex_testing/.git\",\"--work-tree=/home/yan/annex_testing\",\"hash-object\",\"-t\",\"blob\",\"-w\",\"--stdin\",\"--no-filters\"] +ok +add DSCN1810.JPG (checksum...) [2013-07-10 16:31:13 HKT] read: sha256sum [\"/home/yan/annex_testing/.git/annex/tmp/DSCN181012024.JPG\"] +[2013-07-10 16:31:13 HKT] chat: git [\"--git-dir=/home/yan/annex_testing/.git\",\"--work-tree=/home/yan/annex_testing\",\"hash-object\",\"-t\",\"blob\",\"-w\",\"--stdin\",\"--no-filters\"] +[2013-07-10 16:31:13 HKT] Committer: committing 2 changes +[2013-07-10 16:31:13 HKT] Committer: Committing changes to git +[2013-07-10 16:31:13 HKT] feed: git [\"--git-dir=/home/yan/annex_testing/.git\",\"--work-tree=/home/yan/annex_testing\",\"update-index\",\"-z\",\"--index-info\"] +[2013-07-10 16:31:13 HKT] read: git [\"--git-dir=/home/yan/annex_testing/.git\",\"--work-tree=/home/yan/annex_testing\",\"commit\",\"--allow-empty-message\",\"--no-edit\",\"-m\",\"\",\"--quiet\",\"--no-verify\"] +[2013-07-10 16:31:13 HKT] Committer: queued Upload UUID \"df02d32a-7e3a-4e12-a417-7f1d1a1cf1a6\" DSCN1810.JPG Nothing : new file created +[2013-07-10 16:31:13 HKT] chat: git [\"--git-dir=/home/yan/annex_testing/.git\",\"--work-tree=/home/yan/annex_testing\",\"hash-object\",\"-w\",\"--stdin-paths\",\"--no-filters\"] +[2013-07-10 16:31:13 HKT] feed: git [\"--git-dir=/home/yan/annex_testing/.git\",\"--work-tree=/home/yan/annex_testing\",\"update-index\",\"-z\",\"--index-info\"] +[2013-07-10 16:31:13 HKT] read: git [\"--git-dir=/home/yan/annex_testing/.git\",\"--work-tree=/home/yan/annex_testing\",\"show-ref\",\"--hash\",\"refs/heads/git-annex\"] +[2013-07-10 16:31:13 HKT] read: git [\"--git-dir=/home/yan/annex_testing/.git\",\"--work-tree=/home/yan/annex_testing\",\"write-tree\"] +[2013-07-10 16:31:13 HKT] chat: git [\"--git-dir=/home/yan/annex_testing/.git\",\"--work-tree=/home/yan/annex_testing\",\"commit-tree\",\"a8337a989b58b29eee5fd2fa7a4c0b8ec45d5e59\",\"-p\",\"refs/heads/git-annex\"] +[2013-07-10 16:31:13 HKT] call: git [\"--git-dir=/home/yan/annex_testing/.git\",\"--work-tree=/home/yan/annex_testing\",\"update-ref\",\"refs/heads/git-annex\",\"267bf35da4d9abe5ed7fe82ea5df8a8df2ddf940\"] +[2013-07-10 16:31:13 HKT] read: git [\"--git-dir=/home/yan/annex_testing/.git\",\"--work-tree=/home/yan/annex_testing\",\"symbolic-ref\",\"HEAD\"] +[2013-07-10 16:31:13 HKT] read: git [\"--git-dir=/home/yan/annex_testing/.git\",\"--work-tree=/home/yan/annex_testing\",\"show-ref\",\"refs/heads/master\"] +[2013-07-10 16:31:13 HKT] Transferrer: Transferring: Upload UUID \"df02d32a-7e3a-4e12-a417-7f1d1a1cf1a6\" DSCN1810.JPG Nothing +[2013-07-10 16:31:13 HKT] read: git [\"--git-dir=/home/yan/annex_testing/.git\",\"--work-tree=/home/yan/annex_testing\",\"show-ref\",\"git-annex\"] +[2013-07-10 16:31:13 HKT] call: git-annex [\"transferkeys\",\"--readfd\",\"46\",\"--writefd\",\"36\"] +[2013-07-10 16:31:13 HKT] read: git [\"--git-dir=/home/yan/annex_testing/.git\",\"--work-tree=/home/yan/annex_testing\",\"show-ref\",\"--hash\",\"refs/heads/git-annex\"] +[2013-07-10 16:31:13 HKT] read: git [\"--git-dir=/home/yan/annex_testing/.git\",\"--work-tree=/home/yan/annex_testing\",\"log\",\"refs/heads/git-annex..267bf35da4d9abe5ed7fe82ea5df8a8df2ddf940\",\"--oneline\",\"-n1\"] +[2013-07-10 16:31:13 HKT] read: git [\"--git-dir=/home/yan/annex_testing/.git\",\"--work-tree=/home/yan/annex_testing\",\"show-ref\",\"git-annex\"] +[2013-07-10 16:31:13 HKT] read:[2013-07-10 16:31:13 HKT] read: git [\"--git-dir=/home/yan/annex_testing/.git\",\"--work-tree=/home/yan/annex_testing\",\"ls-tree\",\"-z\",\"--\",\"refs/heads/git-annex\",\"uuid.log\",\"remote.log\",\"trust.log\",\"group.log\",\"preferred-content.log\"] + git [\"--git-dir=/home/yan/annex_testing/.git\",\"--work-tree=/home/yan/annex_testing\",\"show-ref\",\"--hash\",\"refs/heads/git-annex\"] +[2013-07-10 16:31:13 HKT] read: git [\"--git-dir=/home/yan/annex_testing/.git\",\"--work-tree=/home/yan/annex_testing\",\"log\",\"refs/heads/git-annex..267bf35da4d9abe5ed7fe82ea5df8a8df2ddf940\",\"--oneline\",\"-n1\"] +[2013-07-10 16:31:13 HKT] chat: git [\"--git-dir=/home/yan/annex_testing/.git\",\"--work-tree=/home/yan/annex_testing\",\"cat-file\",\"--batch\"] +(gpg) [2013-07-10 16:31:13 HKT] TransferWatcher: transfer starting: Upload UUID \"df02d32a-7e3a-4e12-a417-7f1d1a1cf1a6\" DSCN1810.JPG Nothing +[2013-07-10 16:31:13 HKT] chat: gpg [\"--batch\",\"--no-tty\",\"--use-agent\",\"--quiet\",\"--trust-model\",\"always\",\"--batch\",\"--passphrase-fd\",\"48\",\"--symmetric\",\"--force-mdc\"] +[2013-07-10 16:31:14 HKT] call: sh [\"-c\",\"/usr/bin/python2 /home/yan/annex/SocialBusiness/Resources/Infrastructure/Computer_Tools/Records/AsusU24/owncloudannex/owncloudannex.py --dbglevel 1 --stderr\"] + + 16:31:16 [owncloudannex-0.1.1] : 'START' + 16:31:16 [owncloudannex-0.1.1] main : 'ARGS: 'ANNEX_ACTION=store ANNEX_KEY=GPGHMACSHA1--0179fbbf559d5c25cf69b4e92025ff1f007d4f1f ANNEX_HASH_1=fp ANNEX_HASH_2=23 ANNEX_FILE=/home/yan/annex_testing/.git/annex/tmp/GPGHMACSHA1--0179fbbf559d5c25cf69b4e92025ff1f007d4f1f /home/yan/annex/SocialBusiness/Resources/Infrastructure/Computer_Tools/Records/AsusU24/owncloudannex/owncloudannex.py --dbglevel 1 --stderr'' + 16:31:16 [owncloudannex-0.1.1] readFile : ''/home/yan/annex/SocialBusiness/Resources/Infrastructure/Computer_Tools/Records/AsusU24/owncloudannex/owncloudannex.conf' - 'r'' + 16:31:16 [owncloudannex-0.1.1] readFile : 'Done' + 16:31:16 [owncloudannex-0.1.1] login : '' + 16:31:16 [owncloudannex-0.1.1] login : 'Using base: wingyan.no-ip.org - {'Authorization': 'Basic Y2h1bmd5YW41QGdtYWlsLmNvbTp5YW5fd2lraQ=='}' + 16:31:16 [owncloudannex-0.1.1] login : 'res: ' + 16:31:16 [owncloudannex-0.1.1] findInFolder : 'u'gitannex'() - '/'()' + 16:31:17 [owncloudannex-0.1.1] findInFolder : 'propfind: /owncloud/remote.php/webdav - '\n\n Sabre_DAV_Exception_NotAuthenticated\n Username or password does not match\n 1.7.6\n\n'' + 16:31:17 [owncloudannex-0.1.1] findInFolder : 'Failure' + 16:31:17 [owncloudannex-0.1.1] createFolder : '/gitannex' + 16:31:18 [owncloudannex-0.1.1] createFolder : 'Failure: 401 - '\n\n Sabre_DAV_Exception_NotAuthenticated\n Username or password does not match\n 1.7.6\n\n'' + + + owncloud hook exited nonzero! +[2013-07-10 16:31:18 HKT] TransferWatcher: transfer finishing: Transfer {transferDirection = Upload, transferUUID = UUID \"df02d32a-7e3a-4e12-a417-7f1d1a1cf1a6\", transferKey = Key {keyName = \"911ba6148fbcbe4afe53772f1216b8204f403ed4ee06cb90c3c3ac25e56d9402.JPG\", keyBackendName = \"SHA256E\", keySize = Just 1893431, keyMtime = Nothing}} + +I figured out it is a *Username or password does not match*, and i saw the content of owncloudannex.conf as \"uname\": \"myGmail@gmail.com\", but i quickly saw my WebDAV login to owncloud as my user name, not gmail address, so i changed this \"uname\": \"my_normal_user_name_not_gmail_acc\" inside owncloudannex.conf. Finally, i got it work. So, i think, user name should not be a gmail address, should be owncloud login user name. + +Another issue, i had a look into /WebDAV.../gitannex/, it is git repos. file, for my user opinion, it is better that it is a real file content that we can see the file(such as photos) by owncloud web client directly, rather that owncloud is a file server to keep git repos only. + +Thanks + +yan +"""]] diff --git a/doc/tips/owncloudannex/comment_4_492b6922a7c5bb5464fedb46b0c5303b._comment b/doc/tips/owncloudannex/comment_4_492b6922a7c5bb5464fedb46b0c5303b._comment new file mode 100644 index 0000000000..c7a0875df1 --- /dev/null +++ b/doc/tips/owncloudannex/comment_4_492b6922a7c5bb5464fedb46b0c5303b._comment @@ -0,0 +1,17 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawmLB39PC89rfGaA8SwrsnB6tbumezj-aC0" + nickname="Tobias" + subject="comment 4" + date="2013-07-10T08:54:13Z" + content=""" +Hm, an error like that it really should print without the need of debug information. I'll look into it. + +And if you want the real file content, not encrypted, just change \"shared\" to \"none\" in the line: + +git annex initremote owncloud type=hook hooktype=owncloud encryption=shared + +I'm not sure if you can change this after you initialized the remote though. And also, the folder structure and filenames will be as you see them now on the owncloud. I'd need some more data from gitannex to make it show the real directory structure. But that doesn't seem feasible. + +Why would you even want the data unencrypted on owncloud anyways? i mean on flickr, or googledocs, i kinda get it. but for owncloud? + +"""]] diff --git a/doc/tips/owncloudannex/comment_5_1d48ac08714fadcb06d874570d745bd8._comment b/doc/tips/owncloudannex/comment_5_1d48ac08714fadcb06d874570d745bd8._comment new file mode 100644 index 0000000000..170e8b857a --- /dev/null +++ b/doc/tips/owncloudannex/comment_5_1d48ac08714fadcb06d874570d745bd8._comment @@ -0,0 +1,16 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawkQvRLq7nMMEGoEKuYx9oaf67IC0nZfmVI" + nickname="chung yan" + subject="comment 5" + date="2013-07-10T09:43:55Z" + content=""" +hi Tobias + +thanks your sharing, i am still in have a trial of git-annex, so i do not yet have real data on the server, so i just del. all and re-create both client and server repos in owncloud, i can got what you said, and see my photo. + +Actually, i am studying git-annex. 1) It can syncs the data from my client computer to server. 2) i would like that other and anywhere computers through browser can view my server data which do not need to download all data into a new client. So my original goal is i can have a web to display the content of my files, and see owncloud have a nice web. + +Regarding to GDoc, flickr, etc. they have space limitation, so i install owncloud as my own computer. I also see the , but it is not yet at roadmap. + +thanks +"""]] diff --git a/doc/tips/owncloudannex/comment_6_65959f49a2f56bffd6fe48670c0c8d5a._comment b/doc/tips/owncloudannex/comment_6_65959f49a2f56bffd6fe48670c0c8d5a._comment new file mode 100644 index 0000000000..7b10068c50 --- /dev/null +++ b/doc/tips/owncloudannex/comment_6_65959f49a2f56bffd6fe48670c0c8d5a._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawmLB39PC89rfGaA8SwrsnB6tbumezj-aC0" + nickname="Tobias" + subject="comment 6" + date="2013-07-10T09:50:14Z" + content=""" +A 1 terabyte limit(for flickr) should be enough for most people. No? +"""]] diff --git a/doc/tips/owncloudannex/comment_7_7482002991672ef67836bae43b8d0be8._comment b/doc/tips/owncloudannex/comment_7_7482002991672ef67836bae43b8d0be8._comment new file mode 100644 index 0000000000..56e71276d6 --- /dev/null +++ b/doc/tips/owncloudannex/comment_7_7482002991672ef67836bae43b8d0be8._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawkQvRLq7nMMEGoEKuYx9oaf67IC0nZfmVI" + nickname="chung yan" + subject="comment 7" + date="2013-07-10T09:56:54Z" + content=""" +i have not got this 1 terabyte limit from flick before. I will have a look. I still consider some my own data, it is better to keep at my own server. +"""]] diff --git a/doc/tips/powerful_file_matching.mdwn b/doc/tips/powerful_file_matching.mdwn new file mode 100644 index 0000000000..d5d29377c4 --- /dev/null +++ b/doc/tips/powerful_file_matching.mdwn @@ -0,0 +1,36 @@ +git-annex has a powerful syntax for making it act on only certian files. + +The simplest thing is to exclude some files, using wild cards: + + git annex get --exclude '*.mp3' --exclude '*.ogg' + +But you can also exclude files that git-annex's [[location_tracking]] +information indicates are present in a given repository. For example, +if you want to populate newarchive with files, but not those already +on oldarchive, you could do it like this: + + git annex copy --not --in oldarchive --to newarchive + +Without the --not, --in makes it act on files that *are* in the specified +repository. So, to remove files that are on oldarchive: + + git annex drop --in oldarchive + +Or maybe you're curious which files have a lot of copies, and then +also want to know which files have only one copy: + + git annex find --copies 7 + git annex find --not --copies 2 + +The above are the simple examples of specifying what files git-annex +should act on. But you can specify anything you can dream up by combining +the things above, with --and --or -( and -). Those last two strange-looking +options are parentheses, for grouping other options. You will probably +have to escape them from your shell. + +Here are the mp3 files that are in either of two repositories, but have +less than 3 copies: + + git annex find --not --exclude '*.mp3' --and \ + -\( --in usbdrive --or --in archive -\) --and \ + --not --copies 3 diff --git a/doc/tips/recover_data_from_lost+found.mdwn b/doc/tips/recover_data_from_lost+found.mdwn new file mode 100644 index 0000000000..48ef2a1d73 --- /dev/null +++ b/doc/tips/recover_data_from_lost+found.mdwn @@ -0,0 +1,19 @@ +Suppose something goes wrong, and fsck puts all the files in lost+found. +It's actually very easy to recover from this disaster. + +First, check out the git repository again. Then, in the new checkout: + + $ mkdir recovered-content + $ sudo mv ../lost+found/* recovered-content + $ sudo chown you:you recovered-content + $ chmod -R u+w recovered-content + $ git annex add recovered-content + $ git rm recovered-content + $ git commit -m "recovered some content" + $ git annex fsck + +The way that works is that when git-annex adds the same content that was in +the repository before, all the old links to that content start working +again. This works particularly well if the SHA* backends are used, but even +with the default backend it will work pretty well, as long as fsck +preserved the modification time of the files. diff --git a/doc/tips/replacing_Sparkleshare_or_dvcs-autosync_with_the_assistant.mdwn b/doc/tips/replacing_Sparkleshare_or_dvcs-autosync_with_the_assistant.mdwn new file mode 100644 index 0000000000..4363dc85db --- /dev/null +++ b/doc/tips/replacing_Sparkleshare_or_dvcs-autosync_with_the_assistant.mdwn @@ -0,0 +1,41 @@ +Sparkleshare and dvcs-autosync are tools to automatically commit your +changes to git and keep them in sync with other repositories. Unlike +git-annex, they don't store the file content on the side, but directly in +the git repository. Great for small files, less good for big files. + +Here's how to use the [[git-annex assistant|/assistant]] to do the same +thing, but even better! + +---- + +First, get git-annex version 4.20130329 or newer. + +---- + +Let's suppose you're delveloping a video game, written in C. You have +source code, and some large game assets. You want to ensure the source +code is stored in git -- that's what git's for! And you want to store +the game assets in the git annex -- to avod bloating your git repos with +possibly enormous files, but still version control them. + +All you need to do is configure git-annex to treat your C files +as small files. And treat any file larger than, say, 100kb as a large +file that is stored in the annex. + + git config annex.largefiles "largerthan=100kb and not (include=*.c or include=*.h)" + +Now if you run `git annex add`, it will only add the large files to the +annex. You can `git add` the small files directly to git. + +Better, if you run `git annex assistant`, it will *automatically* +add the large files to the annex, and store the small files in git. +It'll notice every time you modify a file, and immediately commit it, +too. And sync it out to other repositories you configure using `git annex +webapp`. + +---- + +It's also possible to disable the use of the annex entirely, and just +have the assistant *always* put every file into git, no matter its size: + + git config annex.largefiles "exclude=*" diff --git a/doc/tips/replacing_Sparkleshare_or_dvcs-autosync_with_the_assistant/comment_1_907e4032ca4a39adb846cf16dbf447dc._comment b/doc/tips/replacing_Sparkleshare_or_dvcs-autosync_with_the_assistant/comment_1_907e4032ca4a39adb846cf16dbf447dc._comment new file mode 100644 index 0000000000..b0ff0114a6 --- /dev/null +++ b/doc/tips/replacing_Sparkleshare_or_dvcs-autosync_with_the_assistant/comment_1_907e4032ca4a39adb846cf16dbf447dc._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="http://hands.com/~phil/" + nickname="hands" + subject="software from the future?" + date="2013-03-31T17:30:34Z" + content=""" +I think you probably meant at least version 4.20130323 ;-) +"""]] diff --git a/doc/tips/replacing_Sparkleshare_or_dvcs-autosync_with_the_assistant/comment_2_902d001ba86657ef0f8cca5b175f99ca._comment b/doc/tips/replacing_Sparkleshare_or_dvcs-autosync_with_the_assistant/comment_2_902d001ba86657ef0f8cca5b175f99ca._comment new file mode 100644 index 0000000000..2367c938d3 --- /dev/null +++ b/doc/tips/replacing_Sparkleshare_or_dvcs-autosync_with_the_assistant/comment_2_902d001ba86657ef0f8cca5b175f99ca._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="http://joeyh.name/" + nickname="joey" + subject="comment 2" + date="2013-03-31T18:50:35Z" + content=""" +I meant 4.20130329 +"""]] diff --git a/doc/tips/replacing_Sparkleshare_or_dvcs-autosync_with_the_assistant/comment_3_a1cf93f9b29658f0f26e9e0ae6057ee3._comment b/doc/tips/replacing_Sparkleshare_or_dvcs-autosync_with_the_assistant/comment_3_a1cf93f9b29658f0f26e9e0ae6057ee3._comment new file mode 100644 index 0000000000..91122360aa --- /dev/null +++ b/doc/tips/replacing_Sparkleshare_or_dvcs-autosync_with_the_assistant/comment_3_a1cf93f9b29658f0f26e9e0ae6057ee3._comment @@ -0,0 +1,60 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawniayrgSdVLUc3c6bf93VbO-_HT4hzxmyo" + nickname="Tobias" + subject="Trying this feature" + date="2013-04-14T13:04:55Z" + content=""" +I just gave this feature a try, but it seems it doesn't work as expected or maybe I don't understand it: + + ~/annex/largefilestest % git init + ~/annex/largefilestest (git)-[master] % git annex init \"test repo\" + ~/annex/largefilestest (git)-[master] % git config annex.largefiles \"not include=*.txt\" + +Now I copy two files to this directory and add both to the annex + + ~/annex/largefilestest (git)-[master] % ll + total 100 + -rw-rw-r-- 1 tobru tobru 93709 Oct 19 16:14 dpkg-get-selections.txt + -rw-rw-r-- 1 tobru tobru 7256 Jan 6 15:52 x3400.jpg + ~/annex/largefilestest (git)-[master] % git annex add . + add x3400.jpg (checksum...) ok + (Recording state in git...) + ~/annex/largefilestest (git)-[master] % git status + # On branch master + # + # Initial commit + # + # Changes to be committed: + # (use \"git rm --cached ...\" to unstage) + # + # new file: x3400.jpg + # + # Untracked files: + # (use \"git add ...\" to include in what will be committed) + # + # dpkg-get-selections.txt + ~/annex/largefilestest (git)-[master] % ll + total 96 + -rw-rw-r-- 1 tobru tobru 93709 Oct 19 16:14 dpkg-get-selections.txt + lrwxrwxrwx 1 tobru tobru 192 Jan 6 15:52 x3400.jpg -> .git/annex/objects/vf/QX/SHA256E-s7256--60e5b69ade5619e37f7fcaa964626da9c415959d861241aa13e2516fffc2dddf.jpg/SHA256E-s7256--60e5b69ade5619e37f7fcaa964626da9c415959d861241aa13e2516fffc2dddf.jpg + +So the picture is added to the annex as expected. But the .txt file is not added to git. Do I have to manually add this to git? And why is the picture seen as new file by git? + +The second question could be answered by: \"run git annex sync\". Is this correct? Because after running this command, git does not see this file as a new file anymore: + + ~/annex/largefilestest (git)-[master] % git annex sync + commit + [master (root-commit) a0afb14] git-annex automatic sync + 1 file changed, 1 insertion(+) + create mode 120000 x3400.jpg + ok + git-annex: no branch is checked out + ~/annex/largefilestest (git)-[master] % git status + # On branch master + # Untracked files: + # (use \"git add ...\" to include in what will be committed) + # + # dpkg-get-selections.txt + nothing added to commit but untracked files present (use \"git add\" to track) + +"""]] diff --git a/doc/tips/replacing_Sparkleshare_or_dvcs-autosync_with_the_assistant/comment_4_e10671908b58c554375787d0f76e2366._comment b/doc/tips/replacing_Sparkleshare_or_dvcs-autosync_with_the_assistant/comment_4_e10671908b58c554375787d0f76e2366._comment new file mode 100644 index 0000000000..14a9090148 --- /dev/null +++ b/doc/tips/replacing_Sparkleshare_or_dvcs-autosync_with_the_assistant/comment_4_e10671908b58c554375787d0f76e2366._comment @@ -0,0 +1,13 @@ +[[!comment format=mdwn + username="http://joeyh.name/" + nickname="joey" + subject="comment 4" + date="2013-04-14T18:37:50Z" + content=""" +Like it says in the tip, `git annex add` will add the large files to git. You can add the small files with `git add`; git-annex won't do that for you. + +To automatically add both sorts of files, you can use the `git annex watch` or `git annex assistant` daemons. The latter also keeps files in sync between repositories automatically. + +(Why did the picture show up as a new file in git? Because you hadn't committed it. This is the same as when you `git add` a file; +it's only staged in the index; `git status` will show it is new until you `git commit`) +"""]] diff --git a/doc/tips/replacing_Sparkleshare_or_dvcs-autosync_with_the_assistant/comment_5_4114380f66b6376c851e93f6876d590b._comment b/doc/tips/replacing_Sparkleshare_or_dvcs-autosync_with_the_assistant/comment_5_4114380f66b6376c851e93f6876d590b._comment new file mode 100644 index 0000000000..5d638a3b83 --- /dev/null +++ b/doc/tips/replacing_Sparkleshare_or_dvcs-autosync_with_the_assistant/comment_5_4114380f66b6376c851e93f6876d590b._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawniayrgSdVLUc3c6bf93VbO-_HT4hzxmyo" + nickname="Tobias" + subject="mimetypes" + date="2013-05-01T20:37:33Z" + content=""" +Does `annex.largefiles` support mimetypes? F.e. `git config annex.largefiles \"not mimetype=text/plain\"` +"""]] diff --git a/doc/tips/setup_a_public_repository_on_a_web_site.mdwn b/doc/tips/setup_a_public_repository_on_a_web_site.mdwn new file mode 100644 index 0000000000..e1fbd1e473 --- /dev/null +++ b/doc/tips/setup_a_public_repository_on_a_web_site.mdwn @@ -0,0 +1,31 @@ +Let's say you want to distribute some big files to the whole world. +You can of course, just drop them onto a website. But perhaps you'd like to +use git-annex to manage those files. And as an added bonus, why not let +anyone in the world clone your site and use `git-annex get`! + +My site like this is [downloads.kitenet.net](https://downloads.kitenet.net). +Here's how I set it up. --[[Joey]] + +1. Set up a web site. I used Apache, and configured it to follow symlinks. + `Options FollowSymLinks` +2. Put some files on the website. Make sure it works. +4. `git init; git annex init` +3. We want users to be able to clone the git repository over http, because + git-annex can download files from it over http as well. For this to + work, `git update-server-info` needs to get run after commits. The + git `post-update` hook will take care of this, you just need to enable + the hook. `chmod +x .git/hooks/post-update` +5. `git annex add; git commit -m added` +6. Make sure users can still download files from the site directly. +7. Instruct advanced users to clone a http url that ends with the "/.git/" + directory. For example, for downloads.kitenet.net, the clone url + is `https://downloads.kitenet.net/.git/` +8. Set up a git `post-receive` hook that runs `git annex merge`, and + the repository's working tree will automatically be updated when + you run `git annex sync` in a clone that can push to the repository. + (Needs git-annex version 4.20130703 or newer; older versions + can use `git annex sync` in the post-receive hook instead.) + +When users clone over http, and run git-annex, it will +automatically learn all about your repository and be able to download files +right out of it, also using http. diff --git a/doc/tips/setup_a_public_repository_on_a_web_site/comment_1_1d0fa6da33e401df1d7ff31979247fec._comment b/doc/tips/setup_a_public_repository_on_a_web_site/comment_1_1d0fa6da33e401df1d7ff31979247fec._comment new file mode 100644 index 0000000000..cc0a0f2b33 --- /dev/null +++ b/doc/tips/setup_a_public_repository_on_a_web_site/comment_1_1d0fa6da33e401df1d7ff31979247fec._comment @@ -0,0 +1,10 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawnYLUTs4jFpBPwOlIIJ6qD8xdqZPboJafM" + nickname="Oluf" + subject="Combine this with a 'public' repository-group" + date="2013-07-16T09:44:26Z" + content=""" +Hi, + +would it be possible to do this whith the contents of a public repository-group (a non-bare public repository)? +"""]] diff --git a/doc/tips/setup_a_public_repository_on_a_web_site/comment_2_b98b761dee9d923153e3c288c1d987ee._comment b/doc/tips/setup_a_public_repository_on_a_web_site/comment_2_b98b761dee9d923153e3c288c1d987ee._comment new file mode 100644 index 0000000000..7bfb89b36a --- /dev/null +++ b/doc/tips/setup_a_public_repository_on_a_web_site/comment_2_b98b761dee9d923153e3c288c1d987ee._comment @@ -0,0 +1,11 @@ +[[!comment format=mdwn + username="http://joeyh.name/" + ip="4.154.4.90" + subject="comment 2" + date="2013-07-16T17:54:28Z" + content=""" +You can choose which files get stored in the public repository, and are thus accessible to the public. +However, note that since the git repository is published, anyone could clone it and see all the names and hashes of your files, even if you've not pushed the file contents to the public repository. + +Currently the way the \"public\" [[repository group|preferred_content]] works only makes it be usable with special remotes. This is because it uses a `preferreddir` setting in the special remote configuration. +"""]] diff --git a/doc/tips/skydriveannex.mdwn b/doc/tips/skydriveannex.mdwn new file mode 100644 index 0000000000..56acdd96c1 --- /dev/null +++ b/doc/tips/skydriveannex.mdwn @@ -0,0 +1,29 @@ +skydriveannex +========= + +Hook program for gitannex to use [skydrive](http://en.wikipedia.org/wiki/SkyDrive) (previously *Windows Live SkyDrive* and *Windows Live Folders*) as backend + +# Requirements: + + python2 + python-yaml + +Credit for the Skydrive api interface goes to https://github.com/mk-fg/python-skydrive + +# Install +Clone the git repository in your home folder. + + git clone git://github.com/TobiasTheViking/skydriveannex.git + +This should make a ~/skydriveannex folder + +# Setup +Run the program once to set it up. + + cd ~/skydriveannex; python2 skydriveannex.py + +# Commands for gitannex: + + git config annex.skydrive-hook '/usr/bin/python2 ~/skydriveannex/skydriveannex.py' + git annex initremote skydrive type=hook hooktype=skydrive encryption=shared + git annex describe skydrive "the skydrive library" diff --git a/doc/tips/untrusted_repositories.mdwn b/doc/tips/untrusted_repositories.mdwn new file mode 100644 index 0000000000..cdb5da7c3d --- /dev/null +++ b/doc/tips/untrusted_repositories.mdwn @@ -0,0 +1,28 @@ +Suppose you have a USB thumb drive and are using it as a git annex +repository. You don't trust the drive, because you could lose it, or +accidentally run it through the laundry. Or, maybe you have a drive that +you know is dying, and you'd like to be warned if there are any files +on it not backed up somewhere else. Maybe the drive has already died +or been lost. + +You can let git-annex know that you don't trust a repository, and it will +adjust its behavior to avoid relying on that repositories's continued +availability. + + # git annex untrust usbdrive + untrust usbdrive ok + +Now when you do a fsck, you'll be warned appropriately: + + # git annex fsck . + fsck my_big_file + Only these untrusted locations may have copies of this file! + 05e296c4-2989-11e0-bf40-bad1535567fe -- portable USB drive + Back it up to trusted locations with git-annex copy. + failed + +Also, git-annex will refuse to drop a file from elsewhere just because +it can see a copy on the untrusted repository. + +It's also possible to tell git-annex that you have an unusually high +level of trust for a repository. See [[trust]] for details. diff --git a/doc/tips/using_Amazon_Glacier.mdwn b/doc/tips/using_Amazon_Glacier.mdwn new file mode 100644 index 0000000000..5e7131eeb2 --- /dev/null +++ b/doc/tips/using_Amazon_Glacier.mdwn @@ -0,0 +1,75 @@ +Amazon Glacier provides low-cost storage, well suited for archiving and +backup. But it takes around 4 hours to get content out of Glacier. + +Recent versions of git-annex support Glacier. To use it, you need to have +[glacier-cli](http://github.com/basak/glacier-cli) installed. + +First, export your Amazon AWS credentials: + + # export AWS_ACCESS_KEY_ID="08TJMT99S3511WOZEP91" + # export AWS_SECRET_ACCESS_KEY="s3kr1t" + +Now, create a gpg key, if you don't already have one. This will be used +to encrypt everything stored in Glacier, for your privacy. Once you have +a gpg key, run `gpg --list-secret-keys` to look up its key id, something +like "2512E3C7" + +Next, create the Glacier remote. + + # git annex initremote glacier type=glacier encryption=2512E3C7 + initremote glacier (encryption setup with gpg key C910D9222512E3C7) (gpg) ok + +The configuration for the Glacier remote is stored in git. So to make another +repository use the same Glacier remote is easy: + + # cd /media/usb/annex + # git pull laptop + # git annex initremote glacier + initremote glacier (gpg) ok + +Now the remote can be used like any other remote. + + # git annex move my_cool_big_file --to glacier + copy my_cool_big_file (gpg) (checking glacier...) (to glacier...) ok + +But, when you try to get a file out of Glacier, it'll queue a retrieval +job: + + # git annex get my_cool_big_file + get my_cool_big_file (from glacier...) (gpg) + glacier: queued retrieval job for archive 'GPGHMACSHA1--862afd4e67e3946587a9ef7fa5beb4e8f1aeb6b8' + Recommend you wait up to 4 hours, and then run this command again. + failed + +Like it says, you'll need to run the command again later. Let's remember to +do that: + + # at now + 4 hours + at> git annex get my_cool_big_file + +Another oddity of Glacier is that git-annex is never entirely sure +if a file is still in Glacier. Glacier inventories take hours to retrieve, +and even when retrieved do not necessarily represent the current state. + +So, git-annex plays it safe, and avoids trusting the inventory: + + # git annex copy important_file --to glacier + copy important_file (gpg) (checking glacier...) (to glacier...) ok + # git annex drop important_file + drop important_file (gpg) (checking glacier...) + Glacier's inventory says it has a copy. + However, the inventory could be out of date, if it was recently removed. + (Use --trust-glacier if you're sure it's still in Glacier.) + + (unsafe) + Could only verify the existence of 0 out of 1 necessary copies + +Like it says, you can use `--trust-glacier` if you're sure +Glacier's inventory is correct and up-to-date. + +A final potential gotcha with Glacier is that glacier-cli keeps a local +mapping of file names to Glacier archives. If this cache is lost, or +you want to retrieve files on a different box than the one that put them in +glacier, you'll need to use `glacier vault sync` to rebuild this cache. + +See [[special_remotes/Glacier]] for details. diff --git a/doc/tips/using_Amazon_S3.mdwn b/doc/tips/using_Amazon_S3.mdwn new file mode 100644 index 0000000000..19997d0265 --- /dev/null +++ b/doc/tips/using_Amazon_S3.mdwn @@ -0,0 +1,37 @@ +git-annex extends git's usual remotes with some [[special_remotes]], that +are not git repositories. This way you can set up a remote using say, +Amazon S3, and use git-annex to transfer files into the cloud. + +First, export your Amazon AWS credentials: + + # export AWS_ACCESS_KEY_ID="08TJMT99S3511WOZEP91" + # export AWS_SECRET_ACCESS_KEY="s3kr1t" + +Now, create a gpg key, if you don't already have one. This will be used +to encrypt everything stored in S3, for your privacy. Once you have +a gpg key, run `gpg --list-secret-keys` to look up its key id, something +like "2512E3C7" + +Next, create the S3 remote, and describe it. + + # git annex initremote cloud type=S3 encryption=2512E3C7 + initremote cloud (encryption setup with gpg key C910D9222512E3C7) (checking bucket) (creating bucket in US) (gpg) ok + # git annex describe cloud "at Amazon's US datacenter" + describe cloud ok + +The configuration for the S3 remote is stored in git. So to make another +repository use the same S3 remote is easy: + + # cd /media/usb/annex + # git pull laptop + # git annex initremote cloud + initremote cloud (gpg) (checking bucket) ok + +Now the remote can be used like any other remote. + + # git annex copy my_cool_big_file --to cloud + copy my_cool_big_file (gpg) (checking cloud...) (to cloud...) ok + # git annex move video/hackity_hack_and_kaxxt.mov --to cloud + move video/hackity_hack_and_kaxxt.mov (checking cloud...) (to cloud...) ok + +See [[special_remotes/S3]] for details. diff --git a/doc/tips/using_Amazon_S3/comment_1_666a26f95024760c99c627eed37b1966._comment b/doc/tips/using_Amazon_S3/comment_1_666a26f95024760c99c627eed37b1966._comment new file mode 100644 index 0000000000..60d96cb44e --- /dev/null +++ b/doc/tips/using_Amazon_S3/comment_1_666a26f95024760c99c627eed37b1966._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawnoUOqs_lbuWyZBqyU6unHgUduJwDDgiKY" + nickname="Matt" + subject="ANNEX_S3 vs AWS for keys" + date="2012-05-29T12:24:25Z" + content=""" +The instructions state ANNEX_S3_ACCESS_KEY_ID and ANNEX_SECRET_ACCESS_KEY but git-annex cannot connect with those constants. git-annex tells me to set both \"AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY\" instead, which works. This is with Xubuntu 12.04. +"""]] diff --git a/doc/tips/using_Amazon_S3/comment_2_f5a0883be7dbb421b584c6dc0165f1ef._comment b/doc/tips/using_Amazon_S3/comment_2_f5a0883be7dbb421b584c6dc0165f1ef._comment new file mode 100644 index 0000000000..dc809cb126 --- /dev/null +++ b/doc/tips/using_Amazon_S3/comment_2_f5a0883be7dbb421b584c6dc0165f1ef._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="http://joeyh.name/" + ip="4.153.81.112" + subject="comment 2" + date="2012-05-29T19:10:42Z" + content=""" +Thanks, I've fixed that. (You could have too.. this is a wiki ;) +"""]] diff --git a/doc/tips/using_Google_Cloud_Storage.mdwn b/doc/tips/using_Google_Cloud_Storage.mdwn new file mode 100644 index 0000000000..d44e4f17f8 --- /dev/null +++ b/doc/tips/using_Google_Cloud_Storage.mdwn @@ -0,0 +1,9 @@ +[Google Cloud Storage](https://cloud.google.com/products/cloud-storage) +supports the same API as Amazon S3, so the +[[S3 special remote|special_remotes/S3]] can be used with it. +Here is a configuration example: + + git annex initremote cloud type=S3 encryption=none host=storage.googleapis.com port=80 + +Thanks to jterrance for the [original tip](https://gist.github.com/4576324). +--[[Joey]] diff --git a/doc/tips/using_Google_Cloud_Storage/comment_1_c576182f39563ae68767391c4227a177._comment b/doc/tips/using_Google_Cloud_Storage/comment_1_c576182f39563ae68767391c4227a177._comment new file mode 100644 index 0000000000..3a4d02f327 --- /dev/null +++ b/doc/tips/using_Google_Cloud_Storage/comment_1_c576182f39563ae68767391c4227a177._comment @@ -0,0 +1,18 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawnaH44G3QbxBAYyDwy0PbvL0ls60XoaR3Y" + nickname="Nigel" + subject="AWS credentials" + date="2013-05-31T10:23:23Z" + content=""" +This looks very valuable - Google are offering a free 5GB up until 2013 June 30th + +Sign in here[1] get your credentials here[2] under the section “Interoperable Access” (Source[3]) + + # Set up AWS credentials + $ export AWS_ACCESS_KEY_ID=\"YOUR-KEY\" + $ export AWS_SECRET_ACCESS_KEY=\"YOUR-SECRET\" + +1. http://gs-signup-redirect.appspot.com/ -or- https://developers.google.com/storage/docs/signup +2. https://storage.cloud.google.com/m +3. http://fog.io/storage/ +"""]] diff --git a/doc/tips/using_box.com_as_a_special_remote.mdwn b/doc/tips/using_box.com_as_a_special_remote.mdwn new file mode 100644 index 0000000000..6616d0a1e2 --- /dev/null +++ b/doc/tips/using_box.com_as_a_special_remote.mdwn @@ -0,0 +1,71 @@ +[Box.com](http://box.com/) is a file storage service, currently notable +for providing 50 gb of free storage if you sign up with its Android client. +(Or a few gb free otherwise.) + +git-annex can use Box as a [[special remote|special_remotes]]. +Recent versions of git-annex make this very easy to set up: + + WEBDAV_USERNAME=you@example.com WEBDAV_PASSWORD=xxxxxxx git annex initremote box.com type=webdav url=https://www.box.com/dav/git-annex chunksize=75mb encryption=you@example.com + +Note the use of chunksize; Box has a 100 mb maximum file size, and this +breaks up large files into chunks before that limit is reached. + +# old davfs2 method + +This method is deprecated, but still documented here just in case. +Note that the files stored using this method cannot reliably be retreived +using the webdav special remote. + +## davfs2 setup + +* First, install + the [davfs2](http://savannah.nongnu.org/projects/davfs2) program, + which can mount Box using WebDAV. On Debian, just `sudo apt-get install davfs2` +* Allow users to mount davfs filesystems, by ensuring that + `/sbin/mount.davfs` is setuid root. On Debian, just `sudo dpkg-reconfigure davfs2` +* Add yourself to the davfs2 group. + + sudo adduser $(whoami) davfs2 + +* Edit `/etc/fstab`, and add a line to mount Box using davfs. + + sudo mkdir -p /media/box.com + echo "https://www.box.com/dav/ /media/box.com davfs noauto,user 0 0" | sudo tee -a /etc/fstab + +* Create `~/.davfs2/davfs2.conf` with some important settings: + + mkdir ~/.davfs2/ + echo use_locks 0 > ~/.davfs2/davfs2.conf + echo cache_size 1 >> ~/.davfs2/davfs2.conf + echo delay_upload 0 >> ~/.davfs2/davfs2.conf + +* Create `~/.davfs2/secrets`. This file contains your Box.com login and password. + Your login is probably the email address you signed up with. + + echo "/media/box.com joey@kitenet.net mypassword" > ~/.davfs2/secrets + chmod 600 ~/.davfs2/secrets + +* Now you should be able to mount Box, as a non-root user: + + mount /media/box.com + +## git-annex setup + +You need git-annex version 3.20120303 or newer, which adds support for chunking +files larger than Box's 100 mb limit. + +Create the special remote, in your git-annex repository. +** This example is non-encrypted; fill in your gpg key ID for a securely +encrypted special remote! ** + + git annex initremote box.com type=directory directory=/media/box.com chunksize=2mb encryption=none + +Now git-annex can copy files to box.com, get files from it, etc, just like +with any other special remote. + + % git annex copy bigfile --to box.com + bigfile (to box.com...) ok + % git annex drop bigfile + bigfile (checking box.com...) ok + % git annex get bigfile + bigfile (from box.com...) ok diff --git a/doc/tips/using_git_annex_with_no_fixed_hostname_and_optimising_ssh.mdwn b/doc/tips/using_git_annex_with_no_fixed_hostname_and_optimising_ssh.mdwn new file mode 100644 index 0000000000..594d8c480f --- /dev/null +++ b/doc/tips/using_git_annex_with_no_fixed_hostname_and_optimising_ssh.mdwn @@ -0,0 +1,59 @@ +## Intro + +This tip is based on my (Matt Ford) experience of using `git annex` with my out-and-about netbook which hits many different wifi networks and has no fixed home or address. + +I'm not using a bare repository that allows pushing (an alternative solution) nor do I fancy allowing `git push` to run against my desktop checked out repository (perhaps I worry over nothing?) + +None of this is really `git annex` specific but I think it is useful to know... + +## Dealing with no fixed hostname + +Essentially set up two repos as per the [[walkthrough]]. + +Desktop as follows: + + cd ~/annex + git init + git annex init "desktop" + +And the laptop like this + + git clone ssh://desktop/annex + git init + git annex init "laptop" + +Now we want to add the the repos as remotes of each other. + +For the laptop it is easy: + + git remote add desktop ssh://desktop/~/annex + +However for the desktop to add an ever changing laptops hostname it's a little tricky. We make use of remote SSH tunnels to do this. Essentially we have the laptop (which always knows it's own name and address and knows the address of the desktop) create a tunnel starting on an arbitrary port at the desktop and heads back to the laptop on it's own SSH server port (22). + +To do this make part of your laptop's SSH config look like this: + + Host desktop + User matt + HostName desktop.example.org + RemoteForward 2222 localhost:22 + +Now on the desktop to connect over the tunnel to the laptop's SSH port you need this: + + Host laptop + User matt + HostName localhost + port 2222 + +So to add the desktop's remote: + +a) From the laptop ensure the tunnel is up + + ssh desktop + +b) From the desktop add the remote + + git remote add laptop ssh://laptop/~/annex + +So now you can work on the train, pop on the wifi at work upon arrival, and sync up with a `git pull && git annex get`. + +An alternative solution may be to use direct tunnels over Openvpn. diff --git a/doc/tips/using_git_annex_with_no_fixed_hostname_and_optimising_ssh/comment_1_c0b7682a2b6f3078457b85683c825baf._comment b/doc/tips/using_git_annex_with_no_fixed_hostname_and_optimising_ssh/comment_1_c0b7682a2b6f3078457b85683c825baf._comment new file mode 100644 index 0000000000..e627ead47c --- /dev/null +++ b/doc/tips/using_git_annex_with_no_fixed_hostname_and_optimising_ssh/comment_1_c0b7682a2b6f3078457b85683c825baf._comment @@ -0,0 +1,10 @@ +[[!comment format=mdwn + username="http://adamspiers.myopenid.com/" + nickname="Adam" + subject="comment 1" + date="2011-12-23T13:31:33Z" + content=""" +ControlPersist is awesome - thanks! + +Here's [an alternative, git-specific approach](http://thread.gmane.org/gmane.comp.version-control.home-dir/502). +"""]] diff --git a/doc/tips/using_gitolite_with_git-annex.mdwn b/doc/tips/using_gitolite_with_git-annex.mdwn new file mode 100644 index 0000000000..fcc3f96c3c --- /dev/null +++ b/doc/tips/using_gitolite_with_git-annex.mdwn @@ -0,0 +1,89 @@ +[Gitolite](https://github.com/sitaramc/gitolite) is a git repository +manager. Here's how to add git-annex support to gitolite, so you can +`git annex copy` files to a gitolite repository, and `git annex get` +files from it. + +Warning : The method described here works with gitolite version g2, avaible in the g2 branch on github. There is an experimental support for g3 in the git-annex branch, if you tested it please add some feedback. + +A nice feature of using gitolite with git-annex is that users can be given +read-only access to a repository, and this allows them to `git annex get` +file contents, but not change anything. + +First, you need new enough versions: + +* gitolite 2.2 is needed -- this version contains a git-annex-shell ADC + and supports "ua" ADCs. +* git-annex 3.20111016 or newer needs to be installed on the gitolite + server. Don't install an older version, it wouldn't be secure! + +And here's how to set it up. The examples are for gitolite as installed +on Debian with apt-get, but the changes described can be made to any +gitolite installation, just with different paths. + +Set `$GL_ADC_PATH` in `.gitolite.rc`, if you have not already done so. + +
+echo '$GL_ADC_PATH = "/usr/local/lib/gitolite/adc/";' >>~gitolite/.gitolite.rc
+
+ +Make the ADC directory, and a "ua" subdirectory. + +
   
+mkdir -p /usr/local/lib/gitolite/adc/ua
+
+ +Install the git-annex-shell ADC into the "ua" subdirectory from the gitolie repository. + +
   
+cd /usr/local/lib/gitolite/adc/ua/
+cp gitolite/contrib/adc/git-annex-shell .
+
+ +Now all gitolite repositories can be used with git-annex just as any +ssh remote normally would be used. For example: + +
+# git clone gitolite@localhost:testing
+Cloning into testing...
+Receiving objects: 100% (18/18), done.
+# cd testing
+# git annex init
+init  ok
+# cp /etc/passwd my-cool-big-file
+# git annex add my-cool-big-file
+add my-cool-big-file ok
+(Recording state in git...)
+# git commit -m added
+[master d36c8b4] added
+ 1 files changed, 1 insertions(+), 0 deletions(-)
+ create mode 120000 my-cool-big-file
+# git push --all
+Counting objects: 17, done.
+Delta compression using up to 2 threads.
+Compressing objects: 100% (12/12), done.
+Writing objects: 100% (14/14), 1.39 KiB, done.
+Total 14 (delta 0), reused 1 (delta 0)
+To gitolite@localhost:testing
+   c552a38..db4653e  git-annex -> git-annex
+   29cd204..d36c8b4  master -> master
+# git annex copy --to origin
+copy my-cool-big-file (checking origin...) (to origin...) 
+WORM-s2502-m1318875140--my-cool-big-file
+        2502 100%    0.00kB/s    0:00:00 (xfer#1, to-check=0/1)
+
+sent 2606 bytes  received 31 bytes  1758.00 bytes/sec
+total size is 2502  speedup is 0.95
+ok
+
+ + +### Troubleshooting + +I got an error like this when setting up gitolite *after* setting up a local git repo and git annex: + +
+git-annex-shell: First run: git-annex init
+Command ssh ["git@git.example.com","git-annex-shell 'configlist' '/~/myrepo.git'"] failed; exit code 1
+
+ +because I forgot to "git push --all" after adding the new gitolite remote. diff --git a/doc/tips/using_gitolite_with_git-annex/comment_10_8767bc8014b459a3cd76f275fd4fa8d6._comment b/doc/tips/using_gitolite_with_git-annex/comment_10_8767bc8014b459a3cd76f275fd4fa8d6._comment new file mode 100644 index 0000000000..b01779cb44 --- /dev/null +++ b/doc/tips/using_gitolite_with_git-annex/comment_10_8767bc8014b459a3cd76f275fd4fa8d6._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="http://ertai.myopenid.com/" + nickname="npouillard" + subject="git-annex no longer supported by gitolite g3" + date="2013-03-25T12:47:21Z" + content=""" +See http://gitolite.com/gitolite/dev-status.html for some details. +"""]] diff --git a/doc/tips/using_gitolite_with_git-annex/comment_11_00715e0b47f09130e0e536e29f7b9258._comment b/doc/tips/using_gitolite_with_git-annex/comment_11_00715e0b47f09130e0e536e29f7b9258._comment new file mode 100644 index 0000000000..1fbbd9b8a0 --- /dev/null +++ b/doc/tips/using_gitolite_with_git-annex/comment_11_00715e0b47f09130e0e536e29f7b9258._comment @@ -0,0 +1,31 @@ +[[!comment format=mdwn + username="http://mildred.fr/" + nickname="mildred" + subject="Problems with URL ending with ".git"" + date="2013-05-24T12:15:16Z" + content=""" +Hi, + +I noticed using the git-annex branch of gitolite v3 that the same URL with \".git\" at the end would not work in git-annex. For example my test repository was `git@git2.mildred.fr:u/mildred/Annex.git` but it didn't work until I converted it to `git@git2.mildred.fr:u/mildred/Annex` + +On the server, the repository is in `repositories/u/mildred/Annex.git` + +If I try a copy with git-annex for example, I would get: + + $ git annex copy titi --to test + copy titi (checking test...) FATAL: u/mildred/Annex.git mildred DENIED + + (unable to check test) failed + git-annex: copy: 1 failed + +(test is the name of my remote and titi is my file) + +Note, in my gitolite conf, I have: + + repo u/CREATOR/[a-zA-Z0-9].* + C = @all + RW+D = CREATOR + RW = WRITERS + R = READERS + +"""]] diff --git a/doc/tips/using_gitolite_with_git-annex/comment_12_7027ce60265b8f24c8ab54553e544068._comment b/doc/tips/using_gitolite_with_git-annex/comment_12_7027ce60265b8f24c8ab54553e544068._comment new file mode 100644 index 0000000000..f692dd93ea --- /dev/null +++ b/doc/tips/using_gitolite_with_git-annex/comment_12_7027ce60265b8f24c8ab54553e544068._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawn5RcmefXjrl1vmbHIiOWQyXGXVKxlm3rg" + nickname="Kavin" + subject="comment 12" + date="2013-07-25T03:20:15Z" + content=""" +latest code of gitolite does not support git-annex ? I could not find a way to make it work ? +"""]] diff --git a/doc/tips/using_gitolite_with_git-annex/comment_13_75218b7409c0e281cb01c9b2791e8cdf._comment b/doc/tips/using_gitolite_with_git-annex/comment_13_75218b7409c0e281cb01c9b2791e8cdf._comment new file mode 100644 index 0000000000..bc674a5921 --- /dev/null +++ b/doc/tips/using_gitolite_with_git-annex/comment_13_75218b7409c0e281cb01c9b2791e8cdf._comment @@ -0,0 +1,20 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawm9tgeFE5v-arAYYftSv3yUTI5Q4qB2C9M" + nickname="Khaije" + subject="git-annex with gitolite FTW" + date="2013-08-13T15:13:07Z" + content=""" +The steps to activate git-annex integration have changed/simplified for v3. + + +1) during install, be sure to use the 'git-annex' branch, rather than master[fn:1]. + +2) to enable git-annex-shell, open ~/.gitolite.rc and insert 'git-annex-shell' => 'ua' into the hash list in the COMMANDS array.[fn:2] +#'git-annex-shell' => 'ua', + + + + +[fn:1] We'd like to have this feature-branch merged to master, so please send Sitaram feedback, positive and negative, based on your experiences. +[fn:2] There is no GL_ADC_PATH and no \"ua\" subdirectory here, and nothing to \"install\"; the command now comes with gitolite. +"""]] diff --git a/doc/tips/using_gitolite_with_git-annex/comment_1_9a2a2a8eac9af97e0c984ad105763a73._comment b/doc/tips/using_gitolite_with_git-annex/comment_1_9a2a2a8eac9af97e0c984ad105763a73._comment new file mode 100644 index 0000000000..807180660b --- /dev/null +++ b/doc/tips/using_gitolite_with_git-annex/comment_1_9a2a2a8eac9af97e0c984ad105763a73._comment @@ -0,0 +1,15 @@ +[[!comment format=mdwn + username="http://www.openid.albertlash.com/openid/" + ip="71.178.29.218" + subject="comment 1" + date="2011-12-24T06:08:45Z" + content=""" +Looks like you are missing a closing double quote on the line: + + +echo '$GL_ADC_PATH = \"/usr/local/lib/gitolite/adc/;' >>~gitolite/.gitolite.rc + +right after /; + +I got this working by the way - great stuff. +"""]] diff --git a/doc/tips/using_gitolite_with_git-annex/comment_2_d8efea4ab9576555fadbb47666ecefa9._comment b/doc/tips/using_gitolite_with_git-annex/comment_2_d8efea4ab9576555fadbb47666ecefa9._comment new file mode 100644 index 0000000000..007a009ea1 --- /dev/null +++ b/doc/tips/using_gitolite_with_git-annex/comment_2_d8efea4ab9576555fadbb47666ecefa9._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="http://joey.kitenet.net/" + nickname="joey" + subject="comment 2" + date="2011-12-24T16:54:31Z" + content=""" +I've fixed the typo (anyone can edit pages in this wiki FWIW.) +"""]] diff --git a/doc/tips/using_gitolite_with_git-annex/comment_3_807035f38509ccb9f93f1929ecd37417._comment b/doc/tips/using_gitolite_with_git-annex/comment_3_807035f38509ccb9f93f1929ecd37417._comment new file mode 100644 index 0000000000..243764054d --- /dev/null +++ b/doc/tips/using_gitolite_with_git-annex/comment_3_807035f38509ccb9f93f1929ecd37417._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="bremner" + ip="156.34.79.193" + subject="repo name conventions?" + date="2011-12-30T21:41:13Z" + content=""" +I'm confused by the fact that the git-annex-shell adc rejects any repo names that don't start with /~/ since none of my repos start that way. It seems work ok if I just delete /\~ from the front of the regex, but I feel like I must be missing something. +"""]] diff --git a/doc/tips/using_gitolite_with_git-annex/comment_4_eb81f824aadc97f098379c5f7e4fba4c._comment b/doc/tips/using_gitolite_with_git-annex/comment_4_eb81f824aadc97f098379c5f7e4fba4c._comment new file mode 100644 index 0000000000..c53ce01d94 --- /dev/null +++ b/doc/tips/using_gitolite_with_git-annex/comment_4_eb81f824aadc97f098379c5f7e4fba4c._comment @@ -0,0 +1,33 @@ +[[!comment format=mdwn + username="http://joey.kitenet.net/" + nickname="joey" + subject="comment 4" + date="2011-12-31T00:29:45Z" + content=""" +Well a repo url like `gitolite@localhost:testing` puts it in the gitolite user's /~/testing + +This worked when I added the gitolite stuff, anyway.. Let's see if it still does: + +
+joey@gnu:~/tmp>mkdir g
+joey@gnu:~/tmp>cd g
+joey@gnu:~/tmp/g>git init
+Initialized empty Git repository in /home/joey/tmp/g/.git/
+joey@gnu:~/tmp/g>git annex init
+init  ok
+joey@gnu:~/tmp/g>git remote add test 'gitolite@localhost:testing'
+joey@gnu:~/tmp/g>touch foo
+joey@gnu:~/tmp/g>git annex add foo
+add foo (checksum...) ok
+(Recording state in git...)
+joey@gnu:~/tmp/g>git annex copy foo --to test --debug
+git [\"--git-dir=/home/joey/tmp/g/.git\",\"--work-tree=/home/joey/tmp/g\",\"ls-files\",\"--cached\",\"-z\",\"--\",\"foo\"]
+git [\"--git-dir=/home/joey/tmp/g/.git\",\"--work-tree=/home/joey/tmp/g\",\"check-attr\",\"annex.numcopies\",\"-z\",\"--stdin\"]
+git [\"--git-dir=/home/joey/tmp/g/.git\",\"--work-tree=/home/joey/tmp/g\",\"show-ref\",\"--hash\",\"refs/heads/git-annex\"]
+git [\"--git-dir=/home/joey/tmp/g/.git\",\"--work-tree=/home/joey/tmp/g\",\"show-ref\",\"git-annex\"]
+git [\"--git-dir=/home/joey/tmp/g/.git\",\"--work-tree=/home/joey/tmp/g\",\"cat-file\",\"--batch\"]
+Running: ssh [\"-4\",\"gitolite@localhost\",\"git-annex-shell 'configlist' '/~/testing'\"]
+
+ +Still seems right, the ADC's regexp will match this the git-annex shell command. +"""]] diff --git a/doc/tips/using_gitolite_with_git-annex/comment_5_f688309532d2993630e9e72e87fb9c46._comment b/doc/tips/using_gitolite_with_git-annex/comment_5_f688309532d2993630e9e72e87fb9c46._comment new file mode 100644 index 0000000000..052fc90d6b --- /dev/null +++ b/doc/tips/using_gitolite_with_git-annex/comment_5_f688309532d2993630e9e72e87fb9c46._comment @@ -0,0 +1,20 @@ +[[!comment format=mdwn + username="bremner" + ip="156.34.79.193" + subject="gitolite gets different paths for different urls" + date="2011-12-31T01:50:49Z" + content=""" +I guess there is some path rewriting going in in gitolite proper because if try a url of the form +ssh://git@localhost/testing, then it still works with gitolite, but fails with the ADC because +the repo is passed as /testing: +
+Running: ssh [\"git@host\",\"git-annex-shell 'configlist' '/recommend'\"]
+Running: ssh [\"git@host\",\"git-annex-shell 'configlist' '/recommend'\"]
+
+ +What I have to ask Sitaram and or find in the docs is if this is a bug or a feature in gitolite. I can see how the leading slash would get swallowed up by this line +
+$repo = \"'$REPO_BASE/$repo.git'\"
+
+in gl-auth-command, but I guess that isn't the whole story. +"""]] diff --git a/doc/tips/using_gitolite_with_git-annex/comment_6_3e203e010a4df5bf03899f867718adc5._comment b/doc/tips/using_gitolite_with_git-annex/comment_6_3e203e010a4df5bf03899f867718adc5._comment new file mode 100644 index 0000000000..ce888cb135 --- /dev/null +++ b/doc/tips/using_gitolite_with_git-annex/comment_6_3e203e010a4df5bf03899f867718adc5._comment @@ -0,0 +1,25 @@ +[[!comment format=mdwn + username="bremner" + ip="156.34.79.193" + subject="ssh://gitolite-host/repo-name is supposed to work" + date="2011-12-31T03:34:17Z" + content=""" +I confirmed with Sitaram that this is intentional, if probably under-documented. +Since the ADC strips the leading /~/ in assigning $start anyway, I guess something like the following will work +
+
+diff --git a/contrib/adc/git-annex-shell b/contrib/adc/git-annex-shell
+index 7f9f5b8..523dfed 100755
+--- a/contrib/adc/git-annex-shell
++++ b/contrib/adc/git-annex-shell
+@@ -28,7 +28,7 @@ my $cmd=$ENV{SSH_ORIGINAL_COMMAND};
+ # the second parameter.
+ # Further parameters are not validated here (see below).
+ die \"bad git-annex-shell command: $cmd\"
+-    unless $cmd =~ m#^(git-annex-shell '\w+' ')/\~/([0-9a-zA-Z][0-9a-zA-Z._\@/+-
++    unless $cmd =~ m#^(git-annex-shell '\w+' ')/(?:\~\/)?([0-9a-zA-Z][0-9a-zA-Z.
+ my $start = $1;
+ my $repo = $2;
+ my $end = $3;
+
+"""]] diff --git a/doc/tips/using_gitolite_with_git-annex/comment_7_f8fd08b6ab47378ad88c87348057220d._comment b/doc/tips/using_gitolite_with_git-annex/comment_7_f8fd08b6ab47378ad88c87348057220d._comment new file mode 100644 index 0000000000..bdbecd4d99 --- /dev/null +++ b/doc/tips/using_gitolite_with_git-annex/comment_7_f8fd08b6ab47378ad88c87348057220d._comment @@ -0,0 +1,10 @@ +[[!comment format=mdwn + username="http://joey.kitenet.net/" + nickname="joey" + subject="comment 7" + date="2011-12-31T18:32:28Z" + content=""" +That patch seems ok, it doesn't seem to allow through any repo locations that were blocked before. + +So, it has my blessing.. but the ADC is in gitolite and will need to be patched there. +"""]] diff --git a/doc/tips/using_gitolite_with_git-annex/comment_8_8249772c142117f88e37975d058aa936._comment b/doc/tips/using_gitolite_with_git-annex/comment_8_8249772c142117f88e37975d058aa936._comment new file mode 100644 index 0000000000..0717bab1c2 --- /dev/null +++ b/doc/tips/using_gitolite_with_git-annex/comment_8_8249772c142117f88e37975d058aa936._comment @@ -0,0 +1,29 @@ +[[!comment format=mdwn + username="bremner" + ip="156.34.79.193" + subject="afaict git annex normalizes urls on the client side." + date="2011-12-31T22:29:38Z" + content=""" +After some debugging printing, here is my current understanding. + +- urls of the form git@host:~repo or ssh://git@host + + - git sends commands like \"git-receive-pack '~/repo' + - gitolite converts these to $REPO_BASE/~/repo which fails. ~/repo would also fail fwiw. + - git-annex sends seems /~/repo, which works + +- urls of the form git@host:/repo or ssh://git@host/repo + + - git sends \"git-receive-pack '/db/cs3383'\" + - gitolite converts this to $REPO_BASE/repo which works + - git annex sends \"git-annex-shell 'inannex' '/repo' ...\" which works, but only with the patch above. + +- urls of the form git@host:repo + + - git sends \"git-receive-pack 'repo' + - gitolite converts this to $REPO_BASE/repo, which works + - git-annex sends \"git-annex-shell 'inannex' '/~/db/cs3383'...\", which also works for git-annex-shell. + +So the weird case is the last one where git and git-annex are sending different things over the wire. +I don't know if you have other motivations for doing the url normalization on the client side, but it isn't needed for gitolite, and in some sense complicates things a little. On the other hand, now that I see what is going on, it isn't a big deal to just strip the leading /~ off in the adc. It does lead to the odd situation of some URLs working for git-annex but not git. +"""]] diff --git a/doc/tips/using_gitolite_with_git-annex/comment_9_28418635a6ed7231b89e02211cd3c236._comment b/doc/tips/using_gitolite_with_git-annex/comment_9_28418635a6ed7231b89e02211cd3c236._comment new file mode 100644 index 0000000000..fc297ff175 --- /dev/null +++ b/doc/tips/using_gitolite_with_git-annex/comment_9_28418635a6ed7231b89e02211cd3c236._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="http://joey.kitenet.net/" + nickname="joey" + subject="comment 9" + date="2012-01-02T16:27:55Z" + content=""" +Ah right. git-annex normalizes all git ssh style user@host:dir to valid uris, which is where the `/~/` comes from. I don't anticipate this changing on the git-annex side. +"""]] diff --git a/doc/tips/using_the_SHA1_backend.mdwn b/doc/tips/using_the_SHA1_backend.mdwn new file mode 100644 index 0000000000..70dc2ef759 --- /dev/null +++ b/doc/tips/using_the_SHA1_backend.mdwn @@ -0,0 +1,11 @@ +A handy alternative to the default [[backend|backends]] is the +SHA1 backend. This backend provides more git-style assurance that your data +has not been damaged. And the checksum means that when you add the same +content to the annex twice, only one copy need be stored in the backend. + +The only reason it's not the default is that it needs to checksum +files when they're added to the annex, and this can slow things down +significantly for really big files. To make SHA1 the default, just +add something like this to `.gitattributes`: + + * annex.backend=SHA1 diff --git a/doc/tips/using_the_web_as_a_special_remote.mdwn b/doc/tips/using_the_web_as_a_special_remote.mdwn new file mode 100644 index 0000000000..3ce02a56a8 --- /dev/null +++ b/doc/tips/using_the_web_as_a_special_remote.mdwn @@ -0,0 +1,57 @@ +The web can be used as a [[special_remote|special_remotes]] too. + + # git annex addurl http://example.com/video.mpeg + addurl example.com_video.mpeg (downloading http://example.com/video.mpeg) + ########################################################## 100.0% + ok + +Now the file is downloaded, and has been added to the annex like any other +file. So it can be renamed, copied to other repositories, and so on. + +Note that git-annex assumes that, if the web site does not 404, and has the +right file size, the file is still present on the web, and this counts as +one [[copy|copies]] of the file. So it will let you remove your last copy, +trusting it can be downloaded again: + + # git annex drop example.com_video.mpeg + drop example.com_video.mpeg (checking http://example.com/video.mpeg) ok + +If you don't [[trust]] the web to this degree, just let git-annex know: + + # git annex untrust web + untrust web ok + +With the result that it will hang onto files: + + # git annex drop example.com_video.mpeg + drop example.com_video.mpeg (unsafe) + Could only verify the existence of 0 out of 1 necessary copies + Also these untrusted repositories may contain the file: + 00000000-0000-0000-0000-000000000001 -- web + (Use --force to override this check, or adjust annex.numcopies.) + failed + +You can also add urls to any file already in the annex: + + # git annex addurl --file my_cool_big_file http://example.com/cool_big_file + addurl my_cool_big_file ok + # git annex whereis my_cool_big_file + whereis my_cool_big_file (2 copies) + 00000000-0000-0000-0000-000000000001 -- web + 27a9510c-760a-11e1-b9a0-c731d2b77df9 -- here + +To add a lot of urls at once, just list them all as parameters to +`git annex addurl`. + +If you're adding a bunch of related files to a directory, or just don't +like the default filenames generated by `addurl`, you can use `--pathdepth` +to specify how many parts of the url are put in the filename. +A positive number drops that many paths from the beginning, while a negative +number takes that many paths from the end. + + # git annex addurl http://example.com/videos/2012/01/video.mpeg + addurl example.com_videos_2012_01_video.mpeg (downloading http://example.com/videos/2012/01/video.mpeg) + # git annex addurl http://example.com/videos/2012/01/video.mpeg --pathdepth=2 + addurl 2012_01_video.mpeg (downloading http://example.com/videos/2012/01/video.mpeg) + # git annex addurl http://example.com/videos/2012/01/video.mpeg --pathdepth=-2 + addurl 01_video.mpeg (downloading http://example.com/videos/2012/01/video.mpeg) diff --git a/doc/tips/using_the_web_as_a_special_remote/comment_1_321a41d611c6fe45e047af9c96c5176c._comment b/doc/tips/using_the_web_as_a_special_remote/comment_1_321a41d611c6fe45e047af9c96c5176c._comment new file mode 100644 index 0000000000..ee1a271eac --- /dev/null +++ b/doc/tips/using_the_web_as_a_special_remote/comment_1_321a41d611c6fe45e047af9c96c5176c._comment @@ -0,0 +1,26 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawlc1og3PqIGudOMkFNrCCNg66vB7s-jLpc" + nickname="Paul" + subject="can addurl use hashing once the file is downloaded?" + date="2012-09-20T21:01:30Z" + content=""" +There are resources that I want to add to my annex that are currently available +via a URL, but it seems like if I add these using `git-annex addurl`, they get +symlinked to file in the annex/objects directory that starts with `URL-...`, +instead of the more typical `SHA256-...`, and this does not change even after +the files are downloaded. + +My concern is that I really want to ensure that these files don't change, which +is the appeal of content-addressable symlinking of normal files (as opposed to +URL addressable ones). + +Would there be a way to automate the injection of hash-based symlinking for +files that are added via addurl? Sometimes I add a bunch of files via ``addurl +--fast``, and after I've download them via ``get``, it would be nice to have +those files have the same level of data integrity as when I download them using +something outside of git-annex, add them to the annex, and do an ``addurl +--file`` afterward. + +Thanks for all of your hard work! + +"""]] diff --git a/doc/tips/using_the_web_as_a_special_remote/comment_2_dfe9c8c49aadff80d2020288584e0390._comment b/doc/tips/using_the_web_as_a_special_remote/comment_2_dfe9c8c49aadff80d2020288584e0390._comment new file mode 100644 index 0000000000..b015cdcece --- /dev/null +++ b/doc/tips/using_the_web_as_a_special_remote/comment_2_dfe9c8c49aadff80d2020288584e0390._comment @@ -0,0 +1,10 @@ +[[!comment format=mdwn + username="http://joeyh.name/" + subject="comment 2" + date="2012-09-20T21:55:57Z" + content=""" +`addurl` only uses the URL- keys if you run it with --fast. Otherwise it downloads the content and hashes it the same as `add` does. + +If you use `--fast`, you can go back and `git annex migrate` the file once it's been downloaded, to convert +it to the SHA backend. +"""]] diff --git a/doc/tips/using_the_web_as_a_special_remote/comment_3_ed8dd3bbd9b9ae7f2309b72b94f61eb1._comment b/doc/tips/using_the_web_as_a_special_remote/comment_3_ed8dd3bbd9b9ae7f2309b72b94f61eb1._comment new file mode 100644 index 0000000000..0601f30058 --- /dev/null +++ b/doc/tips/using_the_web_as_a_special_remote/comment_3_ed8dd3bbd9b9ae7f2309b72b94f61eb1._comment @@ -0,0 +1,18 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawnx8kHW66N3BqmkVpgtXDlYMvr8TJ5VvfY" + nickname="Yaroslav" + subject="how to drop one of the urls?" + date="2013-04-12T14:53:29Z" + content=""" +is there a way to remove one of the urls? e.g. if I have + + $> git annex whereis fail2ban_logo.png + whereis fail2ban_logo.png (1 copy) + 00000000-0000-0000-0000-000000000001 -- web + + web: http://www.fail2ban.org/fail2ban_logo.png + web: http://www.onerussian.com/tmp/statsmodes.png + ok + +and would like to remove the fail2ban.org one... ? +"""]] diff --git a/doc/tips/using_the_web_as_a_special_remote/comment_4_c1133a524989a940f1b5db588707157a._comment b/doc/tips/using_the_web_as_a_special_remote/comment_4_c1133a524989a940f1b5db588707157a._comment new file mode 100644 index 0000000000..bd55a78727 --- /dev/null +++ b/doc/tips/using_the_web_as_a_special_remote/comment_4_c1133a524989a940f1b5db588707157a._comment @@ -0,0 +1,10 @@ +[[!comment format=mdwn + username="http://joeyh.name/" + nickname="joey" + subject="comment 4" + date="2013-04-22T21:28:03Z" + content=""" +You can use `git annex rmurl $file $url`, which I just added to git-annex. + +(Also, `git annex drop $file --from web` will remove all the urls..) +"""]] diff --git a/doc/tips/visualizing_repositories_with_gource.mdwn b/doc/tips/visualizing_repositories_with_gource.mdwn new file mode 100644 index 0000000000..25a69c1b7a --- /dev/null +++ b/doc/tips/visualizing_repositories_with_gource.mdwn @@ -0,0 +1,22 @@ +[Gource](http://code.google.com/p/gource/) is an amazing animated +visualisation of a git repository. + +Normally, gource shows files being added, removed, and changed in +the repository, and the user(s) making the changes. Of course it can be +used in this way in a repository using git-annex too; just run `gource`. + +The other way to use gource with git-annex is to visualise the movement of +annexed file contents between repositories. In this view, the "users" are +repositories, and they move around the file contents that are being added +or removed from them with git-annex. + +[[!img screenshot.jpg]] + +To use gource this way, first go into the directory you want to visualize, +and use `git annex log` to make an input file for `gource`: + + git annex log --gource | tee gource.log + sort gource.log | gource --log-format custom - + +The `git annex log` can take a while, to speed it up you can use something +like `--after "4 months ago"` to limit how far back it goes. diff --git a/doc/tips/visualizing_repositories_with_gource/screenshot.jpg b/doc/tips/visualizing_repositories_with_gource/screenshot.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9d3096b99bbaba15008aa0d1b675b9db64d20493 GIT binary patch literal 78509 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

? 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 literal 0 HcmV?d00001 diff --git a/doc/tips/what_to_do_when_a_repository_is_corrupted.mdwn b/doc/tips/what_to_do_when_a_repository_is_corrupted.mdwn new file mode 100644 index 0000000000..80cb046d90 --- /dev/null +++ b/doc/tips/what_to_do_when_a_repository_is_corrupted.mdwn @@ -0,0 +1,22 @@ +A git-annex repository on a removable USB drive is great, until the cable +falls out at the wrong time and git's repository gets trashed. The way +git checksums everything and the poor quality of USB media makes this +perhaps more likely than you would expect. If this happens to you, +here's a way to recover that makes the most of whatever data is left +on the drive. + +* First, run `git fsck`. If it does not report any problems, your data + is fine, and you don't need to proceed further. +* So `git fsck` says the git repository is corrupted. But probably the data + git-annex stored is fine. Your first step is to clone another copy + of the git repository from somewhere else. Let's call this clone + "$good", and the corrupted repository "$bad". +* Preserve your git configuration changes, and the `annex.uuid` setting: + `mv $bad/.git/config $good/.git/config` +* Move annexed data into the new repository: `mkdir $good/.git/annex; mv + $bad/.git/annex/objects $good/.git/annex/objects` +* Reinitalize git-annex: `cd $good; git annex init` +* Check for any problems with the annexed data: `cd $good; git annex fsck` +* Now you can remove the corrupted repository, the new one is ready to use. + +--[[Joey]] diff --git a/doc/tips/what_to_do_when_you_lose_a_repository.mdwn b/doc/tips/what_to_do_when_you_lose_a_repository.mdwn new file mode 100644 index 0000000000..363eeea4e0 --- /dev/null +++ b/doc/tips/what_to_do_when_you_lose_a_repository.mdwn @@ -0,0 +1,19 @@ +So you lost a thumb drive containing a git-annex repository. Or a hard +drive died or some other misfortune has befallen your data. + +Unless you configured backups, git-annex can't get your data back. But it +can help you deal with the loss. + +Go somewhere that knows about the lost repository, and mark it as +dead: + + git annex dead usbdrive + +This retains the [[location_tracking]] information for the repository, +but avoids trying to access it, or list it as a location where files +are present. + +If you later found the drive, you could let git-annex know it's found +like so: + + git annex semitrust usbdrive diff --git a/doc/tips/what_to_do_when_you_lose_a_repository/comment_1_cf19b8dc304dc37c26717174c4a98aa4._comment b/doc/tips/what_to_do_when_you_lose_a_repository/comment_1_cf19b8dc304dc37c26717174c4a98aa4._comment new file mode 100644 index 0000000000..a7fce26ef8 --- /dev/null +++ b/doc/tips/what_to_do_when_you_lose_a_repository/comment_1_cf19b8dc304dc37c26717174c4a98aa4._comment @@ -0,0 +1,11 @@ +[[!comment format=mdwn + username="http://dlaxalde.myopenid.com/" + nickname="dl" + subject="comment 1" + date="2012-05-31T14:36:33Z" + content=""" +Is there a way to have git-annex completely ignore a repository? I see that +the `dead` command adds the uuid of the repository to `trust.log` but does +not change `uuid.log`. Is it enough to remove the corresponding line in +`uuid.log` and `trust.log`? +"""]] diff --git a/doc/tips/what_to_do_when_you_lose_a_repository/comment_3_fa9ca81668f5faebf2f61b10f82c97d2._comment b/doc/tips/what_to_do_when_you_lose_a_repository/comment_3_fa9ca81668f5faebf2f61b10f82c97d2._comment new file mode 100644 index 0000000000..a8d044c287 --- /dev/null +++ b/doc/tips/what_to_do_when_you_lose_a_repository/comment_3_fa9ca81668f5faebf2f61b10f82c97d2._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="http://joeyh.name/" + ip="4.153.8.243" + subject="comment 3" + date="2012-05-31T17:01:37Z" + content=""" +`dead` is the best we can do. The automatic merging used on the git-annex branch tends to re-add lines that are deleted in one repo when merging with another that still has them. +"""]] diff --git a/doc/tips/yet_another_simple_disk_usage_like_utility.mdwn b/doc/tips/yet_another_simple_disk_usage_like_utility.mdwn new file mode 100644 index 0000000000..961776e193 --- /dev/null +++ b/doc/tips/yet_another_simple_disk_usage_like_utility.mdwn @@ -0,0 +1,9 @@ +Here's the annex-du script that I use: + +#!/bin/sh +git annex find "$@" --include '*' --format='${bytesize}\n' |awk '{ sum += $1; nfiles++; } END { printf "%d files, %.3f MB\n", nfiles, sum/1000000 } ' + +This one can be slow on a large number of files, but it has an advantage of being able to use all of the filtering available in git annex find. +For example, to figure out how much is stored in remote X, do + +annex-du --in=X diff --git a/doc/tips/yet_another_simple_disk_usage_like_utility/comment_1_41b212bde8bc88d2a5dea93bd0dc75f1._comment b/doc/tips/yet_another_simple_disk_usage_like_utility/comment_1_41b212bde8bc88d2a5dea93bd0dc75f1._comment new file mode 100644 index 0000000000..3a88e855e4 --- /dev/null +++ b/doc/tips/yet_another_simple_disk_usage_like_utility/comment_1_41b212bde8bc88d2a5dea93bd0dc75f1._comment @@ -0,0 +1,9 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawln3ckqKx0x_xDZMYwa9Q1bn4I06oWjkog" + nickname="Michael" + subject="comment 1" + date="2013-07-12T19:36:28Z" + content=""" +Ah, I just found that git annex status can do the same :) +Disregard this. +"""]] diff --git a/doc/todo.mdwn b/doc/todo.mdwn new file mode 100644 index 0000000000..79552298b6 --- /dev/null +++ b/doc/todo.mdwn @@ -0,0 +1,4 @@ +This is git-annex's todo list. Link items to [[todo/done]] when done. + +[[!inline pages="./todo/* and !./todo/done and !link(done) +and !*/Discussion" actions=yes postform=yes show=0 archive=yes]] diff --git a/doc/todo/A_really_simple_way_to_pair_devices_like_bittorent_sync.mdwn b/doc/todo/A_really_simple_way_to_pair_devices_like_bittorent_sync.mdwn new file mode 100644 index 0000000000..93ccc083d3 --- /dev/null +++ b/doc/todo/A_really_simple_way_to_pair_devices_like_bittorent_sync.mdwn @@ -0,0 +1,7 @@ +I like the way btsync search for the peer. So if I need to sync my laptop and other family laptop both with a total different and changing network setup the two device found each other do NAT traversal if needed use relays but the end the two folders are synced. But its closed-source :( I like git and git-annex looks really great. I'm learning it now. + +First I thought with xmpp I can sync files without ssh/rsync or other remote access to my devices just with two jabber account. Now I know I can't. :( + +It would be just great to have some means to sync files without cloud just the two device. Without the ssh / rsync jut share some secret and the devices do the rest. :-o + +Anyway thanks for hearing. I'm looking forward to know more about git-annex. Thank you for that sw. =-<>-= diff --git a/doc/todo/A_really_simple_way_to_pair_devices_like_bittorent_sync/comment_1_d828bc374e50a49101c0b863f9b33080._comment b/doc/todo/A_really_simple_way_to_pair_devices_like_bittorent_sync/comment_1_d828bc374e50a49101c0b863f9b33080._comment new file mode 100644 index 0000000000..13571e681d --- /dev/null +++ b/doc/todo/A_really_simple_way_to_pair_devices_like_bittorent_sync/comment_1_d828bc374e50a49101c0b863f9b33080._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawkEHjHAWnJ0BJzdv_hePwU1my8X4wCseh8" + nickname="Sz" + subject="comment 1" + date="2013-07-23T11:00:21Z" + content=""" +Why not use xmpp for file transfer too not only for git sync? +"""]] diff --git a/doc/todo/A_really_simple_way_to_pair_devices_like_bittorent_sync/comment_2_a4badfc248be428e6426a936212cc896._comment b/doc/todo/A_really_simple_way_to_pair_devices_like_bittorent_sync/comment_2_a4badfc248be428e6426a936212cc896._comment new file mode 100644 index 0000000000..2bff793f05 --- /dev/null +++ b/doc/todo/A_really_simple_way_to_pair_devices_like_bittorent_sync/comment_2_a4badfc248be428e6426a936212cc896._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="http://johan.kiviniemi.name/" + ip="83.145.237.224" + subject="comment 2" + date="2013-07-23T11:37:53Z" + content=""" +Transferring the data over XMPP would almost certainly take too much XMPP server bandwidth, but using something like [libjingle](https://developers.google.com/talk/libjingle/) to set up P2P connections should work nicely. That would require libjingle (or equivalent) bindings for Haskell, though. Libjingle negotiates over XMPP to set up the P2P connection and provides a TCP-like layer for reliable, ordered communication. One could use that for both git metadata and the file transfers. +"""]] diff --git a/doc/todo/A_really_simple_way_to_pair_devices_like_bittorent_sync/comment_3_0b04089d3d33fdb48eeb46bf168e9a3c._comment b/doc/todo/A_really_simple_way_to_pair_devices_like_bittorent_sync/comment_3_0b04089d3d33fdb48eeb46bf168e9a3c._comment new file mode 100644 index 0000000000..d0c000d2b1 --- /dev/null +++ b/doc/todo/A_really_simple_way_to_pair_devices_like_bittorent_sync/comment_3_0b04089d3d33fdb48eeb46bf168e9a3c._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawkEHjHAWnJ0BJzdv_hePwU1my8X4wCseh8" + nickname="Sz" + subject="comment 3" + date="2013-07-24T09:08:45Z" + content=""" +I'm for it! +1 +"""]] diff --git a/doc/todo/A_really_simple_way_to_pair_devices_like_bittorent_sync/comment_4_2bcab1b7998b4df08fca41b8d810f115._comment b/doc/todo/A_really_simple_way_to_pair_devices_like_bittorent_sync/comment_4_2bcab1b7998b4df08fca41b8d810f115._comment new file mode 100644 index 0000000000..f3bafca898 --- /dev/null +++ b/doc/todo/A_really_simple_way_to_pair_devices_like_bittorent_sync/comment_4_2bcab1b7998b4df08fca41b8d810f115._comment @@ -0,0 +1,10 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawl9sYlePmv1xK-VvjBdN-5doOa_Xw-jH4U" + nickname="Richard" + subject="comment 4" + date="2013-08-03T07:12:40Z" + content=""" +NAT traversal requires central infrastructure and is unreliable at best. Obviously, a for-profit entity like bittorrent can shoulder that. + +For LAN situations, [zeroconf](http://www.zeroconf.org/) along with passphrase-based pairing may be the long-term answer. Arguably, that would enhance vanilla Git almost as much as it would enhance git-annex, but that does not seem likely to happen. +"""]] diff --git a/doc/todo/Bittorrent-like_features.mdwn b/doc/todo/Bittorrent-like_features.mdwn new file mode 100644 index 0000000000..4ec991a65b --- /dev/null +++ b/doc/todo/Bittorrent-like_features.mdwn @@ -0,0 +1,31 @@ +I made an oops and created a wishlist thread in the forum regarding bittorrent-like behaviour. Sorry, my bad. + +Here's the original thread: +http://git-annex.branchable.com/forum/Wishlist:_Bittorrent-like_transfers/ + +I think I summed up pretty well what bittorrent-like features could be added to git-annex in one of the posts, so I'll copy and paste some of it here (with slight clarifications added in). + +>Disclaimer: I'm thinking out loud of what could make git-annex even more awesome. I don't expect this to be implemented any time soon. Please pardon any dumbassery. + +>Having your remotes (optionally!) act like a swarm would be an awesome feature to have because you bring in a lot of new features that optimize storage, bandwidth, and overall traffic usage. This would be made a lot easier if parts of it were implemented in small steps that added a nifty feature. The best part is, each of these could be implemented by themselves, and they're all features that would be really useful. +> +>Step 1. Concurrent downloads of a file from remotes. +> +>This would make sense to have, it saves upload traffic on your remotes, and you also get faster DL speeds on the receiving end. +> +>Step 2. Implementing part of the super-seeding capabilities. +> +>You upload pieces of a file to different remotes from your laptop, and on your desktop you can download all those pieces and put them together again to get a complete file. If you really wanted to get fancy, you could build in redundancy (ala RAID) so if a remote or two gets lost, you don't lose the entire file. This would be a very efficient use of storage if you have a bunch of free cloud storage accounts (~1GB each) and some big files you want to back up. +> +>Step 3. Setting it up so that those remotes could talk to one another and share those pieces. +> +>This is where it gets more like bittorrent. Useful because you upload 1 copy and in a few hours, have say, 5 complete copies on 5 different remotes. You could add or remove remotes from a swarm locally, and push those changes to those remotes, which then adapt themselves to suit the new rules and share those with other remotes in the swarm (rules should be GPG-signed as a safety precaution). Also, if/when deltas get implemented, you could push that delta to the swarm and have all the remotes adopt it. This is cooler than regular bittorrent because the shared file can be updated. As a safety precaution, the delta could be GPG signed so a corrupt file doesn't contaminate the entire swarm. Each remote could have bandwidth/storage limits set in a dotfile. +> +>This is a high-level idea of how it might work, and it's also a HUGE set of features to add, but if implemented, you'd be saving a ton of resources, adding new use cases, and making git-annex more flexible. + +And this: + +>Obviously, Step 3 would only work on remotes that you have control of processes on, but if given login credentials to cloud storage remotes (potentially dangerous!) they could read/write to something like dropbox or rsync. +> +>Another thing, this would be completely trackerless. You just use remote groups (or create swarm definitions) and share those with your remotes. **It's completely decentralized!** + diff --git a/doc/todo/Build_for_Synology_DSM.mdwn b/doc/todo/Build_for_Synology_DSM.mdwn new file mode 100644 index 0000000000..be45ea6316 --- /dev/null +++ b/doc/todo/Build_for_Synology_DSM.mdwn @@ -0,0 +1 @@ +It would be wonderful if a pre-built package would be available for Synology NAS. Basically, this is an ARM-based Linux. It has most of the required shell commands either out of the box or easily available (through ipkg). But I think it would be difficult to install the Haskell compiler and all the required modules, so it would probably be better to cross-compile targeting ARM. diff --git a/doc/todo/Build_for_Synology_DSM/comment_10_e351084d9a83db3fd6d9d983227a6410._comment b/doc/todo/Build_for_Synology_DSM/comment_10_e351084d9a83db3fd6d9d983227a6410._comment new file mode 100644 index 0000000000..b62a929d7f --- /dev/null +++ b/doc/todo/Build_for_Synology_DSM/comment_10_e351084d9a83db3fd6d9d983227a6410._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawkwjBDXkP9HAQKhjTgThGOxUa1B99y_WRA" + nickname="Franck" + subject="comment 10" + date="2013-06-02T17:23:43Z" + content=""" +I updated the C program to simplify it so it uses a static path for `_chrooter`. In the previous version, I suspect that one can play with symlinks and use it to get a root shell. So, if `_chrooter` is not installed in `/opt/bin` this file has to be edited too before compilation. +"""]] diff --git a/doc/todo/Build_for_Synology_DSM/comment_11_cc67a584f5c460a6fb63cf099c20e573._comment b/doc/todo/Build_for_Synology_DSM/comment_11_cc67a584f5c460a6fb63cf099c20e573._comment new file mode 100644 index 0000000000..324fa8423e --- /dev/null +++ b/doc/todo/Build_for_Synology_DSM/comment_11_cc67a584f5c460a6fb63cf099c20e573._comment @@ -0,0 +1,9 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawkwjBDXkP9HAQKhjTgThGOxUa1B99y_WRA" + nickname="Franck" + subject="comment 11" + date="2013-06-03T09:55:54Z" + content=""" +A last update and I stop spamming this thread: I've implemented access control and simplified customisation. All this has been moved to https://bitbucket.org/franckp/gasp + +"""]] diff --git a/doc/todo/Build_for_Synology_DSM/comment_12_94023593d294b9cf69090fcfd6ca0e5a._comment b/doc/todo/Build_for_Synology_DSM/comment_12_94023593d294b9cf69090fcfd6ca0e5a._comment new file mode 100644 index 0000000000..39c243ec44 --- /dev/null +++ b/doc/todo/Build_for_Synology_DSM/comment_12_94023593d294b9cf69090fcfd6ca0e5a._comment @@ -0,0 +1,9 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawlJEI45rGczFAnuM7gRSj4C6s9AS9yPZDc" + nickname="Kevin" + subject="SynoCommunity" + date="2013-06-26T18:12:39Z" + content=""" +Creating an installable git-annex package available via [SynoCommunity](http://www.synocommunity.com/) would be awesome. They have created [cross-compilation tools](https://github.com/SynoCommunity/spksrc) to help build the packages and integrate the start/stop scripts with the package manager. + +"""]] diff --git a/doc/todo/Build_for_Synology_DSM/comment_13_314255fd503d125b5aeae2f62acfd592._comment b/doc/todo/Build_for_Synology_DSM/comment_13_314255fd503d125b5aeae2f62acfd592._comment new file mode 100644 index 0000000000..3c54a92710 --- /dev/null +++ b/doc/todo/Build_for_Synology_DSM/comment_13_314255fd503d125b5aeae2f62acfd592._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawnrP-0DGtHDJbWSXeiyk0swNkK1aejoN3c" + nickname="sebastien" + subject="comment 13" + date="2013-08-06T12:18:35Z" + content=""" +I post an issue to github synocommunity for that, i hope somenone have some time to package this great features. +"""]] diff --git a/doc/todo/Build_for_Synology_DSM/comment_1_4059016fa8da6af7a3eba8966821e8eb._comment b/doc/todo/Build_for_Synology_DSM/comment_1_4059016fa8da6af7a3eba8966821e8eb._comment new file mode 100644 index 0000000000..074ba998cf --- /dev/null +++ b/doc/todo/Build_for_Synology_DSM/comment_1_4059016fa8da6af7a3eba8966821e8eb._comment @@ -0,0 +1,10 @@ +[[!comment format=mdwn + username="http://joeyh.name/" + nickname="joey" + subject="comment 1" + date="2013-05-24T15:55:42Z" + content=""" +There are already git-annex builds for arm available from eg, Debian. There's a good chance that, assuming you match up the arm variant (armel, armhf, etc) and that the NAS uses glibc and does not have too old a version, that the binary could just be copied in, possibly with some other libraries, and work. This is what's done for the existing Linux standalone builds. + +So, I look at this bug report as \"please add a standalone build for arm\", not as a request to support a specific NAS which I don't have ;) +"""]] diff --git a/doc/todo/Build_for_Synology_DSM/comment_2_8900c2985ab68b3b566c9f5d326471d6._comment b/doc/todo/Build_for_Synology_DSM/comment_2_8900c2985ab68b3b566c9f5d326471d6._comment new file mode 100644 index 0000000000..40e6398f0d --- /dev/null +++ b/doc/todo/Build_for_Synology_DSM/comment_2_8900c2985ab68b3b566c9f5d326471d6._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawkwjBDXkP9HAQKhjTgThGOxUa1B99y_WRA" + nickname="Franck" + subject="comment 2" + date="2013-05-24T21:31:44Z" + content=""" +I tried to run the binary from the Debian package, unfortunately, after installing tons of libraries, git-annex fails complaining that GLIBC is not recent enough. Perhaps a static build for ARM (armel) can solve the problem? Thanks again for your help! +"""]] diff --git a/doc/todo/Build_for_Synology_DSM/comment_3_f2b77368473d42b7f21e9d51d6415b58._comment b/doc/todo/Build_for_Synology_DSM/comment_3_f2b77368473d42b7f21e9d51d6415b58._comment new file mode 100644 index 0000000000..651edacd7c --- /dev/null +++ b/doc/todo/Build_for_Synology_DSM/comment_3_f2b77368473d42b7f21e9d51d6415b58._comment @@ -0,0 +1,10 @@ +[[!comment format=mdwn + username="http://joeyh.name/" + nickname="joey" + subject="comment 3" + date="2013-05-25T04:42:22Z" + content=""" +Which Debian package? Different ones link to different libcs. + +(It's not really possible to statically link something with as many dependencies as git-annex on linux anymore, unfortunately.) +"""]] diff --git a/doc/todo/Build_for_Synology_DSM/comment_4_a55fea734044c270ceb10adf9c8d9a76._comment b/doc/todo/Build_for_Synology_DSM/comment_4_a55fea734044c270ceb10adf9c8d9a76._comment new file mode 100644 index 0000000000..50ae82ca0b --- /dev/null +++ b/doc/todo/Build_for_Synology_DSM/comment_4_a55fea734044c270ceb10adf9c8d9a76._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawkwjBDXkP9HAQKhjTgThGOxUa1B99y_WRA" + nickname="Franck" + subject="comment 4" + date="2013-05-25T07:40:13Z" + content=""" +I've actually tried several ones: 4.20130521 on sid, 3.20120629~bpo60+2 on squeeze-backports, 3.20120629 on wheezy and jessie, plus a package for Ubuntu 11.02. All of them try to load GLIBC 2.6/2.7 while my system has 2.5 only... I'll try a different approach: install Debian in a chroot on the NAS and extract all the required files, including all libraries. +"""]] diff --git a/doc/todo/Build_for_Synology_DSM/comment_5_59865ada057c640ac29855c65cf45dd9._comment b/doc/todo/Build_for_Synology_DSM/comment_5_59865ada057c640ac29855c65cf45dd9._comment new file mode 100644 index 0000000000..725025283f --- /dev/null +++ b/doc/todo/Build_for_Synology_DSM/comment_5_59865ada057c640ac29855c65cf45dd9._comment @@ -0,0 +1,23 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawkwjBDXkP9HAQKhjTgThGOxUa1B99y_WRA" + nickname="Franck" + subject="comment 5" + date="2013-05-25T10:03:24Z" + content=""" +Unfortunately, chroot approach does not work either. While git-annex works fine when I'm in the chroot, it doesn't work any more outside. If I don't copy libc, I get a version error (just like before so this is normal): + + git-annex: /lib/libc.so.6: version `GLIBC_2.7' not found (required by /opt/share/git-annex/bin/git-annex) + git-annex: /lib/libc.so.6: version `GLIBC_2.6' not found (required by /opt/share/git-annex/bin/git-annex) + git-annex: /lib/libc.so.6: version `GLIBC_2.7' not found (required by /opt/share/git-annex/lib/libgmp.so.10) + +When I copy libc from the Debian chroot, then, it complains about libpthread: + + git-annex: relocation error: /lib/libpthread.so.0: symbol __default_rt_sa_restorer, version GLIBC_PRIVATE not defined in file libc.so.6 with link time reference + +If then I copy libpthread also, I get: + + Illegal instruction (core dumped) + +So, I'm stuck... :-( +I'll try to find a way using the version in the chroot instead of trying to export it to the host system... +"""]] diff --git a/doc/todo/Build_for_Synology_DSM/comment_6_6d860b1ad8816077b5fa596a71b12d5c._comment b/doc/todo/Build_for_Synology_DSM/comment_6_6d860b1ad8816077b5fa596a71b12d5c._comment new file mode 100644 index 0000000000..417293db3e --- /dev/null +++ b/doc/todo/Build_for_Synology_DSM/comment_6_6d860b1ad8816077b5fa596a71b12d5c._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawln3ckqKx0x_xDZMYwa9Q1bn4I06oWjkog" + nickname="Michael" + subject="bind mount" + date="2013-05-25T15:55:52Z" + content=""" +You could bind-mount (e.g. mount -o bind /data /chroot/data ) your main Synology fs into the chroot for git-annex to use. +"""]] diff --git a/doc/todo/Build_for_Synology_DSM/comment_7_19ef2d293ba3bc7ece443d7278371c3f._comment b/doc/todo/Build_for_Synology_DSM/comment_7_19ef2d293ba3bc7ece443d7278371c3f._comment new file mode 100644 index 0000000000..47d0923313 --- /dev/null +++ b/doc/todo/Build_for_Synology_DSM/comment_7_19ef2d293ba3bc7ece443d7278371c3f._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawkwjBDXkP9HAQKhjTgThGOxUa1B99y_WRA" + nickname="Franck" + subject="comment 7" + date="2013-05-25T19:01:29Z" + content=""" +This is indeed what I'm doing. But I need to make a wrapper that will call the command in the chroot. Thanks for the tip anyway. :-) +"""]] diff --git a/doc/todo/Build_for_Synology_DSM/comment_8_609b7ad87dfbba49ec1f8c6fc2739ccd._comment b/doc/todo/Build_for_Synology_DSM/comment_8_609b7ad87dfbba49ec1f8c6fc2739ccd._comment new file mode 100644 index 0000000000..8a34909563 --- /dev/null +++ b/doc/todo/Build_for_Synology_DSM/comment_8_609b7ad87dfbba49ec1f8c6fc2739ccd._comment @@ -0,0 +1,12 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawmqz6wCn-Q1vzrsHGvEJHOt_T5ZESilxhc" + nickname="Sören" + subject="comment 8" + date="2013-05-26T13:50:31Z" + content=""" +I have a Synology NAS too, so I thought I could try to run git-annex in a Debian chroot. +As it [turns out](http://forum.synology.com/wiki/index.php/What_kind_of_CPU_does_my_NAS_have), my model (DS213+) runs on a PowerPC CPU instead of ARM. Unfortunately, it isn't compatible with PPC in Debian either because it is a different PowerPC variant. +There is an unofficial Debian port called [powerpcspe](http://wiki.debian.org/PowerPCSPEPort), but ghc doesn't build there yet for [some reason](http://buildd.debian-ports.org/status/package.php?p=git-annex&suite=sid). + +Any chance that there will be a build for this architecture at some point in the future or should I better look for another NAS? ;-) +"""]] diff --git a/doc/todo/Build_for_Synology_DSM/comment_9_d94a73b9a07c5cadf191005f817fd59a._comment b/doc/todo/Build_for_Synology_DSM/comment_9_d94a73b9a07c5cadf191005f817fd59a._comment new file mode 100644 index 0000000000..c8b45fc603 --- /dev/null +++ b/doc/todo/Build_for_Synology_DSM/comment_9_d94a73b9a07c5cadf191005f817fd59a._comment @@ -0,0 +1,29 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawkwjBDXkP9HAQKhjTgThGOxUa1B99y_WRA" + nickname="Franck" + subject="comment 9" + date="2013-06-02T13:14:56Z" + content=""" +Hi, I finally succeeded! :-) + +Here are the main steps: + + 1. install `debian-chroot` on the NAS + 2. create an account `gitannex` in Debian + 3. configure git on this account (this is important otherwise git complains and fails) `git config --global user.email YOUR_EMAIL` and `git config --global user.name YOUR_NAME` + 4. install `gcc` on the NAS (using `ipkg`) + 5. download the files here: https://www.dropbox.com/sh/b7z68a730aj3mnm/95nFOzE1QP + 6. edit `_chrooter` to fit your settings (probably there is nothing to change if your Debian is freshly installed) + 7. run `make install`, everything goes to `/opt/bin`, if you change this, you should also edit line 17 in file `gasp` + 8. create an account `gitannex` on the NAS (doesn't need to be the same name as in Debian, but I feel it is easier) + 9. edit its `.ssh/authorized_keys` to prefix lines as follows `command=\"gasp\" THE_PUBLIC_KEY_AS_USUAL` + 10. it should work + 11. the repositories will be in the Debian account, but it's easy to symlink them in the NAS account if you wish + +The principle is as follows: `command=\"gasp\"` allows to launch `gasp` on SSH connexion instead of the original command given to `ssh`. This command is retrieved by `gasp` and prefixed with `chrooter-` (so, eg, running `ssh git` on the client results in running `chrooter-git` on the NAS). `chrooter-*` commands are symlinks to `chrooter`, this is a setuid root binary that launches `_chrooter`. (This intermediary binary is necessary because `_chrooter` is a script which cannot be setuid, and setuid is required for the chroot and identity change.) Finally, `_chrooter` starts the `debian-chroot` service, chroot to the target dir, changes identity and eventually launches the original command as if it was lauched directly by `gitannex` user in Debian. `_chrooter` and `gasp` are Python scripts, I did not use shell in order to avoid error-prone issues with spaces in arguments (that need to be passed around several times in the process). + +I'll try now to add command-line parameters to `gasp` in order to restrict the commands that can be run through SSH and the repositories allowed. + +Cheers, +Franck +"""]] diff --git a/doc/todo/Move_ssh_config_to___126____47__ssh__47__git-annex__47__config.mdwn b/doc/todo/Move_ssh_config_to___126____47__ssh__47__git-annex__47__config.mdwn new file mode 100644 index 0000000000..5dc063100b --- /dev/null +++ b/doc/todo/Move_ssh_config_to___126____47__ssh__47__git-annex__47__config.mdwn @@ -0,0 +1,7 @@ +### Please describe the problem. +Instead of storing config for each remote in ~/.ssh/config, which mixes the user own config with that of git-annex-assistant, which is irritating if (like me) you store your ssh config in a vcs. Since the option -F allows the choice of the config file, it should be possible to move the config into ~/.ssh/git-annex/config. The only issue I see is according to the ssh man page on my system states that the system-wide config is ignored if a config file is specified on the command line. + +### What version of git-annex are you using? On what operating system? +I'm using git-annex 4.20130601 on a Debian Testing/Unstable/Experimental mix. + +[[!tag design/assistant]] diff --git a/doc/todo/Move_ssh_config_to___126____47__ssh__47__git-annex__47__config/comment_1_284c806e83a32af81b02aea7c7bc285a._comment b/doc/todo/Move_ssh_config_to___126____47__ssh__47__git-annex__47__config/comment_1_284c806e83a32af81b02aea7c7bc285a._comment new file mode 100644 index 0000000000..5997664e0d --- /dev/null +++ b/doc/todo/Move_ssh_config_to___126____47__ssh__47__git-annex__47__config/comment_1_284c806e83a32af81b02aea7c7bc285a._comment @@ -0,0 +1,10 @@ +[[!comment format=mdwn + username="http://joeyh.name/" + nickname="joey" + subject="comment 1" + date="2013-06-11T14:44:47Z" + content=""" +The only interface git provides to do this is `GIT_SSH`, which would have to be set to a wrapper script that runs ssh with the desirned options. + +And if that were used, `git pull` by itself would not work on the repositories set up by the assistant. I don't consider that very nice. +"""]] diff --git a/doc/todo/Move_ssh_config_to___126____47__ssh__47__git-annex__47__config/comment_2_1f55ad6b39906458779b2d604b003ffe._comment b/doc/todo/Move_ssh_config_to___126____47__ssh__47__git-annex__47__config/comment_2_1f55ad6b39906458779b2d604b003ffe._comment new file mode 100644 index 0000000000..3cf75df08d --- /dev/null +++ b/doc/todo/Move_ssh_config_to___126____47__ssh__47__git-annex__47__config/comment_2_1f55ad6b39906458779b2d604b003ffe._comment @@ -0,0 +1,10 @@ +[[!comment format=mdwn + username="http://joeyh.name/" + nickname="joey" + subject="comment 2" + date="2013-06-11T14:48:01Z" + content=""" +Also, if you're going to set up something like local pairing, why would you *not* want to commit that config to git along with your other ssh configs? Config files in $HOME are quite frequently edited by helper programs to configure changes, and I personally commit those changes all the time. + +Perhaps your real problem is that you have one `.ssh/config` that is shared between multiple hosts, and the git-annex settings are specific to a single host. Have you considered using [vcsh](https://github.com/RichiH/vcsh)? +"""]] diff --git a/doc/todo/Move_ssh_config_to___126____47__ssh__47__git-annex__47__config/comment_3_b00dce2374aac6968317d05d23bcfaf7._comment b/doc/todo/Move_ssh_config_to___126____47__ssh__47__git-annex__47__config/comment_3_b00dce2374aac6968317d05d23bcfaf7._comment new file mode 100644 index 0000000000..3442fb2b2b --- /dev/null +++ b/doc/todo/Move_ssh_config_to___126____47__ssh__47__git-annex__47__config/comment_3_b00dce2374aac6968317d05d23bcfaf7._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawk3Wgg0XiqYFwM_Pw1RxZwlpNFi65g17sM" + nickname="James" + subject="comment 3" + date="2013-06-12T01:12:24Z" + content=""" +Ah, ok, I presumed there was an option in git to set a per-repository ssh command. I've looked at vcsh, but I'm not that confident with git remotes, so I don't use it (I use hg). If a per-repository ssh command added to git, would you consider adding this? +"""]] diff --git a/doc/todo/Move_ssh_config_to___126____47__ssh__47__git-annex__47__config/comment_4_743d0b077110c5cac1e2f47187b75333._comment b/doc/todo/Move_ssh_config_to___126____47__ssh__47__git-annex__47__config/comment_4_743d0b077110c5cac1e2f47187b75333._comment new file mode 100644 index 0000000000..5a22c98f72 --- /dev/null +++ b/doc/todo/Move_ssh_config_to___126____47__ssh__47__git-annex__47__config/comment_4_743d0b077110c5cac1e2f47187b75333._comment @@ -0,0 +1,10 @@ +[[!comment format=mdwn + username="http://joeyh.name/" + nickname="joey" + subject="comment 4" + date="2013-06-12T19:23:50Z" + content=""" +If it were sane, I'd probably use it. + +In the meantime, I'm moving this to [[todo]]. +"""]] diff --git a/doc/todo/Please_abort_build_if___34__make_test__34___fails.mdwn b/doc/todo/Please_abort_build_if___34__make_test__34___fails.mdwn new file mode 100644 index 0000000000..592b5e0773 --- /dev/null +++ b/doc/todo/Please_abort_build_if___34__make_test__34___fails.mdwn @@ -0,0 +1,7 @@ +A failure during "make test" should be signalled to the caller by means of +a non-zero exit code. Without that signal, it's very hard to run the +regression test suite in an automated fashion. + +> git-annex used to have a Makefile that ignored make test exit status, +> but that was fixed in commit dab5bddc64ab4ad479a1104748c15d194e138847, +> in October 6th. [[done]] --[[Joey]] diff --git a/doc/todo/Please_add_support_for_monad-control_0.3.x.mdwn b/doc/todo/Please_add_support_for_monad-control_0.3.x.mdwn new file mode 100644 index 0000000000..f822249916 --- /dev/null +++ b/doc/todo/Please_add_support_for_monad-control_0.3.x.mdwn @@ -0,0 +1,9 @@ +Git-annex doesn't compile with the latest version of monad-control. Would it be hard to support that new version? + +> I have been waiting for it to land in Debian before trying to +> deal with its changes. +> +> There is now a branch in git called `new-monad-control` that will build +> with the new monad-control. --[[Joey]] + +>> Now merged to master. [[done]] --[[Joey]] diff --git a/doc/todo/S3.mdwn b/doc/todo/S3.mdwn new file mode 100644 index 0000000000..7e417336f0 --- /dev/null +++ b/doc/todo/S3.mdwn @@ -0,0 +1,24 @@ +Support Amazon S3 as a file storage backend. + +There's a haskell library that looks good. Not yet in Debian. + +Multiple ways of using S3 are possible. Currently implemented as +a special type of git remote. + +Before this can be close, I need to fix: + +## encryption + +TODO + +## unused checking + +One problem is `git annex unused`. Currently it only looks at the local +repository, not remotes. But if something is dropped from the local repo, +and you forget to drop it from S3, cruft can build up there. + +This could be fixed by adding a hook to list all keys present in a remote. +Then unused could scan remotes for keys, and if they were not used locally, +offer the possibility to drop them from the remote. + +[[done]] diff --git a/doc/todo/Slow_transfer_for_a_lot_of_small_files..mdwn b/doc/todo/Slow_transfer_for_a_lot_of_small_files..mdwn new file mode 100644 index 0000000000..00cdad0fe1 --- /dev/null +++ b/doc/todo/Slow_transfer_for_a_lot_of_small_files..mdwn @@ -0,0 +1,20 @@ +What steps will reproduce the problem? +Sync a lot of small files. + +What is the expected output? What do you see instead? +The expected output is hopefully a fast transfer. + +But currently it seems like git-annex is only using one thread to transfer(per host or total?) + +An option to select number of transfer threads to use(possibly per host) would be very nice. + +> Opening a lot of connections to a single host is probably not desirable. +> +> I do want to do something to allow slow hosts to not hold up transfers to +> other hosts, which might involve running multiple queued transfers at +> once. The webapp already allows the user to force a given transfer to +> happen immediately. --[[Joey]] + +And maybe also an option to limit how long a queue the browser should show, it can become quite resource intensive with a long queue. + +> The queue is limited to 20 items for this reason. --[[Joey]] diff --git a/doc/todo/Use_MediaScannerConnection_on_Android.mdwn b/doc/todo/Use_MediaScannerConnection_on_Android.mdwn new file mode 100644 index 0000000000..afce9308d3 --- /dev/null +++ b/doc/todo/Use_MediaScannerConnection_on_Android.mdwn @@ -0,0 +1,7 @@ +Currently if photos or videos are copied into the Camera/DCIM directory on an Android device, or deleted the Gallery doesn't notice the changes. + +It is necessary to call MediaScannerConnection - http://developer.android.com/reference/android/media/MediaScannerConnection.html - to notify the system of the change. + +More info, and some sample Java code: http://stackoverflow.com/questions/13270789/how-to-run-media-scanner-in-android + +It'd be awesome if the assistant did this on files it has changed. Possibly just under Camera/DCIM, but perhaps it should be configurable. MediaScannerConnection is also used to notify and index new music files. diff --git a/doc/todo/Use_a_remote_as_a_sharing_site_for_files_with_obfuscated_URLs.mdwn b/doc/todo/Use_a_remote_as_a_sharing_site_for_files_with_obfuscated_URLs.mdwn new file mode 100644 index 0000000000..a42a81d02b --- /dev/null +++ b/doc/todo/Use_a_remote_as_a_sharing_site_for_files_with_obfuscated_URLs.mdwn @@ -0,0 +1,7 @@ +There are times when it is handy to be able to upload a file to a web host somewhere and share a link for that file to a select few people. + +It seems to be that the assistant could handle this scenario. It could generate a directory with a random name on the remote, and transfer the file there (using the existing filename) and the appropriate URL could be displayed in the assistant webapp to allow the user to copy the URL to send it to the appropriate people. + +Note: Joey and I had a quick chat about this use case at LCA2013. + +[[!tag design/assistant]] diff --git a/doc/todo/Use_a_remote_as_a_sharing_site_for_files_with_obfuscated_URLs/comment_1_1a1f34f4f389267d67e79409c0ca8b1d._comment b/doc/todo/Use_a_remote_as_a_sharing_site_for_files_with_obfuscated_URLs/comment_1_1a1f34f4f389267d67e79409c0ca8b1d._comment new file mode 100644 index 0000000000..35f7351915 --- /dev/null +++ b/doc/todo/Use_a_remote_as_a_sharing_site_for_files_with_obfuscated_URLs/comment_1_1a1f34f4f389267d67e79409c0ca8b1d._comment @@ -0,0 +1,9 @@ +[[!comment format=mdwn + username="http://edheil.wordpress.com/" + ip="99.54.57.201" + subject="comment 1" + date="2013-02-09T22:38:56Z" + content=""" +This would be an extremely cool feature to rip off from Dropbox. :) + +"""]] diff --git a/doc/todo/__96__git_annex_import_--lazy__96___--_Delete_everything_that__39__s_in_the_source_directory_and_also_in_the_target_annex.mdwn b/doc/todo/__96__git_annex_import_--lazy__96___--_Delete_everything_that__39__s_in_the_source_directory_and_also_in_the_target_annex.mdwn new file mode 100644 index 0000000000..c3f6816856 --- /dev/null +++ b/doc/todo/__96__git_annex_import_--lazy__96___--_Delete_everything_that__39__s_in_the_source_directory_and_also_in_the_target_annex.mdwn @@ -0,0 +1,26 @@ +As per IRC + + 22:13:10 < RichiH> joeyh: btw, i have been pondering a `git annex import --lazy` or some such which basically goes through a directory and deletes everything i find in the annex it run from + 22:50:39 < joeyh> not sure of the use case + 23:41:06 < RichiH> joeyh: the use case is "i have important a ton of data into my annexes. now, i am going through the usual crud of cp -ax'ed, rsync'ed, and other random 'new disk, move stuff around and just put a full dump over there' file dumps and would like to delete everything that's annexed already" + 23:41:33 < RichiH> joeyh: that would allow me to spend time on dealing with the files which are not yet annexed + 23:41:54 < RichiH> instead of verifying file after file which has been imported already + 23:43:19 < joeyh> have you tried just running git annex import in a subdirectory and then deleting the dups? + 23:45:34 < joeyh> or in a separate branch for that matter, which you could then merge in, etc + 23:54:08 < joeyh> Thinking anout it some more, it would need to scan the whole work tree to see what keys were there, and populate a lookup table. I prefer to avoid things that need git-annex to do such a large scan and use arbitrary amounts of memory. + 00:58:11 < RichiH> joeyh: that would force everything into the annex, though + 00:58:20 < RichiH> a plain import, that is + 00:58:53 < RichiH> in a usual data dump directory, there's tons of stuff i will never import + 00:59:00 < RichiH> i want to delete large portions of it + 00:59:32 < RichiH> but getting rid of duplicates first allows me to spend my time focused on stuff humans are good at: deciding + 00:59:53 < RichiH> whereas the computer can focus on stuff it's good at: mindless comparision of bits + 01:00:15 < RichiH> joeyh: as you're saying this is complex, maybe i need to rephrase + 01:01:40 < RichiH> what i envision is git annex import --foo to 1) decide what hashing algorithm should be used for this file 2) hash that file 3) look into the annex if that hash is annexed 3a) optionally verify numcopies within the annex 4) delete the file in the source directory + 01:01:47 < RichiH> and then move on to the next file + 01:02:00 < RichiH> if the hash does not exist in the annex, leave it alone + 01:02:50 < RichiH> if the hash exists in annex, but numcopies is not fulfilled, just import it as a normal import would + 01:03:50 < RichiH> that sounds quite easy, to me; in fact i will prolly script it if you decide not to implement it + 01:04:07 < RichiH> but i think it's useful for a _lot_ of people who migrate tons of data into annexes + 01:04:31 < RichiH> thus i would rather see this upstream and not hacked locally + +The only failure mode I see in the above is "file has been dropped elsewhere, numcopies not fulfilled, but that info is not synched to the local repo, yet" -- This could be worked around by always importing the data. diff --git a/doc/todo/__96__git_annex_import_--lazy__96___--_Delete_everything_that__39__s_in_the_source_directory_and_also_in_the_target_annex/comment_1_0cc16eb17151309113cec6d1cccf203d._comment b/doc/todo/__96__git_annex_import_--lazy__96___--_Delete_everything_that__39__s_in_the_source_directory_and_also_in_the_target_annex/comment_1_0cc16eb17151309113cec6d1cccf203d._comment new file mode 100644 index 0000000000..0b4e22e7c5 --- /dev/null +++ b/doc/todo/__96__git_annex_import_--lazy__96___--_Delete_everything_that__39__s_in_the_source_directory_and_also_in_the_target_annex/comment_1_0cc16eb17151309113cec6d1cccf203d._comment @@ -0,0 +1,20 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawl9sYlePmv1xK-VvjBdN-5doOa_Xw-jH4U" + nickname="Richard" + subject="comment 1" + date="2013-08-06T14:22:03Z" + content=""" +To expand a bit on the use case: + +I have several migration directories which I simply moved to new systems or disks with the help of `cp -ax` or `rsync`. +As I don't _need_ the data per se and merely want to hold on to it in case I ever happen to need it again and as disk space is laughably cheap, I have a lot of duplicates. +While I can at least detect bit flips with the help of checksum lists, cleaning those duplicates of duplicated duplicates is quite some effort. +To make things worse, photos, music, videos, letter and whatnot are thrown into the same container directories. + +All in all, getting data out of those data dumps and into a clean structure is quite an effort. +`git annex import --lazy` would help with this effort as I could start with the first directory, sort stuff by hand, and annex it. +As soon as data lives in any of my annexes, I could simply run `git annex import --lazy` to get rid of all duplicates while retaining the unannexed files. +Iterating through this process a few times, I will be left with clean annexes on the one hand and stuff I can simply delete on the other hand. + +I could script all this by hand on my own machine, but I am _certain_ that others would find easy, integrated, and unit tested support for whittling down data dumps over time useful. +"""]] diff --git a/doc/todo/add_--exclude_option_to_git_annex_find.mdwn b/doc/todo/add_--exclude_option_to_git_annex_find.mdwn new file mode 100644 index 0000000000..a797e97f58 --- /dev/null +++ b/doc/todo/add_--exclude_option_to_git_annex_find.mdwn @@ -0,0 +1,4 @@ +Seems pretty self-explanatory. + +> This was already implemented, the --exclude option can be used +> for find as well as most any other subcommand. --[[Joey]] [[done]] diff --git a/doc/todo/add_-all_option.mdwn b/doc/todo/add_-all_option.mdwn new file mode 100644 index 0000000000..2f25759c21 --- /dev/null +++ b/doc/todo/add_-all_option.mdwn @@ -0,0 +1,22 @@ +`--all` would make git-annex operate on either every key with content +present (or in some cases like `get` and `copy --from` on +every keys with content not present). + +This would be useful when a repository has a history with deleted files +whose content you want to keep (so you're not using `dropunused`). +Or when you have a lot of branches and just want to be able to fsck +every file referenced in any branch (or indeed, any file referenced in any +ref). It could also be useful (or even a +good default) in a bare repository. + +A problem with the idea is that `.gitattributes` values for keys not +currently in the tree would not be available (without horrific anounts of +grubbing thru history to find where/when the key used to exist). So +`numcopies` set via `.gitattributes` would not work. This would be a +particular problem for `drop` and for `--auto`. + +--[[Joey]] + +> [[done]]. The .gitattributes problem was solved simply by not +> supporting `drop --all`. `--auto` also cannot be mixed with --all for +> similar reasons. --[[Joey]] diff --git a/doc/todo/add_a_git_backend.mdwn b/doc/todo/add_a_git_backend.mdwn new file mode 100644 index 0000000000..2b224710ed --- /dev/null +++ b/doc/todo/add_a_git_backend.mdwn @@ -0,0 +1,18 @@ +There should be a backend where the file content is stored.. in a git +repository! + +This way, you know your annexed content is safe & versioned, but you only +have to deal with the pain of git with large files in one place, and can +use all of git-annex's features everywhere else. + +> Speaking as a future user, do very, very much want. -- RichiH + +>> Might also be interesting to use `bup` in the git backend, to work +>> around git's big file issues there. So git-annex would pull data out +>> of the git backend using bup. --[[Joey]] + +>>> Very much so. Generally speaking, having one or more versioned storage back-ends with current data in the local annexes sounds incredibly useful. Still being able to get at old data in via the back-end and/or making offline backups of the full history are excellent use cases. -- RichiH + +[[done]], the bup special remote type is written! --[[Joey]] + +> Yay! -- RichiH diff --git a/doc/todo/add_an_icon_for_the_.desktop_file.mdwn b/doc/todo/add_an_icon_for_the_.desktop_file.mdwn new file mode 100644 index 0000000000..3be158a0aa --- /dev/null +++ b/doc/todo/add_an_icon_for_the_.desktop_file.mdwn @@ -0,0 +1 @@ +Maybe add the icon /usr/share/doc/git-annex/html/logo.svg to the .desktp file. diff --git a/doc/todo/add_metadata_to_annexed_files.mdwn b/doc/todo/add_metadata_to_annexed_files.mdwn new file mode 100644 index 0000000000..0fc3e89535 --- /dev/null +++ b/doc/todo/add_metadata_to_annexed_files.mdwn @@ -0,0 +1,5 @@ +I would like to attach metadata to annexed files (objects) without cluttering the workdir with files containing this metadata. A common use case would be to add titles to my photo collection that could than end up in a generated photo album. + +Depending on the implementation it might also be possible to use the metadata facility for a threaded commenting system. + +The first question is whether the metadata is attached to the objects and thus shared by all paths pointing to the same data object or to paths in the worktree. I've no preference here at this point. diff --git a/doc/todo/assistant_cannot_set_up_remote_repo_via_an_ssh_alias_or_an_ip_address.mdwn b/doc/todo/assistant_cannot_set_up_remote_repo_via_an_ssh_alias_or_an_ip_address.mdwn new file mode 100644 index 0000000000..a7b30ded5c --- /dev/null +++ b/doc/todo/assistant_cannot_set_up_remote_repo_via_an_ssh_alias_or_an_ip_address.mdwn @@ -0,0 +1,48 @@ +What steps will reproduce the problem? + +Using the assistant, create an SSH remote. Try to use an alias as the name +of the remote (e.g. I have a server which I have aliased to "homeworld" in +my .ssh/config. When I'm at home, that is an alias for 192.168.1.253. +When I'm not at home, I edit .ssh/config so that "homeworld" becomes an +alias for a hostname at no-ip.com.) Despite the fact that "homeworld" is a +viable ssh target because of the alias, the assistant doesn't recognize it +as a valid host to ssh to. + +I had trouble with an ip address the first time I tried it but just tried +it again and it worked fine, so please disregard that part of the title of +this bug report. + + +What is the expected output? What do you see instead? + +expected output = move to the "create a repository -- rsync or regular" page. +observed output = "cannot resolve host name" + + +What version of git-annex are you using? On what operating system? + + Version: 3.20130102 OS X Lion + + +Please provide any additional information below. + +I realize this is kind of a power user whine. Using an ssh alias which +does not correspond to an actual resolvable hostname (and cannot, because +it's supposed to be a layer of indirection over the hostname) is not an +everyday problem for an average user. + +> The assistant tries to resolve the hostname explicitly +> to catch user's typos, and also expands it to a FQDN, to make +> it more likely to be able to reach the host when roaming to other +> networks. +> +> Also, the assistant sets up it *own* .ssh/config hostname alias, +> in order to make it use the special ssh key that it generates for the host. +> So that is not compatable with using a ssh host alias you've set up. +> Even if it knew about your alias, it would set up a new hostname alias, and +> whatever machinery you have to update the alias would not work. +> +> You can, of course, add git remotes using any ssh alias you like, by +> hand, and restart the assistant and it will use them. --[[Joey]] + +[[!tag /design/assistant]] diff --git a/doc/todo/assistant_git_sync_laddering.mdwn b/doc/todo/assistant_git_sync_laddering.mdwn new file mode 100644 index 0000000000..7dbc6480a2 --- /dev/null +++ b/doc/todo/assistant_git_sync_laddering.mdwn @@ -0,0 +1,10 @@ +When the [[design/assistant]] is running on a pair of remotes, I've seen +them get out of sync, such that every pull and merge results in a conflict, +that then has to be auto-resolved. + +This seems similar to the laddering problem described in this old bug: +[[bugs/making_annex-merge_try_a_fast-forward]] + +--[[Joey]] + +Think I've fixed this. [[done]] --[[Joey]] diff --git a/doc/todo/assistant_parallel_file_transfers.txt b/doc/todo/assistant_parallel_file_transfers.txt new file mode 100644 index 0000000000..aafddf0383 --- /dev/null +++ b/doc/todo/assistant_parallel_file_transfers.txt @@ -0,0 +1,15 @@ +Hi and thank you for an incredible piece of software and great work! + +I've noticed that when I add new files to a repository and I have my USB drive connected, the assistant alternate it's transfers of files. And only transfers one queued file at the time. + +file1 -->> Internet offsite computer +file1 -->> USB drive +file2 -->> Internet offsite computer +file2 -->> USB drive + + +I would prefer a logic where the assistant transfer files in parallel to my different repositories. I know that it might not be a good thing doing that with network accessed repositories, but when I have "low cost", locally attached USB drives it would be great if the transfers could be done in parallel. + + +Is there a configuration option for this already? + diff --git a/doc/todo/assistant_smarter_archive_directory_handling.mdwn b/doc/todo/assistant_smarter_archive_directory_handling.mdwn new file mode 100644 index 0000000000..fa5a3e4fce --- /dev/null +++ b/doc/todo/assistant_smarter_archive_directory_handling.mdwn @@ -0,0 +1,31 @@ +Client repos do not want files in archive directories. This can turn +out to be confusing to users who are using archive directories for their +own purposes and not aware of this special case in the assistant. It can +seem like the assistant is failing to sync their files. + +I thought, first, that it should have a checkbox to enable the archive +directory behavior. + +However, I think I have a better idea. Change the preferred content +expression for clients, so they want files in archive directories, *until* +those files land in an archive. + +This way, only users who set up an archive repo get this behavior. And they +asked for it by setting up that repo! + +Also, the new behavior will mean that files in archive directories still +propigate around to clients. Consider this topology: + + client A ---- client B ---- archive + +If a file is created in client A, and moved to an archive directory before +it syncs to B, it will never get to the archive, and will continue wasting +space on A. With the new behavior, A and B serve as effectively, transfer +repositories for archived content. + +Something vaguely like this should work as the preferred content +expression for the clients: + + exclude=archive/* or (include=archive/* and (not (copies=archive:1 or copies=smallarchive:1))) + +> [[done]] --[[Joey]] diff --git a/doc/todo/assistant_threaded_runtime.mdwn b/doc/todo/assistant_threaded_runtime.mdwn new file mode 100644 index 0000000000..03ba66acf1 --- /dev/null +++ b/doc/todo/assistant_threaded_runtime.mdwn @@ -0,0 +1,40 @@ +The [[design/assistant]] would be better if git-annex used ghc's threaded +runtime (`ghc -threaded`). + +Currently, whenever the assistant code runs some external command, all +threads are blocked waiting for it to finish. + +For transfers, the assistant works around this problem by forking separate +upload processes, and not waiting on them until it sees an indication that +they have finished the transfer. While this works, it's messy.. threaded +would be better. + +When pulling, pushing, and merging, the assistant runs external git +commands, and this does block all other threads. The threaded runtime would +really help here. + +[[done]]; the assistant now builds with the threaded runtime. +Some work still remains to run certian long-running external git commands +in their own threads to prevent them blocking things, but that is easy to +do, now. --[[Joey]] + +--- + +Currently, git-annex seems unstable when built with the threaded runtime. +The test suite tends to hang when testing add. `git-annex` occasionally +hangs, apparently in a futex lock. This is not the assistant hanging, and +git-annex does not otherwise use threads, so this is surprising. --[[Joey]] + +> I've spent a lot of time debugging this, and trying to fix it, in the +> "threaded" branch. There are still deadlocks. --[[Joey]] + +>> Fixed, by switching from `System.Cmd.Utils` to `System.Process` +>> --[[Joey]] + +--- + +It would be possible to not use the threaded runtime. Instead, we could +have a child process pool, with associated continuations to run after a +child process finishes. Then periodically do a nonblocking waitpid on each +process in the pool in turn (waiting for any child could break anything not +using the pool!). This is probably a last resort... diff --git a/doc/todo/auto_remotes.mdwn b/doc/todo/auto_remotes.mdwn new file mode 100644 index 0000000000..715dea7207 --- /dev/null +++ b/doc/todo/auto_remotes.mdwn @@ -0,0 +1,29 @@ +It should be possible for clones to learn about how to contact +each other without remotes needing to always be explicitly set +up. Say that `.git-annex/remote.log` is maintained by git-annex +to contain: + + UUID hostname URI + +The URI comes from configured remotes and maybe from +`file://$(pwd)`, or even `ssh://$(hostname -f)` +for the current repo. This format will merge without +conflicts or data loss. + +Then when content is belived to be in a UUID, and no +configured remote has it, the remote.log can be consulted and +URIs that look likely tried. (file:// ones if the hostname +is the same (or maybe always -- a removable drive might tend +to be mounted at the same location on different hosts), +otherwise ssh:// ones.) + +Question: When should git-annex update the remote.log? +(If not just on init.) Whenever it reads in a repo's remotes? + +> This sounds useful and the log should be updated every time any remote is being accessed. A counter or timestamp (yes, distributed times may be wrong/different) could be used to auto-prune old entries via a global and per-remote config setting. -- RichiH + +--- + +I no longer think I'd use this myself, I find that my repositories quickly +grow the paths I actually use, somewhat organically. Unofficial paths +across university quads come to mind. [[done]] --[[Joey]] diff --git a/doc/todo/auto_remotes/discussion.mdwn b/doc/todo/auto_remotes/discussion.mdwn new file mode 100644 index 0000000000..b9e1522a8f --- /dev/null +++ b/doc/todo/auto_remotes/discussion.mdwn @@ -0,0 +1,7 @@ +Remotes log should probably be stored in ".git/annex/remote.log" +instead of ".git-annex/remote.log" to prevent leaking credentials. + +> The idea is to distribute the info between repositories, which is +> why it'd go in `.git-annex`. Of course that does mean that repository +> location information would be included, and if that'd not desirable +> this feature would need to be turned off. --[[Joey]] diff --git a/doc/todo/automatic_bookkeeping_watch_command.mdwn b/doc/todo/automatic_bookkeeping_watch_command.mdwn new file mode 100644 index 0000000000..28b02aff25 --- /dev/null +++ b/doc/todo/automatic_bookkeeping_watch_command.mdwn @@ -0,0 +1,15 @@ +A "git annex watch" command would help make git-annex usable by users who +don't know how to use git, or don't want to bother typing the git commands. +It would run, in the background, watching via inotify for changes, and +automatically annexing new files, etc. + +The blue sky goal would be something automated like dropbox, except fully +distributed. All files put into the repository would propagate out +to all the other clones of it, as network links allow. Note that while +dropbox allows modifying files, git-annex freezes them upon creation, +so this would not be 100% equivalent to dropbox. --[[Joey]] + +This is a big project with its own [[design pages|design/assistant]]. + +> [[done]].. at least, we have a watch command an an assistant, which +> is still being developed. --[[Joey]] diff --git a/doc/todo/automatic_merge_of_synced_branches_upon___34__git_annex_sync__34__.mdwn b/doc/todo/automatic_merge_of_synced_branches_upon___34__git_annex_sync__34__.mdwn new file mode 100644 index 0000000000..361585a782 --- /dev/null +++ b/doc/todo/automatic_merge_of_synced_branches_upon___34__git_annex_sync__34__.mdwn @@ -0,0 +1,16 @@ +When maintaining several replica of the same git-annex repo "git annex sync" is quite handy. +But it would be even handier if "git annex sync" would also perform automatic "git merge synced/*" actions on all remotes. + +Clearly, this is beneficial when the user wants to keep all working copies synchronized. +This is likely the case in git annex assistant like scenarios. And it's always the case in my day to day scenarios :-) +I'm not sure about other use cases that I've hard time imagining... + +As just discussed on IRC (#vcs-home/OFTC), this could be implemented in various ways: + +1) By doing ssh on each remote and running the appropriate "git merge ..." commands there. + The drawback of this is that quite often it won't be permitted to ssh on the remote and run arbitrary commands there. + +2) Having a default post-receive hook, created at the time of "git annex init" that automatically does the merges when contacted by other remotes as a consequence of "git annex sync". + + +Thanks for git-annex! diff --git a/doc/todo/avoid_unnecessary_union_merges.mdwn b/doc/todo/avoid_unnecessary_union_merges.mdwn new file mode 100644 index 0000000000..5cd4b64373 --- /dev/null +++ b/doc/todo/avoid_unnecessary_union_merges.mdwn @@ -0,0 +1,20 @@ +Some commands cause a union merge unnecessarily. For example, `git annex add` +modifies the location log, which first requires reading the current log (if +any), which triggers a merge. + +Would be good to avoid these unnecessary union merges. First because it's +faster and second because it avoids a possible delay when a user might +ctrl-c and leave the repo in an inconsistent state. In the case of an add, +the file will be in the annex, but no location log will exist for it (fsck +fixes that). + +It may be that all that's needed is to modify Annex.Branch.change +to read the current value, without merging. Then commands like `get`, that +query the branch, will still cause merges, and commands like `add` that +only modify it, will not. Note that for a command like `get`, the merge +occurs before it has done anything, so ctrl-c should not be a problem +there. + +This is a delicate change, I need to take care.. --[[Joey]] + +> [[done]] (assuming I didn't miss any cases where this is not safe!) --[[Joey]] diff --git a/doc/todo/backendSHA1.mdwn b/doc/todo/backendSHA1.mdwn new file mode 100644 index 0000000000..8c16b75ad0 --- /dev/null +++ b/doc/todo/backendSHA1.mdwn @@ -0,0 +1,7 @@ +This backend is not finished. + +In particular, while files can be added using it, git-annex will not notice +when their content changes, and will not create a new key for the new sha1 +of the net content. + +[[done]]; use unlock subcommand and commit changes with git diff --git a/doc/todo/branching.mdwn b/doc/todo/branching.mdwn new file mode 100644 index 0000000000..ad7ece6f10 --- /dev/null +++ b/doc/todo/branching.mdwn @@ -0,0 +1,159 @@ +[[done]] !!! + +The use of `.git-annex` to store logs means that if a repo has branches +and the user switched between them, git-annex will see different logs in +the different branches, and so may miss info about what remotes have which +files (though it can re-learn). + +An alternative would be to store the log data directly in the git repo +as `pristine-tar` does. Problem with that approach is that git won't merge +conflicting changes to log files if they are not in the currently checked +out branch. + +It would be possible to use a branch with a tree like this, to avoid +conflicts: + +key/uuid/time/status + +As long as new files are only added, and old timestamped files deleted, +there would be no conflicts. + +A related problem though is the size of the tree objects git needs to +commit. Having the logs in a separate branch doesn't help with that. +As more keys are added, the tree object size will increase, and git will +take longer and longer to commit, and use more space. One way to deal with +this is simply by splitting the logs amoung subdirectories. Git then can +reuse trees for most directories. (Check: Does it still have to build +dup trees in memory?) + +Another approach would be to have git-annex *delete* old logs. Keep logs +for the currently available files, or something like that. If other log +info is needed, look back through history to find the first occurance of a +log. Maybe even look at other branches -- so if the logs were on master, +a new empty branch could be made and git-annex would still know where to +get keys in that branch. + +Would have to be careful about conflicts when deleting and bringing back +files with the same name. And would need to avoid expensive searching thru +all history to try to find an old log file. + +## fleshed out proposal + +Let's use one branch per uuid, named git-annex/$UUID. + +- I came to realize this would be a good idea when thinking about how + to upgrade. Each individual annex will be upgraded independantly, + so each will want to make a branch, and if the branches aren't distinct, + they will merge conflict for sure. +- TODO: What will need to be done to git to make it push/pull these new + branches? +- A given repo only ever writes to its UUID branch. So no conflicts. + - **problem**: git annex move needs to update log info for other repos! + (possibly solvable by having git-annex-shell update the log info + when content is moved using it) +- (BTW, UUIDs probably don't compress well, and this reduces the bloat of having + them repeated lots of times in the tree.) +- Per UUID branches mean that if it wants to find a file's location + amoung configured remotes, it can examine only their branches, if + desired. +- It's important that the per-repo branches propigate beyond immediate + remotes. If there is a central bare repo, that means push --all. Without + one, it means that when repo B pulls from A, and then C pulls from B, + C needs to get A's branch -- which means that B should have a tracking + branch for A's branch. + +In the branch, only one file is needed. Call it locationlog. git-annex +can cache location log changes and write them all to locationlog in +a single git operation on shutdown. + +- TODO: what if it's ctrl-c'd with changes pending? Perhaps it should + collect them to .git/annex/locationlog, and inject that file on shutdown? +- This will be less overhead than the current staging of all the log files. + +The log is not appended to, so in git we have a series of commits each of +which replaces the log's entire contens. + +To find locations of a key, all (or all relevant) branches need to be +examined, looking backward through the history of each until a log +with a indication of the presense/absense of the key is found. + +- This will be less expensive for files that have recently been added + or transfered. +- It could get pretty slow when digging deeper. +- Only 3 places in git-annex will be affected by any slowdown: move --from, + get and drop. (Update: Now also unused, whereis, fsck) + +## alternate + +As above, but use a single git-annex branch, and keep the per-UUID +info in their own log files. Hope that git can auto-merge as long as +each observing repo only writes to its own files. (Well, it can, but for +non-fast-forward merges, the git-annex branch would need to be checked out, +which is problimatic.) + +Use filenames like: + + / + +That allows one repo to record another's state when doing a +`move`. + +## outside the box approach + +If the problem is limited to only that the `.git-annex/` files make +branching difficult (and not to the related problem that commits to them +and having them in the tree are sorta annoying), then a simple approach +would be to have git-annex look in other branches for location log info +too. + +The problem would then be that any locationlog lookup would need to look in +all other branches (any branch could have more current info after all), +which could get expensive. + +## way outside the box approach + +Another approach I have been mulling over is keeping the log file +branch checked out in .git/annex/logs/ -- this would be a checkout of a git +repository inside a git repository, using "git fake bare" techniques. This +would solve the merge problem, since git auto merge could be used. It would +still mean all the log files are on-disk, which annoys some. It would +require some tighter integration with git, so that after a pull, the log +repo is updated with the data pulled. --[[Joey]] + +> Seems I can't use git fake bare exactly. Instead, the best option +> seems to be `git clone --shared` to make a clone that uses +> `.git/annex/logs/.git` to hold its index etc, but (mostly) uses +> objects from the main repo. There would be some bloat, +> as commits to the logs made in there would not be shared with the main +> repo. Using `GIT_OBJECT_DIRECTORY` might be a way to avoid that bloat. + +## notes + +Another approach could be to use git-notes. It supports merging branches +of notes, with union merge strategy (a hook would have to do this after +a pull, it's not done automatically). + +Problem: Notes are usually attached to git +objects, and there are no git objects corresponding to git-annex keys. + +Problem: Notes are not normally copied when cloning. + +------ + +## elminating the merge problem + +Most of the above options are complicated by the problem of how to merge +changes from remotes. It should be possible to deal with the merge +problem generically. Something like this: + +* We have a local branch `B`. +* For remotes, there are also `origin/B`, `otherremote/B`, etc. +* To merge two branches `B` and `foo/B`, construct a merge commit that + makes each file have all lines that were in either version of the file, + with duplicates removed (probably). Do this without checking out a tree. + -- now implemented as git-union-merge +* As a `post-merge` hook, merge `*/B` into `B`. This will ensure `B` + is always up-to-date after a pull from a remote. +* When pushing to a remote, nothing need to be done, except ensure + `B` is either successfully pushed, or the push fails (and a pull needs to + be done to get the remote's changes merged into `B`). diff --git a/doc/todo/cache_key_info.mdwn b/doc/todo/cache_key_info.mdwn new file mode 100644 index 0000000000..d4352ccf7f --- /dev/null +++ b/doc/todo/cache_key_info.mdwn @@ -0,0 +1,37 @@ +Most of git-annex is designed to be fast no matter how many other files are +in the annex. Things like add/get/drop/move/fsck have good locality; +they will only operate on as many files as you need them to. + +(git commit can get a little slow with a great deal of files, +but that's out of scope -- and recent git-annex versions use queuing +to save git add from piling up too much in the index.) + +But currently two git-annex commands are quite slow when annexes become large +in quantity of files. These are unused and status. +(Both have --fast versions that don't do as much). +> (Update: status has become acceptably fast; most of its slowdown was due to using a bad data structure; scanning the tree is not particularly slow and it no longer looks at the git-annex branch.) + +unused is slow because it needs two pieces of information that are not +quick to look up, and require examining the whole repo, very seekily: + +1. The keys present in the annex. Found by looking thru .git/annex/objects +2. The keys referenced by files in git. Found by finding every file + in git, and looking at its symlink. + +Of these, the first is less expensive (typically, an annex does not have every +key in it). It could be optimized fairly simply, by adding a database +of keys present in the annex that is optimised to list them all. The +database would be updated by the few functions that move content in and +out. + +The second is harder to optimise, because the user can delete, revert, +copy, add, etc files in git at will, and git-annex does not have a good way +to watch that and maintain a database of what keys are being referenced. + +It could use a post-commit hook and examine files changed by commits, etc. +But then staged files would be left out. It might be sufficient to +make --fast trust the database... except unused will suggest *deleting* +data if nothing references it. Or maybe it could be required to have a +clean tree with nothing staged before running git-annex unused. + +Anyway, this is a semi-longterm item for me. --[[Joey]] diff --git a/doc/todo/cache_key_info/comment_1_578df1b3b2cbfdc4aa1805378f35dc48._comment b/doc/todo/cache_key_info/comment_1_578df1b3b2cbfdc4aa1805378f35dc48._comment new file mode 100644 index 0000000000..086e7f3e84 --- /dev/null +++ b/doc/todo/cache_key_info/comment_1_578df1b3b2cbfdc4aa1805378f35dc48._comment @@ -0,0 +1,11 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawl9sYlePmv1xK-VvjBdN-5doOa_Xw-jH4U" + nickname="Richard" + subject="comment 1" + date="2011-05-17T07:27:02Z" + content=""" +Sounds like a good idea. + +* git annex fsck (or similar) should check/rebuild the caches +* I would simply require a clean tree with a verbose error. 80/20 rule and defaulting to save actions. +"""]] diff --git a/doc/todo/checkout.mdwn b/doc/todo/checkout.mdwn new file mode 100644 index 0000000000..50da2d62e1 --- /dev/null +++ b/doc/todo/checkout.mdwn @@ -0,0 +1,23 @@ +The checkout subcommand replaces the symlink that normally points at a +file's content, with a copy of the file. Once you've checked a file out, +you can edit it, and `git commit` it. On commit, git-annex will detect +if the file has been changed, and if it has, `add` its content to the +annex. + +> Internally, this will need to store the original symlink to the file, in +> `.git/annex/checkedout/$filename`. +> +> * git-annex uncheckout moves that back +> * git-annex pre-commit hook checks each file being committed to see if +> it has a symlink there, and if so, removes the symlink and adds the new +> content to the annex. +> +> And it seems the file content should be copied, not moved or hard linked: +> +> * Makes sure other annexes can find it if transferring it from +> this annex. +> * Ensures it's always available for uncheckout. +> * Avoids the last copy of a file's content being lost when +> the checked out file is modified. + +[[done]] diff --git a/doc/todo/direct_mode_guard.mdwn b/doc/todo/direct_mode_guard.mdwn new file mode 100644 index 0000000000..7322cec632 --- /dev/null +++ b/doc/todo/direct_mode_guard.mdwn @@ -0,0 +1,22 @@ +Currently [[/direct_mode]] allows the user to point many normally safe +git commands at his foot and pull the trigger. At LCA2013, a git-annex +user suggested modifying direct mode to make this impossible. + +One way to do it would be to move the .git directory. Instead, make there +be a .git-annex directory in direct mode repositories. git-annex would know +how to use it, and would be extended to support all known safe git +commands, passing parameters through, and in some cases verifying them. + +So, for example, `git annex commit` would run `git commit --git-dir=.git-annex` + +However, `git annex commit -a` would refuse to run, or even do something +intelligent that does not involve staging every direct mode file. + +---- + +One source of problems here is that there is some overlap between git-annex +and git commands. Ie, `git annex add` cannot be a passthrough for `git +add`. The git wrapper could instead be another program, or it could be +something like `git annex git add` + +--[[Joey]] diff --git a/doc/todo/direct_mode_guard/comment_1_431b6e1577bbd30b07dce9002a8fe1a2._comment b/doc/todo/direct_mode_guard/comment_1_431b6e1577bbd30b07dce9002a8fe1a2._comment new file mode 100644 index 0000000000..01cddc8a3e --- /dev/null +++ b/doc/todo/direct_mode_guard/comment_1_431b6e1577bbd30b07dce9002a8fe1a2._comment @@ -0,0 +1,10 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawn-KDr_Z4CMkjS0v_TxQ08SzAB5ecHG3K0" + nickname="Glen" + subject="This sounds good" + date="2013-06-25T10:30:07Z" + content=""" +I think we might have been talking about this feature.. Seems like a good idea to me. + +Glen +"""]] diff --git a/doc/todo/done.mdwn b/doc/todo/done.mdwn new file mode 100644 index 0000000000..e7c98081b7 --- /dev/null +++ b/doc/todo/done.mdwn @@ -0,0 +1,4 @@ +recently fixed [[todo]] items. + +[[!inline pages="./* and link(./done) and !*/Discussion" sort=mtime show=10 +archive=yes]] diff --git a/doc/todo/exclude_files_on_a_given_remote.mdwn b/doc/todo/exclude_files_on_a_given_remote.mdwn new file mode 100644 index 0000000000..e8bb357d31 --- /dev/null +++ b/doc/todo/exclude_files_on_a_given_remote.mdwn @@ -0,0 +1,18 @@ +Say I have some files on remote A. But I'm away from it, and transferring +files from B to C. I'd like to avoid transferring any files I already have +on A. + +Something like: + + git annex copy --to C --exclude-on A + +This would not contact A, just use its cached location log info. + +I suppose I might also sometime want to only act on files that are +thought/known to be on A. + + git annex drop --only-on A + +--[[Joey]] + +[[done]] diff --git a/doc/todo/faster_gnupg_cipher.mdwn b/doc/todo/faster_gnupg_cipher.mdwn new file mode 100644 index 0000000000..a1cfd428d2 --- /dev/null +++ b/doc/todo/faster_gnupg_cipher.mdwn @@ -0,0 +1 @@ +Apparently newer gnupg has support for hardware-accelerated AES-NI. It would be good to have an option to use that. I also wonder if using the same symmetric key for many files presents a security issues (and whether using GPG keys directly would be more secure). diff --git a/doc/todo/faster_gnupg_cipher/comment_1_8f61f7c724a8224e61c015be68f43db7._comment b/doc/todo/faster_gnupg_cipher/comment_1_8f61f7c724a8224e61c015be68f43db7._comment new file mode 100644 index 0000000000..1bf550cdf4 --- /dev/null +++ b/doc/todo/faster_gnupg_cipher/comment_1_8f61f7c724a8224e61c015be68f43db7._comment @@ -0,0 +1,14 @@ +[[!comment format=mdwn + username="http://joeyh.name/" + ip="4.152.108.145" + subject="comment 1" + date="2013-08-01T17:10:56Z" + content=""" +There is a remote.name.annex-gnupg-options git-config setting that can be used to pass options to gpg on a per-remote basis. + +> also wonder if using the same symmetric key for many files presents a security issues (and whether using GPG keys directly would be more secure). + +I am not a cryptographer, but I have today run this question by someone with a good amount of crypo knowledge. My understanding is that reusing a symmetric key is theoretically vulnerable to eg known-plaintext or chosen-plaintext attacks. And that modern ciphers like AES and CAST (gpg default) are designed to resist such attacks. + +If someone was particularly concerned about these attack vectors, it would be pretty easy to add a mode where git-annex uses public key encryption directly. With the disadvantage, of course, that once a file was sent to a special remote and encrypted for a given set of public keys, other keys could not later be granted access to it. +"""]] diff --git a/doc/todo/faster_gnupg_cipher/comment_2_36e1f227a320527653500b445f7c001c._comment b/doc/todo/faster_gnupg_cipher/comment_2_36e1f227a320527653500b445f7c001c._comment new file mode 100644 index 0000000000..08f69d6b86 --- /dev/null +++ b/doc/todo/faster_gnupg_cipher/comment_2_36e1f227a320527653500b445f7c001c._comment @@ -0,0 +1,12 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawl9sYlePmv1xK-VvjBdN-5doOa_Xw-jH4U" + nickname="Richard" + subject="comment 2" + date="2013-08-02T07:21:50Z" + content=""" +Using symmetric keys is significantly cheaper, computation-wise. + +The scheme of encrypting symmetric keys with asymmetric ones is ancient, well-proven, and generally accepted as a good approach. + +Using per-key files makes access control more fine-grained and is only a real performance issue once while creating the private key and a little bit every time more than one file needs to be decrypted as more than one symmetric key needs to be taken care of. +"""]] diff --git a/doc/todo/faster_rsync_remotes.mdwn b/doc/todo/faster_rsync_remotes.mdwn new file mode 100644 index 0000000000..5ece25008b --- /dev/null +++ b/doc/todo/faster_rsync_remotes.mdwn @@ -0,0 +1 @@ +Using an rsync remote is currently very slow when there are a lot of files, since rsync appears to be called for each file copied. It would be awesome if each call to rsync was amortized to copy many files; rsync is very good at copying many small files quickly. diff --git a/doc/todo/faster_rsync_remotes/comment_1_0bc3ee0ae563357675eeccf42461e59a._comment b/doc/todo/faster_rsync_remotes/comment_1_0bc3ee0ae563357675eeccf42461e59a._comment new file mode 100644 index 0000000000..2f320fee2c --- /dev/null +++ b/doc/todo/faster_rsync_remotes/comment_1_0bc3ee0ae563357675eeccf42461e59a._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="http://joeyh.name/" + ip="4.152.108.145" + subject="comment 1" + date="2013-08-01T16:06:42Z" + content=""" +I cannot see a way to do this using rsync's current command-line interface. Ideas how to do it welcomed. +"""]] diff --git a/doc/todo/faster_rsync_remotes/comment_2_ccf6f75450c89ca498c8130054f8d32d._comment b/doc/todo/faster_rsync_remotes/comment_2_ccf6f75450c89ca498c8130054f8d32d._comment new file mode 100644 index 0000000000..67b5feab08 --- /dev/null +++ b/doc/todo/faster_rsync_remotes/comment_2_ccf6f75450c89ca498c8130054f8d32d._comment @@ -0,0 +1,24 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawln4uCaqZRd5_nRQ-iLcJyGctIdw8ebUiM" + nickname="Edward" + subject="Just put multiple source files" + date="2013-08-01T16:29:04Z" + content=""" +It seems like you can just put multiple source files on the command line: + + ed@ed-Ubu64 /tmp$ touch a b c d + ed@ed-Ubu64 /tmp$ mkdir test + ed@ed-Ubu64 /tmp$ rsync -avz a b c d test + sending incremental file list + a + b + c + d + + sent 197 bytes received 88 bytes 570.00 bytes/sec + total size is 0 speedup is 0.00 + ed@ed-Ubu64 /tmp$ ls test + a b c d + +It also appears to work with remote transfers too. +"""]] diff --git a/doc/todo/faster_rsync_remotes/comment_3_2f6a9d23cb8351fbf0f60ed93752e76e._comment b/doc/todo/faster_rsync_remotes/comment_3_2f6a9d23cb8351fbf0f60ed93752e76e._comment new file mode 100644 index 0000000000..1911048be6 --- /dev/null +++ b/doc/todo/faster_rsync_remotes/comment_3_2f6a9d23cb8351fbf0f60ed93752e76e._comment @@ -0,0 +1,14 @@ +[[!comment format=mdwn + username="http://joeyh.name/" + ip="4.152.108.145" + subject="comment 3" + date="2013-08-01T16:58:49Z" + content=""" +git-annex needs to build a specific directory structure on the rsync remote though. It seems it would need to build the whole tree locally, containing only the files it wants to send. + +When using encryption, it would need to encrypt all the files it's going to send and store them locally until it's built the tree. That could use a lot of disk space. + +Also, there's the problem of checking which files are already present in the remote, to avoid re-encrypting and re-sending them. Currently this is done by running rsync with the url of the file, and checking its exit code. rsync does not seem to have an interface that would allow checking multiple files in one call. So any optimisation of the number of rsync calls would only eliminate 1/2 of the current number. + +When using ssh:// urls, the rsync special remote already uses ssh connection caching, which I'd think would eliminate most of the overhead. (If you have a version of git-annex older than 4.20130417, you should upgrade to get this feature.) It should not take very long to start up a new rsync over a cached ssh connection. rsync:// is probably noticably slower. +"""]] diff --git a/doc/todo/faster_rsync_remotes/comment_4_3a2f45defebae3dde336ee5f40c26d7e._comment b/doc/todo/faster_rsync_remotes/comment_4_3a2f45defebae3dde336ee5f40c26d7e._comment new file mode 100644 index 0000000000..44d7d5511e --- /dev/null +++ b/doc/todo/faster_rsync_remotes/comment_4_3a2f45defebae3dde336ee5f40c26d7e._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawln4uCaqZRd5_nRQ-iLcJyGctIdw8ebUiM" + nickname="Edward" + subject="Thanks" + date="2013-08-01T17:03:23Z" + content=""" +I am using an old version of git-annex. I'll try the newer one and see if the connection caching helps! +"""]] diff --git a/doc/todo/file_copy_progress_bar.mdwn b/doc/todo/file_copy_progress_bar.mdwn new file mode 100644 index 0000000000..847c1d1eb6 --- /dev/null +++ b/doc/todo/file_copy_progress_bar.mdwn @@ -0,0 +1,5 @@ +Find a way to copy a file with a progress bar, while still preserving +stat. Easiest way might be to use pv and fix up the permissions etc +after? + +[[done]] diff --git a/doc/todo/free_space_checking_for_local_special_remotes.mdwn b/doc/todo/free_space_checking_for_local_special_remotes.mdwn new file mode 100644 index 0000000000..0fd2eac595 --- /dev/null +++ b/doc/todo/free_space_checking_for_local_special_remotes.mdwn @@ -0,0 +1,4 @@ +Should be possible to configure an annex.diskreserve setting for local +special remotes, such as a directory special remote or possibly a bup +special remote. (Although bup's deltas will make storing some versions of +files take less space than git-annex would have to assume it would take.) diff --git a/doc/todo/free_space_checking_for_local_special_remotes/comment_1_47c254cec58cbbb3ea84c93ef8282f01._comment b/doc/todo/free_space_checking_for_local_special_remotes/comment_1_47c254cec58cbbb3ea84c93ef8282f01._comment new file mode 100644 index 0000000000..00c4c56254 --- /dev/null +++ b/doc/todo/free_space_checking_for_local_special_remotes/comment_1_47c254cec58cbbb3ea84c93ef8282f01._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="http://joeyh.name/" + ip="4.154.7.235" + subject="comment 1" + date="2013-07-11T16:19:26Z" + content=""" +Directory special remotes do already honor annex.diskreserve, it turns out. Let's repurpose this bug to be about adding a per-remote configuration, in case the main annex.diskreserve is not appropriate. +"""]] diff --git a/doc/todo/fsck.mdwn b/doc/todo/fsck.mdwn new file mode 100644 index 0000000000..1dcaad9a51 --- /dev/null +++ b/doc/todo/fsck.mdwn @@ -0,0 +1,11 @@ +add a git annex fsck that finds keys that have no referring file + +(done) + +* Need per-backend fsck support. sha1 can checksum all files in the annex. + WORM can check filesize. + +* Both can check that annex.numcopies is satisfied. Probably only + querying the locationlog, not doing an online verification. + +[[done]] diff --git a/doc/todo/fsck_special_remotes.mdwn b/doc/todo/fsck_special_remotes.mdwn new file mode 100644 index 0000000000..7196baafe6 --- /dev/null +++ b/doc/todo/fsck_special_remotes.mdwn @@ -0,0 +1,13 @@ +`git annex fsck --from remote` + +Basically, this needs to receive each file in turn from the remote, to a +temp file, and then run the existing fsck code on it. Could be quite +expensive, but sometimes you really want to check. + +An unencrypted directory special remote could be optimised, by not actually +copying the file, just dropping a symlink, etc. + +The WORM backend doesn't care about file content, so it would be nice to +avoid transferring the content at all, and only send the size. + +> [[done]] --[[Joey]] diff --git a/doc/todo/git-annex-shell.mdwn b/doc/todo/git-annex-shell.mdwn new file mode 100644 index 0000000000..a9e3b43ede --- /dev/null +++ b/doc/todo/git-annex-shell.mdwn @@ -0,0 +1,15 @@ +[[done]] + +I've been considering adding a `git-annex-shell` command. This would +be similar to `git-shell` (and in fact would pass unknown commands off to +`git-shell`). + +## Reasons + +* Allows locking down an account to only be able to use git-annex (and + git). +* Avoids needing to construct complex shell commands to run on the remote + system. (Mostly already avoided by the plumbing level commands.) +* Could possibly allow multiple things to be done with one ssh connection + in future. +* Allows expanding `~` and `~user` in repopath on the remote system. diff --git a/doc/todo/git-annex_unused_eats_memory.mdwn b/doc/todo/git-annex_unused_eats_memory.mdwn new file mode 100644 index 0000000000..760a6ccf5a --- /dev/null +++ b/doc/todo/git-annex_unused_eats_memory.mdwn @@ -0,0 +1,32 @@ +`git-annex unused` has to compare large sets of data +(all keys with content present in the repository, +with all keys used by files in the repository), and so +uses more memory than git-annex typically needs. + +It used to be a lot worse (hundreds of megabytes). + +Now it only needs enough memory to store a Set of all Keys that currently +have content in the annex. On a lightly populated repository, it runs in +quite low memory use (like 8 mb) even if the git repo has 100 thousand +files. On a repository with lots of file contents, it will use more. + +Still, I would like to reduce this to a purely constant memory use, +as running in constant memory no matter the repo size is a git-annex design +goal. + +One idea is to use a bloom filter. +For example, construct a bloom filter of all keys used by files in +the repository. Then for each key with content present, check if it's +in the bloom filter. Since there can be false positives, this might +miss finding some unused keys. The probability/size of filter +could be tunable. + +> Fixed in `bloom` branch in git. --[[Joey]] +>> [[done]]! --[[Joey]] + +Another way might be to scan the git log for files that got removed +or changed what key they pointed to. Correlate with keys with content +currently present in the repository (possibly using a bloom filter again), +and that would yield a shortlist of keys that are probably not used. +Then scan thru all files in the repo to make sure that none point to keys +on the shortlist. diff --git a/doc/todo/git_annex_init_:_include_repo_description_and__47__or_UUID_in_commit_message.mdwn b/doc/todo/git_annex_init_:_include_repo_description_and__47__or_UUID_in_commit_message.mdwn new file mode 100644 index 0000000000..be7e2dacc8 --- /dev/null +++ b/doc/todo/git_annex_init_:_include_repo_description_and__47__or_UUID_in_commit_message.mdwn @@ -0,0 +1,13 @@ +Would help alot when having to add large(ish) amounts of remotes. + +Maybe detect this kind of commit message and ask user whether to automatically add them? See [[auto_remotes]]: +> Question: When should git-annex update the remote.log? (If not just on init.) Whenever it reads in a repo's remotes? + +---- + +I'm not sure that the above suggestion is going down a path that really +makes sense. If you want a list of repository UUIDs and descriptions, +it's there in machine-usable form in `.git-annex/uuid.log`, there is no +need to try to pull this info out of git commit messages. --[[Joey]] + +[[done]] diff --git a/doc/todo/gitolite_and_gitosis_support.mdwn b/doc/todo/gitolite_and_gitosis_support.mdwn new file mode 100644 index 0000000000..2fca839863 --- /dev/null +++ b/doc/todo/gitolite_and_gitosis_support.mdwn @@ -0,0 +1,39 @@ +gitosis and gitolite should support git-annex being used to send/receive +files from the repositories they manage. Users with read-only access +could only get files, while users with write access could also put and drop +files. + +Doing this right requires modifying both programs, to add [[git-annex-shell]] +to the list of things they can run, and only allow through appropriate +git-annex-shell subcommands to read-only users. + +I have posted an RFC for modifying gitolite to the +[gitolite mailing list](http://groups.google.com/group/gitolite?lnk=srg). + +> I have not developed a patch yet, but all that git-annex needs is a way +> to ssh to the server and run the git-annex-shell command there. +> git-annex-shell is very similar to git-shell. So, one way to enable +> it is simply to set GL_ADC_PATH to a directory containing git-annex-shell. +> +> But, that's not optimal, since git-annex-shell will send off receive-pack +> commands to git, which would bypass gitolite's permissions checking. +> Also, it makes sense to limit readonly users to only download, not +> upload/delete files from git-annex. Instead, I suggest adding something +> like this to gitolite's config: + + # If set, users with W access can write file contents into the git-annex, + # and users with R access can read file contents from the git-annex. + $GL_GIT_ANNEX = 0; + +> If this makes sense, I'm sure I can put a patch together for your +> review. It would involve modifying gl-auth-command so it knows how +> to run git-annex-shell, and how to parse out the "verb" from a +> git-annex-shell command line, and modifying R_COMMANDS and W_COMMANDS. + +As I don't write python, someone else is needed to work on gitosis. +--[[Joey]] + +> [[done]]; support for gitolite is in its `pu` branch, and some changes +> made to git-annefor gitolite is in its `pu` branch, and some changes +> made to git-annex. Word is gitosis is not being maintained so I won't +> worry about try to support it. --[[Joey]] diff --git a/doc/todo/gitrm.mdwn b/doc/todo/gitrm.mdwn new file mode 100644 index 0000000000..e41c334623 --- /dev/null +++ b/doc/todo/gitrm.mdwn @@ -0,0 +1,5 @@ +how to handle git rm file? (should try to drop keys that have no +referring file, if it seems safe..) + +[[done]] -- I think that git annex unused and dropunused are the best +solution to this. diff --git a/doc/todo/hidden_files.mdwn b/doc/todo/hidden_files.mdwn new file mode 100644 index 0000000000..191e9c3286 --- /dev/null +++ b/doc/todo/hidden_files.mdwn @@ -0,0 +1,30 @@ +Add a `git annex hide $file` that behaves like drop, checking counter info +and updating location log to say the current repo no longer has a file -- +but does not actually remove the content. + +Then `git annex unused` can be used to clean it up later. And in the +meantime, it's still locally accessible. This can be useful if you're +planning to need to free up space later, but want to hold onto the content +for a while. Possibly you'll be disconnected later, so it's easier to push +out that intent now. + +-- + +TODO: + +* Make 100% sure this is safe. Drop, etc should never check content files + are present on other repos if the location log doesn't say the repo + has the content. + +* What will `git annex get` do if it's asked to get a file that has been + hidden? + +> Unless I am missing something: Make sure the data is correct (for SHA1 or other tracking) and restore locally. If that's not the case, delete and restore from remote. -- RichiH + +---- + +Is 'unused' a good name? 'clean' and 'autoclean' would make more sense, imo. 'clean' deletes everything, whereas an optional 'autoclean' could try to be smart based on disk usage and/or SHA1, etc. -- RichiH + +> Nah, `git annex unused/dropunused` already exist. --[[Joey]] + +>> OK, in that case forget what I said. No idea about your internal policy, but feel free to delete this part of the page, then. -- RichiH diff --git a/doc/todo/http_headers.mdwn b/doc/todo/http_headers.mdwn new file mode 100644 index 0000000000..9f61bdc931 --- /dev/null +++ b/doc/todo/http_headers.mdwn @@ -0,0 +1,8 @@ +The IA would find it useful to be able to control the http headers +git-annex get, addurl, etc uses. This will allow setting cookies, for +example. + +* annex-web-headers=blah +* Perhaps also annex-web-headers-command=blah + +[[done]] diff --git a/doc/todo/immutable_annexed_files.mdwn b/doc/todo/immutable_annexed_files.mdwn new file mode 100644 index 0000000000..b26838e95e --- /dev/null +++ b/doc/todo/immutable_annexed_files.mdwn @@ -0,0 +1,8 @@ +> josh: Do you do anything in git-annex to try to make the files immutable? +> For instance, removing write permission, or even chattr? +> joey: I don't, but that's a very good idea +> josh: Oh, I just thought of another slightly crazy but handy idea. +> josh: I'd hate to run into a program which somehow followed the symlink and then did an unlink to replace the file. +> josh: To break that, you could create a new directory under annex's internal directory for each file, and make the directory have no write permission. + +[[done]] and done --[[Joey]] diff --git a/doc/todo/incremental_fsck.mdwn b/doc/todo/incremental_fsck.mdwn new file mode 100644 index 0000000000..7c56328b99 --- /dev/null +++ b/doc/todo/incremental_fsck.mdwn @@ -0,0 +1,24 @@ +Justin Azoff realized git-annex should have an incremental fsck. + +This requires storing the last fsck time of each object. + +I would not be strongly opposed to sqlite, but I think there are other +places the data could be stored. One possible place is the mode or mtime +of the .git/annex/objects/xx/yy/$key directories (the parent directories +of where the content is stored). Perhaps the sticky bit could be used to +indicate the content has been fsked, and the mtime indicate the time +of last fsck. Anything that dropped or put in content would need to +clear the sticky bit. --[[Joey]] + +> Basic incremental fsck is done now. +> +> Some enhancements would include: +> +> * --max-age=30d Once the incremental fsck completes and was started 30 days ago, +> start a new one. +> * --time-limit --size-limit --file-limit: Limit how long the fsck runs. + +>> Calling this [[done]]. The `--incremental-schedule` option +>> allows scheduling time between incremental fscks. `--time-limit` is +>> done. I implemented `--smallerthan` independently. Not clear what +>> `--file-limit` would be. --[[Joey]] diff --git a/doc/todo/incremental_fsck/comment_1_609b21141dd5686b2c0eaef2b8d63229._comment b/doc/todo/incremental_fsck/comment_1_609b21141dd5686b2c0eaef2b8d63229._comment new file mode 100644 index 0000000000..709ba078c0 --- /dev/null +++ b/doc/todo/incremental_fsck/comment_1_609b21141dd5686b2c0eaef2b8d63229._comment @@ -0,0 +1,14 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawmBUR4O9mofxVbpb8JV9mEbVfIYv670uJo" + nickname="Justin" + subject="comment 1" + date="2012-09-20T14:11:57Z" + content=""" +I have a [proof of concept written in python](https://github.com/JustinAzoff/git-annex-background-fsck/blob/master/git-annex-background-fsck). + +You can run it and point it the root of an annex or to a subdirectory. In my brief testing it seems to work :-) + +the goal would be to have options like + + git annex fsck /data/annex --check-older-than 1w --check-for 2h --max-load-avg 0.5 +"""]] diff --git a/doc/todo/keep_annexed_files_for_a_while.mdwn b/doc/todo/keep_annexed_files_for_a_while.mdwn new file mode 100644 index 0000000000..cf85b11f3b --- /dev/null +++ b/doc/todo/keep_annexed_files_for_a_while.mdwn @@ -0,0 +1,8 @@ +I don't want files that I dropped to immediately disappear from my local or all of my remotes repos on the next sync. Especially in situations where changes to the git-annex repo get automatically and immediately replicated to remote repos, I want a configurable "grace" period before files in .git/annex/objects get really deleted. + +This has similarities to the "trash" on a desktop. It might also be nice to + +* configure a maximum amount of space of the "trash" +* have a way to see the contents of the trash to easily recover deleted files + +Maybe it would make sense to just move dropped files to the desktops trash? "git annex trash" as an alternative to drop? diff --git a/doc/todo/link_file_to_remote_repo_feature.mdwn b/doc/todo/link_file_to_remote_repo_feature.mdwn new file mode 100644 index 0000000000..d6b41e8059 --- /dev/null +++ b/doc/todo/link_file_to_remote_repo_feature.mdwn @@ -0,0 +1,52 @@ +I have two repos, using SHA1 backend and both using git. +The first one is a laptop, the second one is a usb drive. + +When I drop a file on the laptop repo, the file is not available on that repo until I run *git annex get* +But when the usb drive is plugged in the file is actually available. + +How about adding a feature to link some/all files to the remote repo? + +e.g. +We have *railscasts/196-nested-model-form-part-1.mp4* file added to git, and only available on the usb drive: + + $ git annex whereis 196-nested-model-form-part-1.mp4 + whereis 196-nested-model-form-part-1.mp4 (1 copy) + a7b7d7a4-2a8a-11e1-aebc-d3c589296e81 -- origin (Portable usb drive) + +I can see the link with: + + $ cd railscasts + $ ls -ls 196* + 8 lrwxr-xr-x 1 framallo staff 193 Dec 20 05:49 196-nested-model-form-part-1.mp4 -> ../.git/annex/objects/Wz/6P/SHA256-s16898930--43679c67cd968243f58f8f7fb30690b5f3f067574e318d609a01613a2a14351e/SHA256-s16898930--43679c67cd968243f58f8f7fb30690b5f3f067574e318d609a01613a2a14351e + +I save this in a variable just to make the example more clear: + + ID=".git/annex/objects/Wz/6P/SHA256-s16898930--43679c67cd968243f58f8f7fb30690b5f3f067574e318d609a01613a2a14351e/SHA256-s16898930--43679c67cd968243f58f8f7fb30690b5f3f067574e318d609a01613a2a14351e" + +The file doesn't exist on the local repo: + + $ ls ../$ID + ls: ../$ID: No such file or directory + +however I can create a link to access that file on the remote repo. +First I create a needed dir: + + $ mkdir ../.git/annex/objects/Wz/6P/SHA256-s16898930--43679c67cd968243f58f8f7fb30690b5f3f067574e318d609a01613a2a14351e/ + +Then I link to the remote file: + + $ ln -s /mnt/usb_drive/repo_folder/$ID ../$ID + +now I can open the file in the laptop repo. + + +I think it could be easy to implement. Maybe It's a naive approach, but looks apealing. +Checking if it's a real file or a link shouldn't impact on performance. +The limitation is that it would work only with remote repos on local dirs + +Also allows you to have one directory structure like AFS or other distributed FS. If the file is not local I go to the remote server. +Which is great for apps like Picasa, Itunes, and friends that depends on the file location. + +> This is a duplicate of [[union_mounting]]. So closing it: [[done]]. +> +> It's a good idea, but making sure git-annex correctly handles these links in all cases is a subtle problem that has not yet been tackled. --[[Joey]] diff --git a/doc/todo/network_remotes.mdwn b/doc/todo/network_remotes.mdwn new file mode 100644 index 0000000000..42efa832f5 --- /dev/null +++ b/doc/todo/network_remotes.mdwn @@ -0,0 +1,5 @@ +Support for remote git repositories (ssh:// specifically can be made to +work, although the other end probably needs to have git-annex +installed..) + +[[done]], at least get and put work.. diff --git a/doc/todo/object_dir_reorg_v2.mdwn b/doc/todo/object_dir_reorg_v2.mdwn new file mode 100644 index 0000000000..49666ddc79 --- /dev/null +++ b/doc/todo/object_dir_reorg_v2.mdwn @@ -0,0 +1,25 @@ +Several things suggest now would be a good time to reorgaize the object +directory. This would be annex.version=2. It will be slightly painful for +all users, so this should be the *last* reorg in the forseeable future. + +1. Remove colons from filenames, for [[bugs/fat_support]] + +2. Add hashing, since some filesystems do suck (like er, fat at least :) + [[forum/hashing_objects_directories]] + (Also, may as well hash .git-annex/* while at it -- that's what + really gets big.) + +3. Add filesize metadata for [[bugs/free_space_checking]]. (Currently only + present in WORM, and in an ad-hoc way.) + +4. Perhaps use a generic format that will allow further metadata to be + added later. For example, + "bSHA1,s101111,kf3101c30bb23467deaec5d78c6daa71d395d1879" + + (Probably everything after ",k" should be part of the key, even if it + contains the "," separator character. Otherwise an escaping mechanism + would be needed.) + +[[done]] now! + +Although [[bugs/free_space_checking]] is not quite there --[[Joey]] diff --git a/doc/todo/object_dir_reorg_v2/comment_1_ba03333dc76ff49eccaba375e68cb525._comment b/doc/todo/object_dir_reorg_v2/comment_1_ba03333dc76ff49eccaba375e68cb525._comment new file mode 100644 index 0000000000..261c2a51f3 --- /dev/null +++ b/doc/todo/object_dir_reorg_v2/comment_1_ba03333dc76ff49eccaba375e68cb525._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawl9sYlePmv1xK-VvjBdN-5doOa_Xw-jH4U" + nickname="Richard" + subject="comment 1" + date="2011-03-16T01:16:48Z" + content=""" +If you support generic meta-data, keep in mind that you will need to do conflict resolution. Timestamps may not be synched across all systems, so keeping a log of old metadata could be used, sorting by history and using the latest. Which leaves the situation of two incompatible changes. This would probably mean manual conflict resolution. You will probably have thought of this already, but I still wanted to make sure this is recorded. -- RichiH +"""]] diff --git a/doc/todo/object_dir_reorg_v2/comment_2_81276ac309959dc741bc90101c213ab7._comment b/doc/todo/object_dir_reorg_v2/comment_2_81276ac309959dc741bc90101c213ab7._comment new file mode 100644 index 0000000000..9785f1989e --- /dev/null +++ b/doc/todo/object_dir_reorg_v2/comment_2_81276ac309959dc741bc90101c213ab7._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawl9sYlePmv1xK-VvjBdN-5doOa_Xw-jH4U" + nickname="Richard" + subject="comment 2" + date="2011-03-16T01:19:25Z" + content=""" +Hmm, I added quite a few comments at work, but they are stuck in moderation. Maybe I forgot to log in before adding them. I am surprised this one appeared immediately. -- RichiH +"""]] diff --git a/doc/todo/object_dir_reorg_v2/comment_3_79bdf9c51dec9f52372ce95b53233bb2._comment b/doc/todo/object_dir_reorg_v2/comment_3_79bdf9c51dec9f52372ce95b53233bb2._comment new file mode 100644 index 0000000000..886941be72 --- /dev/null +++ b/doc/todo/object_dir_reorg_v2/comment_3_79bdf9c51dec9f52372ce95b53233bb2._comment @@ -0,0 +1,12 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawl9sYlePmv1xK-VvjBdN-5doOa_Xw-jH4U" + nickname="Richard" + subject="comment 1" + date="2011-03-15T14:08:41Z" + content=""" +What is the potential time-frame for this change? As I am not using git-annex for production yet, I can see myself waiting to avoid any potential hassle. + +Supporting generic metadata seems like a great idea. Though if you are going this path, wouldn't it make sense to avoid metastore for mtime etc and support this natively without outside dependencies? + +-- RichiH +"""]] diff --git a/doc/todo/object_dir_reorg_v2/comment_4_93aada9b1680fed56cc6f0f7c3aca5e5._comment b/doc/todo/object_dir_reorg_v2/comment_4_93aada9b1680fed56cc6f0f7c3aca5e5._comment new file mode 100644 index 0000000000..475359abbf --- /dev/null +++ b/doc/todo/object_dir_reorg_v2/comment_4_93aada9b1680fed56cc6f0f7c3aca5e5._comment @@ -0,0 +1,12 @@ +[[!comment format=mdwn + username="http://joey.kitenet.net/" + nickname="joey" + subject="comment 4" + date="2011-03-16T03:22:45Z" + content=""" +Well, I spent a few hours playing this evening in the 'reorg' branch in git. It seems to be shaping up pretty well; type-based refactoring in haskell makes these kind of big systematic changes a matter of editing until it compiles. And it compiles and test suite passes. But, so far I've only covered 1. 3. and 4. on the list, and have yet to deal with upgrades. + +I'd recommend you not wait before using git-annex. I am committed to provide upgradability between annexes created with all versions of git-annex, going forward. This is important because we can have offline archival drives that sit unused for years. Git-annex will upgrade a repository to current standard the first time it sees it, and I hope the upgrade will be pretty smooth. It was not bad for the annex.version 0 to 1 upgrade earlier. The only annoyance with upgrades is that it will result in some big commits to git, as every symlink in the repo gets changed, and log files get moved to new names. + +(The metadata being stored with keys is data that a particular backend can use, and is static to a given key, so there are no merge issues (and it won't be used to preserve mtimes, etc).) +"""]] diff --git a/doc/todo/object_dir_reorg_v2/comment_5_821c382987f105da72a50e0a5ce61fdc._comment b/doc/todo/object_dir_reorg_v2/comment_5_821c382987f105da72a50e0a5ce61fdc._comment new file mode 100644 index 0000000000..2032bce3c0 --- /dev/null +++ b/doc/todo/object_dir_reorg_v2/comment_5_821c382987f105da72a50e0a5ce61fdc._comment @@ -0,0 +1,12 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawl9sYlePmv1xK-VvjBdN-5doOa_Xw-jH4U" + nickname="Richard" + subject="comment 5" + date="2011-03-16T15:51:30Z" + content=""" +Hashing & segmenting seems to be around the corner, which is nice :) + +Is there a chance that you will optionally add mtime to your native metadata store? If yes, I'd rather wait for v2 to start with the native system from the start. If not, I will probably set it up tonight. + +PS: While posting from work, my comments are held for moderation once again. I am somewhat confused as to why this happens when I can just submit directly from home. And yes, I am using the same auth provider and user in both cases. +"""]] diff --git a/doc/todo/object_dir_reorg_v2/comment_6_8834c3a3f1258c4349d23aff8549bf35._comment b/doc/todo/object_dir_reorg_v2/comment_6_8834c3a3f1258c4349d23aff8549bf35._comment new file mode 100644 index 0000000000..ff86e3970b --- /dev/null +++ b/doc/todo/object_dir_reorg_v2/comment_6_8834c3a3f1258c4349d23aff8549bf35._comment @@ -0,0 +1,10 @@ +[[!comment format=mdwn + username="http://joey.kitenet.net/" + nickname="joey" + subject="comment 6" + date="2011-03-16T16:32:52Z" + content=""" +The mtime cannot be stored for all keys. Consider a SHA1 key. The mtime is irrelevant; 2 files with different mtimes, when added to the SHA1 backend, should get the same key. + +Probably our spam filter doesn't like your work IP. +"""]] diff --git a/doc/todo/object_dir_reorg_v2/comment_7_42501404c82ca07147e2cce0cff59474._comment b/doc/todo/object_dir_reorg_v2/comment_7_42501404c82ca07147e2cce0cff59474._comment new file mode 100644 index 0000000000..fc866c57a6 --- /dev/null +++ b/doc/todo/object_dir_reorg_v2/comment_7_42501404c82ca07147e2cce0cff59474._comment @@ -0,0 +1,12 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawl9sYlePmv1xK-VvjBdN-5doOa_Xw-jH4U" + nickname="Richard" + subject="comment 7" + date="2011-03-16T21:05:38Z" + content=""" +Ah, OK. I assumed the metadata would be attached to a key, not part of the key. This seems to make upgrades/extensions down the line harder than they need to be, but you are right that this way, merges are not, and never will be, an issue. + +Though with the SHA1 backend, changing files can be tracked. This means that tracking changes in mtime or other is possible. It also means that there are potential merge issues. But I won't argue the point endlessly. I can accept design decisions :) + +The prefix at work is from a university netblock so yes, it might be on a few hundred proxy lists etc. +"""]] diff --git a/doc/todo/optimise_git-annex_merge.mdwn b/doc/todo/optimise_git-annex_merge.mdwn new file mode 100644 index 0000000000..91d18ebd77 --- /dev/null +++ b/doc/todo/optimise_git-annex_merge.mdwn @@ -0,0 +1,23 @@ +Typically `git-annex merge` is fast, but it could still be sped up. + +`git-annex merge` runs `git-hash-object` once per file that needs to be +merged. Elsewhere in git-annex, `git-hash-object` is used in a faster mode, +reading files from disk via `--stdin-paths`. But here, the data is not +in raw files on disk, and I doubt writing them is the best approach. +Instead, I'd like a way to stream multiple objects into git using stdin. +Sometime, should look at either extending git-hash-object to support that, +or possibly look at using git-fast-import instead. + +--- + +`git-annex merge` also runs `git show` once per file that needs to be +merged. This could be reduced to a single call to `git-cat-file --batch`, +There is already a Git.CatFile library that can do this easily. --[[Joey]] + +> This is now done, part above remains todo. --[[Joey]] + +--- + +Merging used to use memory proportional to the size of the diff. It now +streams data, running in constant space. This probably sped it up a lot, +as there's much less allocation and GC action. --[[Joey]] diff --git a/doc/todo/optinally_transfer_file_unencryptedly.mdwn b/doc/todo/optinally_transfer_file_unencryptedly.mdwn new file mode 100644 index 0000000000..ef27dc521a --- /dev/null +++ b/doc/todo/optinally_transfer_file_unencryptedly.mdwn @@ -0,0 +1,6 @@ +I have a git-annex repository on a NSLU 2, and transfers are much slower over ssh compared to unencrypted transfers (no wonder at that CPU speed). For the files that I am transferring, no encryption would be necessary. Unfortunately, ssh in Debian does not support "-c none" to disable encryption. + +It would be nice if git-annex would have a way of conveniently transferring files in another way than SSH. I’m not sure what a good way would be – maybe launching a one-shot HTTP-server on the sending end? Haskell libraries for that would be available... Of course it is not always the case that the host reachable with "ssh foo" is also reachable via TCP at "foo:1234"... And there are surely more problem. But still, it would be nice :-) + +> Setting `remote.name.annex-rsync-transport = rsh` will now +> make rsync special remotes use rsh instead of ssh. [[done]] diff --git a/doc/todo/optinally_transfer_file_unencryptedly/comment_1_4be47e7ac85d0f4e7029a96b615545a7._comment b/doc/todo/optinally_transfer_file_unencryptedly/comment_1_4be47e7ac85d0f4e7029a96b615545a7._comment new file mode 100644 index 0000000000..948845b232 --- /dev/null +++ b/doc/todo/optinally_transfer_file_unencryptedly/comment_1_4be47e7ac85d0f4e7029a96b615545a7._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="guilhem" + ip="129.16.20.212" + subject="rsh?" + date="2013-04-09T16:11:50Z" + content=""" +I don't use it myself, but rsync can be used with others remote shells, among which rsh supports unencrypted streams. You probably want to set up a secure authorization mechanism to deny access to intruders, and for that kerberos comes to the rescue :-) I didn't try the combination, but it should work over git-annex already. +"""]] diff --git a/doc/todo/parallel_possibilities.mdwn b/doc/todo/parallel_possibilities.mdwn new file mode 100644 index 0000000000..9c0e69e294 --- /dev/null +++ b/doc/todo/parallel_possibilities.mdwn @@ -0,0 +1,13 @@ +One of my reasons for using haskell was that it provides the possibility of +some parallell processing. Although since git-annex hits the filesystem +heavily and mostly runs other git commands, maybe not a whole lot. + +Anyway, each git-annex command is broken down into a series of independant +actions, which has some potential for parallelism. + +Each action has 3 distinct phases, basically "check", "perform", and +"cleanup". The perform actions are probably parellizable; the cleanup may be +(but not if it has to run git commands to stage state; it can queue +commands though); the check should be easily parallelizable, although they +may access the disk or run minor git query commands, so would probably not +want to run too many of them at once. diff --git a/doc/todo/parallel_possibilities/comment_1_d8e34fc2bc4e5cf761574608f970d496._comment b/doc/todo/parallel_possibilities/comment_1_d8e34fc2bc4e5cf761574608f970d496._comment new file mode 100644 index 0000000000..4aceb3abd3 --- /dev/null +++ b/doc/todo/parallel_possibilities/comment_1_d8e34fc2bc4e5cf761574608f970d496._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawkptNW1PzrVjYlJWP_9e499uH0mjnBV6GQ" + nickname="Christian" + subject="comment 1" + date="2011-04-08T12:41:43Z" + content=""" +I also think, that fetching keys via rsync can be done by one rsync process, when the keys are fetched from one host. This would avoid establishing a new TCP connection for every file. +"""]] diff --git a/doc/todo/parallel_possibilities/comment_2_adb76f06a7997abe4559d3169a3181c3._comment b/doc/todo/parallel_possibilities/comment_2_adb76f06a7997abe4559d3169a3181c3._comment new file mode 100644 index 0000000000..6ecce52c42 --- /dev/null +++ b/doc/todo/parallel_possibilities/comment_2_adb76f06a7997abe4559d3169a3181c3._comment @@ -0,0 +1,12 @@ +[[!comment format=mdwn + username="http://ertai.myopenid.com/" + nickname="npouillard" + subject="comment 2" + date="2011-05-20T20:14:15Z" + content=""" +I agree with Christian. + +One should first make a better use of connections to remotes before exploring parallel possibilities. One should pipeline the requests and answers. + +Of course this could be implemented using parallel&concurrency features of Haskell to do this. +"""]] diff --git a/doc/todo/parallel_possibilities/comment_3_145fb974f45da99b7d4b117a3699cccf._comment b/doc/todo/parallel_possibilities/comment_3_145fb974f45da99b7d4b117a3699cccf._comment new file mode 100644 index 0000000000..a72a1456c4 --- /dev/null +++ b/doc/todo/parallel_possibilities/comment_3_145fb974f45da99b7d4b117a3699cccf._comment @@ -0,0 +1,12 @@ +[[!comment format=mdwn + username="http://joeyh.name/" + ip="4.154.4.90" + subject="comment 3" + date="2013-07-17T19:59:50Z" + content=""" +Note that git-annex now uses locks to communicate amoung multiple processes, so it's now possible to eg run two `git annex get` processes, and one will skip over the file the other is downloading and go on to the next file, and so on. + +This is an especially nice speedup when downloading encrypted data, since the decryption of one file will tend to happen while the other process is downloading the next file (assuming files of approximately the same size, and that decryption takes approxiately as long as downloading). + +The only thing preventing this being done by threads in one process, enabled by a -jN option, is that the output would be a jumbled mess. +"""]] diff --git a/doc/todo/pushpull.mdwn b/doc/todo/pushpull.mdwn new file mode 100644 index 0000000000..6828b35b2f --- /dev/null +++ b/doc/todo/pushpull.mdwn @@ -0,0 +1,4 @@ +--push/--pull should take a reponame and files, and push those files + to that repo; dropping them from the current repo + +[[done]] (move --from/--to) diff --git a/doc/todo/redundancy_stats_in_status.mdwn b/doc/todo/redundancy_stats_in_status.mdwn new file mode 100644 index 0000000000..56095fd333 --- /dev/null +++ b/doc/todo/redundancy_stats_in_status.mdwn @@ -0,0 +1,23 @@ +Currently, `git annex status` only shows the size of 1 copy of each file. +If numcopies is being used for redundancy, much more disk can actually be +in use than status shows. + +One idea: + + known annex size: 2 terabytes (plus 4 terabytes of redundant copies) + +But, to get that number, it would have to walk every location log, +counting how many copies currently exist of each file. That would make +status a lot slower than it is. + +One option is to just put it at the end of the status: + + redundancy: 300% (4 terabytes of copies) + +And ctrl-c if it's taking too long. + +Hmm, fsck looks at that same info. Maybe it could cache the redundancy +level it discovers? Since fsck can be run incrementally, it would be tricky +to get an overall number. And the number would tend to be stale, but +then again it might also be nice if status shows how long ago the last fsck +was. diff --git a/doc/todo/redundancy_stats_in_status/comment_1_9f1c10f8cea4fa60a99cbcc8306dd5de._comment b/doc/todo/redundancy_stats_in_status/comment_1_9f1c10f8cea4fa60a99cbcc8306dd5de._comment new file mode 100644 index 0000000000..801c1da039 --- /dev/null +++ b/doc/todo/redundancy_stats_in_status/comment_1_9f1c10f8cea4fa60a99cbcc8306dd5de._comment @@ -0,0 +1,10 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawl9sYlePmv1xK-VvjBdN-5doOa_Xw-jH4U" + nickname="Richard" + subject="comment 1" + date="2012-10-30T08:09:13Z" + content=""" +I like the idea of using fsck as a pre-run for status. + +Basically, it's the same as `updatedb` and `locate`; locate will warn the user if it considers its cache to be too old, as well. +"""]] diff --git a/doc/todo/resuming_encrypted_uploads.mdwn b/doc/todo/resuming_encrypted_uploads.mdwn new file mode 100644 index 0000000000..b3aaa7f966 --- /dev/null +++ b/doc/todo/resuming_encrypted_uploads.mdwn @@ -0,0 +1,22 @@ +Resuming interrupted uploads to encrypted special remotes is not currently +possible, because gpg does not produce consistent output. Special remotes +that could support resuming include rsync and glacier. + +Without consistent output, git-annex would need to locally cache the encrypted +file, and reuse that cache when resuming an upload. This would make +encrypted uploads more expensive in terms of both file IO and disk space +used. + +[It would be possible to write to the cache at the same time the special +remote is being fed data, and if the special remote upload fails, continue +writing the rest of the file. That would avoid half the overhead, since +the file would not need to be read from, just written to. (Although OS +caching may accomplish the same thing.)] + +Also, `git annex unused` would need to show temp files for uploads, +the same as it currently shows temp files for downloads, and users would +sometimes need to manually dropunused old uploads, that never completed. + +The question, then, is whether resuming uploads is useful enough to add +this overhead and user-visible complexity. +--[[Joey]] diff --git a/doc/todo/resuming_encrypted_uploads/comment_1_1832a6fb78e8ad7c838582f46731ac3b._comment b/doc/todo/resuming_encrypted_uploads/comment_1_1832a6fb78e8ad7c838582f46731ac3b._comment new file mode 100644 index 0000000000..cf35de0492 --- /dev/null +++ b/doc/todo/resuming_encrypted_uploads/comment_1_1832a6fb78e8ad7c838582f46731ac3b._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="http://phil.0x539.de/" + nickname="Philipp Kern" + subject="comment 1" + date="2012-12-28T23:23:29Z" + content=""" +Doesn't the encryption part already write an encrypted version completely to disk before starting to upload? (At least with the rsync remote?) So the disk space and I/O usage is already there. (Except that it's probably a temporary file that's deleted after it was created, so that it's not kept around by the filesystem when certain destructive events strike.) +"""]] diff --git a/doc/todo/resuming_encrypted_uploads/comment_2_2ecc8e782f49e90ed1549e9179eb1a1e._comment b/doc/todo/resuming_encrypted_uploads/comment_2_2ecc8e782f49e90ed1549e9179eb1a1e._comment new file mode 100644 index 0000000000..a2bab9244b --- /dev/null +++ b/doc/todo/resuming_encrypted_uploads/comment_2_2ecc8e782f49e90ed1549e9179eb1a1e._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawln3ckqKx0x_xDZMYwa9Q1bn4I06oWjkog" + nickname="Michael" + subject="comment 2" + date="2012-12-29T08:00:56Z" + content=""" +Being able to resume transfers of encrypted files would absolutely be useful! Disk space is cheap, but bandwidth is not. +"""]] diff --git a/doc/todo/rsync.mdwn b/doc/todo/rsync.mdwn new file mode 100644 index 0000000000..3353f19c43 --- /dev/null +++ b/doc/todo/rsync.mdwn @@ -0,0 +1,4 @@ +Transferring a file from a ssh:// remote should use rsync to allow resuming +of a prior transfer. + +[[done]] diff --git a/doc/todo/smudge.mdwn b/doc/todo/smudge.mdwn new file mode 100644 index 0000000000..6103ffa61e --- /dev/null +++ b/doc/todo/smudge.mdwn @@ -0,0 +1,162 @@ +git-annex should use smudge/clean filters. + +---- + +Update: Currently, this does not look likely to work. In particular, +the clean filter needs to consume all stdin from git, which consists of the +entire content of the file. It cannot optimise by directly accessing +the file in the repository, because git may be cleaning a different +version of the file during a merge. + +So every `git status` would need to read the entire content of all +available files, and checksum them, which is too expensive. + +> Update from GitTogether: Peff thinks a new interface could be added to +> git to handle this sort of case in an efficient way.. just needs someone +> to do the work. --[[Joey]] + +---- + +The clean filter is run when files are staged for commit. So a user could copy +any file into the annex, git add it, and git-annex's clean filter causes +the file's key to be staged, while its value is added to the annex. + +The smudge filter is run when files are checked out. Since git annex +repos have partial content, this would not git annex get the file content. +Instead, if the content is not currently available, it would need to do +something like return empty file content. (Sadly, it cannot create a +symlink, as git still wants to write the file afterwards.) + +So the nice current behavior of unavailable files being clearly missing due +to dangling symlinks, would be lost when using smudge/clean filters. +(Contact git developers to get an interface to do this?) + +Instead, we get the nice behavior of not having to remeber to `git annex +add` files, and just being able to use `git add` or `git commit -a`, +and have it use git-annex when .gitattributes says to. Also, annexed +files can be directly modified without having to `git annex unlock`. + +### design + +In .gitattributes, the user would put something like "* filter=git-annex". +This way they could control which files are annexed vs added normally. + +(git-annex could have further controls to allow eg, passing small files +through to regular processing. At least .gitattributes is a special case, +it should never be annexed...) + +For files not configured this way, git-annex could continue to use +its symlink method -- this would preserve backwards compatability, +and even allow mixing the two methods in a repo as desired. + +To find files in the repository that are annexed, git-annex would do +`ls-files` as now, but would check if found files have the appropriate +filter, rather than the current symlink checks. To determine the key +of a file, rather than reading its symlink, git-annex would need to +look up the git blob associated with the file -- this can be done +efficiently using the existing code in `Branch.catFile`. + +The clean filter would inject the file's content into the annex, and hard +link from the annex to the file. Avoiding duplication of data. + +The smudge filter can't do that, so to avoid duplication of data, it +might always create an empty file. To get the content, `git annex get` +could be used (which would hard link it). A `post-checkout` hook might +be used to set up hard links for all currently available content. + +#### clean + +The trick is doing it efficiently. Since git a2b665d, v1.7.4.1, +something like this works to provide a filename to the clean script: + + git config --global filter.huge.clean huge-clean %f + +This could avoid it needing to read all the current file content from stdin +when doing eg, a git status or git commit. Instead it is passed the +filename that git is operating on, in the working directory. +(Update: No, doesn't work; git may be cleaning a different file content +than is currently on disk, and git requires all stdin be consumed too.) + +So, WORM could just look at that file and easily tell if it is one +it already knows (same mtime and size). If so, it can short-circuit and +do nothing, file content is already cached. + +SHA1 has a harder job. Would not want to re-sha1 the file every time, +probably. So it'd need a local cache of file stat info, mapped to known +objects. + +But: Even with %f, git actually passes the full file content to the clean +filter, and if it fails to consume it all, it will crash (may only happen +if the file is larger than some chunk size; tried with 500 mb file and +saw a SIGPIPE.) This means unnecessary works needs to be done, +and it slows down *everything*, from `git status` to `git commit`. +**showstopper** I have sent a patch to the git mailing list to address +this. (Update: apparently +can't be fixed.) + +#### smudge + +The smudge script can also be provided a filename with %f, but it +cannot directly write to the file or git gets unhappy. + +### dealing with partial content availability + +The smudge filter cannot be allowed to fail, that leaves the tree and +index in a weird state. So if a file's content is requested by calling +the smudge filter, the trick is to instead provide dummy content, +indicating it is not available (and perhaps saying to run "git-annex get"). + +Then, in the clean filter, it has to detect that it's cleaning a file +with that dummy content, and make sure to provide the same identifier as +it would if the file content was there. + +I've a demo implementation of this technique in the scripts below. + +---- + +### test files + +huge-smudge: + +
+#!/bin/sh
+read f
+file="$1"
+echo "smudging $f" >&2
+if [ -e ~/$f ]; then
+	cat ~/$f # possibly expensive copy here
+else
+	echo "$f not available"
+fi
+
+ +huge-clean: + +
+#!/bin/sh
+file="$1"
+cat >/tmp/file
+# in real life, this should be done more efficiently, not trying to read
+# the whole file content!
+if grep -q 'not available' /tmp/file; then
+	awk '{print $1}' /tmp/file # provide what we would if the content were avail!
+	exit 0
+fi
+echo "cleaning $file" >&2
+# XXX store file content here
+echo $file
+
+ +.gitattributes: + +
+*.huge filter=huge
+
+ +in .git/config: + +
+[filter "huge"]
+        clean = huge-clean %f
+        smudge = huge-smudge %f
+
diff --git a/doc/todo/smudge/comment_1_4ea616bcdbc9e9a6fae9f2e2795c31c9._comment b/doc/todo/smudge/comment_1_4ea616bcdbc9e9a6fae9f2e2795c31c9._comment
new file mode 100644
index 0000000000..a4eb3cf235
--- /dev/null
+++ b/doc/todo/smudge/comment_1_4ea616bcdbc9e9a6fae9f2e2795c31c9._comment
@@ -0,0 +1,8 @@
+[[!comment format=mdwn
+ username="http://christian.amsuess.com/chrysn"
+ nickname="chrysn"
+ subject="git-add instead of git-annex-add"
+ date="2011-02-26T21:43:21Z"
+ content="""
+would, with these modifications in place, there still be a way to *really* git-add a file? (my main repository contains both normal git and git-annex files.)
+"""]]
diff --git a/doc/todo/smudge/comment_2_e04b32caa0d2b4c577cdaf382a3ff7f6._comment b/doc/todo/smudge/comment_2_e04b32caa0d2b4c577cdaf382a3ff7f6._comment
new file mode 100644
index 0000000000..3a223e1c7b
--- /dev/null
+++ b/doc/todo/smudge/comment_2_e04b32caa0d2b4c577cdaf382a3ff7f6._comment
@@ -0,0 +1,12 @@
+[[!comment format=mdwn
+ username="http://dieter-be.myopenid.com/"
+ nickname="dieter"
+ subject="symlinks"
+ date="2011-04-03T20:30:21Z"
+ content="""
+> (Sadly, it cannot create a symlink, as git still wants to write the file afterwards.
+> So the nice current behavior of unavailable files being clearly missing due to dangling symlinks, would be lost when using smudge/clean filters. (Contact git developers to get an interface to do this?)
+
+Have you checked what the smudge filter sees when the input is a symlink? Because git supports tracking symlinks, so it should also support pushing symlinks through a smudge filter, right?
+Either way: yes, contact the git devs, one can only ask and hope.  And if you can demonstrate the awesomeness of git-annex they might get more 1interested :)
+"""]]
diff --git a/doc/todo/special_remote_for_amazon_glacier.mdwn b/doc/todo/special_remote_for_amazon_glacier.mdwn
new file mode 100644
index 0000000000..0fa77b5275
--- /dev/null
+++ b/doc/todo/special_remote_for_amazon_glacier.mdwn
@@ -0,0 +1,30 @@
+Amazon's new glacier service would be a nice special remote to support for
+long-term archival.
+
+The main difficulty is that glacier is organized into vaults, and accessing
+a file in a vault takes ~4 hours. A naive implementation would make `git
+annex get` wait for 4 hours, which is certianly not reasonable.
+
+One approach I am pondering is to make each glacier vault a separate
+special remote. You could then request git-annex to spin up a remote, and
+come back later, and be able to access the data stored in it (need to check
+if glacier would also allow adding new data to it then). This is
+conceptually similar to using git-annex with offline removable drives,
+except with glacier, you have a controllable robot to get them plugged in. :)
+
+Ideally, git-annex would arrange for glacier to send it a message when the
+vault becomes available, and the user could queue a list of commands to
+run, or files to transfer, at that point.
+
+--[[Joey]]
+
+> [[done]]! --[[Joey]]
+
+-----
+
+> In the coming months, Amazon S3 will introduce an option that will allow customers to seamlessly move data between Amazon S3 and Amazon Glacier based on data lifecycle policies.
+
+-- 
+
+>> They did, but it's IMHO not very useful for git-annex. It's rather
+>> intended to allow aging S3 storage out to Glacier. --[[Joey]] 
diff --git a/doc/todo/special_remote_for_amazon_glacier/comment_1_68f129441eefcbfebf7a9db680f52759._comment b/doc/todo/special_remote_for_amazon_glacier/comment_1_68f129441eefcbfebf7a9db680f52759._comment
new file mode 100644
index 0000000000..68593be425
--- /dev/null
+++ b/doc/todo/special_remote_for_amazon_glacier/comment_1_68f129441eefcbfebf7a9db680f52759._comment
@@ -0,0 +1,8 @@
+[[!comment format=mdwn
+ username="http://mike.magin.org/"
+ nickname="mmagin"
+ subject="comment 1"
+ date="2012-09-14T04:19:53Z"
+ content="""
+When I first heard about Glacier, it sounded great for a cheap backup copy, and I was thinking about writing a \"hook\" remote, but once I read some better analysis of the pricing (e.g. [[http://www.daemonology.net/blog/2012-09-04-thoughts-on-glacier-pricing.html]]) I rapidly lost interest.
+"""]]
diff --git a/doc/todo/special_remote_for_amazon_glacier/comment_2_c5eeaf8ceee414fa0379831ca52e290c._comment b/doc/todo/special_remote_for_amazon_glacier/comment_2_c5eeaf8ceee414fa0379831ca52e290c._comment
new file mode 100644
index 0000000000..701047f91c
--- /dev/null
+++ b/doc/todo/special_remote_for_amazon_glacier/comment_2_c5eeaf8ceee414fa0379831ca52e290c._comment
@@ -0,0 +1,7 @@
+[[!comment format=mdwn
+ username="basak"
+ subject="comment 2"
+ date="2012-09-21T22:21:04Z"
+ content="""
+I've created a glacier command line interface that integrates with git-annex [here](https://github.com/basak/glacier-cli), currently using the hook special remote mechanism. To get around the time delay, operations which require a job submission will submit the job and then fail. Retrying again four hours later should then succeed. It seems to work pretty well with git-annex.
+"""]]
diff --git a/doc/todo/speed_up_fsck.mdwn b/doc/todo/speed_up_fsck.mdwn
new file mode 100644
index 0000000000..5d5e867f80
--- /dev/null
+++ b/doc/todo/speed_up_fsck.mdwn
@@ -0,0 +1,40 @@
+moving to the git-annex branch has slowed down fsck worse than most
+commands. Actually, some commands have sped up, while others like get
+are slightly slower but are swamped by the normal runtime. 
+
+For fsck though, it has to pull each file's location log info out of git.
+And, it's typically run on the entire tree.
+
+Another slow one in `git annex copy --from`.
+
+It would be possible to run a single `git cat-file --batch` and pass it
+sha1s of location logs for file that is going to be fsked (gotten via
+`read-tree`). Then just read its output until the next requested sha1 to
+chunk it, and pass this in to fsck in a closure.
+
+The difficulty, besides writing that is that everything that works with
+location logs now reads them out of git, would need to find a way to
+provide the info on a side channel of some sort.
+
+If this is implemented, the same infrastructure could be used for other
+commands like whereis and add. --[[Joey]]
+
+> Updated plan:
+> 
+> Run `git ls-file --batch`, and cache its stdin and out handles in Branch
+> state.
+> 
+> To see a git-annex branch file, send it something like
+> "git-annex:uuid.log", and read the content fron stdout handle.
+> 
+> To detect the end of content, send "TOKEN\n", and look for 
+> "TOKEN missing" in its output. A good choice for TOKEN is anything
+> that will never exist in the repo; 40 0's would be a fairly good choice,
+> but even better seems to be something completely invalid and impossible
+> to have as a sha1 or filename or ref: "".
+> 
+> Hmm, except that's actually an error message sent to stderr. Unless
+> stderr is connected to stdout, it might be better to look for a known,
+> empty object. Could just add a git-annex:empty file to that end.
+
+[[done]] --[[Joey]] 
diff --git a/doc/todo/stream_feature__63__.mdwn b/doc/todo/stream_feature__63__.mdwn
new file mode 100644
index 0000000000..860edfc811
--- /dev/null
+++ b/doc/todo/stream_feature__63__.mdwn
@@ -0,0 +1,23 @@
+I am just asking myself, is it stupid to think that streaming in git annex would be a good idea and wouldnt it be totaly easy to implement?
+
+Ok just tried to link to files over ssh, it creates a link but you cant open with it its content ^^
+
+But at least the files you have access over some filesystem as example samba/sshfs or just a other directory or usb-drive you could stream instead of "get"
+
+you could add another mode like direct and indirect, like symbolic-links or something like that?
+
+Sadly linux is to stupid to allow direct ssh links ( thats maybe one of the biggest features hurd has over linux  ) but you could mount with sshfs readonly (checking first if sshfs is installed) to mount the files there and then map the links there.
+
+ok I am not so shure how hard it would be and how much bug potentials it creates, but it would be great I guess.
+
+git annex is a bit like a telephone book, where you get a list of where the targets are. So using it to call the persons so that they drive to you to talk with you is nice. But I think the better feature would be if you just talk with the guy over the telephone directly bevore he comes to you (streaming)
+
+I mean you did one great thing, you did make cloudy thing peer to peer, like git is targeted too but for smaller files, yes there are may use cases without this feature, but I would be really glad if it could do that too, if I give annex 5 locations on other pcs usb-sticks etc, I find it stupid to additionaly do setup all this sources again a second time for streaming, and then I have maybe even 2 different file names because you map stuff in git.
+
+So sorry its late here, I am a bit tired so I maybe dont know what I am talking right now, my english isnt the best, too, but I think it would be a great feature.
+
+I mean on your setup, with slow internet, you maybe always make a get command, but here, if I link to youtube, I have no problem to stream it, or even on internal network between my pcs I have gb-lan, I start directly movies streaming, I would only use get, in rare cases where I need them on a train, the normal thing is to stream stuff.
+
+So I have to go sleep now 
+
+bye
diff --git a/doc/todo/support-non-utf8-locales.mdwn b/doc/todo/support-non-utf8-locales.mdwn
new file mode 100644
index 0000000000..da40118d52
--- /dev/null
+++ b/doc/todo/support-non-utf8-locales.mdwn
@@ -0,0 +1,26 @@
+Currenty, git-annex forces output, particularly of filenames, in a utf-8
+locale.
+
+Note that this does not mean it cannot be used with filenames in other
+encodings. git-annex is entirely encoding agnostic when it comes to 
+manipulating filenames. It just *displays* their names always converted to
+utf-8, which  may not look right when you have a non-utf8 locale.
+
+This had to be done to work around some bugs with haskell's handling
+of filename encodings. In particular,
+
+* [[bugs/unhappy_without_UTF8_locale]]: haskell crashes when told to output 
+  a string with characters > 255 in a non-utf8 locale.
+* [[bugs/problems_with_utf8_names]]: On many OSs, haskell expects
+  non-decoded raw char8 in FilePaths. In order to display a filename,
+  though, it needs to first be decoded, and git-annex currently assumes
+  it was encoded as utf8.
+
+git-annex's behavior is unlikely to improve much until haskell's
+support for utf8 filenames improves. --[[Joey]]
+
+> [[done]] -- I just turned off all encoding handling on stdout and stderr,
+> which avoids these problems nicely. Git-annex now displays just what it
+> input, at least on platforms where haskell does not decode unicode in
+> FilePaths. This will later be a problem when it gets localized, but for
+> now works great. --[[Joey]]
diff --git a/doc/todo/support_S3_multipart_uploads.mdwn b/doc/todo/support_S3_multipart_uploads.mdwn
new file mode 100644
index 0000000000..711ac41b2a
--- /dev/null
+++ b/doc/todo/support_S3_multipart_uploads.mdwn
@@ -0,0 +1,14 @@
+Did not know of this when I wrote S3 support. Ability to resume large
+uploads would be good.
+
+
+
+Also allows supporting files > 5 gb, a S3 limit I was not aware of.
+
+NB: It would work just as well to split the object and upload the N parts
+to S3, but not bother with S3's paperwork to rejoin them into one object. 
+Only reasons not to do that are a) backwards compatability with 
+the existing S3 remote and b) this would not allow accessing the content
+in S3 w/o using git-annex, which could be useful in some scenarios.
+
+--[[Joey]]
diff --git a/doc/todo/support_for_lossy_remotes.mdwn b/doc/todo/support_for_lossy_remotes.mdwn
new file mode 100644
index 0000000000..e757343f47
--- /dev/null
+++ b/doc/todo/support_for_lossy_remotes.mdwn
@@ -0,0 +1,5 @@
+I'm curious if there's a possibility to support lossy remotes. It may be handy to support syncing to special remotes that do lossy compression on the files (e.g., videos and images). For example, one could imagine having a YouTube special remote that only syncs video files. The original files wouldn't be available for download due to the transcoding and compression that YouTube does, so they wouldn't count towards the number of copies. In this YouTube example, the user gains:
+
+1. an online place that their videos are available from
+2. a worst-case scenario "backup"
+3. a remote that they could download smaller video files
diff --git a/doc/todo/support_for_lossy_remotes/comment_1_f5cd9f9deab13ab2d2290ad763906dd3._comment b/doc/todo/support_for_lossy_remotes/comment_1_f5cd9f9deab13ab2d2290ad763906dd3._comment
new file mode 100644
index 0000000000..1e895944cb
--- /dev/null
+++ b/doc/todo/support_for_lossy_remotes/comment_1_f5cd9f9deab13ab2d2290ad763906dd3._comment
@@ -0,0 +1,8 @@
+[[!comment format=mdwn
+ username="http://joeyh.name/"
+ ip="4.154.4.90"
+ subject="comment 1"
+ date="2013-07-16T19:16:54Z"
+ content="""
+There is already one example of a lossy remote: If you use `git annex addurl --relaxed` it generates a key that just uses the url, without its size. When retreiving such a key, any content will be accepted.
+"""]]
diff --git a/doc/todo/support_for_writing_external_special_remotes.mdwn b/doc/todo/support_for_writing_external_special_remotes.mdwn
new file mode 100644
index 0000000000..2d7cd9d15d
--- /dev/null
+++ b/doc/todo/support_for_writing_external_special_remotes.mdwn
@@ -0,0 +1,25 @@
+It would be good to have something in between the hook special remote and
+the built-in special remotes. The hook is easy to set up, but its simple
+interface misses some features that a more full-features interface could
+provide to a third-party program that wants to provide the best possible
+special remote it can w/o being written in haskell:
+
+* No way to send progress updates when uploading, so no progress bars for uploads from hook special remotes in the webapp.
+* No way to verify the `initremote` parameters include all needed configuration, and do any initalization needed.
+* No way to query and/or set the remote.log while it's running. (Well, technically, `git annex enableremote` can set values..)
+* No way to store per-key information to the git-annex branch.
+* No (easy) way to split files into chunks.
+
+Some of these features could be added to git-annex as subcommands. Which would
+improve the general programmability and flexability of git-annex. OTOH,
+running `git-annex upload-progress` repeatedly is pretty ugly. Or the
+interface could provide a channel for the program and git-annex to
+communicate back and forth on. Maybe a mix of the two?
+
+A final feature such an interface should provide is the ability to drop a
+program into PATH and have it just work, without the user needing to do any
+configuration beyond `initremote`. So, `git annex initremote foo type=$bar`
+should look for `git-annex-remote-$bar` in PATH if that's not a built-in
+special remote name.
+
+--[[Joey]]
diff --git a/doc/todo/support_fsck_in_bare_repos.mdwn b/doc/todo/support_fsck_in_bare_repos.mdwn
new file mode 100644
index 0000000000..32ced467e0
--- /dev/null
+++ b/doc/todo/support_fsck_in_bare_repos.mdwn
@@ -0,0 +1,17 @@
+What is says on the tin:
+
+    22:56:54 < RichiH> joeyh_: by the way, i have been thinking about fsck on bare repos
+    22:57:37 < RichiH> joeyh_: the best i could come with is to have a bare and a non-bare access the same repo store
+    22:58:00 < RichiH> joeyh_: alternatively, with the SHA* backend, you have all the information to verify that the local data is correct
+    22:58:41 < RichiH> and verifying that would already be a plus. if there  really _is_ a problem, having the SHA is enough to track issues down
+    23:09:50 < joeyh_> oh, I think I have code that fsck could use on bare repos already.. just a matter of wiring it up
+    23:10:42 < joeyh_> feel free to reopen a bug or whatever so I remember.. the unused command's branch content enumeration could be used in a bare repo
+    23:14:51 < joeyh_> unused/dropunused could work in bare repos too btw
+
+> Also `status`'s total annex keys/size could be handled for bare repos. --[[Joey]] 
+
+>> Fsck is done. Rest not done yet. --[[Joey]]
+
+>>> all [[done]]! --[[Joey]] 
+
+[[!meta title="support unused, dropunused in bare repos"]]
diff --git a/doc/todo/symlink_farming_commit_hook.mdwn b/doc/todo/symlink_farming_commit_hook.mdwn
new file mode 100644
index 0000000000..3e93cb34b8
--- /dev/null
+++ b/doc/todo/symlink_farming_commit_hook.mdwn
@@ -0,0 +1,14 @@
+TODO: implement below
+
+git-annex does use a lot of symlinks. Specicially, relative symlinks,
+that are checked into git. To allow you to move those around without
+annoyance, git-annex can run as a post-commit hook. This way, you can `git mv`
+a symlink to an annexed file, and as soon as you commit, it will be fixed
+up.
+
+`git annex init` tries to set up a post-commit hook that is itself a symlink
+back to git-annex. If you want to have your own shell script in the post-commit
+hook, just make it call `git annex` with no parameters. git-annex will detect
+when it's run from a git hook and do the necessary fixups.
+
+[[done]]
diff --git a/doc/todo/sync_my_local_git-annex_from_a_dump_remote.mdwn b/doc/todo/sync_my_local_git-annex_from_a_dump_remote.mdwn
new file mode 100644
index 0000000000..524782bc72
--- /dev/null
+++ b/doc/todo/sync_my_local_git-annex_from_a_dump_remote.mdwn
@@ -0,0 +1,6 @@
+As discussed on debconf, I have the following use case:
+
+* I have a dump remote, a folder on my webserver where files are uploaded through the web app. I don't have git on the webserver, just a plain folder.
+* I have git-annex repo on a development server. The development server polls the webserver (ssh/ftp) once in an hour and synchronizes the state of the local git-annex repo with the state found on the webserver and commits that.
+* This is not meant to be backup facility. I just want to be able to have a state on my development machine that is very likely to the state on the webserver.
+
diff --git a/doc/todo/sync_my_local_git-annex_from_a_dump_remote/comment_1_81d63854f89f00855cda5ace0fc8262a._comment b/doc/todo/sync_my_local_git-annex_from_a_dump_remote/comment_1_81d63854f89f00855cda5ace0fc8262a._comment
new file mode 100644
index 0000000000..d9abb3a3cb
--- /dev/null
+++ b/doc/todo/sync_my_local_git-annex_from_a_dump_remote/comment_1_81d63854f89f00855cda5ace0fc8262a._comment
@@ -0,0 +1,14 @@
+[[!comment format=mdwn
+ username="http://joeyh.name/"
+ ip="2001:4978:f:21a::2"
+ subject="comment 1"
+ date="2013-08-13T21:44:17Z"
+ content="""
+We had a conversation about this IRL. At the time, I thought I understood what you wanted. Reading the above, I am not so sure.
+
+What I thought you wanted was something like `git annex mirror --from remote`, which would, for each object known to git-annex that the location log said was present on the remote, make sure that the local repo had the object too, and for each object that the location log said was not present on the remote, drop it from the local repo (if numcopies etc allowed).
+
+`git annex mirror --to remote` could also be used as the complement of the above.
+
+If that's not the sort of thing you meant, let me know.
+"""]]
diff --git a/doc/todo/sync_my_local_git-annex_from_a_dump_remote/comment_2_66822b72b1450e79e8edd0c6c21d5aa6._comment b/doc/todo/sync_my_local_git-annex_from_a_dump_remote/comment_2_66822b72b1450e79e8edd0c6c21d5aa6._comment
new file mode 100644
index 0000000000..3d459371f5
--- /dev/null
+++ b/doc/todo/sync_my_local_git-annex_from_a_dump_remote/comment_2_66822b72b1450e79e8edd0c6c21d5aa6._comment
@@ -0,0 +1,14 @@
+[[!comment format=mdwn
+ username="http://thkoch2001.myopenid.com/"
+ nickname="thkoch"
+ subject="pseudocode"
+ date="2013-08-14T04:58:22Z"
+ content="""
+lets say my local annex is in direct mode, then the following might already do what I want:
+
+cd $LOCAL_ANNEX
+rsync --recursive --delete $REMOTE .
+git annex add && git commit
+
+It would however be nice if I could do the same with an annex in indirect mode or even a bare annex.
+"""]]
diff --git a/doc/todo/tahoe_lfs_for_reals.mdwn b/doc/todo/tahoe_lfs_for_reals.mdwn
new file mode 100644
index 0000000000..9019767eb9
--- /dev/null
+++ b/doc/todo/tahoe_lfs_for_reals.mdwn
@@ -0,0 +1,21 @@
+[[forum/tips:_special__95__remotes__47__hook_with_tahoe-lafs]] is a good
+start, but Zooko points out that using Tahoe's directory translation layer
+incurs O(N^2) overhead as the number of objects grows. Also, making
+hash subdirectories in Tahoe is expensive. Instead it would be good to use
+it as a key/value store directly. The catch is that doing so involves
+sending the content to Tahoe, and getting back a key identifier.
+
+This would be fairly easy to do as a [[backend|backends]], which can assign its
+own key names (although typically done before data is stored in it),
+but a tahoe-lafs special remote would be more flexible.
+
+To support a special remote, a mapping is needed from git-annex keys to
+Tahoe keys.
+
+The best place to store this mapping is perhaps as a new field in the
+location log:
+
+	date present repo-uuid newfields
+
+This way, each remote can store its own key-specfic data in the same place
+as other key-specific data, with minimal overhead.
diff --git a/doc/todo/tahoe_lfs_for_reals/comment_1_0a4793ce6a867638f6e510e71dd4bb44._comment b/doc/todo/tahoe_lfs_for_reals/comment_1_0a4793ce6a867638f6e510e71dd4bb44._comment
new file mode 100644
index 0000000000..16ef882a42
--- /dev/null
+++ b/doc/todo/tahoe_lfs_for_reals/comment_1_0a4793ce6a867638f6e510e71dd4bb44._comment
@@ -0,0 +1,10 @@
+[[!comment format=mdwn
+ username="zooko"
+ ip="97.118.97.117"
+ subject="performance"
+ date="2011-05-17T19:20:39Z"
+ content="""
+Hm... O(N^2)? I think it just takes O(N). To read an entry out of a directory you have to download the entire directory (and store it in RAM and parse it). The constants are basically \"too big to be good but not big enough to be prohibitive\", I think. jctang has reported that his special remote hook performs well enough to use, but it would be nice if it were faster.
+
+The Tahoe-LAFS folks are working on speeding up mutable files, by the way, after which we would be able to speed up directories.
+"""]]
diff --git a/doc/todo/tahoe_lfs_for_reals/comment_2_80b9e848edfdc7be21baab7d0cef0e3a._comment b/doc/todo/tahoe_lfs_for_reals/comment_2_80b9e848edfdc7be21baab7d0cef0e3a._comment
new file mode 100644
index 0000000000..6dba86c47c
--- /dev/null
+++ b/doc/todo/tahoe_lfs_for_reals/comment_2_80b9e848edfdc7be21baab7d0cef0e3a._comment
@@ -0,0 +1,13 @@
+[[!comment format=mdwn
+ username="http://joey.kitenet.net/"
+ nickname="joey"
+ subject="comment 2"
+ date="2011-05-17T19:57:33Z"
+ content="""
+Whoops! You'd only told me O(N) twice before..
+
+So this is not too high priority. I think I would like to get the per-remote storage sorted out anyway, since probably it will be the thing needed to convert the URL backend into a special remote, which would then allow ripping out the otherwise unused pluggable backend infrastructure.
+
+Update: Per-remote storage is now sorted out, so this could be implemented
+if it actually made sense to do so.
+"""]]
diff --git a/doc/todo/union_mounting.mdwn b/doc/todo/union_mounting.mdwn
new file mode 100644
index 0000000000..c42a055021
--- /dev/null
+++ b/doc/todo/union_mounting.mdwn
@@ -0,0 +1,10 @@
+It should be possible to union mount annexes. So if multiple drives have
+content, an annex mounting them both would have available all the 
+content from all the drives.
+
+This could be done by just making .git/annex/KEY link to the actual content
+on the mounted annex.
+
+(Need to make sure the [[copy_tracking|copies]] code does not
+confused and think the symlink is a copy of the content.. Also need to make
+sure that code that writes to .git/annex does not follow symlinks.))
diff --git a/doc/todo/union_mounting/comment_1_cb08435812dd7766de26199c73f38e8b._comment b/doc/todo/union_mounting/comment_1_cb08435812dd7766de26199c73f38e8b._comment
new file mode 100644
index 0000000000..3fadf6fa3c
--- /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
index 0000000000..08901ee178
--- /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/untracked_remotes.mdwn b/doc/todo/untracked_remotes.mdwn
new file mode 100644
index 0000000000..883b5acffd
--- /dev/null
+++ b/doc/todo/untracked_remotes.mdwn
@@ -0,0 +1,9 @@
+Seems that a fairly common desire in some use cases is to be able to make a
+clone of a repository and be able to get files, without updating the
+location tracking information. (And without even recording a uuid in the
+remote.log.) Use cases include wanting to have temporary
+clones without cluttering history, and centralized development where the
+developers don't care to know about one-another's systems.
+
+It seems that such an untracked repository would need to automatically
+consider itself untrusted. Is that enough to avoid losing data?
diff --git a/doc/todo/use_cp_reflink.mdwn b/doc/todo/use_cp_reflink.mdwn
new file mode 100644
index 0000000000..39518abf18
--- /dev/null
+++ b/doc/todo/use_cp_reflink.mdwn
@@ -0,0 +1,7 @@
+The unlock command needs to copy a file, and it would be great to use this:
+	cp --reflink=auto src dst
+
+O(1) overhead on BTRFS. Needs coreutils 7.6; and remember that git-annex
+may be used on systems without coreutils..
+
+[[done]]
diff --git a/doc/todo/using_url_backend.mdwn b/doc/todo/using_url_backend.mdwn
new file mode 100644
index 0000000000..1f3cd56281
--- /dev/null
+++ b/doc/todo/using_url_backend.mdwn
@@ -0,0 +1,11 @@
+There is no way to `git annex add` a file using the URL [[backend|backends]].
+
+For now, we have to manually make the symlink. Something like this:
+
+	ln -s .git/annex/URL:http:%%www.example.com%foo.tar.gz
+
+Note the escaping of slashes.
+
+A `git annex register ` command could do this..
+
+[[done]]
diff --git a/doc/todo/windows_support.mdwn b/doc/todo/windows_support.mdwn
new file mode 100644
index 0000000000..08327dba8b
--- /dev/null
+++ b/doc/todo/windows_support.mdwn
@@ -0,0 +1,15 @@
+The git-annex Windows port is not ready for prime time. But it does exist
+now! --[[Joey]] 
+
+## status
+
+* Does not work with Cygwin's build of git (that git does not consistently
+  support use of DOS style paths, which git-annex uses on Windows). 
+  Must use the upstream build of git for Windows.
+* rsync special remotes are known buggy.
+* Bad file locking, it's probably not safe to run more than one git-annex
+  process at the same time on Windows.
+* No support for the assistant or webapp.
+* Ssh connection caching does not work on Windows, so `git annex get`
+  has to connect twice to the remote system over ssh per file, which
+  is much slower than on systems supporting connection caching.
diff --git a/doc/todo/windows_support/comment_10_394127e34e07ab3dc0e7b94ee6898866._comment b/doc/todo/windows_support/comment_10_394127e34e07ab3dc0e7b94ee6898866._comment
new file mode 100644
index 0000000000..fb061962e5
--- /dev/null
+++ b/doc/todo/windows_support/comment_10_394127e34e07ab3dc0e7b94ee6898866._comment
@@ -0,0 +1,8 @@
+[[!comment format=mdwn
+ username="http://joeyh.name/"
+ ip="4.152.108.145"
+ subject="comment 10"
+ date="2013-08-04T18:23:00Z"
+ content="""
+Encryption is now working on Windows.
+"""]]
diff --git a/doc/todo/windows_support/comment_1_3cc26ad8101a22e95a8c60cf0c4dedcc._comment b/doc/todo/windows_support/comment_1_3cc26ad8101a22e95a8c60cf0c4dedcc._comment
new file mode 100644
index 0000000000..fd5b6f5cd3
--- /dev/null
+++ b/doc/todo/windows_support/comment_1_3cc26ad8101a22e95a8c60cf0c4dedcc._comment
@@ -0,0 +1,10 @@
+[[!comment format=mdwn
+ username="https://www.google.com/accounts/o8/id?id=AItOawkRITTYYsN0TFKN7G5sZ6BWGZOTQ88Pz4s"
+ nickname="Zoltán"
+ subject="cygwin"
+ date="2012-05-15T00:14:08Z"
+ content="""
+What about [Cygwin](http://cygwin.com/)? It emulates POSIX fairly well under Windows (including signals, forking, fs (also things like /dev/null, /proc), unix file permissions), has all standard gnu utilities. It also emulates symlinks, but they are unfortunately incompatible with NTFS symlinks introduced in Vista [due to some stupid restrictions on Windows](http://cygwin.com/ml/cygwin/2009-10/msg00756.html).
+
+If git-annex could be modified to not require symlinks to work, the it would be a pretty neat solution (and you get a real shell, not some command.com on drugs (aka cmd.exe))
+"""]]
diff --git a/doc/todo/windows_support/comment_2_8acae818ce468967499050bbe3c532ea._comment b/doc/todo/windows_support/comment_2_8acae818ce468967499050bbe3c532ea._comment
new file mode 100644
index 0000000000..e37a555756
--- /dev/null
+++ b/doc/todo/windows_support/comment_2_8acae818ce468967499050bbe3c532ea._comment
@@ -0,0 +1,12 @@
+[[!comment format=mdwn
+ username="https://www.google.com/accounts/o8/id?id=AItOawk5cj-itfFHq_yhJHdzk3QOPp-PNW_MjPU"
+ nickname="Michael"
+ subject="+1 Cygwin"
+ date="2012-05-23T19:30:21Z"
+ content="""
+Windows support is a must. In my experience, binary file means proprietary editor, which means Windows.
+
+Unfortunately, there's not much overlap between people who use graphical editors in Windows all day vs. people who are willing to tolerate Cygwin's setup.exe, compile a Haskell program, learn git and git-annex's 90-odd subcommands, and use a mintty terminal to manage their repository, especially now that there's a sexy GitHub app for Windows.
+
+That aside, I think Windows-based content producers are still *the* audience for git-annex. First Windows support, then a GUI, then the world.
+"""]]
diff --git a/doc/todo/windows_support/comment_3_bd0a12f4c9b884ab8a06082842381a01._comment b/doc/todo/windows_support/comment_3_bd0a12f4c9b884ab8a06082842381a01._comment
new file mode 100644
index 0000000000..0b48db7502
--- /dev/null
+++ b/doc/todo/windows_support/comment_3_bd0a12f4c9b884ab8a06082842381a01._comment
@@ -0,0 +1,8 @@
+[[!comment format=mdwn
+ username="http://xolus.net/openid/max"
+ nickname="B0FH"
+ subject="What about NTFS support ?"
+ date="2012-08-02T17:45:10Z"
+ content="""
+Has git-annex been tested with an NTFS-formatted disk under Linux ? NTFS is supposed to be case-sensitive and to allow symlinks, and these are supposed to work with ntfs3g.
+"""]]
diff --git a/doc/todo/windows_support/comment_4_ad06b98b2ddac866ffee334e41fee6a8._comment b/doc/todo/windows_support/comment_4_ad06b98b2ddac866ffee334e41fee6a8._comment
new file mode 100644
index 0000000000..66f9ca71fd
--- /dev/null
+++ b/doc/todo/windows_support/comment_4_ad06b98b2ddac866ffee334e41fee6a8._comment
@@ -0,0 +1,8 @@
+[[!comment format=mdwn
+ username="https://www.google.com/accounts/o8/id?id=AItOawlc1og3PqIGudOMkFNrCCNg66vB7s-jLpc"
+ nickname="Paul"
+ subject="Re: What about NTFS support?"
+ date="2012-08-16T19:30:38Z"
+ content="""
+I successfully use git-annex on an NTFS formatted external USB drive, so yes, it is possible and works well.
+"""]]
diff --git a/doc/todo/windows_support/comment_5_444fc7251f57db241b6e80abae41851c._comment b/doc/todo/windows_support/comment_5_444fc7251f57db241b6e80abae41851c._comment
new file mode 100644
index 0000000000..8f76ee2587
--- /dev/null
+++ b/doc/todo/windows_support/comment_5_444fc7251f57db241b6e80abae41851c._comment
@@ -0,0 +1,10 @@
+[[!comment format=mdwn
+ username="https://me.yahoo.com/a/dASECLNzvckz4VwqUGYsvthiplY.#d2c27"
+ nickname="A. D. Sicks"
+ subject="comment 5"
+ date="2012-09-09T23:48:21Z"
+ content="""
+Haskell has C++ binding, so it should be possible to port it to .net/Mono with a VB GUI for Windows users.  Windows has a primitive form of symlinks called shortcuts.  Perhaps shortcut support in Windows could replace the use of symlinks.  I've used shortcuts since XP to put my home Windows directory on another partition and never had a hitch...
+
+If anyone is interested in working on this, hit me up.  I would like to use this in my XP vbox to have access to files on my host OS...I have a student edition of Visual Studio 2005 to do an open source port...
+"""]]
diff --git a/doc/todo/windows_support/comment_6_34f1f60b570c389bb1e741b990064a7e._comment b/doc/todo/windows_support/comment_6_34f1f60b570c389bb1e741b990064a7e._comment
new file mode 100644
index 0000000000..bf9f86f419
--- /dev/null
+++ b/doc/todo/windows_support/comment_6_34f1f60b570c389bb1e741b990064a7e._comment
@@ -0,0 +1,8 @@
+[[!comment format=mdwn
+ username="https://www.google.com/accounts/o8/id?id=AItOawnlwEMhiNYv__mEUABW4scn83yMraC3hqE"
+ nickname="Sean"
+ subject="NTFS symlinks"
+ date="2013-01-11T21:44:21Z"
+ content="""
+It seems that NTFS (from Vista forward) has full POSIX support for symlinks. At least, Wikipedia [seems to think so.](http://en.wikipedia.org/wiki/NTFS_symbolic_link). What about doing like GitHub and using MinGW for compatibility? Cygwin absolutely blows in terms of installation size and compatability with the rest of Windows.
+"""]]
diff --git a/doc/todo/windows_support/comment_7_a5ca56c487257434650420acfa60e39f._comment b/doc/todo/windows_support/comment_7_a5ca56c487257434650420acfa60e39f._comment
new file mode 100644
index 0000000000..0ee09ac5a2
--- /dev/null
+++ b/doc/todo/windows_support/comment_7_a5ca56c487257434650420acfa60e39f._comment
@@ -0,0 +1,8 @@
+[[!comment format=mdwn
+ username="https://www.google.com/accounts/o8/id?id=AItOawlpSOjMH7Iaz56v6Pr9KCFSpbvMXvg-y9o"
+ nickname="Dominik"
+ subject="So close :-)"
+ date="2013-06-30T12:46:40Z"
+ content="""
+I was fighting my way forward until I read here that special remote with ssh+rsync and encryption doesn't work. Interestingly I got everything working so far, ssh login is keybased, gpg -k works and the remote setup also correctly cooperated with gpg... but it just didn't sync. Any ideas how complex it is to get this last missing piece moving?
+"""]]
diff --git a/doc/todo/windows_support/comment_8_61214de7d967740d42905f3823ce2f65._comment b/doc/todo/windows_support/comment_8_61214de7d967740d42905f3823ce2f65._comment
new file mode 100644
index 0000000000..fe193f7e07
--- /dev/null
+++ b/doc/todo/windows_support/comment_8_61214de7d967740d42905f3823ce2f65._comment
@@ -0,0 +1,12 @@
+[[!comment format=mdwn
+ username="http://joeyh.name/"
+ ip="4.154.4.193"
+ subject="comment 8"
+ date="2013-06-30T17:58:08Z"
+ content="""
+It should be easy to fix whatever's wrong the the rsync special remote. Just a matter of debugging that.
+
+Adding encryption support on Windows is stuck at a roadblock I don't know the way around. To drive gpg, git-annex uses the `--passphrase-fd` option, and sends the \"passphrase\" (really a big block of binary foo!) over a file descriptor of a pipe that it set up.
+
+Windows, AFAIK, doesn't have file descriptors, or at least there is no equivilant to them that I have access to in the Haskell POSIX compatability layer for Windows. I am reluctant to fall back to using `--passphrase-file` on Windows, since that would be a massive security hole (as would passing the passphrase as a parameter via `--passphrase=`).
+"""]]
diff --git a/doc/todo/windows_support/comment_9_259a0b1a6f4d8d1944173380adc5e7c8._comment b/doc/todo/windows_support/comment_9_259a0b1a6f4d8d1944173380adc5e7c8._comment
new file mode 100644
index 0000000000..9ae337886e
--- /dev/null
+++ b/doc/todo/windows_support/comment_9_259a0b1a6f4d8d1944173380adc5e7c8._comment
@@ -0,0 +1,8 @@
+[[!comment format=mdwn
+ username="https://www.google.com/accounts/o8/id?id=AItOawlpSOjMH7Iaz56v6Pr9KCFSpbvMXvg-y9o"
+ nickname="Dominik"
+ subject="comment 9"
+ date="2013-07-31T10:29:51Z"
+ content="""
+The tradeoff for me is a \"local\" security hole (where I can secure my own laptop) vs. a remotely exploitable thing... If it needs to go through a file, so be it -- it would however be good if that file would be overwritten with garbage before being deleted :-)
+"""]]
diff --git a/doc/todo/wishlist:_Add_to_Android_version_to_Google_Play.mdwn b/doc/todo/wishlist:_Add_to_Android_version_to_Google_Play.mdwn
new file mode 100644
index 0000000000..f9016fb4d3
--- /dev/null
+++ b/doc/todo/wishlist:_Add_to_Android_version_to_Google_Play.mdwn
@@ -0,0 +1,9 @@
+If possible a frequently updated daily build in separate package for those more adventurous of us.
+
+It would make installing and testing much easier and no need to change configuration settings to allow untrusted source.
+
+> While it's vaid to wish that someone might put the apk into Google Play,
+> I a) don't feel it's ready b) don't know if I want to go through
+> the rigamarole required to use that service and c) don't feel this
+> bug tracker is an appropriate place to track what is effectively a
+> nontechnical request. [[done]] --[[Joey]] 
diff --git a/doc/todo/wishlist:_Advanced_settings_for_xmpp_and_webdav.mdwn b/doc/todo/wishlist:_Advanced_settings_for_xmpp_and_webdav.mdwn
new file mode 100644
index 0000000000..96552eecca
--- /dev/null
+++ b/doc/todo/wishlist:_Advanced_settings_for_xmpp_and_webdav.mdwn
@@ -0,0 +1,7 @@
+It would be very nice with an "advanced settings" for jabber and webdav support.
+
+Currently XMPP fails if you use a google apps account. Since the domain provided in the email is not the same as the XMPP server.
+
+Same goes for webdav support. If i have my own webdav server somewhere on the internet there is no way to set it up in the assistant.
+
+[[!tag /design/assistant]]
diff --git a/doc/todo/wishlist:_Advanced_settings_for_xmpp_and_webdav/comment_1_11c7444ab4988c60732af505b52bde3c._comment b/doc/todo/wishlist:_Advanced_settings_for_xmpp_and_webdav/comment_1_11c7444ab4988c60732af505b52bde3c._comment
new file mode 100644
index 0000000000..61cd3fc2e7
--- /dev/null
+++ b/doc/todo/wishlist:_Advanced_settings_for_xmpp_and_webdav/comment_1_11c7444ab4988c60732af505b52bde3c._comment
@@ -0,0 +1,20 @@
+[[!comment format=mdwn
+ username="https://www.google.com/accounts/o8/id?id=AItOawmWg4VvDTer9f49Y3z-R0AH16P4d1ygotA"
+ nickname="Tobias"
+ subject="Hooks too"
+ date="2013-05-22T10:40:33Z"
+ content="""
+It would actually be very nice if this could be done with hooks too.
+
+Especially with the new hook method.
+
+Take this hook
+
+	mega-hook = /usr/bin/python2 ~/sources/megaannex/megaannex.py
+
+git-annex could make a request with either the parameter(or environment variable) \"getsettingsobject\" that could return. {\"username\": \"\", \"password\": \"\", \"folder\": \"\"}.
+
+The point being git-annex can request from the hooks program what settings it takes, and give a web interface to set it. Then store the information in the creds folder(ew ew, that folder is unencrypted, oh well) and pass it to the hook on run. 
+
+The advantage being that users wouldn't have to edit a settings file manually (this is currently also the case for the IMAP special remote, which also requires a settings file).
+"""]]
diff --git a/doc/todo/wishlist:_An_--all_option_for_dropunused.mdwn b/doc/todo/wishlist:_An_--all_option_for_dropunused.mdwn
new file mode 100644
index 0000000000..bd35c0e55d
--- /dev/null
+++ b/doc/todo/wishlist:_An_--all_option_for_dropunused.mdwn
@@ -0,0 +1,4 @@
+Cleaning out a repository is presently a fairly manual process.  Am I missing a UI trick?  "dropunsed" with no arguments prints nothing at all; I think in that case it should display the list of what could be dropped.
+
+> [[done]]; comments seem satisfactory and I see no reason to complicate
+> dropunused to output something unused already outputs. --[[Joey]]
diff --git a/doc/todo/wishlist:_An_--all_option_for_dropunused/comment_1_d8726d108b3b40116b4ec3c9935f2dff._comment b/doc/todo/wishlist:_An_--all_option_for_dropunused/comment_1_d8726d108b3b40116b4ec3c9935f2dff._comment
new file mode 100644
index 0000000000..6c728a5d00
--- /dev/null
+++ b/doc/todo/wishlist:_An_--all_option_for_dropunused/comment_1_d8726d108b3b40116b4ec3c9935f2dff._comment
@@ -0,0 +1,8 @@
+[[!comment format=mdwn
+ username="http://joeyh.name/"
+ ip="4.154.0.23"
+ subject="comment 1"
+ date="2012-10-22T15:35:30Z"
+ content="""
+`git annex unused` prints the list
+"""]]
diff --git a/doc/todo/wishlist:_An_--all_option_for_dropunused/comment_2_578248f7686ba2d80d7dc8b17c0cdf52._comment b/doc/todo/wishlist:_An_--all_option_for_dropunused/comment_2_578248f7686ba2d80d7dc8b17c0cdf52._comment
new file mode 100644
index 0000000000..a87a367d61
--- /dev/null
+++ b/doc/todo/wishlist:_An_--all_option_for_dropunused/comment_2_578248f7686ba2d80d7dc8b17c0cdf52._comment
@@ -0,0 +1,16 @@
+[[!comment format=mdwn
+ username="http://hands.com/~phil/"
+ nickname="hands"
+ subject="and you can specify ranges to dropunused"
+ date="2012-11-02T09:07:48Z"
+ content="""
+so having run:
+
+    git annex unused
+
+you can then run:
+
+    git annex dropunused 1-10000
+
+or whatever, and it deletes the items in that range from the most recent unused invocation
+"""]]
diff --git a/doc/todo/wishlist:_An_option_like_--git-dir.mdwn b/doc/todo/wishlist:_An_option_like_--git-dir.mdwn
new file mode 100644
index 0000000000..cb9d374b39
--- /dev/null
+++ b/doc/todo/wishlist:_An_option_like_--git-dir.mdwn
@@ -0,0 +1,3 @@
+I'm currently integrating git-annex support into a filesystem synchronization tool that I use, and I have a use case where I'd like to run "git annex sync' on a local directory, and then automatically ssh over to remote hosts and run "git annex sync" in the related annex on that remote host.  However, while I can easily "cd" on the local, there is no really easy way to "cd" on the remote without a hack.
+
+If I could say: git annex --annex-dir=PATH sync, where PATH is the annex directory, it would solve all my problems, and would also provide a nice correlation to the --git-dir option used by most Git commands.  The basic idea is that I shouldn't have to be IN the directory to run git-annex commands, I should be able to tell git-annex which directory to apply its commands to.
diff --git a/doc/todo/wishlist:_An_option_like_--git-dir/comment_1_5d877d90b8bdf21d4b8649744d229efd._comment b/doc/todo/wishlist:_An_option_like_--git-dir/comment_1_5d877d90b8bdf21d4b8649744d229efd._comment
new file mode 100644
index 0000000000..8e7c3c03ea
--- /dev/null
+++ b/doc/todo/wishlist:_An_option_like_--git-dir/comment_1_5d877d90b8bdf21d4b8649744d229efd._comment
@@ -0,0 +1,8 @@
+[[!comment format=mdwn
+ username="https://www.google.com/accounts/o8/id?id=AItOawmBUR4O9mofxVbpb8JV9mEbVfIYv670uJo"
+ nickname="Justin"
+ subject="What about..."
+ date="2012-10-16T16:43:29Z"
+ content="""
+    ssh remotehost \"cd /path/to/annex && git annex sync\"
+"""]]
diff --git a/doc/todo/wishlist:_An_option_like_--git-dir/comment_2_462264821cbc48a433330cbf7ec6044d._comment b/doc/todo/wishlist:_An_option_like_--git-dir/comment_2_462264821cbc48a433330cbf7ec6044d._comment
new file mode 100644
index 0000000000..980658dc64
--- /dev/null
+++ b/doc/todo/wishlist:_An_option_like_--git-dir/comment_2_462264821cbc48a433330cbf7ec6044d._comment
@@ -0,0 +1,8 @@
+[[!comment format=mdwn
+ username="http://joeyh.name/"
+ ip="2001:4978:f:21a::2"
+ subject="comment 2"
+ date="2012-10-17T18:31:58Z"
+ content="""
+You can use `GIT_DIR`. It would not be hard to add a --git-dir option, the only catch is how to communicate that state on to where it constructs its git repository data structure. (I suppose it could just set GIT_DIR..)
+"""]]
diff --git a/doc/todo/wishlist:_An_option_like_--git-dir/comment_3_0c3709b07a0a1091ceeee73b69e0f7ac._comment b/doc/todo/wishlist:_An_option_like_--git-dir/comment_3_0c3709b07a0a1091ceeee73b69e0f7ac._comment
new file mode 100644
index 0000000000..a76c42d9d1
--- /dev/null
+++ b/doc/todo/wishlist:_An_option_like_--git-dir/comment_3_0c3709b07a0a1091ceeee73b69e0f7ac._comment
@@ -0,0 +1,8 @@
+[[!comment format=mdwn
+ username="https://me.yahoo.com/a/2grhJvAC049fJnvALDXek.6MRZMTlg--#eec89"
+ nickname="John"
+ subject="Response"
+ date="2012-10-20T05:21:13Z"
+ content="""
+@Justin If you have full shell access on the remote your solution works fine, but not if git-annex is the only binary you are allowed to execute.
+"""]]
diff --git a/doc/todo/wishlist:_GnuPG_options.mdwn b/doc/todo/wishlist:_GnuPG_options.mdwn
new file mode 100644
index 0000000000..2cadf82136
--- /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 .gitattributes 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 gpg.conf, but it's somewhat dirty since I do want to use compression in general.
+
+Here is how I envision the .git/config:
+
    [annex]
+        gnupg-options = --s2k-cipher-algo AES256 --s2k-digest-algo SHA512 --s2k-count 8388608 --cipher-algo AES256 --compress-algo none
+
+ +And compression could be enabled on say, text files, with a suitable wildcard in the .gitattributes file. +
    *.txt annex.gnupg-options="--s2k-cipher-algo AES256 --s2k-digest-algo SHA512 --s2k-count 8388608 --cipher-algo AES256 --compress-algo zlib"
+
+ +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:_GnuPG_options/comment_1_6662e8a71ce20acc62147ef41ecffa50._comment b/doc/todo/wishlist:_GnuPG_options/comment_1_6662e8a71ce20acc62147ef41ecffa50._comment new file mode 100644 index 0000000000..b756eccade --- /dev/null +++ b/doc/todo/wishlist:_GnuPG_options/comment_1_6662e8a71ce20acc62147ef41ecffa50._comment @@ -0,0 +1,12 @@ +[[!comment format=mdwn + username="http://joeyh.name/" + nickname="joey" + subject="comment 1" + date="2013-03-09T01:54:30Z" + content=""" +I'd be happy to apply a patch implementing annex.gnupg-options and/or per-remote remote.annex-gnupg-options, and I don't think it would be very hard to do. + +The gitattributes thing would be harder to do efficiently, and seems overkill. + + +"""]] diff --git a/doc/todo/wishlist:_Have_a_preview_of_download_or_upload_size.mdwn b/doc/todo/wishlist:_Have_a_preview_of_download_or_upload_size.mdwn new file mode 100644 index 0000000000..12688951d8 --- /dev/null +++ b/doc/todo/wishlist:_Have_a_preview_of_download_or_upload_size.mdwn @@ -0,0 +1,10 @@ +When using SSH remote repository, git-annex uses rsync to download or upload files one at a time. I would like to have a preview of the overall transfer size so that I can estimate the transfer duration. + +This could be done as an option of get, move or copy, or as a separated command. + +If part of get, move or copy, git-annex could print how much has been done or how much left between every files. + +Thanks. + +> [[done]]; `git-annex status .` seems to cover the requested use case. +> --[[Joey]] diff --git a/doc/todo/wishlist:_Have_a_preview_of_download_or_upload_size/comment_1_019a2457e07377510feaa089a93bd76c._comment b/doc/todo/wishlist:_Have_a_preview_of_download_or_upload_size/comment_1_019a2457e07377510feaa089a93bd76c._comment new file mode 100644 index 0000000000..4a59f37f12 --- /dev/null +++ b/doc/todo/wishlist:_Have_a_preview_of_download_or_upload_size/comment_1_019a2457e07377510feaa089a93bd76c._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="http://joeyh.name/" + ip="4.152.108.193" + subject="comment 1" + date="2013-06-25T17:26:25Z" + content=""" +git-annex is designed to work with really large trees of files, and so it processes files one at a time in a stream. To get an overall estimate of the size, it would need to traverse the whole directory to get the total, and then traverse it again to perform the transfer. This would make no-op transfers take twice as long, which is why I'm unlikely to implement it. +"""]] diff --git a/doc/todo/wishlist:_Have_a_preview_of_download_or_upload_size/comment_3_29a154699339bf040af0ee8aa24034f1._comment b/doc/todo/wishlist:_Have_a_preview_of_download_or_upload_size/comment_3_29a154699339bf040af0ee8aa24034f1._comment new file mode 100644 index 0000000000..9f0c040173 --- /dev/null +++ b/doc/todo/wishlist:_Have_a_preview_of_download_or_upload_size/comment_3_29a154699339bf040af0ee8aa24034f1._comment @@ -0,0 +1,15 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawnHRhCe3qwVKQ8_NOGGSYJnAMW6FFyKbOc" + nickname="Holger" + subject="comment 3" + date="2013-07-02T04:05:06Z" + content=""" +What do you think of the following simpler variant: + + % git annex size myfile1.zip + myfile1.zip is 1329 MB + % git annex size mydir/ + mydir contains 2803 files totaling 328 GB. + +If you're worried about running over the tree twice, it may be a good idea to store the size of a subtree along with the other metadata. +"""]] diff --git a/doc/todo/wishlist:_Have_a_preview_of_download_or_upload_size/comment_3_8f7e1c4a5c714cbd719ee170354d79fa._comment b/doc/todo/wishlist:_Have_a_preview_of_download_or_upload_size/comment_3_8f7e1c4a5c714cbd719ee170354d79fa._comment new file mode 100644 index 0000000000..fa9fdfb56d --- /dev/null +++ b/doc/todo/wishlist:_Have_a_preview_of_download_or_upload_size/comment_3_8f7e1c4a5c714cbd719ee170354d79fa._comment @@ -0,0 +1,12 @@ +[[!comment format=mdwn + username="http://joeyh.name/" + ip="4.154.4.193" + subject="comment 3" + date="2013-07-02T17:04:47Z" + content=""" +You can get info like that by running `git annex status .` + +This can also be used to find out how big a download is before starting it. For example, to find all files that are not present locally before running git-annex get: + +`git annex status . --not --in here` +"""]] diff --git a/doc/todo/wishlist:_Have_a_preview_of_download_or_upload_size/comment_4_c7335f757e5546aa841cab38fffe7605._comment b/doc/todo/wishlist:_Have_a_preview_of_download_or_upload_size/comment_4_c7335f757e5546aa841cab38fffe7605._comment new file mode 100644 index 0000000000..b9212a24d3 --- /dev/null +++ b/doc/todo/wishlist:_Have_a_preview_of_download_or_upload_size/comment_4_c7335f757e5546aa841cab38fffe7605._comment @@ -0,0 +1,19 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawnHRhCe3qwVKQ8_NOGGSYJnAMW6FFyKbOc" + nickname="Holger" + subject="comment 4" + date="2013-07-02T21:09:03Z" + content=""" +That's so cool, thanks! + +Do you think it'd be a major change to the repository format if the size of any directory was stored there so that this kind of status lookup becomes a constant time operation? The two most important operations are probably: + +* The total size of a directory, counting only files present here +* The total size of a directory, counting all files present at any location + +Of course, if the above two were constant time operations, you get --not here for free, too. + +To implement this, each node in the directory tree could have two additional 64 bit fields that hold the number of bytes in all files present anywhere (and this set of numbers is synchronized between all repositories), and the number of bytes in all files present here (only kept locally). This is only a small storage overhead (<16 MB if you have a million nodes) and suffices for repositories of size at most 2^64 bytes = 16 exabytes (probably more since most users will be ok with float accuracy). The numbers can be updated in logarithmic time every time a file changes. Instead of two numbers, it may not be that costly to store k numbers where k is the number of locations that a repository is connected to, since k is typically pretty small. + +The number of files can be stored in a similar way. +"""]] diff --git a/doc/todo/wishlist:_Have_a_preview_of_download_or_upload_size/comment_5_d2a845354f23d07880612740cf99ddd4._comment b/doc/todo/wishlist:_Have_a_preview_of_download_or_upload_size/comment_5_d2a845354f23d07880612740cf99ddd4._comment new file mode 100644 index 0000000000..7e160bebf0 --- /dev/null +++ b/doc/todo/wishlist:_Have_a_preview_of_download_or_upload_size/comment_5_d2a845354f23d07880612740cf99ddd4._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawnHRhCe3qwVKQ8_NOGGSYJnAMW6FFyKbOc" + nickname="Holger" + subject="comment 5" + date="2013-07-03T02:43:32Z" + content=""" +Btw, this would also provide a cheap test for whether we need to recurse into the folder in certain copy or get actions (e.g., if number of bytes present here equals number of bytes globally present, we don't need to recurse). +"""]] diff --git a/doc/todo/wishlist:_Option_to_specify_max_transfer_rate.mdwn b/doc/todo/wishlist:_Option_to_specify_max_transfer_rate.mdwn new file mode 100644 index 0000000000..3ecb421978 --- /dev/null +++ b/doc/todo/wishlist:_Option_to_specify_max_transfer_rate.mdwn @@ -0,0 +1,3 @@ +A big part of my online use is done via a low-speed connection over my mobile phone, this is limited to 16KB/sec because I always use up my 500MB quota the very first day of the month. `;-/` So when I need to download big files, I first download them to my online server, then transfer the files to my laptop with git-annex. If I'm connected via GSM, this occupies all the bandwidth and everything else moves like a heavily sedated slug. So if I want to work via VNC or SSH, I have to terminate ongoing transfers with Ctrl-C and then hopefully remember to restart it when I work locally. I know git-annex is robust enough to handle this gracefully, but it would be really nice to have a continuous connection going on in the background, limited to a value I choose. + +rsync(1) has a `--bwlimit` (bandwidth limit) where you can specify max download/upload speed in kilobytes/sec. It would be great if a similar option was integrated into git-annex. Thanks in advance. diff --git a/doc/todo/wishlist:_Option_to_specify_max_transfer_rate/comment_1_4fd870e14b5b70c8a6ade41406294387._comment b/doc/todo/wishlist:_Option_to_specify_max_transfer_rate/comment_1_4fd870e14b5b70c8a6ade41406294387._comment new file mode 100644 index 0000000000..78ca76939a --- /dev/null +++ b/doc/todo/wishlist:_Option_to_specify_max_transfer_rate/comment_1_4fd870e14b5b70c8a6ade41406294387._comment @@ -0,0 +1,10 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawmBUR4O9mofxVbpb8JV9mEbVfIYv670uJo" + nickname="Justin" + subject="trickle" + date="2013-01-22T22:44:52Z" + content=""" +not exactly integrated, but you can easily use trickle for this. + + trickle -d 50 git annex ... +"""]] diff --git a/doc/todo/wishlist:_Option_to_specify_max_transfer_rate/comment_2_dd854f297ad6a94b54be9f3edfd0f766._comment b/doc/todo/wishlist:_Option_to_specify_max_transfer_rate/comment_2_dd854f297ad6a94b54be9f3edfd0f766._comment new file mode 100644 index 0000000000..70f04c6168 --- /dev/null +++ b/doc/todo/wishlist:_Option_to_specify_max_transfer_rate/comment_2_dd854f297ad6a94b54be9f3edfd0f766._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="http://sunny256.sunbase.org/" + nickname="sunny256" + subject="Yay, trickle works" + date="2013-01-23T01:36:21Z" + content=""" +Justin, thanks a lot! trickle(1) works great. I didn't know about this program, but I'm not surprised that such a program is available. It never ceases to amaze me what's possible in a *NIX environment. +"""]] diff --git a/doc/todo/wishlist:_Option_to_specify_max_transfer_rate/comment_3_a8b7e90a473d5937807cc7eb456efe33._comment b/doc/todo/wishlist:_Option_to_specify_max_transfer_rate/comment_3_a8b7e90a473d5937807cc7eb456efe33._comment new file mode 100644 index 0000000000..a5f8f6a1bf --- /dev/null +++ b/doc/todo/wishlist:_Option_to_specify_max_transfer_rate/comment_3_a8b7e90a473d5937807cc7eb456efe33._comment @@ -0,0 +1,10 @@ +[[!comment format=mdwn + username="http://joeyh.name/" + ip="2001:4978:f:21a::2" + subject="comment 3" + date="2013-01-24T01:55:10Z" + content=""" +In addition to trickle, the git-annex man page has examples of how to make rsync use --bwlimit + +Something like trickle is needed to limit rates for remotes not using rsync, however. +"""]] diff --git a/doc/todo/wishlist:_Prevent_repeated_password_prompts_for_one_command.mdwn b/doc/todo/wishlist:_Prevent_repeated_password_prompts_for_one_command.mdwn new file mode 100644 index 0000000000..341a9afa45 --- /dev/null +++ b/doc/todo/wishlist:_Prevent_repeated_password_prompts_for_one_command.mdwn @@ -0,0 +1,45 @@ +Simple, when performing various git annex command over ssh, in particular a multi-file get, and using password authentication, git annex will prompt more than once for a user password. This makes batch updates very inconvenient. + +> I'd suggest using ssh-agent, or a passwordless ssh key. Possibly in +> combination with [[git-annex-shell]] if you want to lock down a +> particular ssh key to only being able to use git-annex and git-daemon. +> +> Combining multiple operations into a single ssh is on the todo list, but +> very far down it. --[[Joey]] + +>> OTOH, automatically running ssh in ControlMaster mode (and stopping it +>> at exit) would be useful and not hard thing for git-annex to do. +>> +>> It'd just need to set the appropriate config options, setting +>> ControlPath to a per-remote socket location that includes git-annex's +>> pid. Then at shutdown, run `ssh -O exit` on each such socket. +>> +>> Complicated slightly by not doing this if the user has already set up +>> more broad ssh connection caching. +>> +>> [[done]]! --[[Joey]] + +--- + +Slightly more elaborate design for using ssh connection caching: + +* Per-uuid ssh socket in `.git/annex/ssh/user@host.socket` +* Can be shared amoung concurrent git-annex processes as well as ssh + invocations inside the current git-annex. +* Also a lock file, `.git/annex/ssh/user@host.lock`. + Open and take shared lock before running ssh; store lock in lock pool. + (Not locking socket directly, because ssh might want to.) +* Run ssh like: `ssh -S .git/annex/ssh/user@host.socket -o ControlMaster=auto -o ControlPersist=yes user@host` +* At shutdown, enumerate all existing sockets, and on each: + 1. Drop any shared lock. + 2. Attempt to take an exclusive lock (non-blocking). + 3. `ssh -q -S .git/annex/ssh/user@host.socket -o ControlMaster=auto -o ControlPersist=yes -O stop user@host` + (Will exit nonzero if ssh is not running on that socket.) + 4. And then remove the socket and the lock file. +* Do same *at startup*. Why? In case an old git-annex was interrupted + and left behind a ssh. May have moved to a different network + in the meantime, etc, and be stalled waiting for a response from the + network, or talking to the wrong interface or something. + (Ie, the reason why I don't use ssh connection caching by default.) +* User should be able to override this, to use their own preferred + connection caching setup. `annex.sshcaching=false` diff --git a/doc/todo/wishlist:_Prevent_repeated_password_prompts_for_one_command/comment_1_3f9c0d08932c2ede61c802a91261a1f7._comment b/doc/todo/wishlist:_Prevent_repeated_password_prompts_for_one_command/comment_1_3f9c0d08932c2ede61c802a91261a1f7._comment new file mode 100644 index 0000000000..2801d8e68f --- /dev/null +++ b/doc/todo/wishlist:_Prevent_repeated_password_prompts_for_one_command/comment_1_3f9c0d08932c2ede61c802a91261a1f7._comment @@ -0,0 +1,14 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawl9sYlePmv1xK-VvjBdN-5doOa_Xw-jH4U" + nickname="Richard" + subject="comment 1" + date="2011-05-06T18:30:02Z" + content=""" +Unless you are forced to use a password, you should really be using a ssh key. + + ssh-keygen + #put local .ssh/id_?sa.pub into remote .ssh/authorized_keys (which needs to be chmod 600) + ssh-add + git annex whatever + +"""]] diff --git a/doc/todo/wishlist:_Provide_a___34__git_annex__34___command_that_will_skip_duplicates.mdwn b/doc/todo/wishlist:_Provide_a___34__git_annex__34___command_that_will_skip_duplicates.mdwn new file mode 100644 index 0000000000..9336535788 --- /dev/null +++ b/doc/todo/wishlist:_Provide_a___34__git_annex__34___command_that_will_skip_duplicates.mdwn @@ -0,0 +1,28 @@ +(Hi, this is paulproteus@debian, AKA Asheesh). + +I've been enjoying using git-annex to archive my data. + +It's great that, by using git-annex and the SHA1 backend, I get a space-saving kind of deduplication through the symbolic links. + +I'm looking for the ability to filter files, before they get added to the annex, so that I don't add new files whose content is already in the annex.look That would help me in terms of personal file organization. + +It seems there is not, so this is a wishlist bug filed so that maybe such a thing might exist. What I would really like to do is: + +* $ git annex add --no-add-if-already-present . +* $ git commit -m "Slurping in some photos I found on my old laptop hard drive" + +And then I'd do something like: + +* $ git clean -f + +to remove the files that didn't get annexed in this run. That way, only one filename would ever point to a particular SHA1. + +I want this because I have copies of various of mine (photos, in particular) scattered across various hard disks. If this feature existed, I could comfortably toss them all into one git annex that grew, bit by bit, to store all of these files exactly once. + +(I would be even happier for "git annex add --unlink-duplicates .") + +(Another way to do this would be to "git annex add" them all, and then use a "git annex remove-duplicates" that could prompt me about which files are duplicates of each other, and then I could pipe that command's output into xargs git rm.) + +(As I write this, I realize it's possible to parse the destination of the symlink in a way that does this..) + +> [[done]]; see [[tips/finding_duplicate_files]] --[[Joey]] diff --git a/doc/todo/wishlist:_Provide_a___34__git_annex__34___command_that_will_skip_duplicates/comment_10_d78d79fb2f3713aa69f45d2691cf8dfe._comment b/doc/todo/wishlist:_Provide_a___34__git_annex__34___command_that_will_skip_duplicates/comment_10_d78d79fb2f3713aa69f45d2691cf8dfe._comment new file mode 100644 index 0000000000..5dbb66cf66 --- /dev/null +++ b/doc/todo/wishlist:_Provide_a___34__git_annex__34___command_that_will_skip_duplicates/comment_10_d78d79fb2f3713aa69f45d2691cf8dfe._comment @@ -0,0 +1,68 @@ +[[!comment format=mdwn + username="http://adamspiers.myopenid.com/" + nickname="Adam" + subject="comment 10" + date="2011-12-23T17:22:11Z" + content=""" +> Your perl script is not O(n). Inserting into perl hash tables has +> overhead of minimum O(n log n). + +What's your source for this assertion? I would expect an amortized +average of `O(1)` per insertion, i.e. `O(n)` for full population. + +> Not counting the overhead of resizing hash tables, +> the grevious slowdown if the bucket size is overcome by data (it +> probably falls back to a linked list or something then), and the +> overhead of traversing the hash tables to get data out. + +None of which necessarily change the algorithmic complexity. However +real benchmarks are far more useful here than complexity analysis, and +[the dangers of premature optimization](http://c2.com/cgi/wiki?PrematureOptimization) +should not be forgotten. + +> Your memory size calculations ignore the overhead of a hash table or +> other data structure to store the data in, which will tend to be +> more than the actual data size it's storing. I estimate your 50 +> million number is off by at least one order of magnitude, and more +> likely two; + +Sure, I was aware of that, but my point still stands. Even 500k keys +per 1GB of RAM does not sound expensive to me. + +> in any case I don't want git-annex to use 1 gb of ram. + +Why not? What's the maximum it should use? 512MB? 256MB? +32MB? I don't see the sense in the author of a program +dictating thresholds which are entirely dependent on the context +in which the program is *run*, not the context in which it's *written*. +That's why systems have files such as `/etc/security/limits.conf`. + +You said you want git-annex to scale to enormous repositories. If you +impose an arbitrary memory restriction such as the above, that means +avoiding implementing *any* kind of functionality which requires `O(n)` +memory or worse. Isn't it reasonable to assume that many users use +git-annex on repositories which are *not* enormous? Even when they do +work with enormous repositories, just like with any other program, +they would naturally expect certain operations to take longer or +become impractical without sufficient RAM. That's why I say that this +restriction amounts to throwing out the baby with the bathwater. +It just means that those who need the functionality would have to +reimplement it themselves, assuming they are able, which is likely +to result in more wheel reinventions. I've already shared +[my implementation](https://github.com/aspiers/git-config/blob/master/bin/git-annex-finddups) +but how many people are likely to find it, let alone get it working? + +> Little known fact: sort(1) will use a temp file as a buffer if too +> much memory is needed to hold the data to sort. + +Interesting. Presumably you are referring to some undocumented +behaviour, rather than `--batch-size` which only applies when merging +multiple files, and not when only sorting STDIN. + +> It's also written in the most efficient language possible and has +> been ruthlessly optimised for 30 years, so I would be very surprised +> if it was not the best choice. + +It's the best choice for sorting. But sorting purely to detect +duplicates is a dismally bad choice. +"""]] diff --git a/doc/todo/wishlist:_Provide_a___34__git_annex__34___command_that_will_skip_duplicates/comment_11_4316d9d74312112dc4c823077af7febe._comment b/doc/todo/wishlist:_Provide_a___34__git_annex__34___command_that_will_skip_duplicates/comment_11_4316d9d74312112dc4c823077af7febe._comment new file mode 100644 index 0000000000..286487eee5 --- /dev/null +++ b/doc/todo/wishlist:_Provide_a___34__git_annex__34___command_that_will_skip_duplicates/comment_11_4316d9d74312112dc4c823077af7febe._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="http://joey.kitenet.net/" + nickname="joey" + subject="comment 11" + date="2011-12-23T17:52:21Z" + content=""" +I don't think that [[tips/finding_duplicate_files]] is hard to find, and the multiple different ways it shows to deal with the duplicate files shows the flexability of the unix pipeline approach. +"""]] diff --git a/doc/todo/wishlist:_Provide_a___34__git_annex__34___command_that_will_skip_duplicates/comment_12_ed6d07f16a11c6eee7e3d5005e8e6fa3._comment b/doc/todo/wishlist:_Provide_a___34__git_annex__34___command_that_will_skip_duplicates/comment_12_ed6d07f16a11c6eee7e3d5005e8e6fa3._comment new file mode 100644 index 0000000000..909beed837 --- /dev/null +++ b/doc/todo/wishlist:_Provide_a___34__git_annex__34___command_that_will_skip_duplicates/comment_12_ed6d07f16a11c6eee7e3d5005e8e6fa3._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="http://joey.kitenet.net/" + nickname="joey" + subject="comment 12" + date="2011-12-23T18:02:24Z" + content=""" +BTW, sort -S '90%' benchmarks consistently 2x as fast as perl's hashes all the way up to 1 million files. Of course the pipeline approach allows you to swap in perl or whatever else is best for you at scale. +"""]] diff --git a/doc/todo/wishlist:_Provide_a___34__git_annex__34___command_that_will_skip_duplicates/comment_1_fd213310ee548d8726791d2b02237fde._comment b/doc/todo/wishlist:_Provide_a___34__git_annex__34___command_that_will_skip_duplicates/comment_1_fd213310ee548d8726791d2b02237fde._comment new file mode 100644 index 0000000000..094e4526eb --- /dev/null +++ b/doc/todo/wishlist:_Provide_a___34__git_annex__34___command_that_will_skip_duplicates/comment_1_fd213310ee548d8726791d2b02237fde._comment @@ -0,0 +1,29 @@ +[[!comment format=mdwn + username="http://joey.kitenet.net/" + nickname="joey" + subject="comment 1" + date="2011-01-27T18:29:44Z" + content=""" +Hey Asheesh, I'm happy you're finding git-annex useful. + +So, there are two forms of duplication going on here. There's duplication of the content, and duplication of the filenames +pointing at that content. + +Duplication of the filenames is probably not a concern, although it's what I thought you were talking about at first. It's probably info worth recording that backup-2010/some_dir/foo and backup-2009/other_dir/foo are two names you've used for the same content in the past. If you really wanted to remove backup-2009/foo, you could do it by writing a script that looks at the basenames of the symlink targets and removes files that point to the same content as other files. + +Using SHA1 ensures that the same key is used for identical files, so generally avoids duplication of content. But if you have 2 disks with an identical file on each, and make them both into annexes, then git-annex will happily retain both +copies of the content, one per disk. It generally considers keeping copies of content a good thing. :) + +So, what if you want to remove the unnecessary copies? Well, there's a really simple way: + +
+cd /media/usb-1
+git remote add other-disk /media/usb-0
+git annex add
+git annex drop
+
+ +This asks git-annex to add everything to the annex, but then remove any file contents that it can safely remove. What can it safely remove? Well, anything that it can verify is on another repository such as \"other-disk\"! So, this will happily drop any duplicated file contents, while leaving all the rest alone. + +In practice, you might not want to have all your old backup disks mounted at the same time and configured as remotes. Look into configuring [[trust]] to avoid needing do to that. If usb-0 is already a trusted disk, all you need is a simple \"git annex drop\" on usb-1. +"""]] diff --git a/doc/todo/wishlist:_Provide_a___34__git_annex__34___command_that_will_skip_duplicates/comment_2_4394bde1c6fd44acae649baffe802775._comment b/doc/todo/wishlist:_Provide_a___34__git_annex__34___command_that_will_skip_duplicates/comment_2_4394bde1c6fd44acae649baffe802775._comment new file mode 100644 index 0000000000..04d58a4598 --- /dev/null +++ b/doc/todo/wishlist:_Provide_a___34__git_annex__34___command_that_will_skip_duplicates/comment_2_4394bde1c6fd44acae649baffe802775._comment @@ -0,0 +1,18 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawkjvjLHW9Omza7x1VEzIFQ8Z5honhRB90I" + nickname="Asheesh" + subject="I actually *do* want to avoid duplication of filenames" + date="2011-01-28T07:30:05Z" + content=""" +I really do want just one filename per file, at least for some cases. + +For my photos, there's no benefit to having a few filenames point to the same file. As I'm putting them all into the git-annex, that is a good time to remove the pure duplicates so that I don't e.g. see them twice when browsing the directory as a gallery. Also, I am uploading my photos to the web, and I want to avoid uploading the same photo (by content) twice. + +I hope that makes things clearer! + +For now I'm just doing this: + +* paulproteus@renaissance:/mnt/backups-terabyte/paulproteus/sd-card-from-2011-01-06/sd-cards/DCIM/100CANON $ for file in *; do hash=$(sha1sum \"$file\"); if ls /home/paulproteus/Photos/in-flickr/.git-annex | grep -q \"$hash\"; then echo already annexed ; else flickr_upload \"$file\" && mv \"$file\" \"/home/paulproteus/Photos/in-flickr/2011-01-28/from-some-nested-sd-card-bk\" && (cd /home/paulproteus/Photos/in-flickr/2011-01-28/from-some-nested-sd-card-bk && git annex add . && git commit -m ...) ; fi; done + +(Yeah, Flickr for my photos for now. I feel sad about betraying the principle of autonomo.us-ness.) +"""]] diff --git a/doc/todo/wishlist:_Provide_a___34__git_annex__34___command_that_will_skip_duplicates/comment_3_076cb22057583957d5179d8ba9004605._comment b/doc/todo/wishlist:_Provide_a___34__git_annex__34___command_that_will_skip_duplicates/comment_3_076cb22057583957d5179d8ba9004605._comment new file mode 100644 index 0000000000..d11119bc3d --- /dev/null +++ b/doc/todo/wishlist:_Provide_a___34__git_annex__34___command_that_will_skip_duplicates/comment_3_076cb22057583957d5179d8ba9004605._comment @@ -0,0 +1,18 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawkjvjLHW9Omza7x1VEzIFQ8Z5honhRB90I" + nickname="Asheesh" + subject="Duplication of the filenames is what I am concerned about" + date="2011-04-29T11:48:22Z" + content=""" +For what it's worth, yes, I want to actually forget I ever had the same file in the filesystem with a duplicated name. I'm not just aiming to clean up the disk's space usage; I'm also aiming to clean things up so that navigating the filesystem is easier. + +I can write my own script to do that based on the symlinks' target (and I wrote something along those lines), but I still think it'd be nicer if git-annex supported this use case. + +Perhaps: + +
git annex drop --by-contents
+ +could let me remove a file from git-annex if the contents are available through a different name. (Right now, \"git annex drop\" requires the name *and* contents match.) + +-- Asheesh. +"""]] diff --git a/doc/todo/wishlist:_Provide_a___34__git_annex__34___command_that_will_skip_duplicates/comment_4_f120d1e83c1a447f2ecce302fc69cf74._comment b/doc/todo/wishlist:_Provide_a___34__git_annex__34___command_that_will_skip_duplicates/comment_4_f120d1e83c1a447f2ecce302fc69cf74._comment new file mode 100644 index 0000000000..a218ee3d51 --- /dev/null +++ b/doc/todo/wishlist:_Provide_a___34__git_annex__34___command_that_will_skip_duplicates/comment_4_f120d1e83c1a447f2ecce302fc69cf74._comment @@ -0,0 +1,35 @@ +[[!comment format=mdwn + username="http://adamspiers.myopenid.com/" + nickname="Adam" + subject="List the duplicate filenames, then let the user decide what to do" + date="2011-12-22T12:31:29Z" + content=""" +I have the same use case as Asheesh but I want to be able to see which filenames point to the same objects and then decide which of the duplicates to drop myself. I think + + git annex drop --by-contents + +would be the wrong approach because how does git-annex know which ones to drop? There's too much potential for error. + +Instead it would be great to have something like + + git annex finddups + +While it's easy enough to knock up a bit of shell or Perl to achieve this, that relies on knowledge of the annex symlink structure, so I think really it belongs inside git-annex. + +If this command gave output similar to the excellent `fastdup` utility: + + Scanning for files... 672 files in 10.439 seconds + Comparing 2 sets of files... + + 2 files (70.71 MB/ea) + /home/adam/media/flat/tour/flat-tour.3gp + /home/adam/videos/tour.3gp + + Found 1 duplicate of 1 file (70.71 MB wasted) + Scanned 672 files (1.96 GB) in 11.415 seconds + +then you could do stuff like + + git annex finddups | grep /home/adam/media/flat | xargs rm + +"""]] diff --git a/doc/todo/wishlist:_Provide_a___34__git_annex__34___command_that_will_skip_duplicates/comment_5_5c30294b3c59fdebb1eef0ae5da4cd4f._comment b/doc/todo/wishlist:_Provide_a___34__git_annex__34___command_that_will_skip_duplicates/comment_5_5c30294b3c59fdebb1eef0ae5da4cd4f._comment new file mode 100644 index 0000000000..e48a4a9b38 --- /dev/null +++ b/doc/todo/wishlist:_Provide_a___34__git_annex__34___command_that_will_skip_duplicates/comment_5_5c30294b3c59fdebb1eef0ae5da4cd4f._comment @@ -0,0 +1,10 @@ +[[!comment format=mdwn + username="http://adamspiers.myopenid.com/" + nickname="Adam" + subject="Here's a Perl version" + date="2011-12-22T15:43:51Z" + content=""" +https://github.com/aspiers/git-config/blob/master/bin/git-annex-finddups + +but it would be better in git-annex itself ... +"""]] diff --git a/doc/todo/wishlist:_Provide_a___34__git_annex__34___command_that_will_skip_duplicates/comment_6_f24541ada1c86d755acba7e9fa7cff24._comment b/doc/todo/wishlist:_Provide_a___34__git_annex__34___command_that_will_skip_duplicates/comment_6_f24541ada1c86d755acba7e9fa7cff24._comment new file mode 100644 index 0000000000..5d8ac8e61b --- /dev/null +++ b/doc/todo/wishlist:_Provide_a___34__git_annex__34___command_that_will_skip_duplicates/comment_6_f24541ada1c86d755acba7e9fa7cff24._comment @@ -0,0 +1,16 @@ +[[!comment format=mdwn + username="http://joey.kitenet.net/" + nickname="joey" + subject="comment 6" + date="2011-12-22T16:39:24Z" + content=""" +My main concern with putting this in git-annex is that finding duplicates necessarily involves storing a list of every key and file in the repository, and git-annex is very carefully built to avoid things that require non-constant memory use, so that it can scale to very big repositories. (The only exception is the `unused` command, and reducing its memory usage is a continuing goal.) + +So I would rather come at this from a different angle.. like providing a way to output a list of files and their associated keys, which the user can then use in their own shell pipelines to find duplicate keys: + + git annex find --include '*' --format='${file} ${key}\n' | sort --key 2 | uniq --all-repeated --skip-fields=1 + +Which is implemented now! + +(Making that pipeline properly handle filenames with spaces is left as an exercise for the reader..) +"""]] diff --git a/doc/todo/wishlist:_Provide_a___34__git_annex__34___command_that_will_skip_duplicates/comment_7_c39f1bb7c61a89b238c61bee1c049767._comment b/doc/todo/wishlist:_Provide_a___34__git_annex__34___command_that_will_skip_duplicates/comment_7_c39f1bb7c61a89b238c61bee1c049767._comment new file mode 100644 index 0000000000..a337002804 --- /dev/null +++ b/doc/todo/wishlist:_Provide_a___34__git_annex__34___command_that_will_skip_duplicates/comment_7_c39f1bb7c61a89b238c61bee1c049767._comment @@ -0,0 +1,54 @@ +[[!comment format=mdwn + username="http://adamspiers.myopenid.com/" + nickname="Adam" + subject="comment 7" + date="2011-12-22T20:04:14Z" + content=""" +> My main concern with putting this in git-annex is that finding +> duplicates necessarily involves storing a list of every key and file +> in the repository + +Only if you want to search the *whole* repository for duplicates, and if +you do, then you're necessarily going to have to chew up memory in +some process anyway, so what difference whether it's git-annex or +(say) a Perl wrapper? + +> and git-annex is very carefully built to avoid things that require +> non-constant memory use, so that it can scale to very big +> repositories. + +That's a worthy goal, but if everything could be implemented with an +O(1) memory footprint then we'd be in much more pleasant world :-) +Even O(n) isn't that bad ... + +That aside, I like your `--format=\"%f %k\n\"` idea a lot. That opens +up the \"black box\" of `.git/annex/objects` and makes nice things +possible, as your pipeline already demonstrates. However, I'm not +sure why you think `git annex find | sort | uniq` would be more +efficient. Not only does the sort require the very thing you were +trying to avoid (i.e. the whole list in memory), but it's also +O(n log n) which is significantly slower than my O(n) Perl script +linked above. + +More considerations about this pipeline: + +* Doesn't it only include locally available files? Ideally it should + spot duplicates even when the backing blob is not available locally. +* What's the point of `--include '*'` ? Doesn't `git annex find` + with no arguments already include all files, modulo the requirement + above that they're locally available? +* Any user using this `git annex find | ...` approach is likely to + run up against its limitations sooner rather than later, because + they're already used to the plethora of options `find(1)` provides. + Rather than reinventing the wheel, is there some way `git annex find` + could harness the power of `find(1)` ? + +Those considerations aside, a combined approach would be to implement + + git annex find --format=... + +and then alter my Perl wrapper to `popen(2)` from that rather than using +`File::Find`. But I doubt you would want to ship Perl wrappers in the +distribution, so if you don't provide a Haskell equivalent then users +who can't code are left high and dry. +"""]] diff --git a/doc/todo/wishlist:_Provide_a___34__git_annex__34___command_that_will_skip_duplicates/comment_8_221ed2e53420278072a6d879c6f251d1._comment b/doc/todo/wishlist:_Provide_a___34__git_annex__34___command_that_will_skip_duplicates/comment_8_221ed2e53420278072a6d879c6f251d1._comment new file mode 100644 index 0000000000..5ac292afeb --- /dev/null +++ b/doc/todo/wishlist:_Provide_a___34__git_annex__34___command_that_will_skip_duplicates/comment_8_221ed2e53420278072a6d879c6f251d1._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="http://adamspiers.myopenid.com/" + nickname="Adam" + subject="How much memory would it actually use anyway?" + date="2011-12-22T20:15:22Z" + content=""" +Another thought - an SHA1 digest is 20 bytes. That means you can fit over 50 million keys into 1GB of RAM. Granted you also need memory to store the values (pathnames) which in many cases will be longer, and some users may also choose more expensive backends than SHA1 ... but even so, it seems to me that you are at risk of throwing the baby out with the bath water. +"""]] diff --git a/doc/todo/wishlist:_Provide_a___34__git_annex__34___command_that_will_skip_duplicates/comment_9_aecfa896c97b9448f235bce18a40621d._comment b/doc/todo/wishlist:_Provide_a___34__git_annex__34___command_that_will_skip_duplicates/comment_9_aecfa896c97b9448f235bce18a40621d._comment new file mode 100644 index 0000000000..82c6921ebb --- /dev/null +++ b/doc/todo/wishlist:_Provide_a___34__git_annex__34___command_that_will_skip_duplicates/comment_9_aecfa896c97b9448f235bce18a40621d._comment @@ -0,0 +1,14 @@ +[[!comment format=mdwn + username="http://joey.kitenet.net/" + nickname="joey" + subject="comment 9" + date="2011-12-23T16:07:39Z" + content=""" +Adam, to answer a lot of points breifly.. + +* --include='*' makes find list files whether their contents are present or not +* Your perl script is not O(n). Inserting into perl hash tables has overhead of minimum O(n log n). Not counting the overhead of resizing hash tables, the grevious slowdown if the bucket size is overcome by data (it probably falls back to a linked list or something then), and the overhead of traversing the hash tables to get data out. +* I think that git-annex's set of file matching options is coming along nicely, and new ones can easily be added, so see no need to pull in unix find(1). +* Your memory size calculations ignore the overhead of a hash table or other data structure to store the data in, which will tend to be more than the actual data size it's storing. I estimate your 50 million number is off by at least one order of magnitude, and more likely two; in any case I don't want git-annex to use 1 gb of ram. +* Little known fact: sort(1) will use a temp file as a buffer if too much memory is needed to hold the data to sort. It's also written in the most efficient language possible and has been ruthlessly optimised for 30 years, so I would be very surprised if it was not the best choice. +"""]] diff --git a/doc/todo/wishlist:_Restore_s3_files_moved_to_Glacier.mdwn b/doc/todo/wishlist:_Restore_s3_files_moved_to_Glacier.mdwn new file mode 100644 index 0000000000..85fc2785c4 --- /dev/null +++ b/doc/todo/wishlist:_Restore_s3_files_moved_to_Glacier.mdwn @@ -0,0 +1,7 @@ +I would like to use the automated AWS lifecycle rules to move the git annex files store on S3 to Glacier after a bit of time. Git annex need must support this kind of S3 files explicitly in order for it to work. + +This is different from the adding a Glacier remote to git annex because of the reasons explained in . + +Basically, the files moved by AWS from S3 to Glacier are not available under the normal Glacier API. In fact, the moved S3 files are listed as available but under the `GLACIER` storage class and need a RESTORE request before they can be GET like other S3 files. Trying to GET an S3 file that has been moved to Glacier will not restore it from Glacier and will result in an 403 error. + +I suppose DELETE needs special care as well. diff --git a/doc/todo/wishlist:_Tell_git_annex___40__assistant__41___which_files___40__not__41___to_annex_via_.gitattributes.mdwn b/doc/todo/wishlist:_Tell_git_annex___40__assistant__41___which_files___40__not__41___to_annex_via_.gitattributes.mdwn new file mode 100644 index 0000000000..f65a95f45a --- /dev/null +++ b/doc/todo/wishlist:_Tell_git_annex___40__assistant__41___which_files___40__not__41___to_annex_via_.gitattributes.mdwn @@ -0,0 +1,9 @@ +Title says it all. + +It would be nice if I could tell git annex (assistant) which files (not) to annex (automatically). + +[[!tag /design/assistant]] + +> [[done]]; use `annex.largefiles` git config to configure criteria for +> which files should be annexed. The rest will be added to git as normal +> files. --[[Joey]] diff --git a/doc/todo/wishlist:___34__git_annex_add__34___multiple_processes.mdwn b/doc/todo/wishlist:___34__git_annex_add__34___multiple_processes.mdwn new file mode 100644 index 0000000000..a04af05b42 --- /dev/null +++ b/doc/todo/wishlist:___34__git_annex_add__34___multiple_processes.mdwn @@ -0,0 +1,10 @@ +Hello, + +i'm in the process of managing my music collection with git-annex. The initial "git annex add" using the sha1 banckend is quite long an i was wondering that it could be nice to launch multiple "sha1sum" processes in parallel to speed up things. + +Anyway, thanks for this wonderful piece of software. + +Jean-Baptiste + +> closing as dup of [[parallel possibilities]] (also see comments below) +> [[done]] --[[Joey]] diff --git a/doc/todo/wishlist:___34__git_annex_add__34___multiple_processes/comment_1_85b14478411a33e6186a64bd41f0910d._comment b/doc/todo/wishlist:___34__git_annex_add__34___multiple_processes/comment_1_85b14478411a33e6186a64bd41f0910d._comment new file mode 100644 index 0000000000..2364b7fb83 --- /dev/null +++ b/doc/todo/wishlist:___34__git_annex_add__34___multiple_processes/comment_1_85b14478411a33e6186a64bd41f0910d._comment @@ -0,0 +1,10 @@ +[[!comment format=mdwn + username="http://joey.kitenet.net/" + nickname="joey" + subject="comment 1" + date="2011-02-25T19:12:42Z" + content=""" +I'd expect the checksumming to be disk bound, not CPU bound, on most systems. + +I suggest you start off on the WORM backend, and then you can run a job later to [[migrate|walkthrough#index14h2]] to the SHA1 backend. +"""]] diff --git a/doc/todo/wishlist:___34__git_annex_add__34___multiple_processes/comment_2_82e857f463cfdf73c70f6c0a9f9a31d6._comment b/doc/todo/wishlist:___34__git_annex_add__34___multiple_processes/comment_2_82e857f463cfdf73c70f6c0a9f9a31d6._comment new file mode 100644 index 0000000000..9b8240658b --- /dev/null +++ b/doc/todo/wishlist:___34__git_annex_add__34___multiple_processes/comment_2_82e857f463cfdf73c70f6c0a9f9a31d6._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="http://joey.kitenet.net/" + nickname="joey" + subject="comment 2" + date="2011-02-25T19:54:28Z" + content=""" +But, see [[todo/parallel_possibilities]] +"""]] diff --git a/doc/todo/wishlist:___34__git_annex_add__34___multiple_processes/comment_3_8af85eba7472d9025c6fae4f03e3ad75._comment b/doc/todo/wishlist:___34__git_annex_add__34___multiple_processes/comment_3_8af85eba7472d9025c6fae4f03e3ad75._comment new file mode 100644 index 0000000000..ee769f0ddd --- /dev/null +++ b/doc/todo/wishlist:___34__git_annex_add__34___multiple_processes/comment_3_8af85eba7472d9025c6fae4f03e3ad75._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="jbd" + ip="89.158.228.148" + subject="comment 3" + date="2011-02-26T10:26:12Z" + content=""" +Thank your for your answer and the link ! +"""]] diff --git a/doc/todo/wishlist:___34__quiet__34___annex_get_for_centralized_use_case.mdwn b/doc/todo/wishlist:___34__quiet__34___annex_get_for_centralized_use_case.mdwn new file mode 100644 index 0000000000..27f4744c8d --- /dev/null +++ b/doc/todo/wishlist:___34__quiet__34___annex_get_for_centralized_use_case.mdwn @@ -0,0 +1,9 @@ +We're using git-annex to manage large files as part of a team. + +We have a central repository of large files that everyone grabs from. + +We would like to be able to get the files without updating the `git-annex` branch. This way it doesn't pollute the history with essentially always unreachable locations. + +I think the easiest way would be to just add an option to not update the `git-annex` branch when `annex get` is executed. + +Thoughts? diff --git a/doc/todo/wishlist:___34__quiet__34___annex_get_for_centralized_use_case/comment_1_5c8812973cf91b046e7fc44d7e86c78e._comment b/doc/todo/wishlist:___34__quiet__34___annex_get_for_centralized_use_case/comment_1_5c8812973cf91b046e7fc44d7e86c78e._comment new file mode 100644 index 0000000000..61d82e2aef --- /dev/null +++ b/doc/todo/wishlist:___34__quiet__34___annex_get_for_centralized_use_case/comment_1_5c8812973cf91b046e7fc44d7e86c78e._comment @@ -0,0 +1,14 @@ +[[!comment format=mdwn + username="http://joeyh.name/" + ip="4.154.6.48" + subject="comment 1" + date="2013-07-17T23:23:50Z" + content=""" +I'm interested to hear that your team is using git-annex. + +Have you tried `git config annex.alwayscommit false`? This will avoid committing, and just store the info in a local journal -- so even `git annex fsck` will still work. + +Hmm, perhaps you want to update the branch when running `git annex copy` to put files onto the server, but not when getting them? A switch to disable updating the branch would then make sense. Any use of fsck would notice the inconsistency though, and commit a fix to the git-annex branch -- unless you also used the new switch when running fsck. + +But what happens if someone makes a change, pushes to the server, but forgets to `git annex copy` the file? Everyone would then be left with a missing file that git annex doesn't know where it is. One of the reasons it tracks the locations is so that if necessary you know which repository such a misplaced fail is stored in. And can go track down that person's laptop and apply a cluebat. ;) Do you do something on the server to prevent this scenario? +"""]] diff --git a/doc/todo/wishlist:___34__quiet__34___annex_get_for_centralized_use_case/comment_2_f36b6a5b128423211aac91a252ecf85f._comment b/doc/todo/wishlist:___34__quiet__34___annex_get_for_centralized_use_case/comment_2_f36b6a5b128423211aac91a252ecf85f._comment new file mode 100644 index 0000000000..3379742d22 --- /dev/null +++ b/doc/todo/wishlist:___34__quiet__34___annex_get_for_centralized_use_case/comment_2_f36b6a5b128423211aac91a252ecf85f._comment @@ -0,0 +1,18 @@ +[[!comment format=mdwn + username="http://caust1c.myopenid.com/" + nickname="asbraithwaite" + subject="comment 2" + date="2013-07-17T23:42:25Z" + content=""" +What would happen if we wanted to copy a file to the server +with that set? or even when running `git annex add` +You understood me exactly though. +We'd like to be able to get the files without a commit, but copy them +with a commit of the changes. +The way we operate, if somebody makes a change with regards to +largefiles, they should also add a test for it. +Ideally, the test suite would catch it. That or people would +realize that theres a new big file and send around an email to +see who was trying to a largefile when they couldn't reach it. + +"""]] diff --git a/doc/todo/wishlist:___34__quiet__34___annex_get_for_centralized_use_case/comment_3_ad1569b2405acacd2e37f42b82f24c88._comment b/doc/todo/wishlist:___34__quiet__34___annex_get_for_centralized_use_case/comment_3_ad1569b2405acacd2e37f42b82f24c88._comment new file mode 100644 index 0000000000..14ab5f65ab --- /dev/null +++ b/doc/todo/wishlist:___34__quiet__34___annex_get_for_centralized_use_case/comment_3_ad1569b2405acacd2e37f42b82f24c88._comment @@ -0,0 +1,10 @@ +[[!comment format=mdwn + username="http://joeyh.name/" + ip="4.154.6.48" + subject="comment 3" + date="2013-07-18T00:12:39Z" + content=""" +Actually, when a file is sent to a git repository on the server, it will update the location log on that side. So even if the client has alwayscommit=false, other clients will learn the file has reached the server. + +So that might just work for you. +"""]] diff --git a/doc/todo/wishlist:___34__quiet__34___annex_get_for_centralized_use_case/comment_4_8aba90150fe178ce9712ad951628f3d6._comment b/doc/todo/wishlist:___34__quiet__34___annex_get_for_centralized_use_case/comment_4_8aba90150fe178ce9712ad951628f3d6._comment new file mode 100644 index 0000000000..c1f148fdbe --- /dev/null +++ b/doc/todo/wishlist:___34__quiet__34___annex_get_for_centralized_use_case/comment_4_8aba90150fe178ce9712ad951628f3d6._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawln3ckqKx0x_xDZMYwa9Q1bn4I06oWjkog" + nickname="Michael" + subject="comment 4" + date="2013-07-18T18:03:00Z" + content=""" +If you can have developers only add large files on the central server, avoiding using git annex sync and only pulling from git repo should work, I think. +"""]] diff --git a/doc/todo/wishlist:___34__quiet__34___annex_get_for_centralized_use_case/comment_5_6f42d240e0021f4dfa37146bea3f5d7e._comment b/doc/todo/wishlist:___34__quiet__34___annex_get_for_centralized_use_case/comment_5_6f42d240e0021f4dfa37146bea3f5d7e._comment new file mode 100644 index 0000000000..aa547d7904 --- /dev/null +++ b/doc/todo/wishlist:___34__quiet__34___annex_get_for_centralized_use_case/comment_5_6f42d240e0021f4dfa37146bea3f5d7e._comment @@ -0,0 +1,16 @@ +[[!comment format=mdwn + username="http://caust1c.myopenid.com/" + nickname="asbraithwaite" + subject="comment 5" + date="2013-07-18T21:17:45Z" + content=""" +Wow. This worked better than expected. I'm still trying to understand the implementation (I'm not familiar with Haskell), but there is some magic going on. + +To explain: + +I didn't expect that when I added a file (Locally and to the annex remote) that when somebody else did a pull, git-annex would recognize this and update the working copy to download and include that file. + +I'm not sure if this is intended behavior or not (since I thought all commands had to go through git-annex) but I like it nonetheless. + +Thanks again for the tips! +"""]] diff --git a/doc/todo/wishlist:___34__quiet__34___annex_get_for_centralized_use_case/comment_6_5fda455febf728b079f26fe42bf7bcab._comment b/doc/todo/wishlist:___34__quiet__34___annex_get_for_centralized_use_case/comment_6_5fda455febf728b079f26fe42bf7bcab._comment new file mode 100644 index 0000000000..3f10e48ad1 --- /dev/null +++ b/doc/todo/wishlist:___34__quiet__34___annex_get_for_centralized_use_case/comment_6_5fda455febf728b079f26fe42bf7bcab._comment @@ -0,0 +1,16 @@ +[[!comment format=mdwn + username="http://caust1c.myopenid.com/" + nickname="asbraithwaite" + subject="comment 6" + date="2013-07-18T21:23:58Z" + content=""" +Disregard that. I was testing it with a particular file that didn't exactly play nice. + +Basically, to test this before production I used `dd bs=1024 count=100000 if=/dev/zero of=bigfile`. + +When I did a `git pull`, it showed the symlink as being valid. + +When I changed the command to `dd bs=1024 count=100000 if=/dev/urandom of=bigfile` then it showed the symlink as bad after pulling. + +Weird! +"""]] diff --git a/doc/todo/wishlist:___34__quiet__34___annex_get_for_centralized_use_case/comment_7_f1052ab997f1a2cccbabfd1533fc0a59._comment b/doc/todo/wishlist:___34__quiet__34___annex_get_for_centralized_use_case/comment_7_f1052ab997f1a2cccbabfd1533fc0a59._comment new file mode 100644 index 0000000000..deb25a9ce7 --- /dev/null +++ b/doc/todo/wishlist:___34__quiet__34___annex_get_for_centralized_use_case/comment_7_f1052ab997f1a2cccbabfd1533fc0a59._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawln3ckqKx0x_xDZMYwa9Q1bn4I06oWjkog" + nickname="Michael" + subject="comment 7" + date="2013-07-18T21:48:06Z" + content=""" +If you wanted to auto-get files on git pull, you could trying putting git annex get into .git/hooks/post-merge (needs to be marked as executable). +"""]] diff --git a/doc/todo/wishlist:___34__quiet__34___annex_get_for_centralized_use_case/comment_8_07804647b6023436878756bd97a23f32._comment b/doc/todo/wishlist:___34__quiet__34___annex_get_for_centralized_use_case/comment_8_07804647b6023436878756bd97a23f32._comment new file mode 100644 index 0000000000..4901a2d97c --- /dev/null +++ b/doc/todo/wishlist:___34__quiet__34___annex_get_for_centralized_use_case/comment_8_07804647b6023436878756bd97a23f32._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="http://joeyh.name/" + ip="4.154.0.140" + subject="comment 8" + date="2013-07-20T20:07:53Z" + content=""" +`dd bs=1024 count=100000 if=/dev/zero of=bigfile` obviously creates the same data each time, so if that same size empty file has previously been in a repository, and the content is still present there, the symlink will automatically point to it when it gets added. In other words, git-annex performs automatic deduplication of file contents, and so it was able to save you a download in this case. +"""]] diff --git a/doc/todo/wishlist:___39__whereis__39___support_in_the_webapp.mdwn b/doc/todo/wishlist:___39__whereis__39___support_in_the_webapp.mdwn new file mode 100644 index 0000000000..c074988b08 --- /dev/null +++ b/doc/todo/wishlist:___39__whereis__39___support_in_the_webapp.mdwn @@ -0,0 +1,4 @@ +I've looked for this feature in the webapp but can't find it... + +I mainly use the webapp and have been wondering 'how many copies of file X are there' and 'where are the copies of file Y'? +This is available in the commandline interface but it would be nice to have this in the webapp too. diff --git a/doc/todo/wishlist:___96__git_annex_fsck_--checksums__96___--_verify_checksums_but_disregard_annex.numcopies.mdwn b/doc/todo/wishlist:___96__git_annex_fsck_--checksums__96___--_verify_checksums_but_disregard_annex.numcopies.mdwn new file mode 100644 index 0000000000..67a7e13e1c --- /dev/null +++ b/doc/todo/wishlist:___96__git_annex_fsck_--checksums__96___--_verify_checksums_but_disregard_annex.numcopies.mdwn @@ -0,0 +1,12 @@ +As the title says, I would like to see an option where git-annex verifies +that all checksums are OK but not that the required number of copies or +other possible metrics are fulfilled. + +-- RichiH + +> --numcopies is provided for times when you want to temporarily override +> annex.numcopies. So, `git annex fsck --numcopies=0` +> +> I don't see any reason to want to disable the other checks and fixups +> fsck does (of bad location log data, for example). So, [[done]] +> --[[Joey]] diff --git a/doc/todo/wishlist:___96__git_annex_fsck_--checksums__96___--_verify_checksums_but_disregard_annex.numcopies/comment_1_6bcf067e4860bdfeb1d7b9fd1702a43a._comment b/doc/todo/wishlist:___96__git_annex_fsck_--checksums__96___--_verify_checksums_but_disregard_annex.numcopies/comment_1_6bcf067e4860bdfeb1d7b9fd1702a43a._comment new file mode 100644 index 0000000000..c8f40caf75 --- /dev/null +++ b/doc/todo/wishlist:___96__git_annex_fsck_--checksums__96___--_verify_checksums_but_disregard_annex.numcopies/comment_1_6bcf067e4860bdfeb1d7b9fd1702a43a._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawl9sYlePmv1xK-VvjBdN-5doOa_Xw-jH4U" + nickname="Richard" + subject="comment 1" + date="2013-07-10T23:09:31Z" + content=""" +As a side note, --numcopies was broken, but it's been fixed with 4.20130709. +"""]] diff --git a/doc/todo/wishlist:___96__git_annex_import__96___--_An_easy_way_to_get_data_into_an_annex.mdwn b/doc/todo/wishlist:___96__git_annex_import__96___--_An_easy_way_to_get_data_into_an_annex.mdwn new file mode 100644 index 0000000000..cd679485b5 --- /dev/null +++ b/doc/todo/wishlist:___96__git_annex_import__96___--_An_easy_way_to_get_data_into_an_annex.mdwn @@ -0,0 +1,13 @@ +`git annex import` would copy data over from external places into the annex. It would be run from within the annex and in the target location where the files need to end up. + +Two basic modes of operation: + +* If run on a normal directory, e.g. an SD card, it would simply copy over and `git annex add $newstuff` + +* If run on another indirect annex, it would copy over the symlinks, copy over the object data, verify that the checksums are OK and add to the annex + +An optional `git annex import --copy-only` would copy over and verify the data, but not yet add it. That would allow the user to import into a decent data structure. If run on non-annexed data, `git annex import --copy-only` would ideally calculate checksums and create symlinks already; thus ensuring data integrity as early as possible. + +-- RichiH + +> [[done]] --[[Joey]] in 2012 diff --git a/doc/todo/wishlist:___96__git_annex_import__96___--_An_easy_way_to_get_data_into_an_annex/comment_1_b9fd1bfaf9a3d238fdb7bc9c2d75fe5f._comment b/doc/todo/wishlist:___96__git_annex_import__96___--_An_easy_way_to_get_data_into_an_annex/comment_1_b9fd1bfaf9a3d238fdb7bc9c2d75fe5f._comment new file mode 100644 index 0000000000..ff9030c99d --- /dev/null +++ b/doc/todo/wishlist:___96__git_annex_import__96___--_An_easy_way_to_get_data_into_an_annex/comment_1_b9fd1bfaf9a3d238fdb7bc9c2d75fe5f._comment @@ -0,0 +1,22 @@ +[[!comment format=mdwn + username="http://joeyh.name/" + ip="4.153.254.222" + subject="comment 1" + date="2013-07-07T18:09:20Z" + content=""" +This is such a good idea that I went into the time machine and arranged for it to be implemented in June 2012: + +
+       import [path ...]
+              Moves files from somewhere outside the git work‐
+              ing copy, and adds them to the annex. Individual
+              files to import can be specified.  If  a  direc‐
+              tory is specified, all files in it are imported,
+              and any subdirectory structure inside it is pre‐
+              served.
+
+               git annex import /media/camera/DCIM/
+
+ +I don't see much use for `--copy-only` though. so did not implement it them (also I needed to spend some of my time at the race track). It seems to me that using `--copy-only` as you describe it would do everything except for add the files to git. You can get the same behavior by using `git annex import`, which only stages the new files but does not commit them, and then moving files around and running `git annex add` on them, followed by committing. +"""]] diff --git a/doc/todo/wishlist:___96__git_annex_import__96___--_An_easy_way_to_get_data_into_an_annex/comment_2_56f6972413c6f0d9f414245b6f4d27b9._comment b/doc/todo/wishlist:___96__git_annex_import__96___--_An_easy_way_to_get_data_into_an_annex/comment_2_56f6972413c6f0d9f414245b6f4d27b9._comment new file mode 100644 index 0000000000..ccdf2e7048 --- /dev/null +++ b/doc/todo/wishlist:___96__git_annex_import__96___--_An_easy_way_to_get_data_into_an_annex/comment_2_56f6972413c6f0d9f414245b6f4d27b9._comment @@ -0,0 +1,62 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawl9sYlePmv1xK-VvjBdN-5doOa_Xw-jH4U" + nickname="Richard" + subject="comment 2" + date="2013-07-10T23:25:52Z" + content=""" +Ugh, learn to read, etc... + +It's not possible to import from other annexes, though. Importing just the files from an indirect repo does nothing: + + ~/test-annex--foo % 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: indirect + trusted repositories: 0 + semitrusted repositories: 2 + 00000000-0000-0000-0000-000000000001 -- web + 8105410b-d8ca-4de4-bb6a-91b9772250dc -- here (richih@adamantium:~/test-annex--foo) + untrusted repositories: 0 + transfers in progress: none + available local disk space: 52 gigabytes (+1 megabyte reserved) + local annex keys: 1 + local annex size: 4 bytes + known annex keys: 1 + known annex size: 4 bytes + bloom filter size: 16 mebibytes (0% full) + backend usage: + SHA256E: 2 + ~/test-annex--foo % ls -l + total 4 + lrwxrwxrwx 1 richih richih 178 Jul 11 01:21 bar -> .git/annex/objects/g7/9v/SHA256E-s4--7d865e959b2466918c9863afca942d0fb89d7c9ac0c99bafc3749504ded97730/SHA256E-s4--7d865e959b2466918c9863afca942d0fb89d7c9ac0c99bafc3749504ded97730 + ~/test-annex--foo % cd ../test-annex + ~/test-annex % 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: indirect + trusted repositories: (merging synced/git-annex into git-annex...) + 0 + semitrusted repositories: 3 + 00000000-0000-0000-0000-000000000001 -- web + 7c50da8c-dc76-4b4a-b46d-8dd16385691a -- here (richih@adamantium:~/test-annex) + d4104a13-a2eb-4f5c-ba54-990ece5c81df -- richih@adamantium:~/test-annex-2 + untrusted repositories: 0 + transfers in progress: none + available local disk space: 52 gigabytes (+1 megabyte reserved) + local annex keys: 1 + local annex size: 4 bytes + known annex keys: 1 + known annex size: 4 bytes + bloom filter size: 16 mebibytes (0% full) + backend usage: + SHA256E: 2 + ~/test-annex % ls -l + total 4 + lrwxrwxrwx 1 richih richih 178 Jul 11 01:20 foo -> .git/annex/objects/91/9x/SHA256E-s4--b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c/SHA256E-s4--b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c + ~/test-annex % git annex import ../test-annex--foo/bar + ~/test-annex % ls -l + total 4 + lrwxrwxrwx 1 richih richih 178 Jul 11 01:20 foo -> .git/annex/objects/91/9x/SHA256E-s4--b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c/SHA256E-s4--b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c + ~/test-annex % + +"""]] diff --git a/doc/todo/wishlist:___96__git_annex_import__96___--_An_easy_way_to_get_data_into_an_annex/comment_3_2c094bef802a2182de4fcd0def1ad29b._comment b/doc/todo/wishlist:___96__git_annex_import__96___--_An_easy_way_to_get_data_into_an_annex/comment_3_2c094bef802a2182de4fcd0def1ad29b._comment new file mode 100644 index 0000000000..d3870e7c94 --- /dev/null +++ b/doc/todo/wishlist:___96__git_annex_import__96___--_An_easy_way_to_get_data_into_an_annex/comment_3_2c094bef802a2182de4fcd0def1ad29b._comment @@ -0,0 +1,12 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawl9sYlePmv1xK-VvjBdN-5doOa_Xw-jH4U" + nickname="Richard" + subject="comment 3" + date="2013-07-11T11:54:25Z" + content=""" +To expand on this a bit: + +* I meant \"ugh, me\" not \"ugh, anyone else\"; I simply did not see it... +* `git annex import` on a separate annex should copy over the symlinks and the objects behind it and then run `git annex add`, thus verifying, fixing symlinks, etc, imo. +* Something that may not be said often enough: Thanks :) +"""]] diff --git a/doc/todo/wishlist:___96__git_annex_import__96___--_An_easy_way_to_get_data_into_an_annex/comment_4_14915c43001f7f72c9fe5119a104ef5c._comment b/doc/todo/wishlist:___96__git_annex_import__96___--_An_easy_way_to_get_data_into_an_annex/comment_4_14915c43001f7f72c9fe5119a104ef5c._comment new file mode 100644 index 0000000000..4d4c1fbc56 --- /dev/null +++ b/doc/todo/wishlist:___96__git_annex_import__96___--_An_easy_way_to_get_data_into_an_annex/comment_4_14915c43001f7f72c9fe5119a104ef5c._comment @@ -0,0 +1,10 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawl9sYlePmv1xK-VvjBdN-5doOa_Xw-jH4U" + nickname="Richard" + subject="comment 4" + date="2013-07-13T15:25:56Z" + content=""" +After more deliberation, there should probably be an option to not have `git annex import` run `git annex add` automagically (but still call `git annex fix`) so manual shuffling around of files is still possible. + +See [[doc/bugs/__96__git_annex_fix__96___run_on_non-annexed_files_is_no-op]] for more of the rationale on this. +"""]] diff --git a/doc/todo/wishlist:___96__git_annex_sync_-m__96__.mdwn b/doc/todo/wishlist:___96__git_annex_sync_-m__96__.mdwn new file mode 100644 index 0000000000..92b5dee270 --- /dev/null +++ b/doc/todo/wishlist:___96__git_annex_sync_-m__96__.mdwn @@ -0,0 +1,10 @@ +Similar to how + + git commit -m 'foo' + +works, if I run + + git annex sync -m 'my hovercraft is full of eels' + +git annex should use that commit message instead of the default ones. That way, I could use [[sync]] directly and not be forced to commit prior to syncing just to make sure I have a useful commit message. + diff --git a/doc/todo/wishlist:_a_spec.remote_for_network_directories_that_would_mount_them_whenever_needed___40__e.g.__44___with_WebDAV__41__.mdwn b/doc/todo/wishlist:_a_spec.remote_for_network_directories_that_would_mount_them_whenever_needed___40__e.g.__44___with_WebDAV__41__.mdwn new file mode 100644 index 0000000000..f2c4254ad0 --- /dev/null +++ b/doc/todo/wishlist:_a_spec.remote_for_network_directories_that_would_mount_them_whenever_needed___40__e.g.__44___with_WebDAV__41__.mdwn @@ -0,0 +1,25 @@ +I've seen the [[tips/using box.com as a special remote]] for using mounted WebDAV remote directory for storage of the tracked files. + +It's quite close to a scenario familiar to me, although with a difference. + +Let me describe my situation. + +I worked on a supplement to a set of textbooks. My work was dependent on the revision of the textbooks (for correct references, etc.). The textbooks were being edited by the author, and published in a WebDAV directory. + +So I set up a Git repo for my work, and also a branch to track the revisions of the textbooks which [I updated by copying them from the WebDAV directory (after mounting it)](http://unix.stackexchange.com/q/25015/4319). The branch where my work was in was either based on the textbooks branch (and rebased/merged and edited to reflect the changes in the new revisions of the textbooks when needed) or contained the repo with textbooks as a submodule. + +The textbooks were large files, so I didn't want them be a part of the Git repo with my supplement work when I publish the repo. But I wanted for those who looked into my public repo to understand how to get the textbooks I'm referring to. + +I haven't solved this problem for myself completely. Now I see I could use git-annex for this. I t would track the state of the textbooks in the repo, without actually storing them there, and whenever one would need to get the missing textbooks in a clone of the repo, git-annex could handle the download from the WebDAV directory for him, right? + +I simply wrote down the rules for reproducing these downloading and saving operations, together with source URL (as a [Makefile](https://gitorious.org/primary-school-informatics-problems/received_2011-11-mathinf-initial-edition/blobs/RULES/Makefile)): + +whenever I wanted to update the revisions of the textbooks (or to download them the first time), I would checkout the branch which included this Makefile and was for holding the textbooks, and the run: + + make get + +-- this target had the temporary mountpoint for the remote directory as prerequisite, and there was a rule to create it, and mount the specified URL at it; then it would sync the files, and I could use Git to track the changes. After I was done inspecting the remote directory, I had to clean up the temporary mountpoint fby unmounting and deleting it. I didn't make it do this automatically after a `get` operation for performance reasons (caching of the remote directory would help if I wanted to access it once again). + +So, this differs from [[tips/using box.com as a special remote]] in that the tip for WebDAV suggest to handle the mounting manually, and git-annex knows nothing about the WebDAV URL where the content comes from. + +So here's my wish: a [[special remote|special remotes]] to track the WebDAV URLs in the repo, and mount the remote directory automatically under the hood, whenever one wants to get a file from there. (Then I assume it should also unmount it immediately in order to clean up after itself, despite possible inefficiencies). diff --git a/doc/todo/wishlist:_a_spec.remote_for_network_directories_that_would_mount_them_whenever_needed___40__e.g.__44___with_WebDAV__41__/comment_1_f46b0c9b49607e9f4f7a27f5a331ce83._comment b/doc/todo/wishlist:_a_spec.remote_for_network_directories_that_would_mount_them_whenever_needed___40__e.g.__44___with_WebDAV__41__/comment_1_f46b0c9b49607e9f4f7a27f5a331ce83._comment new file mode 100644 index 0000000000..32dd9c039b --- /dev/null +++ b/doc/todo/wishlist:_a_spec.remote_for_network_directories_that_would_mount_them_whenever_needed___40__e.g.__44___with_WebDAV__41__/comment_1_f46b0c9b49607e9f4f7a27f5a331ce83._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="http://joeyh.name/" + ip="4.153.14.141" + subject="comment 1" + date="2012-09-25T22:56:22Z" + content=""" +Note that git-annex already has the git configs `remote..annex-start-command` and `remote..annex-stop-command` which can be used to handle mounting and umounting. +"""]] diff --git a/doc/todo/wishlist:_a_spec.remote_for_network_directories_that_would_mount_them_whenever_needed___40__e.g.__44___with_WebDAV__41__/comment_2_1b34e1dd72011c65e881dec2543a0373._comment b/doc/todo/wishlist:_a_spec.remote_for_network_directories_that_would_mount_them_whenever_needed___40__e.g.__44___with_WebDAV__41__/comment_2_1b34e1dd72011c65e881dec2543a0373._comment new file mode 100644 index 0000000000..80c5ed2a93 --- /dev/null +++ b/doc/todo/wishlist:_a_spec.remote_for_network_directories_that_would_mount_them_whenever_needed___40__e.g.__44___with_WebDAV__41__/comment_2_1b34e1dd72011c65e881dec2543a0373._comment @@ -0,0 +1,12 @@ +[[!comment format=mdwn + username="http://lj.rossia.org/users/imz/" + ip="79.165.56.162" + subject="comment 2" + date="2012-09-25T23:33:23Z" + content=""" +I see, thanks for pointing at these config options! Perhaps, that'll be enough. + +I'll have to see whether the information on how to access the remote copy (the source URL and how to mount it) saved in config variables will be transferred to the clones of the repo. + +AFAIU [[location tracking]], usually, git-annex would transfer the information on where to look for copies from one repo to another. +"""]] diff --git a/doc/todo/wishlist:_addurl_https:.mdwn b/doc/todo/wishlist:_addurl_https:.mdwn new file mode 100644 index 0000000000..0a62eda6d6 --- /dev/null +++ b/doc/todo/wishlist:_addurl_https:.mdwn @@ -0,0 +1,11 @@ +It would be nice if "git annex addurl" allowed https: urls, rather than just http:. +To give an example, here is a PDF file: + + https://www.fbo.gov/utils/view?id=59ba4c8aa59101a09827ab7b9a787b05 + +If you switch the https: to http: it redirects you back to https:. + +As more sites provide https: for non-secret traffic, this becomes more of an issue. + +> I've gotten rid of the use of the HTTP library, now it just uses curl. +> [[done]] --[[Joey]] diff --git a/doc/todo/wishlist:_addurl_https:/comment_1_4e8f5e1fc52c3000eb2a1dad0624906e._comment b/doc/todo/wishlist:_addurl_https:/comment_1_4e8f5e1fc52c3000eb2a1dad0624906e._comment new file mode 100644 index 0000000000..fa500b1dd8 --- /dev/null +++ b/doc/todo/wishlist:_addurl_https:/comment_1_4e8f5e1fc52c3000eb2a1dad0624906e._comment @@ -0,0 +1,14 @@ +[[!comment format=mdwn + username="http://joeyh.name/" + ip="2001:4978:f:21a::2" + subject="comment 1" + date="2013-01-26T08:44:38Z" + content=""" +This works fine with \"git annex addurl\". + +However, with --fast, it fails: + + git-annex: user error (https not supported) + +This is because the Haskell HTTP library doesn't support https yet. Seems to be very little momentum on fixing that, perhaps I need to switch the code to use http-enumerator, which does. +"""]] diff --git a/doc/todo/wishlist:_allow_configuration_of_downloader_for_addurl.mdwn b/doc/todo/wishlist:_allow_configuration_of_downloader_for_addurl.mdwn new file mode 100644 index 0000000000..81f7ee4c08 --- /dev/null +++ b/doc/todo/wishlist:_allow_configuration_of_downloader_for_addurl.mdwn @@ -0,0 +1,3 @@ +It would be neat if Git annex addurl allowed a configuration option for a download manager command to do the actual download in place of wget/curl with a placeholder for the file name to save to & URL to get from (if that's all annex needs). That would allow the user to choose a graphical download manager if desired to make progress easier to monitor. The specific circumstance I'm seeing is with [[wishlist:_an___34__assistant__34___for_web-browsing_--_tracking_the_sources_of_the_downloads]]. I found that the existing Firefox addon [FlashGot](http://flashgot.net/) can run any command with arbitrary arguments including placeholders. Right now I've got a [script](https://gist.github.com/andyg0808/5342434) that changes to a user-selected directory and then runs git-annex addurl in it with the provided url. It works fine as a download manager for FlashGot. The issue is that there is no progress information for large file downloads. If git-annex could start a separate download manager to do the actual download, then the user would be able to check status at any time, even though the git-annex command was run from a GUI and not a terminal. + +> [[done]], you can use `annex.web-download-command` now. --[[Joey]] diff --git a/doc/todo/wishlist:_allow_the_same_remote_to_be_accissable_via_different_methods.mdwn b/doc/todo/wishlist:_allow_the_same_remote_to_be_accissable_via_different_methods.mdwn new file mode 100644 index 0000000000..9a3d953cb5 --- /dev/null +++ b/doc/todo/wishlist:_allow_the_same_remote_to_be_accissable_via_different_methods.mdwn @@ -0,0 +1,3 @@ +This is a wishlist item: + +Please allow the same remote to be available via different remotes. So in my LAN my remote is available using a ssh-connection, and when I travel with my laptop, the git-annex can also reach this remote using the Jabber transport. diff --git a/doc/todo/wishlist:_allow_the_same_remote_to_be_accissable_via_different_methods/comment_1_abb6263f3807160222bba1122475c89c._comment b/doc/todo/wishlist:_allow_the_same_remote_to_be_accissable_via_different_methods/comment_1_abb6263f3807160222bba1122475c89c._comment new file mode 100644 index 0000000000..a95ba56f96 --- /dev/null +++ b/doc/todo/wishlist:_allow_the_same_remote_to_be_accissable_via_different_methods/comment_1_abb6263f3807160222bba1122475c89c._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="http://joeyh.name/" + ip="2001:4978:f:21a::2" + subject="comment 1" + date="2013-08-07T16:09:26Z" + content=""" +You can have as many git remotes as you like all pointing at the same repository via different paths. git-annex fully supports this AFAIK. Are you having some problem with it? +"""]] diff --git a/doc/todo/wishlist:_an___34__assistant__34___for_web-browsing_--_tracking_the_sources_of_the_downloads.mdwn b/doc/todo/wishlist:_an___34__assistant__34___for_web-browsing_--_tracking_the_sources_of_the_downloads.mdwn new file mode 100644 index 0000000000..c910ace830 --- /dev/null +++ b/doc/todo/wishlist:_an___34__assistant__34___for_web-browsing_--_tracking_the_sources_of_the_downloads.mdwn @@ -0,0 +1,28 @@ +A replacement for a web-browser's downloads menu that uses git-annex internally ([[`addurl`|tips/using the web as a special remote]] command for the download, and `drop` or `git rm` for the clean up) would be quite helpful: + +say, when working on a topic, writing a paper or similar things, I usually have a Git repo with my text, all relevant data and processing code, and possibly other background information. It's nice to store the literature you needed at the same place where you work. (So that it is easy to catch up with what I was doing and thinking over when I left this work aside for a while, perhaps after cloning the repo from another location.) + +When I find an interesting literature, I save the file to the directory with my work, and read it. Then I might return to it to re-read it. There might be references to this document from my work, so I'd like them to work as links (perhaps pointing at the local file, but also at the source URL for the case when my document is read by someone else not on my system). + +I need to keep track of the source URLs for the documents I have saved which I read and use. + +That's a task that fits well git-annex. + +Note that doing the dull work of copying and pasting the URL and the downloading it and then opening it for reading is a pain to do every time I'm interested in a document I have found on the web. (Of course, I would need to fill out the bibliogrphic information for this document if I want to refer to it, but that can be done later. Initially, I wish I just don't lose the source URL of a document at the moment when I get interested in it and start reading.) + +So, I could be assisted by a replacement of the "downloads" menu of, say, Firefox: whenever I want to open a file for viewing (like a PDF), it should ask me where to save it, and I'd choose the directory with my work, then it should register it with git-annex (so that the source URL is saved, and perhaps it should also write down the referring page's URL somewhere nearby automatically), download it, and open with a viewer for reading. + +Then I'll have the interesting literature there when I'm offline; the source URLs would be saved, so that they can be put into the references. Also, if I distribute this work with Git, at another location git-annex can be used to easily get all the literature again. + +(Hmmm... probably, the browser that this will be simplest for me to implement for is emacs-w3m; simply, some functions calling git-annex...) + +> This seems fairly doable to implement since the git-annex [[design/assistant]] +> already has a webapp. So a javascript toolbar thing could be made that +> submits the current url (or maybe links dragged into it?) to the webapp +> for adding to the annex. +> +> The only wrinkle is that the webapp runs under a new url each time +> it starts, due to using a high port and embedding some auth token in the +> url. --[[Joey]] + +[[!tag /design/assistant]] diff --git a/doc/todo/wishlist:_an___34__assistant__34___for_web-browsing_--_tracking_the_sources_of_the_downloads/comment_1_36ae3c75053d5ec278b5e6eb2084d57a._comment b/doc/todo/wishlist:_an___34__assistant__34___for_web-browsing_--_tracking_the_sources_of_the_downloads/comment_1_36ae3c75053d5ec278b5e6eb2084d57a._comment new file mode 100644 index 0000000000..4c20561c75 --- /dev/null +++ b/doc/todo/wishlist:_an___34__assistant__34___for_web-browsing_--_tracking_the_sources_of_the_downloads/comment_1_36ae3c75053d5ec278b5e6eb2084d57a._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawmBUR4O9mofxVbpb8JV9mEbVfIYv670uJo" + nickname="Justin" + subject="comment 1" + date="2012-09-25T23:55:44Z" + content=""" +You know about `git annex addurl` right? It doesn't help with the browser integration (though I bet there are existing download manager extensions you could re-use for this) but it takes care of the other use cases. +"""]] diff --git a/doc/todo/wishlist:_an___34__assistant__34___for_web-browsing_--_tracking_the_sources_of_the_downloads/comment_3_be8eb800523db8cf7a2c68a28fbf5ea5._comment b/doc/todo/wishlist:_an___34__assistant__34___for_web-browsing_--_tracking_the_sources_of_the_downloads/comment_3_be8eb800523db8cf7a2c68a28fbf5ea5._comment new file mode 100644 index 0000000000..c958fd08f7 --- /dev/null +++ b/doc/todo/wishlist:_an___34__assistant__34___for_web-browsing_--_tracking_the_sources_of_the_downloads/comment_3_be8eb800523db8cf7a2c68a28fbf5ea5._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="http://m-f-k.myopenid.com/" + ip="93.207.162.28" + subject="+1" + date="2012-10-05T20:00:31Z" + content=""" +Copy+Paste+Open is to much for lazy people like me;) I like the idea of a direct browser download manager integration. This would make it so much easier to find find the original source of a downloaded file when you're to lazy to write it down somewhere in the first place … +"""]] diff --git a/doc/todo/wishlist:_an___34__assistant__34___for_web-browsing_--_tracking_the_sources_of_the_downloads/comment_3_d9f725de41a8572c85e4c6d9b4bcc927._comment b/doc/todo/wishlist:_an___34__assistant__34___for_web-browsing_--_tracking_the_sources_of_the_downloads/comment_3_d9f725de41a8572c85e4c6d9b4bcc927._comment new file mode 100644 index 0000000000..30515a49b5 --- /dev/null +++ b/doc/todo/wishlist:_an___34__assistant__34___for_web-browsing_--_tracking_the_sources_of_the_downloads/comment_3_d9f725de41a8572c85e4c6d9b4bcc927._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="http://lj.rossia.org/users/imz/" + ip="79.165.56.162" + subject="the scheme to follow to use the addurl command is longer" + date="2012-09-26T00:07:11Z" + content=""" +Justin, yes, I know. To use [[addurl|tips/using the web as a special remote]], I should force myself not to click on a link to view it, but rather to copy it, paste into the command line with addurl (then it will be downloaded, and I can run a command to view it...). Not terrible, probably learnable. +"""]] diff --git a/doc/todo/wishlist:_an___34__assistant__34___for_web-browsing_--_tracking_the_sources_of_the_downloads/comment_4_f52492e4cc6f965515800bd1c0e05c90._comment b/doc/todo/wishlist:_an___34__assistant__34___for_web-browsing_--_tracking_the_sources_of_the_downloads/comment_4_f52492e4cc6f965515800bd1c0e05c90._comment new file mode 100644 index 0000000000..7efd583c0f --- /dev/null +++ b/doc/todo/wishlist:_an___34__assistant__34___for_web-browsing_--_tracking_the_sources_of_the_downloads/comment_4_f52492e4cc6f965515800bd1c0e05c90._comment @@ -0,0 +1,10 @@ +[[!comment format=mdwn + username="andy" + ip="99.48.75.171" + subject="I've just written a script that does this (with a Firefox download manager)" + date="2013-04-09T02:51:26Z" + content=""" +So I've just written a two-line shell script that makes [FlashGot](http://flashgot.net/) able to do this. It's available on GitHub at . + +This could be even neater if Git-annex had the ability to set the download manager... Wishlist created: [[wishlist:_allow_configuration_of_downloader_for_addurl]] +"""]] diff --git a/doc/todo/wishlist:_an___34__assistant__34___for_web-browsing_--_tracking_the_sources_of_the_downloads/comment_5_5b36656fc5fa124e763f06711d9da32b._comment b/doc/todo/wishlist:_an___34__assistant__34___for_web-browsing_--_tracking_the_sources_of_the_downloads/comment_5_5b36656fc5fa124e763f06711d9da32b._comment new file mode 100644 index 0000000000..494aab7a80 --- /dev/null +++ b/doc/todo/wishlist:_an___34__assistant__34___for_web-browsing_--_tracking_the_sources_of_the_downloads/comment_5_5b36656fc5fa124e763f06711d9da32b._comment @@ -0,0 +1,10 @@ +[[!comment format=mdwn + username="http://joeyh.name/" + nickname="joey" + subject="comment 5" + date="2013-04-09T03:36:29Z" + content=""" +Wishlist implemented ;) + +A full example of doing this would make a nice addition to [[tips]] ... +"""]] diff --git a/doc/todo/wishlist:_an___34__assistant__34___for_web-browsing_--_tracking_the_sources_of_the_downloads/comment_6_285215a4466806baf85b8606f680494a._comment b/doc/todo/wishlist:_an___34__assistant__34___for_web-browsing_--_tracking_the_sources_of_the_downloads/comment_6_285215a4466806baf85b8606f680494a._comment new file mode 100644 index 0000000000..d0545153f9 --- /dev/null +++ b/doc/todo/wishlist:_an___34__assistant__34___for_web-browsing_--_tracking_the_sources_of_the_downloads/comment_6_285215a4466806baf85b8606f680494a._comment @@ -0,0 +1,12 @@ +[[!comment format=mdwn + username="http://joeyh.name/" + nickname="joey" + subject="comment 6" + date="2013-04-11T15:12:00Z" + content=""" +See [[tips/Using_Git-annex_as_a_web_browsing_assistant]]. + +I am leaving this bug open for now, as some javascript that passes the url off to the webapp is still a nice idea to build. + +A solution to the webapp's url always changing could be to have a file:/// url that the webapp creates, and javascript submits to, that then passes the request off to the webapp, probably by using additional javadcript in the html file. Assuming javascript security allows this. +"""]] diff --git a/doc/todo/wishlist:_an___34__assistant__34___for_web-browsing_--_tracking_the_sources_of_the_downloads/comment_7_15bf62e46db4b84ed3156f550f03de42._comment b/doc/todo/wishlist:_an___34__assistant__34___for_web-browsing_--_tracking_the_sources_of_the_downloads/comment_7_15bf62e46db4b84ed3156f550f03de42._comment new file mode 100644 index 0000000000..6c4d579c19 --- /dev/null +++ b/doc/todo/wishlist:_an___34__assistant__34___for_web-browsing_--_tracking_the_sources_of_the_downloads/comment_7_15bf62e46db4b84ed3156f550f03de42._comment @@ -0,0 +1,12 @@ +[[!comment format=mdwn + username="http://joeyh.name/" + nickname="joey" + subject="comment 7" + date="2013-04-11T15:41:16Z" + content=""" +A less round-about method would be to have the webapp listen for links dropped into its page. You could then have the webapp open in a tab, and just drag urls there to add them to the annex. + +I'm not sure if it's possible to intercept drags into a tab. Default browser behavior is certianly to redirect the tab to the url dropped into it. + +If someone finds out how to do this, I will build it. +"""]] diff --git a/doc/todo/wishlist:_annex.largefiles_support_for_mimetypes.mdwn b/doc/todo/wishlist:_annex.largefiles_support_for_mimetypes.mdwn new file mode 100644 index 0000000000..f38e41dd38 --- /dev/null +++ b/doc/todo/wishlist:_annex.largefiles_support_for_mimetypes.mdwn @@ -0,0 +1 @@ +It would be nice to have mimetype support on the `annex.largefiles` configuration directive. F.e. `git config annex.largefiles "not mimetype=text/plain"` diff --git a/doc/todo/wishlist:_command_options_changes.mdwn b/doc/todo/wishlist:_command_options_changes.mdwn new file mode 100644 index 0000000000..b154401818 --- /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:_command_options_changes/comment_1_bfba72a696789bf21b2435dea15f967a._comment b/doc/todo/wishlist:_command_options_changes/comment_1_bfba72a696789bf21b2435dea15f967a._comment new file mode 100644 index 0000000000..0ab113211e --- /dev/null +++ b/doc/todo/wishlist:_command_options_changes/comment_1_bfba72a696789bf21b2435dea15f967a._comment @@ -0,0 +1,17 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawl9sYlePmv1xK-VvjBdN-5doOa_Xw-jH4U" + nickname="Richard" + subject="comment 1" + date="2011-04-17T23:46:37Z" + content=""" +--to and --from seem to have different semantics than --source and --destination. Subtle, but still different. + +That being said, I am not sure --from and --to are needed at all. Calling the local repo . and all remotes by their name, they are arguably redundant and removing them would make the syntax a lot prettier; mv and cp don't need them, either. + +I am not sure changing syntax at this point is considered good style though personally, I wouldn't mind adapting and would actually prefer it over using --to and --from. + +-v and -q would be nice. + + +Richard +"""]] diff --git a/doc/todo/wishlist:_command_options_changes/comment_2_f6a637c78c989382e3c22d41b7fb4cc2._comment b/doc/todo/wishlist:_command_options_changes/comment_2_f6a637c78c989382e3c22d41b7fb4cc2._comment new file mode 100644 index 0000000000..0072ae1d71 --- /dev/null +++ b/doc/todo/wishlist:_command_options_changes/comment_2_f6a637c78c989382e3c22d41b7fb4cc2._comment @@ -0,0 +1,19 @@ +[[!comment format=mdwn + username="http://joey.kitenet.net/" + nickname="joey" + subject="comment 2" + date="2011-04-19T20:13:10Z" + content=""" +Let's see.. + +* -v is already an alias for --verbose + +* I don't find --source and --destination as easy to type or as clear as --from or --to. + +* -F is fast, so it cannot be used for --force. And I have no desire to make it easy to mistype a short option and enable --force; it can lose data. + +@richard while it would be possible to support some syntax like \"git annex copy . remote\"; what is it supposed to do if there are local files named foo and bar, and a remotes named foo and bar? Does \"git annex copy foo bar\" copy file foo to remote bar, or file bar from remote foo? I chose to use --from/--to to specify remotes independant of files to avoid such +ambiguity, which plain old `cp` doesn't have since it's operating entirely on filesystem objects, not both filesystem objects and abstract remotes. + +Seems like nothing to do here. [[done]] --[[Joey]] +"""]] diff --git a/doc/todo/wishlist:_command_options_changes/comment_3_bf1114533d2895804e531e76eb6b8095._comment b/doc/todo/wishlist:_command_options_changes/comment_3_bf1114533d2895804e531e76eb6b8095._comment new file mode 100644 index 0000000000..9fcbae6d20 --- /dev/null +++ b/doc/todo/wishlist:_command_options_changes/comment_3_bf1114533d2895804e531e76eb6b8095._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawl9sYlePmv1xK-VvjBdN-5doOa_Xw-jH4U" + nickname="Richard" + subject="comment 3" + date="2011-04-20T21:28:06Z" + content=""" +Good point. scp fixes this by using a colon, but as colons aren't needed in git-annex remotes' names... -- RichiH +"""]] 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 index 0000000000..156cfb0090 --- /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:_define_remotes_that_must_have_all_files/comment_1_cceccc1a1730ac688d712b81a44e31c3._comment b/doc/todo/wishlist:_define_remotes_that_must_have_all_files/comment_1_cceccc1a1730ac688d712b81a44e31c3._comment new file mode 100644 index 0000000000..1f65fd982f --- /dev/null +++ b/doc/todo/wishlist:_define_remotes_that_must_have_all_files/comment_1_cceccc1a1730ac688d712b81a44e31c3._comment @@ -0,0 +1,10 @@ +[[!comment format=mdwn + username="http://joey.kitenet.net/" + nickname="joey" + subject="comment 1" + date="2011-04-23T16:27:13Z" + content=""" +Seems to have a scalability problem, what happens when such a repository becomes full? + +Another way to accomplish I think the same thing is to pick the repositories that you would include in such a set, and make all other repositories untrusted. And set numcopies as desired. Then git-annex will never remove files from the set of non-untrusted repositories, and fsck will warn if a file is present on only an untrusted repository. +"""]] diff --git a/doc/todo/wishlist:_define_remotes_that_must_have_all_files/comment_2_eec848fcf3979c03cbff2b7407c75a7a._comment b/doc/todo/wishlist:_define_remotes_that_must_have_all_files/comment_2_eec848fcf3979c03cbff2b7407c75a7a._comment new file mode 100644 index 0000000000..1855cdda01 --- /dev/null +++ b/doc/todo/wishlist:_define_remotes_that_must_have_all_files/comment_2_eec848fcf3979c03cbff2b7407c75a7a._comment @@ -0,0 +1,16 @@ +[[!comment format=mdwn + username="gernot" + ip="87.79.209.169" + subject="comment 2" + date="2011-04-24T11:20:05Z" + content=""" +Right, I have thought about untrusting all but a few remotes to achieve +something similar before and I'm sure it would kind of work. It would be more +of an ugly workaround, however, because I would have to untrust remotes that +are, in reality, at least semi-trusted. That's why an extra option/attribute +for that kind of purpose/remote would be nice. + +Obviously I didn't see the scalability problem though. Good Point. Maybe I can +achieve the same thing by writing a log parsing script for myself? + +"""]] diff --git a/doc/todo/wishlist:_disable_automatic_commits.mdwn b/doc/todo/wishlist:_disable_automatic_commits.mdwn new file mode 100644 index 0000000000..5b39c60c75 --- /dev/null +++ b/doc/todo/wishlist:_disable_automatic_commits.mdwn @@ -0,0 +1,36 @@ +When using the [[/assistant]] on some of my repositories, I would like to +retain manual control over the granularity and contents of the commit +history. Some motivating reasons: + +* manually specified commit messages makes the history easier to follow +* make a series of minor changes to a file over a period of a few hours would result in a single commit rather than capturing intermediate incomplete edits + +* manual choice of which files to annex (based on predicted usage) could be useful, e.g. a repo might contain a 4MB PDF which you want available in *every* remote even without `git annex get`, and also some 2MB images which are only required in some remotes + +> This particular case is now catered to by the "manual" repository group +> in preferred content settings. --[[Joey]] + +Obviously this needs to be configurable at least per repository, and +ideally perhaps even per remote, since usage habits can vary from machine +to machine (e.g. I could choose to commit manually from my desktop machine +which has a nice comfy keyboard and large screen, but this would be too +much pain to do from my tiny netbook). + +In fact, this is vaguely related to [[design/assistant/partial_content]], +since the usefulness of the commit history depends on the context of the +data being manipulated, which in turn depends on which subdirectories are +being touched. So any mechanism for disabling sync per directory could +potentially be reused for disabling auto-commit per directory. + +According to Joey, it should be easy to arrange for the watcher thread not +to run, but would need some more work for the assistant to notice manual +commits in order to sync them; however the assistant already does some +crazy inotify watching of git refs, in order to detect incoming pushes, so +detecting manual commits wouldn't be a stretch. + +[[!tag design/assistant]] + +> You can do this now by pausing committing via the webapp, +> or setting `annex.autocommit=false`. +> +> The assustant probably doesn't push such commits yet. 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 index 0000000000..6299899e4f --- /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:_do_round_robin_downloading_of_data/comment_1_460335b0e59ad03871c524f1fe812357._comment b/doc/todo/wishlist:_do_round_robin_downloading_of_data/comment_1_460335b0e59ad03871c524f1fe812357._comment new file mode 100644 index 0000000000..6a5fd3d530 --- /dev/null +++ b/doc/todo/wishlist:_do_round_robin_downloading_of_data/comment_1_460335b0e59ad03871c524f1fe812357._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="http://joey.kitenet.net/" + nickname="joey" + subject="comment 1" + date="2011-04-03T16:39:35Z" + content=""" +I dunno about parrallel downloads -- eek! -- but there is at least room for improvement of what \"git annex get\" does when there are multiple remotes that have a file, and the one it decides to use is not available, or very slow, or whatever. +"""]] diff --git a/doc/todo/wishlist:_git-annex_replicate.mdwn b/doc/todo/wishlist:_git-annex_replicate.mdwn new file mode 100644 index 0000000000..0d926b3375 --- /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_replicate/comment_1_9926132ec6052760cdf28518a24e2358._comment b/doc/todo/wishlist:_git-annex_replicate/comment_1_9926132ec6052760cdf28518a24e2358._comment new file mode 100644 index 0000000000..cec971ee3b --- /dev/null +++ b/doc/todo/wishlist:_git-annex_replicate/comment_1_9926132ec6052760cdf28518a24e2358._comment @@ -0,0 +1,10 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawl9sYlePmv1xK-VvjBdN-5doOa_Xw-jH4U" + nickname="Richard" + subject="comment 1" + date="2011-04-22T18:27:00Z" + content=""" +While having remotes redistribute introduces some obvious security concerns, I might use it. + +As remotes support a cost factor already, you can basically implement bandwidth through that. +"""]] diff --git a/doc/todo/wishlist:_git-annex_replicate/comment_2_c43932f4194aba8fb2470b18e0817599._comment b/doc/todo/wishlist:_git-annex_replicate/comment_2_c43932f4194aba8fb2470b18e0817599._comment new file mode 100644 index 0000000000..9d50d15310 --- /dev/null +++ b/doc/todo/wishlist:_git-annex_replicate/comment_2_c43932f4194aba8fb2470b18e0817599._comment @@ -0,0 +1,12 @@ +[[!comment format=mdwn + username="http://joey.kitenet.net/" + nickname="joey" + subject="comment 2" + date="2011-04-23T16:22:07Z" + content=""" +Besides the cost values, annex.diskreserve was recently added. (But is not available for special remotes.) + +I have held off on adding high-level management stuff like this to git-annex, as it's hard to make it generic enough to cover use cases. + +A low-level way to accomplish this would be to have a way for `git annex get` and/or `copy` to skip files when `numcopies` is already satisfied. Then cron jobs could be used. +"""]] diff --git a/doc/todo/wishlist:_git-annex_replicate/comment_3_c13f4f9c3d5884fc6255fd04feadc2b1._comment b/doc/todo/wishlist:_git-annex_replicate/comment_3_c13f4f9c3d5884fc6255fd04feadc2b1._comment new file mode 100644 index 0000000000..e7eb06b3b1 --- /dev/null +++ b/doc/todo/wishlist:_git-annex_replicate/comment_3_c13f4f9c3d5884fc6255fd04feadc2b1._comment @@ -0,0 +1,10 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawmBUR4O9mofxVbpb8JV9mEbVfIYv670uJo" + nickname="Justin" + subject="comment 3" + date="2011-04-23T17:54:42Z" + content=""" +Hmm, so it seems there is almost a way to do this already. + +I think the one thing that isn't currently possible is to have 'plain' ssh remotes.. basically something just like the directory remote, but able to take a ssh user@host/path url. something like sshfs could be used to fake this, but for things like fsck you would want to do the sha1 calculations on the remote host. +"""]] diff --git a/doc/todo/wishlist:_git-annex_replicate/comment_4_63f24abf086d644dced8b01e1a9948c9._comment b/doc/todo/wishlist:_git-annex_replicate/comment_4_63f24abf086d644dced8b01e1a9948c9._comment new file mode 100644 index 0000000000..3805464a69 --- /dev/null +++ b/doc/todo/wishlist:_git-annex_replicate/comment_4_63f24abf086d644dced8b01e1a9948c9._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="http://joey.kitenet.net/" + nickname="joey" + subject="comment 4" + date="2011-09-19T18:54:46Z" + content=""" +git annex get/copy/drop all now support a --auto flag, which makes them only act on files that have not enough or too many copies. This allows for some crude replication; it doesn't take into account which repositories should be filled up more (beyond honoring annex.diskreserve), nor does it try to optimally use bandwidth (beyond honoring configured annex-cost). You have to run it in every repository that you want to participate in the replication, too. But it's probably a Good Enough solution. See [[walkthrough/automatically_managing_content]]. +"""]] diff --git a/doc/todo/wishlist:_git_annex_diff.mdwn b/doc/todo/wishlist:_git_annex_diff.mdwn new file mode 100644 index 0000000000..4acfee2553 --- /dev/null +++ b/doc/todo/wishlist:_git_annex_diff.mdwn @@ -0,0 +1,9 @@ +git diff is not very helpful for annexed files. + +How about a git annex diff command that allows to compare two versions of an annexed file? + +Should be relatively simple, only there would have to be a way to deal with the situation where not both versions are present in the repository. Either abort with a message showing the command you need to run to get the missing version(s). Or even interactively volunteer to get it automatically, asking the user for confirmation. + +Of course you wouldn't want to diff two large files, but with git annex assistant, all files are annexed by default (right?), so this would be useful. + +There might already be a way to easily diff two versions of an annexed file which I'm missing -- in that case please point me to it! :) 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 index 0000000000..9cd56749e8 --- /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_put_--_same_as_get__44___but_for_defaults/comment_1_d5413c8acce308505e4e2bec82fb1261._comment b/doc/todo/wishlist:_git_annex_put_--_same_as_get__44___but_for_defaults/comment_1_d5413c8acce308505e4e2bec82fb1261._comment new file mode 100644 index 0000000000..fe1d5520f4 --- /dev/null +++ b/doc/todo/wishlist:_git_annex_put_--_same_as_get__44___but_for_defaults/comment_1_d5413c8acce308505e4e2bec82fb1261._comment @@ -0,0 +1,10 @@ +[[!comment format=mdwn + username="http://joey.kitenet.net/" + nickname="joey" + subject="comment 1" + date="2011-04-04T18:13:46Z" + content=""" +This begs the question: What is the default remote? It's probably *not* the same repository that git's master branch is tracking (ie, origin/master). It seems there would have to be an annex.defaultremote setting. + +BTW, mr can easily be configured on a per-repo basis so that \"mr push\" copies to somewhere: `push = git push; git annex push wherever` +"""]] diff --git a/doc/todo/wishlist:_git_annex_put_--_same_as_get__44___but_for_defaults/comment_2_0aa227c85d34dfff4e94febca44abea8._comment b/doc/todo/wishlist:_git_annex_put_--_same_as_get__44___but_for_defaults/comment_2_0aa227c85d34dfff4e94febca44abea8._comment new file mode 100644 index 0000000000..3090b575b7 --- /dev/null +++ b/doc/todo/wishlist:_git_annex_put_--_same_as_get__44___but_for_defaults/comment_2_0aa227c85d34dfff4e94febca44abea8._comment @@ -0,0 +1,12 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawl9sYlePmv1xK-VvjBdN-5doOa_Xw-jH4U" + nickname="Richard" + subject="comment 2" + date="2011-04-04T20:45:30Z" + content=""" +In my case, the remotes are the same, but adding a new option could make sense. + +And while I can tell mr what to do explicitly, I would prefer if it did the right thing all by itself. Having to change configs in two separate places is less than ideal. + +I am not sure what you mean by `git annex push` as that does not exist. Did you mean copy? +"""]] diff --git a/doc/todo/wishlist:_git_annex_put_--_same_as_get__44___but_for_defaults/comment_3_2082f4d708a584a1403cc1d4d005fb56._comment b/doc/todo/wishlist:_git_annex_put_--_same_as_get__44___but_for_defaults/comment_3_2082f4d708a584a1403cc1d4d005fb56._comment new file mode 100644 index 0000000000..01dc7813ff --- /dev/null +++ b/doc/todo/wishlist:_git_annex_put_--_same_as_get__44___but_for_defaults/comment_3_2082f4d708a584a1403cc1d4d005fb56._comment @@ -0,0 +1,10 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawl9sYlePmv1xK-VvjBdN-5doOa_Xw-jH4U" + nickname="Richard" + subject="comment 1" + date="2011-04-04T10:28:01Z" + content=""" +Going one step further, a --min-copy could put all files so that numcopies is satisfied. --all could push to all available ones. + +To take everything another step further, if it was possible to group remotes, one could act on the groups. \"all\" would be an obvious choice for a group that always exists, everything else would be set up by the user. +"""]] diff --git a/doc/todo/wishlist:_git_annex_status.mdwn b/doc/todo/wishlist:_git_annex_status.mdwn new file mode 100644 index 0000000000..6bb5d71f11 --- /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_annex_status/comment_1_994bfd12c5d82e08040d6116915c5090._comment b/doc/todo/wishlist:_git_annex_status/comment_1_994bfd12c5d82e08040d6116915c5090._comment new file mode 100644 index 0000000000..7b5e7bd449 --- /dev/null +++ b/doc/todo/wishlist:_git_annex_status/comment_1_994bfd12c5d82e08040d6116915c5090._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawkSq2FDpK2n66QRUxtqqdbyDuwgbQmUWus" + nickname="Jimmy" + subject="comment 1" + date="2011-04-08T07:23:08Z" + content=""" ++1 for this feature, I've been longing for something like this other than rolling my own perl/shell scripts to parse the outputs of \"git annex whereis .\" to see how many files are on my machine or not. +"""]] diff --git a/doc/todo/wishlist:_git_annex_status/comment_2_c2b0ce025805b774dc77ce264a222824._comment b/doc/todo/wishlist:_git_annex_status/comment_2_c2b0ce025805b774dc77ce264a222824._comment new file mode 100644 index 0000000000..21f9d713cf --- /dev/null +++ b/doc/todo/wishlist:_git_annex_status/comment_2_c2b0ce025805b774dc77ce264a222824._comment @@ -0,0 +1,13 @@ +[[!comment format=mdwn + username="http://christian.amsuess.com/chrysn" + nickname="chrysn" + subject="format, respect working directory" + date="2011-04-26T12:31:02Z" + content=""" +we could include the information about the current directory as well, if the command is not issued in the local git root directory. to avoid large numbers of similar lines, that could look like this: + + Estimated annex size: B MiB (of C MiB; [B/C]%) + Estimated annex size in $PWD: B' MiB (of C' MiB; [B'/C']%) + +with the percentages being replaced with \"complete\" if really all files are present (and not just many enough for the value to be rounded to 100%). +"""]] diff --git a/doc/todo/wishlist:_git_annex_status/comment_3_d1fd70c67243971c96d59e1ffb7ef6e7._comment b/doc/todo/wishlist:_git_annex_status/comment_3_d1fd70c67243971c96d59e1ffb7ef6e7._comment new file mode 100644 index 0000000000..39986144be --- /dev/null +++ b/doc/todo/wishlist:_git_annex_status/comment_3_d1fd70c67243971c96d59e1ffb7ef6e7._comment @@ -0,0 +1,23 @@ +[[!comment format=mdwn + username="http://joey.kitenet.net/" + nickname="joey" + subject="comment 3" + date="2011-05-17T01:15:10Z" + content=""" +What a good idea! + +150 lines of haskell later, I have this: + +
+# git annex status
+supported backends: WORM SHA1 SHA256 SHA512 SHA224 SHA384 SHA1E SHA256E SHA512E SHA224E SHA384E URL
+supported remote types: git S3 bup directory rsync hook
+local annex keys: 32
+local annex size: 58 megabytes
+total annex keys: 38158
+total annex size: 6 terabytes (but 1632 keys have unknown size)
+backend usage: 
+	SHA1: 1789
+	WORM: 36369
+
+"""]] diff --git a/doc/todo/wishlist:_git_annex_status/comment_4_9aeeb83d202dc8fb33ff364b0705ad94._comment b/doc/todo/wishlist:_git_annex_status/comment_4_9aeeb83d202dc8fb33ff364b0705ad94._comment new file mode 100644 index 0000000000..f006f88a0a --- /dev/null +++ b/doc/todo/wishlist:_git_annex_status/comment_4_9aeeb83d202dc8fb33ff364b0705ad94._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="http://christian.amsuess.com/chrysn" + nickname="chrysn" + subject="status of other remotes?" + date="2011-06-15T08:39:24Z" + content=""" +using the location tracking information, it should be possible to show the status of other remotes as well. what about supporting `--from=...` or `--all`? (thus, among other things, one could determine if a remote has a complete checkout.) +"""]] 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 index 0000000000..fd1b40bffc --- /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:_git_backend_for_git-annex/comment_1_04319051fedc583e6c326bb21fcce5a5._comment b/doc/todo/wishlist:_git_backend_for_git-annex/comment_1_04319051fedc583e6c326bb21fcce5a5._comment new file mode 100644 index 0000000000..a691393b1a --- /dev/null +++ b/doc/todo/wishlist:_git_backend_for_git-annex/comment_1_04319051fedc583e6c326bb21fcce5a5._comment @@ -0,0 +1,10 @@ +[[!comment format=mdwn + username="http://joey.kitenet.net/" + nickname="joey" + subject="comment 1" + date="2011-03-28T16:01:30Z" + content=""" +Indeed, see [[todo/add_a_git_backend]], where you and I have already discussed this idea. :) + +With the new support for special remotes, which will be used by S3, it would be possible to make such a git repo, using bup, be a special remote. I think it would be pretty easy to implement now. Not a priority for me though. +"""]] diff --git a/doc/todo/wishlist:_git_backend_for_git-annex/comment_2_7f529f19a47e10b571f65ab382e97fd5._comment b/doc/todo/wishlist:_git_backend_for_git-annex/comment_2_7f529f19a47e10b571f65ab382e97fd5._comment new file mode 100644 index 0000000000..14798e7a71 --- /dev/null +++ b/doc/todo/wishlist:_git_backend_for_git-annex/comment_2_7f529f19a47e10b571f65ab382e97fd5._comment @@ -0,0 +1,14 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawl9sYlePmv1xK-VvjBdN-5doOa_Xw-jH4U" + nickname="Richard" + subject="comment 2" + date="2011-03-28T17:47:38Z" + content=""" +On the plus side, the past me wanted exactly what I had in mind. + +On the meh side, I really forgot about this conversation :/ + +When you say this todo is not a priority, does that mean there's no ETA at all and that it will most likely sleep for a long time? Or the almost usual \"what the heck, I will just wizard it up in two lines of haskell\"? + +-- RichiH +"""]] diff --git a/doc/todo/wishlist:_git_backend_for_git-annex/comment_3_a077bbad3e4b07cce019eb55a45330e7._comment b/doc/todo/wishlist:_git_backend_for_git-annex/comment_3_a077bbad3e4b07cce019eb55a45330e7._comment new file mode 100644 index 0000000000..8c3286d27b --- /dev/null +++ b/doc/todo/wishlist:_git_backend_for_git-annex/comment_3_a077bbad3e4b07cce019eb55a45330e7._comment @@ -0,0 +1,10 @@ +[[!comment format=mdwn + username="http://joey.kitenet.net/" + nickname="joey" + subject="comment 3" + date="2011-03-28T20:05:13Z" + content=""" +Probably more like 150 lines of haskell. Maybe just 50 lines if the bup repository is required to be on the same computer as the git-annex repository. + +Since I do have some repositories where I'd appreciate this level of assurance that data not be lost, it's mostly a matter of me finding a free day. +"""]] diff --git a/doc/todo/wishlist:_git_backend_for_git-annex/comment_4_ecca429e12d734b509c671166a676c9d._comment b/doc/todo/wishlist:_git_backend_for_git-annex/comment_4_ecca429e12d734b509c671166a676c9d._comment new file mode 100644 index 0000000000..cf649a8a25 --- /dev/null +++ b/doc/todo/wishlist:_git_backend_for_git-annex/comment_4_ecca429e12d734b509c671166a676c9d._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawl9sYlePmv1xK-VvjBdN-5doOa_Xw-jH4U" + nickname="Richard" + subject="comment 4" + date="2011-03-28T20:45:35Z" + content=""" +Personally, I would not mind a requirement to keep a local bup repo. I wouldn't want my data to to unncessarily complex setups, anyway. -- RichiH +"""]] diff --git a/doc/todo/wishlist:_git_backend_for_git-annex/comment_5_3459f0b41d818c23c8fb33edb89df634._comment b/doc/todo/wishlist:_git_backend_for_git-annex/comment_5_3459f0b41d818c23c8fb33edb89df634._comment new file mode 100644 index 0000000000..a1300f2e64 --- /dev/null +++ b/doc/todo/wishlist:_git_backend_for_git-annex/comment_5_3459f0b41d818c23c8fb33edb89df634._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="http://joey.kitenet.net/" + nickname="joey" + subject="comment 5" + date="2011-04-08T20:59:37Z" + content=""" +My estimates were pretty close -- the new bup special remote type took 133 lines of code, and 2 hours to write. A testament to the flexibility of the special remote infrastructure. :) +"""]] diff --git a/doc/todo/wishlist:_history_of_operations.mdwn b/doc/todo/wishlist:_history_of_operations.mdwn new file mode 100644 index 0000000000..a79f500ff6 --- /dev/null +++ b/doc/todo/wishlist:_history_of_operations.mdwn @@ -0,0 +1,8 @@ +Hi, + +I would love to have a page of "history" or "events" in the webapp. Similar to how Dropbox or Box show it. +I've been using git-annex for my personal files for a few months now, and I feel like this is the only feature missing to start using it in a production multi-user environment. + +Thanks + +[[!tag design/assistant]] diff --git a/doc/todo/wishlist:_history_of_operations/comment_1_f9a77ce83c6f39b6272d5c577ffbb9f9._comment b/doc/todo/wishlist:_history_of_operations/comment_1_f9a77ce83c6f39b6272d5c577ffbb9f9._comment new file mode 100644 index 0000000000..bb872532d7 --- /dev/null +++ b/doc/todo/wishlist:_history_of_operations/comment_1_f9a77ce83c6f39b6272d5c577ffbb9f9._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawlJEI45rGczFAnuM7gRSj4C6s9AS9yPZDc" + nickname="Kevin" + subject="I second this wishlist item" + date="2013-07-24T16:48:59Z" + content=""" +I too would like to see a recent history of events in the assistant webapp. A simple listing of what changed when and by whom would be sufficient. +"""]] diff --git a/doc/todo/wishlist:_make_partial_files_available_during_transfer.mdwn b/doc/todo/wishlist:_make_partial_files_available_during_transfer.mdwn new file mode 100644 index 0000000000..b021c90914 --- /dev/null +++ b/doc/todo/wishlist:_make_partial_files_available_during_transfer.mdwn @@ -0,0 +1,18 @@ +Imagine this situation: +You have a laptop and a NAS. +On your laptop you want to consume a large media file located on the NAS. +So you type: + + git annex get --from nas mediafile + +But now you have to wait for the download to complete, unless either + +* rsync is pointed directly to the file in the object storage ("--inplace") +or +* the symlink temporarily points to the partial file during a transfer + +which would allow you instantaneous consumption of your media. +It might make sense to make this behavior configurable, because not everyone might agree with having partial content (that mismatches its key) around. + + +So what do you say? diff --git a/doc/todo/wishlist:_make_partial_files_available_during_transfer/comment_3_1304a721da6f5133fdfa1dac507f1ecb._comment b/doc/todo/wishlist:_make_partial_files_available_during_transfer/comment_3_1304a721da6f5133fdfa1dac507f1ecb._comment new file mode 100644 index 0000000000..8e955fd48f --- /dev/null +++ b/doc/todo/wishlist:_make_partial_files_available_during_transfer/comment_3_1304a721da6f5133fdfa1dac507f1ecb._comment @@ -0,0 +1,10 @@ +[[!comment format=mdwn + username="http://joeyh.name/" + ip="4.152.108.194" + subject="comment 3" + date="2012-11-04T19:58:28Z" + content=""" +I'm not at all comfortable with either idea. Temporarily repointing the symlink could lead to accidentially git committing a bad symlink. Or the user accidentially doing something with a partially transferred file. Running rsync in place would break lots of things that assume that, once the file is present, it can be assumed to be the full and correct file. (Obviously fsck doesn't assume that, but checks made by `git annex drop` do, for example.) + +However, you can access partially transferred files by key in `.git/annex/tmp`. It would be easy to write some hack that looks at the symlink to get the key, and then spits out the name of the partial file in `.git/annex/tmp.`. +"""]] diff --git a/doc/todo/wishlist:_more_info_in_the_standard_commit_message_of___96__sync__96__.mdwn b/doc/todo/wishlist:_more_info_in_the_standard_commit_message_of___96__sync__96__.mdwn new file mode 100644 index 0000000000..4e5e3a171b --- /dev/null +++ b/doc/todo/wishlist:_more_info_in_the_standard_commit_message_of___96__sync__96__.mdwn @@ -0,0 +1,3 @@ +Could you include the REPO and UUID information in the "automatic sync" commit message? + +This would make troubleshooting easier. (not there was much trouble with git-annex!) diff --git a/doc/todo/wishlist:_move_pending_transfers_for_a_host_to_the_end_of_the_queue_when_one_fails.mdwn b/doc/todo/wishlist:_move_pending_transfers_for_a_host_to_the_end_of_the_queue_when_one_fails.mdwn new file mode 100644 index 0000000000..e50ebbde5e --- /dev/null +++ b/doc/todo/wishlist:_move_pending_transfers_for_a_host_to_the_end_of_the_queue_when_one_fails.mdwn @@ -0,0 +1,7 @@ +Right now the assistant can have a huge list of pending transfers for certain hosts if its data is a bit outdated, or a host hasn't been synced lately. When starting up it will then attempt each transfer to said host (which will in turn fail, but at times take time to time out), possibly before doing other stuff like attempting to download new files, or copy files to online hosts. + +I suggest that if a transfer fails for host X, and there are other pending transfers, say to host Y and from Z, then all other pending transfers to/from X gets pushed to the back of the queue, to avoid having to wait a long time for several transfers to time out before doing useful stuff. + +The prime example for me was this morning, when a laptop that was turned off had a huge amount of queued transfers to it, resulting in the assistant attempting a load of transfers to that host before it retrieved a new file that I had created on another machine yesterday. + +[[!tag design/assistant]] diff --git a/doc/todo/wishlist:_move_pending_transfers_for_a_host_to_the_end_of_the_queue_when_one_fails/comment_1_82ee9de610a0ac55cd1c27c211079e5b._comment b/doc/todo/wishlist:_move_pending_transfers_for_a_host_to_the_end_of_the_queue_when_one_fails/comment_1_82ee9de610a0ac55cd1c27c211079e5b._comment new file mode 100644 index 0000000000..3b6101ed58 --- /dev/null +++ b/doc/todo/wishlist:_move_pending_transfers_for_a_host_to_the_end_of_the_queue_when_one_fails/comment_1_82ee9de610a0ac55cd1c27c211079e5b._comment @@ -0,0 +1,10 @@ +[[!comment format=mdwn + username="http://joeyh.name/" + ip="4.154.6.49" + subject="comment 1" + date="2012-12-01T19:22:03Z" + content=""" +There are several difficulties with reordering the queue that way. One is that the failure may be intermittent; another is that the queue is fed by a scanning process, so doesn't always have a well-defined end. + +Another way to deal with this problem, which I think I prefer, is to allow multiple actions from the queue to run at once. Then slow or unreachable remotes don't block it from using other remotes. +"""]] diff --git a/doc/todo/wishlist:_move_pending_transfers_for_a_host_to_the_end_of_the_queue_when_one_fails/comment_2_bea55156bd32cf9e6dd9b946ba1bb8c1._comment b/doc/todo/wishlist:_move_pending_transfers_for_a_host_to_the_end_of_the_queue_when_one_fails/comment_2_bea55156bd32cf9e6dd9b946ba1bb8c1._comment new file mode 100644 index 0000000000..62d46bccd0 --- /dev/null +++ b/doc/todo/wishlist:_move_pending_transfers_for_a_host_to_the_end_of_the_queue_when_one_fails/comment_2_bea55156bd32cf9e6dd9b946ba1bb8c1._comment @@ -0,0 +1,10 @@ +[[!comment format=mdwn + username="EskildHustvedt" + ip="84.48.83.221" + subject="comment 2" + date="2012-12-01T19:31:18Z" + content=""" +I agree your method might be preferable, the end result is the same, and would have avoided the issues I had (and, of course, running multiple transfers at once has other benefits as well). + +An alternate way would be to push every transfer NOT from host X to the front of the queue (avoiding most of the \"no defined end\" issue and largely solving the problem), but if multiple actions at once is feasible then that'd still be much nicer. +"""]] 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 index 0000000000..d0b847933c --- /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:_option_to_disable_url_checking_with_addurl/comment_1_868a380faa1e55faa3c2d314e3258e86._comment b/doc/todo/wishlist:_option_to_disable_url_checking_with_addurl/comment_1_868a380faa1e55faa3c2d314e3258e86._comment new file mode 100644 index 0000000000..4bdf4c97bf --- /dev/null +++ b/doc/todo/wishlist:_option_to_disable_url_checking_with_addurl/comment_1_868a380faa1e55faa3c2d314e3258e86._comment @@ -0,0 +1,10 @@ +[[!comment format=mdwn + username="http://joeyh.name/" + nickname="joey" + subject="comment 1" + date="2013-03-11T18:19:34Z" + content=""" +It does this to get the size of the url, so it can record this in the key. It would be possible to add a flag to skip that (--fast is already taken of course), but then you tend to get a lot of keys in your repository with no size info attached, which makes `git annex status` complain that it cannot tell you exactly how big your repo is, and is generally not the best. It also defeats annex.diskreserve checking, for example. + +With --fast, the size is the only info available to ensure that the content behind the url has not changed when downloading it later. I suppose for some urls you may not want that checked, and so a --relaxed type option could make sense in that use case as well, although with the above caveat. +"""]] diff --git a/doc/todo/wishlist:_print_locations_for_files_in_rsync_remote.mdwn b/doc/todo/wishlist:_print_locations_for_files_in_rsync_remote.mdwn new file mode 100644 index 0000000000..3876f21977 --- /dev/null +++ b/doc/todo/wishlist:_print_locations_for_files_in_rsync_remote.mdwn @@ -0,0 +1,6 @@ +Based on an irc conversation earlier today: + +19:50 < warp> joeyh: what is the best way to figure out the (remote) filename for a file stored in an rsync remote? + +20:43 < joeyh> warp: re your other question, probably the best thing would be to make the whereis command print out locations for each remote, as it always does for the web special remotes + diff --git a/doc/todo/wishlist:_push_to_cia.vc_from_the_website__39__s_repo__44___not_your_personal_one.mdwn b/doc/todo/wishlist:_push_to_cia.vc_from_the_website__39__s_repo__44___not_your_personal_one.mdwn new file mode 100644 index 0000000000..f9b4c8c35d --- /dev/null +++ b/doc/todo/wishlist:_push_to_cia.vc_from_the_website__39__s_repo__44___not_your_personal_one.mdwn @@ -0,0 +1,3 @@ +I just added a CIA bot to #vcs-home and tracking commits immediately would be nice. -- RichiH + +[[done]] diff --git a/doc/todo/wishlist:_push_to_cia.vc_from_the_website__39__s_repo__44___not_your_personal_one/comment_1_3480b0ec629ef29a151408d869186bf8._comment b/doc/todo/wishlist:_push_to_cia.vc_from_the_website__39__s_repo__44___not_your_personal_one/comment_1_3480b0ec629ef29a151408d869186bf8._comment new file mode 100644 index 0000000000..5d0edce2ea --- /dev/null +++ b/doc/todo/wishlist:_push_to_cia.vc_from_the_website__39__s_repo__44___not_your_personal_one/comment_1_3480b0ec629ef29a151408d869186bf8._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="http://joey.kitenet.net/" + nickname="joey" + subject="comment 1" + date="2011-09-19T18:57:52Z" + content=""" +JFTR, pushing now happens automatically from branchable. +"""]] 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 index 0000000000..2bfb90b540 --- /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:_recursive_directory_remote_setup__47__addurl/comment_1_b79976afc2242791523e63831f30af71._comment b/doc/todo/wishlist:_recursive_directory_remote_setup__47__addurl/comment_1_b79976afc2242791523e63831f30af71._comment new file mode 100644 index 0000000000..caefee9a82 --- /dev/null +++ b/doc/todo/wishlist:_recursive_directory_remote_setup__47__addurl/comment_1_b79976afc2242791523e63831f30af71._comment @@ -0,0 +1,12 @@ +[[!comment format=mdwn + username="https://launchpad.net/~arand" + nickname="arand" + subject="comment 1" + date="2013-03-10T23:29:55Z" + content=""" +Recursively adding urls is already feasiable with some simple scripting: + +[https://gitorious.org/arand-scripts/arand-scripts/blobs/master/annex-importdir](https://gitorious.org/arand-scripts/arand-scripts/blobs/master/annex-importdir) + +but this is obviously missing the checksumming bit though. +"""]] diff --git a/doc/todo/wishlist:_recursive_directory_remote_setup__47__addurl/comment_2_1741d2392006a9af9cfd1f3b847600b9._comment b/doc/todo/wishlist:_recursive_directory_remote_setup__47__addurl/comment_2_1741d2392006a9af9cfd1f3b847600b9._comment new file mode 100644 index 0000000000..64d5924795 --- /dev/null +++ b/doc/todo/wishlist:_recursive_directory_remote_setup__47__addurl/comment_2_1741d2392006a9af9cfd1f3b847600b9._comment @@ -0,0 +1,9 @@ +[[!comment format=mdwn + username="http://joeyh.name/" + nickname="joey" + subject="comment 2" + date="2013-03-11T03:04:44Z" + content=""" +See also: [[forum/Managing a large number of files archived on many pieces of read-only medium (E.G. DVDs)]] +particularly [[this comment|forum/Managing a large number of files archived on many pieces of read-only medium (E.G. DVDs)#comment-908dbe02f29e011f030bba4ab5ef73d1]] +"""]] diff --git a/doc/todo/wishlist:_simpler_gpg_usage.mdwn b/doc/todo/wishlist:_simpler_gpg_usage.mdwn new file mode 100644 index 0000000000..1236ee234e --- /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:_simpler_gpg_usage/comment_1_6923fa6ebc0bbe7d93edb1d01d7c46c5._comment b/doc/todo/wishlist:_simpler_gpg_usage/comment_1_6923fa6ebc0bbe7d93edb1d01d7c46c5._comment new file mode 100644 index 0000000000..f96f5c3777 --- /dev/null +++ b/doc/todo/wishlist:_simpler_gpg_usage/comment_1_6923fa6ebc0bbe7d93edb1d01d7c46c5._comment @@ -0,0 +1,19 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawmBUR4O9mofxVbpb8JV9mEbVfIYv670uJo" + nickname="Justin" + subject="comment 1" + date="2012-04-29T01:41:57Z" + content=""" +Thinking about this more, I think minimally git-annex could support a + + remote..gpg-options + +or + + remote..gpg-keyring + +for options to be passed to gpg. I'm not sure how automatically setting it to $ANNEX_ROOT/.gnupg/.. would work. + + +I need to read the encryption code to fully understand it, but I also wonder if there is not also a way to just bypass gpg entirely and store the remote-encryption keys locally in plain text. +"""]] diff --git a/doc/todo/wishlist:_simpler_gpg_usage/comment_2_6fc874b6c391df242bd2592c4a65eae8._comment b/doc/todo/wishlist:_simpler_gpg_usage/comment_2_6fc874b6c391df242bd2592c4a65eae8._comment new file mode 100644 index 0000000000..29eb11622a --- /dev/null +++ b/doc/todo/wishlist:_simpler_gpg_usage/comment_2_6fc874b6c391df242bd2592c4a65eae8._comment @@ -0,0 +1,10 @@ +[[!comment format=mdwn + username="http://joey.kitenet.net/" + nickname="joey" + subject="comment 2" + date="2012-04-29T02:39:20Z" + content=""" +The encryption uses a symmetric cipher that is stored in the git repository already. It's just stored encrypted to the various gpg keys that have been configured to use it. It would certianly be possible to store the symmetric cipher unencrypted in the git repo. + +I don't see your idea of gpg-options saving any work. It would still require you to do key distribution and run commands in each repo to set it up. +"""]] diff --git a/doc/todo/wishlist:_simpler_gpg_usage/comment_3_012f340c8c572fe598fc860c1046dabd._comment b/doc/todo/wishlist:_simpler_gpg_usage/comment_3_012f340c8c572fe598fc860c1046dabd._comment new file mode 100644 index 0000000000..051f17a24b --- /dev/null +++ b/doc/todo/wishlist:_simpler_gpg_usage/comment_3_012f340c8c572fe598fc860c1046dabd._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="http://joey.kitenet.net/" + nickname="joey" + subject="comment 3" + date="2012-04-29T02:41:38Z" + content=""" +BTW re your Tweet.. I was so happy to be able to use 'c i a' in Crypto.hs. :) +"""]] diff --git a/doc/todo/wishlist:_simpler_gpg_usage/comment_4_e0c2a13217b795964f3b630c001661ef._comment b/doc/todo/wishlist:_simpler_gpg_usage/comment_4_e0c2a13217b795964f3b630c001661ef._comment new file mode 100644 index 0000000000..c9e3375414 --- /dev/null +++ b/doc/todo/wishlist:_simpler_gpg_usage/comment_4_e0c2a13217b795964f3b630c001661ef._comment @@ -0,0 +1,10 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawmBUR4O9mofxVbpb8JV9mEbVfIYv670uJo" + nickname="Justin" + subject="comment 4" + date="2012-04-29T03:09:03Z" + content=""" +I got a good laugh out of it :-) + +Storing the key unencrypted would make things easier.. I think at least for my use-cases I don't require another layer of protection on top of the ssh keys that provide access to the encrypted remotes themselves. +"""]] diff --git a/doc/todo/wishlist:_simpler_gpg_usage/comment_5_9668b58eb71901e1db8da7db38e068ca._comment b/doc/todo/wishlist:_simpler_gpg_usage/comment_5_9668b58eb71901e1db8da7db38e068ca._comment new file mode 100644 index 0000000000..60b98bde5d --- /dev/null +++ b/doc/todo/wishlist:_simpler_gpg_usage/comment_5_9668b58eb71901e1db8da7db38e068ca._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="http://joey.kitenet.net/" + nickname="joey" + subject="comment 5" + date="2012-04-29T18:04:13Z" + content=""" +encryption=shared is now supported +"""]] diff --git a/doc/todo/wishlist:_spec.remotes_for_other_peer_network_data_stores___40__gnunet__44___freenet__41__.mdwn b/doc/todo/wishlist:_spec.remotes_for_other_peer_network_data_stores___40__gnunet__44___freenet__41__.mdwn new file mode 100644 index 0000000000..545bd861d6 --- /dev/null +++ b/doc/todo/wishlist:_spec.remotes_for_other_peer_network_data_stores___40__gnunet__44___freenet__41__.mdwn @@ -0,0 +1,26 @@ +Apart from Tahoe-LAFS (covered by [[todo/tahoe lfs for reals]] and [[forum/tips: special_remotes/hook with tahoe-lafs]]), [[special remotes]] (which I understand as real storage backends) for other other [peer network data stores](http://en.wikipedia.org/wiki/Distributed_data_store#Peer_network_node_data_stores_2) would be interesting. + +I mean gnunet, freenet, BitTorrent (also trackerless). + +Before dropping a file locally, the BitTorrent client should check that all parts are still available from the peers. + +Of course, there is no guarantee assumed that the content won't disappear from the peer network in future: they act more like a cache rather than an archive on whose lifespan you decide. (I'm only not sure about gnunet now: whether there is a rule of dropping unused content from it, like in freenet.) + +So, a copy in peer networks shouldn't be counted on by git-annex as much as a copy on a storage you control: probably, by efault, it shouldn't let you delete the local copy if there is a copy in a peer network unless you saved it somewhere else. + +(Think of such a scenario: I could save some of my public large data on external disks/DVDs and keep them at home, and also put them onto peer networks with the same nterface of git-annex which I would be used to; I would also use the git-annex interface to check from time to time that the content is still present, i.e. "cached", on the peer networks. Whenever I'm away from home, and unexpectedly need to show this content to someone, or have a look at it for some reason, I could get it from the peer network "cache".) + +Also networks like namecoin (derived from bitcoin) can be used as a key-value store. Despite being a peer network, a system like namecoin actually could offer the publisher more control over the lifespan of the content: he should be able to offer "financial" reward for others processing his key-value data. (But I'm not sure namecoin is designed reasonably for this reward system to work actually; but there might be appearing other similar systems.) + +## A different view: extend the key-value backends with ways to look for the content in other content-addressable storage systems +We might want to look for the registered files in other [content-addressable storage systems](http://en.wikipedia.org/wiki/Content-addressable_storage#Open-source_implementations) (and also to be able to put the files there for storage). + +For example: + +* [**GNUnet**](http://en.wikipedia.org/wiki/Gnunet) uses its own hash format to address the content. git-annex could extend its own [[backends]] with a one to work with GNUnet, and by default have a built-in [[special remote|special remotes]] that would interact with GNUnet when looking for a content or storing some content. No special setup of the special remote in each repo should be necessary, because GNUnet is "global", so we'd just use the user's already configured GNUnet client. Just turning the builtin GNUnet special remote on or off should be an option (in the repo configuration, and when calling the commands that would query it, like `whereis`). +* **freenet** is similar. +* Similarly, a backend for the hashes used in **BitTorrent** and **magnet links** could be used. If we want a trackerless mode, then probably it's a similar case for a "global"/built-in special remote that needs no local setup in each repo. Using a selected tracker would mean setting up a special remote in our repo. +* **Git** itself can be viwed as place to look for the content. There could be a corresponding backend and a builtin special remote (needing no extra setup) to look for the content among the objects stored in the local Git repo. (What if we have a copy of a file that we've put under the control of git-annex in a previous Git commit? We could get it from the object store of Git.) +* **Venti**, [[**Tahoe-LAFS**|todo/tahoe lfs for reals]] would need a backend for their hashes, and a specially setup special remote in each repo where we'd like to use them--because these are not "global" system, we must setup the path to the instance of the filesystem we'd like to use. +* probably, there must be other interesting cases of this kind... +* (I'm also thinking about using somethng like a **bibliographic information** as a key, but then it wouldn't guarantee identical files: the same paper can be stored in different formats, etc. Cf. [**URNs**](http://en.wikipedia.org/wiki/Uniform_resource_name#Examples), via . Also, an URN like bibliographic information can't be computed from the file, it will have to be entered manually or obtained from another directory of URNs.) diff --git a/doc/todo/wishlist:_spec.remotes_for_other_peer_network_data_stores___40__gnunet__44___freenet__41__/comment_1_e2c2047e7401cb95a82ffb686a732859._comment b/doc/todo/wishlist:_spec.remotes_for_other_peer_network_data_stores___40__gnunet__44___freenet__41__/comment_1_e2c2047e7401cb95a82ffb686a732859._comment new file mode 100644 index 0000000000..80a245d144 --- /dev/null +++ b/doc/todo/wishlist:_spec.remotes_for_other_peer_network_data_stores___40__gnunet__44___freenet__41__/comment_1_e2c2047e7401cb95a82ffb686a732859._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="http://joeyh.name/" + ip="4.153.14.141" + subject="comment 1" + date="2012-09-25T22:57:19Z" + content=""" +The best first step to adding such kinds of data stores to git-annex is probably to use the [[special_remotes/hook]] special remote to access them. +"""]] diff --git a/doc/todo/wishlist:_spec.remotes_for_other_peer_network_data_stores___40__gnunet__44___freenet__41__/comment_2_472b576afdb169b233edd01adcb2123d._comment b/doc/todo/wishlist:_spec.remotes_for_other_peer_network_data_stores___40__gnunet__44___freenet__41__/comment_2_472b576afdb169b233edd01adcb2123d._comment new file mode 100644 index 0000000000..c58f97c1cb --- /dev/null +++ b/doc/todo/wishlist:_spec.remotes_for_other_peer_network_data_stores___40__gnunet__44___freenet__41__/comment_2_472b576afdb169b233edd01adcb2123d._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="http://lj.rossia.org/users/imz/" + ip="79.165.56.162" + subject="comment 2" + date="2012-09-25T23:29:49Z" + content=""" +I see. But then, as with Tahoe-LAFS, they also have their own formats for checksums, keys, which could be re-used in git-annex, and that needs special treatment. +"""]] diff --git a/doc/todo/wishlist:_special-case_handling_of_Youtube_URLs_in_Web_special_remote.mdwn b/doc/todo/wishlist:_special-case_handling_of_Youtube_URLs_in_Web_special_remote.mdwn new file mode 100644 index 0000000000..3e08bb8d9f --- /dev/null +++ b/doc/todo/wishlist:_special-case_handling_of_Youtube_URLs_in_Web_special_remote.mdwn @@ -0,0 +1,22 @@ +The [[Web special remote|special remotes/web]] could possibly be improved by detecting when URLs reference a Youtube video page and using [youtube-dl](http://rg3.github.com/youtube-dl/) instead of wget to download the page. Youtube-dl can also handle several other video sites such as vimeo.com and blip.tv, so if this idea were to be implemented, it might make sense to borrow the regular expressions that youtube-dl uses to identify video URLs. A quick grep through the youtube-dl source for the identifier _VALID_URL should find those regexes (in Python's regex format). + +> This is something I've thought about doing for a while.. +> Two things I have not figured out: +> +> * Seems that this should really be user-configurable or a plugin system, +> to handle more than just this one case. +> * Youtube-dl breaks from time to time, I really trust these urls a lot +> less than regular urls. Perhaps per-url trust levels are called for by +> this. +> +> --[[Joey]] + +> > There's a library for this called [quvi][] which supports many +> > different sites and also allows fetching the URL (as opposed to just +> > downloading the file). It seems to me this would be the best tool +> > for this task. One problem to consider here is that a single youtube +> > URL may yield different file contents depending on the quality +> > chosen. Also, it seems that the URLs guessed by quvi may be +> > ephemeral. --[[anarcat]] +> > +> > [quvi]: http://quvi.sourceforge.net/ diff --git a/doc/todo/wishlist:_special-case_handling_of_Youtube_URLs_in_Web_special_remote/comment_1_1a383c30df4fb1767f13d8c670b0c0b5._comment b/doc/todo/wishlist:_special-case_handling_of_Youtube_URLs_in_Web_special_remote/comment_1_1a383c30df4fb1767f13d8c670b0c0b5._comment new file mode 100644 index 0000000000..5569ff94a4 --- /dev/null +++ b/doc/todo/wishlist:_special-case_handling_of_Youtube_URLs_in_Web_special_remote/comment_1_1a383c30df4fb1767f13d8c670b0c0b5._comment @@ -0,0 +1,10 @@ +[[!comment format=mdwn + username="https://rmunn.myopenid.com/" + nickname="rmunn" + subject="comment 1" + date="2012-06-12T15:52:35Z" + content=""" +* One way to handle the configuration might be with regular expressions. If the URL matches regex A, handle it with downloader A' (with option set A''). If the URL matches regex B, handle it with downloader B' and option set B''. And so on. Then if nothing is matched, the default downloader is wget/curl. + +* In my experience, youtube-dl breakages are fixed relatively quickly; a much more serious problem from a trust standpoint is that Youtube videos often disappear. Sometimes due to a legitimate copyright claim, sometimes due to illegitimate copyright claims. (I've seen both happen). Or because the video uploader decided to upload *other* videos that violated copyright, and Youtube closed his/her account, thereby removing *all* his/her videos from the Web. Youtube is definitely an untrustworthy repository as far as \"the file will still be there later on\" is concerned. Perhaps a default trust relationship could go along with the regexes? URLs matching regex A are semitrusted, while URLs matching regex B are untrusted. +"""]] diff --git a/doc/todo/wishlist:_special_remote_Ubuntu_One.mdwn b/doc/todo/wishlist:_special_remote_Ubuntu_One.mdwn new file mode 100644 index 0000000000..b88a038eac --- /dev/null +++ b/doc/todo/wishlist:_special_remote_Ubuntu_One.mdwn @@ -0,0 +1 @@ +Special remote support for [Ubuntu One](http://one.ubuntu.com) would be nice. They're [using propietary but open protocol](https://wiki.ubuntu.com/UbuntuOne/TechnicalDetails#ubuntuone-storageprotocol) based on [Google Protocol Buffers](http://code.google.com/p/protobuf/). There's [protobuf for Haskell](http://code.google.com/p/protobuf-haskell/) so it should be possible to compile [the protocol file](http://bazaar.launchpad.net/~ubuntuone-control-tower/ubuntuone-storage-protocol/trunk/view/head:/ubuntuone/storageprotocol/protocol.proto) to Haskell code and then use that to implement the native Ubuntu special remote. 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 index 0000000000..d3c8556559 --- /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 = :
`` in ``.git/config``. --[[chrysn]] + +[[done]] diff --git a/doc/todo/wishlist:_special_remote_for_sftp_or_rsync/comment_1_6f07d9cc92cf8b4927b3a7d1820c9140._comment b/doc/todo/wishlist:_special_remote_for_sftp_or_rsync/comment_1_6f07d9cc92cf8b4927b3a7d1820c9140._comment new file mode 100644 index 0000000000..c513ed4008 --- /dev/null +++ b/doc/todo/wishlist:_special_remote_for_sftp_or_rsync/comment_1_6f07d9cc92cf8b4927b3a7d1820c9140._comment @@ -0,0 +1,10 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawkSq2FDpK2n66QRUxtqqdbyDuwgbQmUWus" + nickname="Jimmy" + subject="comment 1" + date="2011-04-28T07:47:38Z" + content=""" ++1 for a generic user configurable backend that a user can put shell commands in, which has a disclaimer such that if a user hangs themselves with misconfiguration then its their own fault :P + +I would love to be able to quickly plugin an irods/sector set of put/get/delete/stat(get info) commands into git-annex to access my private clouds which aren't s3 compatible. +"""]] diff --git a/doc/todo/wishlist:_special_remote_for_sftp_or_rsync/comment_2_84e4414c88ae91c048564a2cdc2d3250._comment b/doc/todo/wishlist:_special_remote_for_sftp_or_rsync/comment_2_84e4414c88ae91c048564a2cdc2d3250._comment new file mode 100644 index 0000000000..6243708f94 --- /dev/null +++ b/doc/todo/wishlist:_special_remote_for_sftp_or_rsync/comment_2_84e4414c88ae91c048564a2cdc2d3250._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="http://joey.kitenet.net/" + nickname="joey" + subject="comment 2" + date="2011-04-28T21:22:03Z" + content=""" +Ask and ye shalle receive with an Abbot on top: [[special_remotes/hook]] +"""]] diff --git a/doc/todo/wishlist:_special_remote_for_sftp_or_rsync/comment_3_79de7ac44e3c0f0f5691a56d3fb88897._comment b/doc/todo/wishlist:_special_remote_for_sftp_or_rsync/comment_3_79de7ac44e3c0f0f5691a56d3fb88897._comment new file mode 100644 index 0000000000..dc21ec4885 --- /dev/null +++ b/doc/todo/wishlist:_special_remote_for_sftp_or_rsync/comment_3_79de7ac44e3c0f0f5691a56d3fb88897._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawkSq2FDpK2n66QRUxtqqdbyDuwgbQmUWus" + nickname="Jimmy" + subject="comment 3" + date="2011-04-29T10:43:31Z" + content=""" +Cool!, I just tried adding tahoe-lafs as a remote, and it wasn't too hard. +"""]] diff --git a/doc/todo/wishlist:_special_remote_mega.co.nz.mdwn b/doc/todo/wishlist:_special_remote_mega.co.nz.mdwn new file mode 100644 index 0000000000..41164084a2 --- /dev/null +++ b/doc/todo/wishlist:_special_remote_mega.co.nz.mdwn @@ -0,0 +1,3 @@ +mega.co.nz has 50gb for free accounts. They also have an API, so I guess it wouldn't be too hard to use it as a special remote. + +[[done]], see [[tips/megaannex]]. diff --git a/doc/todo/wishlist:_special_remote_mega.co.nz/comment_2_6ca08ef808d4336fc42d0f279d6627b5._comment b/doc/todo/wishlist:_special_remote_mega.co.nz/comment_2_6ca08ef808d4336fc42d0f279d6627b5._comment new file mode 100644 index 0000000000..542b92a677 --- /dev/null +++ b/doc/todo/wishlist:_special_remote_mega.co.nz/comment_2_6ca08ef808d4336fc42d0f279d6627b5._comment @@ -0,0 +1,44 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawmLB39PC89rfGaA8SwrsnB6tbumezj-aC0" + nickname="Tobias" + subject="Usage of mega hook" + date="2013-05-21T09:09:28Z" + content=""" +megaannex +========= + +Hook program for gitannex to use mega.co.nz as backend + +# Requirements: + + requests>=0.10 + pycrypto + +Credit for the mega api interface goes to: https://github.com/richardasaurus/mega.py + +## Install +Clone the git repository in your home folder. + + git clone git://github.com/TobiasTheViking/megaannex.git + +This should make a ~/megannex folder + +## Setup +Run the program once to make an empty config file + + cd ~/megaannex; python2 megaannex.py + +Edit the megaannex.conf file. Add your mega.co.nz username and password + +Note: The folder option in the megaannex.conf file isn't yet used. + +## Commands for gitannex: + + git config annex.mega-store-hook '/usr/bin/python2 ~/megaannex/megaannex.py store --subject $ANNEX_KEY --file $ANNEX_FILE' + git config annex.mega-retrieve-hook '/usr/bin/python2 ~/megaannex/megaannex.py getfile --subject $ANNEX_KEY --file $ANNEX_FILE' + git config annex.mega-checkpresent-hook '/usr/bin/python2 ~/megaannex/megaannex.py fileexists --subject $ANNEX_KEY' + git config annex.mega-remove-hook '/usr/bin/python2 ~/megaannex/megaannex.py delete --subject $ANNEX_KEY' + git annex initremote mega type=hook hooktype=mega encryption=shared + git annex describe mega \"the mega.co.nz library\" + +"""]] diff --git a/doc/todo/wishlist:_support_copy_--from__61__x_--to__61__y.mdwn b/doc/todo/wishlist:_support_copy_--from__61__x_--to__61__y.mdwn new file mode 100644 index 0000000000..b4f966abb1 --- /dev/null +++ b/doc/todo/wishlist:_support_copy_--from__61__x_--to__61__y.mdwn @@ -0,0 +1,29 @@ +I'd like to be able to: + + git annex copy --from=x --to=y . + +Use case (true story) follows: + +My desktop hard drive was filling up. I dropped some large files which are also stored (via git-annex) on my backup drive. While these aren't irreplaceable files, I'd prefer to have at least two copies of everything I've decided I care enough about to archive. Later, I get a 2nd external drive, and I: + + git annex copy --to=new-external-drive . + +Fantastic! Now I've got everything that was important/useful enough to keep on my desktop backed up a 2nd time onto my new drive. + +But my new drive doesn't have a copy of any of the files I dropped from my desktop. I would like to be able to: + + git annex copy --from=old-external-drive --to=new-external-drive . + +on my desktop, and then my new drive would have a copy of everything, and my desktop drive would still have plenty of space (ie the files I'd dropped to make space would still not be stored on the desktop). + +The git repos on these external drives are both bare (as in ``git init --bare``) because they are used only for backups. Thus I operate on them only as remotes from my main (desktop) repo. + +> I have now implemented the --all option, and it's the default when +> running `git annex get` inside a bare repo. +> +> So, the solution is to `cd` to the repository on old-external-drive, +> and `git remote add newdrive /path/to/new/drive/repo`. Then run `git +> annex copy --all --to newdrive` and it'll move everything. +> +> Calling this [[done]] unless there are other use cases where the double +> copy method is really needed? --[[Joey]] diff --git a/doc/todo/wishlist:_support_copy_--from__61__x_--to__61__y/comment_1_cf8e0f16b723516374c95a93e4da42fc._comment b/doc/todo/wishlist:_support_copy_--from__61__x_--to__61__y/comment_1_cf8e0f16b723516374c95a93e4da42fc._comment new file mode 100644 index 0000000000..cee50a3450 --- /dev/null +++ b/doc/todo/wishlist:_support_copy_--from__61__x_--to__61__y/comment_1_cf8e0f16b723516374c95a93e4da42fc._comment @@ -0,0 +1,12 @@ +[[!comment format=mdwn + username="http://joeyh.name/" + ip="4.154.4.193" + subject="comment 1" + date="2013-06-30T17:43:50Z" + content=""" +A reasonable use case indeed. + +It seems to me that [[add_-all_option]] could also satisfy this use case, as then you could run `git annex get --all` in the new bare remote. + +That would have the benefit of not doing a double copy. +"""]] diff --git a/doc/todo/wishlist:_support_copy_--from__61__x_--to__61__y/comment_2_d35359c9dd4dd4365d9a7caf695ff833._comment b/doc/todo/wishlist:_support_copy_--from__61__x_--to__61__y/comment_2_d35359c9dd4dd4365d9a7caf695ff833._comment new file mode 100644 index 0000000000..8305679a32 --- /dev/null +++ b/doc/todo/wishlist:_support_copy_--from__61__x_--to__61__y/comment_2_d35359c9dd4dd4365d9a7caf695ff833._comment @@ -0,0 +1,16 @@ +[[!comment format=mdwn + username="http://jasonwoof.com/" + nickname="JasonWoof" + subject="thanks, good enough for now." + date="2013-07-15T19:27:58Z" + content=""" +the ``--all`` option works for this use case. That takes care of my problem. Thank you! + +I can imagine other use cases where I'd want ``--from`` and ``--to`` at once, such as: + +1. same situation with my two bare external drives, but I want to only copy my audio-book collection to the new drive, and not my movies. + +2. I've got a large online storage (eg rsync.net) and want copy everything from there onto my new external drive. + +I leave it up to your good judgement when/if this is worth doing. +"""]] diff --git a/doc/todo/wishlist:_support_for_more_ssh_urls_.mdwn b/doc/todo/wishlist:_support_for_more_ssh_urls_.mdwn new file mode 100644 index 0000000000..55b8120a75 --- /dev/null +++ b/doc/todo/wishlist:_support_for_more_ssh_urls_.mdwn @@ -0,0 +1,22 @@ +git-annex does not seem to support all kinds of urls that git does. + +Specifically, if I have ~/bar set up on host foo: + + [remote "foo"] + ## this one is not recognized as ssh url at all + # url = foo:bar + ## this one makes git-annex try to access '/~/bar' literally + # url = ssh://foo/~/bar + ## this one works + url = ssh://foo/home/tv/bar + +> scp-style is now supported. + +> `~` expansions (for the user's home, or other users) +> are somewhat tricky to support as they require running +> code on the remote to lookup homedirs. If git-annex grows a +> `git annex shell` that is run on the remote side +> (something I am [[considering|todo/git-annex-shell]] for other reasons), it +> could handle the expansions there. --[[Joey]] + +> Update: Now `~` expansions are supported. [[done]] diff --git a/doc/todo/wishlist:_swift_backend.mdwn b/doc/todo/wishlist:_swift_backend.mdwn new file mode 100644 index 0000000000..28bd265faf --- /dev/null +++ b/doc/todo/wishlist:_swift_backend.mdwn @@ -0,0 +1,5 @@ +[swift](http://swift.openstack.org/) is the object storage of Openstack. Think S3, but fully open source. As it's backed by rackspace.com, NASA, Dell and several other major players, adoption rates will explode. + +I can provide a test account soonish if need be, else rackspace.com if offering swift storage. Their API gateway lives at https://auth.api.rackspacecloud.com/v1.0 + +Richard diff --git a/doc/todo/wishlist:_swift_backend/comment_1_e6efbb35f61ee521b473a92674036788._comment b/doc/todo/wishlist:_swift_backend/comment_1_e6efbb35f61ee521b473a92674036788._comment new file mode 100644 index 0000000000..98a998c1cf --- /dev/null +++ b/doc/todo/wishlist:_swift_backend/comment_1_e6efbb35f61ee521b473a92674036788._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawkSq2FDpK2n66QRUxtqqdbyDuwgbQmUWus" + nickname="Jimmy" + subject="comment 1" + date="2011-05-14T10:04:36Z" + content=""" +I don't suppose this SWIFT api is compatible with the eucalytpus walrus api ? +"""]] diff --git a/doc/todo/wishlist:_swift_backend/comment_2_5d8c83b0485112e98367b7abaab3f4e3._comment b/doc/todo/wishlist:_swift_backend/comment_2_5d8c83b0485112e98367b7abaab3f4e3._comment new file mode 100644 index 0000000000..97863b095f --- /dev/null +++ b/doc/todo/wishlist:_swift_backend/comment_2_5d8c83b0485112e98367b7abaab3f4e3._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawl9sYlePmv1xK-VvjBdN-5doOa_Xw-jH4U" + nickname="Richard" + subject="comment 2" + date="2011-05-14T15:00:51Z" + content=""" +It does offer a S3 compability layer, but that is de facto non-functioning as of right now. +"""]] diff --git a/doc/todo/wishlist:_swift_backend/comment_3_bf8625b909c3a7321cae40e6f145e874._comment b/doc/todo/wishlist:_swift_backend/comment_3_bf8625b909c3a7321cae40e6f145e874._comment new file mode 100644 index 0000000000..90af10c41c --- /dev/null +++ b/doc/todo/wishlist:_swift_backend/comment_3_bf8625b909c3a7321cae40e6f145e874._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="http://ertai.myopenid.com/" + nickname="npouillard" + subject="+1" + date="2012-09-18T08:52:21Z" + content=""" +OVH (french IT company) is migrating its could storage infrastructure to swift/openstack and the next few weeks. +"""]] 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 index 0000000000..4b661101d7 --- /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:_vicfg_possible_repo_group_names.mdwn b/doc/todo/wishlist:_vicfg_possible_repo_group_names.mdwn new file mode 100644 index 0000000000..e30cc619af --- /dev/null +++ b/doc/todo/wishlist:_vicfg_possible_repo_group_names.mdwn @@ -0,0 +1,16 @@ +git annex vicfg should display valid repository group names + +For trust levels the possible values are displayed: + + # Repository trust configuration + # (Valid trust levels: trusted semitrusted untrusted dead) + ... + +The same is not currently done for repository groups + + # Repository groups + # (Separate group names with spaces) + +Thanks. + +> [[done]] --[[Joey]] diff --git a/doc/todo/wishlist:alias_system.mdwn b/doc/todo/wishlist:alias_system.mdwn new file mode 100644 index 0000000000..1f5012966e --- /dev/null +++ b/doc/todo/wishlist:alias_system.mdwn @@ -0,0 +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/transferring_data.mdwn b/doc/transferring_data.mdwn new file mode 100644 index 0000000000..d1ec5963f5 --- /dev/null +++ b/doc/transferring_data.mdwn @@ -0,0 +1,19 @@ +git-annex can transfer data to or from any of a repository's git remotes. +Depending on where the remote is, the data transfer is done using rsync +(over ssh or locally), or plain cp (with copy-on-write +optimisations on supported filesystems), or using curl (for repositories +on the web). Some [[special_remotes]] are also supported that are not +traditional git remotes. + +If a data transfer is interrupted, git-annex retains the partial transfer +to allow it to be automatically resumed later. + +It's equally easy to transfer a single file to or from a repository, +or to launch a retrievel of a massive pile of files from whatever +repositories they are scattered amongst. + +git-annex automatically uses whatever remotes are currently accessible, +preferring ones that are less expensive to talk to. + +[[!img repomap.png caption="A real-world repository interconnection map +(generated by git-annex map)"]] diff --git a/doc/trust.mdwn b/doc/trust.mdwn new file mode 100644 index 0000000000..1fd47fd1d3 --- /dev/null +++ b/doc/trust.mdwn @@ -0,0 +1,59 @@ +Git-annex supports several levels of trust of a repository: + +* semitrusted (default) +* untrusted +* trusted +* dead + +## semitrusted + +Normally, git-annex does not fully trust its stored [[location_tracking]] +information. When removing content, it will directly check +that other repositories have enough [[copies]]. + +Generally that explicit checking is a good idea. Consider that the current +[[location_tracking]] information for a remote may not yet have propagated +out. Or, a remote may have suffered a catastrophic loss of data, or itself +been lost. + +There is still some trust involved here. A semitrusted repository is +depended on to retain a copy of the file content; possibly the only +[[copy|copies]]. + +(Being semitrusted is the default. The `git annex semitrust` command +restores a repository to this default, when it has been overridden. +The `--semitrust` option can temporarily restore a repository to this +default.) + +## untrusted + +An untrusted repository is not trusted to retain data at all. Git-annex +will retain sufficient [[copies]] of data elsewhere. + +This is a good choice for eg, portable drives that could get lost. Or, +if a disk is known to be dying, you can set it to untrusted and let +`git annex fsck` warn about data that needs to be copied off it. + +To configure a repository as untrusted, use the `git annex untrust` +command. + +## trusted + +Sometimes, you may have reasons to fully trust the location tracking +information for a repository. For example, it may be an offline +archival drive, from which you rarely or never remove content. Deciding +when it makes sense to trust the tracking info is up to you. + +One way to handle this is just to use `--force` when a command cannot +access a remote you trust. Or to use `--trust` to specify a repisitory to +trust temporarily. + +To configure a repository as fully and permanently trusted, +use the `git annex trust` command. + +## dead + +This is used to indicate that you have no trust that the repository +exists at all. It's appropriate to use when a drive has been lost, +or a directory irretrevably deleted. It will make git-annex avoid +even showing the repository as a place where data might still reside. diff --git a/doc/upgrades.mdwn b/doc/upgrades.mdwn new file mode 100644 index 0000000000..6cf54477cc --- /dev/null +++ b/doc/upgrades.mdwn @@ -0,0 +1,98 @@ +Occasionally improvments are made to how git-annex stores its data, +that require an upgrade process to convert repositories made with an older +version to be used by a newer version. It's annoying, it should happen +rarely, but sometimes, it's worth it. + +There's a committment that git-annex will always support upgrades from all +past versions. After all, you may have offline drives from an earlier +git-annex, and might want to use them with a newer git-annex. + +git-annex will notice if it is run in a repository that +needs an upgrade, and refuse to do anything. To upgrade, +use the "git annex upgrade" command. + +The upgrade process is guaranteed to be conflict-free. Unless you +already have git conflicts in your repository or between repositories. +Upgrading a repository with conflicts is not recommended; resolve the +conflicts first before upgrading git-annex. + +## Upgrade events, so far + +### v3 -> v4 (git-annex version 4.x) + +v4 is only used for [[direct_mode]], and no upgrade needs to be done from +existing v3 repositories, they will continue to work. + +### v2 -> v3 (git-annex version 3.x) + +Involved moving the .git-annex/ directory into a separate git-annex branch. + +After this upgrade, you should make sure you include the git-annex branch +when git pushing and pulling. + +### tips for this upgrade + +This upgrade is easier (and faster!) than the previous upgrades. +You don't need to upgrade every repository at once; it's sufficient +to upgrade each repository only when you next use it. + +Example upgrade process: + + cd localrepo + git pull + git annex upgrade + git commit -m "upgrade v2 to v3" + git gc + +### v1 -> v2 (git-annex version 0.20110316) + +Involved adding hashing to .git/annex/ and changing the names of all keys. +Symlinks changed. + +Also, hashing was added to location log files in .git-annex/. +And .gitattributes needed to have another line added to it. + +Previously, files added to the SHA [[backends]] did not have their file +size tracked, while files added to the WORM backend did. Files added to +the SHA backends after the conversion will have their file size tracked, +and that information will be used by git-annex for disk free space checking. +To ensure that information is available for all your annexed files, see +[[upgrades/SHA_size]]. + +### tips for this upgrade + +This upgrade can tend to take a while, if you have a lot of files. + +Each clone of a repository should be individually upgraded. +Until a repository's remotes have been upgraded, git-annex +will refuse to communicate with them. + +Start by upgrading one repository, and then you can commit +the changes git-annex staged during upgrade, and push them out to other +repositories. And then upgrade those other repositories. Doing it this +way avoids git-annex doing some duplicate work during the upgrade. + +Example upgrade process: + + cd localrepo + git pull + git annex upgrade + git commit -m "upgrade v1 to v2" + git push + + ssh remote + cd remoterepo + git pull + git annex upgrade + ... + +### v0 -> v1 (git-annex version 0.04) + +Involved a reorganisation of the layout of .git/annex/. Symlinks changed. + +Handled more or less transparently, although git-annex was just 2 weeks +old at the time, and had few users other than Joey. + +Before doing this upgrade, set annex.version: + + git config annex.version 0 diff --git a/doc/upgrades/SHA_size.mdwn b/doc/upgrades/SHA_size.mdwn new file mode 100644 index 0000000000..97603ba913 --- /dev/null +++ b/doc/upgrades/SHA_size.mdwn @@ -0,0 +1,20 @@ +Before version 2 of the git-annex repository, files added to the SHA +[[backends]] did not have their file size tracked, while files added to the +WORM backend did. The file size information is used for disk free space +checking. + +Files added to the SHA backends after the conversion will have their file +size tracked automatically. This disk free space checking is an optional +feature and since you're more likely to be using more recently added files, +you're unlikely to see any bad effect if you do nothing. + +That said, if you have old files added to SHA backends that lack file size +tracking info, here's how you can add that info. After [[upgrading|upgrades]] +to repository version 2, in each repository run: + + git annex migrate + git commit -m 'migrated keys for v2' + +The usual caveats about [[tips/migrating_data_to_a_new_backend]] +apply; you will end up with unused keys that you can later clean up with +`git annex unused`. diff --git a/doc/upgrades/SHA_size/comment_1_20f9b7b75786075de666b2146dc13a60._comment b/doc/upgrades/SHA_size/comment_1_20f9b7b75786075de666b2146dc13a60._comment new file mode 100644 index 0000000000..7b6be15321 --- /dev/null +++ b/doc/upgrades/SHA_size/comment_1_20f9b7b75786075de666b2146dc13a60._comment @@ -0,0 +1,12 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawkjvjLHW9Omza7x1VEzIFQ8Z5honhRB90I" + nickname="Asheesh" + subject="The fact that the keys changed causes merge conflicts" + date="2012-06-25T00:28:59Z" + content=""" +FYI, I have run into a problem where if you 'git annex sync' between various 'git annex v3' repositories, if the different repositories are using different encodings of the SHA1 information (one including size, one not), then the 'git merge' will declare that they conflict. + +There's no indication that 'git annex migrate' is the right tool to run, except from perusing the 'git annex' man page. In my opinion this is a major user interface problem. + +-- Asheesh. +"""]] diff --git a/doc/use_case/Alice.mdwn b/doc/use_case/Alice.mdwn new file mode 100644 index 0000000000..cdd3ea546d --- /dev/null +++ b/doc/use_case/Alice.mdwn @@ -0,0 +1,24 @@ +### use case: The Nomad + +Alice is always on the move, often with her trusty netbook and a small +handheld terabyte USB drive, or a smaller USB keydrive. She has a server +out there on the net. She stores data, encrypted in the Cloud. + +All these things can have different files on them, but Alice no longer +has to deal with the tedious process of keeping them manually in sync, +or remembering where she put a file. git-annex manages all these data +sources as if they were git remotes. +[[more about special remotes|special_remotes]] + +When she has 1 bar on her cell, Alice queues up interesting files on her +server for later. At a coffee shop, she has git-annex download them to her +USB drive. High in the sky or in a remote cabin, she catches up on +podcasts, videos, and games, first letting git-annex copy them from +her USB drive to the netbook (this saves battery power). +[[more about transferring data|transferring_data]] + +When she's done, she tells git-annex which to keep and which to remove. +They're all removed from her netbook to save space, and Alice knows +that next time she syncs up to the net, her changes will be synced back +to her server. +[[more about distributed version control|distributed_version_control]] diff --git a/doc/use_case/Bob.mdwn b/doc/use_case/Bob.mdwn new file mode 100644 index 0000000000..42d10ea975 --- /dev/null +++ b/doc/use_case/Bob.mdwn @@ -0,0 +1,25 @@ +### use case: The Archivist + +Bob has many drives to archive his data, most of them kept offline, in a +safe place. + +With git-annex, Bob has a single directory tree that includes all +his files, even if their content is being stored offline. He can +reorganize his files using that tree, committing new versions to git, +without worry about accidentally deleting anything. + +When Bob needs access to some files, git-annex can tell him which drive(s) +they're on, and easily make them available. Indeed, every drive knows what +is on every other drive. +[[more about location tracking|location_tracking]] + +Bob thinks long-term, and so he appreciates that git-annex uses a simple +repository format. He knows his files will be accessible in the future +even if the world has forgotten about git-annex and git. +[[more about future-proofing|future_proofing]] + +Run in a cron job, git-annex adds new files to archival drives at night. It +also helps Bob keep track of intentional, and unintentional copies of +files, and logs information he can use to decide when it's time to duplicate +the content of old drives. +[[more about backup copies|copies]] diff --git a/doc/users.mdwn b/doc/users.mdwn new file mode 100644 index 0000000000..b9bab48ecf --- /dev/null +++ b/doc/users.mdwn @@ -0,0 +1,9 @@ +Users of this wiki, feel free to create a subpage of this one and talk +about yourself on it, within reason. You can link to it to sign your +comments. + +List of users +============= +[[!inline pages="users/* and !users/*/* and !*/Discussion" +feeds=no archive=yes sort=title template=titlepage +rootpage="users" postformtext="Add yourself as an git-annex user:"]] diff --git a/doc/users/chrysn.mdwn b/doc/users/chrysn.mdwn new file mode 100644 index 0000000000..f5c07b88b3 --- /dev/null +++ b/doc/users/chrysn.mdwn @@ -0,0 +1,5 @@ +* **name**: chrysn +* **website**: +* **uses git-annex for**: managing the family's photos (and possibly videos and music in the future) +* **likes git-annex because**: it adds a layer of commit semantics over a regular file system without keeping everything in duplicate locally +* **would like git-annex to**: not be required any more as git itself learns to use cow filesystems to avoid abundant disk usage and gets better with sparser checkouts (git-annex might then still be a simpler tool that watches over what can be safely dropped for a sparser checkout) diff --git a/doc/users/fmarier.mdwn b/doc/users/fmarier.mdwn new file mode 100644 index 0000000000..d04b6968df --- /dev/null +++ b/doc/users/fmarier.mdwn @@ -0,0 +1,6 @@ +# François Marier + +Free Software and Debian Developer. Lead developer of [Libravatar](https://www.libravatar.org) + +* [Blog](http://feeding.cloud.geek.nz) and [homepage](http://fmarier.org) +* [Identica](http://identi.ca/fmarier) / [Twitter](https://twitter.com/fmarier) diff --git a/doc/users/gebi.mdwn b/doc/users/gebi.mdwn new file mode 100644 index 0000000000..121bedbdd7 --- /dev/null +++ b/doc/users/gebi.mdwn @@ -0,0 +1 @@ +Michael Gebetsroither diff --git a/doc/users/joey.mdwn b/doc/users/joey.mdwn new file mode 100644 index 0000000000..306e1cc768 --- /dev/null +++ b/doc/users/joey.mdwn @@ -0,0 +1,2 @@ +Joey Hess + diff --git a/doc/videos.mdwn b/doc/videos.mdwn new file mode 100644 index 0000000000..f1adeeac40 --- /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 index 0000000000..30d1e37d51 --- /dev/null +++ b/doc/videos/FOSDEM2012.mdwn @@ -0,0 +1,7 @@ +
+A
15 minute introduction to git-annex, +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 index 0000000000..0f29ce0523 --- /dev/null +++ b/doc/videos/LCA2013.mdwn @@ -0,0 +1,8 @@ +
+A 45 minute talk and demo of git-annex and the assistant), 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 index 0000000000..7e891c2f77 --- /dev/null +++ b/doc/videos/git-annex_assistant_archiving.mdwn @@ -0,0 +1,5 @@ +
+A 9 minute screencast +covering archiving your files with the [[git-annex assistant|/assistant]]. diff --git a/doc/videos/git-annex_assistant_introduction.mdwn b/doc/videos/git-annex_assistant_introduction.mdwn new file mode 100644 index 0000000000..93f9df1bad --- /dev/null +++ b/doc/videos/git-annex_assistant_introduction.mdwn @@ -0,0 +1,5 @@ +
+A 8 minute screencast +introducing the [[git-annex assistant|/assistant]]. diff --git a/doc/videos/git-annex_assistant_introduction/comment_1_f42ad4183c2c28319d3705a82fceb82f._comment b/doc/videos/git-annex_assistant_introduction/comment_1_f42ad4183c2c28319d3705a82fceb82f._comment new file mode 100644 index 0000000000..c969bacbcb --- /dev/null +++ b/doc/videos/git-annex_assistant_introduction/comment_1_f42ad4183c2c28319d3705a82fceb82f._comment @@ -0,0 +1,15 @@ +[[!comment format=mdwn + username="modules" + ip="85.16.227.39" + subject="Great screencast" + date="2013-03-16T14:16:37Z" + content=""" +I am starting to understand the concept :) Thank you. + +The assistant is working perfect with local repos and removable drives but i have problems to setup a remote repos. On a fresh debian server with base setup and rsync installed and ssh-keys for login. I got a green \"Scanned 93.xxx.xx.xxx_annex\" message on dashboard, but there is never a finished transfer. Its \"queued\" on all files and nothing seems to happens on clicking the \"play\" button behind files (x-ing files removes them from dashboard). Also i saw there is a second option in your screencast for remote servers (\"Use a git reposoitory on the server\") which does not show up on my side. Do i need to setup git-annex on server for second option? (with git-annex version 4.20130314). + + + + + +"""]] diff --git a/doc/videos/git-annex_assistant_introduction/comment_2_b62f4eeeac1138570f7cb8c98d41c2cb._comment b/doc/videos/git-annex_assistant_introduction/comment_2_b62f4eeeac1138570f7cb8c98d41c2cb._comment new file mode 100644 index 0000000000..8b6d2a1e95 --- /dev/null +++ b/doc/videos/git-annex_assistant_introduction/comment_2_b62f4eeeac1138570f7cb8c98d41c2cb._comment @@ -0,0 +1,12 @@ +[[!comment format=mdwn + username="http://joeyh.name/" + nickname="joey" + subject="comment 2" + date="2013-03-16T16:04:30Z" + content=""" +@modules: Yes, you're only given the option to use a git repository on the server if it has both git and git-annex installed. You can install any version of git-annex there. For example, Debian stable ships with one that will work. +I plan to make that screen clearer when the git repository option is not available. + +I'm not sure what'd going on with your rsync transfers not running. I can say that if the \"play\" icon is visible, the transfer has been paused. While a transfer is running, the \"pause\" icon is visible instead, to let you pause it. +This may be as simple as you having misunderstood the icons and paused the currently running transfer, which prevents any transfers from running. If not, suggest you enable debug logs in the Preferences page, and consider filing a [[bug_report|bugs]]. +"""]] diff --git a/doc/videos/git-annex_assistant_remote_sharing.mdwn b/doc/videos/git-annex_assistant_remote_sharing.mdwn new file mode 100644 index 0000000000..6d9a97e8ef --- /dev/null +++ b/doc/videos/git-annex_assistant_remote_sharing.mdwn @@ -0,0 +1,6 @@ +
+A 6 minute screencast +showing how to share files between your computers in different locations, +such as home and work. diff --git a/doc/videos/git-annex_assistant_sync_demo.mdwn b/doc/videos/git-annex_assistant_sync_demo.mdwn new file mode 100644 index 0000000000..2df2a3a91a --- /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](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 index 0000000000..3909f73b5b --- /dev/null +++ b/doc/videos/git-annex_watch_demo.mdwn @@ -0,0 +1,7 @@ +A quick screencast demoing the `git annex watch` daemon. + + + +[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 index 0000000000..b982d32fd4 --- /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](http://joeyh.name/screencasts/git-annex-webapp.ogg) + +[[!meta date="Sun Jul 29 14:41:41 2012 -0400"]] diff --git a/doc/walkthrough.mdwn b/doc/walkthrough.mdwn new file mode 100644 index 0000000000..f401524f51 --- /dev/null +++ b/doc/walkthrough.mdwn @@ -0,0 +1,25 @@ +A walkthrough of the basic features of git-annex. + +[[!toc]] + +[[!inline feeds=no trail=yes show=0 template=walkthrough pagenames=""" + walkthrough/creating_a_repository + walkthrough/adding_a_remote + walkthrough/adding_files + walkthrough/renaming_files + walkthrough/getting_file_content + walkthrough/syncing + walkthrough/transferring_files:_When_things_go_wrong + walkthrough/removing_files + walkthrough/removing_files:_When_things_go_wrong + walkthrough/modifying_annexed_files + walkthrough/using_ssh_remotes + walkthrough/moving_file_content_between_repositories + walkthrough/using_tags_and_branches + walkthrough/unused_data + walkthrough/fsck:_verifying_your_data + walkthrough/fsck:_when_things_go_wrong + walkthrough/backups + walkthrough/automatically_managing_content + walkthrough/more +"""]] diff --git a/doc/walkthrough/adding_a_remote.mdwn b/doc/walkthrough/adding_a_remote.mdwn new file mode 100644 index 0000000000..97690dfcdf --- /dev/null +++ b/doc/walkthrough/adding_a_remote.mdwn @@ -0,0 +1,19 @@ +Like any other git repository, git-annex repositories have remotes. +Let's start by adding a USB drive as a remote. + + # sudo mount /media/usb + # cd /media/usb + # git clone ~/annex + # cd annex + # git annex init "portable USB drive" + # git remote add laptop ~/annex + # cd ~/annex + # git remote add usbdrive /media/usb/annex + +This is all standard ad-hoc distributed git repository setup. +The only git-annex specific part is telling it the name +of the new repository created on the USB drive. + +Notice that both repos are set up as remotes of one another. This lets +either get annexed files from the other. You'll want to do that even +if you are using git in a more centralized fashion. diff --git a/doc/walkthrough/adding_a_remote/comment_1_0a59355bd33a796aec97173607e6adc9._comment b/doc/walkthrough/adding_a_remote/comment_1_0a59355bd33a796aec97173607e6adc9._comment new file mode 100644 index 0000000000..4b0b9c0fd2 --- /dev/null +++ b/doc/walkthrough/adding_a_remote/comment_1_0a59355bd33a796aec97173607e6adc9._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawl9sYlePmv1xK-VvjBdN-5doOa_Xw-jH4U" + nickname="Richard" + subject="comment 1" + date="2011-03-19T01:18:49Z" + content=""" +After doing the above with two required copy per file, `git annex fsck` complained that I had only one copy per file even though I had created my clone, already. Once I `git pull`ed from the second repo, not getting any changes for obvious reasons, `git annex fsck` was happy. So I am not sure how my addition was incorrect. -- RichiH +"""]] diff --git a/doc/walkthrough/adding_a_remote/comment_2_f8cd79ef1593a8181a7f1086a87713e8._comment b/doc/walkthrough/adding_a_remote/comment_2_f8cd79ef1593a8181a7f1086a87713e8._comment new file mode 100644 index 0000000000..015417a4f7 --- /dev/null +++ b/doc/walkthrough/adding_a_remote/comment_2_f8cd79ef1593a8181a7f1086a87713e8._comment @@ -0,0 +1,9 @@ +[[!comment format=mdwn + username="http://joey.kitenet.net/" + nickname="joey" + subject="comment 2" + date="2011-03-19T15:35:38Z" + content=""" +Yes, you have to pull down location tracking information in order for fsck to be satisfied in that situation. But since this is a walkthrough, and neither fsck or numcopies settings are mentioned until later, it's ok for this pull to be described a few steps along in [[getting file content]]. + +"""]] diff --git a/doc/walkthrough/adding_a_remote/comment_3_60691af4400521b5a8c8d75efe3b44cb._comment b/doc/walkthrough/adding_a_remote/comment_3_60691af4400521b5a8c8d75efe3b44cb._comment new file mode 100644 index 0000000000..9280f2dccf --- /dev/null +++ b/doc/walkthrough/adding_a_remote/comment_3_60691af4400521b5a8c8d75efe3b44cb._comment @@ -0,0 +1,9 @@ +[[!comment format=mdwn + username="http://dieter-be.myopenid.com/" + nickname="dieter" + subject="comment 3" + date="2011-04-02T20:24:33Z" + content=""" + * why the `git remote add laptop ~/annex` ? this remote already exists under the name origin. + * doesn't the last command need to be `git remote add usbdrive /media/usb/annex`? because the actual repo would be in /media/usb/annex, not /media/usb? +"""]] diff --git a/doc/walkthrough/adding_a_remote/comment_4_6f7cf5c330272c96b3abeb6612075c9d._comment b/doc/walkthrough/adding_a_remote/comment_4_6f7cf5c330272c96b3abeb6612075c9d._comment new file mode 100644 index 0000000000..b4dcb6422a --- /dev/null +++ b/doc/walkthrough/adding_a_remote/comment_4_6f7cf5c330272c96b3abeb6612075c9d._comment @@ -0,0 +1,10 @@ +[[!comment format=mdwn + username="http://joey.kitenet.net/" + nickname="joey" + subject="comment 4" + date="2011-04-03T02:32:17Z" + content=""" +Good spotting on the last line, fixed. + +The laptop remote is indeed redundant, but it leads to clearer views of what is going on later in the walkthrough (\"git pull laptop master\", \"(copying from laptop...)\"). And if the original clone is made from a central bare repo, this reinforces that you'll want to set up remotes for other repos on the computer. +"""]] diff --git a/doc/walkthrough/adding_files.mdwn b/doc/walkthrough/adding_files.mdwn new file mode 100644 index 0000000000..d1b5a04f77 --- /dev/null +++ b/doc/walkthrough/adding_files.mdwn @@ -0,0 +1,11 @@ + # cd ~/annex + # cp /tmp/big_file . + # cp /tmp/debian.iso . + # git annex add . + add big_file (checksum...) ok + add debian.iso (checksum...) ok + # git commit -a -m added + +When you add a file to the annex and commit it, only a symlink to +the annexed content is committed. The content itself is stored in +git-annex's backend. diff --git a/doc/walkthrough/automatically_managing_content.mdwn b/doc/walkthrough/automatically_managing_content.mdwn new file mode 100644 index 0000000000..0080ebcb5e --- /dev/null +++ b/doc/walkthrough/automatically_managing_content.mdwn @@ -0,0 +1,45 @@ +Once you have multiple repositories, and have perhaps configured numcopies, +any given file can have many more copies than is needed, or perhaps fewer +than you would like. How to manage this? + +The whereis subcommand can be used to see how many copies of a file are known, +but then you have to decide what to get or drop. In this example, there +are perhaps not enough copies of the first file, and too many of the second +file. + + # cd /media/usbdrive + # git annex whereis + whereis my_cool_big_file (1 copy) + 0c443de8-e644-11df-acbf-f7cd7ca6210d -- laptop + whereis other_file (3 copies) + 0c443de8-e644-11df-acbf-f7cd7ca6210d -- laptop + 62b39bbe-4149-11e0-af01-bb89245a1e61 -- here (usb drive) + 7570b02e-15e9-11e0-adf0-9f3f94cb2eaa -- backup drive + +What would be handy is some automated versions of get and drop, that only +gets a file if there are not yet enough copies of it, or only drops a file +if there are too many copies. Well, these exist, just use the --auto option. + + # git annex get --auto --numcopies=2 + get my_cool_big_file (from laptop...) ok + # git annex drop --auto --numcopies=2 + drop other_file ok + +With two quick commands, git-annex was able to decide for you how to +work toward having two copies of your files. + + # git annex whereis + whereis my_cool_big_file (2 copies) + 0c443de8-e644-11df-acbf-f7cd7ca6210d -- laptop + 62b39bbe-4149-11e0-af01-bb89245a1e61 -- here (usb drive) + whereis other_file (2 copies) + 0c443de8-e644-11df-acbf-f7cd7ca6210d -- laptop + 7570b02e-15e9-11e0-adf0-9f3f94cb2eaa -- backup drive + +The --auto option can also be used with the copy command, +again this lets git-annex decide whether to actually copy content. + +The above shows how to use --auto to manage content based on the number +of copies. It's also possible to configure, on a per-repository basis, +which content is desired. Then --auto also takes that into account +see [[preferred_content]] for details. diff --git a/doc/walkthrough/backups.mdwn b/doc/walkthrough/backups.mdwn new file mode 100644 index 0000000000..9723022b4c --- /dev/null +++ b/doc/walkthrough/backups.mdwn @@ -0,0 +1,25 @@ +git-annex can be configured to require more than one copy of a file exists, +as a simple backup for your data. This is controlled by the "annex.numcopies" +setting, which defaults to 1 copy. Let's change that to require 2 copies, +and send a copy of every file to a USB drive. + + # echo "* annex.numcopies=2" >> .gitattributes + # git annex copy . --to usbdrive + +Now when we try to `git annex drop` a file, it will verify that it +knows of 2 other repositories that have a copy before removing its +content from the current repository. + +You can also vary the number of copies needed, depending on the file name. +So, if you want 3 copies of all your flac files, but only 1 copy of oggs: + + # echo "*.ogg annex.numcopies=1" >> .gitattributes + # echo "*.flac annex.numcopies=3" >> .gitattributes + +Or, you might want to make a directory for important stuff, and configure +it so anything put in there is backed up more thoroughly: + + # mkdir important_stuff + # echo "* annex.numcopies=3" > important_stuff/.gitattributes + +For more details about the numcopies setting, see [[copies]]. diff --git a/doc/walkthrough/creating_a_repository.mdwn b/doc/walkthrough/creating_a_repository.mdwn new file mode 100644 index 0000000000..51ff1c72b3 --- /dev/null +++ b/doc/walkthrough/creating_a_repository.mdwn @@ -0,0 +1,6 @@ +This is very straightforward. Just tell it a description of the repository. + + # mkdir ~/annex + # cd ~/annex + # git init + # git annex init "my laptop" diff --git a/doc/walkthrough/fsck:_verifying_your_data.mdwn b/doc/walkthrough/fsck:_verifying_your_data.mdwn new file mode 100644 index 0000000000..d036332fb3 --- /dev/null +++ b/doc/walkthrough/fsck:_verifying_your_data.mdwn @@ -0,0 +1,16 @@ +You can use the fsck subcommand to check for problems in your data. What +can be checked depends on the key-value [[backend|backends]] you've used +for the data. For example, when you use the SHA1 backend, fsck will verify +that the checksums of your files are good. Fsck also checks that the +annex.numcopies setting is satisfied for all files. + + # git annex fsck + fsck some_file (checksum...) ok + fsck my_cool_big_file (checksum...) ok + ... + +You can also specify the files to check. This is particularly useful if +you're using sha1 and don't want to spend a long time checksumming everything. + + # git annex fsck my_cool_big_file + fsck my_cool_big_file (checksum...) ok diff --git a/doc/walkthrough/fsck:_when_things_go_wrong.mdwn b/doc/walkthrough/fsck:_when_things_go_wrong.mdwn new file mode 100644 index 0000000000..85d9f20fe0 --- /dev/null +++ b/doc/walkthrough/fsck:_when_things_go_wrong.mdwn @@ -0,0 +1,13 @@ +Fsck never deletes possibly bad data; instead it will be moved to +`.git/annex/bad/` for you to recover. Here is a sample of what fsck +might say about a badly messed up annex: + + # git annex fsck + fsck my_cool_big_file (checksum...) + git-annex: Bad file content; moved to .git/annex/bad/SHA1:7da006579dd64330eb2456001fd01948430572f2 + git-annex: ** No known copies exist of my_cool_big_file + failed + fsck important_file + git-annex: Only 1 of 2 copies exist. Run git annex get somewhere else to back it up. + failed + git-annex: 2 failed diff --git a/doc/walkthrough/getting_file_content.mdwn b/doc/walkthrough/getting_file_content.mdwn new file mode 100644 index 0000000000..f41e17770a --- /dev/null +++ b/doc/walkthrough/getting_file_content.mdwn @@ -0,0 +1,12 @@ +A repository does not always have all annexed file contents available. +When you need the content of a file, you can use "git annex get" to +make it available. + +We can use this to copy everything in the laptop's annex to the +USB drive. + + # cd /media/usb/annex + # git fetch laptop; git merge laptop/master + # git annex get . + get my_cool_big_file (from laptop...) ok + get iso/debian.iso (from laptop...) ok diff --git a/doc/walkthrough/modifying_annexed_files.mdwn b/doc/walkthrough/modifying_annexed_files.mdwn new file mode 100644 index 0000000000..693eae9447 --- /dev/null +++ b/doc/walkthrough/modifying_annexed_files.mdwn @@ -0,0 +1,44 @@ +Normally, the content of files in the annex is prevented from being modified. +(Unless your repository is using [[direct_mode]].) + +That's a good thing, because it might be the only copy, you wouldn't +want to lose it in a fumblefingered mistake. + + # echo oops > my_cool_big_file + bash: my_cool_big_file: Permission denied + +In order to modify a file, it should first be unlocked. + + # git annex unlock my_cool_big_file + unlock my_cool_big_file (copying...) ok + +That replaces the symlink that normally points at its content with a copy +of the content. You can then modify the file like any regular file. Because +it is a regular file. + +(If you decide you don't need to modify the file after all, or want to discard +modifications, just use `git annex lock`.) + +When you `git commit`, git-annex's pre-commit hook will automatically +notice that you are committing an unlocked file, and add its new content +to the annex. The file will be replaced with a symlink to the new content, +and this symlink is what gets committed to git in the end. + + # echo "now smaller, but even cooler" > my_cool_big_file + # git commit my_cool_big_file -m "changed an annexed file" + add my_cool_big_file ok + [master 64cda67] changed an annexed file + 1 files changed, 1 insertions(+), 1 deletions(-) + +There is one problem with using `git commit` like this: Git wants to first +stage the entire contents of the file in its index. That can be slow for +big files (sorta why git-annex exists in the first place). So, the +automatic handling on commit is a nice safety feature, since it prevents +the file content being accidentally committed into git. But when working with +big files, it's faster to explicitly add them to the annex yourself +before committing. + + # echo "now smaller, but even cooler yet" > my_cool_big_file + # git annex add my_cool_big_file + add my_cool_big_file ok + # git commit my_cool_big_file -m "changed an annexed file" diff --git a/doc/walkthrough/modifying_annexed_files/comment_1_624b4a0b521b553d68ab6049f7dbaf8c._comment b/doc/walkthrough/modifying_annexed_files/comment_1_624b4a0b521b553d68ab6049f7dbaf8c._comment new file mode 100644 index 0000000000..5fc26496e0 --- /dev/null +++ b/doc/walkthrough/modifying_annexed_files/comment_1_624b4a0b521b553d68ab6049f7dbaf8c._comment @@ -0,0 +1,14 @@ +[[!comment format=mdwn + username="http://lj.rossia.org/users/imz/" + ip="79.165.56.162" + subject="sorta why git-annex exists in the first place -- not only the slow index " + date="2012-09-25T22:04:01Z" + content=""" +> Git wants to first stage the entire contents of the file in its index. That can be slow for big files (sorta why git-annex exists in the first place) + +I think that git-annex's usefulness is not only because of the Git's index overhead: I like its idea because it will help track the copies in the \"[[special remotes]]\", which are not Git because + +* they are either not under my control (e.g., web URLs), +* or because it's not convenient to hold a Git repo there (an external disk/DVD with files can be viewed easily by a human, but imposing a Git repo structure on it would either at least double the consume space: for the history of the commits and for the working dir, or make it \"unreadable\" for a human, if it is a bare repo); +* or because it's nearly impossible to put a Git repo on a storage like peer networks without special tools. +"""]] diff --git a/doc/walkthrough/modifying_annexed_files/comment_2_b000622304535d32b69db17d51156b21._comment b/doc/walkthrough/modifying_annexed_files/comment_2_b000622304535d32b69db17d51156b21._comment new file mode 100644 index 0000000000..6a2d2c2a60 --- /dev/null +++ b/doc/walkthrough/modifying_annexed_files/comment_2_b000622304535d32b69db17d51156b21._comment @@ -0,0 +1,10 @@ +[[!comment format=mdwn + username="http://yarikoptic.myopenid.com/" + nickname="site-myopenid" + subject="feature request: unlock --drop" + date="2013-05-30T20:26:52Z" + content=""" +ATM unlock copies original file for modifications, so that original copy remains under annex and possibly to-be-edited copy created. +But if I am \"unlock\"ing file I might simply not be interested in a previous copy and want to maintain only a single (possibly edited) new copy. +What if there was a mode where the actual file is simply moved into \"unlocked\" location for editing, thus effectively dropping the actual content from git annex. That would allow to not inquire lengthy copying/wasting local space. If then I would need a previous copy I would just \"get\" it again. +"""]] diff --git a/doc/walkthrough/more.mdwn b/doc/walkthrough/more.mdwn new file mode 100644 index 0000000000..0a4a5b94e8 --- /dev/null +++ b/doc/walkthrough/more.mdwn @@ -0,0 +1,3 @@ +So ends the walkthrough. By now you should be able to use git-annex. + +Want more? See [[tips]] for lots more features and advice. diff --git a/doc/walkthrough/moving_file_content_between_repositories.mdwn b/doc/walkthrough/moving_file_content_between_repositories.mdwn new file mode 100644 index 0000000000..3ffcc11750 --- /dev/null +++ b/doc/walkthrough/moving_file_content_between_repositories.mdwn @@ -0,0 +1,13 @@ +Often you will want to move some file contents from a repository to some +other one. For example, your laptop's disk is getting full; time to move +some files to an external disk before moving another file from a file +server to your laptop. Doing that by hand (by using `git annex get` and +`git annex drop`) is possible, but a bit of a pain. `git annex move` +makes it very easy. + + # git annex move my_cool_big_file --to usbdrive + move my_cool_big_file (to usbdrive...) ok + # git annex move video/hackity_hack_and_kaxxt.mov --from fileserver + move video/hackity_hack_and_kaxxt.mov (from fileserver...) + SHA256-s86050597--6ae2688bc533437766a48aa19f2c06be14d1bab9c70b468af445d4f07b65f41e 100% 82MB 199.1KB/s 07:02 + ok diff --git a/doc/walkthrough/moving_file_content_between_repositories/comment_1_4c30ade91fc7113a95960aa3bd1d5427._comment b/doc/walkthrough/moving_file_content_between_repositories/comment_1_4c30ade91fc7113a95960aa3bd1d5427._comment new file mode 100644 index 0000000000..b3dc8fe7a2 --- /dev/null +++ b/doc/walkthrough/moving_file_content_between_repositories/comment_1_4c30ade91fc7113a95960aa3bd1d5427._comment @@ -0,0 +1,19 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawl9sYlePmv1xK-VvjBdN-5doOa_Xw-jH4U" + nickname="Richard" + subject="comment 1" + date="2011-03-22T23:41:51Z" + content=""" +I may be missing something obvious, but when I copy to a remote repository, the object files are created, but no softlinks are created. When I pull everything from the remote, it pulls only files the local repo knows about already. + + A + / \ + B C + +Moving from B to A creates no symlinks in A but the object files are moved to A. Copying back from A to B restores the object files in B and keeps them in A. + +Copying from A to an empty C does not create any object files nor symlinks. Copying from C to A creates no symlinks in A but the object files are copied to A. + +-- RichiH + +"""]] diff --git a/doc/walkthrough/moving_file_content_between_repositories/comment_2_7d90e1e150e7524ba31687108fcc38d6._comment b/doc/walkthrough/moving_file_content_between_repositories/comment_2_7d90e1e150e7524ba31687108fcc38d6._comment new file mode 100644 index 0000000000..a6f8e9cf97 --- /dev/null +++ b/doc/walkthrough/moving_file_content_between_repositories/comment_2_7d90e1e150e7524ba31687108fcc38d6._comment @@ -0,0 +1,10 @@ +[[!comment format=mdwn + username="http://joey.kitenet.net/" + nickname="joey" + subject="comment 2" + date="2011-03-23T00:38:10Z" + content=""" +`git annex move` only moves content. All symlink management is handled by git, so you have to keep repositories in sync using git as you would any other repo. When you `git pull B` in A, it will get whatever symlinks were added to B. + +(It can be useful to use a central bare repo and avoid needing to git pull from one repo to another, then you can just always push commits to the central repo, and pull down all changes from other repos.) +"""]] diff --git a/doc/walkthrough/moving_file_content_between_repositories/comment_3_558d80384434207b9cfc033763863de3._comment b/doc/walkthrough/moving_file_content_between_repositories/comment_3_558d80384434207b9cfc033763863de3._comment new file mode 100644 index 0000000000..9a128f1ed6 --- /dev/null +++ b/doc/walkthrough/moving_file_content_between_repositories/comment_3_558d80384434207b9cfc033763863de3._comment @@ -0,0 +1,12 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawl9sYlePmv1xK-VvjBdN-5doOa_Xw-jH4U" + nickname="Richard" + subject="comment 3" + date="2011-03-23T02:07:49Z" + content=""" +Ah yes, I feel kinda stupid in hindsight. + +As the central server is most likely a common use case, would you object if I added that to the walkthrough? If you have any best practices on how to automate a push with every copy to a bare remote? AFAIK, git does not store information about bare/non-bare remotes, but this could easily be put into .git/config by git annex. + +-- RichiH +"""]] diff --git a/doc/walkthrough/moving_file_content_between_repositories/comment_4_a2f343eceed9e9fba1670f21e0fc6af4._comment b/doc/walkthrough/moving_file_content_between_repositories/comment_4_a2f343eceed9e9fba1670f21e0fc6af4._comment new file mode 100644 index 0000000000..8b4d9a0538 --- /dev/null +++ b/doc/walkthrough/moving_file_content_between_repositories/comment_4_a2f343eceed9e9fba1670f21e0fc6af4._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="http://joey.kitenet.net/" + nickname="joey" + subject="comment 4" + date="2011-03-23T15:28:00Z" + content=""" +I would not mind if the walkthrough documented the central git repo case. But I don't want to complicate it unduely (it's long enough), and it's important that the fully distributed case be shown to work, and I assume that people already have basic git knowledge, so documenting the details of set up of a bare git repo is sorta out of scope. (There are also a lot of way to do it, using github, or gitosis, or raw git, etc.) +"""]] diff --git a/doc/walkthrough/removing_files.mdwn b/doc/walkthrough/removing_files.mdwn new file mode 100644 index 0000000000..9b85d9c3ba --- /dev/null +++ b/doc/walkthrough/removing_files.mdwn @@ -0,0 +1,17 @@ +When you're using git-annex you can `git rm` a file just like you usually +would with git. Just like with git, this removes the file from your work +tree, but it does not remove the file's content from the git repository. +If you check the file back out, or revert the removal, you can get it back. + +Git-annex adds the ability to remove the content of a file from your local +repository to save space. This is called "dropping" the file. + +You can always drop files safely. Git-annex checks that some other +repository still has the file before removing it. + + # git annex drop iso/debian.iso + drop iso/Debian_5.0.iso ok + +Once dropped, the file will still appear in your work tree as a broken symlink. +You can use `git annex get` to as usual to get this file back to your local +repository. diff --git a/doc/walkthrough/removing_files/comment_1_cb65e7c510b75be1c51f655b058667c6._comment b/doc/walkthrough/removing_files/comment_1_cb65e7c510b75be1c51f655b058667c6._comment new file mode 100644 index 0000000000..1c8719cecd --- /dev/null +++ b/doc/walkthrough/removing_files/comment_1_cb65e7c510b75be1c51f655b058667c6._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="DavidEdmondson" + subject="Is it necessary to commit after the 'drop'?" + date="2011-09-05T15:43:25Z" + content=""" +In fact is it possible? Nothing changed as far as git is concerned. + +"""]] diff --git a/doc/walkthrough/removing_files/comment_2_64709ea4558915edd5c8ca4486965b07._comment b/doc/walkthrough/removing_files/comment_2_64709ea4558915edd5c8ca4486965b07._comment new file mode 100644 index 0000000000..f5fb8dc7f5 --- /dev/null +++ b/doc/walkthrough/removing_files/comment_2_64709ea4558915edd5c8ca4486965b07._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="http://joey.kitenet.net/" + nickname="joey" + subject="comment 2" + date="2011-09-05T15:59:27Z" + content=""" +Good catch. It used to be necessary before there was a git-annex branch, but not now. +"""]] diff --git a/doc/walkthrough/removing_files:_When_things_go_wrong.mdwn b/doc/walkthrough/removing_files:_When_things_go_wrong.mdwn new file mode 100644 index 0000000000..2d3c0cde08 --- /dev/null +++ b/doc/walkthrough/removing_files:_When_things_go_wrong.mdwn @@ -0,0 +1,24 @@ +Before dropping a file, git-annex wants to be able to look at other +remotes, and verify that they still have a file. After all, it could +have been dropped from them too. If the remotes are not mounted/available, +you'll see something like this. + + # git annex drop important_file other.iso + drop important_file (unsafe) + Could only verify the existence of 0 out of 1 necessary copies + Unable to access these remotes: usbdrive + Try making some of these repositories available: + 58d84e8a-d9ae-11df-a1aa-ab9aa8c00826 -- portable USB drive + ca20064c-dbb5-11df-b2fe-002170d25c55 -- backup SATA drive + (Use --force to override this check, or adjust annex.numcopies.) + failed + drop other.iso (unsafe) + Could only verify the existence of 0 out of 1 necessary copies + No other repository is known to contain the file. + (Use --force to override this check, or adjust annex.numcopies.) + failed + +Here you might --force it to drop `important_file` if you [[trust]] your backup. +But `other.iso` looks to have never been copied to anywhere else, so if +it's something you want to hold onto, you'd need to transfer it to +some other repository before dropping it. diff --git a/doc/walkthrough/renaming_files.mdwn b/doc/walkthrough/renaming_files.mdwn new file mode 100644 index 0000000000..85964d1ea5 --- /dev/null +++ b/doc/walkthrough/renaming_files.mdwn @@ -0,0 +1,13 @@ + # cd ~/annex + # git mv big_file my_cool_big_file + # mkdir iso + # git mv debian.iso iso/ + # git commit -m moved + +You can use any normal git operations to move files around, or even +make copies or delete them. + +Notice that, since annexed files are represented by symlinks, +the symlink will break when the file is moved into a subdirectory. +But, git-annex will fix this up for you when you commit -- +it has a pre-commit hook that watches for and corrects broken symlinks. diff --git a/doc/walkthrough/syncing.mdwn b/doc/walkthrough/syncing.mdwn new file mode 100644 index 0000000000..3c43e502c3 --- /dev/null +++ b/doc/walkthrough/syncing.mdwn @@ -0,0 +1,29 @@ +Notice that in the [[previous example|getting_file_content]], you had to +git fetch and merge from laptop first. This lets git-annex know what has +changed in laptop, and so it knows about the files present there and can +get them. + +If you have a lot of repositories to keep in sync, manually fetching and +merging from them can become tedious. To automate it there is a handy +sync command, which also even commits your changes for you. + + # cd /media/usb/annex + # git annex sync + commit + nothing to commit (working directory clean) + ok + pull laptop + ok + push laptop + ok + +After you run sync, the repository will be updated with all changes made to +its remotes, and any changes in the repository will be pushed out to its +remotes, where a sync will get them. This is especially useful when using +git in a distributed fashion, without a +[[central bare repository|tips/centralized_git_repository_tutorial]]. See +[[sync]] for details. + +Note that syncing only syncs the metadata about your files that is stored +in git. It does not sync the contents of files, that are managed by +git-annex. diff --git a/doc/walkthrough/transferring_files:_When_things_go_wrong.mdwn b/doc/walkthrough/transferring_files:_When_things_go_wrong.mdwn new file mode 100644 index 0000000000..cfb70aaf9a --- /dev/null +++ b/doc/walkthrough/transferring_files:_When_things_go_wrong.mdwn @@ -0,0 +1,17 @@ +After a while, you'll have several annexes, with different file contents. +You don't have to try to keep all that straight; git-annex does +[[location_tracking]] for you. If you ask it to get a file and the drive +or file server is not accessible, it will let you know what it needs to get +it: + + # git annex get video/hackity_hack_and_kaxxt.mov + get video/_why_hackity_hack_and_kaxxt.mov (not available) + Unable to access these remotes: usbdrive, server + Try making some of these repositories available: + 5863d8c0-d9a9-11df-adb2-af51e6559a49 -- my home file server + 58d84e8a-d9ae-11df-a1aa-ab9aa8c00826 -- portable USB drive + ca20064c-dbb5-11df-b2fe-002170d25c55 -- backup SATA drive + failed + # sudo mount /media/usb + # git annex get video/hackity_hack_and_kaxxt.mov + get video/hackity_hack_and_kaxxt.mov (from usbdrive...) ok diff --git a/doc/walkthrough/unused_data.mdwn b/doc/walkthrough/unused_data.mdwn new file mode 100644 index 0000000000..1d655b89ae --- /dev/null +++ b/doc/walkthrough/unused_data.mdwn @@ -0,0 +1,35 @@ +It's possible for data to accumulate in the annex that no files in any +branch point to anymore. One way it can happen is if you `git rm` a file +without first calling `git annex drop`. And, when you modify an annexed +file, the old content of the file remains in the annex. Another way is when +migrating between key-value [[backends]]. + +This might be historical data you want to preserve, so git-annex defaults to +preserving it. So from time to time, you may want to check for such data: + + # git annex unused + unused . (checking for unused data...) + Some annexed data is no longer used by any files in the repository. + NUMBER KEY + 1 SHA256-s86050597--6ae2688bc533437766a48aa19f2c06be14d1bab9c70b468af445d4f07b65f41e + 2 SHA1-s14--f1358ec1873d57350e3dc62054dc232bc93c2bd1 + (To see where data was previously used, try: git log --stat -S'KEY') + (To remove unwanted data: git-annex dropunused NUMBER) + ok + +After running `git annex unused`, you can follow the instructions to examine +the history of files that used the data, and if you decide you don't need that +data anymore, you can easily remove it from your local repository. + + # git annex dropunused 1 + dropunused 1 ok + +Hint: To drop a lot of unused data, use a command like this: + + # git annex dropunused 1-1000 + +Rather than removing the data, you can instead send it to other +repositories: + + # git annex copy --unused --to backup + # git annex move --unused --to archive diff --git a/doc/walkthrough/unused_data/comment_1_684b7b652d3a8ec04f32129c5528f1ab._comment b/doc/walkthrough/unused_data/comment_1_684b7b652d3a8ec04f32129c5528f1ab._comment new file mode 100644 index 0000000000..2be2a6463f --- /dev/null +++ b/doc/walkthrough/unused_data/comment_1_684b7b652d3a8ec04f32129c5528f1ab._comment @@ -0,0 +1,22 @@ +[[!comment format=mdwn + username="bremner" + ip="156.34.89.108" + subject="finding data that isn't unused, but should be." + date="2012-10-17T20:32:11Z" + content=""" +Sometimes links to annexed data still exists on some branch, when it was supposed to be dropped. Here is how I found these; perhaps there is a simpler way. + + % git annex find --format '${key}\n' | sort > /tmp/known-keys + % find .git/annex/objects -type f -exec basename {} \; | sort > /tmp/local-keys + % comm -23 /tmp/local-keys /tmp/known-keys + +to look for what branch these are on, try + + % git log --stat --all -S$key + +for one of the keys output above. In my case it was the same remote branch keeping them all alive. + + +*EDIT* sort key lists to make comm work properly + +"""]] diff --git a/doc/walkthrough/using_bup.mdwn b/doc/walkthrough/using_bup.mdwn new file mode 100644 index 0000000000..3a6a8776aa --- /dev/null +++ b/doc/walkthrough/using_bup.mdwn @@ -0,0 +1,37 @@ +Another [[special_remote|special_remotes]] that git-annex can use is +a [[special_remotes/bup]] repository. Bup stores large file contents +in a git repository of its own, with deduplication. Combined with +git-annex, you can have git on both the frontend and the backend. + +Here's how to create a bup remote, and describe it. + +[[!template id=note text=""" +Instead of specifying a remote system, you could choose to make a bup +remote that is only accessible on the current system, by passing +"buprepo=/big/mybup". +"""]] + + # git annex initremote mybup type=bup encryption=none buprepo=example.com:/big/mybup + initremote bup (bup init) + Initialized empty Git repository in /big/mybup/ + ok + # git annex describe mybup "my bup repository at example.com" + describe mybup ok + +Now the remote can be used like any other remote. + + # git annex move my_cool_big_file --to mybup + move my_cool_big_file (to mybup...) + Receiving index from server: 1100/1100, done. + ok + +Note that, unlike other remotes, bup does not really support removing +content from its git repositories. This is a feature. :) + + # git annex move my_cool_big_file --from mybup + move my_cool_big_file (from mybup...) + content cannot be removed from bup remote + failed + git-annex: 1 failed + +See [[special_remotes/bup]] for details. diff --git a/doc/walkthrough/using_ssh_remotes.mdwn b/doc/walkthrough/using_ssh_remotes.mdwn new file mode 100644 index 0000000000..1dc8fa55b9 --- /dev/null +++ b/doc/walkthrough/using_ssh_remotes.mdwn @@ -0,0 +1,33 @@ +So far in this walkthrough, git-annex has been used with a remote +repository on a USB drive. But it can also be used with a git remote +that is truly remote, a host accessed by ssh. + +Say you have a desktop on the same network as your laptop and want +to clone the laptop's annex to it: + + # git clone ssh://mylaptop/home/me/annex ~/annex + # cd ~/annex + # git annex init "my desktop" + +Now you can get files and they will be transferred (using `rsync` via `ssh`): + + # git annex get my_cool_big_file + get my_cool_big_file (getting UUID for origin...) (from origin...) + SHA256-s86050597--6ae2688bc533437766a48aa19f2c06be14d1bab9c70b468af445d4f07b65f41e 100% 2159 2.1KB/s 00:00 + ok + +When you drop files, git-annex will ssh over to the remote and make +sure the file's content is still there before removing it locally: + + # git annex drop my_cool_big_file + drop my_cool_big_file (checking origin..) ok + +Note that normally git-annex prefers to use non-ssh remotes, like +a USB drive, before ssh remotes. They are assumed to be faster/cheaper to +access, if available. There is a annex-cost setting you can configure in +`.git/config` to adjust which repositories it prefers. See +[[the_man_page|git-annex]] for details. + +Also, note that you need full shell access for this to work -- +git-annex needs to be able to ssh in and run commands. Or at least, +your shell needs to be able to run the [[git-annex-shell]] command. diff --git a/doc/walkthrough/using_ssh_remotes/comment_10_98e97c4d7fbbcd449eddf683967a71d6._comment b/doc/walkthrough/using_ssh_remotes/comment_10_98e97c4d7fbbcd449eddf683967a71d6._comment new file mode 100644 index 0000000000..6cd9bb8485 --- /dev/null +++ b/doc/walkthrough/using_ssh_remotes/comment_10_98e97c4d7fbbcd449eddf683967a71d6._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawla7u6eLKNYZ09Z7xwBffqLaXquMQC07fU" + nickname="Matthias" + subject="Hint for Debian/Ubuntu" + date="2012-11-07T13:10:15Z" + content=""" +In Debian/Ubuntu the default .bashrc returns immediately if the shell is non-interactive. Make sure to setup the PATH such that it is updated with the location of git-annex-shell before this check! This just cost me an hour of debugging as I didn't notice the return statement early on... +"""]] diff --git a/doc/walkthrough/using_ssh_remotes/comment_11_f2775a151ed50caba27057bd9c38bae2._comment b/doc/walkthrough/using_ssh_remotes/comment_11_f2775a151ed50caba27057bd9c38bae2._comment new file mode 100644 index 0000000000..9768300dc6 --- /dev/null +++ b/doc/walkthrough/using_ssh_remotes/comment_11_f2775a151ed50caba27057bd9c38bae2._comment @@ -0,0 +1,13 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawniXLhO9mLn-7EDfawdENZ2fQwlGy5w_oc" + nickname="Michał" + subject="unrecognized command" + date="2013-03-12T06:32:56Z" + content=""" +Thanks Matthias, I fought with this as well, this was the tip I needed to move on. I'm using the Linux standalone, and I had 2 issues getting everything to work without getting git-annex-shell errors. + +1. The autoinstalled wrapper could not be found, I had to comment the \"Ubuntu exit\" line and add the $HOME/.ssh to path to get rid of \"command not found\" +2. Had to modify the wrapper by replacing the \"$SSH_ORIGINAL_COMMAND\" by \"$@\" to get rid of \"fatal: unrecognized command ''\" + + +"""]] diff --git a/doc/walkthrough/using_ssh_remotes/comment_12_a8bc6110128431ca2a8624ddc75ea364._comment b/doc/walkthrough/using_ssh_remotes/comment_12_a8bc6110128431ca2a8624ddc75ea364._comment new file mode 100644 index 0000000000..28c3aa92dd --- /dev/null +++ b/doc/walkthrough/using_ssh_remotes/comment_12_a8bc6110128431ca2a8624ddc75ea364._comment @@ -0,0 +1,10 @@ +[[!comment format=mdwn + username="http://joeyh.name/" + nickname="joey" + subject="comment 12" + date="2013-03-12T11:15:11Z" + content=""" +@Michael, the standalone tarball is really meant to run the git-annex assistant. The first time \"git annex webapp\" is run, it will set up the ssh wrapper for you. + +I have updated the wrapper to work when ssh is not configured to force a key to run a command. +"""]] diff --git a/doc/walkthrough/using_ssh_remotes/comment_2_365db5820d96d5daa62c19fd76fcdf1e._comment b/doc/walkthrough/using_ssh_remotes/comment_2_365db5820d96d5daa62c19fd76fcdf1e._comment new file mode 100644 index 0000000000..8973978ad8 --- /dev/null +++ b/doc/walkthrough/using_ssh_remotes/comment_2_365db5820d96d5daa62c19fd76fcdf1e._comment @@ -0,0 +1,13 @@ +[[!comment format=mdwn + username="http://joeyh.name/" + ip="4.153.81.112" + subject="comment 2" + date="2012-05-27T20:53:05Z" + content=""" +When `git annex get` does nothing, it's because it doesn't know a place to get the file from. + +This can happen if the `git-annex` branch has not propigated from the place where the file was added. +For example, if on the laptop you had run `git pull ssh master`, that would only pull the master branch, not the git-annex branch. + +An easy way to ensure the git-annex branch is kept in sync is to run `git annex sync` +"""]] diff --git a/doc/walkthrough/using_ssh_remotes/comment_2_451fd0c6a25ee61ef137e8e5be0c286b._comment b/doc/walkthrough/using_ssh_remotes/comment_2_451fd0c6a25ee61ef137e8e5be0c286b._comment new file mode 100644 index 0000000000..2121401968 --- /dev/null +++ b/doc/walkthrough/using_ssh_remotes/comment_2_451fd0c6a25ee61ef137e8e5be0c286b._comment @@ -0,0 +1,16 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawkaT0B6s9jQuMzQUYRVBgWqtO7BhT_ZSaE" + nickname="Fernando Seabra" + subject="cannot get files" + date="2012-05-27T20:33:09Z" + content=""" +Hi, + +I could successfully clone my ssh repo's annex to my laptop, following these instructions. +I'm also able to sync the repositories (laptop and ssh) when I commit new files in the ssh repo. + +However, every time I try to get files from the ssh repo (using 'git annex get some_file'), nothing happens. +Do you know what can be happening? + +Thanks! +"""]] diff --git a/doc/walkthrough/using_ssh_remotes/comment_3_b2f15a46620385da26d5fe8f11ebfc1a._comment b/doc/walkthrough/using_ssh_remotes/comment_3_b2f15a46620385da26d5fe8f11ebfc1a._comment new file mode 100644 index 0000000000..75a133d840 --- /dev/null +++ b/doc/walkthrough/using_ssh_remotes/comment_3_b2f15a46620385da26d5fe8f11ebfc1a._comment @@ -0,0 +1,15 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawkaT0B6s9jQuMzQUYRVBgWqtO7BhT_ZSaE" + nickname="Fernando Seabra" + subject="comment 3" + date="2012-05-27T21:13:50Z" + content=""" +Thanks for the quick replay! + +I already did 'git annex sync', but it didn't work. The steps were: 'git clone ssh...', then 'cd annex', then 'git annex init \"laptop\"' + +After that, I did a 'git annex sync', and tried to get the file, but nothing happens. That's why I found it weird. +Any other thing that might have happened? + +Thanks again! +"""]] diff --git a/doc/walkthrough/using_ssh_remotes/comment_4_433ccc87fbb0a13e32d59d77f0b4e56c._comment b/doc/walkthrough/using_ssh_remotes/comment_4_433ccc87fbb0a13e32d59d77f0b4e56c._comment new file mode 100644 index 0000000000..3df03abc2c --- /dev/null +++ b/doc/walkthrough/using_ssh_remotes/comment_4_433ccc87fbb0a13e32d59d77f0b4e56c._comment @@ -0,0 +1,8 @@ +[[!comment format=mdwn + username="http://joeyh.name/" + ip="4.153.81.112" + subject="comment 4" + date="2012-05-27T21:33:11Z" + content=""" +Try running `git annex whereis` on the file and see where it says it is. +"""]] diff --git a/doc/walkthrough/using_ssh_remotes/comment_5_a9805c7965da0b88a1c9f7f207c450a1._comment b/doc/walkthrough/using_ssh_remotes/comment_5_a9805c7965da0b88a1c9f7f207c450a1._comment new file mode 100644 index 0000000000..703b89ebfa --- /dev/null +++ b/doc/walkthrough/using_ssh_remotes/comment_5_a9805c7965da0b88a1c9f7f207c450a1._comment @@ -0,0 +1,18 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawkaT0B6s9jQuMzQUYRVBgWqtO7BhT_ZSaE" + nickname="Fernando Seabra" + subject="comment 5" + date="2012-05-27T21:42:56Z" + content=""" +Hi, + +I guess the problem is with git-annex-shell. I tried to do 'git annex get file --from name_ssh_repo', and I got the following: + +bash: git-annex-shell: command not found; failed; exit code 127 + +The same thing happens if I try to do 'git annex whereis' + +git-annex-shell is indeed installed. How can I make my shell recognize this command? + +Thanks a lot! +"""]] diff --git a/doc/walkthrough/using_ssh_remotes/comment_6_9d5c12c056892b706cf100ea01866685._comment b/doc/walkthrough/using_ssh_remotes/comment_6_9d5c12c056892b706cf100ea01866685._comment new file mode 100644 index 0000000000..4d5961ca90 --- /dev/null +++ b/doc/walkthrough/using_ssh_remotes/comment_6_9d5c12c056892b706cf100ea01866685._comment @@ -0,0 +1,12 @@ +[[!comment format=mdwn + username="http://joeyh.name/" + ip="4.153.81.112" + subject="comment 6" + date="2012-05-27T22:08:50Z" + content=""" +git-annex-shell needs to be installed in the `PATH` on any host that will hold annexed files. + +If you installed with cabal, it might be `.cabal/bin/`. Whereever it was installed to is apparently not on the PATH that is set when you ssh into that host. + + +"""]] diff --git a/doc/walkthrough/using_ssh_remotes/comment_7_725e7dbb2d0a74a035127cb01ee0442c._comment b/doc/walkthrough/using_ssh_remotes/comment_7_725e7dbb2d0a74a035127cb01ee0442c._comment new file mode 100644 index 0000000000..700b170ad6 --- /dev/null +++ b/doc/walkthrough/using_ssh_remotes/comment_7_725e7dbb2d0a74a035127cb01ee0442c._comment @@ -0,0 +1,16 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawkaT0B6s9jQuMzQUYRVBgWqtO7BhT_ZSaE" + nickname="Fernando Seabra" + subject="comment 7" + date="2012-05-27T23:35:17Z" + content=""" +Hi, + +It was already installed in PATH. In fact, I can call it from the command line, and it is recognized (e.g. calling 'git-annex-shell' gives me 'git-annex-shell: bad parameters'). However, every time I do a 'git annex whereis' or 'git annex get file --from repo', it gives me the following error: + +bash: git-annex-shell: command not found +Command ssh [\"-S\",\"/Users/username/annex/.git/annex/ssh/username@example.edu\",\"-o\",\"ControlMaster=auto\",\"-o\",\"ControlPersist=yes\",\"username@example.edu\",\"git-annex-shell 'configlist' '/~/annex'\"] failed; exit code 127 + +I tried to run this ssh command, but it gives me the same 'command not found' error. It seems that the problem is with the ssh repo? +The ssh repo has a git-annex-shell working and installed in PATH. +"""]] diff --git a/doc/walkthrough/using_ssh_remotes/comment_8_8448e55026d2c2b50d8da41707686bea._comment b/doc/walkthrough/using_ssh_remotes/comment_8_8448e55026d2c2b50d8da41707686bea._comment new file mode 100644 index 0000000000..148f016db7 --- /dev/null +++ b/doc/walkthrough/using_ssh_remotes/comment_8_8448e55026d2c2b50d8da41707686bea._comment @@ -0,0 +1,16 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawmB-gCGEs--zfmvYU-__Hj2FbliUXgxMDs" + nickname="Jakub" + subject="Path problems" + date="2012-07-13T19:15:15Z" + content=""" +Hi, + +I have a same 'git-annex-shell command not found' problem as above. I've installed git annex via cabal into my ~/.haskell_bin directory. Then I've added this dir both to ~/.bashrc and ~/.zshrc. I can run git annex or 'git annex-shell' and everything is fine. My guess is that haskell is trying to spawn git-annex-shell with some current $PATH unaware shell like dash maybe? + +I've fixed this behavior by using a really ugly hack - I've symlinked ~/.haskell_bin/git-annex-shell to /usr/bin/git-annex-shell on all my machines and the problem is gone. Somehow haskell (or whatever is trying to call git-annex-shell) is unaware of path modifications from .bashrc/.zshrc + +Here is the path modification I've used: + +export PATH=~/.haskell_bin:$PATH +"""]] diff --git a/doc/walkthrough/using_ssh_remotes/comment_9_61833299a9878f23ac57598fa6da8839._comment b/doc/walkthrough/using_ssh_remotes/comment_9_61833299a9878f23ac57598fa6da8839._comment new file mode 100644 index 0000000000..ddb96da369 --- /dev/null +++ b/doc/walkthrough/using_ssh_remotes/comment_9_61833299a9878f23ac57598fa6da8839._comment @@ -0,0 +1,23 @@ +[[!comment format=mdwn + username="https://www.google.com/accounts/o8/id?id=AItOawmB-gCGEs--zfmvYU-__Hj2FbliUXgxMDs" + nickname="Jakub" + subject="Fixed" + date="2012-07-13T19:27:46Z" + content=""" +Found the problem: + +One should never use ~ in such path: + +WRONG export PATH=~/somedir:$PATH + +Instead one should use $HOME: + +GOOD export PATH=$HOME/somedir:$PATH + +Can I surpress the message that shell failed with status 255 when a repo is unavailible? I've got two repos pointing to one machine - either via vpn or local lan and I keep getting erros if one is unavailible: + +ssh: connect to host 10.9.0.1 port 39882: No route to host +Command ssh [\"-S\",\"/home/pielgrzym/annex/.git/annex/ssh/nas\",\"-o\",\"ControlMaster=auto\",\"-o\",\"ControlPersist=yes\",\"nas\",\"git-annex-shell 'configlist' '/~/annex'\"] failed; exit code 255 + + +"""]] diff --git a/doc/walkthrough/using_tags_and_branches.mdwn b/doc/walkthrough/using_tags_and_branches.mdwn new file mode 100644 index 0000000000..b147ea4661 --- /dev/null +++ b/doc/walkthrough/using_tags_and_branches.mdwn @@ -0,0 +1,14 @@ +Like git, git-annex hangs on to every old version of a file (by default), +so you can make tags and branches, and can check them out later to look at +the old files. + + # git tag 1.0 + # rm -f my_cool_big_file + # git commit -m deleted + # git checkout 1.0 + # cat my_cool_big_file + yay! old version still here + +Of course, when you `git checkout` an old branch, some old versions of +files may not be locally available, and may be stored in some other +repository. You can use `git annex get` to get them as usual. diff --git a/ghci b/ghci new file mode 100755 index 0000000000..206bbbc7ac --- /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.cabal b/git-annex.cabal new file mode 100644 index 0000000000..47b87eec3c --- /dev/null +++ b/git-annex.cabal @@ -0,0 +1,166 @@ +Name: git-annex +Version: 4.20130802 +Cabal-Version: >= 1.8 +License: GPL-3 +Maintainer: Joey Hess +Author: Joey Hess +Stability: Stable +Copyright: 2010-2013 Joey Hess +License-File: COPYRIGHT +Homepage: http://git-annex.branchable.com/ +Build-type: Custom +Category: Utility +Synopsis: manage files with git, without checking their contents into git +Description: + git-annex allows managing files with git, without checking the file + contents into git. While that may seem paradoxical, it is useful when + dealing with files larger than git can currently easily handle, whether due + to limitations in memory, time, or disk space. + . + Even without file content tracking, being able to manage files with git, + move files around and delete files with versioned directory trees, and use + branches and distributed clones, are all very handy reasons to use git. And + annexed files can co-exist in the same git repository with regularly + versioned files, which is convenient for maintaining documents, Makefiles, + etc that are associated with annexed files but that benefit from full + revision control. + +Flag S3 + Description: Enable S3 support + +Flag WebDAV + Description: Enable WebDAV support + +Flag Inotify + Description: Enable inotify support + +Flag Dbus + Description: Enable dbus support + +Flag Assistant + Description: Enable git-annex assistant and watch command + +Flag Webapp + Description: Enable git-annex webapp + +Flag Pairing + Description: Enable pairing + +Flag XMPP + Description: Enable notifications using XMPP + +Flag DNS + Description: Enable the haskell DNS library for DNS lookup + +Flag Production + Description: Enable production build (slower build; faster binary) + +Flag Android + Description: Building for Android + Default: False + +Flag TestSuite + Description: Embed the test suite into git-annex + +Flag TDFA + Description: Use regex-tdfa for wildcards + +Flag Feed + Description: Enable podcast feed support + +Executable git-annex + Main-Is: git-annex.hs + Build-Depends: MissingH, hslogger, directory, filepath, + containers, utf8-string, network (>= 2.0), mtl (>= 2), + bytestring, old-locale, time, HTTP, + extensible-exceptions, dataenc, SHA, process, json, + base (>= 4.5 && < 4.8), monad-control, MonadCatchIO-transformers, + IfElse, text, QuickCheck >= 2.1, bloomfilter, edit-distance, process, + SafeSemaphore, uuid, random, dlist, unix-compat + -- 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: -Wall + CPP-Options: -DWITH_CLIBS + Extensions: PackageImports + -- Some things don't work with the non-threaded RTS. + GHC-Options: -threaded + + if flag(Production) + GHC-Options: -O2 + + if os(windows) + CPP-Options: -D__WINDOWS__ + else + Build-Depends: unix + + if flag(TestSuite) + Build-Depends: HUnit + CPP-Options: -DWITH_TESTSUITE + + if flag(TDFA) + Build-Depends: regex-tdfa + CPP-Options: -DWITH_TDFA + + if flag(S3) + Build-Depends: hS3 + CPP-Options: -DWITH_S3 + + if flag(WebDAV) + Build-Depends: DAV (>= 0.3), http-conduit, xml-conduit, http-types + CPP-Options: -DWITH_WEBDAV + + if flag(Assistant) && ! os(windows) && ! os(solaris) + Build-Depends: async, stm (>= 2.3) + CPP-Options: -DWITH_ASSISTANT + + if flag(Android) + Build-Depends: data-endian + CPP-Options: -D__ANDROID__ + + if flag(Assistant) + if os(linux) && flag(Inotify) + Build-Depends: hinotify + CPP-Options: -DWITH_INOTIFY + else + if os(darwin) + Build-Depends: hfsevents + CPP-Options: -DWITH_FSEVENTS + else + if (! os(windows) && ! os(solaris) && ! os(linux)) + CPP-Options: -DWITH_KQUEUE + C-Sources: Utility/libkqueue.c + + if os(linux) && flag(Dbus) + Build-Depends: dbus (>= 0.10.3) + CPP-Options: -DWITH_DBUS + + if flag(Webapp) + Build-Depends: + yesod, yesod-default, yesod-static, yesod-form, yesod-core, + case-insensitive, http-types, transformers, wai, wai-logger, warp, + blaze-builder, crypto-api, hamlet, clientsession, aeson, + template-haskell, data-default + CPP-Options: -DWITH_WEBAPP + + if flag(Pairing) + Build-Depends: network-multicast, network-info + CPP-Options: -DWITH_PAIRING + + if flag(XMPP) + Build-Depends: network-protocol-xmpp, gnutls (>= 0.1.4), xml-types + CPP-Options: -DWITH_XMPP + + if flag(DNS) + Build-Depends: dns + CPP-Options: -DWITH_DNS + + if flag(Feed) + Build-Depends: feed + CPP-Options: -DWITH_FEED + +source-repository head + type: git + location: git://git-annex.branchable.com/ diff --git a/git-annex.hs b/git-annex.hs new file mode 100644 index 0000000000..0f45f53ebc --- /dev/null +++ b/git-annex.hs @@ -0,0 +1,34 @@ +{- git-annex main program stub + - + - Copyright 2010-2013 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE CPP #-} + +import System.Environment +import System.FilePath + +import qualified GitAnnex +import qualified GitAnnexShell +#ifdef WITH_TESTSUITE +import qualified Test +#endif + +main :: IO () +main = run =<< getProgName + where + run n + | isshell n = go GitAnnexShell.run + | otherwise = go GitAnnex.run + isshell n = takeFileName n == "git-annex-shell" + go a = do + ps <- getArgs +#ifdef WITH_TESTSUITE + if ps == ["test"] + then Test.main + else a ps +#else + a ps +#endif diff --git a/git-union-merge.hs b/git-union-merge.hs new file mode 100644 index 0000000000..0e4cd644ce --- /dev/null +++ b/git-union-merge.hs @@ -0,0 +1,48 @@ +{- git-union-merge program + - + - Copyright 2011 Joey Hess + - + - Licensed under the GNU GPL version 3 or higher. + -} + +import System.Environment + +import Common +import qualified Git.UnionMerge +import qualified Git.Config +import qualified Git.CurrentRepo +import qualified Git.Branch +import qualified Git.Index +import qualified Git + +header :: String +header = "Usage: git-union-merge ref ref newref" + +usage :: IO a +usage = error $ "bad parameters\n\n" ++ header + +tmpIndex :: Git.Repo -> FilePath +tmpIndex g = Git.localGitDir g "index.git-union-merge" + +setup :: Git.Repo -> IO () +setup = cleanup -- idempotency + +cleanup :: Git.Repo -> IO () +cleanup g = nukeFile $ tmpIndex g + +parseArgs :: IO [String] +parseArgs = do + args <- getArgs + if length args /= 3 + then usage + else return args + +main :: IO () +main = do + [aref, bref, newref] <- map Git.Ref <$> parseArgs + g <- Git.Config.read =<< Git.CurrentRepo.get + _ <- Git.Index.override $ tmpIndex g + setup g + Git.UnionMerge.merge aref bref g + _ <- Git.Branch.commit "union merge" newref [aref, bref] g + cleanup g diff --git a/standalone/android/Makefile b/standalone/android/Makefile new file mode 100644 index 0000000000..39b8df0546 --- /dev/null +++ b/standalone/android/Makefile @@ -0,0 +1,153 @@ +# Cross-compiles utilities needed for git-annex on Android, +# and builds the Android app. + +# Add Android cross-compiler to PATH (as installed by ghc-android) +# (This directory also needs to have a cc that is a symlink to the prefixed +# gcc cross-compiler executable.) +ANDROID_CROSS_COMPILER?=$(HOME)/.ghc/android-14/arm-linux-androideabi-4.7/bin +PATH:=$(ANDROID_CROSS_COMPILER):$(PATH) + +# Paths to the Android SDK and NDK. +export ANDROID_SDK_ROOT?=$(HOME)/tmp/adt-bundle-linux-x86/sdk +export ANDROID_NDK_ROOT?=$(HOME)/tmp/android-ndk-r8d + +# Where to store the source tree used to build utilities. This +# directory will be created by `make source`. +GIT_ANNEX_ANDROID_SOURCETREE?=$(HOME)/tmp/android-sourcetree + +GITTREE=$(GIT_ANNEX_ANDROID_SOURCETREE)/git/installed-tree + +build: start + $(MAKE) $(GIT_ANNEX_ANDROID_SOURCETREE)/openssl/build-stamp + $(MAKE) $(GIT_ANNEX_ANDROID_SOURCETREE)/openssh/build-stamp + $(MAKE) $(GIT_ANNEX_ANDROID_SOURCETREE)/busybox/build-stamp + $(MAKE) $(GIT_ANNEX_ANDROID_SOURCETREE)/rsync/build-stamp + $(MAKE) $(GIT_ANNEX_ANDROID_SOURCETREE)/gnupg/build-stamp + $(MAKE) $(GIT_ANNEX_ANDROID_SOURCETREE)/git/build-stamp + $(MAKE) $(GIT_ANNEX_ANDROID_SOURCETREE)/term/build-stamp + + # Debug build because it does not need signing keys. + cd $(GIT_ANNEX_ANDROID_SOURCETREE)/term && tools/build-debug + + # Install executables as pseudo-libraries so they will be + # unpacked from the .apk. + mkdir -p $(GIT_ANNEX_ANDROID_SOURCETREE)/term/libs/armeabi + cp ../../tmp/androidtree/dist/build/git-annex/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 \ + $(GITTREE)/libexec/git-core/git-daemon \ + $(GITTREE)/libexec/git-core/git-show-index \ + $(GITTREE)/libexec/git-core/mergetools \ + $(GITTREE)/libexec/git-core/git-credential-* \ + $(GITTREE)/libexec/git-core/git-cvsserver \ + $(GITTREE)/libexec/git-core/git-cvsimport \ + $(GITTREE)/libexec/git-core/git-fast-import \ + $(GITTREE)/libexec/git-core/git-http-backend \ + $(GITTREE)/libexec/git-core/git-imap-send \ + $(GITTREE)/libexec/git-core/git-instaweb \ + $(GITTREE)/libexec/git-core/git-p4 \ + $(GITTREE)/libexec/git-core/git-remote-test* \ + $(GITTREE)/libexec/git-core/git-submodule \ + $(GITTREE)/libexec/git-core/git-svn \ + $(GITTREE)/libexec/git-core/git-web--browse + # Most of git is in one multicall binary, but a few important + # commands are still shell scripts. Those are put into + # a tarball, along with a list of all the links that should be + # set up. + cd $(GITTREE) && mkdir -p links + cd $(GITTREE) && find -samefile bin/git -not -wholename ./bin/git > links/git + 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 $(GIT_ANNEX_ANDROID_SOURCETREE)/git/git.tar.gz $(GIT_ANNEX_ANDROID_SOURCETREE)/term/libs/armeabi/lib.git.tar.gz.so + + git rev-parse HEAD > $(GIT_ANNEX_ANDROID_SOURCETREE)/term/libs/armeabi/lib.version.so + + 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 + +$(GIT_ANNEX_ANDROID_SOURCETREE)/openssl/build-stamp: + cd $(GIT_ANNEX_ANDROID_SOURCETREE)/openssl && CC=$$(which cc) ./Configure android + cd $(GIT_ANNEX_ANDROID_SOURCETREE)/openssl && $(MAKE) + touch $@ + +$(GIT_ANNEX_ANDROID_SOURCETREE)/openssh/build-stamp: 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 $@ + +$(GIT_ANNEX_ANDROID_SOURCETREE)/busybox/build-stamp: 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 $@ + +$(GIT_ANNEX_ANDROID_SOURCETREE)/git/build-stamp: + 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 $@ + +$(GIT_ANNEX_ANDROID_SOURCETREE)/rsync/build-stamp: 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 $@ + +$(GIT_ANNEX_ANDROID_SOURCETREE)/gnupg/build-stamp: + 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 --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 $@ + +$(GIT_ANNEX_ANDROID_SOURCETREE)/term/build-stamp: 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 $(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: $(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 $(GIT_ANNEX_ANDROID_SOURCETREE) diff --git a/standalone/android/busybox_config b/standalone/android/busybox_config new file mode 100644 index 0000000000..28ea880d98 --- /dev/null +++ b/standalone/android/busybox_config @@ -0,0 +1,997 @@ +# Run "make android2_defconfig", then "make". +# +# Tested with the standalone toolchain from ndk r6: +# android-ndk-r6/build/tools/make-standalone-toolchain.sh --platform=android-8 +# +CONFIG_HAVE_DOT_CONFIG=y + +# +# Busybox Settings +# + +# +# General Configuration +# +# CONFIG_DESKTOP is not set +# CONFIG_EXTRA_COMPAT is not set +# CONFIG_INCLUDE_SUSv2 is not set +# CONFIG_USE_PORTABLE_CODE is not set +CONFIG_PLATFORM_LINUX=y +CONFIG_FEATURE_BUFFERS_USE_MALLOC=y +# CONFIG_FEATURE_BUFFERS_GO_ON_STACK is not set +# CONFIG_FEATURE_BUFFERS_GO_IN_BSS is not set +# CONFIG_SHOW_USAGE is not set +# CONFIG_FEATURE_VERBOSE_USAGE is not set +# CONFIG_FEATURE_COMPRESS_USAGE is not set +CONFIG_FEATURE_INSTALLER=y +# CONFIG_INSTALL_NO_USR is not set +# CONFIG_LOCALE_SUPPORT is not set +# CONFIG_UNICODE_SUPPORT is not set +# CONFIG_UNICODE_USING_LOCALE is not set +# CONFIG_FEATURE_CHECK_UNICODE_IN_ENV is not set +CONFIG_SUBST_WCHAR=0 +CONFIG_LAST_SUPPORTED_WCHAR=0 +# CONFIG_UNICODE_COMBINING_WCHARS is not set +# CONFIG_UNICODE_WIDE_WCHARS is not set +# CONFIG_UNICODE_BIDI_SUPPORT is not set +# CONFIG_UNICODE_NEUTRAL_TABLE is not set +# CONFIG_UNICODE_PRESERVE_BROKEN is not set +# CONFIG_LONG_OPTS is not set +# CONFIG_FEATURE_DEVPTS is not set +# CONFIG_FEATURE_CLEAN_UP is not set +# CONFIG_FEATURE_UTMP is not set +# CONFIG_FEATURE_WTMP is not set +# CONFIG_FEATURE_PIDFILE is not set +# CONFIG_FEATURE_SUID is not set +# CONFIG_FEATURE_SUID_CONFIG is not set +# CONFIG_FEATURE_SUID_CONFIG_QUIET is not set +# CONFIG_SELINUX is not set +# CONFIG_FEATURE_PREFER_APPLETS is not set +CONFIG_BUSYBOX_EXEC_PATH="/proc/self/exe" +CONFIG_FEATURE_SYSLOG=y +# CONFIG_FEATURE_HAVE_RPC is not set + +# +# Build Options +# +# CONFIG_STATIC is not set +# CONFIG_PIE is not set +# CONFIG_NOMMU is not set +# CONFIG_BUILD_LIBBUSYBOX is not set +# CONFIG_FEATURE_INDIVIDUAL is not set +# CONFIG_FEATURE_SHARED_BUSYBOX is not set +# CONFIG_LFS is not set +CONFIG_CROSS_COMPILER_PREFIX="arm-linux-androideabi-" +CONFIG_EXTRA_CFLAGS="" + +# +# Debugging Options +# +# CONFIG_DEBUG is not set +# CONFIG_DEBUG_PESSIMIZE is not set +# CONFIG_WERROR is not set +CONFIG_NO_DEBUG_LIB=y +# CONFIG_DMALLOC is not set +# CONFIG_EFENCE is not set + +# +# Installation Options ("make install" behavior) +# +CONFIG_INSTALL_APPLET_SYMLINKS=y +# CONFIG_INSTALL_APPLET_HARDLINKS is not set +# CONFIG_INSTALL_APPLET_SCRIPT_WRAPPERS is not set +# CONFIG_INSTALL_APPLET_DONT is not set +# CONFIG_INSTALL_SH_APPLET_SYMLINK is not set +# CONFIG_INSTALL_SH_APPLET_HARDLINK is not set +# CONFIG_INSTALL_SH_APPLET_SCRIPT_WRAPPER is not set +CONFIG_PREFIX="./_install" + +# +# Busybox Library Tuning +# +# CONFIG_FEATURE_SYSTEMD is not set +# CONFIG_FEATURE_RTMINMAX is not set +CONFIG_PASSWORD_MINLEN=6 +CONFIG_MD5_SMALL=1 +# CONFIG_FEATURE_FAST_TOP is not set +# CONFIG_FEATURE_ETC_NETWORKS is not set +CONFIG_FEATURE_USE_TERMIOS=y +# CONFIG_FEATURE_EDITING is not set +CONFIG_FEATURE_EDITING_MAX_LEN=0 +# CONFIG_FEATURE_EDITING_VI is not set +CONFIG_FEATURE_EDITING_HISTORY=0 +# CONFIG_FEATURE_EDITING_SAVEHISTORY is not set +# CONFIG_FEATURE_TAB_COMPLETION is not set +# CONFIG_FEATURE_USERNAME_COMPLETION is not set +# CONFIG_FEATURE_EDITING_FANCY_PROMPT is not set +# CONFIG_FEATURE_EDITING_ASK_TERMINAL is not set +# CONFIG_FEATURE_NON_POSIX_CP is not set +# CONFIG_FEATURE_VERBOSE_CP_MESSAGE is not set +CONFIG_FEATURE_COPYBUF_KB=4 +# CONFIG_FEATURE_SKIP_ROOTFS is not set +# CONFIG_MONOTONIC_SYSCALL is not set +# CONFIG_IOCTL_HEX2STR_ERROR is not set +# CONFIG_FEATURE_HWIB is not set + +# +# Applets +# + +# +# Archival Utilities +# +CONFIG_FEATURE_SEAMLESS_XZ=y +CONFIG_FEATURE_SEAMLESS_LZMA=y +CONFIG_FEATURE_SEAMLESS_BZ2=y +CONFIG_FEATURE_SEAMLESS_GZ=y +CONFIG_FEATURE_SEAMLESS_Z=y +CONFIG_AR=y +CONFIG_FEATURE_AR_LONG_FILENAMES=y +CONFIG_FEATURE_AR_CREATE=y +CONFIG_BUNZIP2=y +CONFIG_BZIP2=y +CONFIG_CPIO=y +CONFIG_FEATURE_CPIO_O=y +CONFIG_FEATURE_CPIO_P=y +CONFIG_DPKG=y +CONFIG_DPKG_DEB=y +# CONFIG_FEATURE_DPKG_DEB_EXTRACT_ONLY is not set +CONFIG_GUNZIP=y +CONFIG_GZIP=y +# CONFIG_FEATURE_GZIP_LONG_OPTIONS is not set +CONFIG_LZOP=y +CONFIG_LZOP_COMPR_HIGH=y +CONFIG_RPM2CPIO=y +CONFIG_RPM=y +CONFIG_TAR=y +CONFIG_FEATURE_TAR_CREATE=y +CONFIG_FEATURE_TAR_AUTODETECT=y +CONFIG_FEATURE_TAR_FROM=y +CONFIG_FEATURE_TAR_OLDGNU_COMPATIBILITY=y +CONFIG_FEATURE_TAR_OLDSUN_COMPATIBILITY=y +CONFIG_FEATURE_TAR_GNU_EXTENSIONS=y +# CONFIG_FEATURE_TAR_LONG_OPTIONS is not set +# CONFIG_FEATURE_TAR_TO_COMMAND is not set +CONFIG_FEATURE_TAR_UNAME_GNAME=y +CONFIG_FEATURE_TAR_NOPRESERVE_TIME=y +# CONFIG_FEATURE_TAR_SELINUX is not set +CONFIG_UNCOMPRESS=y +CONFIG_UNLZMA=y +CONFIG_FEATURE_LZMA_FAST=y +CONFIG_LZMA=y +CONFIG_UNXZ=y +CONFIG_XZ=y +CONFIG_UNZIP=y + +# +# Coreutils +# +CONFIG_BASENAME=y +CONFIG_CAT=y +# CONFIG_DATE is not set +# CONFIG_FEATURE_DATE_ISOFMT is not set +# CONFIG_FEATURE_DATE_NANO is not set +# CONFIG_FEATURE_DATE_COMPAT is not set +# CONFIG_ID is not set +# CONFIG_GROUPS is not set +CONFIG_TEST=y +CONFIG_FEATURE_TEST_64=y +CONFIG_TOUCH=y +CONFIG_TR=y +CONFIG_FEATURE_TR_CLASSES=y +CONFIG_FEATURE_TR_EQUIV=y +CONFIG_BASE64=y +CONFIG_CAL=y +CONFIG_CATV=y +CONFIG_CHGRP=y +CONFIG_CHMOD=y +CONFIG_CHOWN=y +# CONFIG_FEATURE_CHOWN_LONG_OPTIONS is not set +CONFIG_CHROOT=y +CONFIG_CKSUM=y +CONFIG_COMM=y +CONFIG_CP=y +# CONFIG_FEATURE_CP_LONG_OPTIONS is not set +CONFIG_CUT=y +CONFIG_DD=y +CONFIG_FEATURE_DD_SIGNAL_HANDLING=y +CONFIG_FEATURE_DD_THIRD_STATUS_LINE=y +CONFIG_FEATURE_DD_IBS_OBS=y +# CONFIG_DF is not set +# CONFIG_FEATURE_DF_FANCY is not set +CONFIG_DIRNAME=y +CONFIG_DOS2UNIX=y +CONFIG_UNIX2DOS=y +CONFIG_DU=y +CONFIG_FEATURE_DU_DEFAULT_BLOCKSIZE_1K=y +CONFIG_ECHO=y +CONFIG_FEATURE_FANCY_ECHO=y +# CONFIG_ENV is not set +# CONFIG_FEATURE_ENV_LONG_OPTIONS is not set +CONFIG_EXPAND=y +# CONFIG_FEATURE_EXPAND_LONG_OPTIONS is not set +# CONFIG_EXPR is not set +# CONFIG_EXPR_MATH_SUPPORT_64 is not set +CONFIG_FALSE=y +CONFIG_FOLD=y +# CONFIG_FSYNC is not set +CONFIG_HEAD=y +CONFIG_FEATURE_FANCY_HEAD=y +# CONFIG_HOSTID is not set +CONFIG_INSTALL=y +# CONFIG_FEATURE_INSTALL_LONG_OPTIONS is not set +CONFIG_LN=y +# CONFIG_LOGNAME is not set +CONFIG_LS=y +CONFIG_FEATURE_LS_FILETYPES=y +CONFIG_FEATURE_LS_FOLLOWLINKS=y +CONFIG_FEATURE_LS_RECURSIVE=y +CONFIG_FEATURE_LS_SORTFILES=y +CONFIG_FEATURE_LS_TIMESTAMPS=y +CONFIG_FEATURE_LS_USERNAME=y +# CONFIG_FEATURE_LS_COLOR is not set +# CONFIG_FEATURE_LS_COLOR_IS_DEFAULT is not set +CONFIG_MD5SUM=y +CONFIG_MKDIR=y +# CONFIG_FEATURE_MKDIR_LONG_OPTIONS is not set +CONFIG_MKFIFO=y +CONFIG_MKNOD=y +CONFIG_MV=y +# CONFIG_FEATURE_MV_LONG_OPTIONS is not set +CONFIG_NICE=y +CONFIG_NOHUP=y +CONFIG_OD=y +CONFIG_PRINTENV=y +CONFIG_PRINTF=y +CONFIG_PWD=y +CONFIG_READLINK=y +CONFIG_FEATURE_READLINK_FOLLOW=y +CONFIG_REALPATH=y +CONFIG_RM=y +CONFIG_RMDIR=y +# CONFIG_FEATURE_RMDIR_LONG_OPTIONS is not set +CONFIG_SEQ=y +CONFIG_SHA1SUM=y +CONFIG_SHA256SUM=y +CONFIG_SHA512SUM=y +CONFIG_SLEEP=y +CONFIG_FEATURE_FANCY_SLEEP=y +CONFIG_FEATURE_FLOAT_SLEEP=y +CONFIG_SORT=y +CONFIG_FEATURE_SORT_BIG=y +CONFIG_SPLIT=y +CONFIG_FEATURE_SPLIT_FANCY=y +# CONFIG_STAT is not set +# CONFIG_FEATURE_STAT_FORMAT is not set +CONFIG_STTY=y +CONFIG_SUM=y +CONFIG_SYNC=y +CONFIG_TAC=y +CONFIG_TAIL=y +CONFIG_FEATURE_FANCY_TAIL=y +CONFIG_TEE=y +CONFIG_FEATURE_TEE_USE_BLOCK_IO=y +CONFIG_TRUE=y +# CONFIG_TTY is not set +CONFIG_UNAME=y +CONFIG_UNEXPAND=y +# CONFIG_FEATURE_UNEXPAND_LONG_OPTIONS is not set +CONFIG_UNIQ=y +# CONFIG_USLEEP is not set +CONFIG_UUDECODE=y +CONFIG_UUENCODE=y +CONFIG_WC=y +CONFIG_FEATURE_WC_LARGE=y +# CONFIG_WHO is not set +CONFIG_WHOAMI=y +CONFIG_YES=y + +# +# Common options for cp and mv +# +CONFIG_FEATURE_PRESERVE_HARDLINKS=y + +# +# Common options for ls, more and telnet +# +CONFIG_FEATURE_AUTOWIDTH=y + +# +# Common options for df, du, ls +# +CONFIG_FEATURE_HUMAN_READABLE=y + +# +# Common options for md5sum, sha1sum, sha256sum, sha512sum +# +CONFIG_FEATURE_MD5_SHA1_SUM_CHECK=y + +# +# Console Utilities +# +CONFIG_CHVT=y +CONFIG_FGCONSOLE=y +CONFIG_CLEAR=y +CONFIG_DEALLOCVT=y +CONFIG_DUMPKMAP=y +# CONFIG_KBD_MODE is not set +# CONFIG_LOADFONT is not set +CONFIG_LOADKMAP=y +CONFIG_OPENVT=y +CONFIG_RESET=y +CONFIG_RESIZE=y +CONFIG_FEATURE_RESIZE_PRINT=y +CONFIG_SETCONSOLE=y +# CONFIG_FEATURE_SETCONSOLE_LONG_OPTIONS is not set +# CONFIG_SETFONT is not set +# CONFIG_FEATURE_SETFONT_TEXTUAL_MAP is not set +CONFIG_DEFAULT_SETFONT_DIR="" +CONFIG_SETKEYCODES=y +CONFIG_SETLOGCONS=y +CONFIG_SHOWKEY=y +# CONFIG_FEATURE_LOADFONT_PSF2 is not set +# CONFIG_FEATURE_LOADFONT_RAW is not set + +# +# Debian Utilities +# +CONFIG_MKTEMP=y +CONFIG_PIPE_PROGRESS=y +CONFIG_RUN_PARTS=y +# CONFIG_FEATURE_RUN_PARTS_LONG_OPTIONS is not set +CONFIG_FEATURE_RUN_PARTS_FANCY=y +CONFIG_START_STOP_DAEMON=y +CONFIG_FEATURE_START_STOP_DAEMON_FANCY=y +# CONFIG_FEATURE_START_STOP_DAEMON_LONG_OPTIONS is not set +CONFIG_WHICH=y + +# +# Editors +# +CONFIG_PATCH=y +CONFIG_VI=y +CONFIG_FEATURE_VI_MAX_LEN=0 +# CONFIG_FEATURE_VI_8BIT is not set +# CONFIG_FEATURE_VI_COLON is not set +# CONFIG_FEATURE_VI_YANKMARK is not set +CONFIG_FEATURE_VI_SEARCH=y +# CONFIG_FEATURE_VI_REGEX_SEARCH is not set +CONFIG_FEATURE_VI_USE_SIGNALS=y +# CONFIG_FEATURE_VI_DOT_CMD is not set +# CONFIG_FEATURE_VI_READONLY is not set +# CONFIG_FEATURE_VI_SETOPTS is not set +# CONFIG_FEATURE_VI_SET is not set +CONFIG_FEATURE_VI_WIN_RESIZE=y +# CONFIG_FEATURE_VI_ASK_TERMINAL is not set +# CONFIG_FEATURE_VI_OPTIMIZE_CURSOR is not set +# CONFIG_AWK is not set +# CONFIG_FEATURE_AWK_LIBM is not set +CONFIG_CMP=y +CONFIG_DIFF=y +# CONFIG_FEATURE_DIFF_LONG_OPTIONS is not set +CONFIG_FEATURE_DIFF_DIR=y +# CONFIG_ED is not set +CONFIG_SED=y +# CONFIG_FEATURE_ALLOW_EXEC is not set + +# +# Finding Utilities +# +CONFIG_FIND=y +CONFIG_FEATURE_FIND_PRINT0=y +CONFIG_FEATURE_FIND_MTIME=y +# CONFIG_FEATURE_FIND_MMIN is not set +CONFIG_FEATURE_FIND_PERM=y +CONFIG_FEATURE_FIND_TYPE=y +# CONFIG_FEATURE_FIND_XDEV is not set +# CONFIG_FEATURE_FIND_MAXDEPTH is not set +# CONFIG_FEATURE_FIND_NEWER is not set +# CONFIG_FEATURE_FIND_INUM is not set +# CONFIG_FEATURE_FIND_EXEC is not set +# CONFIG_FEATURE_FIND_USER is not set +# CONFIG_FEATURE_FIND_GROUP is not set +# CONFIG_FEATURE_FIND_NOT is not set +# CONFIG_FEATURE_FIND_DEPTH is not set +# CONFIG_FEATURE_FIND_PAREN is not set +# CONFIG_FEATURE_FIND_SIZE is not set +# CONFIG_FEATURE_FIND_PRUNE is not set +# CONFIG_FEATURE_FIND_DELETE is not set +# CONFIG_FEATURE_FIND_PATH is not set +# CONFIG_FEATURE_FIND_REGEX is not set +# CONFIG_FEATURE_FIND_CONTEXT is not set +# CONFIG_FEATURE_FIND_LINKS is not set +CONFIG_GREP=y +# CONFIG_FEATURE_GREP_EGREP_ALIAS is not set +# CONFIG_FEATURE_GREP_FGREP_ALIAS is not set +# CONFIG_FEATURE_GREP_CONTEXT is not set +CONFIG_XARGS=y +CONFIG_FEATURE_XARGS_SUPPORT_CONFIRMATION=y +CONFIG_FEATURE_XARGS_SUPPORT_QUOTES=y +CONFIG_FEATURE_XARGS_SUPPORT_TERMOPT=y +CONFIG_FEATURE_XARGS_SUPPORT_ZERO_TERM=y + +# +# Init Utilities +# +# CONFIG_BOOTCHARTD is not set +# CONFIG_FEATURE_BOOTCHARTD_BLOATED_HEADER is not set +# CONFIG_FEATURE_BOOTCHARTD_CONFIG_FILE is not set +# CONFIG_HALT is not set +# CONFIG_FEATURE_CALL_TELINIT is not set +# CONFIG_TELINIT_PATH="" +# CONFIG_INIT is not set +# CONFIG_FEATURE_USE_INITTAB is not set +# CONFIG_FEATURE_KILL_REMOVED is not set +# CONFIG_FEATURE_KILL_DELAY=0 +# CONFIG_FEATURE_INIT_SCTTY is not set +# CONFIG_FEATURE_INIT_SYSLOG is not set +# CONFIG_FEATURE_EXTRA_QUIET is not set +# CONFIG_FEATURE_INIT_COREDUMPS is not set +# CONFIG_FEATURE_INITRD is not set +# CONFIG_INIT_TERMINAL_TYPE="linux" +# CONFIG_MESG is not set +# CONFIG_FEATURE_MESG_ENABLE_ONLY_GROUP=y + +# +# Login/Password Management Utilities +# +# CONFIG_ADD_SHELL is not set +# CONFIG_REMOVE_SHELL is not set +# CONFIG_FEATURE_SHADOWPASSWDS is not set +# CONFIG_USE_BB_PWD_GRP is not set +# CONFIG_USE_BB_SHADOW is not set +# CONFIG_USE_BB_CRYPT is not set +# CONFIG_USE_BB_CRYPT_SHA is not set +# CONFIG_ADDUSER is not set +# CONFIG_FEATURE_ADDUSER_LONG_OPTIONS is not set +# CONFIG_FEATURE_CHECK_NAMES is not set +CONFIG_FIRST_SYSTEM_ID=0 +CONFIG_LAST_SYSTEM_ID=0 +# CONFIG_ADDGROUP is not set +# CONFIG_FEATURE_ADDGROUP_LONG_OPTIONS is not set +# CONFIG_FEATURE_ADDUSER_TO_GROUP is not set +# CONFIG_DELUSER is not set +# CONFIG_DELGROUP is not set +# CONFIG_FEATURE_DEL_USER_FROM_GROUP is not set +# CONFIG_GETTY is not set +# CONFIG_LOGIN is not set +# CONFIG_PAM is not set +# CONFIG_LOGIN_SCRIPTS is not set +# CONFIG_FEATURE_NOLOGIN is not set +# CONFIG_FEATURE_SECURETTY is not set +# CONFIG_PASSWD is not set +# CONFIG_FEATURE_PASSWD_WEAK_CHECK is not set +# CONFIG_CRYPTPW is not set +# CONFIG_CHPASSWD is not set +# CONFIG_SU is not set +# CONFIG_FEATURE_SU_SYSLOG is not set +# CONFIG_FEATURE_SU_CHECKS_SHELLS is not set +# CONFIG_SULOGIN is not set +# CONFIG_VLOCK is not set + +# +# Linux Ext2 FS Progs +# +CONFIG_CHATTR=y +# CONFIG_FSCK is not set +CONFIG_LSATTR=y +CONFIG_TUNE2FS=y + +# +# Linux Module Utilities +# +# CONFIG_MODINFO is not set +# CONFIG_MODPROBE_SMALL is not set +# CONFIG_FEATURE_MODPROBE_SMALL_OPTIONS_ON_CMDLINE=y +# CONFIG_FEATURE_MODPROBE_SMALL_CHECK_ALREADY_LOADED=y +# CONFIG_INSMOD is not set +# CONFIG_RMMOD is not set +# CONFIG_LSMOD is not set +# CONFIG_FEATURE_LSMOD_PRETTY_2_6_OUTPUT is not set +# CONFIG_MODPROBE is not set +# CONFIG_FEATURE_MODPROBE_BLACKLIST is not set +# CONFIG_DEPMOD is not set + +# +# Options common to multiple modutils +# +# CONFIG_FEATURE_2_4_MODULES is not set +# CONFIG_FEATURE_INSMOD_TRY_MMAP is not set +# CONFIG_FEATURE_INSMOD_VERSION_CHECKING is not set +# CONFIG_FEATURE_INSMOD_KSYMOOPS_SYMBOLS is not set +# CONFIG_FEATURE_INSMOD_LOADINKMEM is not set +# CONFIG_FEATURE_INSMOD_LOAD_MAP is not set +# CONFIG_FEATURE_INSMOD_LOAD_MAP_FULL is not set +# CONFIG_FEATURE_CHECK_TAINTED_MODULE is not set +# CONFIG_FEATURE_MODUTILS_ALIAS is not set +# CONFIG_FEATURE_MODUTILS_SYMBOLS is not set +# CONFIG_DEFAULT_MODULES_DIR="/lib/modules" +# CONFIG_DEFAULT_DEPMOD_FILE="modules.dep" + +# +# Linux System Utilities +# +CONFIG_BLOCKDEV=y +CONFIG_REV=y +# CONFIG_ACPID is not set +# CONFIG_FEATURE_ACPID_COMPAT is not set +CONFIG_BLKID=y +# CONFIG_FEATURE_BLKID_TYPE is not set +CONFIG_DMESG=y +CONFIG_FEATURE_DMESG_PRETTY=y +CONFIG_FBSET=y +CONFIG_FEATURE_FBSET_FANCY=y +CONFIG_FEATURE_FBSET_READMODE=y +CONFIG_FDFLUSH=y +CONFIG_FDFORMAT=y +CONFIG_FDISK=y +CONFIG_FDISK_SUPPORT_LARGE_DISKS=y +CONFIG_FEATURE_FDISK_WRITABLE=y +# CONFIG_FEATURE_AIX_LABEL is not set +# CONFIG_FEATURE_SGI_LABEL is not set +# CONFIG_FEATURE_SUN_LABEL is not set +# CONFIG_FEATURE_OSF_LABEL is not set +# CONFIG_FEATURE_GPT_LABEL is not set +CONFIG_FEATURE_FDISK_ADVANCED=y +CONFIG_FINDFS=y +CONFIG_FLOCK=y +CONFIG_FREERAMDISK=y +# CONFIG_FSCK_MINIX is not set +# CONFIG_MKFS_EXT2 is not set +# CONFIG_MKFS_MINIX is not set +# CONFIG_FEATURE_MINIX2 is not set +# CONFIG_MKFS_REISER is not set +# CONFIG_MKFS_VFAT is not set +CONFIG_GETOPT=y +CONFIG_FEATURE_GETOPT_LONG=y +CONFIG_HEXDUMP=y +CONFIG_FEATURE_HEXDUMP_REVERSE=y +CONFIG_HD=y +# CONFIG_HWCLOCK is not set +# CONFIG_FEATURE_HWCLOCK_LONG_OPTIONS is not set +# CONFIG_FEATURE_HWCLOCK_ADJTIME_FHS is not set +# CONFIG_IPCRM is not set +# CONFIG_IPCS is not set +CONFIG_LOSETUP=y +CONFIG_LSPCI=y +CONFIG_LSUSB=y +# CONFIG_MDEV is not set +# CONFIG_FEATURE_MDEV_CONF is not set +# CONFIG_FEATURE_MDEV_RENAME is not set +# CONFIG_FEATURE_MDEV_RENAME_REGEXP is not set +# CONFIG_FEATURE_MDEV_EXEC is not set +# CONFIG_FEATURE_MDEV_LOAD_FIRMWARE is not set +CONFIG_MKSWAP=y +CONFIG_FEATURE_MKSWAP_UUID=y +CONFIG_MORE=y +# CONFIG_MOUNT is not set +# CONFIG_FEATURE_MOUNT_FAKE is not set +# CONFIG_FEATURE_MOUNT_VERBOSE is not set +# CONFIG_FEATURE_MOUNT_HELPERS is not set +# CONFIG_FEATURE_MOUNT_LABEL is not set +# CONFIG_FEATURE_MOUNT_NFS is not set +# CONFIG_FEATURE_MOUNT_CIFS is not set +# CONFIG_FEATURE_MOUNT_FLAGS is not set +# CONFIG_FEATURE_MOUNT_FSTAB is not set +# CONFIG_PIVOT_ROOT is not set +# CONFIG_RDATE is not set +CONFIG_RDEV=y +CONFIG_READPROFILE=y +CONFIG_RTCWAKE=y +CONFIG_SCRIPT=y +CONFIG_SCRIPTREPLAY=y +# CONFIG_SETARCH is not set +# CONFIG_SWAPONOFF is not set +# CONFIG_FEATURE_SWAPON_PRI is not set +# CONFIG_SWITCH_ROOT is not set +# CONFIG_UMOUNT is not set +# CONFIG_FEATURE_UMOUNT_ALL is not set +# CONFIG_FEATURE_MOUNT_LOOP is not set +# CONFIG_FEATURE_MOUNT_LOOP_CREATE is not set +# CONFIG_FEATURE_MTAB_SUPPORT is not set +CONFIG_VOLUMEID=y + +# +# Filesystem/Volume identification +# +CONFIG_FEATURE_VOLUMEID_EXT=y +CONFIG_FEATURE_VOLUMEID_BTRFS=y +CONFIG_FEATURE_VOLUMEID_REISERFS=y +CONFIG_FEATURE_VOLUMEID_FAT=y +CONFIG_FEATURE_VOLUMEID_HFS=y +CONFIG_FEATURE_VOLUMEID_JFS=y +CONFIG_FEATURE_VOLUMEID_XFS=y +CONFIG_FEATURE_VOLUMEID_NTFS=y +CONFIG_FEATURE_VOLUMEID_ISO9660=y +CONFIG_FEATURE_VOLUMEID_UDF=y +CONFIG_FEATURE_VOLUMEID_LUKS=y +CONFIG_FEATURE_VOLUMEID_LINUXSWAP=y +CONFIG_FEATURE_VOLUMEID_CRAMFS=y +CONFIG_FEATURE_VOLUMEID_ROMFS=y +CONFIG_FEATURE_VOLUMEID_SYSV=y +CONFIG_FEATURE_VOLUMEID_OCFS2=y +CONFIG_FEATURE_VOLUMEID_LINUXRAID=y + +# +# Miscellaneous Utilities +# +# CONFIG_CONSPY is not set +# CONFIG_NANDWRITE is not set +CONFIG_NANDDUMP=y +CONFIG_SETSERIAL=y +# CONFIG_UBIATTACH is not set +# CONFIG_UBIDETACH is not set +# CONFIG_UBIMKVOL is not set +# CONFIG_UBIRMVOL is not set +# CONFIG_UBIRSVOL is not set +# CONFIG_UBIUPDATEVOL is not set +# CONFIG_ADJTIMEX is not set +# CONFIG_BBCONFIG is not set +# CONFIG_FEATURE_COMPRESS_BBCONFIG is not set +CONFIG_BEEP=y +CONFIG_FEATURE_BEEP_FREQ=4000 +CONFIG_FEATURE_BEEP_LENGTH_MS=30 +CONFIG_CHAT=y +CONFIG_FEATURE_CHAT_NOFAIL=y +# CONFIG_FEATURE_CHAT_TTY_HIFI is not set +CONFIG_FEATURE_CHAT_IMPLICIT_CR=y +CONFIG_FEATURE_CHAT_SWALLOW_OPTS=y +CONFIG_FEATURE_CHAT_SEND_ESCAPES=y +CONFIG_FEATURE_CHAT_VAR_ABORT_LEN=y +CONFIG_FEATURE_CHAT_CLR_ABORT=y +CONFIG_CHRT=y +# CONFIG_CROND is not set +# CONFIG_FEATURE_CROND_D is not set +# CONFIG_FEATURE_CROND_CALL_SENDMAIL is not set +CONFIG_FEATURE_CROND_DIR="" +# CONFIG_CRONTAB is not set +CONFIG_DC=y +CONFIG_FEATURE_DC_LIBM=y +# CONFIG_DEVFSD is not set +# CONFIG_DEVFSD_MODLOAD is not set +# CONFIG_DEVFSD_FG_NP is not set +# CONFIG_DEVFSD_VERBOSE is not set +# CONFIG_FEATURE_DEVFS is not set +CONFIG_DEVMEM=y +# CONFIG_EJECT is not set +# CONFIG_FEATURE_EJECT_SCSI is not set +CONFIG_FBSPLASH=y +CONFIG_FLASHCP=y +CONFIG_FLASH_LOCK=y +CONFIG_FLASH_UNLOCK=y +# CONFIG_FLASH_ERASEALL is not set +# CONFIG_IONICE is not set +CONFIG_INOTIFYD=y +# CONFIG_LAST is not set +# CONFIG_FEATURE_LAST_SMALL is not set +# CONFIG_FEATURE_LAST_FANCY is not set +# CONFIG_LESS is not set +CONFIG_FEATURE_LESS_MAXLINES=0 +# CONFIG_FEATURE_LESS_BRACKETS is not set +# CONFIG_FEATURE_LESS_FLAGS is not set +# CONFIG_FEATURE_LESS_MARKS is not set +# CONFIG_FEATURE_LESS_REGEXP is not set +# CONFIG_FEATURE_LESS_WINCH is not set +# CONFIG_FEATURE_LESS_DASHCMD is not set +# CONFIG_FEATURE_LESS_LINENUMS is not set +CONFIG_HDPARM=y +CONFIG_FEATURE_HDPARM_GET_IDENTITY=y +CONFIG_FEATURE_HDPARM_HDIO_SCAN_HWIF=y +CONFIG_FEATURE_HDPARM_HDIO_UNREGISTER_HWIF=y +CONFIG_FEATURE_HDPARM_HDIO_DRIVE_RESET=y +CONFIG_FEATURE_HDPARM_HDIO_TRISTATE_HWIF=y +CONFIG_FEATURE_HDPARM_HDIO_GETSET_DMA=y +CONFIG_MAKEDEVS=y +# CONFIG_FEATURE_MAKEDEVS_LEAF is not set +CONFIG_FEATURE_MAKEDEVS_TABLE=y +CONFIG_MAN=y +# CONFIG_MICROCOM is not set +# CONFIG_MOUNTPOINT is not set +# CONFIG_MT is not set +CONFIG_RAIDAUTORUN=y +# CONFIG_READAHEAD is not set +# CONFIG_RFKILL is not set +# CONFIG_RUNLEVEL is not set +CONFIG_RX=y +CONFIG_SETSID=y +CONFIG_STRINGS=y +# CONFIG_TASKSET is not set +# CONFIG_FEATURE_TASKSET_FANCY is not set +CONFIG_TIME=y +CONFIG_TIMEOUT=y +CONFIG_TTYSIZE=y +CONFIG_VOLNAME=y +# CONFIG_WALL is not set +# CONFIG_WATCHDOG is not set + +# +# Networking Utilities +# +# CONFIG_NAMEIF is not set +# CONFIG_FEATURE_NAMEIF_EXTENDED is not set +CONFIG_NBDCLIENT=y +CONFIG_NC=y +CONFIG_NC_SERVER=y +CONFIG_NC_EXTRA=y +# CONFIG_NC_110_COMPAT is not set +# CONFIG_PING is not set +# CONFIG_PING6 is not set +# CONFIG_FEATURE_FANCY_PING is not set +CONFIG_WHOIS=y +# CONFIG_FEATURE_IPV6 is not set +# CONFIG_FEATURE_UNIX_LOCAL is not set +# CONFIG_FEATURE_PREFER_IPV4_ADDRESS is not set +# CONFIG_VERBOSE_RESOLUTION_ERRORS is not set +CONFIG_ARP=y +# CONFIG_ARPING is not set +# CONFIG_BRCTL is not set +# CONFIG_FEATURE_BRCTL_FANCY is not set +# CONFIG_FEATURE_BRCTL_SHOW is not set +CONFIG_DNSD=y +# CONFIG_ETHER_WAKE is not set +CONFIG_FAKEIDENTD=y +CONFIG_FTPD=y +CONFIG_FEATURE_FTP_WRITE=y +CONFIG_FEATURE_FTPD_ACCEPT_BROKEN_LIST=y +CONFIG_FTPGET=y +CONFIG_FTPPUT=y +# CONFIG_FEATURE_FTPGETPUT_LONG_OPTIONS is not set +# CONFIG_HOSTNAME is not set +CONFIG_HTTPD=y +CONFIG_FEATURE_HTTPD_RANGES=y +CONFIG_FEATURE_HTTPD_USE_SENDFILE=y +CONFIG_FEATURE_HTTPD_SETUID=y +CONFIG_FEATURE_HTTPD_BASIC_AUTH=y +# CONFIG_FEATURE_HTTPD_AUTH_MD5 is not set +CONFIG_FEATURE_HTTPD_CGI=y +CONFIG_FEATURE_HTTPD_CONFIG_WITH_SCRIPT_INTERPR=y +CONFIG_FEATURE_HTTPD_SET_REMOTE_PORT_TO_ENV=y +CONFIG_FEATURE_HTTPD_ENCODE_URL_STR=y +CONFIG_FEATURE_HTTPD_ERROR_PAGES=y +CONFIG_FEATURE_HTTPD_PROXY=y +CONFIG_FEATURE_HTTPD_GZIP=y +CONFIG_IFCONFIG=y +CONFIG_FEATURE_IFCONFIG_STATUS=y +# CONFIG_FEATURE_IFCONFIG_SLIP is not set +CONFIG_FEATURE_IFCONFIG_MEMSTART_IOADDR_IRQ=y +CONFIG_FEATURE_IFCONFIG_HW=y +CONFIG_FEATURE_IFCONFIG_BROADCAST_PLUS=y +# CONFIG_IFENSLAVE is not set +# CONFIG_IFPLUGD is not set +CONFIG_IFUPDOWN=y +CONFIG_IFUPDOWN_IFSTATE_PATH="/var/run/ifstate" +CONFIG_FEATURE_IFUPDOWN_IP=y +CONFIG_FEATURE_IFUPDOWN_IP_BUILTIN=y +# CONFIG_FEATURE_IFUPDOWN_IFCONFIG_BUILTIN is not set +CONFIG_FEATURE_IFUPDOWN_IPV4=y +# CONFIG_FEATURE_IFUPDOWN_IPV6 is not set +CONFIG_FEATURE_IFUPDOWN_MAPPING=y +# CONFIG_FEATURE_IFUPDOWN_EXTERNAL_DHCP is not set +# CONFIG_INETD is not set +# CONFIG_FEATURE_INETD_SUPPORT_BUILTIN_ECHO is not set +# CONFIG_FEATURE_INETD_SUPPORT_BUILTIN_DISCARD is not set +# CONFIG_FEATURE_INETD_SUPPORT_BUILTIN_TIME is not set +# CONFIG_FEATURE_INETD_SUPPORT_BUILTIN_DAYTIME is not set +# CONFIG_FEATURE_INETD_SUPPORT_BUILTIN_CHARGEN is not set +# CONFIG_FEATURE_INETD_RPC is not set +CONFIG_IP=y +CONFIG_FEATURE_IP_ADDRESS=y +CONFIG_FEATURE_IP_LINK=y +CONFIG_FEATURE_IP_ROUTE=y +CONFIG_FEATURE_IP_TUNNEL=y +CONFIG_FEATURE_IP_RULE=y +CONFIG_FEATURE_IP_SHORT_FORMS=y +# CONFIG_FEATURE_IP_RARE_PROTOCOLS is not set +CONFIG_IPADDR=y +CONFIG_IPLINK=y +CONFIG_IPROUTE=y +CONFIG_IPTUNNEL=y +CONFIG_IPRULE=y +CONFIG_IPCALC=y +CONFIG_FEATURE_IPCALC_FANCY=y +# CONFIG_FEATURE_IPCALC_LONG_OPTIONS is not set +CONFIG_NETSTAT=y +CONFIG_FEATURE_NETSTAT_WIDE=y +CONFIG_FEATURE_NETSTAT_PRG=y +# CONFIG_NSLOOKUP is not set +# CONFIG_NTPD is not set +# CONFIG_FEATURE_NTPD_SERVER is not set +CONFIG_PSCAN=y +CONFIG_ROUTE=y +# CONFIG_SLATTACH is not set +CONFIG_TCPSVD=y +# CONFIG_TELNET is not set +# CONFIG_FEATURE_TELNET_TTYPE is not set +# CONFIG_FEATURE_TELNET_AUTOLOGIN is not set +# CONFIG_TELNETD is not set +# CONFIG_FEATURE_TELNETD_STANDALONE is not set +# CONFIG_FEATURE_TELNETD_INETD_WAIT is not set +# CONFIG_TFTP is not set +# CONFIG_TFTPD is not set +# CONFIG_FEATURE_TFTP_GET is not set +# CONFIG_FEATURE_TFTP_PUT is not set +# CONFIG_FEATURE_TFTP_BLOCKSIZE is not set +# CONFIG_FEATURE_TFTP_PROGRESS_BAR is not set +# CONFIG_TFTP_DEBUG is not set +# CONFIG_TRACEROUTE is not set +# CONFIG_TRACEROUTE6 is not set +# CONFIG_FEATURE_TRACEROUTE_VERBOSE is not set +# CONFIG_FEATURE_TRACEROUTE_SOURCE_ROUTE is not set +# CONFIG_FEATURE_TRACEROUTE_USE_ICMP is not set +CONFIG_TUNCTL=y +CONFIG_FEATURE_TUNCTL_UG=y +# CONFIG_UDHCPD is not set +# CONFIG_DHCPRELAY is not set +# CONFIG_DUMPLEASES is not set +# CONFIG_FEATURE_UDHCPD_WRITE_LEASES_EARLY is not set +# CONFIG_FEATURE_UDHCPD_BASE_IP_ON_MAC is not set +CONFIG_DHCPD_LEASES_FILE="" +CONFIG_UDHCPC=y +CONFIG_FEATURE_UDHCPC_ARPING=y +# CONFIG_FEATURE_UDHCP_PORT is not set +CONFIG_UDHCP_DEBUG=9 +CONFIG_FEATURE_UDHCP_RFC3397=y +CONFIG_FEATURE_UDHCP_8021Q=y +CONFIG_UDHCPC_DEFAULT_SCRIPT="/usr/share/udhcpc/default.script" +CONFIG_UDHCPC_SLACK_FOR_BUGGY_SERVERS=80 +CONFIG_IFUPDOWN_UDHCPC_CMD_OPTIONS="-R -n" +# CONFIG_UDPSVD is not set +# CONFIG_VCONFIG is not set +CONFIG_WGET=y +CONFIG_FEATURE_WGET_STATUSBAR=y +CONFIG_FEATURE_WGET_AUTHENTICATION=y +# CONFIG_FEATURE_WGET_LONG_OPTIONS is not set +CONFIG_FEATURE_WGET_TIMEOUT=y +# CONFIG_ZCIP is not set + +# +# Print Utilities +# +CONFIG_LPD=y +CONFIG_LPR=y +CONFIG_LPQ=y + +# +# Mail Utilities +# +CONFIG_MAKEMIME=y +CONFIG_FEATURE_MIME_CHARSET="us-ascii" +CONFIG_POPMAILDIR=y +CONFIG_FEATURE_POPMAILDIR_DELIVERY=y +CONFIG_REFORMIME=y +CONFIG_FEATURE_REFORMIME_COMPAT=y +CONFIG_SENDMAIL=y + +# +# Process Utilities +# +CONFIG_IOSTAT=y +CONFIG_MPSTAT=y +CONFIG_NMETER=y +CONFIG_PMAP=y +# CONFIG_POWERTOP is not set +CONFIG_PSTREE=y +CONFIG_PWDX=y +CONFIG_SMEMCAP=y +# CONFIG_FREE is not set +CONFIG_FUSER=y +# CONFIG_KILL is not set +# CONFIG_KILLALL is not set +# CONFIG_KILLALL5 is not set +# CONFIG_PGREP is not set +CONFIG_PIDOF=y +CONFIG_FEATURE_PIDOF_SINGLE=y +CONFIG_FEATURE_PIDOF_OMIT=y +# CONFIG_PKILL is not set +# CONFIG_PS is not set +# CONFIG_FEATURE_PS_WIDE is not set +# CONFIG_FEATURE_PS_TIME is not set +# CONFIG_FEATURE_PS_ADDITIONAL_COLUMNS is not set +# CONFIG_FEATURE_PS_UNUSUAL_SYSTEMS is not set +CONFIG_RENICE=y +CONFIG_BB_SYSCTL=y +CONFIG_TOP=y +CONFIG_FEATURE_TOP_CPU_USAGE_PERCENTAGE=y +CONFIG_FEATURE_TOP_CPU_GLOBAL_PERCENTS=y +CONFIG_FEATURE_TOP_SMP_CPU=y +CONFIG_FEATURE_TOP_DECIMALS=y +CONFIG_FEATURE_TOP_SMP_PROCESS=y +CONFIG_FEATURE_TOPMEM=y +CONFIG_FEATURE_SHOW_THREADS=y +# CONFIG_UPTIME is not set +CONFIG_WATCH=y + +# +# Runit Utilities +# +CONFIG_RUNSV=y +CONFIG_RUNSVDIR=y +# CONFIG_FEATURE_RUNSVDIR_LOG is not set +CONFIG_SV=y +CONFIG_SV_DEFAULT_SERVICE_DIR="/var/service" +CONFIG_SVLOGD=y +CONFIG_CHPST=y +CONFIG_SETUIDGID=y +CONFIG_ENVUIDGID=y +CONFIG_ENVDIR=y +CONFIG_SOFTLIMIT=y +# CONFIG_CHCON is not set +# CONFIG_FEATURE_CHCON_LONG_OPTIONS is not set +# CONFIG_GETENFORCE is not set +# CONFIG_GETSEBOOL is not set +# CONFIG_LOAD_POLICY is not set +# CONFIG_MATCHPATHCON is not set +# CONFIG_RESTORECON is not set +# CONFIG_RUNCON is not set +# CONFIG_FEATURE_RUNCON_LONG_OPTIONS is not set +# CONFIG_SELINUXENABLED is not set +# CONFIG_SETENFORCE is not set +# CONFIG_SETFILES is not set +# CONFIG_FEATURE_SETFILES_CHECK_OPTION is not set +# CONFIG_SETSEBOOL is not set +# CONFIG_SESTATUS is not set + +# +# Shells +# +CONFIG_ASH=y +# CONFIG_ASH_BASH_COMPAT is not set +# CONFIG_ASH_IDLE_TIMEOUT is not set +CONFIG_ASH_JOB_CONTROL=y +# CONFIG_ASH_ALIAS is not set +CONFIG_ASH_GETOPTS=y +# CONFIG_ASH_BUILTIN_ECHO is not set +# CONFIG_ASH_BUILTIN_PRINTF is not set +# CONFIG_ASH_BUILTIN_TEST is not set +# CONFIG_ASH_CMDCMD is not set +# CONFIG_ASH_MAIL is not set +# CONFIG_ASH_OPTIMIZE_FOR_SIZE is not set +# CONFIG_ASH_RANDOM_SUPPORT is not set +# CONFIG_ASH_EXPAND_PRMT is not set +CONFIG_CTTYHACK=y +# CONFIG_HUSH is not set +# CONFIG_HUSH_BASH_COMPAT is not set +# CONFIG_HUSH_BRACE_EXPANSION is not set +# CONFIG_HUSH_HELP is not set +# CONFIG_HUSH_INTERACTIVE is not set +# CONFIG_HUSH_SAVEHISTORY is not set +# CONFIG_HUSH_JOB is not set +# CONFIG_HUSH_TICK is not set +# CONFIG_HUSH_IF is not set +# CONFIG_HUSH_LOOPS is not set +# CONFIG_HUSH_CASE is not set +# CONFIG_HUSH_FUNCTIONS is not set +# CONFIG_HUSH_LOCAL is not set +# CONFIG_HUSH_RANDOM_SUPPORT is not set +# CONFIG_HUSH_EXPORT_N is not set +# CONFIG_HUSH_MODE_X is not set +# CONFIG_MSH is not set +CONFIG_FEATURE_SH_IS_ASH=y +# CONFIG_FEATURE_SH_IS_HUSH is not set +# CONFIG_FEATURE_SH_IS_NONE is not set +# CONFIG_FEATURE_BASH_IS_ASH is not set +# CONFIG_FEATURE_BASH_IS_HUSH is not set +CONFIG_FEATURE_BASH_IS_NONE=y +# CONFIG_SH_MATH_SUPPORT is not set +# CONFIG_SH_MATH_SUPPORT_64 is not set +# CONFIG_FEATURE_SH_EXTRA_QUIET is not set +CONFIG_FEATURE_SH_STANDALONE=y +# CONFIG_FEATURE_SH_NOFORK is not set +# CONFIG_FEATURE_SH_HISTFILESIZE is not set + +# +# System Logging Utilities +# +# CONFIG_SYSLOGD is not set +# CONFIG_FEATURE_ROTATE_LOGFILE is not set +# CONFIG_FEATURE_REMOTE_LOG is not set +# CONFIG_FEATURE_SYSLOGD_DUP is not set +# CONFIG_FEATURE_SYSLOGD_CFG is not set +CONFIG_FEATURE_SYSLOGD_READ_BUFFER_SIZE=0 +# CONFIG_FEATURE_IPC_SYSLOG is not set +CONFIG_FEATURE_IPC_SYSLOG_BUFFER_SIZE=0 +# CONFIG_LOGREAD is not set +# CONFIG_FEATURE_LOGREAD_REDUCED_LOCKING is not set +CONFIG_KLOGD=y +CONFIG_FEATURE_KLOGD_KLOGCTL=y +# CONFIG_LOGGER is not set diff --git a/standalone/android/dropbear.patch b/standalone/android/dropbear.patch new file mode 100644 index 0000000000..84c7dfb6d6 --- /dev/null +++ b/standalone/android/dropbear.patch @@ -0,0 +1,55 @@ +From 014dadb02fd984828a6232534c47dba8e2f7818a Mon Sep 17 00:00:00 2001 +From: Joey Hess +Date: Wed, 13 Feb 2013 15:29:52 -0400 +Subject: [PATCH] android patch for dropbear + +* Disable HOME override +* Use urandom to avoid blocking on every ssh connection. +* Enable use of netbsd_getpass.c +--- + cli-auth.c | 1 + + cli-main.c | 2 -- + options.h | 2 +- + 3 files changed, 2 insertions(+), 3 deletions(-) + +diff --git a/cli-auth.c b/cli-auth.c +index 4c17a21..91dfdf8 100644 +--- a/cli-auth.c ++++ b/cli-auth.c +@@ -31,6 +31,7 @@ + #include "ssh.h" + #include "packet.h" + #include "runopts.h" ++#include "netbsd_getpass.c" + + void cli_authinitialise() { + +diff --git a/cli-main.c b/cli-main.c +index 106006b..68cf023 100644 +--- a/cli-main.c ++++ b/cli-main.c +@@ -47,8 +47,6 @@ int main(int argc, char ** argv) { + _dropbear_exit = cli_dropbear_exit; + _dropbear_log = cli_dropbear_log; + +- putenv("HOME=/data/local"); +- + disallow_core(); + + cli_getopts(argc, argv); +diff --git a/options.h b/options.h +index 7625151..48e404d 100644 +--- a/options.h ++++ b/options.h +@@ -159,7 +159,7 @@ etc) slower (perhaps by 50%). Recommended for most small systems. */ + * however significantly reduce the security of your ssh connections + * if the PRNG state becomes guessable - make sure you know what you are + * doing if you change this. */ +-#define DROPBEAR_RANDOM_DEV "/dev/random" ++#define DROPBEAR_RANDOM_DEV "/dev/urandom" + + /* prngd must be manually set up to produce output */ + /*#define DROPBEAR_PRNGD_SOCKET "/var/run/dropbear-rng"*/ +-- +1.7.10.4 + diff --git a/standalone/android/evilsplicer-headers.hs b/standalone/android/evilsplicer-headers.hs new file mode 100644 index 0000000000..35a20a001b --- /dev/null +++ b/standalone/android/evilsplicer-headers.hs @@ -0,0 +1,27 @@ + + +{- This file was modified by the EvilSplicer, adding these headers, + - and expanding Template Haskell. + - + - ** DO NOT COMMIT ** + -} +import qualified Data.Monoid +import qualified Data.Map +import qualified Data.Map as Data.Map.Base +import qualified Data.Foldable +import qualified Data.Text +import qualified Data.Text.Lazy.Builder +import qualified Text.Shakespeare +import qualified Text.Hamlet +import qualified Text.Julius +import qualified Text.Css +import qualified "blaze-markup" Text.Blaze.Internal +import qualified Yesod.Widget +import qualified Yesod.Routes.TH.Types +import qualified Yesod.Routes.Dispatch +import qualified WaiAppStatic.Storage.Embedded +import qualified Data.FileEmbed +import qualified Data.ByteString.Internal +{- End EvilSplicer headers. -} + + diff --git a/standalone/android/haskell-patches/DAV_0.3-0001-build-without-TH.patch b/standalone/android/haskell-patches/DAV_0.3-0001-build-without-TH.patch new file mode 100644 index 0000000000..3fbf764c2d --- /dev/null +++ b/standalone/android/haskell-patches/DAV_0.3-0001-build-without-TH.patch @@ -0,0 +1,306 @@ +From d195f807dac2351d29aeff00d2aee3e151eb82e3 Mon Sep 17 00:00:00 2001 +From: Joey Hess +Date: Thu, 18 Apr 2013 19:37:28 -0400 +Subject: [PATCH] build without TH + +Used the EvilSplicer to expand the TH + +Left off CmdArgs to save time. +--- + DAV.cabal | 20 +---- + Network/Protocol/HTTP/DAV.hs | 53 ++++++++++--- + Network/Protocol/HTTP/DAV/TH.hs | 167 ++++++++++++++++++++++++++++++++++++++- + 3 files changed, 207 insertions(+), 33 deletions(-) + +diff --git a/DAV.cabal b/DAV.cabal +index 774d4e5..8b85133 100644 +--- a/DAV.cabal ++++ b/DAV.cabal +@@ -38,25 +38,7 @@ library + , transformers >= 0.3 + , xml-conduit >= 1.0 && <= 1.1 + , xml-hamlet >= 0.4 && <= 0.5 +-executable hdav +- main-is: hdav.hs +- ghc-options: -Wall +- build-depends: base >= 4.5 && <= 5 +- , bytestring +- , bytestring +- , case-insensitive >= 0.4 +- , cmdargs >= 0.9 +- , containers +- , http-conduit >= 1.4 +- , http-types >= 0.7 +- , lens >= 3.0 +- , lifted-base >= 0.1 +- , mtl >= 2.1 +- , network >= 2.3 +- , resourcet >= 0.3 +- , transformers >= 0.3 +- , xml-conduit >= 1.0 && <= 1.1 +- , xml-hamlet >= 0.4 && <= 0.5 ++ , text + + source-repository head + type: git +diff --git a/Network/Protocol/HTTP/DAV.hs b/Network/Protocol/HTTP/DAV.hs +index 02e5d15..c0be362 100644 +--- a/Network/Protocol/HTTP/DAV.hs ++++ b/Network/Protocol/HTTP/DAV.hs +@@ -52,7 +52,8 @@ import Network.HTTP.Types (hContentType, Method, Status, RequestHeaders, unautho + + import qualified Text.XML as XML + import Text.XML.Cursor (($/), (&/), element, node, fromDocument, checkName) +-import Text.Hamlet.XML (xml) ++import Text.Hamlet.XML ++import qualified Data.Text + + import Data.CaseInsensitive (mk) + +@@ -246,18 +247,48 @@ makeCollection url username password = withDS url username password $ + propname :: XML.Document + propname = XML.Document (XML.Prologue [] Nothing []) root [] + where +- root = XML.Element "D:propfind" (Map.fromList [("xmlns:D", "DAV:")]) [xml| +- +-|] ++ root = XML.Element "D:propfind" (Map.fromList [("xmlns:D", "DAV:")]) $ concat ++ [[XML.NodeElement ++ (XML.Element ++ (XML.Name ++ (Data.Text.pack "D:allprop") Nothing Nothing) ++ Map.empty ++ (concat []))]] ++ + + locky :: XML.Document + locky = XML.Document (XML.Prologue [] Nothing []) root [] + where +- root = XML.Element "D:lockinfo" (Map.fromList [("xmlns:D", "DAV:")]) [xml| +- +- +- +- +-Haskell DAV user +-|] ++ root = XML.Element "D:lockinfo" (Map.fromList [("xmlns:D", "DAV:")]) $ concat ++ [[XML.NodeElement ++ (XML.Element ++ (XML.Name ++ (Data.Text.pack "D:lockscope") Nothing Nothing) ++ Map.empty ++ (concat ++ [[XML.NodeElement ++ (XML.Element ++ (XML.Name ++ (Data.Text.pack "D:exclusive") Nothing Nothing) ++ Map.empty ++ (concat []))]]))], ++ [XML.NodeElement ++ (XML.Element ++ (XML.Name ++ (Data.Text.pack "D:locktype") Nothing Nothing) ++ Map.empty ++ (concat ++ [[XML.NodeElement ++ (XML.Element ++ (XML.Name (Data.Text.pack "D:write") Nothing Nothing) ++ Map.empty ++ (concat []))]]))], ++ [XML.NodeElement ++ (XML.Element ++ (XML.Name (Data.Text.pack "D:owner") Nothing Nothing) ++ Map.empty ++ (concat ++ [[XML.NodeContent ++ (Data.Text.pack "Haskell DAV user")]]))]] ++ + +diff --git a/Network/Protocol/HTTP/DAV/TH.hs b/Network/Protocol/HTTP/DAV/TH.hs +index 036a2bc..4d3c0f4 100644 +--- a/Network/Protocol/HTTP/DAV/TH.hs ++++ b/Network/Protocol/HTTP/DAV/TH.hs +@@ -16,11 +16,13 @@ + -- You should have received a copy of the GNU General Public License + -- along with this program. If not, see . + +-{-# LANGUAGE TemplateHaskell #-} ++{-# LANGUAGE RankNTypes #-} + + module Network.Protocol.HTTP.DAV.TH where + +-import Control.Lens (makeLenses) ++import Control.Lens ++import qualified Control.Lens.Type ++import qualified Data.Functor + import qualified Data.ByteString as B + import Network.HTTP.Conduit (Manager, Request) + +@@ -33,4 +35,163 @@ data DAVContext a = DAVContext { + , _basicusername :: B.ByteString + , _basicpassword :: B.ByteString + } +-makeLenses ''DAVContext ++allowedMethods :: ++ forall a_a4Oo. ++ Control.Lens.Type.Lens' (DAVContext a_a4Oo) [B.ByteString] ++allowedMethods ++ _f_a5tt ++ (DAVContext __allowedMethods'_a5tu ++ __baseRequest_a5tw ++ __complianceClasses_a5tx ++ __httpManager_a5ty ++ __lockToken_a5tz ++ __basicusername_a5tA ++ __basicpassword_a5tB) ++ = ((\ __allowedMethods_a5tv ++ -> DAVContext ++ __allowedMethods_a5tv ++ __baseRequest_a5tw ++ __complianceClasses_a5tx ++ __httpManager_a5ty ++ __lockToken_a5tz ++ __basicusername_a5tA ++ __basicpassword_a5tB) ++ Data.Functor.<$> (_f_a5tt __allowedMethods'_a5tu)) ++{-# INLINE allowedMethods #-} ++baseRequest :: ++ forall a_a4Oo a_a5tC. ++ Control.Lens.Type.Lens (DAVContext a_a4Oo) (DAVContext a_a5tC) (Request a_a4Oo) (Request a_a5tC) ++baseRequest ++ _f_a5tD ++ (DAVContext __allowedMethods_a5tE ++ __baseRequest'_a5tF ++ __complianceClasses_a5tH ++ __httpManager_a5tI ++ __lockToken_a5tJ ++ __basicusername_a5tK ++ __basicpassword_a5tL) ++ = ((\ __baseRequest_a5tG ++ -> DAVContext ++ __allowedMethods_a5tE ++ __baseRequest_a5tG ++ __complianceClasses_a5tH ++ __httpManager_a5tI ++ __lockToken_a5tJ ++ __basicusername_a5tK ++ __basicpassword_a5tL) ++ Data.Functor.<$> (_f_a5tD __baseRequest'_a5tF)) ++{-# INLINE baseRequest #-} ++basicpassword :: ++ forall a_a4Oo. ++ Control.Lens.Type.Lens' (DAVContext a_a4Oo) B.ByteString ++basicpassword ++ _f_a5tM ++ (DAVContext __allowedMethods_a5tN ++ __baseRequest_a5tO ++ __complianceClasses_a5tP ++ __httpManager_a5tQ ++ __lockToken_a5tR ++ __basicusername_a5tS ++ __basicpassword'_a5tT) ++ = ((\ __basicpassword_a5tU ++ -> DAVContext ++ __allowedMethods_a5tN ++ __baseRequest_a5tO ++ __complianceClasses_a5tP ++ __httpManager_a5tQ ++ __lockToken_a5tR ++ __basicusername_a5tS ++ __basicpassword_a5tU) ++ Data.Functor.<$> (_f_a5tM __basicpassword'_a5tT)) ++{-# INLINE basicpassword #-} ++basicusername :: ++ forall a_a4Oo. ++ Control.Lens.Type.Lens' (DAVContext a_a4Oo) B.ByteString ++basicusername ++ _f_a5tV ++ (DAVContext __allowedMethods_a5tW ++ __baseRequest_a5tX ++ __complianceClasses_a5tY ++ __httpManager_a5tZ ++ __lockToken_a5u0 ++ __basicusername'_a5u1 ++ __basicpassword_a5u3) ++ = ((\ __basicusername_a5u2 ++ -> DAVContext ++ __allowedMethods_a5tW ++ __baseRequest_a5tX ++ __complianceClasses_a5tY ++ __httpManager_a5tZ ++ __lockToken_a5u0 ++ __basicusername_a5u2 ++ __basicpassword_a5u3) ++ Data.Functor.<$> (_f_a5tV __basicusername'_a5u1)) ++{-# INLINE basicusername #-} ++complianceClasses :: ++ forall a_a4Oo. ++ Control.Lens.Type.Lens' (DAVContext a_a4Oo) [B.ByteString] ++complianceClasses ++ _f_a5u4 ++ (DAVContext __allowedMethods_a5u5 ++ __baseRequest_a5u6 ++ __complianceClasses'_a5u7 ++ __httpManager_a5u9 ++ __lockToken_a5ua ++ __basicusername_a5ub ++ __basicpassword_a5uc) ++ = ((\ __complianceClasses_a5u8 ++ -> DAVContext ++ __allowedMethods_a5u5 ++ __baseRequest_a5u6 ++ __complianceClasses_a5u8 ++ __httpManager_a5u9 ++ __lockToken_a5ua ++ __basicusername_a5ub ++ __basicpassword_a5uc) ++ Data.Functor.<$> (_f_a5u4 __complianceClasses'_a5u7)) ++{-# INLINE complianceClasses #-} ++httpManager :: ++ forall a_a4Oo. Control.Lens.Type.Lens' (DAVContext a_a4Oo) Manager ++httpManager ++ _f_a5ud ++ (DAVContext __allowedMethods_a5ue ++ __baseRequest_a5uf ++ __complianceClasses_a5ug ++ __httpManager'_a5uh ++ __lockToken_a5uj ++ __basicusername_a5uk ++ __basicpassword_a5ul) ++ = ((\ __httpManager_a5ui ++ -> DAVContext ++ __allowedMethods_a5ue ++ __baseRequest_a5uf ++ __complianceClasses_a5ug ++ __httpManager_a5ui ++ __lockToken_a5uj ++ __basicusername_a5uk ++ __basicpassword_a5ul) ++ Data.Functor.<$> (_f_a5ud __httpManager'_a5uh)) ++{-# INLINE httpManager #-} ++lockToken :: ++ forall a_a4Oo. ++ Control.Lens.Type.Lens' (DAVContext a_a4Oo) (Maybe B.ByteString) ++lockToken ++ _f_a5um ++ (DAVContext __allowedMethods_a5un ++ __baseRequest_a5uo ++ __complianceClasses_a5up ++ __httpManager_a5uq ++ __lockToken'_a5ur ++ __basicusername_a5ut ++ __basicpassword_a5uu) ++ = ((\ __lockToken_a5us ++ -> DAVContext ++ __allowedMethods_a5un ++ __baseRequest_a5uo ++ __complianceClasses_a5up ++ __httpManager_a5uq ++ __lockToken_a5us ++ __basicusername_a5ut ++ __basicpassword_a5uu) ++ Data.Functor.<$> (_f_a5um __lockToken'_a5ur)) ++{-# INLINE lockToken #-} +-- +1.7.10.4 + diff --git a/standalone/android/haskell-patches/HTTP_4000.2.8-0001-build-with-base-4.8.patch b/standalone/android/haskell-patches/HTTP_4000.2.8-0001-build-with-base-4.8.patch new file mode 100644 index 0000000000..3114653f2a --- /dev/null +++ b/standalone/android/haskell-patches/HTTP_4000.2.8-0001-build-with-base-4.8.patch @@ -0,0 +1,31 @@ +From 32d0741c64e6bd280e46f7c452db9462fbac05f9 Mon Sep 17 00:00:00 2001 +From: Joey Hess +Date: Tue, 7 May 2013 18:21:04 -0400 +Subject: [PATCH] fix build + +--- + HTTP.cabal | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +diff --git a/HTTP.cabal b/HTTP.cabal +index 76cb5d6..edddf26 100644 +--- a/HTTP.cabal ++++ b/HTTP.cabal +@@ -85,12 +85,12 @@ Library + Network.HTTP.Utils + Paths_HTTP + GHC-options: -fwarn-missing-signatures -Wall +- Build-depends: base >= 2 && < 4.7, network < 2.5, parsec ++ Build-depends: base >= 2 && < 4.8, network < 2.5, parsec + Extensions: FlexibleInstances + if flag(old-base) + Build-depends: base < 3 + else +- Build-depends: base >= 3, array, old-time, bytestring ++ Build-depends: base >= 3, array, old-time, bytestring (>= 0.10.3.0) + + if flag(mtl1) + Build-depends: mtl >= 1.1 && < 1.2 +-- +1.7.10.4 + diff --git a/standalone/android/haskell-patches/MissingH_1.2.0.0_0001-fix-build-not-Android-specific.patch b/standalone/android/haskell-patches/MissingH_1.2.0.0_0001-fix-build-not-Android-specific.patch new file mode 100644 index 0000000000..50f641da7d --- /dev/null +++ b/standalone/android/haskell-patches/MissingH_1.2.0.0_0001-fix-build-not-Android-specific.patch @@ -0,0 +1,34 @@ +From 8c4220e4dd48ad197aa0ad49214e6e7bd768044e Mon Sep 17 00:00:00 2001 +From: Joey Hess +Date: Thu, 28 Feb 2013 23:28:57 -0400 +Subject: [PATCH] fix build (not Android specific) + +--- + src/System/Cmd/Utils.hs | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +diff --git a/src/System/Cmd/Utils.hs b/src/System/Cmd/Utils.hs +index a9fa46f..6c6aba2 100644 +--- a/src/System/Cmd/Utils.hs ++++ b/src/System/Cmd/Utils.hs +@@ -325,7 +325,7 @@ forceSuccess (PipeHandle pid fp args funcname) = + Just (Exited (ExitSuccess)) -> return () + Just (Exited (ExitFailure fc)) -> + cmdfailed funcname fp args fc +- Just (Terminated sig) -> ++ Just (Terminated sig _) -> + warnfail fp args $ "Terminated by signal " ++ show sig + Just (Stopped sig) -> + warnfail fp args $ "Stopped by signal " ++ show sig +@@ -351,7 +351,7 @@ safeSystem command args = + case ec of + Exited ExitSuccess -> return () + Exited (ExitFailure fc) -> cmdfailed "safeSystem" command args fc +- Terminated s -> cmdsignalled "safeSystem" command args s ++ Terminated s _ -> cmdsignalled "safeSystem" command args s + Stopped s -> cmdsignalled "safeSystem" command args s + #endif + +-- +1.7.10.4 + 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 index 0000000000..787caf45cc --- /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 +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/async_2.0.1.4_0001-allow-building-with-unreleased-ghc.patch b/standalone/android/haskell-patches/async_2.0.1.4_0001-allow-building-with-unreleased-ghc.patch new file mode 100644 index 0000000000..e959941b8c --- /dev/null +++ b/standalone/android/haskell-patches/async_2.0.1.4_0001-allow-building-with-unreleased-ghc.patch @@ -0,0 +1,25 @@ +From 55f424de9946c4d1d89837bb18698437aecfcfa4 Mon Sep 17 00:00:00 2001 +From: Joey Hess +Date: Thu, 28 Feb 2013 23:29:16 -0400 +Subject: [PATCH] allow building with unreleased ghc + +--- + async.cabal | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/async.cabal b/async.cabal +index 8e47d9d..ff317c7 100644 +--- a/async.cabal ++++ b/async.cabal +@@ -70,7 +70,7 @@ source-repository head + + library + exposed-modules: Control.Concurrent.Async +- build-depends: base >= 4.3 && < 4.7, stm >= 2.2 && < 2.5 ++ build-depends: base >= 4.3 && < 4.8, stm >= 2.2 && < 2.5 + + test-suite test-async + type: exitcode-stdio-1.0 +-- +1.7.10.4 + diff --git a/standalone/android/haskell-patches/case-insensitive_0.4.0.1_0001-allow-building-with-unreleased-ghc.patch b/standalone/android/haskell-patches/case-insensitive_0.4.0.1_0001-allow-building-with-unreleased-ghc.patch new file mode 100644 index 0000000000..2d7c45089a --- /dev/null +++ b/standalone/android/haskell-patches/case-insensitive_0.4.0.1_0001-allow-building-with-unreleased-ghc.patch @@ -0,0 +1,27 @@ +From efd0e93de82c0b5554a4f3a4517e6127f405f6da Mon Sep 17 00:00:00 2001 +From: Joey Hess +Date: Thu, 28 Feb 2013 23:29:36 -0400 +Subject: [PATCH] allow building with unreleased ghc + +--- + case-insensitive.cabal | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +diff --git a/case-insensitive.cabal b/case-insensitive.cabal +index a73479d..18a1a51 100644 +--- a/case-insensitive.cabal ++++ b/case-insensitive.cabal +@@ -25,8 +25,8 @@ source-repository head + + Library + GHC-Options: -Wall +- build-depends: base >= 3 && < 4.6 +- , bytestring >= 0.9 && < 0.10 ++ build-depends: base >= 3 && < 4.8 ++ , bytestring >= 0.9 && < 0.15 + , text >= 0.3 && < 0.12 + , hashable >= 1.0 && < 1.2 + exposed-modules: Data.CaseInsensitive +-- +1.7.10.4 + diff --git a/standalone/android/haskell-patches/certificate_1.3.7-0001-support-Android-cert-store.patch b/standalone/android/haskell-patches/certificate_1.3.7-0001-support-Android-cert-store.patch new file mode 100644 index 0000000000..5f772bfdfe --- /dev/null +++ b/standalone/android/haskell-patches/certificate_1.3.7-0001-support-Android-cert-store.patch @@ -0,0 +1,37 @@ +From 3779c75175e895f94b21341ebd6361e9d6af54fd Mon Sep 17 00:00:00 2001 +From: Joey Hess +Date: Thu, 9 May 2013 12:36:23 -0400 +Subject: [PATCH] support Android cert store + +Android puts it in a different place and has only hashed files. +See https://github.com/vincenthz/hs-certificate/issues/19 +--- + System/Certificate/X509/Unix.hs | 5 +++-- + 1 file changed, 3 insertions(+), 2 deletions(-) + +diff --git a/System/Certificate/X509/Unix.hs b/System/Certificate/X509/Unix.hs +index 8463465..74e9503 100644 +--- a/System/Certificate/X509/Unix.hs ++++ b/System/Certificate/X509/Unix.hs +@@ -35,7 +35,8 @@ import qualified Control.Exception as E + import Data.Char + + defaultSystemPath :: FilePath +-defaultSystemPath = "/etc/ssl/certs/" ++defaultSystemPath = "/system/etc/security/cacerts/" ++--defaultSystemPath = "/etc/ssl/certs/" + + envPathOverride :: String + envPathOverride = "SYSTEM_CERTIFICATE_PATH" +@@ -47,7 +48,7 @@ listDirectoryCerts path = (map (path ) . filter isCert <$> getDirectoryConten + && isDigit (s !! 9) + && (s !! 8) == '.' + && all isHexDigit (take 8 s) +- isCert x = (not $ isPrefixOf "." x) && (not $ isHashedFile x) ++ isCert x = (not $ isPrefixOf "." x) + + getSystemCertificateStore :: IO CertificateStore + getSystemCertificateStore = makeCertificateStore . concat <$> (getSystemPath >>= listDirectoryCerts >>= mapM readCertificates) +-- +1.8.2.rc3 + diff --git a/standalone/android/haskell-patches/cipher-aes_0.1.7-0001-fix-cross-build.patch b/standalone/android/haskell-patches/cipher-aes_0.1.7-0001-fix-cross-build.patch new file mode 100644 index 0000000000..fab0ae6ef7 --- /dev/null +++ b/standalone/android/haskell-patches/cipher-aes_0.1.7-0001-fix-cross-build.patch @@ -0,0 +1,34 @@ +From d456247000ab839a1d32749717f4f8f92e37dbba Mon Sep 17 00:00:00 2001 +From: Joey Hess +Date: Tue, 7 May 2013 17:45:45 -0400 +Subject: [PATCH] fix cross build + +--- + cipher-aes.cabal | 5 +---- + 1 file changed, 1 insertion(+), 4 deletions(-) + +diff --git a/cipher-aes.cabal b/cipher-aes.cabal +index 02ddfd0..eb916e3 100644 +--- a/cipher-aes.cabal ++++ b/cipher-aes.cabal +@@ -31,16 +31,13 @@ Extra-Source-Files: Tests/*.hs + + Library + Build-Depends: base >= 4 && < 5 +- , bytestring ++ , bytestring >= 0.10.3.0 + Exposed-modules: Crypto.Cipher.AES + ghc-options: -Wall + C-sources: cbits/aes_generic.c + cbits/aes.c + cbits/gf.c + cbits/cpu.c +- if os(linux) && (arch(i386) || arch(x86_64)) +- CC-options: -mssse3 -maes -mpclmul -DWITH_AESNI +- C-sources: cbits/aes_x86ni.c + + Test-Suite test-cipher-aes + type: exitcode-stdio-1.0 +-- +1.7.10.4 + diff --git a/standalone/android/haskell-patches/distributive_0.3-0001-fixes-for-cross-build.patch b/standalone/android/haskell-patches/distributive_0.3-0001-fixes-for-cross-build.patch new file mode 100644 index 0000000000..87cdef3089 --- /dev/null +++ b/standalone/android/haskell-patches/distributive_0.3-0001-fixes-for-cross-build.patch @@ -0,0 +1,39 @@ +From ddf49377d37c82575c1b0b712a476fa93fc00d6b Mon Sep 17 00:00:00 2001 +From: Joey Hess +Date: Thu, 18 Apr 2013 17:39:28 -0400 +Subject: [PATCH] fixes for cross build + +--- + distributive.cabal | 2 +- + src/Data/Distributive.hs | 2 -- + 2 files changed, 1 insertion(+), 3 deletions(-) + +diff --git a/distributive.cabal b/distributive.cabal +index 66ac73c..5204755 100644 +--- a/distributive.cabal ++++ b/distributive.cabal +@@ -12,7 +12,7 @@ bug-reports: http://github.com/ekmett/distributive/issues + copyright: Copyright (C) 2011 Edward A. Kmett + synopsis: Haskell 98 Distributive functors -- Dual to Traversable + description: Haskell 98 Distributive functors -- Dual to Traversable +-build-type: Custom ++build-type: Simple + extra-source-files: + .ghci + .travis.yml +diff --git a/src/Data/Distributive.hs b/src/Data/Distributive.hs +index 6f5613d..66eaed2 100644 +--- a/src/Data/Distributive.hs ++++ b/src/Data/Distributive.hs +@@ -26,8 +26,6 @@ import Data.Functor.Identity + import Data.Functor.Product + import Data.Functor.Reverse + +-{-# ANN module "ignore Use section" #-} +- + -- | This is the categorical dual of 'Traversable'. However, there appears + -- to be little benefit to allow the distribution via an arbitrary comonad + -- so we restrict ourselves to 'Functor'. +-- +1.8.2.rc3 + diff --git a/standalone/android/haskell-patches/dns_0.3.6-0001-use-getprop-to-get-dns-server.patch b/standalone/android/haskell-patches/dns_0.3.6-0001-use-getprop-to-get-dns-server.patch new file mode 100644 index 0000000000..069bdd20a3 --- /dev/null +++ b/standalone/android/haskell-patches/dns_0.3.6-0001-use-getprop-to-get-dns-server.patch @@ -0,0 +1,73 @@ +From 8459f93270c7a6e8a2ebd415db2110a66bf1ec41 Mon Sep 17 00:00:00 2001 +From: Joey Hess +Date: Wed, 15 May 2013 20:31:14 -0400 +Subject: [PATCH] use getprop to get dns server + +--- + Network/DNS/Resolver.hs | 13 +++++++++++-- + dns.cabal | 4 ++++ + 2 files changed, 15 insertions(+), 2 deletions(-) + +diff --git a/Network/DNS/Resolver.hs b/Network/DNS/Resolver.hs +index 70ab9ed..9b27336 100644 +--- a/Network/DNS/Resolver.hs ++++ b/Network/DNS/Resolver.hs +@@ -41,6 +41,8 @@ import Network.Socket.ByteString.Lazy + import Prelude hiding (lookup) + import System.Random + import System.Timeout ++import System.Process (readProcess) ++import System.Directory + + #if mingw32_HOST_OS == 1 + import Network.Socket (send) +@@ -73,7 +75,7 @@ data ResolvConf = ResolvConf { + -} + defaultResolvConf :: ResolvConf + defaultResolvConf = ResolvConf { +- resolvInfo = RCFilePath "/etc/resolv.conf" ++ resolvInfo = RCFilePath "/system/etc/resolv.conf" + , resolvTimeout = 3 * 1000 * 1000 + , resolvBufsize = 512 + } +@@ -111,7 +113,14 @@ makeResolvSeed conf = ResolvSeed <$> addr + where + addr = case resolvInfo conf of + RCHostName numhost -> makeAddrInfo numhost +- RCFilePath file -> toAddr <$> readFile file >>= makeAddrInfo ++ RCFilePath file -> do ++ exists <- doesFileExist file ++ if exists ++ then toAddr <$> readFile file >>= makeAddrInfo ++ else do ++ s <- readProcess "getprop" ["net.dns1"] "" ++ makeAddrInfo $ takeWhile (/= '\n') s ++ + toAddr cs = let l:_ = filter ("nameserver" `isPrefixOf`) $ lines cs + in extract l + extract = reverse . dropWhile isSpace . reverse . dropWhile isSpace . drop 11 +diff --git a/dns.cabal b/dns.cabal +index 40671f6..2c19734 100644 +--- a/dns.cabal ++++ b/dns.cabal +@@ -34,6 +34,8 @@ library + , network >= 2.3 + , network-conduit + , random ++ , process ++ , directory + else + Build-Depends: base >= 4 && < 5 + , attoparsec +@@ -49,6 +51,8 @@ library + , network-bytestring + , network-conduit + , random ++ , process ++ , directory + Source-Repository head + Type: git + Location: git://github.com/kazu-yamamoto/dns.git +-- +1.7.10.4 + diff --git a/standalone/android/haskell-patches/file-embed_0.0.4.7-0001-remove-TH-and-export-one-symbol-used-by-TH.patch b/standalone/android/haskell-patches/file-embed_0.0.4.7-0001-remove-TH-and-export-one-symbol-used-by-TH.patch new file mode 100644 index 0000000000..ff50d3947e --- /dev/null +++ b/standalone/android/haskell-patches/file-embed_0.0.4.7-0001-remove-TH-and-export-one-symbol-used-by-TH.patch @@ -0,0 +1,193 @@ +From 256ff157005f44c97fa5affe2ed9655815b3788e Mon Sep 17 00:00:00 2001 +From: Joey Hess +Date: Mon, 15 Apr 2013 12:38:22 -0400 +Subject: [PATCH] remove TH and export one symbol used by TH + +--- + Data/.FileEmbed.hs.swp | Bin 16384 -> 0 bytes + Data/FileEmbed.hs | 80 +++---------------------------------------------- + 2 files changed, 4 insertions(+), 76 deletions(-) + delete mode 100644 Data/.FileEmbed.hs.swp + +diff --git a/Data/.FileEmbed.hs.swp b/Data/.FileEmbed.hs.swp +deleted file mode 100644 +index 1b2ddbfaa71697e9df3869555aee8c97ca7ea0cb..0000000000000000000000000000000000000000 +GIT binary patch +literal 0 +HcmV?d00001 + +literal 16384 +zcmeHNZEPGz8J?z;l0w=5RfRyn>$8>HBX?)xk`I~qq+D`I3}?sToJzq>+`YRw-^O>l +z*WKCLCgwvzNFb1);!ir*%6;!D~AU?_ukSIa|0TNP$5Jm93GrM~q +zjuRvP0NRxw-|fsh@60^&&O0;jTz%?+xp_KLykFqiFT`)B+;d-jZzZDh-@^(gRkt_UayqggyLH(tOcke!Zz&#`JZUR?@)Xi5oLp=NyHc47r3|DD +z?1q6*wF*b~iTkJDJT;yfqgTJ`{BBC6GARQo11SS311SS311SS311SS31OG=1sNNp& +zPxNOGa0R$6!tMCX1MiLA@sU0$11SS311SS311SS311SS311SS311SS311SUlqYT(h +zA*RvxX$}D3{-0w2zq&_=9|B(oJ`TJeP{1ls23`y71@-_h+%3fOz|Vo70p9>V3^ahF +zz%+0#un)Kc_}e}qegu3C_%d)6aDW-$b-)X+5aMazbHMw6w*zCq9^f|M$M{{sb>Io$ +zDsTmu2a3QOfxq4*#4mxL0AB%~0z$wCs=#SrKk&!BLOct68+a6001g9(fV+S@fnUE& +zh$n$h0-pdb0VUu*;P-b5@f+Y7;Bnwnz!hK(r~;F~i!T-8@4!!iYruDb&jKF=LO=l} +z;0?g*f#>cJ;yXYO7zh6R5+S|?d;oYSa0ECE>;;}ffaVv#4}hnDCxOR-j{zSATEGS1 +zUf>TH+wTKwz-8bZPyu+%$AEi)y8$7HpN>=%&@2UQZ=IZNDccep(X*R1=Uo!Qv&r|F +z8Jcqy6-rc7zT>V&%DFV2<%^snec$sb!$18fCO`ckYgMW_*Oh*5hAw!aPtCB~-K3yr +zHzc*~fa+4Z)bM;i>?!JSs<)EY;NTk@!fF`JX +zv>3Y3yhZ_fP_B{J(t=EaWs>r`cn*w|i$SmBsN>)V!d0{a3W`nN>yg!w?y722*IsoR +zIjW1e6I2H&$qQI17t5PU8d6Ln`|m=;if3thDtR$n3Za#w9SzTI*ou}jEt$zvX1`)S~V9(`4Css&o76R4UEVgY_)e>q`~-uF1^iL|+^_<~`SLQkP~+I={=s +zQKTEGGGiGjn24J*LHx6xfM%%a_<^B28kDZxn%k4fsOc5bDaxmfoNn(4&sEY@h7~A^-}^l# +z*HdSlW)ntrY@$T4n56TGuod$b)sPe1mtjh|;#q2X16deQ?%kpd`@|>?exEx_%T}C_ +zAF|EdMR<`&z3$E|k4;n?*OJNf^GB+*K;<2^xw8u^N_Kl4Tger;-!<9kSkw6<`KcV7z2los87-D+PCek^ +zLl^t`BV)Id&9Qw(oi{T8dSa_%FN!%qBdTt0YlQ+WwVi +z5TP1|9a6r;`r+!bsHn?+u{b<1RC87c7s9Hdxqiz@s( +z&mpowdKmc5BeJu}oNw~lAK)LB{f5_+n)igwzE@B9jBNhq1`no6rCl4irbtf|X4vqp +zUvI{*7Dx!zZ!yFAm#@QA$LdCS8sPUoVK>0m3)8&CbN$AgMgvyc3^2>}HcT%Rmc`3k +zP7G(yoh_bs1G^>33iaor^jn_aojaRIj`m|j9_|@V2ph5h`=_K3FLA!tDZ&YN9PDji +z1XyIT5cXS;h+xyWj!Z1PxqP(7Cwg`?yW$D>@1um>WBF*@ryYg0SS%ISYxYFEiQ)Z; +znW(&iYb6}dsP&eOTj4jgNnKZn#VT{r8* +z&X#?XFyGG&*MNnFtZ4Pi^Il%AO25j@;8oca8J01^i@wvX4i&g{iw^N(!YVCZ_{ie5 +zIB%RNe<-~0>X+BPHsP{ryQ`tSDvM{#s#IJ$Q><;e%HA*@I!Ehm>Bnt#+{>VxS=BY= +zF&#KzxYPQmQR9=wZiq~pO$3+rCXmD$p;&ojyI7Us(3D+IYBZJ+RUdm_{YsR08vSk= +zg^`cMe#7hbcnT}0D@E69hWM^0nzj=5q~c0poT|qcPM<%1x +z(gJ+`e*))O7w26*|L5^>9q0OIfiD6d0UiM^0B;5E1O9|_{L{cS;4xqcI0Wnm{(>|7 +zmw?Xzp9a=}67W3E?$?2D0$%_Kcn$C(&g|EKEnpEi415pg^Q*w;fe!&Iz!6{^xE=T% +z+WHLe7{KlB0_l@7kTQ@mkTQ@mkTQ@m@ZZS*MbFzpgaMiW!X4$}y6-5-8#zuowaEXY +zO(D^Or`kBev0xM}pL2t-)p8mRLO6q=aT5mD!ELj%WS92}IBf8pMleGe4v*>U5U#dd +z8>({f!okrwx^0Nk@8+Ii=#C-#?_Dw^w;EPm;t(zeFDmK?6|dF8x(Pv&6-7o7z1H^= +zp6{&cwkmJVyo6NW|1E4~>r)#MRJ +zMs2LC=hGcn)&p|kwa&PDJQgEt$EO3jZUuIaqSKi^9_lz +z9hWDvK4Heq9If<{sAr2sLAk_D| +z`qc+J6D-CzXMwU2H^e;Cr|*ba{SoCH#C(s9#asr$L#)) + +--- | Embed a single file in your source code. +--- +--- > import qualified Data.ByteString +--- > +--- > myFile :: Data.ByteString.ByteString +--- > myFile = $(embedFile "dirName/fileName") +-embedFile :: FilePath -> Q Exp +-embedFile fp = +-#if MIN_VERSION_template_haskell(2,7,0) +- qAddDependentFile fp >> +-#endif +- (runIO $ B.readFile fp) >>= bsToExp +- +--- | Embed a directory recusrively in your source code. +--- +--- > import qualified Data.ByteString +--- > +--- > myDir :: [(FilePath, Data.ByteString.ByteString)] +--- > myDir = $(embedDir "dirName") +-embedDir :: FilePath -> Q Exp +-embedDir fp = do +- typ <- [t| [(FilePath, B.ByteString)] |] +- e <- ListE <$> ((runIO $ fileList fp) >>= mapM (pairToExp fp)) +- return $ SigE e typ +- + -- | Get a directory tree in the IO monad. + -- + -- This is the workhorse of 'embedDir' + getDir :: FilePath -> IO [(FilePath, B.ByteString)] + getDir = fileList + +-pairToExp :: FilePath -> (FilePath, B.ByteString) -> Q Exp +-pairToExp _root (path, bs) = do +-#if MIN_VERSION_template_haskell(2,7,0) +- qAddDependentFile $ _root ++ '/' : path +-#endif +- exp' <- bsToExp bs +- return $! TupE [LitE $ StringL path, exp'] +- +-bsToExp :: B.ByteString -> Q Exp +-bsToExp bs = do +- helper <- [| stringToBs |] +- let chars = B8.unpack bs +- return $! AppE helper $! LitE $! StringL chars +- + stringToBs :: String -> B.ByteString + stringToBs = B8.pack + +@@ -123,23 +68,6 @@ padSize i = + let s = show i + in replicate (sizeLen - length s) '0' ++ s + +-#if MIN_VERSION_template_haskell(2,5,0) +-dummySpace :: Int -> Q Exp +-dummySpace space = do +- let size = padSize space +- let start = magic ++ size +- let chars = LitE $ StringPrimL $ +-#if MIN_VERSION_template_haskell(2,6,0) +- map (toEnum . fromEnum) $ +-#endif +- start ++ replicate space '0' +- let len = LitE $ IntegerL $ fromIntegral $ length start + space +- upi <- [|unsafePerformIO|] +- pack <- [|unsafePackAddressLen|] +- getInner' <- [|getInner|] +- return $ getInner' `AppE` (upi `AppE` (pack `AppE` len `AppE` chars)) +-#endif +- + inject :: B.ByteString -- ^ bs to inject + -> B.ByteString -- ^ original BS containing dummy + -> Maybe B.ByteString -- ^ new BS, or Nothing if there is insufficient dummy space +-- +1.8.2.rc3 + diff --git a/standalone/android/haskell-patches/gnutls_0.1.4-0001-statically-link-with-gnutls.patch b/standalone/android/haskell-patches/gnutls_0.1.4-0001-statically-link-with-gnutls.patch new file mode 100644 index 0000000000..6c4ae0c63b --- /dev/null +++ b/standalone/android/haskell-patches/gnutls_0.1.4-0001-statically-link-with-gnutls.patch @@ -0,0 +1,35 @@ +From c46af28d00a67d372bf59490d288c8cb77bae307 Mon Sep 17 00:00:00 2001 +From: Joey Hess +Date: Sun, 21 Apr 2013 17:14:03 -0400 +Subject: [PATCH] statically link with gnutls + +This requires libgnutls.a (and no .so) be installed in the ugly hardcoded +lib dir. When built this way, the haskell gnutls library will link the +library into executables with no further options. + +Also includes dependencies of libgnutls (needed since it's a static +library). +--- + gnutls.cabal | 3 ++- + 1 file changed, 2 insertions(+), 1 deletion(-) + +diff --git a/gnutls.cabal b/gnutls.cabal +index a20e7ed..d8f4a1f 100644 +--- a/gnutls.cabal ++++ b/gnutls.cabal +@@ -31,10 +31,11 @@ source-repository this + library + hs-source-dirs: lib + ghc-options: -Wall -O2 ++ LD-Options: -L /home/joey/.ghc/android-14/arm-linux-androideabi-4.7/arm-linux-androideabi/sysroot/usr/lib/ -lgcrypt -lgpg-error -lz + + build-depends: + base >= 4.0 && < 5.0 +- , bytestring >= 0.9 ++ , bytestring >= 0.10.3.0 + , transformers >= 0.2 + , monads-tf >= 0.1 && < 0.2 + +-- +1.7.10.4 + diff --git a/standalone/android/haskell-patches/gsasl_0.3.5-0001-link-with-libgsasl.patch b/standalone/android/haskell-patches/gsasl_0.3.5-0001-link-with-libgsasl.patch new file mode 100644 index 0000000000..42206a1cf7 --- /dev/null +++ b/standalone/android/haskell-patches/gsasl_0.3.5-0001-link-with-libgsasl.patch @@ -0,0 +1,25 @@ +From df0f41f92d003f7d59ef927737ffec3a9bd61827 Mon Sep 17 00:00:00 2001 +From: Joey Hess +Date: Tue, 7 May 2013 18:41:01 -0400 +Subject: [PATCH] avoid cabal hell + +--- + gsasl.cabal | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/gsasl.cabal b/gsasl.cabal +index d991873..c5c2b19 100644 +--- a/gsasl.cabal ++++ b/gsasl.cabal +@@ -31,7 +31,7 @@ library + build-depends: + base >= 4.0 && < 5.0 + , transformers >= 0.2 +- , bytestring >= 0.9 ++ , bytestring >= 0.10.3.0 + + pkgconfig-depends: libgsasl >= 1.1 + +-- +1.7.10.4 + diff --git a/standalone/android/haskell-patches/hS3_0.5.7_0001-fix-build.patch b/standalone/android/haskell-patches/hS3_0.5.7_0001-fix-build.patch new file mode 100644 index 0000000000..c0158c0f40 --- /dev/null +++ b/standalone/android/haskell-patches/hS3_0.5.7_0001-fix-build.patch @@ -0,0 +1,23 @@ +From 643b3c9fd95967c5911107f46498cd851e68f97d Mon Sep 17 00:00:00 2001 +From: Joey Hess +Date: Tue, 7 May 2013 18:26:33 -0400 +Subject: [PATCH] fix build + +--- + hS3.cabal | 3 --- + 1 file changed, 3 deletions(-) + +diff --git a/hS3.cabal b/hS3.cabal +index 35f7496..e04bf65 100644 +--- a/hS3.cabal ++++ b/hS3.cabal +@@ -44,6 +44,3 @@ Library + Network.AWS.AWSConnection, + Network.AWS.Authentication, + Network.AWS.ArrowUtils +- +-Executable hs3 +- main-is: hS3.hs +-- +1.7.10.4 + diff --git a/standalone/android/haskell-patches/hamlet_1.1.6.1_0001-remove-TH.patch b/standalone/android/haskell-patches/hamlet_1.1.6.1_0001-remove-TH.patch new file mode 100644 index 0000000000..1c511a1321 --- /dev/null +++ b/standalone/android/haskell-patches/hamlet_1.1.6.1_0001-remove-TH.patch @@ -0,0 +1,294 @@ +From b2c677ed39f1aca3a1111691ba51b26f7fd414a4 Mon Sep 17 00:00:00 2001 +From: Joey Hess +Date: Wed, 8 May 2013 01:50:58 -0400 +Subject: [PATCH] remove TH + +--- + Text/Hamlet.hs | 219 ++------------------------------------------------------ + hamlet.cabal | 2 +- + 2 files changed, 7 insertions(+), 214 deletions(-) + +diff --git a/Text/Hamlet.hs b/Text/Hamlet.hs +index 4ac870a..63b8555 100644 +--- a/Text/Hamlet.hs ++++ b/Text/Hamlet.hs +@@ -11,35 +11,26 @@ + 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 ++ ++ -- referred to in TH splices ++ , attrsToHtml ++ , asHtmlUrl + ) where + + import Text.Shakespeare.Base +@@ -90,14 +81,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 +142,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 +156,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 +diff --git a/hamlet.cabal b/hamlet.cabal +index 73fa6a8..4348508 100644 +--- a/hamlet.cabal ++++ b/hamlet.cabal +@@ -50,7 +50,7 @@ library + , text >= 0.7 && < 0.12 + , containers >= 0.2 + , blaze-builder >= 0.2 && < 0.4 +- , process >= 1.0 && < 1.2 ++ , process >= 1.0 && < 1.3 + , blaze-html >= 0.5 && < 0.6 + , blaze-markup >= 0.5.1 && < 0.6 + +-- +1.7.10.4 + diff --git a/standalone/android/haskell-patches/iproute_1.2.11_0001-build-without-IPv6-stuff.patch b/standalone/android/haskell-patches/iproute_1.2.11_0001-build-without-IPv6-stuff.patch new file mode 100644 index 0000000000..bb9caec770 --- /dev/null +++ b/standalone/android/haskell-patches/iproute_1.2.11_0001-build-without-IPv6-stuff.patch @@ -0,0 +1,47 @@ +From 7beec2e707d59f9573aa2dc7c57bd2a62f16b480 Mon Sep 17 00:00:00 2001 +From: Joey Hess +Date: Wed, 15 May 2013 19:06:03 -0400 +Subject: [PATCH] build without IPv6 stuff + +--- + Data/IP.hs | 2 +- + Data/IP/Addr.hs | 3 +++ + 2 files changed, 4 insertions(+), 1 deletion(-) + +diff --git a/Data/IP.hs b/Data/IP.hs +index cffef93..ea486c9 100644 +--- a/Data/IP.hs ++++ b/Data/IP.hs +@@ -6,7 +6,7 @@ module Data.IP ( + -- ** IP data + IP (..) + , IPv4, toIPv4, fromIPv4, fromHostAddress, toHostAddress +- , IPv6, toIPv6, fromIPv6, fromHostAddress6, toHostAddress6 ++ , IPv6, toIPv6, fromIPv6 -- , fromHostAddress6, toHostAddress6 + -- ** IP range data + , IPRange (..) + , AddrRange (addr, mask, mlen) +diff --git a/Data/IP/Addr.hs b/Data/IP/Addr.hs +index faaf0c7..5b556fb 100644 +--- a/Data/IP/Addr.hs ++++ b/Data/IP/Addr.hs +@@ -312,6 +312,7 @@ toHostAddress (IP4 addr4) + | byteOrder == LittleEndian = fixByteOrder addr4 + | otherwise = addr4 + ++{- + -- | The 'fromHostAddress6' function converts 'HostAddress6' to 'IPv6'. + fromHostAddress6 :: HostAddress6 -> IPv6 + fromHostAddress6 = IP6 +@@ -320,6 +321,8 @@ fromHostAddress6 = IP6 + toHostAddress6 :: IPv6 -> HostAddress6 + toHostAddress6 (IP6 addr6) = addr6 + ++-} ++ + fixByteOrder :: Word32 -> Word32 + fixByteOrder s = d1 .|. d2 .|. d3 .|. d4 + where +-- +1.7.10.4 + diff --git a/standalone/android/haskell-patches/lens_3.8.5-0001-build-without-TH.patch b/standalone/android/haskell-patches/lens_3.8.5-0001-build-without-TH.patch new file mode 100644 index 0000000000..62efccc322 --- /dev/null +++ b/standalone/android/haskell-patches/lens_3.8.5-0001-build-without-TH.patch @@ -0,0 +1,293 @@ +From bbb49942123f06a36b170966e445692297f71d26 Mon Sep 17 00:00:00 2001 +From: Joey Hess +Date: Thu, 18 Apr 2013 19:14:30 -0400 +Subject: [PATCH] build without TH + +--- + lens.cabal | 13 +------------ + src/Control/Exception/Lens.hs | 2 +- + src/Control/Lens.hs | 6 +++--- + src/Control/Lens/Equality.hs | 4 ++-- + src/Control/Lens/Fold.hs | 6 +++--- + src/Control/Lens/Internal.hs | 2 +- + src/Control/Lens/Internal/Zipper.hs | 2 +- + src/Control/Lens/Iso.hs | 2 -- + src/Control/Lens/Lens.hs | 2 +- + src/Control/Lens/Operators.hs | 2 +- + src/Control/Lens/Plated.hs | 2 +- + src/Control/Lens/Setter.hs | 2 -- + src/Control/Lens/TH.hs | 2 +- + src/Data/Data/Lens.hs | 6 +++--- + 14 files changed, 19 insertions(+), 34 deletions(-) + +diff --git a/lens.cabal b/lens.cabal +index a06b3ce..a654b3d 100644 +--- a/lens.cabal ++++ b/lens.cabal +@@ -10,7 +10,7 @@ stability: provisional + homepage: http://github.com/ekmett/lens/ + bug-reports: http://github.com/ekmett/lens/issues + copyright: Copyright (C) 2012-2013 Edward A. Kmett +-build-type: Custom ++build-type: Simple + tested-with: GHC == 7.0.4, GHC == 7.4.1, GHC == 7.4.2, GHC == 7.6.1, GHC == 7.7.20121213, GHC == 7.7.20130117 + synopsis: Lenses, Folds and Traversals + description: +@@ -171,7 +171,6 @@ library + containers >= 0.4.0 && < 0.6, + distributive >= 0.3 && < 1, + filepath >= 1.2.0.0 && < 1.4, +- generic-deriving == 1.4.*, + ghc-prim, + hashable >= 1.1.2.3 && < 1.3, + MonadCatchIO-transformers >= 0.3 && < 0.4, +@@ -233,14 +232,12 @@ library + Control.Lens.Review + Control.Lens.Setter + Control.Lens.Simple +- Control.Lens.TH + Control.Lens.Traversal + Control.Lens.Tuple + Control.Lens.Type + Control.Lens.Wrapped + Control.Lens.Zipper + Control.Lens.Zoom +- Control.Monad.Error.Lens + Control.Parallel.Strategies.Lens + Control.Seq.Lens + Data.Array.Lens +@@ -264,12 +261,8 @@ library + Data.Typeable.Lens + Data.Vector.Lens + Data.Vector.Generic.Lens +- Generics.Deriving.Lens +- GHC.Generics.Lens + System.Exit.Lens + System.FilePath.Lens +- System.IO.Error.Lens +- Language.Haskell.TH.Lens + Numeric.Lens + + if flag(safe) +@@ -368,7 +361,6 @@ test-suite doctests + deepseq, + doctest >= 0.9.1, + filepath, +- generic-deriving, + mtl, + nats, + parallel, +@@ -394,7 +386,6 @@ benchmark plated + comonad, + criterion, + deepseq, +- generic-deriving, + lens, + transformers + +@@ -429,7 +420,6 @@ benchmark unsafe + comonads-fd, + criterion, + deepseq, +- generic-deriving, + lens, + transformers + +@@ -446,6 +436,5 @@ benchmark zipper + comonads-fd, + criterion, + deepseq, +- generic-deriving, + lens, + transformers +diff --git a/src/Control/Exception/Lens.hs b/src/Control/Exception/Lens.hs +index 5c26d4e..9909132 100644 +--- a/src/Control/Exception/Lens.hs ++++ b/src/Control/Exception/Lens.hs +@@ -112,7 +112,7 @@ import Prelude + , Maybe(..), Either(..), Functor(..), String, IO + ) + +-{-# ANN module "HLint: ignore Use Control.Exception.catch" #-} ++ + + -- $setup + -- >>> :set -XNoOverloadedStrings +diff --git a/src/Control/Lens.hs b/src/Control/Lens.hs +index 8481e44..74700ae 100644 +--- a/src/Control/Lens.hs ++++ b/src/Control/Lens.hs +@@ -59,7 +59,7 @@ module Control.Lens + , module Control.Lens.Review + , module Control.Lens.Setter + , module Control.Lens.Simple +-#ifndef DISABLE_TEMPLATE_HASKELL ++#if 0 + , module Control.Lens.TH + #endif + , module Control.Lens.Traversal +@@ -89,7 +89,7 @@ import Control.Lens.Reified + import Control.Lens.Review + import Control.Lens.Setter + import Control.Lens.Simple +-#ifndef DISABLE_TEMPLATE_HASKELL ++#if 0 + import Control.Lens.TH + #endif + import Control.Lens.Traversal +@@ -99,4 +99,4 @@ import Control.Lens.Wrapped + import Control.Lens.Zipper + import Control.Lens.Zoom + +-{-# ANN module "HLint: ignore Use import/export shortcut" #-} ++ +diff --git a/src/Control/Lens/Equality.hs b/src/Control/Lens/Equality.hs +index 982c2d7..3a3fe1a 100644 +--- a/src/Control/Lens/Equality.hs ++++ b/src/Control/Lens/Equality.hs +@@ -28,8 +28,8 @@ module Control.Lens.Equality + import Control.Lens.Internal.Setter + import Control.Lens.Type + +-{-# ANN module "HLint: ignore Use id" #-} +-{-# ANN module "HLint: ignore Eta reduce" #-} ++ ++ + + -- $setup + -- >>> import Control.Lens +diff --git a/src/Control/Lens/Fold.hs b/src/Control/Lens/Fold.hs +index ae5100d..467eb37 100644 +--- a/src/Control/Lens/Fold.hs ++++ b/src/Control/Lens/Fold.hs +@@ -161,9 +161,9 @@ import Data.Traversable + -- >>> let g :: Expr -> Expr; g = Debug.SimpleReflect.Vars.g + -- >>> let timingOut :: NFData a => a -> IO a; timingOut = fmap (fromMaybe (error "timeout")) . timeout (5*10^6) . evaluate . force + +-{-# ANN module "HLint: ignore Eta reduce" #-} +-{-# ANN module "HLint: ignore Use camelCase" #-} +-{-# ANN module "HLint: ignore Use curry" #-} ++ ++ ++ + + infixl 8 ^.., ^?, ^?!, ^@.., ^@?, ^@?! + +diff --git a/src/Control/Lens/Internal.hs b/src/Control/Lens/Internal.hs +index 295662e..539642d 100644 +--- a/src/Control/Lens/Internal.hs ++++ b/src/Control/Lens/Internal.hs +@@ -43,4 +43,4 @@ import Control.Lens.Internal.Review + import Control.Lens.Internal.Setter + import Control.Lens.Internal.Zoom + +-{-# ANN module "HLint: ignore Use import/export shortcut" #-} ++ +diff --git a/src/Control/Lens/Internal/Zipper.hs b/src/Control/Lens/Internal/Zipper.hs +index 95875b7..76060be 100644 +--- a/src/Control/Lens/Internal/Zipper.hs ++++ b/src/Control/Lens/Internal/Zipper.hs +@@ -53,7 +53,7 @@ import Data.Profunctor.Unsafe + -- >>> import Control.Lens + -- >>> import Data.Char + +-{-# ANN module "HLint: ignore Use foldl" #-} ++ + + ------------------------------------------------------------------------------ + -- * Jacket +diff --git a/src/Control/Lens/Iso.hs b/src/Control/Lens/Iso.hs +index 62d40ef..235511a 100644 +--- a/src/Control/Lens/Iso.hs ++++ b/src/Control/Lens/Iso.hs +@@ -70,8 +70,6 @@ import Data.Profunctor.Unsafe + import Unsafe.Coerce + #endif + +-{-# ANN module "HLint: ignore Use on" #-} +- + -- $setup + -- >>> :set -XNoOverloadedStrings + -- >>> import Control.Lens +diff --git a/src/Control/Lens/Lens.hs b/src/Control/Lens/Lens.hs +index ff2a45f..5401ec4 100644 +--- a/src/Control/Lens/Lens.hs ++++ b/src/Control/Lens/Lens.hs +@@ -120,7 +120,7 @@ import Data.Profunctor + import Data.Profunctor.Rep + import Data.Profunctor.Unsafe + +-{-# ANN module "HLint: ignore Use ***" #-} ++ + + -- $setup + -- >>> :set -XNoOverloadedStrings +diff --git a/src/Control/Lens/Operators.hs b/src/Control/Lens/Operators.hs +index d88cb49..fa7b37e 100644 +--- a/src/Control/Lens/Operators.hs ++++ b/src/Control/Lens/Operators.hs +@@ -107,4 +107,4 @@ import Control.Lens.Review + import Control.Lens.Setter + import Control.Lens.Zipper + +-{-# ANN module "HLint: ignore Use import/export shortcut" #-} ++ +diff --git a/src/Control/Lens/Plated.hs b/src/Control/Lens/Plated.hs +index 07d9212..27070c0 100644 +--- a/src/Control/Lens/Plated.hs ++++ b/src/Control/Lens/Plated.hs +@@ -95,7 +95,7 @@ import Data.Data.Lens + import Data.Monoid + import Data.Tree + +-{-# ANN module "HLint: ignore Reduce duplication" #-} ++ + + -- | A 'Plated' type is one where we know how to extract its immediate self-similar children. + -- +diff --git a/src/Control/Lens/Setter.hs b/src/Control/Lens/Setter.hs +index 2acbfa6..4a12c6b 100644 +--- a/src/Control/Lens/Setter.hs ++++ b/src/Control/Lens/Setter.hs +@@ -87,8 +87,6 @@ import Data.Profunctor + import Data.Profunctor.Rep + import Data.Profunctor.Unsafe + +-{-# ANN module "HLint: ignore Avoid lambda" #-} +- + -- $setup + -- >>> import Control.Lens + -- >>> import Control.Monad.State +diff --git a/src/Control/Lens/TH.hs b/src/Control/Lens/TH.hs +index fbf4adb..ee723d7 100644 +--- a/src/Control/Lens/TH.hs ++++ b/src/Control/Lens/TH.hs +@@ -87,7 +87,7 @@ import Language.Haskell.TH + import Language.Haskell.TH.Syntax + import Language.Haskell.TH.Lens + +-{-# ANN module "HLint: ignore Use foldl" #-} ++ + + -- | Flags for 'Lens' construction + data LensFlag +diff --git a/src/Data/Data/Lens.hs b/src/Data/Data/Lens.hs +index cf1e7c9..b39dacf 100644 +--- a/src/Data/Data/Lens.hs ++++ b/src/Data/Data/Lens.hs +@@ -65,9 +65,9 @@ import Data.Monoid + import GHC.Exts (realWorld#) + #endif + +-{-# ANN module "HLint: ignore Eta reduce" #-} +-{-# ANN module "HLint: ignore Use foldl" #-} +-{-# ANN module "HLint: ignore Reduce duplication" #-} ++ ++ ++ + + -- $setup + -- >>> :set -XNoOverloadedStrings +-- +1.8.2.rc3 + diff --git a/standalone/android/haskell-patches/libxml-sax_0.7.3-0001-static-link-with-libxml2.patch b/standalone/android/haskell-patches/libxml-sax_0.7.3-0001-static-link-with-libxml2.patch new file mode 100644 index 0000000000..752f601cc7 --- /dev/null +++ b/standalone/android/haskell-patches/libxml-sax_0.7.3-0001-static-link-with-libxml2.patch @@ -0,0 +1,27 @@ +From 9d53e3fa4516a948a6e84987e9c1c9fd07f973bf Mon Sep 17 00:00:00 2001 +From: Joey Hess +Date: Sun, 21 Apr 2013 15:44:51 -0400 +Subject: [PATCH] static link with libxml2 + +This requires libxml2.a (and no .so) be installed in the ugly hardcoded +lib dir. When built this way, the haskell library will link the +C library into executables with no further options. +--- + libxml-sax.cabal | 1 + + 1 file changed, 1 insertion(+) + +diff --git a/libxml-sax.cabal b/libxml-sax.cabal +index 5edfdb6..338bc55 100644 +--- a/libxml-sax.cabal ++++ b/libxml-sax.cabal +@@ -31,6 +31,7 @@ library + hs-source-dirs: lib + ghc-options: -Wall -O2 + cc-options: -Wall ++ LD-Options: -L /home/joey/.ghc/android-14/arm-linux-androideabi-4.7/arm-linux-androideabi/sysroot/usr/lib/ + + build-depends: + base >= 4.1 && < 5.0 +-- +1.7.10.4 + diff --git a/standalone/android/haskell-patches/lifted-base_0.2.0.2_0001-hacked-for-newer-ghc.patch b/standalone/android/haskell-patches/lifted-base_0.2.0.2_0001-hacked-for-newer-ghc.patch new file mode 100644 index 0000000000..b61dc17ba9 --- /dev/null +++ b/standalone/android/haskell-patches/lifted-base_0.2.0.2_0001-hacked-for-newer-ghc.patch @@ -0,0 +1,163 @@ +From 4bb0de1e6213ec925820c8b9cc3ff5f3c3c72d7a Mon Sep 17 00:00:00 2001 +From: Joey Hess +Date: Thu, 28 Feb 2013 23:31:27 -0400 +Subject: [PATCH] hacked for newer ghc + +--- + Control/Concurrent/Lifted.hs | 2 +- + Control/Exception/Lifted.hs | 11 ++-------- + Setup.hs | 46 ++---------------------------------------- + lifted-base.cabal | 9 ++++----- + 4 files changed, 9 insertions(+), 59 deletions(-) + +diff --git a/Control/Concurrent/Lifted.hs b/Control/Concurrent/Lifted.hs +index 4bc58a8..e4445e6 100644 +--- a/Control/Concurrent/Lifted.hs ++++ b/Control/Concurrent/Lifted.hs +@@ -124,7 +124,7 @@ import Control.Concurrent.SampleVar.Lifted + #endif + import Control.Exception.Lifted ( throwTo + #if MIN_VERSION_base(4,6,0) +- , SomeException, try, mask ++ , SomeException, try + #endif + ) + #include "inlinable.h" +diff --git a/Control/Exception/Lifted.hs b/Control/Exception/Lifted.hs +index 871cda7..0b9d8b7 100644 +--- a/Control/Exception/Lifted.hs ++++ b/Control/Exception/Lifted.hs +@@ -50,8 +50,8 @@ module Control.Exception.Lifted + -- |The following functions allow a thread to control delivery of + -- asynchronous exceptions during a critical region. + #if MIN_VERSION_base(4,3,0) +- , mask, mask_ +- , uninterruptibleMask, uninterruptibleMask_ ++ , mask_ ++ , uninterruptibleMask_ + , getMaskingState + #if MIN_VERSION_base(4,4,0) + , allowInterrupt +@@ -266,10 +266,6 @@ evaluate = liftBase ∘ E.evaluate + -------------------------------------------------------------------------------- + + #if MIN_VERSION_base(4,3,0) +--- |Generalized version of 'E.mask'. +-mask ∷ MonadBaseControl IO m ⇒ ((∀ a. m a → m a) → m b) → m b +-mask = liftBaseOp E.mask ∘ liftRestore +-{-# INLINABLE mask #-} + + liftRestore ∷ MonadBaseControl IO m + ⇒ ((∀ a. m a → m a) → b) +@@ -283,9 +279,6 @@ mask_ = liftBaseOp_ E.mask_ + {-# INLINABLE mask_ #-} + + -- |Generalized version of 'E.uninterruptibleMask'. +-uninterruptibleMask ∷ MonadBaseControl IO m ⇒ ((∀ a. m a → m a) → m b) → m b +-uninterruptibleMask = liftBaseOp E.uninterruptibleMask ∘ liftRestore +-{-# INLINABLE uninterruptibleMask #-} + + -- |Generalized version of 'E.uninterruptibleMask_'. + uninterruptibleMask_ ∷ MonadBaseControl IO m ⇒ m a → m a +diff --git a/Setup.hs b/Setup.hs +index 33956e1..9a994af 100644 +--- a/Setup.hs ++++ b/Setup.hs +@@ -1,44 +1,2 @@ +-#! /usr/bin/env runhaskell +- +-{-# LANGUAGE NoImplicitPrelude, UnicodeSyntax #-} +- +-module Main (main) where +- +- +-------------------------------------------------------------------------------- +--- Imports +-------------------------------------------------------------------------------- +- +--- from base +-import System.IO ( IO ) +- +--- from cabal +-import Distribution.Simple ( defaultMainWithHooks +- , simpleUserHooks +- , UserHooks(haddockHook) +- ) +- +-import Distribution.Simple.LocalBuildInfo ( LocalBuildInfo(..) ) +-import Distribution.Simple.Program ( userSpecifyArgs ) +-import Distribution.Simple.Setup ( HaddockFlags ) +-import Distribution.PackageDescription ( PackageDescription(..) ) +- +- +-------------------------------------------------------------------------------- +--- Cabal setup program which sets the CPP define '__HADDOCK __' when haddock is run. +-------------------------------------------------------------------------------- +- +-main ∷ IO () +-main = defaultMainWithHooks hooks +- where +- hooks = simpleUserHooks { haddockHook = haddockHook' } +- +--- Define __HADDOCK__ for CPP when running haddock. +-haddockHook' ∷ PackageDescription → LocalBuildInfo → UserHooks → HaddockFlags → IO () +-haddockHook' pkg lbi = +- haddockHook simpleUserHooks pkg (lbi { withPrograms = p }) +- where +- p = userSpecifyArgs "haddock" ["--optghc=-D__HADDOCK__"] (withPrograms lbi) +- +- +--- The End --------------------------------------------------------------------- ++import Distribution.Simple ++main = defaultMain +diff --git a/lifted-base.cabal b/lifted-base.cabal +index 54ef418..8da5086 100644 +--- a/lifted-base.cabal ++++ b/lifted-base.cabal +@@ -9,7 +9,7 @@ Copyright: (c) 2011-2012 Bas van Dijk, Anders Kaseorg + Homepage: https://github.com/basvandijk/lifted-base + Bug-reports: https://github.com/basvandijk/lifted-base/issues + Category: Control +-Build-type: Custom ++Build-type: Simple + Cabal-version: >= 1.9.2 + Description: @lifted-base@ exports IO operations from the base library lifted to + any instance of 'MonadBase' or 'MonadBaseControl'. +@@ -37,7 +37,6 @@ Library + Exposed-modules: Control.Exception.Lifted + Control.Concurrent.MVar.Lifted + Control.Concurrent.Chan.Lifted +- Control.Concurrent.Lifted + Data.IORef.Lifted + System.Timeout.Lifted + if impl(ghc < 7.6) +@@ -46,7 +45,7 @@ Library + Control.Concurrent.QSemN.Lifted + Control.Concurrent.SampleVar.Lifted + +- Build-depends: base >= 3 && < 4.7 ++ Build-depends: base >= 3 && < 4.8 + , base-unicode-symbols >= 0.1.1 && < 0.3 + , transformers-base >= 0.4 && < 0.5 + , monad-control >= 0.3 && < 0.4 +@@ -64,7 +63,7 @@ test-suite test-lifted-base + hs-source-dirs: test + + build-depends: lifted-base +- , base >= 3 && < 4.7 ++ , base >= 3 && < 4.8 + , transformers >= 0.2 && < 0.4 + , transformers-base >= 0.4 && < 0.5 + , monad-control >= 0.3 && < 0.4 +@@ -87,7 +86,7 @@ benchmark bench-lifted-base + ghc-options: -O2 + + build-depends: lifted-base +- , base >= 3 && < 4.7 ++ , base >= 3 && < 4.8 + , transformers >= 0.2 && < 0.4 + , criterion >= 0.5 && < 0.7 + , monad-control >= 0.3 && < 0.4 +-- +1.7.10.4 + diff --git a/standalone/android/haskell-patches/monad-control_0.3.1.4_0001-build-with-newer-ghc.patch b/standalone/android/haskell-patches/monad-control_0.3.1.4_0001-build-with-newer-ghc.patch new file mode 100644 index 0000000000..ee1c996d80 --- /dev/null +++ b/standalone/android/haskell-patches/monad-control_0.3.1.4_0001-build-with-newer-ghc.patch @@ -0,0 +1,25 @@ +From 3dde0175096903207c9774d8f6bba9b81ab6c2f9 Mon Sep 17 00:00:00 2001 +From: Joey Hess +Date: Thu, 28 Feb 2013 23:31:45 -0400 +Subject: [PATCH] build with newer ghc + +--- + monad-control.cabal | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/monad-control.cabal b/monad-control.cabal +index 2e3eb46..b12ffaf 100644 +--- a/monad-control.cabal ++++ b/monad-control.cabal +@@ -56,7 +56,7 @@ Library + + Exposed-modules: Control.Monad.Trans.Control + +- Build-depends: base >= 3 && < 4.7 ++ Build-depends: base >= 3 && < 4.8 + , base-unicode-symbols >= 0.1.1 && < 0.3 + , transformers >= 0.2 && < 0.4 + , transformers-base >= 0.4.1 && < 0.5 +-- +1.7.10.4 + diff --git a/standalone/android/haskell-patches/monad-logger_0.2.3.2_0001-remove-TH-logging-stuff.patch b/standalone/android/haskell-patches/monad-logger_0.2.3.2_0001-remove-TH-logging-stuff.patch new file mode 100644 index 0000000000..e684c67a79 --- /dev/null +++ b/standalone/android/haskell-patches/monad-logger_0.2.3.2_0001-remove-TH-logging-stuff.patch @@ -0,0 +1,124 @@ +From ca88563e63cc31f0b96b00d3a4fe1f0c56b1e1eb Mon Sep 17 00:00:00 2001 +From: Joey Hess +Date: Thu, 28 Feb 2013 23:32:01 -0400 +Subject: [PATCH] remove TH logging stuff + +--- + Control/Monad/Logger.hs | 76 ----------------------------------------------- + monad-logger.cabal | 2 +- + 2 files changed, 1 insertion(+), 77 deletions(-) + +diff --git a/Control/Monad/Logger.hs b/Control/Monad/Logger.hs +index fd1282b..80b8ed9 100644 +--- a/Control/Monad/Logger.hs ++++ b/Control/Monad/Logger.hs +@@ -27,18 +27,6 @@ module Control.Monad.Logger + , LoggingT (..) + , runStderrLoggingT + , runStdoutLoggingT +- -- * TH logging +- , logDebug +- , logInfo +- , logWarn +- , logError +- , logOther +- -- * TH logging with source +- , logDebugS +- , logInfoS +- , logWarnS +- , logErrorS +- , logOtherS + ) where + + import Language.Haskell.TH.Syntax (Lift (lift), Q, Exp, Loc (..), qLocation) +@@ -91,13 +79,6 @@ import Control.Monad.Writer.Class ( MonadWriter (..) ) + data LogLevel = LevelDebug | LevelInfo | LevelWarn | LevelError | LevelOther Text + deriving (Eq, Prelude.Show, Prelude.Read, Ord) + +-instance Lift LogLevel where +- lift LevelDebug = [|LevelDebug|] +- lift LevelInfo = [|LevelInfo|] +- lift LevelWarn = [|LevelWarn|] +- lift LevelError = [|LevelError|] +- lift (LevelOther x) = [|LevelOther $ pack $(lift $ unpack x)|] +- + type LogSource = Text + + class Monad m => MonadLogger m where +@@ -128,63 +109,6 @@ instance (MonadLogger m, Monoid w) => MonadLogger (Strict.WriterT w m) where DEF + instance (MonadLogger m, Monoid w) => MonadLogger (Strict.RWST r w s m) where DEF + #undef DEF + +-logTH :: LogLevel -> Q Exp +-logTH level = +- [|monadLoggerLog $(qLocation >>= liftLoc) $(lift level) . (id :: Text -> Text)|] +- +--- | Generates a function that takes a 'Text' and logs a 'LevelDebug' message. Usage: +--- +--- > $(logDebug) "This is a debug log message" +-logDebug :: Q Exp +-logDebug = logTH LevelDebug +- +--- | See 'logDebug' +-logInfo :: Q Exp +-logInfo = logTH LevelInfo +--- | See 'logDebug' +-logWarn :: Q Exp +-logWarn = logTH LevelWarn +--- | See 'logDebug' +-logError :: Q Exp +-logError = logTH LevelError +- +--- | Generates a function that takes a 'Text' and logs a 'LevelOther' message. Usage: +--- +--- > $(logOther "My new level") "This is a log message" +-logOther :: Text -> Q Exp +-logOther = logTH . LevelOther +- +-liftLoc :: Loc -> Q Exp +-liftLoc (Loc a b c (d1, d2) (e1, e2)) = [|Loc +- $(lift a) +- $(lift b) +- $(lift c) +- ($(lift d1), $(lift d2)) +- ($(lift e1), $(lift e2)) +- |] +- +--- | Generates a function that takes a 'LogSource' and 'Text' and logs a 'LevelDebug' message. Usage: +--- +--- > $logDebug "SomeSource" "This is a debug log message" +-logDebugS :: Q Exp +-logDebugS = [|\a b -> monadLoggerLogSource $(qLocation >>= liftLoc) a LevelDebug (b :: Text)|] +- +--- | See 'logDebugS' +-logInfoS :: Q Exp +-logInfoS = [|\a b -> monadLoggerLogSource $(qLocation >>= liftLoc) a LevelInfo (b :: Text)|] +--- | See 'logDebugS' +-logWarnS :: Q Exp +-logWarnS = [|\a b -> monadLoggerLogSource $(qLocation >>= liftLoc) a LevelWarn (b :: Text)|] +--- | See 'logDebugS' +-logErrorS :: Q Exp +-logErrorS = [|\a b -> monadLoggerLogSource $(qLocation >>= liftLoc) a LevelError (b :: Text)|] +- +--- | Generates a function that takes a 'LogSource', a level name and a 'Text' and logs a 'LevelOther' message. Usage: +--- +--- > $logOther "SomeSource" "My new level" "This is a log message" +-logOtherS :: Q Exp +-logOtherS = [|\src level msg -> monadLoggerLogSource $(qLocation >>= liftLoc) src (LevelOther level) (msg :: Text)|] +- + -- | Monad transformer that adds a new logging function. + -- + -- Since 0.2.2 +diff --git a/monad-logger.cabal b/monad-logger.cabal +index ab71424..fa3d292 100644 +--- a/monad-logger.cabal ++++ b/monad-logger.cabal +@@ -24,4 +24,4 @@ library + , transformers-base + , monad-control + , mtl +- , bytestring ++ , bytestring >= 0.10.3.0 +-- +1.7.10.4 + diff --git a/standalone/android/haskell-patches/network-conduit_0.6.2.2_0001-NoDelay-does-not-work-on-Android.patch b/standalone/android/haskell-patches/network-conduit_0.6.2.2_0001-NoDelay-does-not-work-on-Android.patch new file mode 100644 index 0000000000..35bafa774f --- /dev/null +++ b/standalone/android/haskell-patches/network-conduit_0.6.2.2_0001-NoDelay-does-not-work-on-Android.patch @@ -0,0 +1,43 @@ +From 3e05f3a3bf886c302fb6d6caa7ee92cf9736b6ad Mon Sep 17 00:00:00 2001 +From: Joey Hess +Date: Thu, 28 Feb 2013 23:33:45 -0400 +Subject: [PATCH] NoDelay does not work on Android + +(I think the other change is no-op) +--- + Data/Conduit/Network/Utils.hs | 6 +++--- + 1 file changed, 3 insertions(+), 3 deletions(-) + +diff --git a/Data/Conduit/Network/Utils.hs b/Data/Conduit/Network/Utils.hs +index 32a7286..01ff84e 100644 +--- a/Data/Conduit/Network/Utils.hs ++++ b/Data/Conduit/Network/Utils.hs +@@ -6,14 +6,14 @@ module Data.Conduit.Network.Utils + , getSocket + ) where + +-import Network.Socket (AddrInfo, Socket, SocketType) ++import Network.Socket (Socket, SocketType) + import qualified Network.Socket as NS + import Data.String (IsString (fromString)) + import Control.Exception (bracketOnError, IOException) + import qualified Control.Exception as E + + -- | Attempt to connect to the given host/port using given @SocketType@. +-getSocket :: String -> Int -> SocketType -> IO (Socket, AddrInfo) ++getSocket :: String -> Int -> SocketType -> IO (Socket, NS.AddrInfo) + getSocket host' port' sockettype = do + let hints = NS.defaultHints { + NS.addrFlags = [NS.AI_ADDRCONFIG] +@@ -93,7 +93,7 @@ bindPort p s sockettype = do + sockOpts = + case sockettype of + NS.Datagram -> [(NS.ReuseAddr,1)] +- _ -> [(NS.NoDelay,1), (NS.ReuseAddr,1)] ++ _ -> [(NS.ReuseAddr,1)] -- Android seems to not have NoDelay + + theBody addr = + bracketOnError +-- +1.7.10.4 + diff --git a/standalone/android/haskell-patches/network-protocol-xmpp_0.4.4-0001-avoid-using-gnuidn.patch b/standalone/android/haskell-patches/network-protocol-xmpp_0.4.4-0001-avoid-using-gnuidn.patch new file mode 100644 index 0000000000..26734fa708 --- /dev/null +++ b/standalone/android/haskell-patches/network-protocol-xmpp_0.4.4-0001-avoid-using-gnuidn.patch @@ -0,0 +1,60 @@ +From d15ae2193eff9cd38ebce641279996233434b50f Mon Sep 17 00:00:00 2001 +From: Joey Hess +Date: Sun, 21 Apr 2013 16:05:53 -0400 +Subject: [PATCH] avoid using gnuidn + +IDN is only used to handle the domain name part of a XMPP server JID. +Which seems not worth the bloat on Android. +--- + lib/Network/Protocol/XMPP/JID.hs | 11 ++++------- + network-protocol-xmpp.cabal | 1 - + 2 files changed, 4 insertions(+), 8 deletions(-) + +diff --git a/lib/Network/Protocol/XMPP/JID.hs b/lib/Network/Protocol/XMPP/JID.hs +index 91745e0..2a50409 100644 +--- a/lib/Network/Protocol/XMPP/JID.hs ++++ b/lib/Network/Protocol/XMPP/JID.hs +@@ -29,7 +29,6 @@ module Network.Protocol.XMPP.JID + + import qualified Data.Text + import Data.Text (Text) +-import qualified Data.Text.IDN.StringPrep as SP + import Data.String (IsString, fromString) + + newtype Node = Node { strNode :: Text } +@@ -85,16 +84,14 @@ parseJID str = maybeJID where + then Just Nothing + else fmap Just (f x) + maybeJID = do +- preppedNode <- nullable node (stringprepM SP.xmppNode) +- preppedDomain <- stringprepM SP.nameprep domain +- preppedResource <- nullable resource (stringprepM SP.xmppResource) ++ preppedNode <- nullable node (stringprepM id) ++ preppedDomain <- stringprepM id domain ++ preppedResource <- nullable resource (stringprepM id) + return $ JID + (fmap Node preppedNode) + (Domain preppedDomain) + (fmap Resource preppedResource) +- stringprepM p x = case SP.stringprep p SP.defaultFlags x of +- Left _ -> Nothing +- Right y -> Just y ++ stringprepM p x = Just x + + parseJID_ :: Text -> JID + parseJID_ text = case parseJID text of +diff --git a/network-protocol-xmpp.cabal b/network-protocol-xmpp.cabal +index 807cda9..3aaad67 100644 +--- a/network-protocol-xmpp.cabal ++++ b/network-protocol-xmpp.cabal +@@ -30,7 +30,6 @@ library + build-depends: + base >= 4.0 && < 5.0 + , bytestring >= 0.9 +- , gnuidn >= 0.2 && < 0.3 + , gnutls >= 0.1.4 && < 0.3 + , gsasl >= 0.3 && < 0.4 + , libxml-sax >= 0.7 && < 0.8 +-- +1.7.10.4 + diff --git a/standalone/android/haskell-patches/network_2.4.1.0_0001-android-port-fixes.patch b/standalone/android/haskell-patches/network_2.4.1.0_0001-android-port-fixes.patch new file mode 100644 index 0000000000..d7d0608d21 --- /dev/null +++ b/standalone/android/haskell-patches/network_2.4.1.0_0001-android-port-fixes.patch @@ -0,0 +1,1960 @@ +From 9750532bd6200353fe09dda65ee6fb59702c4ac1 Mon Sep 17 00:00:00 2001 +From: Joey Hess +Date: Thu, 28 Feb 2013 23:32:15 -0400 +Subject: [PATCH] android port fixes + +Build note: Ensure a hsc2hs in PATH is modified to pass -x to the real +one, to enable cross-compiling. +--- + Network/Socket.hsc | 22 +- + Network/Socket/ByteString.hsc | 2 +- + Network/Socket/Internal.hsc | 2 +- + Network/Socket/Types.hsc | 4 +- + cbits/HsNet.c | 14 + + config.guess | 562 ++++++++++++++++++++++------------------- + config.sub | 384 ++++++++++++++++++++-------- + configure | 1 + + include/HsNetworkConfig.h | 8 +- + 9 files changed, 612 insertions(+), 387 deletions(-) + +diff --git a/Network/Socket.hsc b/Network/Socket.hsc +index 259e843..e6c0feb 100644 +--- a/Network/Socket.hsc ++++ b/Network/Socket.hsc +@@ -38,7 +38,7 @@ module Network.Socket + , SockAddr(..) + , SocketStatus(..) + , HostAddress +-#if defined(IPV6_SOCKET_SUPPORT) ++#if defined(IPV6_SOCKET_SUPPORTNO) + , HostAddress6 + , FlowInfo + , ScopeID +@@ -55,7 +55,7 @@ module Network.Socket + , HostName + , ServiceName + +-#if defined(IPV6_SOCKET_SUPPORT) ++#if defined(IPV6_SOCKET_SUPPORT) || 1 + , AddrInfo(..) + + , AddrInfoFlag(..) +@@ -134,7 +134,7 @@ module Network.Socket + -- * Special constants + , aNY_PORT + , iNADDR_ANY +-#if defined(IPV6_SOCKET_SUPPORT) ++#if defined(IPV6_SOCKET_SUPPORTNO) + , iN6ADDR_ANY + #endif + , sOMAXCONN +@@ -330,16 +330,6 @@ socket family stype protocol = do + setNonBlockIfNeeded fd + socket_status <- newMVar NotConnected + let sock = MkSocket fd family stype protocol socket_status +-#if HAVE_DECL_IPV6_V6ONLY +-# if defined(mingw32_HOST_OS) +- -- the IPv6Only option is only supported on Windows Vista and later, +- -- so trying to change it might throw an error +- when (family == AF_INET6) $ +- E.catch (setSocketOption sock IPv6Only 0) $ (\(_ :: E.IOException) -> return ()) +-# else +- when (family == AF_INET6) $ setSocketOption sock IPv6Only 0 +-# endif +-#endif + return sock + + -- | Build a pair of connected socket objects using the given address +@@ -1043,9 +1033,9 @@ aNY_PORT = 0 + iNADDR_ANY :: HostAddress + iNADDR_ANY = htonl (#const INADDR_ANY) + +-foreign import CALLCONV unsafe "htonl" htonl :: Word32 -> Word32 ++foreign import CALLCONV unsafe "my_htonl" htonl :: Word32 -> Word32 + +-#if defined(IPV6_SOCKET_SUPPORT) ++#if defined(IPV6_SOCKET_SUPPORTNO) + -- | The IPv6 wild card address. + + iN6ADDR_ANY :: HostAddress6 +@@ -1219,7 +1209,7 @@ unpackBits ((k,v):xs) r + ----------------------------------------------------------------------------- + -- Address and service lookups + +-#if defined(IPV6_SOCKET_SUPPORT) ++#if defined(IPV6_SOCKET_SUPPORT) || 1 + + -- | Flags that control the querying behaviour of 'getAddrInfo'. + data AddrInfoFlag +diff --git a/Network/Socket/ByteString.hsc b/Network/Socket/ByteString.hsc +index bec2eb9..cb8ed8c 100644 +--- a/Network/Socket/ByteString.hsc ++++ b/Network/Socket/ByteString.hsc +@@ -201,7 +201,7 @@ sendMany sock@(MkSocket fd _ _ _ _) cs = do + liftM fromIntegral . withIOVec cs $ \(iovsPtr, iovsLen) -> + throwSocketErrorWaitWrite sock "writev" $ + c_writev (fromIntegral fd) iovsPtr +- (fromIntegral (min iovsLen (#const IOV_MAX))) ++ (fromIntegral (min iovsLen (0x0026))) + #else + sendMany sock = sendAll sock . B.concat + #endif +diff --git a/Network/Socket/Internal.hsc b/Network/Socket/Internal.hsc +index 96fe9c6..df5ce64 100644 +--- a/Network/Socket/Internal.hsc ++++ b/Network/Socket/Internal.hsc +@@ -24,7 +24,7 @@ module Network.Socket.Internal + ( + -- * Socket addresses + HostAddress +-#if defined(IPV6_SOCKET_SUPPORT) ++#if defined(IPV6_SOCKET_SUPPORTNO) + , HostAddress6 + , FlowInfo + , ScopeID +diff --git a/Network/Socket/Types.hsc b/Network/Socket/Types.hsc +index 7ad24f1..dad1d1d 100644 +--- a/Network/Socket/Types.hsc ++++ b/Network/Socket/Types.hsc +@@ -705,8 +705,8 @@ intToPortNumber v = PortNum (htons (fromIntegral v)) + portNumberToInt :: PortNumber -> Int + portNumberToInt (PortNum po) = fromIntegral (ntohs po) + +-foreign import CALLCONV unsafe "ntohs" ntohs :: Word16 -> Word16 +-foreign import CALLCONV unsafe "htons" htons :: Word16 -> Word16 ++foreign import CALLCONV unsafe "my_ntohs" ntohs :: Word16 -> Word16 ++foreign import CALLCONV unsafe "my_htons" htons :: Word16 -> Word16 + --foreign import CALLCONV unsafe "ntohl" ntohl :: Word32 -> Word32 + + instance Enum PortNumber where +diff --git a/cbits/HsNet.c b/cbits/HsNet.c +index 86b55dc..5ea1199 100644 +--- a/cbits/HsNet.c ++++ b/cbits/HsNet.c +@@ -6,3 +6,17 @@ + + #define INLINE + #include "HsNet.h" ++ ++#include ++uint16_t my_htons(uint16_t v) ++{ ++ htons(v); ++} ++uint32_t my_htonl(uint32_t v) ++{ ++ htonl(v); ++} ++uint16_t my_ntohs(uint16_t v) ++{ ++ ntohs(v); ++} +diff --git a/config.guess b/config.guess +index c38553d..1804e9f 100644 +--- a/config.guess ++++ b/config.guess +@@ -1,13 +1,14 @@ + #! /bin/sh + # Attempt to guess a canonical system name. + # Copyright (C) 1992, 1993, 1994, 1995, 1996, 1997, 1998, 1999, +-# 2000, 2001, 2002, 2003, 2004, 2005 Free Software Foundation, Inc. ++# 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, ++# 2011, 2012, 2013 Free Software Foundation, Inc. + +-timestamp='2006-02-23' ++timestamp='2012-12-29' + + # This file is free software; you can redistribute it and/or modify it + # under the terms of the GNU General Public License as published by +-# the Free Software Foundation; either version 2 of the License, or ++# the Free Software Foundation; either version 3 of the License, or + # (at your option) any later version. + # + # This program is distributed in the hope that it will be useful, but +@@ -16,26 +17,22 @@ timestamp='2006-02-23' + # General Public License for more details. + # + # You should have received a copy of the GNU General Public License +-# along with this program; if not, write to the Free Software +-# Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA +-# 02110-1301, USA. ++# along with this program; if not, see . + # + # As a special exception to the GNU General Public License, if you + # distribute this file as part of a program that contains a + # configuration script generated by Autoconf, you may include it under +-# the same distribution terms that you use for the rest of that program. +- +- +-# Originally written by Per Bothner . +-# Please send patches to . Submit a context +-# diff and a properly formatted ChangeLog entry. ++# the same distribution terms that you use for the rest of that ++# program. This Exception is an additional permission under section 7 ++# of the GNU General Public License, version 3 ("GPLv3"). ++# ++# Originally written by Per Bothner. + # +-# This script attempts to guess a canonical system name similar to +-# config.sub. If it succeeds, it prints the system name on stdout, and +-# exits with 0. Otherwise, it exits with 1. ++# You can get the latest version of this script from: ++# http://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=config.guess;hb=HEAD + # +-# The plan is that this can be called by configure scripts if you +-# don't specify an explicit build system type. ++# Please send patches with a ChangeLog entry to config-patches@gnu.org. ++ + + me=`echo "$0" | sed -e 's,.*/,,'` + +@@ -55,8 +52,9 @@ version="\ + GNU config.guess ($timestamp) + + Originally written by Per Bothner. +-Copyright (C) 1992, 1993, 1994, 1995, 1996, 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005 +-Free Software Foundation, Inc. ++Copyright (C) 1992, 1993, 1994, 1995, 1996, 1997, 1998, 1999, 2000, ++2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, ++2012, 2013 Free Software Foundation, Inc. + + This is free software; see the source for copying conditions. There is NO + warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE." +@@ -143,7 +141,7 @@ UNAME_VERSION=`(uname -v) 2>/dev/null` || UNAME_VERSION=unknown + case "${UNAME_MACHINE}:${UNAME_SYSTEM}:${UNAME_RELEASE}:${UNAME_VERSION}" in + *:NetBSD:*:*) + # NetBSD (nbsd) targets should (where applicable) match one or +- # more of the tupples: *-*-netbsdelf*, *-*-netbsdaout*, ++ # more of the tuples: *-*-netbsdelf*, *-*-netbsdaout*, + # *-*-netbsdecoff* and *-*-netbsd*. For targets that recently + # switched to ELF, *-*-netbsd* would select the old + # object file format. This provides both forward +@@ -160,6 +158,7 @@ case "${UNAME_MACHINE}:${UNAME_SYSTEM}:${UNAME_RELEASE}:${UNAME_VERSION}" in + arm*) machine=arm-unknown ;; + sh3el) machine=shl-unknown ;; + sh3eb) machine=sh-unknown ;; ++ sh5el) machine=sh5le-unknown ;; + *) machine=${UNAME_MACHINE_ARCH}-unknown ;; + esac + # The Operating System including object format, if it has switched +@@ -168,7 +167,7 @@ case "${UNAME_MACHINE}:${UNAME_SYSTEM}:${UNAME_RELEASE}:${UNAME_VERSION}" in + arm*|i386|m68k|ns32k|sh3*|sparc|vax) + eval $set_cc_for_build + if echo __ELF__ | $CC_FOR_BUILD -E - 2>/dev/null \ +- | grep __ELF__ >/dev/null ++ | grep -q __ELF__ + then + # Once all utilities can be ECOFF (netbsdecoff) or a.out (netbsdaout). + # Return netbsd for either. FIX? +@@ -178,7 +177,7 @@ case "${UNAME_MACHINE}:${UNAME_SYSTEM}:${UNAME_RELEASE}:${UNAME_VERSION}" in + fi + ;; + *) +- os=netbsd ++ os=netbsd + ;; + esac + # The OS release +@@ -199,6 +198,10 @@ case "${UNAME_MACHINE}:${UNAME_SYSTEM}:${UNAME_RELEASE}:${UNAME_VERSION}" in + # CPU_TYPE-MANUFACTURER-OPERATING_SYSTEM is used. + echo "${machine}-${os}${release}" + exit ;; ++ *:Bitrig:*:*) ++ UNAME_MACHINE_ARCH=`arch | sed 's/Bitrig.//'` ++ echo ${UNAME_MACHINE_ARCH}-unknown-bitrig${UNAME_RELEASE} ++ exit ;; + *:OpenBSD:*:*) + UNAME_MACHINE_ARCH=`arch | sed 's/OpenBSD.//'` + echo ${UNAME_MACHINE_ARCH}-unknown-openbsd${UNAME_RELEASE} +@@ -210,7 +213,7 @@ case "${UNAME_MACHINE}:${UNAME_SYSTEM}:${UNAME_RELEASE}:${UNAME_VERSION}" in + echo ${UNAME_MACHINE}-unknown-solidbsd${UNAME_RELEASE} + exit ;; + macppc:MirBSD:*:*) +- echo powerppc-unknown-mirbsd${UNAME_RELEASE} ++ echo powerpc-unknown-mirbsd${UNAME_RELEASE} + exit ;; + *:MirBSD:*:*) + echo ${UNAME_MACHINE}-unknown-mirbsd${UNAME_RELEASE} +@@ -221,7 +224,7 @@ case "${UNAME_MACHINE}:${UNAME_SYSTEM}:${UNAME_RELEASE}:${UNAME_VERSION}" in + UNAME_RELEASE=`/usr/sbin/sizer -v | awk '{print $3}'` + ;; + *5.*) +- UNAME_RELEASE=`/usr/sbin/sizer -v | awk '{print $4}'` ++ UNAME_RELEASE=`/usr/sbin/sizer -v | awk '{print $4}'` + ;; + esac + # According to Compaq, /usr/sbin/psrinfo has been available on +@@ -267,7 +270,10 @@ case "${UNAME_MACHINE}:${UNAME_SYSTEM}:${UNAME_RELEASE}:${UNAME_VERSION}" in + # A Xn.n version is an unreleased experimental baselevel. + # 1.2 uses "1.2" for uname -r. + echo ${UNAME_MACHINE}-dec-osf`echo ${UNAME_RELEASE} | sed -e 's/^[PVTX]//' | tr 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 'abcdefghijklmnopqrstuvwxyz'` +- exit ;; ++ # Reset EXIT trap before exiting to avoid spurious non-zero exit code. ++ exitcode=$? ++ trap '' 0 ++ exit $exitcode ;; + Alpha\ *:Windows_NT*:*) + # How do we know it's Interix rather than the generic POSIX subsystem? + # Should we change UNAME_MACHINE based on the output of uname instead +@@ -293,12 +299,12 @@ case "${UNAME_MACHINE}:${UNAME_SYSTEM}:${UNAME_RELEASE}:${UNAME_VERSION}" in + echo s390-ibm-zvmoe + exit ;; + *:OS400:*:*) +- echo powerpc-ibm-os400 ++ echo powerpc-ibm-os400 + exit ;; + arm:RISC*:1.[012]*:*|arm:riscix:1.[012]*:*) + echo arm-acorn-riscix${UNAME_RELEASE} + exit ;; +- arm:riscos:*:*|arm:RISCOS:*:*) ++ arm*:riscos:*:*|arm*:RISCOS:*:*) + echo arm-unknown-riscos + exit ;; + SR2?01:HI-UX/MPP:*:* | SR8000:HI-UX/MPP:*:*) +@@ -322,14 +328,33 @@ case "${UNAME_MACHINE}:${UNAME_SYSTEM}:${UNAME_RELEASE}:${UNAME_VERSION}" in + case `/usr/bin/uname -p` in + sparc) echo sparc-icl-nx7; exit ;; + esac ;; ++ s390x:SunOS:*:*) ++ echo ${UNAME_MACHINE}-ibm-solaris2`echo ${UNAME_RELEASE}|sed -e 's/[^.]*//'` ++ exit ;; + sun4H:SunOS:5.*:*) + echo sparc-hal-solaris2`echo ${UNAME_RELEASE}|sed -e 's/[^.]*//'` + exit ;; + sun4*:SunOS:5.*:* | tadpole*:SunOS:5.*:*) + echo sparc-sun-solaris2`echo ${UNAME_RELEASE}|sed -e 's/[^.]*//'` + exit ;; +- i86pc:SunOS:5.*:*) +- echo i386-pc-solaris2`echo ${UNAME_RELEASE}|sed -e 's/[^.]*//'` ++ i86pc:AuroraUX:5.*:* | i86xen:AuroraUX:5.*:*) ++ echo i386-pc-auroraux${UNAME_RELEASE} ++ exit ;; ++ i86pc:SunOS:5.*:* | i86xen:SunOS:5.*:*) ++ eval $set_cc_for_build ++ SUN_ARCH="i386" ++ # If there is a compiler, see if it is configured for 64-bit objects. ++ # Note that the Sun cc does not turn __LP64__ into 1 like gcc does. ++ # This test works for both compilers. ++ if [ "$CC_FOR_BUILD" != 'no_compiler_found' ]; then ++ if (echo '#ifdef __amd64'; echo IS_64BIT_ARCH; echo '#endif') | \ ++ (CCOPTS= $CC_FOR_BUILD -E - 2>/dev/null) | \ ++ grep IS_64BIT_ARCH >/dev/null ++ then ++ SUN_ARCH="x86_64" ++ fi ++ fi ++ echo ${SUN_ARCH}-pc-solaris2`echo ${UNAME_RELEASE}|sed -e 's/[^.]*//'` + exit ;; + sun4*:SunOS:6*:*) + # According to config.sub, this is the proper way to canonicalize +@@ -373,23 +398,23 @@ case "${UNAME_MACHINE}:${UNAME_SYSTEM}:${UNAME_RELEASE}:${UNAME_VERSION}" in + # MiNT. But MiNT is downward compatible to TOS, so this should + # be no problem. + atarist[e]:*MiNT:*:* | atarist[e]:*mint:*:* | atarist[e]:*TOS:*:*) +- echo m68k-atari-mint${UNAME_RELEASE} ++ echo m68k-atari-mint${UNAME_RELEASE} + exit ;; + atari*:*MiNT:*:* | atari*:*mint:*:* | atarist[e]:*TOS:*:*) + echo m68k-atari-mint${UNAME_RELEASE} +- exit ;; ++ exit ;; + *falcon*:*MiNT:*:* | *falcon*:*mint:*:* | *falcon*:*TOS:*:*) +- echo m68k-atari-mint${UNAME_RELEASE} ++ echo m68k-atari-mint${UNAME_RELEASE} + exit ;; + milan*:*MiNT:*:* | milan*:*mint:*:* | *milan*:*TOS:*:*) +- echo m68k-milan-mint${UNAME_RELEASE} +- exit ;; ++ echo m68k-milan-mint${UNAME_RELEASE} ++ exit ;; + hades*:*MiNT:*:* | hades*:*mint:*:* | *hades*:*TOS:*:*) +- echo m68k-hades-mint${UNAME_RELEASE} +- exit ;; ++ echo m68k-hades-mint${UNAME_RELEASE} ++ exit ;; + *:*MiNT:*:* | *:*mint:*:* | *:*TOS:*:*) +- echo m68k-unknown-mint${UNAME_RELEASE} +- exit ;; ++ echo m68k-unknown-mint${UNAME_RELEASE} ++ exit ;; + m68k:machten:*:*) + echo m68k-apple-machten${UNAME_RELEASE} + exit ;; +@@ -459,8 +484,8 @@ EOF + echo m88k-motorola-sysv3 + exit ;; + AViiON:dgux:*:*) +- # DG/UX returns AViiON for all architectures +- UNAME_PROCESSOR=`/usr/bin/uname -p` ++ # DG/UX returns AViiON for all architectures ++ UNAME_PROCESSOR=`/usr/bin/uname -p` + if [ $UNAME_PROCESSOR = mc88100 ] || [ $UNAME_PROCESSOR = mc88110 ] + then + if [ ${TARGET_BINARY_INTERFACE}x = m88kdguxelfx ] || \ +@@ -473,7 +498,7 @@ EOF + else + echo i586-dg-dgux${UNAME_RELEASE} + fi +- exit ;; ++ exit ;; + M88*:DolphinOS:*:*) # DolphinOS (SVR3) + echo m88k-dolphin-sysv3 + exit ;; +@@ -530,7 +555,7 @@ EOF + echo rs6000-ibm-aix3.2 + fi + exit ;; +- *:AIX:*:[45]) ++ *:AIX:*:[4567]) + IBM_CPU_ID=`/usr/sbin/lsdev -C -c processor -S available | sed 1q | awk '{ print $1 }'` + if /usr/sbin/lsattr -El ${IBM_CPU_ID} | grep ' POWER' >/dev/null 2>&1; then + IBM_ARCH=rs6000 +@@ -573,52 +598,52 @@ EOF + 9000/[678][0-9][0-9]) + if [ -x /usr/bin/getconf ]; then + sc_cpu_version=`/usr/bin/getconf SC_CPU_VERSION 2>/dev/null` +- sc_kernel_bits=`/usr/bin/getconf SC_KERNEL_BITS 2>/dev/null` +- case "${sc_cpu_version}" in +- 523) HP_ARCH="hppa1.0" ;; # CPU_PA_RISC1_0 +- 528) HP_ARCH="hppa1.1" ;; # CPU_PA_RISC1_1 +- 532) # CPU_PA_RISC2_0 +- case "${sc_kernel_bits}" in +- 32) HP_ARCH="hppa2.0n" ;; +- 64) HP_ARCH="hppa2.0w" ;; ++ sc_kernel_bits=`/usr/bin/getconf SC_KERNEL_BITS 2>/dev/null` ++ case "${sc_cpu_version}" in ++ 523) HP_ARCH="hppa1.0" ;; # CPU_PA_RISC1_0 ++ 528) HP_ARCH="hppa1.1" ;; # CPU_PA_RISC1_1 ++ 532) # CPU_PA_RISC2_0 ++ case "${sc_kernel_bits}" in ++ 32) HP_ARCH="hppa2.0n" ;; ++ 64) HP_ARCH="hppa2.0w" ;; + '') HP_ARCH="hppa2.0" ;; # HP-UX 10.20 +- esac ;; +- esac ++ esac ;; ++ esac + fi + if [ "${HP_ARCH}" = "" ]; then + eval $set_cc_for_build +- sed 's/^ //' << EOF >$dummy.c ++ sed 's/^ //' << EOF >$dummy.c + +- #define _HPUX_SOURCE +- #include +- #include ++ #define _HPUX_SOURCE ++ #include ++ #include + +- int main () +- { +- #if defined(_SC_KERNEL_BITS) +- long bits = sysconf(_SC_KERNEL_BITS); +- #endif +- long cpu = sysconf (_SC_CPU_VERSION); ++ int main () ++ { ++ #if defined(_SC_KERNEL_BITS) ++ long bits = sysconf(_SC_KERNEL_BITS); ++ #endif ++ long cpu = sysconf (_SC_CPU_VERSION); + +- switch (cpu) +- { +- case CPU_PA_RISC1_0: puts ("hppa1.0"); break; +- case CPU_PA_RISC1_1: puts ("hppa1.1"); break; +- case CPU_PA_RISC2_0: +- #if defined(_SC_KERNEL_BITS) +- switch (bits) +- { +- case 64: puts ("hppa2.0w"); break; +- case 32: puts ("hppa2.0n"); break; +- default: puts ("hppa2.0"); break; +- } break; +- #else /* !defined(_SC_KERNEL_BITS) */ +- puts ("hppa2.0"); break; +- #endif +- default: puts ("hppa1.0"); break; +- } +- exit (0); +- } ++ switch (cpu) ++ { ++ case CPU_PA_RISC1_0: puts ("hppa1.0"); break; ++ case CPU_PA_RISC1_1: puts ("hppa1.1"); break; ++ case CPU_PA_RISC2_0: ++ #if defined(_SC_KERNEL_BITS) ++ switch (bits) ++ { ++ case 64: puts ("hppa2.0w"); break; ++ case 32: puts ("hppa2.0n"); break; ++ default: puts ("hppa2.0"); break; ++ } break; ++ #else /* !defined(_SC_KERNEL_BITS) */ ++ puts ("hppa2.0"); break; ++ #endif ++ default: puts ("hppa1.0"); break; ++ } ++ exit (0); ++ } + EOF + (CCOPTS= $CC_FOR_BUILD -o $dummy $dummy.c 2>/dev/null) && HP_ARCH=`$dummy` + test -z "$HP_ARCH" && HP_ARCH=hppa +@@ -638,7 +663,7 @@ EOF + # => hppa64-hp-hpux11.23 + + if echo __LP64__ | (CCOPTS= $CC_FOR_BUILD -E - 2>/dev/null) | +- grep __LP64__ >/dev/null ++ grep -q __LP64__ + then + HP_ARCH="hppa2.0w" + else +@@ -709,22 +734,22 @@ EOF + exit ;; + C1*:ConvexOS:*:* | convex:ConvexOS:C1*:*) + echo c1-convex-bsd +- exit ;; ++ exit ;; + C2*:ConvexOS:*:* | convex:ConvexOS:C2*:*) + if getsysinfo -f scalar_acc + then echo c32-convex-bsd + else echo c2-convex-bsd + fi +- exit ;; ++ exit ;; + C34*:ConvexOS:*:* | convex:ConvexOS:C34*:*) + echo c34-convex-bsd +- exit ;; ++ exit ;; + C38*:ConvexOS:*:* | convex:ConvexOS:C38*:*) + echo c38-convex-bsd +- exit ;; ++ exit ;; + C4*:ConvexOS:*:* | convex:ConvexOS:C4*:*) + echo c4-convex-bsd +- exit ;; ++ exit ;; + CRAY*Y-MP:*:*:*) + echo ymp-cray-unicos${UNAME_RELEASE} | sed -e 's/\.[^.]*$/.X/' + exit ;; +@@ -748,14 +773,14 @@ EOF + exit ;; + F30[01]:UNIX_System_V:*:* | F700:UNIX_System_V:*:*) + FUJITSU_PROC=`uname -m | tr 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 'abcdefghijklmnopqrstuvwxyz'` +- FUJITSU_SYS=`uname -p | tr 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 'abcdefghijklmnopqrstuvwxyz' | sed -e 's/\///'` +- FUJITSU_REL=`echo ${UNAME_RELEASE} | sed -e 's/ /_/'` +- echo "${FUJITSU_PROC}-fujitsu-${FUJITSU_SYS}${FUJITSU_REL}" +- exit ;; ++ FUJITSU_SYS=`uname -p | tr 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 'abcdefghijklmnopqrstuvwxyz' | sed -e 's/\///'` ++ FUJITSU_REL=`echo ${UNAME_RELEASE} | sed -e 's/ /_/'` ++ echo "${FUJITSU_PROC}-fujitsu-${FUJITSU_SYS}${FUJITSU_REL}" ++ exit ;; + 5000:UNIX_System_V:4.*:*) +- FUJITSU_SYS=`uname -p | tr 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 'abcdefghijklmnopqrstuvwxyz' | sed -e 's/\///'` +- FUJITSU_REL=`echo ${UNAME_RELEASE} | tr 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 'abcdefghijklmnopqrstuvwxyz' | sed -e 's/ /_/'` +- echo "sparc-fujitsu-${FUJITSU_SYS}${FUJITSU_REL}" ++ FUJITSU_SYS=`uname -p | tr 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 'abcdefghijklmnopqrstuvwxyz' | sed -e 's/\///'` ++ FUJITSU_REL=`echo ${UNAME_RELEASE} | tr 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 'abcdefghijklmnopqrstuvwxyz' | sed -e 's/ /_/'` ++ echo "sparc-fujitsu-${FUJITSU_SYS}${FUJITSU_REL}" + exit ;; + i*86:BSD/386:*:* | i*86:BSD/OS:*:* | *:Ascend\ Embedded/OS:*:*) + echo ${UNAME_MACHINE}-pc-bsdi${UNAME_RELEASE} +@@ -767,38 +792,51 @@ EOF + echo ${UNAME_MACHINE}-unknown-bsdi${UNAME_RELEASE} + exit ;; + *:FreeBSD:*:*) +- case ${UNAME_MACHINE} in +- pc98) +- echo i386-unknown-freebsd`echo ${UNAME_RELEASE}|sed -e 's/[-(].*//'` ;; ++ UNAME_PROCESSOR=`/usr/bin/uname -p` ++ case ${UNAME_PROCESSOR} in ++ amd64) ++ echo x86_64-unknown-freebsd`echo ${UNAME_RELEASE}|sed -e 's/[-(].*//'` ;; + *) +- echo ${UNAME_MACHINE}-unknown-freebsd`echo ${UNAME_RELEASE}|sed -e 's/[-(].*//'` ;; ++ echo ${UNAME_PROCESSOR}-unknown-freebsd`echo ${UNAME_RELEASE}|sed -e 's/[-(].*//'` ;; + esac + exit ;; + i*:CYGWIN*:*) + echo ${UNAME_MACHINE}-pc-cygwin + exit ;; +- i*:MINGW*:*) +- echo ${UNAME_MACHINE}-pc-mingw32 ++ *:MINGW64*:*) ++ echo ${UNAME_MACHINE}-pc-mingw64 + exit ;; +- i*:MSYS_NT-*:*:*) ++ *:MINGW*:*) + echo ${UNAME_MACHINE}-pc-mingw32 + exit ;; ++ i*:MSYS*:*) ++ echo ${UNAME_MACHINE}-pc-msys ++ exit ;; + i*:windows32*:*) +- # uname -m includes "-pc" on this system. +- echo ${UNAME_MACHINE}-mingw32 ++ # uname -m includes "-pc" on this system. ++ echo ${UNAME_MACHINE}-mingw32 + exit ;; + i*:PW*:*) + echo ${UNAME_MACHINE}-pc-pw32 + exit ;; +- x86:Interix*:[345]*) +- echo i586-pc-interix${UNAME_RELEASE} +- exit ;; +- EM64T:Interix*:[345]*) +- echo x86_64-unknown-interix${UNAME_RELEASE} +- exit ;; ++ *:Interix*:*) ++ case ${UNAME_MACHINE} in ++ x86) ++ echo i586-pc-interix${UNAME_RELEASE} ++ exit ;; ++ authenticamd | genuineintel | EM64T) ++ echo x86_64-unknown-interix${UNAME_RELEASE} ++ exit ;; ++ IA64) ++ echo ia64-unknown-interix${UNAME_RELEASE} ++ exit ;; ++ esac ;; + [345]86:Windows_95:* | [345]86:Windows_98:* | [345]86:Windows_NT:*) + echo i${UNAME_MACHINE}-pc-mks + exit ;; ++ 8664:Windows_NT:*) ++ echo x86_64-pc-mks ++ exit ;; + i*:Windows_NT*:* | Pentium*:Windows_NT*:*) + # How do we know it's Interix rather than the generic POSIX subsystem? + # It also conflicts with pre-2.0 versions of AT&T UWIN. Should we +@@ -828,17 +866,68 @@ EOF + i*86:Minix:*:*) + echo ${UNAME_MACHINE}-pc-minix + exit ;; ++ aarch64:Linux:*:*) ++ echo ${UNAME_MACHINE}-unknown-linux-gnu ++ exit ;; ++ aarch64_be:Linux:*:*) ++ UNAME_MACHINE=aarch64_be ++ echo ${UNAME_MACHINE}-unknown-linux-gnu ++ exit ;; ++ alpha:Linux:*:*) ++ case `sed -n '/^cpu model/s/^.*: \(.*\)/\1/p' < /proc/cpuinfo` in ++ EV5) UNAME_MACHINE=alphaev5 ;; ++ EV56) UNAME_MACHINE=alphaev56 ;; ++ PCA56) UNAME_MACHINE=alphapca56 ;; ++ PCA57) UNAME_MACHINE=alphapca56 ;; ++ EV6) UNAME_MACHINE=alphaev6 ;; ++ EV67) UNAME_MACHINE=alphaev67 ;; ++ EV68*) UNAME_MACHINE=alphaev68 ;; ++ esac ++ objdump --private-headers /bin/sh | grep -q ld.so.1 ++ if test "$?" = 0 ; then LIBC="libc1" ; else LIBC="" ; fi ++ echo ${UNAME_MACHINE}-unknown-linux-gnu${LIBC} ++ exit ;; + arm*:Linux:*:*) ++ eval $set_cc_for_build ++ if echo __ARM_EABI__ | $CC_FOR_BUILD -E - 2>/dev/null \ ++ | grep -q __ARM_EABI__ ++ then ++ echo ${UNAME_MACHINE}-unknown-linux-gnu ++ else ++ if echo __ARM_PCS_VFP | $CC_FOR_BUILD -E - 2>/dev/null \ ++ | grep -q __ARM_PCS_VFP ++ then ++ echo ${UNAME_MACHINE}-unknown-linux-gnueabi ++ else ++ echo ${UNAME_MACHINE}-unknown-linux-gnueabihf ++ fi ++ fi ++ exit ;; ++ avr32*:Linux:*:*) + echo ${UNAME_MACHINE}-unknown-linux-gnu + exit ;; + cris:Linux:*:*) +- echo cris-axis-linux-gnu ++ echo ${UNAME_MACHINE}-axis-linux-gnu + exit ;; + crisv32:Linux:*:*) +- echo crisv32-axis-linux-gnu ++ echo ${UNAME_MACHINE}-axis-linux-gnu + exit ;; + frv:Linux:*:*) +- echo frv-unknown-linux-gnu ++ echo ${UNAME_MACHINE}-unknown-linux-gnu ++ exit ;; ++ hexagon:Linux:*:*) ++ echo ${UNAME_MACHINE}-unknown-linux-gnu ++ exit ;; ++ i*86:Linux:*:*) ++ LIBC=gnu ++ eval $set_cc_for_build ++ sed 's/^ //' << EOF >$dummy.c ++ #ifdef __dietlibc__ ++ LIBC=dietlibc ++ #endif ++EOF ++ eval `$CC_FOR_BUILD -E $dummy.c 2>/dev/null | grep '^LIBC'` ++ echo "${UNAME_MACHINE}-pc-linux-${LIBC}" + exit ;; + ia64:Linux:*:*) + echo ${UNAME_MACHINE}-unknown-linux-gnu +@@ -849,74 +938,33 @@ EOF + m68*:Linux:*:*) + echo ${UNAME_MACHINE}-unknown-linux-gnu + exit ;; +- mips:Linux:*:*) +- eval $set_cc_for_build +- sed 's/^ //' << EOF >$dummy.c +- #undef CPU +- #undef mips +- #undef mipsel +- #if defined(__MIPSEL__) || defined(__MIPSEL) || defined(_MIPSEL) || defined(MIPSEL) +- CPU=mipsel +- #else +- #if defined(__MIPSEB__) || defined(__MIPSEB) || defined(_MIPSEB) || defined(MIPSEB) +- CPU=mips +- #else +- CPU= +- #endif +- #endif +-EOF +- eval "`$CC_FOR_BUILD -E $dummy.c 2>/dev/null | sed -n ' +- /^CPU/{ +- s: ::g +- p +- }'`" +- test x"${CPU}" != x && { echo "${CPU}-unknown-linux-gnu"; exit; } +- ;; +- mips64:Linux:*:*) ++ mips:Linux:*:* | mips64:Linux:*:*) + eval $set_cc_for_build + sed 's/^ //' << EOF >$dummy.c + #undef CPU +- #undef mips64 +- #undef mips64el ++ #undef ${UNAME_MACHINE} ++ #undef ${UNAME_MACHINE}el + #if defined(__MIPSEL__) || defined(__MIPSEL) || defined(_MIPSEL) || defined(MIPSEL) +- CPU=mips64el ++ CPU=${UNAME_MACHINE}el + #else + #if defined(__MIPSEB__) || defined(__MIPSEB) || defined(_MIPSEB) || defined(MIPSEB) +- CPU=mips64 ++ CPU=${UNAME_MACHINE} + #else + CPU= + #endif + #endif + EOF +- eval "`$CC_FOR_BUILD -E $dummy.c 2>/dev/null | sed -n ' +- /^CPU/{ +- s: ::g +- p +- }'`" ++ eval `$CC_FOR_BUILD -E $dummy.c 2>/dev/null | grep '^CPU'` + test x"${CPU}" != x && { echo "${CPU}-unknown-linux-gnu"; exit; } + ;; + or32:Linux:*:*) +- echo or32-unknown-linux-gnu +- exit ;; +- ppc:Linux:*:*) +- echo powerpc-unknown-linux-gnu ++ echo ${UNAME_MACHINE}-unknown-linux-gnu + exit ;; +- ppc64:Linux:*:*) +- echo powerpc64-unknown-linux-gnu ++ padre:Linux:*:*) ++ echo sparc-unknown-linux-gnu + exit ;; +- alpha:Linux:*:*) +- case `sed -n '/^cpu model/s/^.*: \(.*\)/\1/p' < /proc/cpuinfo` in +- EV5) UNAME_MACHINE=alphaev5 ;; +- EV56) UNAME_MACHINE=alphaev56 ;; +- PCA56) UNAME_MACHINE=alphapca56 ;; +- PCA57) UNAME_MACHINE=alphapca56 ;; +- EV6) UNAME_MACHINE=alphaev6 ;; +- EV67) UNAME_MACHINE=alphaev67 ;; +- EV68*) UNAME_MACHINE=alphaev68 ;; +- esac +- objdump --private-headers /bin/sh | grep ld.so.1 >/dev/null +- if test "$?" = 0 ; then LIBC="libc1" ; else LIBC="" ; fi +- echo ${UNAME_MACHINE}-unknown-linux-gnu${LIBC} ++ parisc64:Linux:*:* | hppa64:Linux:*:*) ++ echo hppa64-unknown-linux-gnu + exit ;; + parisc:Linux:*:* | hppa:Linux:*:*) + # Look for CPU level +@@ -926,14 +974,17 @@ EOF + *) echo hppa-unknown-linux-gnu ;; + esac + exit ;; +- parisc64:Linux:*:* | hppa64:Linux:*:*) +- echo hppa64-unknown-linux-gnu ++ ppc64:Linux:*:*) ++ echo powerpc64-unknown-linux-gnu ++ exit ;; ++ ppc:Linux:*:*) ++ echo powerpc-unknown-linux-gnu + exit ;; + s390:Linux:*:* | s390x:Linux:*:*) + echo ${UNAME_MACHINE}-ibm-linux + exit ;; + sh64*:Linux:*:*) +- echo ${UNAME_MACHINE}-unknown-linux-gnu ++ echo ${UNAME_MACHINE}-unknown-linux-gnu + exit ;; + sh*:Linux:*:*) + echo ${UNAME_MACHINE}-unknown-linux-gnu +@@ -941,75 +992,18 @@ EOF + sparc:Linux:*:* | sparc64:Linux:*:*) + echo ${UNAME_MACHINE}-unknown-linux-gnu + exit ;; ++ tile*:Linux:*:*) ++ echo ${UNAME_MACHINE}-unknown-linux-gnu ++ exit ;; + vax:Linux:*:*) + echo ${UNAME_MACHINE}-dec-linux-gnu + exit ;; + x86_64:Linux:*:*) +- echo x86_64-unknown-linux-gnu ++ echo ${UNAME_MACHINE}-unknown-linux-gnu ++ exit ;; ++ xtensa*:Linux:*:*) ++ echo ${UNAME_MACHINE}-unknown-linux-gnu + exit ;; +- i*86:Linux:*:*) +- # The BFD linker knows what the default object file format is, so +- # first see if it will tell us. cd to the root directory to prevent +- # problems with other programs or directories called `ld' in the path. +- # Set LC_ALL=C to ensure ld outputs messages in English. +- ld_supported_targets=`cd /; LC_ALL=C ld --help 2>&1 \ +- | sed -ne '/supported targets:/!d +- s/[ ][ ]*/ /g +- s/.*supported targets: *// +- s/ .*// +- p'` +- case "$ld_supported_targets" in +- elf32-i386) +- TENTATIVE="${UNAME_MACHINE}-pc-linux-gnu" +- ;; +- a.out-i386-linux) +- echo "${UNAME_MACHINE}-pc-linux-gnuaout" +- exit ;; +- coff-i386) +- echo "${UNAME_MACHINE}-pc-linux-gnucoff" +- exit ;; +- "") +- # Either a pre-BFD a.out linker (linux-gnuoldld) or +- # one that does not give us useful --help. +- echo "${UNAME_MACHINE}-pc-linux-gnuoldld" +- exit ;; +- esac +- # Determine whether the default compiler is a.out or elf +- eval $set_cc_for_build +- sed 's/^ //' << EOF >$dummy.c +- #include +- #ifdef __ELF__ +- # ifdef __GLIBC__ +- # if __GLIBC__ >= 2 +- LIBC=gnu +- # else +- LIBC=gnulibc1 +- # endif +- # else +- LIBC=gnulibc1 +- # endif +- #else +- #if defined(__INTEL_COMPILER) || defined(__PGI) || defined(__sun) +- LIBC=gnu +- #else +- LIBC=gnuaout +- #endif +- #endif +- #ifdef __dietlibc__ +- LIBC=dietlibc +- #endif +-EOF +- eval "`$CC_FOR_BUILD -E $dummy.c 2>/dev/null | sed -n ' +- /^LIBC/{ +- s: ::g +- p +- }'`" +- test x"${LIBC}" != x && { +- echo "${UNAME_MACHINE}-pc-linux-${LIBC}" +- exit +- } +- test x"${TENTATIVE}" != x && { echo "${TENTATIVE}"; exit; } +- ;; + i*86:DYNIX/ptx:4*:*) + # ptx 4.0 does uname -s correctly, with DYNIX/ptx in there. + # earlier versions are messed up and put the nodename in both +@@ -1017,11 +1011,11 @@ EOF + echo i386-sequent-sysv4 + exit ;; + i*86:UNIX_SV:4.2MP:2.*) +- # Unixware is an offshoot of SVR4, but it has its own version +- # number series starting with 2... +- # I am not positive that other SVR4 systems won't match this, ++ # Unixware is an offshoot of SVR4, but it has its own version ++ # number series starting with 2... ++ # I am not positive that other SVR4 systems won't match this, + # I just have to hope. -- rms. +- # Use sysv4.2uw... so that sysv4* matches it. ++ # Use sysv4.2uw... so that sysv4* matches it. + echo ${UNAME_MACHINE}-pc-sysv4.2uw${UNAME_VERSION} + exit ;; + i*86:OS/2:*:*) +@@ -1038,7 +1032,7 @@ EOF + i*86:syllable:*:*) + echo ${UNAME_MACHINE}-pc-syllable + exit ;; +- i*86:LynxOS:2.*:* | i*86:LynxOS:3.[01]*:* | i*86:LynxOS:4.0*:*) ++ i*86:LynxOS:2.*:* | i*86:LynxOS:3.[01]*:* | i*86:LynxOS:4.[02]*:*) + echo i386-unknown-lynxos${UNAME_RELEASE} + exit ;; + i*86:*DOS:*:*) +@@ -1053,7 +1047,7 @@ EOF + fi + exit ;; + i*86:*:5:[678]*) +- # UnixWare 7.x, OpenUNIX and OpenServer 6. ++ # UnixWare 7.x, OpenUNIX and OpenServer 6. + case `/bin/uname -X | grep "^Machine"` in + *486*) UNAME_MACHINE=i486 ;; + *Pentium) UNAME_MACHINE=i586 ;; +@@ -1081,10 +1075,13 @@ EOF + exit ;; + pc:*:*:*) + # Left here for compatibility: +- # uname -m prints for DJGPP always 'pc', but it prints nothing about +- # the processor, so we play safe by assuming i386. +- echo i386-pc-msdosdjgpp +- exit ;; ++ # uname -m prints for DJGPP always 'pc', but it prints nothing about ++ # the processor, so we play safe by assuming i586. ++ # Note: whatever this is, it MUST be the same as what config.sub ++ # prints for the "djgpp" host, or else GDB configury will decide that ++ # this is a cross-build. ++ echo i586-pc-msdosdjgpp ++ exit ;; + Intel:Mach:3*:*) + echo i386-pc-mach3 + exit ;; +@@ -1119,8 +1116,18 @@ EOF + /bin/uname -p 2>/dev/null | /bin/grep entium >/dev/null \ + && { echo i586-ncr-sysv4.3${OS_REL}; exit; } ;; + 3[34]??:*:4.0:* | 3[34]??,*:*:4.0:*) +- /bin/uname -p 2>/dev/null | grep 86 >/dev/null \ +- && { echo i486-ncr-sysv4; exit; } ;; ++ /bin/uname -p 2>/dev/null | grep 86 >/dev/null \ ++ && { echo i486-ncr-sysv4; exit; } ;; ++ NCR*:*:4.2:* | MPRAS*:*:4.2:*) ++ OS_REL='.3' ++ test -r /etc/.relid \ ++ && OS_REL=.`sed -n 's/[^ ]* [^ ]* \([0-9][0-9]\).*/\1/p' < /etc/.relid` ++ /bin/uname -p 2>/dev/null | grep 86 >/dev/null \ ++ && { echo i486-ncr-sysv4.3${OS_REL}; exit; } ++ /bin/uname -p 2>/dev/null | /bin/grep entium >/dev/null \ ++ && { echo i586-ncr-sysv4.3${OS_REL}; exit; } ++ /bin/uname -p 2>/dev/null | /bin/grep pteron >/dev/null \ ++ && { echo i586-ncr-sysv4.3${OS_REL}; exit; } ;; + m68*:LynxOS:2.*:* | m68*:LynxOS:3.0*:*) + echo m68k-unknown-lynxos${UNAME_RELEASE} + exit ;; +@@ -1133,7 +1140,7 @@ EOF + rs6000:LynxOS:2.*:*) + echo rs6000-unknown-lynxos${UNAME_RELEASE} + exit ;; +- PowerPC:LynxOS:2.*:* | PowerPC:LynxOS:3.[01]*:* | PowerPC:LynxOS:4.0*:*) ++ PowerPC:LynxOS:2.*:* | PowerPC:LynxOS:3.[01]*:* | PowerPC:LynxOS:4.[02]*:*) + echo powerpc-unknown-lynxos${UNAME_RELEASE} + exit ;; + SM[BE]S:UNIX_SV:*:*) +@@ -1153,10 +1160,10 @@ EOF + echo ns32k-sni-sysv + fi + exit ;; +- PENTIUM:*:4.0*:*) # Unisys `ClearPath HMP IX 4000' SVR4/MP effort +- # says +- echo i586-unisys-sysv4 +- exit ;; ++ PENTIUM:*:4.0*:*) # Unisys `ClearPath HMP IX 4000' SVR4/MP effort ++ # says ++ echo i586-unisys-sysv4 ++ exit ;; + *:UNIX_System_V:4*:FTX*) + # From Gerald Hewes . + # How about differentiating between stratus architectures? -djm +@@ -1182,11 +1189,11 @@ EOF + exit ;; + R[34]000:*System_V*:*:* | R4000:UNIX_SYSV:*:* | R*000:UNIX_SV:*:*) + if [ -d /usr/nec ]; then +- echo mips-nec-sysv${UNAME_RELEASE} ++ echo mips-nec-sysv${UNAME_RELEASE} + else +- echo mips-unknown-sysv${UNAME_RELEASE} ++ echo mips-unknown-sysv${UNAME_RELEASE} + fi +- exit ;; ++ exit ;; + BeBox:BeOS:*:*) # BeOS running on hardware made by Be, PPC only. + echo powerpc-be-beos + exit ;; +@@ -1196,6 +1203,12 @@ EOF + BePC:BeOS:*:*) # BeOS running on Intel PC compatible. + echo i586-pc-beos + exit ;; ++ BePC:Haiku:*:*) # Haiku running on Intel PC compatible. ++ echo i586-pc-haiku ++ exit ;; ++ x86_64:Haiku:*:*) ++ echo x86_64-unknown-haiku ++ exit ;; + SX-4:SUPER-UX:*:*) + echo sx4-nec-superux${UNAME_RELEASE} + exit ;; +@@ -1205,6 +1218,15 @@ EOF + SX-6:SUPER-UX:*:*) + echo sx6-nec-superux${UNAME_RELEASE} + exit ;; ++ SX-7:SUPER-UX:*:*) ++ echo sx7-nec-superux${UNAME_RELEASE} ++ exit ;; ++ SX-8:SUPER-UX:*:*) ++ echo sx8-nec-superux${UNAME_RELEASE} ++ exit ;; ++ SX-8R:SUPER-UX:*:*) ++ echo sx8r-nec-superux${UNAME_RELEASE} ++ exit ;; + Power*:Rhapsody:*:*) + echo powerpc-apple-rhapsody${UNAME_RELEASE} + exit ;; +@@ -1214,6 +1236,16 @@ EOF + *:Darwin:*:*) + UNAME_PROCESSOR=`uname -p` || UNAME_PROCESSOR=unknown + case $UNAME_PROCESSOR in ++ i386) ++ eval $set_cc_for_build ++ if [ "$CC_FOR_BUILD" != 'no_compiler_found' ]; then ++ if (echo '#ifdef __LP64__'; echo IS_64BIT_ARCH; echo '#endif') | \ ++ (CCOPTS= $CC_FOR_BUILD -E - 2>/dev/null) | \ ++ grep IS_64BIT_ARCH >/dev/null ++ then ++ UNAME_PROCESSOR="x86_64" ++ fi ++ fi ;; + unknown) UNAME_PROCESSOR=powerpc ;; + esac + echo ${UNAME_PROCESSOR}-apple-darwin${UNAME_RELEASE} +@@ -1229,7 +1261,10 @@ EOF + *:QNX:*:4*) + echo i386-pc-qnx + exit ;; +- NSE-?:NONSTOP_KERNEL:*:*) ++ NEO-?:NONSTOP_KERNEL:*:*) ++ echo neo-tandem-nsk${UNAME_RELEASE} ++ exit ;; ++ NSE-*:NONSTOP_KERNEL:*:*) + echo nse-tandem-nsk${UNAME_RELEASE} + exit ;; + NSR-?:NONSTOP_KERNEL:*:*) +@@ -1274,13 +1309,13 @@ EOF + echo pdp10-unknown-its + exit ;; + SEI:*:*:SEIUX) +- echo mips-sei-seiux${UNAME_RELEASE} ++ echo mips-sei-seiux${UNAME_RELEASE} + exit ;; + *:DragonFly:*:*) + echo ${UNAME_MACHINE}-unknown-dragonfly`echo ${UNAME_RELEASE}|sed -e 's/[-(].*//'` + exit ;; + *:*VMS:*:*) +- UNAME_MACHINE=`(uname -p) 2>/dev/null` ++ UNAME_MACHINE=`(uname -p) 2>/dev/null` + case "${UNAME_MACHINE}" in + A*) echo alpha-dec-vms ; exit ;; + I*) echo ia64-dec-vms ; exit ;; +@@ -1295,11 +1330,14 @@ EOF + i*86:rdos:*:*) + echo ${UNAME_MACHINE}-pc-rdos + exit ;; ++ i*86:AROS:*:*) ++ echo ${UNAME_MACHINE}-pc-aros ++ exit ;; ++ x86_64:VMkernel:*:*) ++ echo ${UNAME_MACHINE}-unknown-esx ++ exit ;; + esac + +-#echo '(No uname command or uname output not recognized.)' 1>&2 +-#echo "${UNAME_MACHINE}:${UNAME_SYSTEM}:${UNAME_RELEASE}:${UNAME_VERSION}" 1>&2 +- + eval $set_cc_for_build + cat >$dummy.c < + printf ("m68k-sony-newsos%s\n", + #ifdef NEWSOS4 +- "4" ++ "4" + #else +- "" ++ "" + #endif +- ); exit (0); ++ ); exit (0); + #endif + #endif + +@@ -1455,9 +1493,9 @@ This script, last modified $timestamp, has failed to recognize + the operating system you are using. It is advised that you + download the most up to date version of the config scripts from + +- http://savannah.gnu.org/cgi-bin/viewcvs/*checkout*/config/config/config.guess ++ http://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=config.guess;hb=HEAD + and +- http://savannah.gnu.org/cgi-bin/viewcvs/*checkout*/config/config/config.sub ++ http://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=config.sub;hb=HEAD + + If the version you run ($0) is already up to date, please + send the following data and any information you think might be +diff --git a/config.sub b/config.sub +index ad9f395..802a224 100644 +--- a/config.sub ++++ b/config.sub +@@ -1,43 +1,42 @@ + #! /bin/sh + # Configuration validation subroutine script. + # Copyright (C) 1992, 1993, 1994, 1995, 1996, 1997, 1998, 1999, +-# 2000, 2001, 2002, 2003, 2004, 2005 Free Software Foundation, Inc. ++# 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, ++# 2011, 2012, 2013 Free Software Foundation, Inc. + +-timestamp='2006-02-23' ++timestamp='2012-12-29' + +-# This file is (in principle) common to ALL GNU software. +-# The presence of a machine in this file suggests that SOME GNU software +-# can handle that machine. It does not imply ALL GNU software can. +-# +-# This file is free software; you can redistribute it and/or modify +-# it under the terms of the GNU General Public License as published by +-# the Free Software Foundation; either version 2 of the License, or ++# This file is free software; you can redistribute it and/or modify it ++# under the terms of the GNU General Public License as published by ++# the Free Software Foundation; either version 3 of the License, or + # (at your option) any later version. + # +-# This program is distributed in the hope that it will be useful, +-# but WITHOUT ANY WARRANTY; without even the implied warranty of +-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +-# GNU General Public License for more details. ++# This program is distributed in the hope that it will be useful, but ++# WITHOUT ANY WARRANTY; without even the implied warranty of ++# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ++# General Public License for more details. + # + # You should have received a copy of the GNU General Public License +-# along with this program; if not, write to the Free Software +-# Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA +-# 02110-1301, USA. ++# along with this program; if not, see . + # + # As a special exception to the GNU General Public License, if you + # distribute this file as part of a program that contains a + # configuration script generated by Autoconf, you may include it under +-# the same distribution terms that you use for the rest of that program. ++# the same distribution terms that you use for the rest of that ++# program. This Exception is an additional permission under section 7 ++# of the GNU General Public License, version 3 ("GPLv3"). + + +-# Please send patches to . Submit a context +-# diff and a properly formatted ChangeLog entry. ++# Please send patches with a ChangeLog entry to config-patches@gnu.org. + # + # Configuration subroutine to validate and canonicalize a configuration type. + # Supply the specified configuration type as an argument. + # If it is invalid, we print an error message on stderr and exit with code 1. + # Otherwise, we print the canonical config type on stdout and succeed. + ++# You can get the latest version of this script from: ++# http://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=config.sub;hb=HEAD ++ + # This file is supposed to be the same for all GNU packages + # and recognize all the CPU types, system types and aliases + # that are meaningful with *any* GNU software. +@@ -71,8 +70,9 @@ Report bugs and patches to ." + version="\ + GNU config.sub ($timestamp) + +-Copyright (C) 1992, 1993, 1994, 1995, 1996, 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005 +-Free Software Foundation, Inc. ++Copyright (C) 1992, 1993, 1994, 1995, 1996, 1997, 1998, 1999, 2000, ++2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, ++2012, 2013 Free Software Foundation, Inc. + + This is free software; see the source for copying conditions. There is NO + warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE." +@@ -119,12 +119,18 @@ esac + # Here we must recognize all the valid KERNEL-OS combinations. + maybe_os=`echo $1 | sed 's/^\(.*\)-\([^-]*-[^-]*\)$/\2/'` + case $maybe_os in +- nto-qnx* | linux-gnu* | linux-dietlibc | linux-newlib* | linux-uclibc* | \ +- uclinux-uclibc* | uclinux-gnu* | kfreebsd*-gnu* | knetbsd*-gnu* | netbsd*-gnu* | \ ++ nto-qnx* | linux-gnu* | linux-android* | linux-dietlibc | linux-newlib* | \ ++ linux-musl* | linux-uclibc* | uclinux-uclibc* | uclinux-gnu* | kfreebsd*-gnu* | \ ++ knetbsd*-gnu* | netbsd*-gnu* | \ ++ kopensolaris*-gnu* | \ + storm-chaos* | os2-emx* | rtmk-nova*) + os=-$maybe_os + basic_machine=`echo $1 | sed 's/^\(.*\)-\([^-]*-[^-]*\)$/\1/'` + ;; ++ android-linux) ++ os=-linux-android ++ basic_machine=`echo $1 | sed 's/^\(.*\)-\([^-]*-[^-]*\)$/\1/'`-unknown ++ ;; + *) + basic_machine=`echo $1 | sed 's/-[^-]*$//'` + if [ $basic_machine != $1 ] +@@ -147,10 +153,13 @@ case $os in + -convergent* | -ncr* | -news | -32* | -3600* | -3100* | -hitachi* |\ + -c[123]* | -convex* | -sun | -crds | -omron* | -dg | -ultra | -tti* | \ + -harris | -dolphin | -highlevel | -gould | -cbm | -ns | -masscomp | \ +- -apple | -axis | -knuth | -cray) ++ -apple | -axis | -knuth | -cray | -microblaze*) + os= + basic_machine=$1 + ;; ++ -bluegene*) ++ os=-cnk ++ ;; + -sim | -cisco | -oki | -wec | -winbond) + os= + basic_machine=$1 +@@ -165,10 +174,10 @@ case $os in + os=-chorusos + basic_machine=$1 + ;; +- -chorusrdb) +- os=-chorusrdb ++ -chorusrdb) ++ os=-chorusrdb + basic_machine=$1 +- ;; ++ ;; + -hiux*) + os=-hiuxwe2 + ;; +@@ -213,6 +222,12 @@ case $os in + -isc*) + basic_machine=`echo $1 | sed -e 's/86-.*/86-pc/'` + ;; ++ -lynx*178) ++ os=-lynxos178 ++ ;; ++ -lynx*5) ++ os=-lynxos5 ++ ;; + -lynx*) + os=-lynxos + ;; +@@ -237,23 +252,34 @@ case $basic_machine in + # Some are omitted here because they have special meanings below. + 1750a | 580 \ + | a29k \ ++ | aarch64 | aarch64_be \ + | alpha | alphaev[4-8] | alphaev56 | alphaev6[78] | alphapca5[67] \ + | alpha64 | alpha64ev[4-8] | alpha64ev56 | alpha64ev6[78] | alpha64pca5[67] \ + | am33_2.0 \ +- | arc | arm | arm[bl]e | arme[lb] | armv[2345] | armv[345][lb] | avr \ ++ | arc \ ++ | arm | arm[bl]e | arme[lb] | armv[2-8] | armv[3-8][lb] | armv7[arm] \ ++ | avr | avr32 \ ++ | be32 | be64 \ + | bfin \ + | c4x | clipper \ + | d10v | d30v | dlx | dsp16xx \ +- | fr30 | frv \ ++ | epiphany \ ++ | fido | fr30 | frv \ + | h8300 | h8500 | hppa | hppa1.[01] | hppa2.0 | hppa2.0[nw] | hppa64 \ ++ | hexagon \ + | i370 | i860 | i960 | ia64 \ + | ip2k | iq2000 \ +- | m32r | m32rle | m68000 | m68k | m88k | maxq | mb | microblaze | mcore \ ++ | le32 | le64 \ ++ | lm32 \ ++ | m32c | m32r | m32rle | m68000 | m68k | m88k \ ++ | maxq | mb | microblaze | microblazeel | mcore | mep | metag \ + | mips | mipsbe | mipseb | mipsel | mipsle \ + | mips16 \ + | mips64 | mips64el \ +- | mips64vr | mips64vrel \ ++ | mips64octeon | mips64octeonel \ + | mips64orion | mips64orionel \ ++ | mips64r5900 | mips64r5900el \ ++ | mips64vr | mips64vrel \ + | mips64vr4100 | mips64vr4100el \ + | mips64vr4300 | mips64vr4300el \ + | mips64vr5000 | mips64vr5000el \ +@@ -266,31 +292,42 @@ case $basic_machine in + | mipsisa64sr71k | mipsisa64sr71kel \ + | mipstx39 | mipstx39el \ + | mn10200 | mn10300 \ ++ | moxie \ + | mt \ + | msp430 \ ++ | nds32 | nds32le | nds32be \ + | nios | nios2 \ + | ns16k | ns32k \ ++ | open8 \ + | or32 \ + | pdp10 | pdp11 | pj | pjl \ +- | powerpc | powerpc64 | powerpc64le | powerpcle | ppcbe \ ++ | powerpc | powerpc64 | powerpc64le | powerpcle \ + | pyramid \ +- | sh | sh[1234] | sh[24]a | sh[23]e | sh[34]eb | shbe | shle | sh[1234]le | sh3ele \ ++ | rl78 | rx \ ++ | score \ ++ | sh | sh[1234] | sh[24]a | sh[24]aeb | sh[23]e | sh[34]eb | sheb | shbe | shle | sh[1234]le | sh3ele \ + | sh64 | sh64le \ +- | sparc | sparc64 | sparc64b | sparc86x | sparclet | sparclite \ +- | sparcv8 | sparcv9 | sparcv9b \ +- | strongarm \ +- | tahoe | thumb | tic4x | tic80 | tron \ +- | v850 | v850e \ ++ | sparc | sparc64 | sparc64b | sparc64v | sparc86x | sparclet | sparclite \ ++ | sparcv8 | sparcv9 | sparcv9b | sparcv9v \ ++ | spu \ ++ | tahoe | tic4x | tic54x | tic55x | tic6x | tic80 | tron \ ++ | ubicom32 \ ++ | v850 | v850e | v850e1 | v850e2 | v850es | v850e2v3 \ + | we32k \ +- | x86 | xscale | xscalee[bl] | xstormy16 | xtensa \ +- | z8k) ++ | x86 | xc16x | xstormy16 | xtensa \ ++ | z8k | z80) + basic_machine=$basic_machine-unknown + ;; +- m32c) +- basic_machine=$basic_machine-unknown ++ c54x) ++ basic_machine=tic54x-unknown ++ ;; ++ c55x) ++ basic_machine=tic55x-unknown + ;; +- m6811 | m68hc11 | m6812 | m68hc12) +- # Motorola 68HC11/12. ++ c6x) ++ basic_machine=tic6x-unknown ++ ;; ++ m6811 | m68hc11 | m6812 | m68hc12 | m68hcs12x | picochip) + basic_machine=$basic_machine-unknown + os=-none + ;; +@@ -300,6 +337,21 @@ case $basic_machine in + basic_machine=mt-unknown + ;; + ++ strongarm | thumb | xscale) ++ basic_machine=arm-unknown ++ ;; ++ xgate) ++ basic_machine=$basic_machine-unknown ++ os=-none ++ ;; ++ xscaleeb) ++ basic_machine=armeb-unknown ++ ;; ++ ++ xscaleel) ++ basic_machine=armel-unknown ++ ;; ++ + # We use `pc' rather than `unknown' + # because (1) that's what they normally are, and + # (2) the word "unknown" tends to confuse beginning users. +@@ -314,29 +366,37 @@ case $basic_machine in + # Recognize the basic CPU types with company name. + 580-* \ + | a29k-* \ ++ | aarch64-* | aarch64_be-* \ + | alpha-* | alphaev[4-8]-* | alphaev56-* | alphaev6[78]-* \ + | alpha64-* | alpha64ev[4-8]-* | alpha64ev56-* | alpha64ev6[78]-* \ + | alphapca5[67]-* | alpha64pca5[67]-* | arc-* \ + | arm-* | armbe-* | armle-* | armeb-* | armv*-* \ +- | avr-* \ ++ | avr-* | avr32-* \ ++ | be32-* | be64-* \ + | bfin-* | bs2000-* \ +- | c[123]* | c30-* | [cjt]90-* | c4x-* | c54x-* | c55x-* | c6x-* \ ++ | c[123]* | c30-* | [cjt]90-* | c4x-* \ + | clipper-* | craynv-* | cydra-* \ + | d10v-* | d30v-* | dlx-* \ + | elxsi-* \ +- | f30[01]-* | f700-* | fr30-* | frv-* | fx80-* \ ++ | f30[01]-* | f700-* | fido-* | fr30-* | frv-* | fx80-* \ + | h8300-* | h8500-* \ + | hppa-* | hppa1.[01]-* | hppa2.0-* | hppa2.0[nw]-* | hppa64-* \ ++ | hexagon-* \ + | i*86-* | i860-* | i960-* | ia64-* \ + | ip2k-* | iq2000-* \ +- | m32r-* | m32rle-* \ ++ | le32-* | le64-* \ ++ | lm32-* \ ++ | m32c-* | m32r-* | m32rle-* \ + | m68000-* | m680[012346]0-* | m68360-* | m683?2-* | m68k-* \ +- | m88110-* | m88k-* | maxq-* | mcore-* \ ++ | m88110-* | m88k-* | maxq-* | mcore-* | metag-* \ ++ | microblaze-* | microblazeel-* \ + | mips-* | mipsbe-* | mipseb-* | mipsel-* | mipsle-* \ + | mips16-* \ + | mips64-* | mips64el-* \ +- | mips64vr-* | mips64vrel-* \ ++ | mips64octeon-* | mips64octeonel-* \ + | mips64orion-* | mips64orionel-* \ ++ | mips64r5900-* | mips64r5900el-* \ ++ | mips64vr-* | mips64vrel-* \ + | mips64vr4100-* | mips64vr4100el-* \ + | mips64vr4300-* | mips64vr4300el-* \ + | mips64vr5000-* | mips64vr5000el-* \ +@@ -351,29 +411,36 @@ case $basic_machine in + | mmix-* \ + | mt-* \ + | msp430-* \ ++ | nds32-* | nds32le-* | nds32be-* \ + | nios-* | nios2-* \ + | none-* | np1-* | ns16k-* | ns32k-* \ ++ | open8-* \ + | orion-* \ + | pdp10-* | pdp11-* | pj-* | pjl-* | pn-* | power-* \ +- | powerpc-* | powerpc64-* | powerpc64le-* | powerpcle-* | ppcbe-* \ ++ | powerpc-* | powerpc64-* | powerpc64le-* | powerpcle-* \ + | pyramid-* \ +- | romp-* | rs6000-* \ +- | sh-* | sh[1234]-* | sh[24]a-* | sh[23]e-* | sh[34]eb-* | shbe-* \ ++ | rl78-* | romp-* | rs6000-* | rx-* \ ++ | sh-* | sh[1234]-* | sh[24]a-* | sh[24]aeb-* | sh[23]e-* | sh[34]eb-* | sheb-* | shbe-* \ + | shle-* | sh[1234]le-* | sh3ele-* | sh64-* | sh64le-* \ +- | sparc-* | sparc64-* | sparc64b-* | sparc86x-* | sparclet-* \ ++ | sparc-* | sparc64-* | sparc64b-* | sparc64v-* | sparc86x-* | sparclet-* \ + | sparclite-* \ +- | sparcv8-* | sparcv9-* | sparcv9b-* | strongarm-* | sv1-* | sx?-* \ +- | tahoe-* | thumb-* \ ++ | sparcv8-* | sparcv9-* | sparcv9b-* | sparcv9v-* | sv1-* | sx?-* \ ++ | tahoe-* \ + | tic30-* | tic4x-* | tic54x-* | tic55x-* | tic6x-* | tic80-* \ ++ | tile*-* \ + | tron-* \ +- | v850-* | v850e-* | vax-* \ ++ | ubicom32-* \ ++ | v850-* | v850e-* | v850e1-* | v850es-* | v850e2-* | v850e2v3-* \ ++ | vax-* \ + | we32k-* \ +- | x86-* | x86_64-* | xps100-* | xscale-* | xscalee[bl]-* \ +- | xstormy16-* | xtensa-* \ ++ | x86-* | x86_64-* | xc16x-* | xps100-* \ ++ | xstormy16-* | xtensa*-* \ + | ymp-* \ +- | z8k-*) ++ | z8k-* | z80-*) + ;; +- m32c-*) ++ # Recognize the basic CPU types without company name, with glob match. ++ xtensa*) ++ basic_machine=$basic_machine-unknown + ;; + # Recognize the various machine names and aliases which stand + # for a CPU type and a company and sometimes even an OS. +@@ -391,7 +458,7 @@ case $basic_machine in + basic_machine=a29k-amd + os=-udi + ;; +- abacus) ++ abacus) + basic_machine=abacus-unknown + ;; + adobe68k) +@@ -437,6 +504,10 @@ case $basic_machine in + basic_machine=m68k-apollo + os=-bsd + ;; ++ aros) ++ basic_machine=i386-pc ++ os=-aros ++ ;; + aux) + basic_machine=m68k-apple + os=-aux +@@ -445,10 +516,35 @@ case $basic_machine in + basic_machine=ns32k-sequent + os=-dynix + ;; ++ blackfin) ++ basic_machine=bfin-unknown ++ os=-linux ++ ;; ++ blackfin-*) ++ basic_machine=bfin-`echo $basic_machine | sed 's/^[^-]*-//'` ++ os=-linux ++ ;; ++ bluegene*) ++ basic_machine=powerpc-ibm ++ os=-cnk ++ ;; ++ c54x-*) ++ basic_machine=tic54x-`echo $basic_machine | sed 's/^[^-]*-//'` ++ ;; ++ c55x-*) ++ basic_machine=tic55x-`echo $basic_machine | sed 's/^[^-]*-//'` ++ ;; ++ c6x-*) ++ basic_machine=tic6x-`echo $basic_machine | sed 's/^[^-]*-//'` ++ ;; + c90) + basic_machine=c90-cray + os=-unicos + ;; ++ cegcc) ++ basic_machine=arm-unknown ++ os=-cegcc ++ ;; + convex-c1) + basic_machine=c1-convex + os=-bsd +@@ -477,8 +573,8 @@ case $basic_machine in + basic_machine=craynv-cray + os=-unicosmp + ;; +- cr16c) +- basic_machine=cr16c-unknown ++ cr16 | cr16-*) ++ basic_machine=cr16-unknown + os=-elf + ;; + crds | unos) +@@ -516,6 +612,10 @@ case $basic_machine in + basic_machine=m88k-motorola + os=-sysv3 + ;; ++ dicos) ++ basic_machine=i686-pc ++ os=-dicos ++ ;; + djgpp) + basic_machine=i586-pc + os=-msdosdjgpp +@@ -631,7 +731,6 @@ case $basic_machine in + i370-ibm* | ibm*) + basic_machine=i370-ibm + ;; +-# I'm not sure what "Sysv32" means. Should this be sysv3.2? + i*86v32) + basic_machine=`echo $1 | sed -e 's/86.*/86-pc/'` + os=-sysv32 +@@ -670,6 +769,14 @@ case $basic_machine in + basic_machine=m68k-isi + os=-sysv + ;; ++ m68knommu) ++ basic_machine=m68k-unknown ++ os=-linux ++ ;; ++ m68knommu-*) ++ basic_machine=m68k-`echo $basic_machine | sed 's/^[^-]*-//'` ++ os=-linux ++ ;; + m88k-omron*) + basic_machine=m88k-omron + ;; +@@ -681,10 +788,21 @@ case $basic_machine in + basic_machine=ns32k-utek + os=-sysv + ;; ++ microblaze*) ++ basic_machine=microblaze-xilinx ++ ;; ++ mingw64) ++ basic_machine=x86_64-pc ++ os=-mingw64 ++ ;; + mingw32) + basic_machine=i386-pc + os=-mingw32 + ;; ++ mingw32ce) ++ basic_machine=arm-unknown ++ os=-mingw32ce ++ ;; + miniframe) + basic_machine=m68000-convergent + ;; +@@ -713,10 +831,18 @@ case $basic_machine in + ms1-*) + basic_machine=`echo $basic_machine | sed -e 's/ms1-/mt-/'` + ;; ++ msys) ++ basic_machine=i386-pc ++ os=-msys ++ ;; + mvs) + basic_machine=i370-ibm + os=-mvs + ;; ++ nacl) ++ basic_machine=le32-unknown ++ os=-nacl ++ ;; + ncr3000) + basic_machine=i486-ncr + os=-sysv4 +@@ -781,6 +907,12 @@ case $basic_machine in + np1) + basic_machine=np1-gould + ;; ++ neo-tandem) ++ basic_machine=neo-tandem ++ ;; ++ nse-tandem) ++ basic_machine=nse-tandem ++ ;; + nsr-tandem) + basic_machine=nsr-tandem + ;; +@@ -811,6 +943,14 @@ case $basic_machine in + basic_machine=i860-intel + os=-osf + ;; ++ parisc) ++ basic_machine=hppa-unknown ++ os=-linux ++ ;; ++ parisc-*) ++ basic_machine=hppa-`echo $basic_machine | sed 's/^[^-]*-//'` ++ os=-linux ++ ;; + pbd) + basic_machine=sparc-tti + ;; +@@ -855,9 +995,10 @@ case $basic_machine in + ;; + power) basic_machine=power-ibm + ;; +- ppc) basic_machine=powerpc-unknown ++ ppc | ppcbe) basic_machine=powerpc-unknown + ;; +- ppc-*) basic_machine=powerpc-`echo $basic_machine | sed 's/^[^-]*-//'` ++ ppc-* | ppcbe-*) ++ basic_machine=powerpc-`echo $basic_machine | sed 's/^[^-]*-//'` + ;; + ppcle | powerpclittle | ppc-le | powerpc-little) + basic_machine=powerpcle-unknown +@@ -882,7 +1023,11 @@ case $basic_machine in + basic_machine=i586-unknown + os=-pw32 + ;; +- rdos) ++ rdos | rdos64) ++ basic_machine=x86_64-pc ++ os=-rdos ++ ;; ++ rdos32) + basic_machine=i386-pc + os=-rdos + ;; +@@ -912,6 +1057,10 @@ case $basic_machine in + sb1el) + basic_machine=mipsisa64sb1el-unknown + ;; ++ sde) ++ basic_machine=mipsisa32-sde ++ os=-elf ++ ;; + sei) + basic_machine=mips-sei + os=-seiux +@@ -923,6 +1072,9 @@ case $basic_machine in + basic_machine=sh-hitachi + os=-hms + ;; ++ sh5el) ++ basic_machine=sh5le-unknown ++ ;; + sh64) + basic_machine=sh64-unknown + ;; +@@ -944,6 +1096,9 @@ case $basic_machine in + basic_machine=i860-stratus + os=-sysv4 + ;; ++ strongarm-* | thumb-*) ++ basic_machine=arm-`echo $basic_machine | sed 's/^[^-]*-//'` ++ ;; + sun2) + basic_machine=m68000-sun + ;; +@@ -1000,17 +1155,9 @@ case $basic_machine in + basic_machine=t90-cray + os=-unicos + ;; +- tic54x | c54x*) +- basic_machine=tic54x-unknown +- os=-coff +- ;; +- tic55x | c55x*) +- basic_machine=tic55x-unknown +- os=-coff +- ;; +- tic6x | c6x*) +- basic_machine=tic6x-unknown +- os=-coff ++ tile*) ++ basic_machine=$basic_machine-unknown ++ os=-linux-gnu + ;; + tx39) + basic_machine=mipstx39-unknown +@@ -1079,6 +1226,9 @@ case $basic_machine in + xps | xps100) + basic_machine=xps100-honeywell + ;; ++ xscale-* | xscalee[bl]-*) ++ basic_machine=`echo $basic_machine | sed 's/^xscale/arm/'` ++ ;; + ymp) + basic_machine=ymp-cray + os=-unicos +@@ -1087,6 +1237,10 @@ case $basic_machine in + basic_machine=z8k-unknown + os=-sim + ;; ++ z80-*-coff) ++ basic_machine=z80-unknown ++ os=-sim ++ ;; + none) + basic_machine=none-none + os=-none +@@ -1125,10 +1279,10 @@ case $basic_machine in + we32k) + basic_machine=we32k-att + ;; +- sh[1234] | sh[24]a | sh[34]eb | sh[1234]le | sh[23]ele) ++ sh[1234] | sh[24]a | sh[24]aeb | sh[34]eb | sh[1234]le | sh[23]ele) + basic_machine=sh-unknown + ;; +- sparc | sparcv8 | sparcv9 | sparcv9b) ++ sparc | sparcv8 | sparcv9 | sparcv9b | sparcv9v) + basic_machine=sparc-sun + ;; + cydra) +@@ -1172,9 +1326,12 @@ esac + if [ x"$os" != x"" ] + then + case $os in +- # First match some system type aliases +- # that might get confused with valid system types. ++ # First match some system type aliases ++ # that might get confused with valid system types. + # -solaris* is a basic system type, with this one exception. ++ -auroraux) ++ os=-auroraux ++ ;; + -solaris1 | -solaris1.*) + os=`echo $os | sed -e 's|solaris1|sunos4|'` + ;; +@@ -1195,21 +1352,23 @@ case $os in + # Each alternative MUST END IN A *, to match a version number. + # -sysv* is not here because it comes later, after sysvr4. + -gnu* | -bsd* | -mach* | -minix* | -genix* | -ultrix* | -irix* \ +- | -*vms* | -sco* | -esix* | -isc* | -aix* | -sunos | -sunos[34]*\ +- | -hpux* | -unos* | -osf* | -luna* | -dgux* | -solaris* | -sym* \ ++ | -*vms* | -sco* | -esix* | -isc* | -aix* | -cnk* | -sunos | -sunos[34]*\ ++ | -hpux* | -unos* | -osf* | -luna* | -dgux* | -auroraux* | -solaris* \ ++ | -sym* | -kopensolaris* \ + | -amigaos* | -amigados* | -msdos* | -newsos* | -unicos* | -aof* \ +- | -aos* \ ++ | -aos* | -aros* \ + | -nindy* | -vxsim* | -vxworks* | -ebmon* | -hms* | -mvs* \ + | -clix* | -riscos* | -uniplus* | -iris* | -rtu* | -xenix* \ + | -hiux* | -386bsd* | -knetbsd* | -mirbsd* | -netbsd* \ +- | -openbsd* | -solidbsd* \ ++ | -bitrig* | -openbsd* | -solidbsd* \ + | -ekkobsd* | -kfreebsd* | -freebsd* | -riscix* | -lynxos* \ + | -bosx* | -nextstep* | -cxux* | -aout* | -elf* | -oabi* \ + | -ptx* | -coff* | -ecoff* | -winnt* | -domain* | -vsta* \ + | -udi* | -eabi* | -lites* | -ieee* | -go32* | -aux* \ +- | -chorusos* | -chorusrdb* \ +- | -cygwin* | -pe* | -psos* | -moss* | -proelf* | -rtems* \ +- | -mingw32* | -linux-gnu* | -linux-newlib* | -linux-uclibc* \ ++ | -chorusos* | -chorusrdb* | -cegcc* \ ++ | -cygwin* | -msys* | -pe* | -psos* | -moss* | -proelf* | -rtems* \ ++ | -mingw32* | -mingw64* | -linux-gnu* | -linux-android* \ ++ | -linux-newlib* | -linux-musl* | -linux-uclibc* \ + | -uxpv* | -beos* | -mpeix* | -udk* \ + | -interix* | -uwin* | -mks* | -rhapsody* | -darwin* | -opened* \ + | -openstep* | -oskit* | -conix* | -pw32* | -nonstopux* \ +@@ -1217,7 +1376,7 @@ case $os in + | -os2* | -vos* | -palmos* | -uclinux* | -nucleus* \ + | -morphos* | -superux* | -rtmk* | -rtmk-nova* | -windiss* \ + | -powermax* | -dnix* | -nx6 | -nx7 | -sei* | -dragonfly* \ +- | -skyos* | -haiku* | -rdos*) ++ | -skyos* | -haiku* | -rdos* | -toppers* | -drops* | -es*) + # Remember, each alternative MUST END IN *, to match a version number. + ;; + -qnx*) +@@ -1256,7 +1415,7 @@ case $os in + -opened*) + os=-openedition + ;; +- -os400*) ++ -os400*) + os=-os400 + ;; + -wince*) +@@ -1305,7 +1464,7 @@ case $os in + -sinix*) + os=-sysv4 + ;; +- -tpf*) ++ -tpf*) + os=-tpf + ;; + -triton*) +@@ -1347,6 +1506,11 @@ case $os in + -zvmoe) + os=-zvmoe + ;; ++ -dicos*) ++ os=-dicos ++ ;; ++ -nacl*) ++ ;; + -none) + ;; + *) +@@ -1369,6 +1533,12 @@ else + # system, and we'll never get to this point. + + case $basic_machine in ++ score-*) ++ os=-elf ++ ;; ++ spu-*) ++ os=-elf ++ ;; + *-acorn) + os=-riscix1.2 + ;; +@@ -1378,9 +1548,21 @@ case $basic_machine in + arm*-semi) + os=-aout + ;; +- c4x-* | tic4x-*) +- os=-coff +- ;; ++ c4x-* | tic4x-*) ++ os=-coff ++ ;; ++ hexagon-*) ++ os=-elf ++ ;; ++ tic54x-*) ++ os=-coff ++ ;; ++ tic55x-*) ++ os=-coff ++ ;; ++ tic6x-*) ++ os=-coff ++ ;; + # This must come before the *-dec entry. + pdp10-*) + os=-tops20 +@@ -1399,13 +1581,13 @@ case $basic_machine in + ;; + m68000-sun) + os=-sunos3 +- # This also exists in the configure program, but was not the +- # default. +- # os=-sunos4 + ;; + m68*-cisco) + os=-aout + ;; ++ mep-*) ++ os=-elf ++ ;; + mips*-cisco) + os=-elf + ;; +@@ -1430,7 +1612,7 @@ case $basic_machine in + *-ibm) + os=-aix + ;; +- *-knuth) ++ *-knuth) + os=-mmixware + ;; + *-wec) +@@ -1535,7 +1717,7 @@ case $basic_machine in + -sunos*) + vendor=sun + ;; +- -aix*) ++ -cnk*|-aix*) + vendor=ibm + ;; + -beos*) +diff --git a/configure b/configure +index a9e9814..7fd6318 100755 +--- a/configure ++++ b/configure +@@ -1,4 +1,5 @@ + #! /bin/sh ++set -- --host=arm-linux-androideabi + # Guess values for system-dependent variables and create Makefiles. + # Generated by GNU Autoconf 2.69 for Haskell network package 2.3.0.14. + # +diff --git a/include/HsNetworkConfig.h b/include/HsNetworkConfig.h +index c6e704d..4edc892 100644 +--- a/include/HsNetworkConfig.h ++++ b/include/HsNetworkConfig.h +@@ -8,7 +8,7 @@ + #define HAVE_ARPA_INET_H 1 + + /* Define to 1 if you have a BSDish sendfile(2) implementation. */ +-#define HAVE_BSD_SENDFILE 1 ++/* #undef HAVE_BSD_SENDFILE */ + + /* Define to 1 if you have the declaration of `AI_ADDRCONFIG', and to 0 if you + don't. */ +@@ -55,7 +55,7 @@ + #define HAVE_LIMITS_H 1 + + /* Define to 1 if you have a Linux sendfile(2) implementation. */ +-/* #undef HAVE_LINUX_SENDFILE */ ++#define HAVE_LINUX_SENDFILE 1 + + /* Define to 1 if you have the header file. */ + #define HAVE_MEMORY_H 1 +@@ -91,10 +91,10 @@ + #define HAVE_STRUCT_MSGHDR_MSG_CONTROL 1 + + /* Define to 1 if `sa_len' is a member of `struct sockaddr'. */ +-#define HAVE_STRUCT_SOCKADDR_SA_LEN 1 ++/* #undef HAVE_STRUCT_SOCKADDR_SA_LEN */ + + /* Define to 1 if you have both SO_PEERCRED and struct ucred. */ +-/* #undef HAVE_STRUCT_UCRED */ ++#define HAVE_STRUCT_UCRED 1 + + /* Define to 1 if you have the `symlink' function. */ + #define HAVE_SYMLINK 1 +-- +1.7.10.4 + diff --git a/standalone/android/haskell-patches/network_2.4.1.0_0002-remove-Network.BSD-symbols-not-available-in-bionic.patch b/standalone/android/haskell-patches/network_2.4.1.0_0002-remove-Network.BSD-symbols-not-available-in-bionic.patch new file mode 100644 index 0000000000..324809ad92 --- /dev/null +++ b/standalone/android/haskell-patches/network_2.4.1.0_0002-remove-Network.BSD-symbols-not-available-in-bionic.patch @@ -0,0 +1,157 @@ +From 8456a3b26261212052bad4d1de207504a3f85995 Mon Sep 17 00:00:00 2001 +From: Joey Hess +Date: Fri, 19 Apr 2013 15:14:10 -0400 +Subject: [PATCH] remove Network.BSD symbols not available in bionic + +--- + Network/BSD.hsc | 98 --------------------------------------------------------- + 1 file changed, 98 deletions(-) + +diff --git a/Network/BSD.hsc b/Network/BSD.hsc +index c199036..f0c9f5b 100644 +--- a/Network/BSD.hsc ++++ b/Network/BSD.hsc +@@ -30,15 +30,6 @@ module Network.BSD + , getHostByAddr + , hostAddress + +-#if defined(HAVE_GETHOSTENT) && !defined(cygwin32_HOST_OS) && !defined(mingw32_HOST_OS) && !defined(_WIN32) +- , getHostEntries +- +- -- ** Low level functionality +- , setHostEntry +- , getHostEntry +- , endHostEntry +-#endif +- + -- * Service names + , ServiceEntry(..) + , ServiceName +@@ -64,14 +55,6 @@ module Network.BSD + , getProtocolNumber + , defaultProtocol + +-#if !defined(cygwin32_HOST_OS) && !defined(mingw32_HOST_OS) && !defined(_WIN32) +- , getProtocolEntries +- -- ** Low level functionality +- , setProtocolEntry +- , getProtocolEntry +- , endProtocolEntry +-#endif +- + -- * Port numbers + , PortNumber + +@@ -83,11 +66,7 @@ module Network.BSD + #if !defined(cygwin32_HOST_OS) && !defined(mingw32_HOST_OS) && !defined(_WIN32) + , getNetworkByName + , getNetworkByAddr +- , getNetworkEntries + -- ** Low level functionality +- , setNetworkEntry +- , getNetworkEntry +- , endNetworkEntry + #endif + ) where + +@@ -305,31 +284,6 @@ getProtocolNumber proto = do + (ProtocolEntry _ _ num) <- getProtocolByName proto + return num + +-#if !defined(cygwin32_HOST_OS) && !defined(mingw32_HOST_OS) && !defined(_WIN32) +-getProtocolEntry :: IO ProtocolEntry -- Next Protocol Entry from DB +-getProtocolEntry = withLock $ do +- ent <- throwNoSuchThingIfNull "getProtocolEntry" "no such protocol entry" +- $ trySysCall c_getprotoent +- peek ent +- +-foreign import ccall unsafe "getprotoent" c_getprotoent :: IO (Ptr ProtocolEntry) +- +-setProtocolEntry :: Bool -> IO () -- Keep DB Open ? +-setProtocolEntry flg = withLock $ trySysCall $ c_setprotoent (fromBool flg) +- +-foreign import ccall unsafe "setprotoent" c_setprotoent :: CInt -> IO () +- +-endProtocolEntry :: IO () +-endProtocolEntry = withLock $ trySysCall $ c_endprotoent +- +-foreign import ccall unsafe "endprotoent" c_endprotoent :: IO () +- +-getProtocolEntries :: Bool -> IO [ProtocolEntry] +-getProtocolEntries stayOpen = withLock $ do +- setProtocolEntry stayOpen +- getEntries (getProtocolEntry) (endProtocolEntry) +-#endif +- + -- --------------------------------------------------------------------------- + -- Host lookups + +@@ -404,31 +358,6 @@ getHostByAddr family addr = do + foreign import CALLCONV safe "gethostbyaddr" + c_gethostbyaddr :: Ptr HostAddress -> CInt -> CInt -> IO (Ptr HostEntry) + +-#if defined(HAVE_GETHOSTENT) && !defined(cygwin32_HOST_OS) && !defined(mingw32_HOST_OS) && !defined(_WIN32) +-getHostEntry :: IO HostEntry +-getHostEntry = withLock $ do +- throwNoSuchThingIfNull "getHostEntry" "unable to retrieve host entry" +- $ trySysCall $ c_gethostent +- >>= peek +- +-foreign import ccall unsafe "gethostent" c_gethostent :: IO (Ptr HostEntry) +- +-setHostEntry :: Bool -> IO () +-setHostEntry flg = withLock $ trySysCall $ c_sethostent (fromBool flg) +- +-foreign import ccall unsafe "sethostent" c_sethostent :: CInt -> IO () +- +-endHostEntry :: IO () +-endHostEntry = withLock $ c_endhostent +- +-foreign import ccall unsafe "endhostent" c_endhostent :: IO () +- +-getHostEntries :: Bool -> IO [HostEntry] +-getHostEntries stayOpen = do +- setHostEntry stayOpen +- getEntries (getHostEntry) (endHostEntry) +-#endif +- + -- --------------------------------------------------------------------------- + -- Accessing network information + +@@ -490,33 +419,6 @@ getNetworkByAddr addr family = withLock $ do + foreign import ccall unsafe "getnetbyaddr" + c_getnetbyaddr :: NetworkAddr -> CInt -> IO (Ptr NetworkEntry) + +-getNetworkEntry :: IO NetworkEntry +-getNetworkEntry = withLock $ do +- throwNoSuchThingIfNull "getNetworkEntry" "no more network entries" +- $ trySysCall $ c_getnetent +- >>= peek +- +-foreign import ccall unsafe "getnetent" c_getnetent :: IO (Ptr NetworkEntry) +- +--- | Open the network name database. The parameter specifies +--- whether a connection is maintained open between various +--- networkEntry calls +-setNetworkEntry :: Bool -> IO () +-setNetworkEntry flg = withLock $ trySysCall $ c_setnetent (fromBool flg) +- +-foreign import ccall unsafe "setnetent" c_setnetent :: CInt -> IO () +- +--- | Close the connection to the network name database. +-endNetworkEntry :: IO () +-endNetworkEntry = withLock $ trySysCall $ c_endnetent +- +-foreign import ccall unsafe "endnetent" c_endnetent :: IO () +- +--- | Get the list of network entries. +-getNetworkEntries :: Bool -> IO [NetworkEntry] +-getNetworkEntries stayOpen = do +- setNetworkEntry stayOpen +- getEntries (getNetworkEntry) (endNetworkEntry) + #endif + + -- Mutex for name service lockdown +-- +1.8.2.rc3 + diff --git a/standalone/android/haskell-patches/network_2.4.1.0_0003-configure-misdetects-accept4.patch b/standalone/android/haskell-patches/network_2.4.1.0_0003-configure-misdetects-accept4.patch new file mode 100644 index 0000000000..9be862b5a4 --- /dev/null +++ b/standalone/android/haskell-patches/network_2.4.1.0_0003-configure-misdetects-accept4.patch @@ -0,0 +1,34 @@ +From 5b14dd83f9ff1d187871fc7c6e956cad95bc4e9b Mon Sep 17 00:00:00 2001 +From: Joey Hess +Date: Fri, 19 Apr 2013 15:36:25 -0400 +Subject: [PATCH] configure misdetects accept4 + +--- + Network/Socket.hsc | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +diff --git a/Network/Socket.hsc b/Network/Socket.hsc +index e6c0feb..49d090b 100644 +--- a/Network/Socket.hsc ++++ b/Network/Socket.hsc +@@ -505,7 +505,7 @@ accept sock@(MkSocket s family stype protocol status) = do + #else + with (fromIntegral sz) $ \ ptr_len -> do + new_sock <- +-# ifdef HAVE_ACCEPT4 ++# if 0 + throwSocketErrorIfMinus1RetryMayBlock "accept" + (threadWaitRead (fromIntegral s)) + (c_accept4 s sockaddr ptr_len (#const SOCK_NONBLOCK)) +@@ -1589,7 +1589,7 @@ foreign import CALLCONV SAFE_ON_WIN "connect" + c_connect :: CInt -> Ptr SockAddr -> CInt{-CSockLen???-} -> IO CInt + foreign import CALLCONV unsafe "accept" + c_accept :: CInt -> Ptr SockAddr -> Ptr CInt{-CSockLen???-} -> IO CInt +-#ifdef HAVE_ACCEPT4 ++#if 0 + foreign import CALLCONV unsafe "accept4" + c_accept4 :: CInt -> Ptr SockAddr -> Ptr CInt{-CSockLen???-} -> CInt -> IO CInt + #endif +-- +1.8.2.rc3 + diff --git a/standalone/android/haskell-patches/network_2.4.1.0_0004-getprotobyname-hack-for-tcp-and-udp.patch b/standalone/android/haskell-patches/network_2.4.1.0_0004-getprotobyname-hack-for-tcp-and-udp.patch new file mode 100644 index 0000000000..4cc22cbcac --- /dev/null +++ b/standalone/android/haskell-patches/network_2.4.1.0_0004-getprotobyname-hack-for-tcp-and-udp.patch @@ -0,0 +1,28 @@ +From b1a581007759e2d9e53ef776e4f10d1de87b8377 Mon Sep 17 00:00:00 2001 +From: Joey Hess +Date: Tue, 7 May 2013 14:51:09 -0400 +Subject: [PATCH] getprotobyname hack for tcp and udp + +Otherwise, core network stuff fails to get the numbers for these protocols. +--- + Network/BSD.hsc | 4 ++++ + 1 file changed, 4 insertions(+) + +diff --git a/Network/BSD.hsc b/Network/BSD.hsc +index f0c9f5b..a289143 100644 +--- a/Network/BSD.hsc ++++ b/Network/BSD.hsc +@@ -259,6 +259,10 @@ instance Storable ProtocolEntry where + poke _p = error "Storable.poke(BSD.ProtocolEntry) not implemented" + + getProtocolByName :: ProtocolName -> IO ProtocolEntry ++getProtocolByName "tcp" = return $ ++ ProtocolEntry {protoName = "tcp", protoAliases = ["TCP"], protoNumber = 6} ++getProtocolByName "udp" = return $ ++ ProtocolEntry {protoName = "udp", protoAliases = ["UDP"], protoNumber = 17} + getProtocolByName name = withLock $ do + withCString name $ \ name_cstr -> do + throwNoSuchThingIfNull "getProtocolByName" ("no such protocol name: " ++ name) +-- +1.8.2.rc3 + 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 index 0000000000..38cecc5c72 --- /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 +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/primitive_0.5.0.1_0001-disable-i386-opt-stuff-to-allow-cross-compilation.patch b/standalone/android/haskell-patches/primitive_0.5.0.1_0001-disable-i386-opt-stuff-to-allow-cross-compilation.patch new file mode 100644 index 0000000000..1bd9268718 --- /dev/null +++ b/standalone/android/haskell-patches/primitive_0.5.0.1_0001-disable-i386-opt-stuff-to-allow-cross-compilation.patch @@ -0,0 +1,24 @@ +From 5cb5c3dabb213f809b8328b0b4049f7c754e9c77 Mon Sep 17 00:00:00 2001 +From: Joey Hess +Date: Thu, 28 Feb 2013 23:34:32 -0400 +Subject: [PATCH] disable i386 opt stuff to allow cross-compilation + +--- + primitive.cabal | 3 --- + 1 file changed, 3 deletions(-) + +diff --git a/primitive.cabal b/primitive.cabal +index 8c4328a..9a6093f 100644 +--- a/primitive.cabal ++++ b/primitive.cabal +@@ -51,7 +51,4 @@ Library + includes: primitive-memops.h + c-sources: cbits/primitive-memops.c + cc-options: -O3 -ftree-vectorize -fomit-frame-pointer +- if arch(i386) || arch(x86_64) { +- cc-options: -msse2 +- } + +-- +1.7.10.4 + diff --git a/standalone/android/haskell-patches/profunctors_3.3-0001-fix-cross-build.patch b/standalone/android/haskell-patches/profunctors_3.3-0001-fix-cross-build.patch new file mode 100644 index 0000000000..45397f3e5d --- /dev/null +++ b/standalone/android/haskell-patches/profunctors_3.3-0001-fix-cross-build.patch @@ -0,0 +1,26 @@ +From 392602f5ff14c0b5a801397d075ddcbcd890aa83 Mon Sep 17 00:00:00 2001 +From: Joey Hess +Date: Thu, 18 Apr 2013 17:50:59 -0400 +Subject: [PATCH] fix cross build + +--- + src/Data/Profunctor/Unsafe.hs | 3 --- + 1 file changed, 3 deletions(-) + +diff --git a/src/Data/Profunctor/Unsafe.hs b/src/Data/Profunctor/Unsafe.hs +index 025c7c4..0249274 100644 +--- a/src/Data/Profunctor/Unsafe.hs ++++ b/src/Data/Profunctor/Unsafe.hs +@@ -40,9 +40,6 @@ import Data.Tagged + import Prelude hiding (id,(.),sequence) + import Unsafe.Coerce + +-{-# ANN module "Hlint: ignore Redundant lambda" #-} +-{-# ANN module "Hlint: ignore Collapse lambdas" #-} +- + infixr 9 #. + infixl 8 .# + +-- +1.8.2.rc3 + diff --git a/standalone/android/haskell-patches/resourcet_0.4.4_0001-hack-to-build-with-hacked-up-lifted-base-which-is-cu.patch b/standalone/android/haskell-patches/resourcet_0.4.4_0001-hack-to-build-with-hacked-up-lifted-base-which-is-cu.patch new file mode 100644 index 0000000000..bcf3439fac --- /dev/null +++ b/standalone/android/haskell-patches/resourcet_0.4.4_0001-hack-to-build-with-hacked-up-lifted-base-which-is-cu.patch @@ -0,0 +1,44 @@ +From c10ab80793a21dce0c7516725e1ca3b36a87aa25 Mon Sep 17 00:00:00 2001 +From: Joey Hess +Date: Thu, 28 Feb 2013 23:35:08 -0400 +Subject: [PATCH] hack to build with hacked up lifted-base, which is currently + lacking a mask + +--- + Control/Monad/Trans/Resource.hs | 6 +++--- + 1 file changed, 3 insertions(+), 3 deletions(-) + +diff --git a/Control/Monad/Trans/Resource.hs b/Control/Monad/Trans/Resource.hs +index d209dd8..61ab349 100644 +--- a/Control/Monad/Trans/Resource.hs ++++ b/Control/Monad/Trans/Resource.hs +@@ -5,7 +5,7 @@ + {-# LANGUAGE TypeFamilies #-} + {-# LANGUAGE RankNTypes #-} + {-# LANGUAGE CPP #-} +-{-# LANGUAGE DeriveDataTypeable #-} ++{-# LANGUAGE DeriveDataTypeable, ImpredicativeTypes #-} + #if __GLASGOW_HASKELL__ >= 704 + {-# LANGUAGE ConstraintKinds #-} + #endif +@@ -554,7 +554,7 @@ GOX(Monoid w, Strict.WriterT w) + -- + -- Since 0.3.0 + resourceForkIO :: MonadBaseControl IO m => ResourceT m () -> ResourceT m ThreadId +-resourceForkIO (ResourceT f) = ResourceT $ \r -> L.mask $ \restore -> ++resourceForkIO (ResourceT f) = ResourceT $ \r -> + -- We need to make sure the counter is incremented before this call + -- returns. Otherwise, the parent thread may call runResourceT before + -- the child thread increments, and all resources will be freed +@@ -565,7 +565,7 @@ resourceForkIO (ResourceT f) = ResourceT $ \r -> L.mask $ \restore -> + (liftBaseDiscard forkIO $ bracket_ + (return ()) + (stateCleanup r) +- (restore $ f r)) ++ (return ())) + + -- | A @Monad@ based on some monad which allows running of some 'IO' actions, + -- via unsafe calls. This applies to 'IO' and 'ST', for instance. +-- +1.7.10.4 + diff --git a/standalone/android/haskell-patches/shakespeare-css_1.0.2_0001-remove-TH.patch b/standalone/android/haskell-patches/shakespeare-css_1.0.2_0001-remove-TH.patch new file mode 100644 index 0000000000..f868197a8c --- /dev/null +++ b/standalone/android/haskell-patches/shakespeare-css_1.0.2_0001-remove-TH.patch @@ -0,0 +1,274 @@ +From 8f058e84892a8c4202275f524f74bd6a7097ad40 Mon Sep 17 00:00:00 2001 +From: Joey Hess +Date: Wed, 8 May 2013 02:07:15 -0400 +Subject: [PATCH] remove TH + +--- + Text/Cassius.hs | 23 -------------- + Text/Css.hs | 84 ------------------------------------------------- + Text/CssCommon.hs | 4 --- + Text/Lucius.hs | 30 +----------------- + shakespeare-css.cabal | 2 +- + 5 files changed, 2 insertions(+), 141 deletions(-) + +diff --git a/Text/Cassius.hs b/Text/Cassius.hs +index ce05374..ae56b0a 100644 +--- a/Text/Cassius.hs ++++ b/Text/Cassius.hs +@@ -13,10 +13,6 @@ module Text.Cassius + , renderCss + , renderCssUrl + -- * Parsing +- , cassius +- , cassiusFile +- , cassiusFileDebug +- , cassiusFileReload + -- * ToCss instances + -- ** Color + , Color (..) +@@ -27,11 +23,8 @@ module Text.Cassius + , AbsoluteUnit (..) + , AbsoluteSize (..) + , absoluteSize +- , EmSize (..) +- , ExSize (..) + , PercentageSize (..) + , percentageSize +- , PixelSize (..) + -- * Internal + , cassiusUsedIdentifiers + ) where +@@ -42,25 +35,9 @@ import Language.Haskell.TH.Quote (QuasiQuoter (..)) + import Language.Haskell.TH.Syntax + import qualified Data.Text.Lazy as TL + import Text.CssCommon +-import Text.Lucius (lucius) + import qualified Text.Lucius + import Text.IndentToBrace (i2b) + +-cassius :: QuasiQuoter +-cassius = QuasiQuoter { quoteExp = quoteExp lucius . i2b } +- +-cassiusFile :: FilePath -> Q Exp +-cassiusFile fp = do +-#ifdef GHC_7_4 +- qAddDependentFile fp +-#endif +- contents <- fmap TL.unpack $ qRunIO $ readUtf8File fp +- quoteExp cassius contents +- +-cassiusFileDebug, cassiusFileReload :: FilePath -> Q Exp +-cassiusFileDebug = cssFileDebug True [|Text.Lucius.parseTopLevels|] Text.Lucius.parseTopLevels +-cassiusFileReload = cassiusFileDebug +- + -- | Determine which identifiers are used by the given template, useful for + -- creating systems like yesod devel. + cassiusUsedIdentifiers :: String -> [(Deref, VarType)] +diff --git a/Text/Css.hs b/Text/Css.hs +index 8e6fc09..401a166 100644 +--- a/Text/Css.hs ++++ b/Text/Css.hs +@@ -108,19 +108,6 @@ cssUsedIdentifiers toi2b parseBlocks s' = + (scope, rest') = go rest + go' (k, v) = k ++ v + +-cssFileDebug :: Bool -- ^ perform the indent-to-brace conversion +- -> Q Exp -> Parser [TopLevel] -> FilePath -> Q Exp +-cssFileDebug toi2b parseBlocks' parseBlocks fp = do +- s <- fmap TL.unpack $ qRunIO $ readUtf8File fp +-#ifdef GHC_7_4 +- qAddDependentFile fp +-#endif +- let vs = cssUsedIdentifiers toi2b parseBlocks s +- c <- mapM vtToExp vs +- cr <- [|cssRuntime toi2b|] +- parseBlocks'' <- parseBlocks' +- return $ cr `AppE` parseBlocks'' `AppE` (LitE $ StringL fp) `AppE` ListE c +- + combineSelectors :: Selector -> Selector -> Selector + combineSelectors a b = do + a' <- a +@@ -202,17 +189,6 @@ cssRuntime toi2b parseBlocks fp cd render' = unsafePerformIO $ do + + addScope scope = map (DerefIdent . Ident *** CDPlain . fromString) scope ++ cd + +-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 = [|CDPlain . toCss|] +- c VTUrl = [|CDUrl|] +- c VTUrlParam = [|CDUrlParam|] +- + getVars :: Monad m => [(String, String)] -> Content -> m [(Deref, VarType)] + getVars _ ContentRaw{} = return [] + getVars scope (ContentVar d) = +@@ -268,68 +244,8 @@ compressBlock (Block x y blocks) = + cc (ContentRaw a:ContentRaw b:c) = cc $ ContentRaw (a ++ b) : c + cc (a:b) = a : cc b + +-blockToCss :: Name -> Scope -> Block -> Q Exp +-blockToCss r scope (Block sel props subblocks) = +- [|(:) (Css' $(selectorToBuilder r scope sel) $(listE $ map go props)) +- . foldr (.) id $(listE $ map subGo subblocks) +- |] +- where +- go (x, y) = tupE [contentsToBuilder r scope x, contentsToBuilder r scope y] +- subGo (Block sel' b c) = +- blockToCss r scope $ Block sel'' b c +- where +- sel'' = combineSelectors sel sel' +- +-selectorToBuilder :: Name -> Scope -> Selector -> Q Exp +-selectorToBuilder r scope sels = +- contentsToBuilder r scope $ intercalate [ContentRaw ","] sels +- +-contentsToBuilder :: Name -> Scope -> [Content] -> Q Exp +-contentsToBuilder r scope contents = +- appE [|mconcat|] $ listE $ map (contentToBuilder r scope) contents +- +-contentToBuilder :: Name -> Scope -> Content -> Q Exp +-contentToBuilder _ _ (ContentRaw x) = +- [|fromText . pack|] `appE` litE (StringL x) +-contentToBuilder _ scope (ContentVar d) = +- case d of +- DerefIdent (Ident s) +- | Just val <- lookup s scope -> [|fromText . pack|] `appE` litE (StringL val) +- _ -> [|toCss|] `appE` return (derefToExp [] d) +-contentToBuilder r _ (ContentUrl u) = +- [|fromText|] `appE` +- (varE r `appE` return (derefToExp [] u) `appE` listE []) +-contentToBuilder r _ (ContentUrlParam u) = +- [|fromText|] `appE` +- ([|uncurry|] `appE` varE r `appE` return (derefToExp [] u)) +- + type Scope = [(String, String)] + +-topLevelsToCassius :: [TopLevel] -> Q Exp +-topLevelsToCassius a = do +- r <- newName "_render" +- lamE [varP r] $ appE [|CssNoWhitespace . foldr ($) []|] $ fmap ListE $ go r [] a +- where +- go _ _ [] = return [] +- go r scope (TopBlock b:rest) = do +- e <- [|(++) $ map Css ($(blockToCss r scope b) [])|] +- es <- go r scope rest +- return $ e : es +- go r scope (TopAtBlock name s b:rest) = do +- let s' = contentsToBuilder r scope s +- e <- [|(:) $ AtBlock $(lift name) $(s') $(blocksToCassius r scope b)|] +- es <- go r scope rest +- return $ e : es +- go r scope (TopAtDecl dec cs:rest) = do +- e <- [|(:) $ AtDecl $(lift dec) $(contentsToBuilder r scope cs)|] +- es <- go r scope rest +- return $ e : es +- go r scope (TopVar k v:rest) = go r ((k, v) : scope) rest +- +-blocksToCassius :: Name -> Scope -> [Block] -> Q Exp +-blocksToCassius r scope a = do +- appE [|foldr ($) []|] $ listE $ map (blockToCss r scope) a +- + renderCss :: Css -> TL.Text + renderCss css = + toLazyText $ mconcat $ map go tops-- FIXME use a foldr +diff --git a/Text/CssCommon.hs b/Text/CssCommon.hs +index 719e0a8..8c40e8c 100644 +--- a/Text/CssCommon.hs ++++ b/Text/CssCommon.hs +@@ -1,4 +1,3 @@ +-{-# LANGUAGE TemplateHaskell #-} + {-# LANGUAGE GeneralizedNewtypeDeriving #-} + {-# LANGUAGE FlexibleInstances #-} + {-# LANGUAGE CPP #-} +@@ -156,6 +155,3 @@ showSize :: Rational -> String -> String + showSize value' unit = printf "%f" value ++ unit + where value = fromRational value' :: Double + +-mkSizeType "EmSize" "em" +-mkSizeType "ExSize" "ex" +-mkSizeType "PixelSize" "px" +diff --git a/Text/Lucius.hs b/Text/Lucius.hs +index b71614e..a902e1c 100644 +--- a/Text/Lucius.hs ++++ b/Text/Lucius.hs +@@ -6,12 +6,8 @@ + {-# OPTIONS_GHC -fno-warn-missing-fields #-} + module Text.Lucius + ( -- * Parsing +- lucius +- , luciusFile +- , luciusFileDebug +- , luciusFileReload + -- ** Runtime +- , luciusRT ++ luciusRT + , luciusRT' + , -- * Datatypes + Css +@@ -31,11 +27,8 @@ module Text.Lucius + , AbsoluteUnit (..) + , AbsoluteSize (..) + , absoluteSize +- , EmSize (..) +- , ExSize (..) + , PercentageSize (..) + , percentageSize +- , PixelSize (..) + -- * Internal + , parseTopLevels + , luciusUsedIdentifiers +@@ -57,18 +50,6 @@ import Data.Either (partitionEithers) + import Data.Monoid (mconcat) + import Data.List (isSuffixOf) + +--- | +--- +--- >>> renderCss ([lucius|foo{bar:baz}|] undefined) +--- "foo{bar:baz}" +-lucius :: QuasiQuoter +-lucius = QuasiQuoter { quoteExp = luciusFromString } +- +-luciusFromString :: String -> Q Exp +-luciusFromString s = +- topLevelsToCassius +- $ either (error . show) id $ parse parseTopLevels s s +- + whiteSpace :: Parser () + whiteSpace = many whiteSpace1 >> return () + +@@ -179,15 +160,6 @@ parseComment = do + _ <- manyTill anyChar $ try $ string "*/" + return $ ContentRaw "" + +-luciusFile :: FilePath -> Q Exp +-luciusFile fp = do +- contents <- fmap TL.unpack $ qRunIO $ readUtf8File fp +- luciusFromString contents +- +-luciusFileDebug, luciusFileReload :: FilePath -> Q Exp +-luciusFileDebug = cssFileDebug False [|parseTopLevels|] parseTopLevels +-luciusFileReload = luciusFileDebug +- + parseTopLevels :: Parser [TopLevel] + parseTopLevels = + go id +diff --git a/shakespeare-css.cabal b/shakespeare-css.cabal +index de2497b..874a3b5 100644 +--- a/shakespeare-css.cabal ++++ b/shakespeare-css.cabal +@@ -33,7 +33,7 @@ library + , shakespeare >= 1.0 && < 1.1 + , template-haskell + , text >= 0.11.1.1 && < 0.12 +- , process >= 1.0 && < 1.2 ++ , process >= 1.0 && < 1.3 + , parsec >= 2 && < 4 + , transformers + +-- +1.7.10.4 + diff --git a/standalone/android/haskell-patches/shakespeare-css_1.0.2_0002-expose-modules-used-by-TH.patch b/standalone/android/haskell-patches/shakespeare-css_1.0.2_0002-expose-modules-used-by-TH.patch new file mode 100644 index 0000000000..5bf57d527a --- /dev/null +++ b/standalone/android/haskell-patches/shakespeare-css_1.0.2_0002-expose-modules-used-by-TH.patch @@ -0,0 +1,26 @@ +From 23e96f0d948e7a26febf1745a4c373faf579c8ee Mon Sep 17 00:00:00 2001 +From: Joey Hess +Date: Mon, 15 Apr 2013 16:32:31 -0400 +Subject: [PATCH] expose modules used by TH + +--- + shakespeare-css.cabal | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/shakespeare-css.cabal b/shakespeare-css.cabal +index de2497b..468353a 100644 +--- a/shakespeare-css.cabal ++++ b/shakespeare-css.cabal +@@ -39,8 +39,8 @@ library + + exposed-modules: Text.Cassius + Text.Lucius +- other-modules: Text.MkSizeType + Text.Css ++ other-modules: Text.MkSizeType + Text.IndentToBrace + Text.CssCommon + ghc-options: -Wall +-- +1.8.2.rc3 + diff --git a/standalone/android/haskell-patches/shakespeare-i18n_1.0.0.2_0001-remove-TH.patch b/standalone/android/haskell-patches/shakespeare-i18n_1.0.0.2_0001-remove-TH.patch new file mode 100644 index 0000000000..60528db0dd --- /dev/null +++ b/standalone/android/haskell-patches/shakespeare-i18n_1.0.0.2_0001-remove-TH.patch @@ -0,0 +1,162 @@ +From b128412ecee9677b788abecbbf1fd1edd447eea2 Mon Sep 17 00:00:00 2001 +From: Joey Hess +Date: Thu, 28 Feb 2013 23:35:59 -0400 +Subject: [PATCH] remove TH + +--- + Text/Shakespeare/I18N.hs | 130 +--------------------------------------------- + 1 file changed, 1 insertion(+), 129 deletions(-) + +diff --git a/Text/Shakespeare/I18N.hs b/Text/Shakespeare/I18N.hs +index 1b486ed..aa5e358 100644 +--- a/Text/Shakespeare/I18N.hs ++++ b/Text/Shakespeare/I18N.hs +@@ -51,10 +51,7 @@ + -- + -- You can also adapt those instructions for use with other systems. + module Text.Shakespeare.I18N +- ( mkMessage +- , mkMessageFor +- , mkMessageVariant +- , RenderMessage (..) ++ ( RenderMessage (..) + , ToMessage (..) + , SomeMessage (..) + , Lang +@@ -115,133 +112,8 @@ type Lang = Text + -- + -- 3. create a 'RenderMessage' instance + -- +-mkMessage :: String -- ^ base name to use for translation type +- -> FilePath -- ^ subdirectory which contains the translation files +- -> Lang -- ^ default translation language +- -> Q [Dec] +-mkMessage dt folder lang = +- mkMessageCommon True "Msg" "Message" dt dt folder lang + + +--- | create 'RenderMessage' instance for an existing data-type +-mkMessageFor :: String -- ^ master translation data type +- -> String -- ^ existing type to add translations for +- -> FilePath -- ^ path to translation folder +- -> Lang -- ^ default language +- -> Q [Dec] +-mkMessageFor master dt folder lang = mkMessageCommon False "" "" master dt folder lang +- +--- | create an additional set of translations for a type created by `mkMessage` +-mkMessageVariant :: String -- ^ master translation data type +- -> String -- ^ existing type to add translations for +- -> FilePath -- ^ path to translation folder +- -> Lang -- ^ default language +- -> Q [Dec] +-mkMessageVariant master dt folder lang = mkMessageCommon False "Msg" "Message" master dt folder lang +- +--- |used by 'mkMessage' and 'mkMessageFor' to generate a 'RenderMessage' and possibly a message data type +-mkMessageCommon :: Bool -- ^ generate a new datatype from the constructors found in the .msg files +- -> String -- ^ string to append to constructor names +- -> String -- ^ string to append to datatype name +- -> String -- ^ base name of master datatype +- -> String -- ^ base name of translation datatype +- -> FilePath -- ^ path to translation folder +- -> Lang -- ^ default lang +- -> Q [Dec] +-mkMessageCommon genType prefix postfix master dt folder lang = do +- files <- qRunIO $ getDirectoryContents folder +- (_files', contents) <- qRunIO $ fmap (unzip . catMaybes) $ mapM (loadLang folder) files +-#ifdef GHC_7_4 +- mapM_ qAddDependentFile _files' +-#endif +- sdef <- +- case lookup lang contents of +- Nothing -> error $ "Did not find main language file: " ++ unpack lang +- Just def -> toSDefs def +- mapM_ (checkDef sdef) $ map snd contents +- let mname = mkName $ dt ++ postfix +- c1 <- fmap concat $ mapM (toClauses prefix dt) contents +- c2 <- mapM (sToClause prefix dt) sdef +- c3 <- defClause +- return $ +- ( if genType +- then ((DataD [] mname [] (map (toCon dt) sdef) []) :) +- else id) +- [ InstanceD +- [] +- (ConT ''RenderMessage `AppT` (ConT $ mkName master) `AppT` ConT mname) +- [ FunD (mkName "renderMessage") $ c1 ++ c2 ++ [c3] +- ] +- ] +- +-toClauses :: String -> String -> (Lang, [Def]) -> Q [Clause] +-toClauses prefix dt (lang, defs) = +- mapM go defs +- where +- go def = do +- a <- newName "lang" +- (pat, bod) <- mkBody dt (prefix ++ constr def) (map fst $ vars def) (content def) +- guard <- fmap NormalG [|$(return $ VarE a) == pack $(lift $ unpack lang)|] +- return $ Clause +- [WildP, ConP (mkName ":") [VarP a, WildP], pat] +- (GuardedB [(guard, bod)]) +- [] +- +-mkBody :: String -- ^ datatype +- -> String -- ^ constructor +- -> [String] -- ^ variable names +- -> [Content] +- -> Q (Pat, Exp) +-mkBody dt cs vs ct = do +- vp <- mapM go vs +- let pat = RecP (mkName cs) (map (varName dt *** VarP) vp) +- let ct' = map (fixVars vp) ct +- pack' <- [|Data.Text.pack|] +- tomsg <- [|toMessage|] +- let ct'' = map (toH pack' tomsg) ct' +- mapp <- [|mappend|] +- let app a b = InfixE (Just a) mapp (Just b) +- e <- +- case ct'' of +- [] -> [|mempty|] +- [x] -> return x +- (x:xs) -> return $ foldl' app x xs +- return (pat, e) +- where +- toH pack' _ (Raw s) = pack' `AppE` SigE (LitE (StringL s)) (ConT ''String) +- toH _ tomsg (Var d) = tomsg `AppE` derefToExp [] d +- go x = do +- let y = mkName $ '_' : x +- return (x, y) +- fixVars vp (Var d) = Var $ fixDeref vp d +- fixVars _ (Raw s) = Raw s +- fixDeref vp (DerefIdent (Ident i)) = DerefIdent $ Ident $ fixIdent vp i +- fixDeref vp (DerefBranch a b) = DerefBranch (fixDeref vp a) (fixDeref vp b) +- fixDeref _ d = d +- fixIdent vp i = +- case lookup i vp of +- Nothing -> i +- Just y -> nameBase y +- +-sToClause :: String -> String -> SDef -> Q Clause +-sToClause prefix dt sdef = do +- (pat, bod) <- mkBody dt (prefix ++ sconstr sdef) (map fst $ svars sdef) (scontent sdef) +- return $ Clause +- [WildP, ConP (mkName "[]") [], pat] +- (NormalB bod) +- [] +- +-defClause :: Q Clause +-defClause = do +- a <- newName "sub" +- c <- newName "langs" +- d <- newName "msg" +- rm <- [|renderMessage|] +- return $ Clause +- [VarP a, ConP (mkName ":") [WildP, VarP c], VarP d] +- (NormalB $ rm `AppE` VarE a `AppE` VarE c `AppE` VarE d) +- [] +- + toCon :: String -> SDef -> Con + toCon dt (SDef c vs _) = + RecC (mkName $ "Msg" ++ c) $ map go vs +-- +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 index 0000000000..98a16ae079 --- /dev/null +++ b/standalone/android/haskell-patches/shakespeare-js_1.1.2_0001-remove-TH.patch @@ -0,0 +1,308 @@ +From 332c71b3f6bc4786b914e675020a23c492beee5a Mon Sep 17 00:00:00 2001 +From: Joey Hess +Date: Tue, 7 May 2013 19:28:06 -0400 +Subject: [PATCH] remove TH + +--- + Text/Coffee.hs | 54 ------------------------------------------------- + Text/Julius.hs | 56 ++++----------------------------------------------- + Text/Roy.hs | 54 ------------------------------------------------- + Text/TypeScript.hs | 57 +--------------------------------------------------- + 4 files changed, 5 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..1a0376f 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,9 +28,11 @@ module Text.Julius + , renderJavascriptUrl + + -- ** internal, used by 'Text.Coffee' +- , javascriptSettings + -- ** internal + , juliusUsedIdentifiers ++ ++ -- used by TH splices ++ , asJavascriptUrl + ) where + + import Language.Haskell.TH.Quote (QuasiQuoter (..)) +@@ -101,47 +94,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: + 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/shakespeare_1.0.3_0001-export-symbol-used-by-TH-splices.patch b/standalone/android/haskell-patches/shakespeare_1.0.3_0001-export-symbol-used-by-TH-splices.patch new file mode 100644 index 0000000000..aa30b255a5 --- /dev/null +++ b/standalone/android/haskell-patches/shakespeare_1.0.3_0001-export-symbol-used-by-TH-splices.patch @@ -0,0 +1,139 @@ +From 3cb1056782c29b0b68bdcff8fa49d3ea92126956 Mon Sep 17 00:00:00 2001 +From: Joey Hess +Date: Mon, 15 Apr 2013 16:46:15 -0400 +Subject: [PATCH] export symbol used by TH splices + +--- + Text/.Shakespeare.hs.swp | Bin 24576 -> 0 bytes + Text/Shakespeare.hs | 2 ++ + 2 files changed, 2 insertions(+) + delete mode 100644 Text/.Shakespeare.hs.swp + +diff --git a/Text/.Shakespeare.hs.swp b/Text/.Shakespeare.hs.swp +deleted file mode 100644 +index 4d6cd6a0295fdfb59f32a66b4af556c0630dd5b0..0000000000000000000000000000000000000000 +GIT binary patch +literal 0 +HcmV?d00001 + +literal 24576 +zcmeI4e~et$RmWd`KnqD)L_(BS5xTW4?M$;fu@g0M7u$*LtdmXsW9?l#wDxBEJo9Gf +z#WU|s-h1QO^^dk7RGJp46wwy`NCj0Y(S!!1AgEe}NKk}8ErJRNG@z(KqJ@T13Q|#9 +zR6ghact2*x>kWSanvuTVnRm}U_uO;OJ@?*o&-2-xr{<5SdmDFqe16RHu3haOfAZ_E +z_a3Fwww7 +z0}~BQG%(S?|0fM({p-CS(4lL=qu?5g>-pOOzWx0}{5=c)#QwgHzc+z9s33JFpNR%0 +z8klHcqJfD9CK{M%V4{JE1|}MqXkem&i3TPb_}{AmzvX$m5$_9fi0A%aVgN6{(es`I +zzX&dZcYzu3y*GH??}AT&_ks{CfP2BM;5zUML4hxWFM!X2XTa0oxI;GN(`@b5q3 +zdEW+q1)c*R0v`lD@F2Ja+zkHk^`6%R`@wf#=Xt*dj)L33^FQo)>);_U2fj-nMfm&Z@;M38Pg{=v0;4eAEh}Oh1S2h`)X| +zaMUe7^VK8erq$k&-{go$)yw+d5jmw@!>_`_lJ=8eE^Ye#V16}jZGhf}|gJeHq%I3sW^nO}B(&sBOwLMuVwp$B8=cC!v3~8z{d^Yb`{L(y$e%RNGLh +zAjzeZ#-zQ6{PbKv@6P+(K>$gF-lS_ukPf=ptg3Cl=wH?vSz7N0i$-HjKN78?4mw5; +zOwXx?8hL_1s6wHyZrIeiQE^+iN`qM^PJ>+3lor~9s3{7pl~RjV=*x;o-N7V6;={*;lR=J&F23q6JsMS|4#z5o|G5(pQD1n;kz|g;l(OBq%sbz3nkM^ph9XCf`ziHH63zL|5=(x`Mn +zie|KtTKTh}$2ewz>J3mMYOzdOnp9L#a8WHY5CQ66$6_DHg3T-nGbNrt&#s}ru6lkb +z-IGZY_?REM327$~zo2`jJM~D%8MJ5<9Wdo_Co1*Z*bC;I#D23gt@Z4;&imBGN+6XP +zsb?pa)=zw_xf#q#7j=VczBC0NJ;&&fxCMGwcpwjF=k?Vx@c +z^gSLsbekfjS5M^Ok +zH?374L@iuErXM8y2=x3&wR$SnL?fnCh0-rodOocRH)Fg?Oa~L?Y~R(#Rc*AYhUbaj +zJH#lS%-XwEyU(K0?)iPSbht5y`r?;%U?+Y{iiHf4Y86%?dA{JY7|iTb^T*thiY9vT +zdF>O*sg4J*ru&L!kDE3hKQV}?YT7D^lecwTmb-F8$G6r_-%qe!=~gm`7UW05uhYw( +zDS)YZFmMG~d`_MAcP%rnbY(FfB+cNc-wWi|X$qI+%N)xdOf;{#Bw>R1GU}J40Wk>E +zhFu)7@r2bxrYAG#wAq_1dl@T(;gHBGEmfMfKLwFyx_@g7Jtk+&o?vku7t?DjBylrH +zS-)lI><_&p$@IeQU{lTmv$Hi-B`LKrI#RCi@qynB+aZSh06V3IrakOmz+b1B$|h8r +zV9^m+@#c>;PDbJ*RBfRDE*S4Qf2{5(bu&leC=OedM|sPQ1B0;3yiqm#Wm>h9xF_Xx +zZ#z>eY`cnw-7;Xkdt>RL#^O4@Xst0X`;o}+rr!3jt=@8E{^-i7xf6@?$BwQzzq-;f +z3x4gc>D|*ia{;f+bdzRP4WBr-DUaiW7-Oj&ANXmgzth7;qn_8%3NRMKFo!)=Gb<-s +z1H| +z>!7QH1U~z>(gauxvJCZ@I>=JqYwIzwtyQ-C<$}BhN?`~!c} +zE{B-7?AFeS8}V5SGZd#He3NYVwAbM~&%z7LQMb8*_TfP{9Hb5J;>>n+Y+(t*UR-(b +zp@V9s9mO+4KZ$0-NEVnbm1p|Cu#Hl+edh8eHF{y1qL>*p+HDoYhxZ?S@Z|mn=hTUy +z87Hkrn4SmyWE{c4g@wF{yw;&^uokjAn`f6KXP+^Q@zg^k=xM;ft>&Uh%@+2oZaR8&sXp3ob|s5J`T=;IZy}Jf|sECb?^k}fd_!py%9`4 +z6Aer>Fwww70}~BQG%(S?L<18IOf)djz(fQ8M>L>HpA`x01v;3wgBy%^s9NfdJJhyW +zx$X#>62P5160U{OHhqY9H6NCUd(D)nUR{{9;KN`YoCT5>um`*e{2M`qFM*!{_kdf#8^BHACGPou +z1AGkp97w=%@HTKOxCPt{zQ$euv*118GB^YFfZM@$xa)rryZ}B0o&uM^Nw5TN0sqN8 +z|I6S>@KMkK?*O-gf8gH#x4_dN0Y|_bxB~K08cP4p&4sIOn+8zRrWWd)0Op?18#c8w +zQbi`SDO7v*X(n~$Lc(LX9p%iA>~EWHsD^mWxa&Bf&6~)(g3Ij7?e?hPu%W +zJjS+Lv?=YnPo3y17Q8zeZZYNm_RrW;~1!6FNlzJW^d@|vON+Tb$7Tv6&MeJ+{YH%2H#kA|~m6?9V* +zNmqo6SPQ#{<~p#%*M5~n-8SLqaRHiDA)Cn8G$OH(sv5V# +zOUWRR;k9gv_0{d`Ae+J@{4=}qZ2iCU$MW@ +z%$UUEnb}@YUQvepwwkt5j*(C^-E(Q589^;?{!42=|0UyNB+}B@M#q|qwlA~Os?c89 +zx)#JoCT=_s*N9rKoibj=jvCh7%$RyrqOG=Jyp&dq+|7_#l-e$FrMot}F6ObOX2thb +z3)ihO#i&M#cDN3R>DSg|dkddgb>Rxl*an4qZMO7def9#)kFRuk8Nuw{Y=Z!FKJNrG +z)qRIkG4vZM?HK3fRPH@xDyJ$*>nc^L*EC8$#5J(>2&`}n&6t7}wQZY`bz`L~k5b`h +zPFwMpJ+JJBb9YcPhY88ViidT@C3cyN7B*%PG{t{4Nf?#f0H+<1F>gxo;b>tlylUe8 +zr`6o!gTHn6&(I>#27oON%-$p>8UU5}?SMrNGBpQikuc +zCzsLY4*d}K9#!uauxa!EhACl)lf%Bv|=O3jF)t>(74Dk14nO7ChpvtIqvFA1I*D~isu9iZex;Z +zxu(_Fk%as}9J?%mK{O;uSaOjH_8XsMu}e;=5IRHPp)6RoRSa5w3D5lLMYlNSPxbT~ +zH}qo-g7Z>kv#s3c5*zZ7JYhXlG7a-gA-A8()0MQO##BZUpZAlox_+=L3986%XSy^t +zj_!a?8{U*|j#I{_1f;nn*%lgH3|M+4*+rH3$@!lny7#o4DLMb2<e +z&%v|cBj8bR8~6@q_s@Y}1-}B;z%rNyH-OJ@X8#57UJ!s5miiEDR>HGK=S|PeE(MPM(`qM +z`ric4fEDm|@HNi#zXU!4?ganAS^jUq3*ZCbeV_^M0Y3?@1z%+>WIR4CW0HTwgxqH< +zfv|-x`Kn_hNxDR_LtxP;PJdenY{}A=$FxepI$6?SN1mijH{?0m0=DV8ZAAy&2ZAdN}1mOL_@WPFL;5c`0h1d!~z6GieF@e{jxo?X|g +z+-i89-9%C?PCXzE{&yB=r44bw%#GYEx;c`?rN|#F}bITFChvVt>G%S&hp>mVe +zQE6dIbapZ_cQ0O+W(&EhGj+_^o8^)Q#1^P~YRPEg65o&;t?9pJFD*ZG&pnsyD2aDR +zkIf%FJb!eKvtjexl+INflWw|})pky+UAv~$U3CtQLb=y@7W-SwSHGGfc4{}VRa2=( +zGisZJGvoxwdCR=$!*}!UPfDh#vn5v0G|)%ug3wZx?kY5uK8{PMO)~#ZgsUCiKH9T~ +z{=$JWE41AJRU1Hu($TS1DYI1vSBX4~tgP>Vr`j^bQ{t&>jNO<7?7G{}L}L^GBEY)W==6B601}3#WEPUQX5)|f}sSMOQePggeLaXEY>HZdNVpwNi`+JfKOKT +zW0~=A+nMAHOJ816)pIJ;jMG|PctyQ!GSmbiv3Qr)u&ouEmum^pkkc@$ +zw#m&VrMmx{u&J%gPFTffSIcHAo>@*W5;6-uS6@+S&5%ay{F +cT45AdikYcun%oMsfwvWjb+Ig{u-NAPH%ay)TmS$7 + +diff --git a/Text/Shakespeare.hs b/Text/Shakespeare.hs +index d300951..fabbf66 100644 +--- a/Text/Shakespeare.hs ++++ b/Text/Shakespeare.hs +@@ -22,6 +22,8 @@ module Text.Shakespeare + #ifdef TEST_EXPORT + , preFilter + #endif ++ -- used by TH splices ++ , pack' + ) where + + import Data.List (intersperse) +-- +1.8.2.rc3 + 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 index 0000000000..5a5b8eeb82 --- /dev/null +++ b/standalone/android/haskell-patches/shakespeare_1.0.3_0001-remove-TH.patch @@ -0,0 +1,208 @@ +From 10484c5f68431349b249f07517c392c4a90bdb05 Mon Sep 17 00:00:00 2001 +From: Joey Hess +Date: Wed, 8 May 2013 01:47:19 -0400 +Subject: [PATCH] remove TH + +--- + Text/Shakespeare.hs | 109 ---------------------------------------------- + Text/Shakespeare/Base.hs | 28 ------------ + shakespeare.cabal | 2 +- + 3 files changed, 1 insertion(+), 138 deletions(-) + +diff --git a/Text/Shakespeare.hs b/Text/Shakespeare.hs +index 7750135..fabbf66 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 +@@ -135,39 +131,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 +@@ -302,54 +265,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)] +@@ -369,30 +284,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 +diff --git a/shakespeare.cabal b/shakespeare.cabal +index 01c8d5d..0fff966 100644 +--- a/shakespeare.cabal ++++ b/shakespeare.cabal +@@ -27,7 +27,7 @@ library + , template-haskell + , parsec >= 2 && < 4 + , text >= 0.7 && < 0.12 +- , process >= 1.0 && < 1.2 ++ , process >= 1.0 && < 1.3 + + exposed-modules: + Text.Shakespeare +-- +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 index 0000000000..5a343d8759 --- /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 +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/split_0.2.1.2_0001-modify-to-build-with-unreleased-ghc.patch b/standalone/android/haskell-patches/split_0.2.1.2_0001-modify-to-build-with-unreleased-ghc.patch new file mode 100644 index 0000000000..472ccd6785 --- /dev/null +++ b/standalone/android/haskell-patches/split_0.2.1.2_0001-modify-to-build-with-unreleased-ghc.patch @@ -0,0 +1,25 @@ +From 2feaef797641587a3da83753ee17d20e712c79cf Mon Sep 17 00:00:00 2001 +From: Joey Hess +Date: Thu, 28 Feb 2013 23:36:30 -0400 +Subject: [PATCH] modify to build with unreleased ghc + +--- + split.cabal | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/split.cabal b/split.cabal +index 2183c3e..29b9b32 100644 +--- a/split.cabal ++++ b/split.cabal +@@ -51,7 +51,7 @@ Source-repository head + + Library + ghc-options: -Wall +- build-depends: base <4.7 ++ build-depends: base <4.8 + exposed-modules: Data.List.Split, Data.List.Split.Internals + default-language: Haskell2010 + Hs-source-dirs: src +-- +1.7.10.4 + diff --git a/standalone/android/haskell-patches/syb_0.3.7_0001-hack-for-cross-compiling.patch b/standalone/android/haskell-patches/syb_0.3.7_0001-hack-for-cross-compiling.patch new file mode 100644 index 0000000000..e18d6127fe --- /dev/null +++ b/standalone/android/haskell-patches/syb_0.3.7_0001-hack-for-cross-compiling.patch @@ -0,0 +1,25 @@ +From c40fe2c484096c5de4cac8ca14a0ca5d892999f7 Mon Sep 17 00:00:00 2001 +From: Joey Hess +Date: Thu, 28 Feb 2013 23:36:43 -0400 +Subject: [PATCH] hack for cross-compiling + +--- + syb.cabal | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/syb.cabal b/syb.cabal +index 0aee93d..0a645c6 100644 +--- a/syb.cabal ++++ b/syb.cabal +@@ -17,7 +17,7 @@ description: + + category: Generics + stability: provisional +-build-type: Custom ++build-type: Simple + cabal-version: >= 1.6 + + extra-source-files: tests/*.hs, +-- +1.7.10.4 + diff --git a/standalone/android/haskell-patches/unix-time_0.1.4_0001-hacks-for-android.patch b/standalone/android/haskell-patches/unix-time_0.1.4_0001-hacks-for-android.patch new file mode 100644 index 0000000000..cff7e76e37 --- /dev/null +++ b/standalone/android/haskell-patches/unix-time_0.1.4_0001-hacks-for-android.patch @@ -0,0 +1,81 @@ +From 4023b952871ad2bc248db887716d06932ac0dbb9 Mon Sep 17 00:00:00 2001 +From: Joey Hess +Date: Wed, 8 May 2013 14:00:19 -0400 +Subject: [PATCH] hacks for android + +--- + cbits/conv.c | 4 +--- + unix-time.cabal | 28 ++-------------------------- + 2 files changed, 3 insertions(+), 29 deletions(-) + +diff --git a/cbits/conv.c b/cbits/conv.c +index 3b6a129..5a68f91 100644 +--- a/cbits/conv.c ++++ b/cbits/conv.c +@@ -1,5 +1,3 @@ +-#include "config.h" +- + #if IS_LINUX + /* Linux cheats AC_CHECK_FUNCS(strptime_l), sigh. */ + #define THREAD_SAFE 0 +@@ -51,7 +49,7 @@ time_t c_parse_unix_time_gmt(char *fmt, char *src) { + #else + strptime(src, fmt, &dst); + #endif +- return timegm(&dst); ++ return NULL; /* timegm(&dst); */ + } + + void c_format_unix_time(char *fmt, time_t src, char* dst, int siz) { +diff --git a/unix-time.cabal b/unix-time.cabal +index a905d63..f32d952 100644 +--- a/unix-time.cabal ++++ b/unix-time.cabal +@@ -8,7 +8,7 @@ Synopsis: Unix time parser/formatter and utilities + Description: Fast parser\/formatter\/utilities for Unix time + Category: Data + Cabal-Version: >= 1.10 +-Build-Type: Configure ++Build-Type: Simple + Extra-Source-Files: cbits/conv.c cbits/config.h.in configure configure.ac + Extra-Tmp-Files: config.log config.status autom4te.cache cbits/config.h + +@@ -21,34 +21,10 @@ Library + Data.UnixTime.Types + Data.UnixTime.Sys + Build-Depends: base >= 4 && < 5 +- , bytestring ++ , bytestring (>= 0.10.3.0) + , old-time + C-Sources: cbits/conv.c + +-Test-Suite doctests +- Type: exitcode-stdio-1.0 +- HS-Source-Dirs: test +- Ghc-Options: -threaded -Wall +- Main-Is: doctests.hs +- Build-Depends: base +- , doctest >= 0.9.3 +- +-Test-Suite spec +- Type: exitcode-stdio-1.0 +- Default-Language: Haskell2010 +- Hs-Source-Dirs: test +- Ghc-Options: -Wall +- Main-Is: Spec.hs +- Other-Modules: UnixTimeSpec +- Build-Depends: base +- , bytestring +- , hspec +- , old-locale +- , old-time +- , QuickCheck +- , time +- , unix-time +- + Source-Repository head + Type: git + Location: https://github.com/kazu-yamamoto/unix-time +-- +1.7.10.4 + diff --git a/standalone/android/haskell-patches/unix_2.6.0.1_0001-remove-stuff-not-available-on-Android.patch b/standalone/android/haskell-patches/unix_2.6.0.1_0001-remove-stuff-not-available-on-Android.patch new file mode 100644 index 0000000000..ff1da944cf --- /dev/null +++ b/standalone/android/haskell-patches/unix_2.6.0.1_0001-remove-stuff-not-available-on-Android.patch @@ -0,0 +1,91 @@ +From abca378462337ca0eb13a7e4d3073cb96a50d36c Mon Sep 17 00:00:00 2001 +From: Joey Hess +Date: Thu, 28 Feb 2013 23:37:23 -0400 +Subject: [PATCH] remove stuff not available on Android + +--- + System/Posix/Resource.hsc | 4 ++++ + System/Posix/Terminal/Common.hsc | 29 +++-------------------------- + 2 files changed, 7 insertions(+), 26 deletions(-) + +diff --git a/System/Posix/Resource.hsc b/System/Posix/Resource.hsc +index 6651998..2615b1e 100644 +--- a/System/Posix/Resource.hsc ++++ b/System/Posix/Resource.hsc +@@ -101,7 +101,9 @@ packResource ResourceTotalMemory = (#const RLIMIT_AS) + #endif + + unpackRLimit :: CRLim -> ResourceLimit ++#if 0 + unpackRLimit (#const RLIM_INFINITY) = ResourceLimitInfinity ++#endif + #ifdef RLIM_SAVED_MAX + unpackRLimit (#const RLIM_SAVED_MAX) = ResourceLimitUnknown + unpackRLimit (#const RLIM_SAVED_CUR) = ResourceLimitUnknown +@@ -109,7 +111,9 @@ unpackRLimit (#const RLIM_SAVED_CUR) = ResourceLimitUnknown + unpackRLimit other = ResourceLimit (fromIntegral other) + + packRLimit :: ResourceLimit -> Bool -> CRLim ++#if 0 + packRLimit ResourceLimitInfinity _ = (#const RLIM_INFINITY) ++#endif + #ifdef RLIM_SAVED_MAX + packRLimit ResourceLimitUnknown True = (#const RLIM_SAVED_CUR) + packRLimit ResourceLimitUnknown False = (#const RLIM_SAVED_MAX) +diff --git a/System/Posix/Terminal/Common.hsc b/System/Posix/Terminal/Common.hsc +index 3a6254d..32a22f2 100644 +--- a/System/Posix/Terminal/Common.hsc ++++ b/System/Posix/Terminal/Common.hsc +@@ -419,11 +419,7 @@ foreign import ccall unsafe "tcsendbreak" + -- | @drainOutput fd@ calls @tcdrain@ to block until all output + -- written to @Fd@ @fd@ has been transmitted. + drainOutput :: Fd -> IO () +-drainOutput (Fd fd) = throwErrnoIfMinus1_ "drainOutput" (c_tcdrain fd) +- +-foreign import ccall unsafe "tcdrain" +- c_tcdrain :: CInt -> IO CInt +- ++drainOutput (Fd fd) = error "drainOutput not implemented" + + data QueueSelector + = InputQueue -- TCIFLUSH +@@ -434,16 +430,7 @@ data QueueSelector + -- pending input and\/or output for @Fd@ @fd@, + -- as indicated by the @QueueSelector@ @queues@. + discardData :: Fd -> QueueSelector -> IO () +-discardData (Fd fd) queue = +- throwErrnoIfMinus1_ "discardData" (c_tcflush fd (queue2Int queue)) +- where +- queue2Int :: QueueSelector -> CInt +- queue2Int InputQueue = (#const TCIFLUSH) +- queue2Int OutputQueue = (#const TCOFLUSH) +- queue2Int BothQueues = (#const TCIOFLUSH) +- +-foreign import ccall unsafe "tcflush" +- c_tcflush :: CInt -> CInt -> IO CInt ++discardData (Fd fd) queue = error "discardData not implemented" + + data FlowAction + = SuspendOutput -- ^ TCOOFF +@@ -455,17 +442,7 @@ data FlowAction + -- flow of data on @Fd@ @fd@, as indicated by + -- @action@. + controlFlow :: Fd -> FlowAction -> IO () +-controlFlow (Fd fd) action = +- throwErrnoIfMinus1_ "controlFlow" (c_tcflow fd (action2Int action)) +- where +- action2Int :: FlowAction -> CInt +- action2Int SuspendOutput = (#const TCOOFF) +- action2Int RestartOutput = (#const TCOON) +- action2Int TransmitStop = (#const TCIOFF) +- action2Int TransmitStart = (#const TCION) +- +-foreign import ccall unsafe "tcflow" +- c_tcflow :: CInt -> CInt -> IO CInt ++controlFlow (Fd fd) action = error "controlFlow not implemented" + + -- | @getTerminalProcessGroupID fd@ calls @tcgetpgrp@ to + -- obtain the @ProcessGroupID@ of the foreground process group +-- +1.7.10.4 + diff --git a/standalone/android/haskell-patches/vector_0.10.0.1_0001-disable-optimisation-that-breaks-when-cross-compilin.patch b/standalone/android/haskell-patches/vector_0.10.0.1_0001-disable-optimisation-that-breaks-when-cross-compilin.patch new file mode 100644 index 0000000000..aa50d9c938 --- /dev/null +++ b/standalone/android/haskell-patches/vector_0.10.0.1_0001-disable-optimisation-that-breaks-when-cross-compilin.patch @@ -0,0 +1,25 @@ +From 3a4ee8091ba9da44f9f4a04522a5ff45fabe70d9 Mon Sep 17 00:00:00 2001 +From: Joey Hess +Date: Thu, 28 Feb 2013 23:37:56 -0400 +Subject: [PATCH] disable optimisation that breaks when cross-compiling + +This needs TH to work actually. +--- + Data/Vector/Fusion/Stream/Monadic.hs | 1 - + 1 file changed, 1 deletion(-) + +diff --git a/Data/Vector/Fusion/Stream/Monadic.hs b/Data/Vector/Fusion/Stream/Monadic.hs +index 51fec75..b089b3d 100644 +--- a/Data/Vector/Fusion/Stream/Monadic.hs ++++ b/Data/Vector/Fusion/Stream/Monadic.hs +@@ -101,7 +101,6 @@ import GHC.Exts ( SpecConstrAnnotation(..) ) + + data SPEC = SPEC | SPEC2 + #if __GLASGOW_HASKELL__ >= 700 +-{-# ANN type SPEC ForceSpecConstr #-} + #endif + + emptyStream :: String +-- +1.7.10.4 + diff --git a/standalone/android/haskell-patches/wai-app-static_1.3.1-remove-TH.patch b/standalone/android/haskell-patches/wai-app-static_1.3.1-remove-TH.patch new file mode 100644 index 0000000000..30bf5256a0 --- /dev/null +++ b/standalone/android/haskell-patches/wai-app-static_1.3.1-remove-TH.patch @@ -0,0 +1,36 @@ +From c18ae75852b1340ca502528138bf421659f61a3d Mon Sep 17 00:00:00 2001 +From: Joey Hess +Date: Mon, 15 Apr 2013 12:44:15 -0400 +Subject: [PATCH] remove TH + +Should not need these icons in git-annex, so not worth using the Evil +Splicer. +--- + Network/Wai/Application/Static.hs | 4 ---- + 1 file changed, 4 deletions(-) + +diff --git a/Network/Wai/Application/Static.hs b/Network/Wai/Application/Static.hs +index 3195fbb..b48aa01 100644 +--- a/Network/Wai/Application/Static.hs ++++ b/Network/Wai/Application/Static.hs +@@ -33,8 +33,6 @@ import Control.Monad.IO.Class (liftIO) + + import Blaze.ByteString.Builder (toByteString) + +-import Data.FileEmbed (embedFile) +- + import Data.Text (Text) + import qualified Data.Text as T + +@@ -198,8 +196,6 @@ staticAppPieces _ _ req + H.status405 + [("Content-Type", "text/plain")] + "Only GET is supported" +-staticAppPieces _ [".hidden", "folder.png"] _ = return $ W.responseLBS H.status200 [("Content-Type", "image/png")] $ L.fromChunks [$(embedFile "images/folder.png")] +-staticAppPieces _ [".hidden", "haskell.png"] _ = return $ W.responseLBS H.status200 [("Content-Type", "image/png")] $ L.fromChunks [$(embedFile "images/haskell.png")] + staticAppPieces ss rawPieces req = liftIO $ do + case toPieces rawPieces of + Just pieces -> checkPieces ss pieces req >>= response +-- +1.8.2.rc3 + diff --git a/standalone/android/haskell-patches/wai-extra_1.3.2.1_0001-disable-CGI-module.patch b/standalone/android/haskell-patches/wai-extra_1.3.2.1_0001-disable-CGI-module.patch new file mode 100644 index 0000000000..7d5d6e2ba2 --- /dev/null +++ b/standalone/android/haskell-patches/wai-extra_1.3.2.1_0001-disable-CGI-module.patch @@ -0,0 +1,26 @@ +From dc6d0128e666dcab07ddee56a22a4177ebfc0c7b Mon Sep 17 00:00:00 2001 +From: Joey Hess +Date: Thu, 28 Feb 2013 23:38:33 -0400 +Subject: [PATCH] disable CGI module + +I don't need it and it failed to build. +--- + wai-extra.cabal | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/wai-extra.cabal b/wai-extra.cabal +index 9e9f0fc..007dd0f 100644 +--- a/wai-extra.cabal ++++ b/wai-extra.cabal +@@ -44,7 +44,7 @@ Library + , void >= 0.5 && < 0.6 + , stringsearch >= 0.3 && < 0.4 + +- Exposed-modules: Network.Wai.Handler.CGI ++ Exposed-modules: + Network.Wai.Middleware.AcceptOverride + Network.Wai.Middleware.Autohead + Network.Wai.Middleware.CleanPath +-- +1.7.10.4 + diff --git a/standalone/android/haskell-patches/xml-hamlet_0.4.0.3-0001-remove-TH-code.patch b/standalone/android/haskell-patches/xml-hamlet_0.4.0.3-0001-remove-TH-code.patch new file mode 100644 index 0000000000..e6bda563df --- /dev/null +++ b/standalone/android/haskell-patches/xml-hamlet_0.4.0.3-0001-remove-TH-code.patch @@ -0,0 +1,108 @@ +From 3e988dec5ea248611d07d59914e3eb131dc6a165 Mon Sep 17 00:00:00 2001 +From: Joey Hess +Date: Thu, 18 Apr 2013 17:44:46 -0400 +Subject: [PATCH] remove TH code + +--- + Text/Hamlet/XML.hs | 81 +----------------------------------------------------- + 1 file changed, 1 insertion(+), 80 deletions(-) + +diff --git a/Text/Hamlet/XML.hs b/Text/Hamlet/XML.hs +index f587410..bf8ce9e 100644 +--- a/Text/Hamlet/XML.hs ++++ b/Text/Hamlet/XML.hs +@@ -1,8 +1,7 @@ + {-# LANGUAGE TemplateHaskell #-} + {-# OPTIONS_GHC -fno-warn-missing-fields #-} + module Text.Hamlet.XML +- ( xml +- , xmlFile ++ ( + ) where + + import Language.Haskell.TH.Syntax +@@ -18,81 +17,3 @@ import Data.String (fromString) + import qualified Data.Foldable as F + import Data.Maybe (fromMaybe) + import qualified Data.Map as Map +- +-xml :: QuasiQuoter +-xml = QuasiQuoter { quoteExp = strToExp } +- +-xmlFile :: FilePath -> Q Exp +-xmlFile = strToExp . TL.unpack <=< qRunIO . readUtf8File +- +-strToExp :: String -> Q Exp +-strToExp s = +- case parseDoc s of +- Error e -> error e +- Ok x -> docsToExp [] x +- +-docsToExp :: Scope -> [Doc] -> Q Exp +-docsToExp scope docs = [| concat $(fmap ListE $ mapM (docToExp scope) docs) |] +- +-docToExp :: Scope -> Doc -> Q Exp +-docToExp scope (DocTag name attrs cs) = +- [| [ X.NodeElement (X.Element ($(liftName name)) $(mkAttrs scope attrs) $(docsToExp scope cs)) +- ] |] +-docToExp _ (DocContent (ContentRaw s)) = [| [ X.NodeContent (pack $(lift s)) ] |] +-docToExp scope (DocContent (ContentVar d)) = [| [ X.NodeContent $(return $ derefToExp scope d) ] |] +-docToExp scope (DocContent (ContentEmbed d)) = return $ derefToExp scope d +-docToExp scope (DocForall deref ident@(Ident ident') inside) = do +- let list' = derefToExp scope deref +- name <- newName ident' +- let scope' = (ident, VarE name) : scope +- inside' <- docsToExp scope' inside +- let lam = LamE [VarP name] inside' +- [| F.concatMap $(return lam) $(return list') |] +-docToExp scope (DocWith [] inside) = docsToExp scope inside +-docToExp scope (DocWith ((deref, ident@(Ident name)):dis) inside) = do +- let deref' = derefToExp scope deref +- name' <- newName name +- let scope' = (ident, VarE name') : scope +- inside' <- docToExp scope' (DocWith dis inside) +- let lam = LamE [VarP name'] inside' +- return $ lam `AppE` deref' +-docToExp scope (DocMaybe deref ident@(Ident name) just nothing) = do +- let deref' = derefToExp scope deref +- name' <- newName name +- let scope' = (ident, VarE name') : scope +- inside' <- docsToExp scope' just +- let inside'' = LamE [VarP name'] inside' +- nothing' <- +- case nothing of +- Nothing -> [| [] |] +- Just n -> docsToExp scope n +- [| maybe $(return nothing') $(return inside'') $(return deref') |] +-docToExp scope (DocCond conds final) = do +- unit <- [| () |] +- body <- fmap GuardedB $ mapM go $ conds ++ [(DerefIdent $ Ident "otherwise", fromMaybe [] final)] +- return $ CaseE unit [Match (TupP []) body []] +- where +- go (deref, inside) = do +- inside' <- docsToExp scope inside +- return (NormalG $ derefToExp scope deref, inside') +- +-mkAttrs :: Scope -> [(Maybe Deref, String, [Content])] -> Q Exp +-mkAttrs _ [] = [| Map.empty |] +-mkAttrs scope ((mderef, name, value):rest) = do +- rest' <- mkAttrs scope rest +- this <- [| Map.insert $(liftName name) (T.concat $(fmap ListE $ mapM go value)) |] +- let with = [| $(return this) $(return rest') |] +- case mderef of +- Nothing -> with +- Just deref -> [| if $(return $ derefToExp scope deref) then $(with) else $(return rest') |] +- where +- go (ContentRaw s) = [| pack $(lift s) |] +- go (ContentVar d) = return $ derefToExp scope d +- go ContentEmbed{} = error "Cannot use embed interpolation in attribute value" +- +-liftName :: String -> Q Exp +-liftName s = do +- X.Name local mns _ <- return $ fromString s +- case mns of +- Nothing -> [| X.Name (pack $(lift $ unpack local)) Nothing Nothing |] +- Just ns -> [| X.Name (pack $(lift $ unpack local)) (Just $ pack $(lift $ unpack ns)) Nothing |] +-- +1.8.2.rc3 + 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 index 0000000000..fd641a1aa3 --- /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 +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 +- +- +- +- 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/haskell-patches/yesod-core_1.1.8_0002-replaced-TH-in-Yesod.Internal.Core.patch b/standalone/android/haskell-patches/yesod-core_1.1.8_0002-replaced-TH-in-Yesod.Internal.Core.patch new file mode 100644 index 0000000000..af0b3d15b6 --- /dev/null +++ b/standalone/android/haskell-patches/yesod-core_1.1.8_0002-replaced-TH-in-Yesod.Internal.Core.patch @@ -0,0 +1,267 @@ +From 9ae3db0b3292b53715232fecec3c5e2bf03b89cd Mon Sep 17 00:00:00 2001 +From: Joey Hess <joey@kitenet.net> +Date: Fri, 1 Mar 2013 01:02:53 -0400 +Subject: [PATCH 2/2] replaced TH in Yesod.Internal.Core + +Done by running a build with -ddump-splices and manually pasting in the +spliced code, and then modifying it until it compiles. + +(This predated the Evil Splicer, and both this and the previous patch need +to be redone to use it.) +--- + Yesod/Internal/Core.hs | 211 +++++++++++++++++++++++++++++++++++++++++++++--- + 1 file changed, 201 insertions(+), 10 deletions(-) + +diff --git a/Yesod/Internal/Core.hs b/Yesod/Internal/Core.hs +index 90c05fc..b9a0ae8 100644 +--- a/Yesod/Internal/Core.hs ++++ b/Yesod/Internal/Core.hs +@@ -96,6 +96,9 @@ import System.Log.FastLogger (Logger, mkLogger, loggerDate, LogStr (..), loggerP + import Control.Monad.Logger (LogLevel (LevelInfo, LevelOther), LogSource) + import System.Log.FastLogger.Date (ZonedDate) + import System.IO (stdout) ++import qualified Data.Foldable ++import qualified Text.Blaze.Internal ++import qualified Text.Hamlet + + yesodVersion :: String + yesodVersion = showVersion Paths_yesod_core.version +@@ -164,7 +167,28 @@ 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 = error "defaultLayout not implemented" ++ defaultLayout w = do ++ p <- widgetToPageContent w ++ mmsg <- getMessage ++ hamletToRepHtml $ \ _render_ay88 -> do { id ++ ((Text.Blaze.Internal.preEscapedText . T.pack) ++ "<!DOCTYPE html>\n<html><head><title>"); ++ id (TBH.toHtml (pageTitle p)); ++ id ((Text.Blaze.Internal.preEscapedText . T.pack) ""); ++ id (pageHead p) _render_ay88; ++ id ((Text.Blaze.Internal.preEscapedText . T.pack) ""); ++ Text.Hamlet.maybeH ++ mmsg ++ (\ msg_ay89 ++ -> do { id ++ ((Text.Blaze.Internal.preEscapedText . T.pack) ++ "

"); ++ id (TBH.toHtml msg_ay89); ++ id ((Text.Blaze.Internal.preEscapedText . T.pack) "

") }) ++ Nothing; ++ id (pageBody p) _render_ay88; ++ id ++ ((Text.Blaze.Internal.preEscapedText . T.pack) "") } + + -- | 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 +@@ -505,11 +529,45 @@ applyLayout' title body = fmap chooseRep $ defaultLayout $ do + + -- | The default error handler for 'errorHandler'. + defaultErrorHandler :: Yesod y => ErrorResponse -> GHandler sub y ChooseRep +-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" ++defaultErrorHandler NotFound = do ++ r <- waiRequest ++ let path' = TE.decodeUtf8With TEE.lenientDecode $ W.rawPathInfo r ++ applyLayout' "Not Found" $ \ _render_ayac -> do { id ++ ((Text.Blaze.Internal.preEscapedText . T.pack) ++ "

Not Found

"); ++ id (TBH.toHtml path'); ++ id ((Text.Blaze.Internal.preEscapedText . T.pack) "

") } ++defaultErrorHandler (PermissionDenied msg) = ++ applyLayout' "Permission Denied" $ \ _render_ayah -> do { id ++ ((Text.Blaze.Internal.preEscapedText . T.pack) ++ "

Permission denied

"); ++ id (TBH.toHtml msg); ++ id ((Text.Blaze.Internal.preEscapedText . T.pack) "

") } ++defaultErrorHandler (InvalidArgs ia) = ++ applyLayout' "Invalid Arguments" $ \ _render_ayam -> do { id ++ ((Text.Blaze.Internal.preEscapedText . T.pack) ++ "

Invalid Arguments

    "); ++ Data.Foldable.mapM_ ++ (\ msg_ayan ++ -> do { id ((Text.Blaze.Internal.preEscapedText . T.pack) "
  • "); ++ id (TBH.toHtml msg_ayan); ++ id ((Text.Blaze.Internal.preEscapedText . T.pack) "
  • ") }) ++ ia; ++ id ((Text.Blaze.Internal.preEscapedText . T.pack) "
") } ++defaultErrorHandler (InternalError e) = do ++ applyLayout' "Internal Server Error" $ \ _render_ayau -> do { id ++ ((Text.Blaze.Internal.preEscapedText . T.pack) ++ "

Internal Server Error

");
++              id (TBH.toHtml e);
++              id ((Text.Blaze.Internal.preEscapedText . T.pack) "
") } ++defaultErrorHandler (BadMethod m) = ++ applyLayout' "Bad Method" $ \ _render_ayaz -> do { id ++ ((Text.Blaze.Internal.preEscapedText . T.pack) ++ "

Method Not Supported

Method "); ++ id (TBH.toHtml (S8.unpack m)); ++ id ++ ((Text.Blaze.Internal.preEscapedText . T.pack) ++ " not supported

") } + + -- | Return the same URL if the user is authorized to see it. + -- +@@ -565,10 +623,99 @@ widgetToPageContent w = do + -- modernizr should be at the end of the 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 = error "TODO" +- +- headAll = error "TODO" +- let bodyScript = error "TODO" ++ regularScriptLoad = \ _render_aybs -> do { Data.Foldable.mapM_ ++ (\ s_aybt ++ -> id (mkScriptTag s_aybt) _render_aybs) ++ scripts; ++ Text.Hamlet.maybeH ++ jscript ++ (\ j_aybu ++ -> Text.Hamlet.maybeH ++ jsLoc ++ (\ s_aybv ++ -> do { id ++ ((Text.Blaze.Internal.preEscapedText . T.pack) ++ "") }) ++ (Just ++ (do { id ++ ((Text.Blaze.Internal.preEscapedText . T.pack) "") }))) ++ Nothing } ++ ++ headAll = \ _render_aybz -> do ++ { id head' _render_aybz; ++ Data.Foldable.mapM_ ++ (\ s_aybA -> id (mkLinkTag s_aybA) _render_aybz) ++ stylesheets; ++ Data.Foldable.mapM_ ++ (\ s_aybB ++ -> do { Text.Hamlet.maybeH ++ (right (snd s_aybB)) ++ (\ t_aybC ++ -> Text.Hamlet.maybeH ++ (fst s_aybB) ++ (\ media_aybD ++ -> do { id ++ ((Text.Blaze.Internal.preEscapedText . T.pack) ++ "") }) ++ (Just ++ (do { id ++ ((Text.Blaze.Internal.preEscapedText . T.pack) ++ "") }))) ++ Nothing; ++ Text.Hamlet.maybeH ++ (left (snd s_aybB)) ++ (\ content_aybE ++ -> Text.Hamlet.maybeH ++ (fst s_aybB) ++ (\ media_aybF ++ -> do { id ++ ((Text.Blaze.Internal.preEscapedText . T.pack) ++ "") }) ++ (Just ++ (do { id ++ ((Text.Blaze.Internal.preEscapedText . T.pack) ++ "") }))) ++ Nothing }) ++ css; ++ case jsLoader master of ++ BottomOfBody -> return () ++ BottomOfHeadAsync asyncJsLoader -> id (asyncJsLoader asyncScripts mcomplete) _render_aybz ++ BottomOfHeadBlocking -> id regularScriptLoad _render_aybz ++ } ++ ++ let bodyScript = \ _render_aybL -> do { ++ id body _render_aybL; ++ id regularScriptLoad _render_aybL } + + return $ PageContent title headAll (case jsLoader master of + BottomOfBody -> bodyScript +@@ -611,6 +758,50 @@ 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 = error "TODO" ++{- ++ \ _render_aybU ++ -> do { Text.Hamlet.maybeH ++ (left eyn) ++ (\ yn_aybV ++ -> do { id ++ ((Text.Blaze.Internal.preEscapedText . T.pack) "") }) ++ Nothing; ++ Text.Hamlet.maybeH ++ (right eyn) ++ (\ yn_aybW ++ -> do { id ++ ((Text.Blaze.Internal.preEscapedText . T.pack) "") }) ++ Nothing; ++ Text.Hamlet.maybeH ++ mcomplete ++ (\ complete_aybY ++ -> do { id ++ ((Text.Blaze.Internal.preEscapedText . T.pack) ++ "") }) ++ (Just ++ (do { id ++ ((Text.Blaze.Internal.preEscapedText . T.pack) ++ "") })) } ++-} + + asyncHelper :: (url -> [x] -> Text) + -> [Script (url)] +-- +1.7.10.4 + diff --git a/standalone/android/haskell-patches/yesod-core_1.1.8_0003-exports-for-TH-splices.patch b/standalone/android/haskell-patches/yesod-core_1.1.8_0003-exports-for-TH-splices.patch new file mode 100644 index 0000000000..440b57ac8b --- /dev/null +++ b/standalone/android/haskell-patches/yesod-core_1.1.8_0003-exports-for-TH-splices.patch @@ -0,0 +1,26 @@ +From b7e01a2fded6575678db234e1f2de1f104f11376 Mon Sep 17 00:00:00 2001 +From: Joey Hess +Date: Mon, 15 Apr 2013 15:25:07 -0400 +Subject: [PATCH 3/3] exports for TH splices + +--- + Yesod/Widget.hs | 3 +++ + 1 file changed, 3 insertions(+) + +diff --git a/Yesod/Widget.hs b/Yesod/Widget.hs +index bf79150..01ae294 100644 +--- a/Yesod/Widget.hs ++++ b/Yesod/Widget.hs +@@ -52,6 +52,9 @@ module Yesod.Widget + , addScriptEither + -- * Internal + , unGWidget ++ ++ -- used by TH code ++ , liftW + ) where + + import Data.Monoid +-- +1.8.2.rc3 + diff --git a/standalone/android/haskell-patches/yesod-default_1.1.3.2_0001-remove-TH.patch b/standalone/android/haskell-patches/yesod-default_1.1.3.2_0001-remove-TH.patch new file mode 100644 index 0000000000..e6048ee0a4 --- /dev/null +++ b/standalone/android/haskell-patches/yesod-default_1.1.3.2_0001-remove-TH.patch @@ -0,0 +1,102 @@ +From 8ff7908799eb69d440168ff3df1fe3187879df33 Mon Sep 17 00:00:00 2001 +From: Joey Hess +Date: Thu, 28 Feb 2013 23:39:57 -0400 +Subject: [PATCH] remove TH + +--- + Yesod/Default/Util.hs | 61 +------------------------------------------------ + 1 file changed, 1 insertion(+), 60 deletions(-) + +diff --git a/Yesod/Default/Util.hs b/Yesod/Default/Util.hs +index 578b9bc..178e342 100644 +--- a/Yesod/Default/Util.hs ++++ b/Yesod/Default/Util.hs +@@ -5,8 +5,6 @@ + module Yesod.Default.Util + ( addStaticContentExternal + , globFile +- , widgetFileNoReload +- , widgetFileReload + , TemplateLanguage (..) + , defaultTemplateLanguages + , WidgetFileSettings +@@ -21,9 +19,6 @@ import Yesod.Core -- purposely using complete import so that Haddock will see ad + import Control.Monad (when, unless) + import System.Directory (doesFileExist, createDirectoryIfMissing) + import Language.Haskell.TH.Syntax +-import Text.Lucius (luciusFile, luciusFileReload) +-import Text.Julius (juliusFile, juliusFileReload) +-import Text.Cassius (cassiusFile, cassiusFileReload) + import Text.Hamlet (HamletSettings, defaultHamletSettings) + import Data.Maybe (catMaybes) + import Data.Default (Default (def)) +@@ -72,13 +67,7 @@ data TemplateLanguage = TemplateLanguage + + defaultTemplateLanguages :: HamletSettings -> [TemplateLanguage] + defaultTemplateLanguages hset = +- [ TemplateLanguage False "hamlet" whamletFile' whamletFile' +- , TemplateLanguage True "cassius" cassiusFile cassiusFileReload +- , TemplateLanguage True "julius" juliusFile juliusFileReload +- , TemplateLanguage True "lucius" luciusFile luciusFileReload +- ] +- where +- whamletFile' = whamletFileWithSettings hset ++ [ ] + + data WidgetFileSettings = WidgetFileSettings + { wfsLanguages :: HamletSettings -> [TemplateLanguage] +@@ -87,51 +76,3 @@ data WidgetFileSettings = WidgetFileSettings + + instance Default WidgetFileSettings where + def = WidgetFileSettings defaultTemplateLanguages defaultHamletSettings +- +-widgetFileNoReload :: WidgetFileSettings -> FilePath -> Q Exp +-widgetFileNoReload wfs x = combine "widgetFileNoReload" x False $ wfsLanguages wfs $ wfsHamletSettings wfs +- +-widgetFileReload :: WidgetFileSettings -> FilePath -> Q Exp +-widgetFileReload wfs x = combine "widgetFileReload" x True $ wfsLanguages wfs $ wfsHamletSettings wfs +- +-combine :: String -> String -> Bool -> [TemplateLanguage] -> Q Exp +-combine func file isReload tls = do +- mexps <- qmexps +- case catMaybes mexps of +- [] -> error $ concat +- [ "Called " +- , func +- , " on " +- , show file +- , ", but no template were found." +- ] +- exps -> return $ DoE $ map NoBindS exps +- where +- qmexps :: Q [Maybe Exp] +- qmexps = mapM go tls +- +- go :: TemplateLanguage -> Q (Maybe Exp) +- go tl = whenExists file (tlRequiresToWidget tl) (tlExtension tl) ((if isReload then tlReload else tlNoReload) tl) +- +-whenExists :: String +- -> Bool -- ^ requires toWidget wrap +- -> String -> (FilePath -> Q Exp) -> Q (Maybe Exp) +-whenExists = warnUnlessExists False +- +-warnUnlessExists :: Bool +- -> String +- -> Bool -- ^ requires toWidget wrap +- -> String -> (FilePath -> Q Exp) -> Q (Maybe Exp) +-warnUnlessExists shouldWarn x wrap glob f = do +- let fn = globFile glob x +- e <- qRunIO $ doesFileExist fn +- when (shouldWarn && not e) $ qRunIO $ putStrLn $ "widget file not found: " ++ fn +- if e +- then do +- ex <- f fn +- if wrap +- then do +- tw <- [|toWidget|] +- return $ Just $ tw `AppE` ex +- else return $ Just ex +- else return Nothing +-- +1.7.10.4 + diff --git a/standalone/android/haskell-patches/yesod-form_1.2.1.1-0001-prepare-for-Evil-Splicer.patch b/standalone/android/haskell-patches/yesod-form_1.2.1.1-0001-prepare-for-Evil-Splicer.patch new file mode 100644 index 0000000000..c24055b1f0 --- /dev/null +++ b/standalone/android/haskell-patches/yesod-form_1.2.1.1-0001-prepare-for-Evil-Splicer.patch @@ -0,0 +1,83 @@ +From a603bac40f0a0f6232fbfb056a778860270101de Mon Sep 17 00:00:00 2001 +From: Joey Hess +Date: Mon, 15 Apr 2013 15:59:56 -0400 +Subject: [PATCH 1/2] prepare for Evil Splicer + +--- + Yesod/Form/Functions.hs | 3 +-- + evilsplicer-headers.hs | 9 +++++++++ + yesod-form.cabal | 5 +++-- + 3 files changed, 13 insertions(+), 4 deletions(-) + create mode 100644 evilsplicer-headers.hs + +diff --git a/Yesod/Form/Functions.hs b/Yesod/Form/Functions.hs +index db3e493..89eb1e8 100644 +--- a/Yesod/Form/Functions.hs ++++ b/Yesod/Form/Functions.hs +@@ -54,10 +54,9 @@ import Text.Blaze (Markup, toMarkup) + #define toHtml toMarkup + import Yesod.Handler (GHandler, getRequest, runRequestBody, newIdent, getYesod) + import Yesod.Core (RenderMessage, SomeMessage (..)) +-import Yesod.Widget (GWidget, whamlet) ++import Yesod.Widget (GWidget) + import Yesod.Request (reqToken, reqWaiRequest, reqGetParams, languages) + import Network.Wai (requestMethod) +-import Text.Hamlet (shamlet) + import Data.Monoid (mempty) + import Data.Maybe (listToMaybe, fromMaybe) + import Yesod.Message (RenderMessage (..)) +diff --git a/evilsplicer-headers.hs b/evilsplicer-headers.hs +new file mode 100644 +index 0000000..865d043 +--- /dev/null ++++ b/evilsplicer-headers.hs +@@ -0,0 +1,9 @@ ++import qualified Data.Text.Lazy.Builder ++import qualified Text.Shakespeare ++import qualified Text.Hamlet ++import qualified Data.Monoid ++import qualified Text.Julius ++import qualified "blaze-markup" Text.Blaze.Internal ++import qualified "blaze-markup" Text.Blaze as Text.Blaze.Markup ++import qualified Yesod.Widget ++import qualified Data.Foldable +diff --git a/yesod-form.cabal b/yesod-form.cabal +index a0d2a80..ae99ddc 100644 +--- a/yesod-form.cabal ++++ b/yesod-form.cabal +@@ -18,7 +18,7 @@ library + , yesod-persistent >= 1.1 && < 1.2 + , time >= 1.1.4 + , hamlet >= 1.1 && < 1.2 +- , shakespeare-css >= 1.0 && < 1.1 ++ , shakespeare-css == 1.0.2 + , shakespeare-js >= 1.0.2 && < 1.2 + , persistent >= 1.0 && < 1.2 + , template-haskell +@@ -37,6 +37,7 @@ library + , attoparsec >= 0.10 && < 0.11 + , crypto-api >= 0.8 && < 0.11 + , aeson ++ , shakespeare + + exposed-modules: Yesod.Form + Yesod.Form.Class +@@ -45,7 +46,6 @@ library + Yesod.Form.Input + Yesod.Form.Fields + Yesod.Form.Jquery +- Yesod.Form.Nic + Yesod.Form.MassInput + Yesod.Form.I18n.English + Yesod.Form.I18n.Portuguese +@@ -56,6 +56,7 @@ library + Yesod.Form.I18n.Japanese + -- FIXME Yesod.Helpers.Crud + ghc-options: -Wall ++ Extensions: PackageImports + + test-suite test + type: exitcode-stdio-1.0 +-- +1.8.2.rc3 + diff --git a/standalone/android/haskell-patches/yesod-form_1.2.1.1-0002-expand-TH.patch b/standalone/android/haskell-patches/yesod-form_1.2.1.1-0002-expand-TH.patch new file mode 100644 index 0000000000..3ce48e5fcb --- /dev/null +++ b/standalone/android/haskell-patches/yesod-form_1.2.1.1-0002-expand-TH.patch @@ -0,0 +1,1606 @@ +From f98c22ec71695537e0e008a0bd54affdf8a60f64 Mon Sep 17 00:00:00 2001 +From: Joey Hess +Date: Mon, 15 Apr 2013 17:35:57 -0400 +Subject: [PATCH 2/2] expand TH + +Used the EvilSplicer, and then some manual fixups, as it is apparently +buggy. Also a few module import fixes. +--- + Yesod/Form/Fields.hs | 623 ++++++++++++++++++++++++++++++++++++++---------- + Yesod/Form/Functions.hs | 240 +++++++++++++++---- + Yesod/Form/Jquery.hs | 141 ++++++++--- + Yesod/Form/MassInput.hs | 228 ++++++++++++++---- + Yesod/Form/Nic.hs | 59 ++++- + 5 files changed, 1042 insertions(+), 249 deletions(-) + +diff --git a/Yesod/Form/Fields.hs b/Yesod/Form/Fields.hs +index 7917ce2..db76ea2 100644 +--- a/Yesod/Form/Fields.hs ++++ b/Yesod/Form/Fields.hs +@@ -46,11 +46,22 @@ module Yesod.Form.Fields + , optionsEnum + ) where + ++import qualified Data.Text.Lazy.Builder ++import qualified Text.Shakespeare ++import qualified Data.Monoid ++import qualified Text.Julius ++import qualified "blaze-markup" Text.Blaze.Internal ++import qualified "blaze-markup" Text.Blaze as Text.Blaze.Internal ++import qualified "blaze-html" Text.Blaze.Html ++import qualified Yesod.Widget ++import qualified Text.Css ++import qualified Control.Monad ++import qualified Data.Foldable + import Yesod.Form.Types + import Yesod.Form.I18n.English + import Yesod.Form.Functions (parseHelper) + import Yesod.Handler (getMessageRender) +-import Yesod.Widget (toWidget, whamlet, GWidget) ++import Yesod.Widget (toWidget, GWidget) + import Yesod.Message (RenderMessage (renderMessage), SomeMessage (..)) + import Text.Hamlet + import Text.Blaze (ToMarkup (toMarkup), preEscapedToMarkup, unsafeByteString) +@@ -108,10 +119,24 @@ intField = Field + Right (a, "") -> Right a + _ -> Left $ MsgInvalidInteger s + +- , fieldView = \theId name attrs val isReq -> toWidget [hamlet| +-$newline never +- +-|] ++ , fieldView = \theId name attrs val isReq -> toWidget $ \ _render_amMY ++ -> do { id ++ ((Text.Blaze.Internal.preEscapedText . pack) "") } ++ + , fieldEnctype = UrlEncoded + } + where +@@ -125,10 +150,24 @@ doubleField = Field + Right (a, "") -> Right a + _ -> Left $ MsgInvalidNumber s + +- , fieldView = \theId name attrs val isReq -> toWidget [hamlet| +-$newline never +- +-|] ++ , fieldView = \theId name attrs val isReq -> toWidget $ \ _render_amNa ++ -> do { id ++ ((Text.Blaze.Internal.preEscapedText . pack) "") } ++ + , fieldEnctype = UrlEncoded + } + where showVal = either id (pack . show) +@@ -136,10 +175,24 @@ $newline never + dayField :: RenderMessage master FormMessage => Field sub master Day + dayField = Field + { fieldParse = parseHelper $ parseDate . unpack +- , fieldView = \theId name attrs val isReq -> toWidget [hamlet| +-$newline never +- +-|] ++ , fieldView = \theId name attrs val isReq -> toWidget $ \ _render_amNk ++ -> do { id ++ ((Text.Blaze.Internal.preEscapedText . pack) "") } ++ + , fieldEnctype = UrlEncoded + } + where showVal = either id (pack . show) +@@ -147,10 +200,23 @@ $newline never + timeField :: RenderMessage master FormMessage => Field sub master TimeOfDay + timeField = Field + { fieldParse = parseHelper parseTime +- , fieldView = \theId name attrs val isReq -> toWidget [hamlet| +-$newline never +- +-|] ++ , fieldView = \theId name attrs val isReq -> toWidget $ \ _render_amNx ++ -> do { id ++ ((Text.Blaze.Internal.preEscapedText . pack) "") } ++ + , fieldEnctype = UrlEncoded + } + where +@@ -163,10 +229,18 @@ $newline never + htmlField :: RenderMessage master FormMessage => Field sub master Html + htmlField = Field + { fieldParse = parseHelper $ Right . preEscapedText . sanitizeBalance +- , fieldView = \theId name attrs val _isReq -> toWidget [hamlet| +-$newline never +-") } ++ + , fieldEnctype = UrlEncoded + } + where showVal = either id (pack . renderHtml) +@@ -192,10 +266,18 @@ instance ToHtml Textarea where + textareaField :: RenderMessage master FormMessage => Field sub master Textarea + textareaField = Field + { fieldParse = parseHelper $ Right . Textarea +- , fieldView = \theId name attrs val _isReq -> toWidget [hamlet| +-$newline never +-") } ++ + , fieldEnctype = UrlEncoded + } + +@@ -203,10 +285,19 @@ hiddenField :: (PathPiece p, RenderMessage master FormMessage) + => Field sub master p + hiddenField = Field + { fieldParse = parseHelper $ maybe (Left MsgValueRequired) Right . fromPathPiece +- , fieldView = \theId name attrs val _isReq -> toWidget [hamlet| +-$newline never +- +-|] ++ , fieldView = \theId name attrs val _isReq -> toWidget $ \ _render_amNZ ++ -> do { id ++ ((Text.Blaze.Internal.preEscapedText . pack) ++ "") } ++ + , fieldEnctype = UrlEncoded + } + +@@ -214,20 +305,50 @@ textField :: RenderMessage master FormMessage => Field sub master Text + textField = Field + { fieldParse = parseHelper $ Right + , fieldView = \theId name attrs val isReq -> +- [whamlet| +-$newline never +- +-|] ++ do { toWidget ++ ((Text.Blaze.Internal.preEscapedText . pack) "") } ++ + , fieldEnctype = UrlEncoded + } + + passwordField :: RenderMessage master FormMessage => Field sub master Text + passwordField = Field + { fieldParse = parseHelper $ Right +- , fieldView = \theId name attrs val isReq -> toWidget [hamlet| +-$newline never +- +-|] ++ , fieldView = \theId name attrs val isReq -> toWidget $ \ _render_amOg ++ -> do { id ++ ((Text.Blaze.Internal.preEscapedText . pack) "") } ++ + , fieldEnctype = UrlEncoded + } + +@@ -305,10 +426,24 @@ emailField = Field + then Right s + else Left $ MsgInvalidEmail s + #endif +- , fieldView = \theId name attrs val isReq -> toWidget [hamlet| +-$newline never +- +-|] ++ , fieldView = \theId name attrs val isReq -> toWidget $ \ _render_amOO ++ -> do { id ++ ((Text.Blaze.Internal.preEscapedText . pack) "") } ++ + , fieldEnctype = UrlEncoded + } + +@@ -317,20 +452,60 @@ searchField :: RenderMessage master FormMessage => AutoFocus -> Field sub master + searchField autoFocus = Field + { fieldParse = parseHelper Right + , fieldView = \theId name attrs val isReq -> do +- [whamlet|\ +-$newline never +- +-|] ++ do { toWidget ++ ((Text.Blaze.Internal.preEscapedText . pack) "") } ++ + when autoFocus $ do + -- we want this javascript to be placed immediately after the field +- [whamlet| +-$newline never +-") } ++ ++ toWidget $ \ _render_amP5 ++ -> (Text.Css.CssNoWhitespace ++ . (foldr ($) [])) ++ [((++) ++ $ (map ++ Text.Css.Css ++ ((((:) ++ (Text.Css.Css' ++ (Data.Monoid.mconcat [toCss theId]) ++ [(Data.Monoid.mconcat ++ [(Text.Css.fromText ++ . Text.Css.pack) ++ "-webkit-appearance"], ++ Data.Monoid.mconcat ++ [(Text.Css.fromText ++ . Text.Css.pack) ++ "textfield"])])) ++ . (foldr (.) id [])) ++ [])))] ++ + , fieldEnctype = UrlEncoded + } + +@@ -341,10 +516,25 @@ urlField = Field + Nothing -> Left $ MsgInvalidUrl s + Just _ -> Right s + , fieldView = \theId name attrs val isReq -> +- [whamlet| +-$newline never +- +-|] ++ do { toWidget ++ ((Text.Blaze.Internal.preEscapedText . pack) "") } ++ + , fieldEnctype = UrlEncoded + } + +@@ -353,18 +543,48 @@ selectFieldList = selectField . optionsPairs + + selectField :: (Eq a, RenderMessage master FormMessage) => GHandler sub master (OptionList a) -> Field sub master a + selectField = selectFieldHelper +- (\theId name attrs inside -> [whamlet| +-$newline never +-"); ++ toWidget inside; ++ toWidget ++ ((Text.Blaze.Internal.preEscapedText . pack) "") }) ++ -- outside ++ (\_theId _name isSel -> do { toWidget ++ ((Text.Blaze.Internal.preEscapedText . pack) ++ "") }) ++ -- onOpt ++ (\_theId _name _attrs value isSel text -> do { toWidget ++ ((Text.Blaze.Internal.preEscapedText . pack) "") }) ++ -- inside + + multiSelectFieldList :: (Eq a, RenderMessage master FormMessage, RenderMessage master msg) => [(msg, a)] -> Field sub master [a] + multiSelectFieldList = multiSelectField . optionsPairs +@@ -385,12 +605,40 @@ multiSelectField ioptlist = + view theId name attrs val isReq = do + opts <- fmap olOptions $ lift ioptlist + let selOpts = map (id &&& (optselected val)) opts +- [whamlet| +-$newline never +- "); ++ Data.Foldable.mapM_ ++ (\ (opt_amPV, optsel_amPW) ++ -> do { toWidget ++ ((Text.Blaze.Internal.preEscapedText . pack) "") }) ++ selOpts; ++ toWidget ++ ((Text.Blaze.Internal.preEscapedText . pack) "") } ++ + where + optselected (Left _) _ = False + optselected (Right vals) opt = (optionInternalValue opt) `elem` vals +@@ -400,41 +648,140 @@ radioFieldList = radioField . optionsPairs + + radioField :: (Eq a, RenderMessage master FormMessage) => GHandler sub master (OptionList a) -> Field sub master a + radioField = selectFieldHelper +- (\theId _name _attrs inside -> [whamlet| +-$newline never +-
^{inside} +-|]) +- (\theId name isSel -> [whamlet| +-$newline never +-