amp 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (295) hide show
  1. data/.gitignore +1 -0
  2. data/.hgignore +26 -0
  3. data/AUTHORS +2 -0
  4. data/History.txt +6 -0
  5. data/LICENSE +37 -0
  6. data/MANIFESTO +7 -0
  7. data/Manifest.txt +294 -0
  8. data/README.md +129 -0
  9. data/Rakefile +102 -0
  10. data/SCHEDULE.markdown +12 -0
  11. data/STYLE +27 -0
  12. data/TODO.markdown +149 -0
  13. data/ampfile.rb +47 -0
  14. data/bin/amp +30 -0
  15. data/bin/amp1.9 +30 -0
  16. data/ext/amp/bz2/README.txt +39 -0
  17. data/ext/amp/bz2/bz2.c +1582 -0
  18. data/ext/amp/bz2/extconf.rb +77 -0
  19. data/ext/amp/bz2/mkmf.log +29 -0
  20. data/ext/amp/mercurial_patch/extconf.rb +5 -0
  21. data/ext/amp/mercurial_patch/mpatch.c +405 -0
  22. data/ext/amp/priority_queue/extconf.rb +5 -0
  23. data/ext/amp/priority_queue/priority_queue.c +947 -0
  24. data/ext/amp/support/extconf.rb +5 -0
  25. data/ext/amp/support/support.c +250 -0
  26. data/lib/amp.rb +200 -0
  27. data/lib/amp/commands/command.rb +507 -0
  28. data/lib/amp/commands/command_support.rb +137 -0
  29. data/lib/amp/commands/commands/config.rb +143 -0
  30. data/lib/amp/commands/commands/help.rb +29 -0
  31. data/lib/amp/commands/commands/init.rb +10 -0
  32. data/lib/amp/commands/commands/templates.rb +137 -0
  33. data/lib/amp/commands/commands/version.rb +7 -0
  34. data/lib/amp/commands/commands/workflow.rb +28 -0
  35. data/lib/amp/commands/commands/workflows/git/add.rb +65 -0
  36. data/lib/amp/commands/commands/workflows/git/copy.rb +27 -0
  37. data/lib/amp/commands/commands/workflows/git/mv.rb +23 -0
  38. data/lib/amp/commands/commands/workflows/git/rm.rb +60 -0
  39. data/lib/amp/commands/commands/workflows/hg/add.rb +53 -0
  40. data/lib/amp/commands/commands/workflows/hg/addremove.rb +86 -0
  41. data/lib/amp/commands/commands/workflows/hg/annotate.rb +46 -0
  42. data/lib/amp/commands/commands/workflows/hg/archive.rb +126 -0
  43. data/lib/amp/commands/commands/workflows/hg/branch.rb +28 -0
  44. data/lib/amp/commands/commands/workflows/hg/branches.rb +30 -0
  45. data/lib/amp/commands/commands/workflows/hg/bundle.rb +115 -0
  46. data/lib/amp/commands/commands/workflows/hg/clone.rb +95 -0
  47. data/lib/amp/commands/commands/workflows/hg/commit.rb +42 -0
  48. data/lib/amp/commands/commands/workflows/hg/copy.rb +31 -0
  49. data/lib/amp/commands/commands/workflows/hg/debug/dirstate.rb +32 -0
  50. data/lib/amp/commands/commands/workflows/hg/debug/index.rb +36 -0
  51. data/lib/amp/commands/commands/workflows/hg/default.rb +9 -0
  52. data/lib/amp/commands/commands/workflows/hg/diff.rb +30 -0
  53. data/lib/amp/commands/commands/workflows/hg/forget.rb +11 -0
  54. data/lib/amp/commands/commands/workflows/hg/heads.rb +25 -0
  55. data/lib/amp/commands/commands/workflows/hg/identify.rb +23 -0
  56. data/lib/amp/commands/commands/workflows/hg/import.rb +135 -0
  57. data/lib/amp/commands/commands/workflows/hg/incoming.rb +85 -0
  58. data/lib/amp/commands/commands/workflows/hg/info.rb +18 -0
  59. data/lib/amp/commands/commands/workflows/hg/log.rb +21 -0
  60. data/lib/amp/commands/commands/workflows/hg/manifest.rb +13 -0
  61. data/lib/amp/commands/commands/workflows/hg/merge.rb +53 -0
  62. data/lib/amp/commands/commands/workflows/hg/move.rb +28 -0
  63. data/lib/amp/commands/commands/workflows/hg/outgoing.rb +61 -0
  64. data/lib/amp/commands/commands/workflows/hg/pull.rb +74 -0
  65. data/lib/amp/commands/commands/workflows/hg/push.rb +20 -0
  66. data/lib/amp/commands/commands/workflows/hg/remove.rb +45 -0
  67. data/lib/amp/commands/commands/workflows/hg/resolve.rb +83 -0
  68. data/lib/amp/commands/commands/workflows/hg/revert.rb +53 -0
  69. data/lib/amp/commands/commands/workflows/hg/root.rb +13 -0
  70. data/lib/amp/commands/commands/workflows/hg/serve.rb +38 -0
  71. data/lib/amp/commands/commands/workflows/hg/status.rb +116 -0
  72. data/lib/amp/commands/commands/workflows/hg/tag.rb +69 -0
  73. data/lib/amp/commands/commands/workflows/hg/tags.rb +27 -0
  74. data/lib/amp/commands/commands/workflows/hg/tip.rb +13 -0
  75. data/lib/amp/commands/commands/workflows/hg/update.rb +27 -0
  76. data/lib/amp/commands/commands/workflows/hg/verify.rb +9 -0
  77. data/lib/amp/commands/commands/workflows/hg/view.rb +36 -0
  78. data/lib/amp/commands/dispatch.rb +181 -0
  79. data/lib/amp/commands/hooks.rb +81 -0
  80. data/lib/amp/dependencies/amp_support.rb +1 -0
  81. data/lib/amp/dependencies/amp_support/ruby_amp_support.rb +103 -0
  82. data/lib/amp/dependencies/minitar.rb +979 -0
  83. data/lib/amp/dependencies/priority_queue.rb +18 -0
  84. data/lib/amp/dependencies/priority_queue/c_priority_queue.rb +1 -0
  85. data/lib/amp/dependencies/priority_queue/poor_priority_queue.rb +46 -0
  86. data/lib/amp/dependencies/priority_queue/ruby_priority_queue.rb +525 -0
  87. data/lib/amp/dependencies/python_config.rb +211 -0
  88. data/lib/amp/dependencies/trollop.rb +713 -0
  89. data/lib/amp/dependencies/zip/ioextras.rb +155 -0
  90. data/lib/amp/dependencies/zip/stdrubyext.rb +111 -0
  91. data/lib/amp/dependencies/zip/tempfile_bugfixed.rb +186 -0
  92. data/lib/amp/dependencies/zip/zip.rb +1850 -0
  93. data/lib/amp/dependencies/zip/zipfilesystem.rb +609 -0
  94. data/lib/amp/dependencies/zip/ziprequire.rb +90 -0
  95. data/lib/amp/encoding/base85.rb +97 -0
  96. data/lib/amp/encoding/binary_diff.rb +82 -0
  97. data/lib/amp/encoding/difflib.rb +166 -0
  98. data/lib/amp/encoding/mercurial_diff.rb +378 -0
  99. data/lib/amp/encoding/mercurial_patch.rb +1 -0
  100. data/lib/amp/encoding/patch.rb +292 -0
  101. data/lib/amp/encoding/pure_ruby/ruby_mercurial_patch.rb +123 -0
  102. data/lib/amp/extensions/ditz.rb +41 -0
  103. data/lib/amp/extensions/lighthouse.rb +167 -0
  104. data/lib/amp/graphs/ancestor.rb +147 -0
  105. data/lib/amp/graphs/copies.rb +261 -0
  106. data/lib/amp/merges/merge_state.rb +164 -0
  107. data/lib/amp/merges/merge_ui.rb +322 -0
  108. data/lib/amp/merges/simple_merge.rb +450 -0
  109. data/lib/amp/profiling_hacks.rb +36 -0
  110. data/lib/amp/repository/branch_manager.rb +234 -0
  111. data/lib/amp/repository/dir_state.rb +950 -0
  112. data/lib/amp/repository/journal.rb +203 -0
  113. data/lib/amp/repository/lock.rb +207 -0
  114. data/lib/amp/repository/repositories/bundle_repository.rb +214 -0
  115. data/lib/amp/repository/repositories/http_repository.rb +377 -0
  116. data/lib/amp/repository/repositories/local_repository.rb +2661 -0
  117. data/lib/amp/repository/repository.rb +94 -0
  118. data/lib/amp/repository/store.rb +485 -0
  119. data/lib/amp/repository/tag_manager.rb +319 -0
  120. data/lib/amp/repository/updatable.rb +532 -0
  121. data/lib/amp/repository/verification.rb +431 -0
  122. data/lib/amp/repository/versioned_file.rb +475 -0
  123. data/lib/amp/revlogs/bundle_revlogs.rb +246 -0
  124. data/lib/amp/revlogs/changegroup.rb +217 -0
  125. data/lib/amp/revlogs/changelog.rb +338 -0
  126. data/lib/amp/revlogs/changeset.rb +521 -0
  127. data/lib/amp/revlogs/file_log.rb +165 -0
  128. data/lib/amp/revlogs/index.rb +493 -0
  129. data/lib/amp/revlogs/manifest.rb +195 -0
  130. data/lib/amp/revlogs/node.rb +18 -0
  131. data/lib/amp/revlogs/revlog.rb +1032 -0
  132. data/lib/amp/revlogs/revlog_support.rb +126 -0
  133. data/lib/amp/server/amp_user.rb +44 -0
  134. data/lib/amp/server/extension/amp_extension.rb +396 -0
  135. data/lib/amp/server/extension/authorization.rb +201 -0
  136. data/lib/amp/server/fancy_http_server.rb +252 -0
  137. data/lib/amp/server/fancy_views/_browser.haml +28 -0
  138. data/lib/amp/server/fancy_views/_diff_file.haml +13 -0
  139. data/lib/amp/server/fancy_views/_navbar.haml +17 -0
  140. data/lib/amp/server/fancy_views/changeset.haml +31 -0
  141. data/lib/amp/server/fancy_views/commits.haml +32 -0
  142. data/lib/amp/server/fancy_views/file.haml +35 -0
  143. data/lib/amp/server/fancy_views/file_diff.haml +23 -0
  144. data/lib/amp/server/fancy_views/harshcss/all_hallows_eve.css +72 -0
  145. data/lib/amp/server/fancy_views/harshcss/amy.css +147 -0
  146. data/lib/amp/server/fancy_views/harshcss/twilight.css +138 -0
  147. data/lib/amp/server/fancy_views/stylesheet.sass +175 -0
  148. data/lib/amp/server/http_server.rb +140 -0
  149. data/lib/amp/server/repo_user_management.rb +287 -0
  150. data/lib/amp/support/amp_config.rb +164 -0
  151. data/lib/amp/support/amp_ui.rb +287 -0
  152. data/lib/amp/support/docs.rb +54 -0
  153. data/lib/amp/support/generator.rb +78 -0
  154. data/lib/amp/support/ignore.rb +144 -0
  155. data/lib/amp/support/loaders.rb +93 -0
  156. data/lib/amp/support/logger.rb +103 -0
  157. data/lib/amp/support/match.rb +151 -0
  158. data/lib/amp/support/multi_io.rb +87 -0
  159. data/lib/amp/support/openers.rb +121 -0
  160. data/lib/amp/support/ruby_19_compatibility.rb +66 -0
  161. data/lib/amp/support/support.rb +1095 -0
  162. data/lib/amp/templates/blank.commit.erb +23 -0
  163. data/lib/amp/templates/blank.log.erb +18 -0
  164. data/lib/amp/templates/default.commit.erb +23 -0
  165. data/lib/amp/templates/default.log.erb +26 -0
  166. data/lib/amp/templates/template.rb +165 -0
  167. data/site/Rakefile +24 -0
  168. data/site/src/about/ampfile.haml +57 -0
  169. data/site/src/about/commands.haml +106 -0
  170. data/site/src/about/index.haml +33 -0
  171. data/site/src/about/performance.haml +31 -0
  172. data/site/src/about/workflows.haml +34 -0
  173. data/site/src/contribute/index.haml +65 -0
  174. data/site/src/contribute/style.haml +297 -0
  175. data/site/src/css/active4d.css +114 -0
  176. data/site/src/css/all_hallows_eve.css +72 -0
  177. data/site/src/css/all_themes.css +3299 -0
  178. data/site/src/css/amp.css +260 -0
  179. data/site/src/css/amy.css +147 -0
  180. data/site/src/css/blackboard.css +88 -0
  181. data/site/src/css/brilliance_black.css +605 -0
  182. data/site/src/css/brilliance_dull.css +599 -0
  183. data/site/src/css/cobalt.css +149 -0
  184. data/site/src/css/cur_amp.css +185 -0
  185. data/site/src/css/dawn.css +121 -0
  186. data/site/src/css/eiffel.css +121 -0
  187. data/site/src/css/espresso_libre.css +109 -0
  188. data/site/src/css/idle.css +62 -0
  189. data/site/src/css/iplastic.css +80 -0
  190. data/site/src/css/lazy.css +73 -0
  191. data/site/src/css/mac_classic.css +123 -0
  192. data/site/src/css/magicwb_amiga.css +104 -0
  193. data/site/src/css/pastels_on_dark.css +188 -0
  194. data/site/src/css/reset.css +55 -0
  195. data/site/src/css/slush_poppies.css +85 -0
  196. data/site/src/css/spacecadet.css +51 -0
  197. data/site/src/css/sunburst.css +180 -0
  198. data/site/src/css/twilight.css +137 -0
  199. data/site/src/css/zenburnesque.css +91 -0
  200. data/site/src/get/index.haml +32 -0
  201. data/site/src/helpers.rb +121 -0
  202. data/site/src/images/amp_logo.png +0 -0
  203. data/site/src/images/carbonica.png +0 -0
  204. data/site/src/images/revolution.png +0 -0
  205. data/site/src/images/tab-bg.png +0 -0
  206. data/site/src/images/tab-sliding-left.png +0 -0
  207. data/site/src/images/tab-sliding-right.png +0 -0
  208. data/site/src/include/_footer.haml +22 -0
  209. data/site/src/include/_header.haml +17 -0
  210. data/site/src/index.haml +104 -0
  211. data/site/src/learn/index.haml +46 -0
  212. data/site/src/scripts/jquery-1.3.2.min.js +19 -0
  213. data/site/src/scripts/jquery.cookie.js +96 -0
  214. data/tasks/stats.rake +155 -0
  215. data/tasks/yard.rake +171 -0
  216. data/test/dirstate_tests/dirstate +0 -0
  217. data/test/dirstate_tests/hgrc +5 -0
  218. data/test/dirstate_tests/test_dir_state.rb +192 -0
  219. data/test/functional_tests/resources/.hgignore +2 -0
  220. data/test/functional_tests/resources/STYLE.txt +25 -0
  221. data/test/functional_tests/resources/command.rb +372 -0
  222. data/test/functional_tests/resources/commands/annotate.rb +57 -0
  223. data/test/functional_tests/resources/commands/experimental/lolcats.rb +17 -0
  224. data/test/functional_tests/resources/commands/heads.rb +22 -0
  225. data/test/functional_tests/resources/commands/manifest.rb +12 -0
  226. data/test/functional_tests/resources/commands/status.rb +90 -0
  227. data/test/functional_tests/resources/version2/.hgignore +5 -0
  228. data/test/functional_tests/resources/version2/STYLE.txt +25 -0
  229. data/test/functional_tests/resources/version2/command.rb +372 -0
  230. data/test/functional_tests/resources/version2/commands/annotate.rb +45 -0
  231. data/test/functional_tests/resources/version2/commands/experimental/lolcats.rb +17 -0
  232. data/test/functional_tests/resources/version2/commands/heads.rb +22 -0
  233. data/test/functional_tests/resources/version2/commands/manifest.rb +12 -0
  234. data/test/functional_tests/resources/version2/commands/status.rb +90 -0
  235. data/test/functional_tests/resources/version3/.hgignore +5 -0
  236. data/test/functional_tests/resources/version3/STYLE.txt +31 -0
  237. data/test/functional_tests/resources/version3/command.rb +376 -0
  238. data/test/functional_tests/resources/version3/commands/annotate.rb +45 -0
  239. data/test/functional_tests/resources/version3/commands/experimental/lolcats.rb +17 -0
  240. data/test/functional_tests/resources/version3/commands/heads.rb +22 -0
  241. data/test/functional_tests/resources/version3/commands/manifest.rb +12 -0
  242. data/test/functional_tests/resources/version3/commands/status.rb +90 -0
  243. data/test/functional_tests/resources/version4/.hgignore +5 -0
  244. data/test/functional_tests/resources/version4/STYLE.txt +31 -0
  245. data/test/functional_tests/resources/version4/command.rb +376 -0
  246. data/test/functional_tests/resources/version4/commands/experimental/lolcats.rb +17 -0
  247. data/test/functional_tests/resources/version4/commands/heads.rb +22 -0
  248. data/test/functional_tests/resources/version4/commands/manifest.rb +12 -0
  249. data/test/functional_tests/resources/version4/commands/stats.rb +25 -0
  250. data/test/functional_tests/resources/version4/commands/status.rb +90 -0
  251. data/test/functional_tests/resources/version5_1/.hgignore +5 -0
  252. data/test/functional_tests/resources/version5_1/STYLE.txt +2 -0
  253. data/test/functional_tests/resources/version5_1/command.rb +374 -0
  254. data/test/functional_tests/resources/version5_1/commands/experimental/lolcats.rb +17 -0
  255. data/test/functional_tests/resources/version5_1/commands/heads.rb +22 -0
  256. data/test/functional_tests/resources/version5_1/commands/manifest.rb +12 -0
  257. data/test/functional_tests/resources/version5_1/commands/stats.rb +25 -0
  258. data/test/functional_tests/resources/version5_1/commands/status.rb +90 -0
  259. data/test/functional_tests/resources/version5_2/.hgignore +5 -0
  260. data/test/functional_tests/resources/version5_2/STYLE.txt +14 -0
  261. data/test/functional_tests/resources/version5_2/command.rb +376 -0
  262. data/test/functional_tests/resources/version5_2/commands/experimental/lolcats.rb +17 -0
  263. data/test/functional_tests/resources/version5_2/commands/manifest.rb +12 -0
  264. data/test/functional_tests/resources/version5_2/commands/newz.rb +12 -0
  265. data/test/functional_tests/resources/version5_2/commands/stats.rb +25 -0
  266. data/test/functional_tests/resources/version5_2/commands/status.rb +90 -0
  267. data/test/functional_tests/test_functional.rb +604 -0
  268. data/test/localrepo_tests/test_local_repo.rb +121 -0
  269. data/test/localrepo_tests/testrepo.tar.gz +0 -0
  270. data/test/manifest_tests/00manifest.i +0 -0
  271. data/test/manifest_tests/test_manifest.rb +72 -0
  272. data/test/merge_tests/base.txt +10 -0
  273. data/test/merge_tests/expected.local.txt +16 -0
  274. data/test/merge_tests/local.txt +11 -0
  275. data/test/merge_tests/remote.txt +11 -0
  276. data/test/merge_tests/test_merge.rb +26 -0
  277. data/test/revlog_tests/00changelog.i +0 -0
  278. data/test/revlog_tests/revision_added_changelog.i +0 -0
  279. data/test/revlog_tests/test_adding_index.i +0 -0
  280. data/test/revlog_tests/test_revlog.rb +333 -0
  281. data/test/revlog_tests/testindex.i +0 -0
  282. data/test/store_tests/store.tar.gz +0 -0
  283. data/test/store_tests/test_fncache_store.rb +122 -0
  284. data/test/test_amp.rb +9 -0
  285. data/test/test_base85.rb +14 -0
  286. data/test/test_bdiff.rb +42 -0
  287. data/test/test_commands.rb +122 -0
  288. data/test/test_difflib.rb +50 -0
  289. data/test/test_helper.rb +15 -0
  290. data/test/test_journal.rb +29 -0
  291. data/test/test_match.rb +134 -0
  292. data/test/test_mdiff.rb +74 -0
  293. data/test/test_mpatch.rb +14 -0
  294. data/test/test_support.rb +24 -0
  295. metadata +385 -0
@@ -0,0 +1,377 @@
1
+ require 'uri'
2
+
3
+ # to shut up those fucking warnings!
4
+ # taken from http://www.5dollarwhitebox.org/drupal/node/64
5
+ class Net::HTTP
6
+ alias_method :old_initialize, :initialize
7
+ def initialize(*args)
8
+ old_initialize(*args)
9
+ require 'openssl' unless defined? OpenSSL
10
+ @ssl_context = OpenSSL::SSL::SSLContext.new
11
+ @ssl_context.verify_mode = OpenSSL::SSL::VERIFY_NONE
12
+ end
13
+ end
14
+
15
+ module Amp
16
+ module Repositories
17
+ ##
18
+ # = This is the class for connecting to an HTTP[S]-based repository.
19
+ # The protocol's pretty simple - just ?cmd="command", and any other
20
+ # args you need. Should be pretty easy.
21
+ class HTTPRepository < Repository
22
+ include RevlogSupport::Node
23
+
24
+ DEFAULT_HEADERS = {"User-agent" => "Amp-#{Amp::VERSION}",
25
+ "Accept" => "Application/Mercurial-0.1"}
26
+
27
+ ##
28
+ # The URL we connect to for this repository
29
+ attr_reader :url
30
+
31
+ ##
32
+ # Should the repository connect via SSL?
33
+ attr_accessor :secure
34
+
35
+ ##
36
+ # Returns whether the repository is local or not. Which it isn't. Because
37
+ # we're connecting over HTTP.
38
+ #
39
+ # @return [Boolean] +false+. Because the repo isn't local.
40
+ def local?; false; end
41
+
42
+ ##
43
+ # Standard initializer for a repository. However, "create" is a no-op.
44
+ #
45
+ # @param [String] path the URL for the repository.
46
+ # @param [Boolean] create this is useless since we can't create remote repos
47
+ # @param [Amp::AmpConfig] config the configuration for Amp right now.
48
+ def initialize(path="", create=false, config=nil)
49
+ @url, @config = URI.parse(path), config
50
+ @auth_mode = :none
51
+ raise InvalidArgumentError.new("Invalid URL for an HTTP repo!") if @url.nil?
52
+ end
53
+
54
+ ##
55
+ # Loads the capabilities from the server when necessary. (Lazy loading)
56
+ #
57
+ # @return [Hash] the capabilities of the server, in the form:
58
+ # { capability => true }
59
+ # or
60
+ # { capability => "capability;settings;"}
61
+ def get_capabilities
62
+ return @capabilities if @capabilities
63
+ begin
64
+ @capabilities = {}
65
+ do_read("capabilities").first.split.each do |k|
66
+ if k.include? "="
67
+ key, value = k.split("=", 2)
68
+ @capabilities[key] = value
69
+ else
70
+ @capabilities[k] = true
71
+ end
72
+ end
73
+ rescue
74
+ @capabilities = []
75
+ end
76
+ @capabilities
77
+ end
78
+
79
+ ##
80
+ # Unsupported - raises an error.
81
+ def lock; raise RepoError.new("You can't lock an HTTP repo."); end
82
+
83
+ ##
84
+ # Looks up a node with the given key. The key could be a node ID (full or
85
+ # partial), an index number (though this is slightly risky as it might
86
+ # match a node ID partially), "tip", and so on. See {LocalRepository#[]}.
87
+ #
88
+ # @param [String] key the key to look up - could be node ID, revision index,
89
+ # and so on.
90
+ # @return [String] the full node ID of the requested node on the remote server
91
+ def lookup(key)
92
+ require_capability("lookup", "Look up Remote Revision")
93
+ data = do_read("lookup", :key => key).first
94
+ code, data = data.chomp.split(" ", 2)
95
+
96
+ return data.unhexlify if code.to_i > 0
97
+ raise RepoError.new("Unknown Revision #{data}")
98
+ end
99
+
100
+ ##
101
+ # Gets all the heads of the repository. Returned in binary form.
102
+ #
103
+ # @return [Array<String>] the full, binary node_ids of all the heads on
104
+ # the remote server.
105
+ def heads
106
+ data = do_read("heads").first
107
+ data.chomp.split(" ").map {|h| h.unhexlify }
108
+ end
109
+
110
+ ##
111
+ # Gets the node IDs of all the branch roots in the repository. Uses
112
+ # the supplied nodes to use to search for branches.
113
+ #
114
+ # @param [Array<String>] nodes the nodes to use as heads to search for
115
+ # branches. The search starts at each supplied node (or the tip, if
116
+ # left empty), and goes to that tree's root, and returns the relevant
117
+ # information for the branch.
118
+ # @return [Array<Array<String>>] An array of arrays of strings. Each array
119
+ # has 4 components: [head, root, parent1, parent2].
120
+ def branches(nodes)
121
+ n = nodes.map {|n| n.hexlify }.join(" ")
122
+ data = do_read("branches", :nodes => n).first
123
+ data.split("\n").map do |b|
124
+ b.split(" ").map {|b| b.unhexlify}
125
+ end
126
+ end
127
+
128
+ ##
129
+ # Asks the server to bundle up the given nodes into a changegroup, and returns it
130
+ # uncompressed. This is for pulls.
131
+ #
132
+ # @todo figure out what the +kind+ parameter is for
133
+ # @param [Array<String>] nodes the nodes to package into the changegroup
134
+ # @param [NilClass] kind (UNUSED)
135
+ # @return [StringIO] the uncompressed changegroup as a stream
136
+ def changegroup(nodes, kind)
137
+ n = nodes.map{|i| i.hexlify }.join ' '
138
+ f = do_read('changegroup', n.empty? ? {} : {:roots => n}).first
139
+
140
+ s = StringIO.new "",(ruby_19? ? "w+:ASCII-8BIT" : "w+")
141
+ s.write Zlib::Inflate.inflate(f)
142
+ s.pos = 0
143
+ s
144
+ end
145
+
146
+ ##
147
+ # Asks the server to bundle up all the necessary nodes between the lists
148
+ # bases and heads. It is returned as a stream that reads it in a decompressed
149
+ # fashion. This is for pulls.
150
+ #
151
+ # @param [Array<String>] bases the base nodes of the subset we're requesting.
152
+ # Should be an array (or any Enumerable) of node ids.
153
+ # @param [Array<String>] heads the heads of the subset we're requesting.
154
+ # These nodes will be retrieved as well. Should be an array of node IDs.
155
+ # @param [NilClass] source i have no idea (UNUSED)
156
+ # @return [StringIO] the uncompressed changegroup subset as a stream.
157
+ def changegroup_subset(bases, heads, source)
158
+ #require_capability 'changegroupsubset', 'look up remote changes'
159
+ base_list = bases.map {|n| n.hexlify }.join ' '
160
+ head_list = heads.map {|n| n.hexlify }.join ' '
161
+ # p base_list, head_list
162
+ f, code = *do_read("changegroupsubset", :bases => base_list, :heads => head_list)
163
+
164
+ s = StringIO.new "",(ruby_19? ? "w+:ASCII-8BIT" : "w+")
165
+ s.write Zlib::Inflate.inflate(f)
166
+ s.rewind
167
+ s
168
+ end
169
+
170
+ ##
171
+ # Sends a bundled up changegroup to the server, who will add it to its repository.
172
+ # Uses the bundle format.
173
+ #
174
+ # @param [StringIO] cg the changegroup to push as a stream.
175
+ # @param [Array<String>] heads the heads of the changegroup being sent
176
+ # @param [NilClass] source no idea UNUSED
177
+ # @return [Fixnum] the response code from the server (1 indicates success)
178
+ def unbundle(cg, heads, source)
179
+ # have to stream bundle to a temp file because we do not have
180
+ # http 1.1 chunked transfer
181
+
182
+ type = ''
183
+ types = capable? 'unbundle'
184
+
185
+ # servers older than d1b16a746db6 will send 'unbundle' as a boolean
186
+ # capability
187
+ # this will be a list of allowed bundle compression types
188
+ types = types.split ',' rescue ['']
189
+
190
+ # pick a compression format
191
+ types.each do |x|
192
+ (type = x and break) if RevlogSupport::ChangeGroup::BUNDLE_HEADERS.include? x
193
+ end
194
+
195
+ # compress and create the bundle
196
+ data = RevlogSupport::ChangeGroup.write_bundle cg, type
197
+
198
+ # send the data
199
+ resp = do_read 'unbundle', :data => data.string,
200
+ :headers => {'Content-Type' => 'application/octet-stream'},
201
+ :heads => heads.map{|h| h.hexlify }.join(' ')
202
+ # parse output
203
+ resp_code, output = resp.first.split "\n"
204
+
205
+ # make sure the reponse was in an expected format (i.e. with a response code)
206
+ unless resp_code.to_i.to_s == resp_code
207
+ raise abort("push failed (unexpected response): #{resp}")
208
+ end
209
+
210
+ # output any text from the server
211
+ UI::say output
212
+ # return 1 for success, 0 for failure
213
+ resp_code.to_i
214
+ end
215
+
216
+ def stream_out
217
+ do_cmd 'stream_out'
218
+ end
219
+
220
+ ##
221
+ # For each provided pair of nodes, return the nodes between the pair.
222
+ #
223
+ # @param [Array<Array<String>>] an array of node pairs, so an array of an array
224
+ # of strings. The first node is the head, the second node is the root of the pair.
225
+ # @return [Array<Array<String>>] for each pair, we return 1 array, which contains
226
+ # the node IDs of every node between the pair.
227
+ # add lstrip to split_newlines to fix but not cure bug
228
+ def between(pairs)
229
+ batch = 8
230
+ ret = []
231
+
232
+ (0..(pairs.size)).step(batch) do |i|
233
+ n = pairs[i..(i+batch-1)].map {|p| p.map {|k| k.hexlify }.join("-") }.join(" ")
234
+ d, code = *do_read("between", :pairs => n)
235
+
236
+ raise RepoError.new("unexpected code: #{code}") unless code == 200
237
+
238
+ ret += d.lstrip.split_newlines.map {|l| (l && l.split(" ").map{|i| i.unhexlify }) || []}
239
+ end
240
+ Amp::UI.debug "between returns: #{ret.inspect}"
241
+ ret
242
+ end
243
+
244
+ private
245
+
246
+ ##
247
+ # Runs the given command by the server, gets the response. Takes the name of the command,
248
+ # the data, headers, etc. The command is assumed to be a GET request, unless args[:data] is
249
+ # set, in which case it is sent via POST.
250
+ #
251
+ # @param [String] command the command to send to the server, such as "heads"
252
+ # @param [Hash] args the arguments you need to provide - for lookup, it
253
+ # might be the revision indicies.
254
+ # @return [String] the response data from the server.
255
+ def do_cmd(command, args={})
256
+ require 'net/http'
257
+
258
+ # Be safe for recursive calls
259
+ work_args = args.dup
260
+ # grab data, but don't leave it in, or it'll be added to the query string
261
+ data = work_args.delete(:data) || nil
262
+ # and headers, but don't leave it in, or it'll be added to the query string
263
+ headers = work_args.delete(:headers) || {}
264
+
265
+ # Our query string is "cmd => command" plus any other parts of the args hash
266
+ query = { "cmd" => command }
267
+ query.merge! work_args
268
+
269
+ # break it up, make a query
270
+ host = @url.host
271
+ path = @url.path
272
+ # Was having trouble with this... should be safe now
273
+ path += "?" + URI.escape(query.map {|k,v| "#{k}=#{v}"}.join("&"), /[^-_!~*'()a-zA-Z\d;\/?:@&=+$,\[\]]/n)
274
+
275
+ # silly scoping
276
+ response = nil
277
+ # Create an HTTP object so we can send our request. static methods aren't flexible
278
+ # enough for us
279
+ sess = Net::HTTP.new host, @url.port
280
+ # Use SSL if necessary
281
+ sess.use_ssl = true if secure
282
+ # Let's send our request!
283
+ sess.start do |http|
284
+ # if we have data, it's a POST
285
+ if data
286
+ req = Net::HTTP::Post.new(path)
287
+ req.body = data
288
+ else
289
+ # otherwise, it's a GET
290
+ req = Net::HTTP::Get.new(path)
291
+ end
292
+ if @auth_mode == :digest
293
+ # Set digest headers
294
+ req.digest_auth @username, @password, @auth_digest
295
+ elsif @auth_mode == :basic
296
+ # Set basic auth headers
297
+ req.basic_auth @username, @password
298
+ end
299
+ # Copy over the default headers
300
+ DEFAULT_HEADERS.each {|k, v| req[k] = v}
301
+ # Then overwrite them (and add new ones) from our arguments
302
+ headers.each {|k, v| req[k] = v}
303
+ # And send the request!
304
+ response = http.request(req)
305
+ end
306
+ # Case on response - we'll be using the kind_of? style of switch statement
307
+ # here
308
+ case response
309
+ when Net::HTTPRedirection
310
+ # Redirect to a new URL - grab the new URL...
311
+ newurl = response["Location"]
312
+ @url = URI.parse(newurl)
313
+ # and try that again.
314
+ do_cmd(command, args)
315
+ when Net::HTTPUnauthorized
316
+ if @auth_mode == :digest
317
+ # no other handlers!
318
+ raise AuthorizationError.new("Failed to authenticate to local repository!")
319
+ elsif @auth_mode == :basic
320
+ # failed to authenticate via basic, so escalate to digest mode
321
+ @auth_mode = :digest
322
+ @auth_digest = response
323
+ do_cmd command, args
324
+ else
325
+ # They want a username and password. A few routes:
326
+ # First, check the URL for the username:password@host format
327
+ @username ||= @url.user
328
+ @password ||= @url.password
329
+ # and start off with basic authentication
330
+ @auth_mode = :basic
331
+ # If the URL didn't contain the username AND password, ask the user for them.
332
+ unless @username && @password
333
+ UI::say "==> HTTP Authentication Required"
334
+
335
+ @username = UI::ask 'username: '
336
+ @password = UI::ask 'password: ', :password
337
+ end
338
+
339
+ # Recursively call the command
340
+ do_cmd command, args
341
+ end
342
+ else
343
+ # We got a successful response! Woo!
344
+ response
345
+ end
346
+ end
347
+
348
+ ##
349
+ # This is a helper for do_cmd - it splits up the response object into
350
+ # two relevant parts: the response body, and the response code.
351
+ #
352
+ # @param [String] command the remote command to execute, such as "heads"
353
+ # @param [Hash] args the arguments to pass to the request. Takes some special values. All
354
+ # other values are sent in the query string.
355
+ # @option args [String] :data (nil) the POST data to send
356
+ # @option args [Hash] :headers ({}) the headers to send with the request, not including
357
+ # any authentication or user-agent headers.
358
+ # @return [Array] the response data, in the form [body, response_code]
359
+ def do_read(command, args={})
360
+ response = do_cmd(command, args)
361
+ [response.body, response.code.to_i]
362
+ end
363
+ end
364
+
365
+ ##
366
+ # A special form of the HTTPRepository, except that it is secured over SSL (HTTPS).
367
+ # Other than that, nothing fancy about it.
368
+ class HTTPSRepository < HTTPRepository
369
+ def initialize(*args)
370
+ require 'net/https'
371
+
372
+ super(*args)
373
+ self.secure = true
374
+ end
375
+ end
376
+ end
377
+ end
@@ -0,0 +1,2661 @@
1
+ require 'fileutils'
2
+
3
+ module Amp
4
+ module Repositories
5
+
6
+ ##
7
+ # A Local Repository is a repository that works on local repo's, such
8
+ # as your working directory. This makes it pretty damn important, and also
9
+ # pretty damn complicated. Have fun!
10
+ class LocalRepository < Repository
11
+ include Amp::RevlogSupport::Node
12
+ include Amp::Repositories::BranchManager
13
+ include Amp::Repositories::TagManager
14
+ include Amp::Repositories::Updatable
15
+ include Amp::Repositories::Verification
16
+
17
+ # The config is an {AmpConfig} for this repo (and uses .hg/hgrc)
18
+ attr_accessor :config
19
+
20
+ attr_reader :root
21
+ attr_reader :root_pathname # save some computation here
22
+ attr_reader :hg
23
+ attr_reader :hg_opener
24
+ attr_reader :branch_manager
25
+ attr_reader :store_opener
26
+ attr_reader :store
27
+
28
+ ##
29
+ # Initializes a new directory to the given path, and with the current
30
+ # configuration.
31
+ #
32
+ # @param [String] path a path to the Repository.
33
+ # @param [Boolean] create Should we create a new one? Usually for
34
+ # the "amp init" command.
35
+ # @param [Amp::AmpConfig] config the configuration loaded from the user's
36
+ # system. Will have some settings overwritten by the repo's hgrc.
37
+ def initialize(path="", create=false, config=nil)
38
+ @capabilities = {}
39
+ @root = path[-1, 1] == '/' ? path[0..-2] : path # no ending slashes
40
+ @root = File.expand_path @root
41
+ @hg = File.join @root, ".hg"
42
+ @file_opener = Amp::Opener.new @root
43
+ @file_opener.default = :open_file # these two are the same, pretty much
44
+ @hg_opener = Amp::Opener.new @root
45
+ @hg_opener.default = :open_hg # just with different defaults
46
+ @filters = {}
47
+ @changelog = nil
48
+ @manifest = nil
49
+ @dirstate = nil
50
+ requirements = []
51
+
52
+ # make a repo if necessary
53
+ unless File.directory? @hg
54
+ if create
55
+ then requirements = init config
56
+ else raise RepoError.new("Repository #{path} not found")
57
+ end
58
+ end
59
+
60
+ # no point in reading what we _just_ wrote...
61
+ unless create
62
+ # read requires
63
+ # save it if something's up
64
+ @hg_opener.open("requires", 'r') {|f| f.each {|r| requirements << r.strip } } rescue nil
65
+ end
66
+
67
+ @store = Stores.pick requirements, @hg, Amp::Opener
68
+ @config = Amp::AmpConfig.new :parent_config => config
69
+ @config.read_file File.join(@hg,"hgrc")
70
+ end
71
+
72
+ def local?; true; end
73
+
74
+ def relative_join(file, cur_dir=FileUtils.pwd)
75
+ @root_pathname ||= Pathname.new(@root)
76
+ Pathname.new(File.expand_path(File.join(cur_dir, file))).relative_path_from(@root_pathname).to_s
77
+ end
78
+
79
+
80
+ def inspect; "#<LocalRepository @root=#{@root.inspect}>"; end
81
+
82
+ ##
83
+ # Creates this repository's folders and structure.
84
+ #
85
+ # @param [AmpConfig] config the configuration for this user so
86
+ # we know what neato features to use (like filename cache)
87
+ # @return [Array<String>] the requirements that we found are returned,
88
+ # so further configuration can go down.
89
+ def init(config=@config)
90
+ # make the directory if it's not there
91
+ FileUtils.makedirs @hg
92
+
93
+ requirements = ["revlogv1"]
94
+
95
+ # add some requirements
96
+ if config["format"]["usestore", Boolean] || true
97
+ FileUtils.mkdir "#{@hg}/store"
98
+ requirements << "store"
99
+ requirements << "fncache" if config["format"]["usefncache", Boolean, true]
100
+
101
+ # add the changelog
102
+ make_changelog
103
+ end
104
+
105
+
106
+ # write the requires file
107
+ write_requires requirements
108
+ end
109
+
110
+ ##
111
+ # Has the repository been changed since the last commit?
112
+ # Returns true if there are NO outstanding changes or uncommitted merges.
113
+ #
114
+ # @return [Boolean] is the repo pristine
115
+ def pristine?
116
+ dirstate.parents.last == RevlogSupport::Node::NULL_ID &&
117
+ status(:only => [:modified, :added, :removed, :deleted]).all? {|_, v| v.empty? }
118
+ end
119
+
120
+ ##
121
+ # @see pristine?
122
+ def changed?; !pristine?; end
123
+
124
+ ##
125
+ # Effectively FileUtils.pwd
126
+ #
127
+ # @return [String] the current location
128
+ def cwd
129
+ dirstate.cwd
130
+ end
131
+ alias_method :pwd, :cwd
132
+
133
+ ##
134
+ # Returns the relative path from +src+ to +dest+.
135
+ #
136
+ # @param [String] src This is a directory! If this is relative,
137
+ # it is assumed to be relative to the root.
138
+ # @param [String] dest This MUST be within root! It also is a file.
139
+ # @return [String] the relative path
140
+ def path_to(src, dest)
141
+ dirstate.path_to src, dest
142
+ end
143
+
144
+ ##
145
+ # Gets the changeset at the given revision.
146
+ #
147
+ # @param [String, Integer] rev the revision index (Integer) or
148
+ # node_id (String) that we want to access. if nil, returns
149
+ # the working directory. if the string is 'tip', it returns the
150
+ # latest head. Can be either a string or an integer;
151
+ # this shit is smart.
152
+ # @return [Changeset] the changeset at the given revision index or node
153
+ # id. Could be working directory.
154
+ def [](rev)
155
+ if rev.nil?
156
+ return WorkingDirectoryChangeset.new(self)
157
+ end
158
+ rev = rev.to_i if rev.to_i.to_s == rev
159
+ return Changeset.new(self, rev)
160
+ end
161
+
162
+ ##
163
+ # Iterates over each changeset in the repository, from oldest to newest.
164
+ #
165
+ # @yield each changeset in the repository is yielded to the caller, in order
166
+ # from oldest to newest. (Actually, lowest revision # to highest revision #)
167
+ def each(&block)
168
+ 0.upto(size - 1) { |i| yield self[i]}
169
+ end
170
+
171
+ ##
172
+ # Creates a lock at the given path. At first it tries to just make it straight away.
173
+ # If this fails, we then sleep for up to a given amount of time (defaults to 10 minutes!)
174
+ # and continually try to acquire the lock.
175
+ #
176
+ # @raise [LockHeld] if the lock cannot be acquired, this exception is raised
177
+ # @param [String] lockname the name of the lock to create
178
+ # @param [Boolean] wait should we wait for the lock to be released?
179
+ # @param [Proc, #call] release_proc a proc to run when the lock is released
180
+ # @param [Proc, #call] acquire_proc a proc to run when we get the lock
181
+ # @param [String] desc the description of the lock to show if someone stomps on it
182
+ # @return [Lock] a lock at the given location.
183
+ def make_lock(lockname, wait, release_proc, acquire_proc, desc)
184
+ begin
185
+ lock = Lock.new(lockname, :timeout => 0, :release_fxn => release_proc, :desc => desc)
186
+ rescue LockHeld => err
187
+ raise unless wait
188
+ UI.warn("waiting for lock on #{desc} held by #{err.locker}")
189
+ lock = Lock.new(lockname, :timeout => @config["ui","timeout","600"].to_i,
190
+ :release_proc => release_proc, :desc => desc)
191
+ end
192
+ acquire_proc.call if acquire_proc
193
+ return lock
194
+ end
195
+
196
+ ##
197
+ # Locks the repository's .hg/store directory. Returns the lock, or if a block is given,
198
+ # runs the block with the lock, and clears the lock afterward.
199
+ #
200
+ # @yield When a block is given, that block is executed under locked
201
+ # conditions. That code can be guaranteed it is the only code running on the
202
+ # store in a destructive manner.
203
+ # @param [Boolean] wait (true) wait for the lock to expire?
204
+ # @return [Lock] the lock on the .hg/store directory
205
+ def lock_store(wait = true)
206
+ return @lock_ref if @lock_ref && @lock_ref.weakref_alive?
207
+
208
+ lock = make_lock(store_join("lock"), wait, nil, nil, "repository #{root}")
209
+ @lock_ref = WeakRef.new(lock)
210
+ if block_given?
211
+ begin
212
+ yield
213
+ ensure
214
+ @lock_ref = nil
215
+ lock.release
216
+ end
217
+ else
218
+ return lock
219
+ end
220
+ end
221
+
222
+ ##
223
+ # Locks the repository's .hg/store directory. Returns the lock, or if a block is given,
224
+ # runs the block with the lock, and clears the lock afterward.
225
+ #
226
+ # @yield When a block is given, that block is executed under locked
227
+ # conditions. That code can be guaranteed it is the only code running on the
228
+ # working directory in a destructive manner.
229
+ # @param [Boolean] wait (true) wait for the lock to expire?
230
+ # @return [Lock] the lock on the .hg/store directory
231
+ def lock_working(wait = true)
232
+ return @working_lock_ref if @working_lock_ref && @working_lock_ref.weakref_alive?
233
+
234
+ lock = make_lock(join("wlock"), wait, nil, nil, "working directory of #{root}")
235
+ @working_lock_ref = WeakRef.new(lock)
236
+ if block_given?
237
+ begin
238
+ yield
239
+ ensure
240
+ @working_lock_ref = nil
241
+ lock.release
242
+ end
243
+ else
244
+ return lock
245
+ end
246
+ end
247
+
248
+ ##
249
+ # Takes a block, and runs that block with both the store and the working directory locked.
250
+ #
251
+ # @param [Boolean] wait (true) should we wait for locks, or jsut give up early?
252
+ def lock_working_and_store(wait=true)
253
+ lock_store(wait) do
254
+ lock_working(wait) do
255
+ yield
256
+ end
257
+ end
258
+ end
259
+
260
+ ##
261
+ # Gets the file-log for the given path, so we can look at an individual
262
+ # file's history, for example.
263
+ #
264
+ # @param [String] f the path to the file
265
+ # @return [FileLog] a filelog (a type of revision log) for the given file
266
+ def file(f)
267
+ f = f[1..-1] if f[0, 1] == "/"
268
+ FileLog.new @store.opener, f
269
+ end
270
+
271
+ ##
272
+ # Returns the parent changesets of the specified changeset. Defaults to the
273
+ # working directory, if +change_id+ is unspecified.
274
+ #
275
+ # @param [Integer, String] change_id the ID (or index) of the requested changeset
276
+ # @return [Array<Changeset>] the parent changesets of the requested changeset
277
+ def parents(change_id = nil)
278
+ self[change_id].parents
279
+ end
280
+
281
+ ##
282
+ # Gets a versioned file for the given path, so we can look at the individual
283
+ # file's history with the file object itself.
284
+ #
285
+ # @param [String] path the path to the file
286
+ # @param [Hash] opts the options for creating the versioned file
287
+ # @option [String] opts change_id (nil) The ID of the changeset in question
288
+ # @option [String, Integer] opts file_id (nil) the revision # or node ID of
289
+ # into the file_log
290
+ def versioned_file(path, opts={})
291
+ VersionedFile.new(self, path, opts)
292
+ end
293
+
294
+ ##
295
+ # Gets a versioned file, but using the working directory, so we are looking
296
+ # past the last commit. Important because it uses a different class. Duh.
297
+ #
298
+ # @param [String] path the path to the file
299
+ # @param [Hash] opts the options for creating the versioned file
300
+ # @option [String] opts change_id (nil) The ID of the changeset in question
301
+ # @option [String, Integer] opts file_id (nil) the revision # or node ID of
302
+ # into the file_log
303
+ def working_file(path, opts={})
304
+ VersionedWorkingFile.new(self, path, opts)
305
+ end
306
+
307
+ ##
308
+ # Reads from a file, but in the working directory.
309
+ # Uses encoding if we are set up to do so.
310
+ #
311
+ # @param [String] filename the file to read from the working directory
312
+ # @return [String] the data read from the file, encoded if we are set
313
+ # up to do so.
314
+ def working_read(filename)
315
+ data = @file_opener.open(filename, "r") {|f| f.read }
316
+ data = @filters["encode"].call(filename, data) if @filters["encode"]
317
+ data
318
+ end
319
+
320
+ ##
321
+ # Writes to a file, but in the working directory. Uses encoding if we are
322
+ # set up to do so. Also handles symlinks and executables. Ugh.
323
+ #
324
+ # @param [String] path the path to the file to write to
325
+ # @param [String] data the data to write
326
+ # @param [String] flags the flags to set
327
+ def working_write(path, data, flags)
328
+ @file_opener.open(path, "w") do |file|
329
+ file.write(data)
330
+ end
331
+ if flags && flags.include?('x')
332
+ File.amp_set_executable(working_join(path), true)
333
+ end
334
+ end
335
+
336
+ ##
337
+ # Returns the changelog for this repository. This changelog basically
338
+ # is the history of all commits.
339
+ #
340
+ # @return [ChangeLog] the commit history object for the entire repo.
341
+ def changelog
342
+ return @changelog if @changelog
343
+
344
+ @changelog = ChangeLog.new @store.opener
345
+
346
+ if path = ENV['HG_PENDING']
347
+ if path =~ /^#{root}/
348
+ @changelog.read_pending('00changelog.i.a')
349
+ end
350
+ end
351
+
352
+ @changelog
353
+ end
354
+
355
+ ##
356
+ # Returns the merge state for this repository. The merge state keeps track
357
+ # of what files need to be merged for an update to be successfully completed.
358
+ #
359
+ # @return [MergeState] the repository's merge state.
360
+ def merge_state
361
+ @merge_state ||= Amp::Merges::MergeState.new(self)
362
+ end
363
+
364
+ ##
365
+ # Returns the manifest for this repository. The manifest keeps track
366
+ # of what files exist at what times, and if they have certain flags
367
+ # (such as executable, or is it a symlink).
368
+ #
369
+ # @return [Manifest] the manifest for the repository
370
+ def manifest
371
+ return @manifest if @manifest
372
+
373
+ changelog #load the changelog
374
+ @manifest = Manifest.new @store.opener
375
+ end
376
+
377
+ ##
378
+ # Returns the dirstate for this repository. The dirstate keeps track
379
+ # of files status, such as removed, added, merged, and so on. It also
380
+ # keeps track of the working directory.
381
+ #
382
+ # @return [DirState] the dirstate for this local repository.
383
+ def dirstate
384
+ return @dirstate if @dirstate
385
+
386
+ opener = Amp::Opener.new @root
387
+ opener.default = :open_hg
388
+
389
+ @dirstate = DirState.new(@root, @config, opener)
390
+ @dirstate.read!
391
+ end
392
+
393
+ ##
394
+ # Returns the URL of this repository. Uses the "file:" scheme as such.
395
+ #
396
+ # @return [String] the URL pointing to this repo
397
+ def url; "file:#{@root}"; end
398
+
399
+ ##
400
+ # Opens a file using our opener. Can only access files in .hg/
401
+ def open(*args, &block)
402
+ @hg_opener.open(*args, &block)
403
+ end
404
+
405
+ ##
406
+ # Joins the path to the repo's root (not .hg, the working dir root)
407
+ #
408
+ # @param path the path we're joining
409
+ # @return [String] the path joined to the working directory's root
410
+ def working_join(path)
411
+ File.join(@root, path)
412
+ end
413
+
414
+ ##
415
+ # Joins the path from this repo's path (.hg), to the file provided.
416
+ #
417
+ # @param file the file we need the path for
418
+ # @return [String] the repo's root, joined with the file's path
419
+ def join(file)
420
+ File.join(@hg, file)
421
+ end
422
+
423
+ ##
424
+ # Joins the path, with a bunch of other args, to the store's directory.
425
+ # Used for opening {FileLog}s and whatnot.
426
+ #
427
+ # @param file the path to the file
428
+ # @return [String] the path to the file from the store.
429
+ def store_join(file)
430
+ @store.join file
431
+ end
432
+
433
+ ##
434
+ # Looks up an identifier for a revision in the commit history. This
435
+ # key could be an integer (specifying a revision number), "." for
436
+ # the latest revision, "null" for the null revision, "tip" for
437
+ # the tip of the repository, a node_id (in hex or binary form) for
438
+ # a revision in the changelog. Yeah. It's a flexible method.
439
+ #
440
+ # @param key the key to lookup in the history of the repo
441
+ # @return [String] a node_id into the changelog for the requested revision
442
+ def lookup(key)
443
+ key = key.to_i if key.to_i.to_s == key.to_s # casting for things like "10"
444
+ case key
445
+ when Fixnum, Bignum, Integer
446
+ changelog.node_id_for_index(key)
447
+ when "."
448
+ dirstate.parents().first
449
+ when "null", nil
450
+ NULL_ID
451
+ when "tip"
452
+ changelog.tip
453
+ else
454
+
455
+ n = changelog.id_match(key)
456
+ return n if n
457
+
458
+ return tags[key] if tags[key]
459
+ return branch_tags[key] if branch_tags[key]
460
+
461
+ n = changelog.partial_id_match(key)
462
+ return n if n
463
+
464
+ # bail
465
+ raise RepoError.new("unknown revision #{key}")
466
+ end
467
+ end
468
+
469
+ ##
470
+ # Finds the nodes between two nodes - this algorithm is ported from the
471
+ # python for mercurial (localrepo.py:1247, for 1.2.1 source). Since this
472
+ # is used by servers, it implements their algorithm... which seems to
473
+ # intentionally not return every node between +top+ and +bottom+.
474
+ # Each one is twice as far from +top+ as the previous.
475
+ #
476
+ # @param [Array<String, String>] An array of node-id pairs, which are arrays
477
+ # of [+top+, +bottom+], which are:
478
+ # top [String] the "top" - or most recent - revision's node ID
479
+ # bottom [String] the "bottom" - or oldest - revision's node ID
480
+ #
481
+ # return [Array<String>] a list of node IDs that are between +top+ and +bottom+
482
+ def between(pairs)
483
+ pairs.map do |top, bottom|
484
+ node, list, counter = top, [], 0
485
+ add_me = 1
486
+ while node != bottom && node != NULL_ID
487
+ if counter == add_me
488
+ list << node
489
+ add_me *= 2
490
+ end
491
+ parent = changelog.parents_for_node(node).first
492
+ node = parent
493
+ counter += 1
494
+ end
495
+ list
496
+ end
497
+ end
498
+
499
+ ##
500
+ # Pull new changegroups from +remote+
501
+ # This does not apply the changes, but pulls them onto
502
+ # the local server.
503
+ #
504
+ # @param [String] remote the path of the remote source (will either be
505
+ # an HTTP repo or an SSH repo)
506
+ # @param [{Symbol => [String] or Boolean}] this reads two parameters from
507
+ # opts -- heads and force. heads is the changesets to collect. If this
508
+ # is empty, it will pull from tip.
509
+ def pull(remote, opts={:heads => nil, :force => nil})
510
+ lock_store do
511
+ # get the common nodes, missing nodes, and the remote heads
512
+ # this is findcommonincoming in the Python code, for those with both open
513
+ common, fetch, remote_heads = *common_nodes(remote, :heads => opts[:heads],
514
+ :force => opts[:force])
515
+
516
+ UI::status 'requesting all changes' if fetch == [NULL_ID]
517
+ if fetch.empty?
518
+ UI::status 'no changes found'
519
+ return 0
520
+ end
521
+
522
+ if (opts[:heads].nil? || opts[:heads].empty?) && remote.capable?('changegroupsubset')
523
+ opts[:heads] = remote_heads
524
+ end
525
+ opts[:heads] ||= []
526
+ cg = if opts[:heads].empty?
527
+ remote.changegroup fetch, :pull
528
+ else
529
+ # check for capabilities
530
+ unless remote.capable? 'changegroupsubset'
531
+ raise abort('Partial pull cannot be done because' +
532
+ 'the other repository doesn\'t support' +
533
+ 'changegroupsubset')
534
+ end # end unless
535
+
536
+ remote.changegroup_subset fetch, opts[:heads], :pull
537
+ end
538
+
539
+ add_changegroup cg, :pull, remote.url
540
+ end
541
+ end
542
+
543
+ ##
544
+ # Add a changegroup to the repo.
545
+ #
546
+ # Return values:
547
+ # - nothing changed or no source: 0
548
+ # - more heads than before: 1+added_heads (2..n)
549
+ # - fewer heads than before: -1-removed_heads (-2..-n)
550
+ # - number of heads stays the same: 1
551
+ #
552
+ # Don't the first and last conflict? they stay the same if
553
+ # nothing has changed...
554
+ def add_changegroup(source, type, url, opts={:empty => []})
555
+ run_hook :pre_changegroup, :throw => true, :source => type, :url => url
556
+ changesets = files = revisions = 0
557
+
558
+ return 0 if source.string.empty?
559
+
560
+ rev_map = proc {|x| changelog.revision_index_for_node x }
561
+ cs_map = proc do |x|
562
+ UI::debug "add changeset #{short x}"
563
+ changelog.size
564
+ end
565
+
566
+ # write changelog data to temp files so concurrent readers will not
567
+ # see inconsistent view
568
+ changelog.delay_update
569
+ old_heads = changelog.heads.size
570
+ new_heads = nil # scoping
571
+ changesets = nil # scoping
572
+ cor = nil # scoping
573
+ cnr = nil # scoping
574
+ heads = nil # scoping
575
+
576
+ Journal::start join('journal') do |journal|
577
+ UI::status 'adding changeset'
578
+
579
+ # pull of the changeset group
580
+ cor = changelog.size - 1
581
+ unless changelog.add_group(source, cs_map, journal) || opts[:empty].any?
582
+ raise abort("received changelog group is empty")
583
+ end
584
+
585
+ cnr = changelog.size - 1
586
+ changesets = cnr - cor
587
+
588
+ # pull off the manifest group
589
+ UI::status 'adding manifests'
590
+
591
+ # No need to check for empty manifest group here:
592
+ # if the result of the merge of 1 and 2 is the same in 3 and 4,
593
+ # no new manifest will be created and the manifest group will be
594
+ # empty during the pull
595
+ manifest.add_group source, rev_map, journal
596
+
597
+ # process the files
598
+ UI::status 'adding file changes'
599
+
600
+ loop do
601
+ f = Amp::RevlogSupport::ChangeGroup.get_chunk source
602
+ break if f.empty?
603
+
604
+ UI::debug "adding #{f} revisions"
605
+ fl = file f
606
+ o = fl.index_size
607
+ unless fl.add_group source, rev_map, journal
608
+ raise abort('received file revlog group is empty')
609
+ end
610
+ revisions += fl.index_size - o
611
+ files += 1
612
+ end # end loop
613
+
614
+ new_heads = changelog.heads.size
615
+ heads = ""
616
+
617
+ unless old_heads.zero? || new_heads == old_heads
618
+ heads = " (+#{new_heads - old_heads} heads)"
619
+ end
620
+
621
+ UI::status("added #{changesets} changesets" +
622
+ " with #{revisions} changes to #{files} files#{heads}")
623
+
624
+ if changesets > 0
625
+ changelog.write_pending
626
+ p = proc { changelog.write_pending && root or "" }
627
+ run_hook :pre_txnchangegroup, :throw => true,
628
+ :node => changelog.node_id_for_index(cor+1).hexlify,
629
+ :source => type,
630
+ :url => url
631
+ end
632
+
633
+ changelog.finalize journal
634
+
635
+ end # end Journal::start
636
+
637
+ if changesets > 0
638
+ # forcefully update the on-disk branch cache
639
+ UI::debug 'updating the branch cache'
640
+ branch_tags
641
+ run_hook :post_changegroup, :node => changelog.node_id_for_index(cor+1).hexlify, :source => type, :url => url
642
+
643
+ ((cor+1)..(cnr+1)).to_a.each do |i|
644
+ run_hook :incoming, :node => changelog.node_id_for_index(i).hexlify,
645
+ :source => type,
646
+ :url => url
647
+ end # end each
648
+ end # end if
649
+
650
+ hdz = branch_heads
651
+ # never return 0 here
652
+ ret = if new_heads < old_heads
653
+ new_heads - old_heads - 1
654
+ else
655
+ new_heads - old_heads + 1
656
+ end # end if
657
+
658
+ class << ret
659
+ def success?; self <= 1 || hdz.size == 1; end
660
+ end
661
+
662
+ ret
663
+ end # end def
664
+
665
+ ##
666
+ # A changegroup, of some sort.
667
+ def changegroup(base_nodes, source)
668
+ changegroup_subset(base_nodes, heads, source)
669
+ end
670
+
671
+ ##
672
+ # Prints information about the changegroup we are going to receive.
673
+ #
674
+ # @param [Array<String>] nodes the list of node IDs we are receiving
675
+ # @param [Symbol] source how are we receiving the changegroup?
676
+ # @todo add more debug info
677
+ def changegroup_info(nodes, source)
678
+ # print info
679
+ if source == :bundle
680
+ UI.status("#{nodes.size} changesets found")
681
+ end
682
+ # debug stuff
683
+ end
684
+
685
+ ##
686
+ # Faster version of changegroup_subset. Useful when pushing working dir.
687
+ #
688
+ # Generate a changegruop of all nodes that we have that a recipient
689
+ # doesn't
690
+ #
691
+ # This is much easier than the previous function as we can assume that
692
+ # the recipient has any changegnode we aren't sending them.
693
+ #
694
+ # @param [[String]] common the set of common nodes between remote and self
695
+ # @param [Amp::Repository] source
696
+ def get_changegroup(common, source)
697
+ # Call the hooks
698
+ run_hook :pre_outgoing, :throw => true, :source => source
699
+
700
+ nodes = changelog.find_missing common
701
+ revset = Hash.with_keys(nodes.map {|n| changelog.rev(n)})
702
+
703
+ changegroup_info nodes, source
704
+
705
+ identity = proc {|x| x }
706
+
707
+ # ok so this method goes through the generic revlog, and looks for nodes
708
+ # in the changeset(s) we're pushing. Works by the link_rev - basically,
709
+ # the changelog says "hey we're at revision 35", and any changes to any
710
+ # files in any revision logs for that commit will have a link_revision
711
+ # of 35. So we just look for 35!
712
+ gen_node_list = proc do |log|
713
+ log.select {|r| revset[r.link_rev] }.map {|r| r.node_id }
714
+ end
715
+
716
+ # Ok.... I've tried explaining this 3 times and failed.
717
+ #
718
+ # Goal of this proc: We need to update the changed_files hash to reflect
719
+ # which files (typically file logs) have changed since the last push.
720
+ #
721
+ # How it works: it generates a proc that takes a node_id. That node_id
722
+ # will be looked up in the changelog.i file, which happens to store a
723
+ # list of files that were changed in that commit! So really, this method
724
+ # just takes a node_id, and adds filenamess to the list of changed files.
725
+ changed_file_collector = proc do |changed_fileset|
726
+ proc do |cl_node|
727
+ c = changelog.read cl_node
728
+ c[3].each {|fname| changed_fileset[fname] = true }
729
+ end
730
+ end
731
+
732
+ lookup_revlink_func = proc do |revlog|
733
+ # given a revision, return the node
734
+ # good thing the python has a description of what this does
735
+ #
736
+ # *snort*
737
+ lookup_revlink = proc do |n|
738
+ changelog.node revlog[n].link_rev
739
+ end
740
+ end
741
+
742
+ # This constructs a changegroup, or a list of all changed files.
743
+ # If you're here, looking at this code, this bears repeating:
744
+ # - Changelog
745
+ # -- ChangeSet+
746
+ #
747
+ # A Changelog (history of a branch) is an array of ChangeSets,
748
+ # and a ChangeSet is just a single revision, containing what files
749
+ # were changed, who did it, and the commit message. THIS IS JUST A
750
+ # RECEIPT!!!
751
+ #
752
+ # The REASON we construct a changegroup here is because this is called
753
+ # when we push, and we push a changelog (usually bundled to conserve
754
+ # space). This is where we make that receipt, called a changegroup.
755
+ #
756
+ # 'nuff tangent, time to fucking code
757
+ generate_group = proc do
758
+ result = []
759
+ changed_files = {}
760
+
761
+ coll = changed_file_collector[changed_files]
762
+ # get the changelog's changegroups
763
+ changelog.group(nodes, identity, coll) {|chunk| result << chunk }
764
+
765
+
766
+ node_iter = gen_node_list[manifest]
767
+ look = lookup_revlink_func[manifest]
768
+ # get the manifest's changegroups
769
+ manifest.group(node_iter, look) {|chunk| result << chunk }
770
+
771
+ changed_files.keys.sort.each do |fname|
772
+ file_revlog = file fname
773
+ # warning: useless comment
774
+ if file_revlog.index_size.zero?
775
+ raise abort("empty or missing revlog for #{fname}")
776
+ end
777
+
778
+ node_list = gen_node_list[file_revlog]
779
+
780
+ if node_list.any?
781
+ result << RevlogSupport::ChangeGroup.chunk_header(fname.size)
782
+ result << fname
783
+
784
+ lookup = lookup_revlink_func[file_revlog] # Proc#call
785
+ # more changegroups
786
+ file_revlog.group(node_list, lookup) {|chunk| result << chunk }
787
+ end
788
+ end
789
+ result << RevlogSupport::ChangeGroup.closing_chunk
790
+
791
+ run_hook :post_outgoing, :node => nodes[0].hexlify, :source => source
792
+
793
+ result
794
+ end
795
+
796
+ s = StringIO.new "",(ruby_19? ? "w+:ASCII-8BIT" : "w+")
797
+ generate_group[].each {|chunk| s.write chunk }
798
+ s.rewind
799
+ s
800
+ end
801
+
802
+ ##
803
+ # This function generates a changegroup consisting of all the nodes
804
+ # that are descendents of any of the bases, and ancestors of any of
805
+ # the heads.
806
+ #
807
+ # It is fairly complex in determining which filenodes and which
808
+ # manifest nodes need to be included for the changeset to be complete
809
+ # is non-trivial.
810
+ #
811
+ # Another wrinkle is doing the reverse, figuring out which changeset in
812
+ # the changegroup a particular filenode or manifestnode belongs to.
813
+ #
814
+ # The caller can specify some nodes that must be included in the
815
+ # changegroup using the extranodes argument. It should be a dict
816
+ # where the keys are the filenames (or 1 for the manifest), and the
817
+ # values are lists of (node, linknode) tuples, where node is a wanted
818
+ # node and linknode is the changelog node that should be transmitted as
819
+ # the linkrev.
820
+ #
821
+ # MAD SHOUTZ to Eric Hopper, who actually had the balls to document a
822
+ # good chunk of this code in the Python. He is a really great man, and
823
+ # deserves whatever thanks we can give him. *Peace*
824
+ #
825
+ # @param [String => [(String, String)]] extra_nodes the key is a filename
826
+ # and the value is a list of (node, link_node) tuples
827
+ def changegroup_subset(bases, new_heads, source, extra_nodes=nil)
828
+ unless extra_nodes
829
+ if new_heads.sort! == heads.sort!
830
+ common = []
831
+
832
+ # parents of bases are known from both sides
833
+ bases.each do |base|
834
+ changelog.parents_for_node(base).each do |parent|
835
+ common << parent unless parent.null? # == NULL_ID
836
+ end # end each
837
+ end # end each
838
+
839
+ # BAIL
840
+ return get_changegroup(common, source)
841
+ end # end if
842
+ end # end unless
843
+
844
+ run_hook :pre_outgoing, :throw => true, :source => source # call dem hooks
845
+
846
+
847
+ # missing changelog list, bases, and heads
848
+ #
849
+ # Some bases may turn out to be superfluous, and some heads may be as
850
+ # well. #nodes_between will return the minimal set of bases and heads
851
+ # necessary to recreate the changegroup.
852
+ # missing_cl_list, bases, heads = changelog.nodes_between(bases, heads)
853
+ btw = changelog.nodes_between(bases, heads)
854
+ missing_cl_list, bases, heads = btw[:between], btw[:roots], btw[:heads]
855
+ changegroup_info missing_cl_list, source
856
+
857
+ # Known heads are the list of heads about which it is assumed the recipient
858
+ # of this changegroup will know.
859
+ known_heads = []
860
+
861
+ # We assume that all parents of bases are known heads.
862
+ bases.each do |base|
863
+ changelog.parents_for_node(base).each do |parent|
864
+ known_heads << parent
865
+ end # end each
866
+ end # end each
867
+
868
+ if known_heads.any? # unless known_heads.empty?
869
+ # Now that we know what heads are known, we can compute which
870
+ # changesets are known. The recipient must know about all
871
+ # changesets required to reach the known heads from the null
872
+ # changeset.
873
+ has_cl_set = changelog.nodes_between(nil, known_heads)[:between]
874
+
875
+ # cast to a hash for latter usage
876
+ has_cl_set = Hash.with_keys has_cl_set
877
+ else
878
+ # If there were no known heads, the recipient cannot be assumed to
879
+ # know about any changesets.
880
+ has_cl_set = {}
881
+ end
882
+
883
+ # We don't know which manifests are missing yet
884
+ missing_mf_set = {}
885
+ # Nor do we know which filenodes are missing.
886
+ missing_fn_set = {}
887
+
888
+ ########
889
+ # Here are procs for further usage
890
+
891
+ # A changeset always belongs to itself, so the changenode lookup
892
+ # function for a changenode is +identity+
893
+ identity = proc {|x| x }
894
+
895
+ # A function generating function. Sets up an enviroment for the
896
+ # inner function.
897
+ cmp_by_rev_function = proc do |rvlg|
898
+ # Compare two nodes by their revision number in the environment's
899
+ # revision history. Since the revision number both represents the
900
+ # most efficient order to read the nodes in, and represents a
901
+ # topological sorting of the nodes, this function if often useful.
902
+ proc {|a, b| rvlg.rev(a) <=> rvlg.rev(b) }
903
+ end
904
+
905
+ # If we determine that a particular file or manifest node must be a
906
+ # node that the recipient of the changegroup will already have, we can
907
+ # also assume the recipient will have all the parents. This function
908
+ # prunes them from the set of missing nodes.
909
+ prune_parents = proc do |rvlg, hasses, missing|
910
+ has_list = hasses.keys
911
+ has_list.sort!(&cmp_by_rev_function(rvlg))
912
+
913
+ has_list.each do |node|
914
+ parent_list = revlog.parent_for_node(node).select {|p| p.not_null? }
915
+ end
916
+
917
+ while parent_list.any?
918
+ n = parent_list.pop
919
+ unless hasses.include? n
920
+ hasses[n] = 1
921
+ p = revlog.parent_for_node(node).select {|p| p.not_null? }
922
+ parent_list += p
923
+ end
924
+ end
925
+
926
+ hasses.each do |n|
927
+ missing.slice!(n - 1, 1) # pop(n, None)
928
+ end
929
+ end
930
+
931
+ # This is a function generating function used to set up an environment
932
+ # for the inner funciont to execute in.
933
+ manifest_and_file_collector = proc do |changed_fileset|
934
+ # This is an information gathering function that gathers
935
+ # information from each changeset node that goes out as part of
936
+ # the changegroup. The information gathered is a list of which
937
+ # manifest nodes are potentially required (the recipient may already
938
+ # have them) and total list of all files which were changed in any
939
+ # changeset in the changegroup.
940
+ #
941
+ # We also remember the first changenode we saw any manifest
942
+ # referenced by so we can later determine which changenode owns
943
+ # the manifest.
944
+
945
+ # this is what we're returning
946
+ proc do |cl_node|
947
+ c = changelog.read cl_node
948
+ c[3].each do |f|
949
+ # This is to make sure we only have one instance of each
950
+ # filename string for each filename
951
+ changed_fileset[f] ||= f
952
+ end # end each
953
+
954
+ missing_mf_set[c[0]] ||= cl_node
955
+ end # end proc
956
+ end # end proc
957
+
958
+ # Figure out which manifest nodes (of the ones we think might be part
959
+ # of the changegroup) the recipients must know about and remove them
960
+ # from the changegroup.
961
+ prune_manifest = proc do
962
+ has_mnfst_set = {}
963
+ missing_mf_set.values.each do |node|
964
+ # If a 'missing' manifest thinks it belongs to a changenode
965
+ # the recipient is assumed to have, obviously the recipient
966
+ # must have the manifest.
967
+ link_node = changelog.node manifest.link_rev(manifest.revision_index_for_node(node))
968
+ has_mnfst_set[n] = 1 if has_cl_set.include? link_node
969
+ end # end each
970
+
971
+ prune_parents[manifest, has_mnfst_set, missing_mf_set] # Proc#call
972
+ end # end proc
973
+
974
+ # Use the information collected in collect_manifests_and_files to say
975
+ # which changenode any manifestnode belongs to.
976
+ lookup_manifest_link = proc {|node| missing_mf_set[node] }
977
+
978
+ # A function generating function that sets up the initial environment
979
+ # the inner function.
980
+ filenode_collector = proc do |changed_files|
981
+ next_rev = []
982
+
983
+ # This gathers information from each manifestnode included in the
984
+ # changegroup about which filenodes the manifest node references
985
+ # so we can include those in the changegroup too.
986
+ #
987
+ # It also remembers which changenode each filenode belongs to. It
988
+ # does this by assuming the a filenode belongs to the changenode
989
+ # the first manifest that references it belongs to.
990
+ collect_missing_filenodes = proc do |node|
991
+ r = manifest.rev node
992
+
993
+ if r == next_rev[0]
994
+
995
+ # If the last rev we looked at was the one just previous,
996
+ # we only need to see a diff.
997
+ delta_manifest = manifest.read_delta node
998
+
999
+ # For each line in the delta
1000
+ delta_manifest.each do |f, fnode|
1001
+ f = changed_files[f]
1002
+
1003
+ # And if the file is in the list of files we care
1004
+ # about.
1005
+ if f
1006
+ # Get the changenode this manifest belongs to
1007
+ cl_node = missing_mf_set[node]
1008
+
1009
+ # Create the set of filenodes for the file if
1010
+ # there isn't one already.
1011
+ ndset = missing_fn_set[f] ||= {}
1012
+
1013
+ # And set the filenode's changelog node to the
1014
+ # manifest's if it hasn't been set already.
1015
+ ndset[fnode] ||= cl_node
1016
+ end
1017
+ end
1018
+ else
1019
+ # Otherwise we need a full manifest.
1020
+ m = manifest.read node
1021
+
1022
+ # For every file in we care about.
1023
+ changed_files.each do |f|
1024
+ fnode = m[f]
1025
+
1026
+ # If it's in the manifest
1027
+ if fnode
1028
+ # See comments above.
1029
+ cl_node = msng_mnfst_set[mnfstnode]
1030
+ ndset = missing_fn_set[f] ||= {}
1031
+ ndset[fnode] ||= cl_node
1032
+ end
1033
+ end
1034
+ end
1035
+
1036
+ # Remember the revision we hope to see next.
1037
+ next_rev[0] = r + 1
1038
+ end # end proc
1039
+ end # end proc
1040
+
1041
+ # We have a list of filenodes we think need for a file, let's remove
1042
+ # all those we know the recipient must have.
1043
+ prune_filenodes = proc do |f, f_revlog|
1044
+ missing_set = missing_fn_set[f]
1045
+ hasset = {}
1046
+
1047
+ # If a 'missing' filenode thinks it belongs to a changenode we
1048
+ # assume the recipient must have, the the recipient must have
1049
+ # that filenode.
1050
+ missing_set.each do |n|
1051
+ cl_node = changelog.node f_revlog[n].link_rev
1052
+ hasset[n] = true if has_cl_set.include? cl_node
1053
+ end
1054
+
1055
+ prune_parents[f_revlog, hasset, missing_set] # Proc#call
1056
+ end # end proc
1057
+
1058
+ # Function that returns a function.
1059
+ lookup_filenode_link_func = proc do |name|
1060
+ missing_set = missing_fn_set[name]
1061
+
1062
+ # lookup the changenode the filenode belongs to
1063
+ lookup_filenode_link = proc do |node|
1064
+ missing_set[node]
1065
+ end # end proc
1066
+ end # end proc
1067
+
1068
+ # add the nodes that were explicitly requested.
1069
+ add_extra_nodes = proc do |name, nodes|
1070
+ return unless extra_nodes && extra_nodes[name]
1071
+
1072
+ extra_nodes[name].each do |node, link_node|
1073
+ nodes[node] = link_node unless nodes[node]
1074
+ end
1075
+
1076
+ end
1077
+
1078
+ # Now that we have all theses utility functions to help out and
1079
+ # logically divide up the task, generate the group.
1080
+ generate_group = proc do
1081
+ changed_files = {}
1082
+ group = changelog.group(missing_cl_list, identity, &manifest_and_file_collector[changed_files])
1083
+ group.each { |chunk| yield chunk }
1084
+ prune_manifests.call
1085
+ add_extra_nodes[1, msng_mnfst_set]
1086
+ msng_mnfst_lst = msng_mnfst_set.keys
1087
+
1088
+ msng_mnfst_lst.sort!(&cmp_by_rev_function[manifest])
1089
+
1090
+ group = manifest.group(msng_mnfst_lst, lookup_filenode_link,
1091
+ filenode_collector[changed_files])
1092
+
1093
+ group.each {|chunk| yield chunk }
1094
+
1095
+ msng_mnfst_lst = nil
1096
+ msng_mnfst_set.clear
1097
+
1098
+ if extra_nodes
1099
+ extra_nodes.each do |fname|
1100
+ next if fname.kind_of?(Integer)
1101
+ msng_mnfst_set[fname] ||= {}
1102
+ changed_files[fname] = true
1103
+ end
1104
+ end
1105
+
1106
+ changed_files.sort.each do |fname|
1107
+ file_revlog = file(fname)
1108
+ unless file_revlog.size > 0
1109
+ raise abort("empty or missing revlog for #{fname}")
1110
+ end
1111
+
1112
+ if msng_mnfst_set[fname]
1113
+ prune_filenodes[fname, file_revlog]
1114
+ add_extra_nodes[fname, missing_fn_set[fname]]
1115
+ missing_fn_list = missing_fn_set[fname].keys
1116
+ else
1117
+ missing_fn_list = []
1118
+ end
1119
+
1120
+ if missing_fn_list.size > 0
1121
+ yield ChangeGroup.chunk_header(fname.size)
1122
+ yield fname
1123
+ missing_fn_list.sort!(&cmp_by_rev_function[file_revlog])
1124
+ group = file_revlog.group(missing_fn_list,
1125
+ lookup_filenode_link_func[fname])
1126
+ group.each {|chunk| yield chunk }
1127
+ end
1128
+ if missing_fn_set[fname]
1129
+ missing_fn_set.delete fname
1130
+ end
1131
+ end
1132
+
1133
+ yield ChangeGroup.close_chunk
1134
+
1135
+ if missing_cl_list
1136
+ run_hook :post_outgoing
1137
+ end
1138
+ end # end proc
1139
+
1140
+ s = StringIO.new "",(ruby_19? ? "w+:ASCII-8BIT" : "w+")
1141
+ generate_group.call do |chunk|
1142
+ s.write chunk
1143
+ end
1144
+ s.seek(0, IO::SEEK_SET)
1145
+
1146
+ end # end def
1147
+
1148
+ ##
1149
+ # Revert a file or group of files to +revision+. If +opts[:unlink]+
1150
+ # is true, then the files
1151
+ #
1152
+ # @param [Array<String>] files a list of files to revert
1153
+ # @return [Boolean] a success marker
1154
+ def revert(files, opts={})
1155
+ # get the parents - used in checking if we haven an uncommitted merge
1156
+ parent, p2 = dirstate.parents
1157
+
1158
+ # get the revision
1159
+ rev = opts[:revision] || opts[:rev] || opts[:to]
1160
+
1161
+ # check to make sure it's logically possible
1162
+ unless rev || p2 == RevlogSupport::Node::NULL_ID
1163
+ raise abort("uncommitted merge - please provide a specific revision")
1164
+ end
1165
+
1166
+ # if we have anything here, then create a matcher
1167
+ matcher = if files
1168
+ Amp::Match.create :files => files ,
1169
+ :includer => opts[:include],
1170
+ :excluder => opts[:exclude]
1171
+ else
1172
+ # else just return nil
1173
+ # we can return nil because when it gets used in :match => matcher,
1174
+ # it will be as though it's not even there
1175
+ nil
1176
+ end
1177
+
1178
+ # the changeset we use as a guide
1179
+ changeset = self[rev]
1180
+
1181
+ # get the files that need to be changed
1182
+ stats = status :node_1 => rev, :match => matcher
1183
+
1184
+ ###
1185
+ # now make the changes
1186
+ ###
1187
+
1188
+ ##########
1189
+ # MODIFIED and DELETED
1190
+ ##########
1191
+ # Just write the old data to the files
1192
+ (stats[:modified] + stats[:deleted]).each do |path|
1193
+ File.open path, 'w' do |file|
1194
+ file.write changeset.get_file(path).data
1195
+ end
1196
+ UI::status "restored\t#{path}"
1197
+ end
1198
+
1199
+ ##########
1200
+ # REMOVED
1201
+ ##########
1202
+ # these files are set to be removed, and have thus far been dropped from the filesystem
1203
+ # we restore them and we alert the repo
1204
+ stats[:removed].each do |path|
1205
+ File.open path, 'w' do |file|
1206
+ file.write changeset.get_file(path).data
1207
+ end
1208
+
1209
+ dirstate.normal path # pretend nothing happened
1210
+ UI::status "saved\t#{path}"
1211
+ end
1212
+
1213
+ ##########
1214
+ # ADDED
1215
+ ##########
1216
+ # these files have been added SINCE +rev+
1217
+ stats[:added].each do |path|
1218
+ remove path
1219
+ UI::status "destroyed\t#{path}"
1220
+ end # pretend these files were never even there
1221
+
1222
+ true # success marker
1223
+ end
1224
+
1225
+ # Return list of roots of the subsets of missing nodes from remote
1226
+ #
1227
+ # If base dict is specified, assume that these nodes and their parents
1228
+ # exist on the remote side and that no child of a node of base exists
1229
+ # in both remote and self.
1230
+ # Furthermore base will be updated to include the nodes that exists
1231
+ # in self and remote but no children exists in self and remote.
1232
+ # If a list of heads is specified, return only nodes which are heads
1233
+ # or ancestors of these heads.
1234
+ #
1235
+ # All the ancestors of base are in self and in remote.
1236
+ # All the descendants of the list returned are missing in self.
1237
+ # (and so we know that the rest of the nodes are missing in remote, see
1238
+ # outgoing)
1239
+ def find_incoming_roots(remote, opts={:base => nil, :heads => nil,
1240
+ :force => false, :base => nil})
1241
+ common_nodes(remote, opts)[1]
1242
+ end
1243
+
1244
+ ##
1245
+ # Find the common nodes, missing nodes, and remote heads.
1246
+ #
1247
+ # So in this code, we use opts[:base] and fetch as hashes
1248
+ # instead of arrays. We could very well use arrays, but hashes have
1249
+ # O(1) lookup time, and since these could get RFH (Really Fucking
1250
+ # Huge), we decided to take the liberty and just use hash for now.
1251
+ #
1252
+ # If opts[:base] (Hash) is specified, assume that these nodes and their parents
1253
+ # exist on the remote side and that no child of a node of base exists
1254
+ # in both remote and self.
1255
+ # Furthermore base will be updated to include the nodes that exists
1256
+ # in self and remote but no children exists in self and remote.
1257
+ # If a list of heads is specified, return only nodes which are heads
1258
+ # or ancestors of these heads.
1259
+ #
1260
+ # All the ancestors of base are in self and in remote.
1261
+ #
1262
+ # @param [Amp::Repository] remote the repository we're pulling from
1263
+ # @param [(Array<>, Array<>, Array<>)] the common nodes, missing nodes, and
1264
+ # remote heads
1265
+ def common_nodes(remote, opts={:heads => nil, :force => nil, :base => nil})
1266
+ # variable prep!
1267
+ node_map = changelog.node_map
1268
+ search = []
1269
+ unknown = []
1270
+ fetch = {}
1271
+ seen = {}
1272
+ seen_branch = {}
1273
+ opts[:base] ||= {}
1274
+ opts[:heads] ||= remote.heads
1275
+
1276
+ # if we've got nothing...
1277
+ if changelog.tip == NULL_ID
1278
+ opts[:base][NULL_ID] = true # 1 is stored in the Python
1279
+
1280
+ return [NULL_ID], [NULL_ID], opts[:heads].dup unless opts[:heads] == [NULL_ID]
1281
+ return [NULL_ID], [], [] # if we didn't trip ^, we're returning this
1282
+ end
1283
+
1284
+ # assume we're closer to the tip than the root
1285
+ # and start by examining heads
1286
+ UI::status 'searching for changes'
1287
+
1288
+ opts[:heads].each do |head|
1289
+ if !node_map.include?(head)
1290
+ unknown << head
1291
+ else
1292
+ opts[:base][head] = true # 1 is stored in the Python
1293
+ end
1294
+ end
1295
+
1296
+ opts[:heads] = unknown # the ol' switcheroo
1297
+ return opts[:base].keys, [], [] if unknown.empty? # BAIL
1298
+
1299
+ # make a hash with keys of unknown
1300
+ requests = Hash.with_keys unknown
1301
+ count = 0
1302
+ # Search through the remote branches
1303
+ # a branch here is a linear part of history, with 4 (four)
1304
+ # parts:
1305
+ #
1306
+ # head, root, first parent, second parent
1307
+ # (a branch always has two parents (or none) by definition)
1308
+ #
1309
+ # Here's where we start using the Hashes instead of Arrays
1310
+ # trick. Keep an eye out for opts[:base] and opts[:heads]!
1311
+ unknown = remote.branches(*unknown)
1312
+ until unknown.empty?
1313
+ r = []
1314
+
1315
+ while node = unknown.shift
1316
+ next if seen.include?(node[0])
1317
+ UI::debug "examining #{short node[0]}:#{short node[1]}"
1318
+
1319
+ if node[0] == NULL_ID
1320
+ # Do nothing...
1321
+ elsif seen_branch.include? node
1322
+ UI::debug 'branch already found'
1323
+ next
1324
+ elsif node_map.include? node[1]
1325
+ UI::debug "found incomplete branch #{short node[0]}:#{short node[1]}"
1326
+ search << node[0..1]
1327
+ seen_branch[node] = true # 1 in the python
1328
+ else
1329
+ unless seen.include?(node[1]) || fetch.include?(node[1])
1330
+ if node_map.include?(node[2]) and node_map.include?(node[3])
1331
+ UI::debug "found new changset #{short node[1]}"
1332
+ fetch[node[1]] = true # 1 in the python
1333
+ end # end if
1334
+
1335
+ node[2..3].each do |p|
1336
+ opts[:base][p] = true if node_map.include? p
1337
+ end
1338
+ end # end unless
1339
+
1340
+ node[2..3].each do |p|
1341
+ unless requests.include?(p) || node_map.include?(p)
1342
+ r << p
1343
+ requests[p] = true # 1 in the python
1344
+ end # end unless
1345
+ end # end each
1346
+ end # end if
1347
+
1348
+ seen[node[0]] = true # 1 in the python
1349
+ end # end while
1350
+
1351
+ unless r.empty?
1352
+ count += 1
1353
+
1354
+ UI::debug "request #{count}: #{r.map{|i| short i }}"
1355
+
1356
+ (0..(r.size-1)).step(10) do |p|
1357
+ remote.branches(r[p..(p+9)]).each do |b|
1358
+ UI::debug "received #{short b[0]}:#{short b[1]}"
1359
+ unknown << b
1360
+ end
1361
+ end
1362
+ end # end unless
1363
+ end # end until
1364
+
1365
+ # sorry for the ambiguous variable names
1366
+ # the python doesn't name them either, which
1367
+ # means I have no clue what these are
1368
+ find_proc = proc do |item1, item2|
1369
+ fetch[item1] = true
1370
+ opts[:base][item2] = true
1371
+ end
1372
+
1373
+ # do a binary search on the branches we found
1374
+ search, new_count = *binary_search(:find => search,
1375
+ :repo => remote,
1376
+ :node_map => node_map,
1377
+ :on_find => find_proc)
1378
+ count += new_count # keep keeping track of the total
1379
+
1380
+ # sanity check, because this method is sooooo fucking long
1381
+ fetch.keys.each do |f|
1382
+ if node_map.include? f
1383
+ raise RepoError.new("already have changeset #{short f[0..3]}")
1384
+ end
1385
+ end
1386
+
1387
+ if opts[:base].keys == [NULL_ID]
1388
+ if opts[:force]
1389
+ UI::warn 'repository is unrelated'
1390
+ else
1391
+ raise RepoError.new('repository is unrelated')
1392
+ end
1393
+ end
1394
+
1395
+ UI::debug "found new changesets starting at #{fetch.keys.map{|f| short f }.join ' '}"
1396
+ UI::debug "#{count} total queries"
1397
+
1398
+ # on with the show!
1399
+ [opts[:base].keys, fetch.keys, opts[:heads]]
1400
+ end
1401
+
1402
+ ##
1403
+ # Call the hooks that run under +call+
1404
+ #
1405
+ # @param [Symbol] call the location in the system where the hooks
1406
+ # are to be called
1407
+ def run_hook(call, opts={:throw => false})
1408
+ Hook.run_hook(call, opts)
1409
+ end
1410
+
1411
+ ##
1412
+ # Adds a list of file paths to the repository for the next commit.
1413
+ #
1414
+ # @param [String, Array<String>] paths the paths of the files we need to
1415
+ # add to the next commit
1416
+ # @return [Array<String>] which files WEREN'T added
1417
+ def add(*paths)
1418
+ lock_working do
1419
+ rejected = []
1420
+ paths.flatten!
1421
+
1422
+ paths.each do |file|
1423
+ path = working_join file
1424
+
1425
+ st = File.lstat(path) rescue nil
1426
+
1427
+ unless st
1428
+ UI.warn "#{file} does not exist!"
1429
+ rejected << file
1430
+ next
1431
+ end
1432
+
1433
+ if st.size > 10.mb
1434
+ UI.warn "#{file}: files over 10MB may cause memory and" +
1435
+ "performance problems\n" +
1436
+ "(use 'amp revert #{file}' to unadd the file)\n"
1437
+ end
1438
+
1439
+
1440
+ state = dirstate[file]
1441
+
1442
+
1443
+ if File.ftype(path) != 'file' && File.ftype(path) != 'link'
1444
+ # fail if it's not a file or link
1445
+ UI.warn "#{file} not added: only files and symlinks supported. Type is #{File.ftype path}"
1446
+ rejected << path
1447
+ elsif state.added? || state.modified? || state.normal?
1448
+ # fail if it's being tracked
1449
+ UI.warn "#{file} already tracked!"
1450
+ elsif state.removed?
1451
+ # check back on it if it's being removed
1452
+ dirstate.normal_lookup file
1453
+ else
1454
+ # else add it
1455
+ dirstate.add file
1456
+ #Amp::Logger.info("added #{file}")
1457
+ end
1458
+ end
1459
+
1460
+ dirstate.write unless rejected.size == paths.size
1461
+ return rejected
1462
+ end
1463
+ end
1464
+
1465
+ ##
1466
+ # Returns the number of revisions the repository is tracking.
1467
+ #
1468
+ # @return [Integer] how many revisions there have been
1469
+ def size
1470
+ changelog.size
1471
+ end
1472
+
1473
+ ##
1474
+ # Forgets an added file or files from the repository. Doesn't delete the
1475
+ # files, it just says "don't add this on the next commit."
1476
+ #
1477
+ # @param [Array, String] list a file path (or list of file paths) to
1478
+ # "forget".
1479
+ # @return [Boolean] success marker
1480
+ def forget(list)
1481
+ lock_working do
1482
+ list = [*list]
1483
+
1484
+ successful = list.any? do |f|
1485
+ if dirstate[f].status != :added
1486
+ UI.warn "#{f} not being added! can't forget it"
1487
+ false
1488
+ else
1489
+ dirstate.forget f
1490
+ true
1491
+ end
1492
+ end
1493
+
1494
+ dirstate.write if successful
1495
+ end
1496
+
1497
+ true
1498
+ end
1499
+
1500
+ ##
1501
+ # Removes the file (or files) from the repository. Marks them as removed
1502
+ # in the DirState, and if the :unlink option is provided, the files are
1503
+ # deleted from the filesystem.
1504
+ #
1505
+ # @param list the list of files. Could also just be 1 file as a string.
1506
+ # should be paths.
1507
+ # @param opts the options for this removal.
1508
+ # @option [Boolean] opts :unlink (false) whether or not to delete the
1509
+ # files from the filesystem after marking them as removed from the
1510
+ # DirState.
1511
+ # @return [Boolean] success?
1512
+ def remove(list, opts={})
1513
+ list = [*list]
1514
+
1515
+ # Should we delete the filez?
1516
+ if opts[:unlink]
1517
+ list.each do |f|
1518
+ ignore_missing_files do
1519
+ FileUtils.safe_unlink working_join(f)
1520
+ end
1521
+ end
1522
+ end
1523
+
1524
+ lock_working do
1525
+ # Save ourselves a dirstate write
1526
+ successful = list.any? do |f|
1527
+ if opts[:unlink] && File.exists?(working_join(f))
1528
+ # Uh, why is the file still there? Don't remove it from the dirstate
1529
+ UI.warn("#{f} still exists!")
1530
+ false # no success
1531
+ elsif dirstate[f].added?
1532
+ # Is it already added? if so, forgettaboutit
1533
+ dirstate.forget f
1534
+ #Amp::Logger.info("forgot #{f}")
1535
+ true # success!
1536
+ elsif !dirstate.tracking?(f)
1537
+ # Are we not even tracking this file? dumbass
1538
+ UI.warn("#{f} not being tracked!")
1539
+ false # no success
1540
+ else
1541
+ # Woooo we can delete it
1542
+ dirstate.remove f
1543
+ #Amp::Logger.info("removed #{f}")
1544
+ true
1545
+ end
1546
+ end
1547
+
1548
+ # Write 'em out boss
1549
+ dirstate.write if successful
1550
+ end
1551
+
1552
+ true
1553
+ end
1554
+
1555
+ ##
1556
+ # Returns the parents that aren't NULL_ID
1557
+ def living_parents
1558
+ dirstate.parents.select {|p| p != NULL_ID }
1559
+ end
1560
+
1561
+ ##
1562
+ # There are two ways to push to remote repo:
1563
+ #
1564
+ # addchangegroup assumes local user can lock remote
1565
+ # repo (local filesystem, old ssh servers).
1566
+ #
1567
+ # unbundle assumes local user cannot lock remote repo (new ssh
1568
+ # servers, http servers).
1569
+ def push(remote_repo, opts={:force => false, :revs => nil})
1570
+ if remote_repo.capable? "unbundle"
1571
+ push_unbundle remote_repo, opts
1572
+ else
1573
+ push_add_changegroup remote_repo, opts
1574
+ end
1575
+ end
1576
+
1577
+ ##
1578
+ # Push and add a changegroup
1579
+ # @todo -- add default values for +opts+
1580
+ def push_add_changegroup(remote, opts={})
1581
+ # no locking cuz we rockz
1582
+ ret = pre_push remote, opts
1583
+
1584
+ if ret[0]
1585
+ cg, remote_heads = *ret
1586
+ remote.add_changegroup cg, :push, url
1587
+ else
1588
+ ret[1]
1589
+ end
1590
+ end
1591
+
1592
+ ##
1593
+ # Push an unbundled dohickey
1594
+ # @todo -- add default values for +opts+
1595
+ def push_unbundle(remote, opts={})
1596
+ # local repo finds heads on server, finds out what revs it
1597
+ # must push. once revs transferred, if server finds it has
1598
+ # different heads (someone else won commit/push race), server
1599
+ # aborts.
1600
+
1601
+ ret = pre_push remote, opts
1602
+
1603
+ if ret[0]
1604
+ cg, remote_heads = *ret
1605
+ remote_heads = ['force'] if opts[:force]
1606
+ remote.unbundle cg, remote_heads, :push
1607
+ else
1608
+ ret[1]
1609
+ end
1610
+ end
1611
+
1612
+ ##
1613
+ # Return list of nodes that are roots of subsets not in remote
1614
+ #
1615
+ # If base dict is specified, assume that these nodes and their parents
1616
+ # exist on the remote side.
1617
+ # If a list of heads is specified, return only nodes which are heads
1618
+ # or ancestors of these heads, and return a second element which
1619
+ # contains all remote heads which get new children.
1620
+ def find_outgoing_roots(remote, opts={:base => nil, :heads => nil, :force => false})
1621
+ base, heads, force = opts[:base], opts[:heads], opts[:force]
1622
+ if base.nil?
1623
+ base = {}
1624
+ find_incoming_roots remote, :base => base, :heads => heads, :force => force
1625
+ end
1626
+
1627
+ UI::debug("common changesets up to "+base.keys.map {|k| k.short_hex}.join(" "))
1628
+
1629
+ remain = Hash.with_keys changelog.node_map.keys, nil
1630
+
1631
+ # prune everything remote has from the tree
1632
+ remain.delete NULL_ID
1633
+ remove = base.keys
1634
+ while remove.any?
1635
+ node = remove.shift
1636
+ if remain.include? node
1637
+ remain.delete node
1638
+ changelog.parents_for_node(node).each {|p| remove << p }
1639
+ end
1640
+ end
1641
+
1642
+ # find every node whose parents have been pruned
1643
+ subset = []
1644
+ # find every remote head that will get new children
1645
+ updated_heads = {}
1646
+ remain.keys.each do |n|
1647
+ p1, p2 = changelog.parents_for_node n
1648
+ subset << n unless remain.include?(p1) || remain.include?(p2)
1649
+ if heads && heads.any?
1650
+ updated_heads[p1] = true if heads.include? p1
1651
+ updated_heads[p2] = true if heads.include? p2
1652
+ end
1653
+ end
1654
+
1655
+ # this is the set of all roots we have to push
1656
+ if heads && heads.any?
1657
+ return subset, updated_heads.keys
1658
+ else
1659
+ return subset
1660
+ end
1661
+ end
1662
+
1663
+ ##
1664
+ # The branches available in this repository.
1665
+ #
1666
+ # @param [Array<String>] nodes the list of nodes. this can be optionally left empty
1667
+ # @return [Array<String>] the branches, active and inactive!
1668
+ def branches(*nodes)
1669
+ branches = []
1670
+ nodes = [changelog.tip] if nodes.empty?
1671
+
1672
+ # for each node, find its first parent (adam and eve, basically)
1673
+ # -- that's our branch!
1674
+ nodes.each do |node|
1675
+ t = node
1676
+ # traverse the tree, staying to the left side
1677
+ # node
1678
+ # / \
1679
+ # parent1 parent2
1680
+ # .... ....
1681
+ # This will get us the first parent. When it's finally NULL_ID,
1682
+ # we have a root -- this is the basis for our branch.
1683
+ loop do
1684
+ parents = changelog.parents_for_node t
1685
+ if parents[1] != NULL_ID || parents[0] == NULL_ID
1686
+ branches << [node, t, *parents]
1687
+ break
1688
+ end
1689
+ t = parents.first # get the first parent and start again
1690
+ end
1691
+ end
1692
+
1693
+ branches
1694
+ end
1695
+
1696
+ ##
1697
+ # Copies a file from +source+ to +destination+, while being careful of the
1698
+ # specified options. This method will perform all necessary file manipulation
1699
+ # and dirstate changes and so forth. Just give 'er a source and a destination.
1700
+ #
1701
+ # @param [String] source the path to the source file
1702
+ # @param [String] destination the path to the destination file
1703
+ # @param [Hash] opts the options for the copy
1704
+ # @option [Boolean] opts :after (false) should the file be deleted?
1705
+ # @return [Boolean] success?
1706
+ def copy(source, destination, opts)
1707
+ # Traverse repository subdirectories
1708
+ src = relative_join source
1709
+ target = relative_join destination
1710
+
1711
+ # Is there a tracked file at our destination? If so, get its state.
1712
+ state = dirstate[target].status
1713
+ # abstarget is the full path to the target. Needed for system calls
1714
+ # (just to be safe)
1715
+ abstarget = working_join target
1716
+
1717
+ # If true, we're copying into a directory, so be smart about it.
1718
+ if File.directory? abstarget
1719
+ abstarget = File.join abstarget, File.basename(src)
1720
+ target = File.join target, File.basename(src)
1721
+ end
1722
+ abssrc = working_join(src)
1723
+
1724
+
1725
+ exists = File.exist? abstarget
1726
+ # If the file's there, and we aren't forcing the copy, then we should let
1727
+ # the user know they might overwrite an existing file in the repo.
1728
+ if (!opts[:after] && exists || opts[:after] && [:merged, :normal].include?(state))
1729
+ unless opts[:force]
1730
+ Amp::UI.warn "#{target} not overwriting, file exists"
1731
+ return false
1732
+ end
1733
+ end
1734
+
1735
+ return if opts[:after] && !exists
1736
+ unless opts[:"dry-run"]
1737
+ # Performs actual file copy from one locatino to another.
1738
+ # Overwrites file if it's there.
1739
+ begin
1740
+ File.safe_unlink(abstarget) if exists
1741
+
1742
+ target_dir = File.dirname abstarget
1743
+ File.makedirs target_dir unless File.directory? target_dir
1744
+ File.copy(abssrc, abstarget)
1745
+ rescue Errno::ENOENT
1746
+ # This happens if the file has been deleted between the check up above
1747
+ # (exists = File.exist? abstarget) and the call to File.safe_unlink.
1748
+ Amp::UI.warn("#{target}: deleted in working copy in the last 2 microseconds")
1749
+ rescue StandardError => e
1750
+ Amp::UI.warn("#{target} - cannot copy: #{e}")
1751
+ return false
1752
+ end
1753
+ end
1754
+
1755
+ # Be nice and give the user some output
1756
+ if opts[:verbose] || opts[:"dry-run"]
1757
+ action = opts[:rename] ? "moving" : "copying"
1758
+ Amp::UI.status("#{action} #{src} to #{target}")
1759
+ end
1760
+ return false if opts[:"dry-run"]
1761
+
1762
+ # in case the source of the copy is marked as the destination of a
1763
+ # different copy (that hasn't yet been committed either), we should
1764
+ # do some extra handling
1765
+ origsrc = dirstate.copy_map[src] || src
1766
+ if target == origsrc
1767
+ # We're copying back to our original location! D'oh.
1768
+ unless [:merged, :normal].include?(state)
1769
+ dirstate.maybe_dirty target
1770
+ end
1771
+ else
1772
+ if dirstate[origsrc].added? && origsrc == src
1773
+ # we copying an added (but uncommitted) file?
1774
+ UI.warn("#{origsrc} has not been committed yet, so no copy data" +
1775
+ "will be stored for #{target}")
1776
+ if [:untracked, :removed].include?(dirstate[target].status)
1777
+ add [target]
1778
+ end
1779
+ else
1780
+ dirstate_copy src, target
1781
+ end
1782
+ end
1783
+
1784
+ # Clean up if we're doing a move, and not a copy.
1785
+ remove([src], :unlink => !(opts[:after])) if opts[:rename]
1786
+ end
1787
+
1788
+ ##
1789
+ # Copy a file from +source+ to +dest+. Really simple, peeps.
1790
+ # The reason this shit is even *slightly* complicated because
1791
+ # it deals with file types. Otherwise I could write this
1792
+ # in, what, 3 lines?
1793
+ #
1794
+ # @param [String] source the from
1795
+ # @param [String] dest the to
1796
+ def dirstate_copy(source, dest)
1797
+ path = working_join dest
1798
+
1799
+ if !File.exist?(path) || File.ftype(path) == 'link'
1800
+ UI::warn "#{dest} doesn't exist!"
1801
+ elsif not (File.ftype(path) == 'file' || File.ftype(path) == 'link')
1802
+ UI::warn "copy failed: #{dest} is neither a file nor a symlink"
1803
+ else
1804
+ lock_working do
1805
+ # HOME FREE!!!!!!! i love getting out of school before noon :-D
1806
+ # add it if it makes sense (like it was previously removed or untracked)
1807
+ # and then copy da hoe
1808
+ state = dirstate[dest].status
1809
+ dirstate.add dest if [:untracked, :removed].include?(state)
1810
+ dirstate.copy source => dest
1811
+ dirstate.write
1812
+
1813
+ #Amp::Logger.info("copy #{source} -> #{dest}")
1814
+ end
1815
+ end
1816
+ end
1817
+
1818
+ ##
1819
+ # Undelete a file. For instance, if you remove something and then
1820
+ # find out that you NEED that file, you can use this command.
1821
+ #
1822
+ # @param [[String]] list the files to be undeleted
1823
+ def undelete(list)
1824
+ manifests = living_parents.map do |p|
1825
+ manifest.read changelog.read(p).first
1826
+ end
1827
+
1828
+ # now we actually restore the files
1829
+ list.each do |file|
1830
+ unless dirstate[file].removed?
1831
+ UI.warn "#{file} isn't being removed!"
1832
+ else
1833
+ m = manifests[0] || manifests[1]
1834
+ data = file(f).read m[f]
1835
+ add_file file, data, m.flags(f) # add_file is wwrite in the python
1836
+ dirstate.normal f # we know it's clean, we just restored it
1837
+ end
1838
+ end
1839
+ end
1840
+ alias_method :restore, :undelete
1841
+
1842
+ ##
1843
+ # Write data to a file in the CODE repo, not the .hg
1844
+ #
1845
+ # @param [String] file_name
1846
+ # @param [String] data (no trailing newlines are appended)
1847
+ # @param [[String]] flags we're really just looking for links
1848
+ # and executables, here
1849
+ def add_file(file_name, data, flags)
1850
+ data = filter "decode", file_name, data
1851
+ path = "#{@root}/#{file_name}"
1852
+
1853
+ File.unlink path rescue nil
1854
+
1855
+ if flags.include? 'l' # if it's a link
1856
+ @file_opener.symlink path, data
1857
+ else
1858
+ @file_opener.open(path, 'w') {|f| f.write data }
1859
+ File.set_flag path, false, true if flags.include? 'x'
1860
+ end
1861
+ end
1862
+
1863
+ ##
1864
+ # Returns the node_id's of the heads of the repository.
1865
+ def heads(start=nil, options={:closed => true})
1866
+ heads = changelog.heads(start)
1867
+ should_show = lambda do |head|
1868
+ return true if options[:closed]
1869
+
1870
+ extras = changelog.read(head)[5]
1871
+ return !(extras["close"])
1872
+ end
1873
+ heads = heads.select {|h| should_show[h] }
1874
+ heads.map! {|h| [changelog.rev(h), h] }
1875
+ heads.sort! {|arr1, arr2| arr2[0] <=> arr1[0] }
1876
+ heads.map! {|r, n| n}
1877
+ end
1878
+
1879
+ ##
1880
+ # Walk recursively through the directory tree (or a changeset)
1881
+ # finding all files matched by the match function
1882
+ #
1883
+ # @param [String, Integer] node selects which changeset to walk
1884
+ # @param [Amp::Match] match the matcher decides how to pick the files
1885
+ # @param [Array<String>] an array of filenames
1886
+ def walk(node=nil, match = Match.create({}) { true })
1887
+ self[node].walk(match) # calls Changeset#walk
1888
+ end
1889
+
1890
+ ##
1891
+ # Returns the requested file at the given revision annotated by
1892
+ # line number, so you can see who committed which lines in the file's
1893
+ # history.
1894
+ #
1895
+ # @param file The name of the file to annotate
1896
+ # @param [Integer, String] rev (nil) The revision to look at for
1897
+ # annotation
1898
+ def annotate(file, revision=nil, opts={})
1899
+ changeset = self[revision]
1900
+ file = changeset.get_file(file)
1901
+ return file.annotate(opts[:follow_copies], opts[:line_numbers])
1902
+ end
1903
+
1904
+ ##
1905
+ # This gives the status of the repository, comparing 2 node in
1906
+ # its history. Now, with no parameters, it's going to compare the
1907
+ # last revision with the working directory, which is the most common
1908
+ # usage - that answers "what is the current status of the repository,
1909
+ # compared to the last time a commit happened?". However, given any
1910
+ # two revisions, it can compare them.
1911
+ #
1912
+ # @example @repo.status # => {:unknown => ['code/smthng.rb'], :added => [], ...}
1913
+ # @param [Hash] opts the options for this command. there's a bunch.
1914
+ # @option [String, Integer] opts :node_1 (".") an identifier for the starting
1915
+ # revision
1916
+ # @option [String, Integer] opts :node_2 (nil) an identifier for the ending
1917
+ # revision. Defaults to the working directory.
1918
+ # @option [Proc] opts :match (proc { true }) a proc that will match
1919
+ # a file, so we know if we're interested in it.
1920
+ # @option [Boolean] opts :ignored (false) do we want to see files we're
1921
+ # ignoring?
1922
+ # @option [Boolean] opts :clean (false) do we want to see files that are
1923
+ # totally unchanged?
1924
+ # @option [Boolean] opts :unknown (false) do we want to see files we've
1925
+ # never seen before (i.e. files the user forgot to add to the repo)?
1926
+ # @return [Hash<Symbol => Array<String>>] no, I'm not kidding. the keys are:
1927
+ # :modified, :added, :removed, :deleted, :unknown, :ignored, :clean. The
1928
+ # keys are the type of change, and the values are arrays of filenames
1929
+ # (local to the root) that are under each key.
1930
+ def status(opts={:node_1 => '.'})
1931
+ run_hook :status
1932
+
1933
+ node1, node2, match = opts[:node_1], opts[:node_2], opts[:match]
1934
+
1935
+ match = Match.create({}) { true } unless match
1936
+
1937
+ node1 = self[node1] unless node1.kind_of? Changeset # get changeset objects
1938
+ node2 = self[node2] unless node2.kind_of? Changeset
1939
+
1940
+ write_dirstate = false
1941
+
1942
+ # are we working with working directories?
1943
+ working = node2.revision == nil
1944
+ parent_working = working && node1 == self["."]
1945
+
1946
+ # load the working directory's manifest
1947
+ node2.manifest if !working && node2.revision < node1.revision
1948
+
1949
+ if working
1950
+ # get the dirstate's latest status
1951
+ status = dirstate.status(opts[:ignored], opts[:clean], opts[:unknown], match)
1952
+
1953
+ # this case is run about 99% of the time
1954
+ # do we need to do hashes on any files to see if they've changed?
1955
+ if parent_working && status[:lookup].any?
1956
+ # lookup.any? is a shortcut for !lookup.empty?
1957
+ clean, modified, write_dirstate = *fix_files(status[:lookup], node1, node2)
1958
+
1959
+ status[:clean] += clean
1960
+ status[:modified] += modified
1961
+ end
1962
+ else
1963
+ status = {:clean => [], :modified => [], :lookup => [], :unknown => [], :ignored => [],
1964
+ :removed => [], :added => [], :deleted => []}
1965
+ end
1966
+ # if we're working with old revisions...
1967
+ unless parent_working
1968
+ # get the older revision manifest
1969
+ mf1 = node1.manifest.dup
1970
+
1971
+ if working
1972
+ # get the working directory manifest. note, it's a tweaked
1973
+ # manifest to reflect working directory files
1974
+ mf2 = self["."].manifest.dup
1975
+
1976
+ # mark them as not in the manifest to force checking later
1977
+ files_for_later = status[:lookup] + status[:modified] + status[:added]
1978
+ files_for_later.each {|file| mf2.mark_for_later file, node2 }
1979
+
1980
+ # remove any files we've marked as removed them from the '.' manifest
1981
+ status[:removed].each {|file| mf2.delete file }
1982
+ else
1983
+ # if we aren't working with the working directory, then we'll
1984
+ # just use the old revision's information
1985
+ status[:removed], status[:unknown], status[:ignored] = [], [], []
1986
+ mf2 = node2.manifest.dup
1987
+ end
1988
+
1989
+ # Every file in the later revision (or working directory)
1990
+ mf2.each do |file, node|
1991
+ # Does it exist in the old manifest? If so, it wasn't added.
1992
+ if mf1[file]
1993
+ # the tests to run
1994
+ tests = [ mf1.flags[file] != mf2.flags[file] ,
1995
+ mf1[file] != mf2[file] &&
1996
+ (mf2[file] || node1[file] === node2[file]) ]
1997
+
1998
+ # It's in the old manifest, so lets check if its been changed
1999
+ # Else, it must be unchanged
2000
+ if tests.any?
2001
+ status[:modified] << file
2002
+ status[:clean] << file if opts[:clean]
2003
+ end
2004
+
2005
+ # Remove that file from the old manifest, since we've checked it
2006
+ mf1.delete file
2007
+ else
2008
+ # if it's not in the old manifest, it's been added
2009
+ status[:added] << file
2010
+ end
2011
+ end
2012
+
2013
+ # Anything left in the old manifest is a file we've removed since the
2014
+ # first revision.
2015
+ status[:removed] = mf1.keys
2016
+ end
2017
+
2018
+ # We're done!
2019
+ status.delete :lookup # because nobody cares about it
2020
+ delta = status.delete :delta
2021
+
2022
+ status.map {|k, v| [k, v.sort] }.to_hash # sort dem fuckers
2023
+ status[:delta] = delta
2024
+ status.select {|k, _| opts[:only] ? opts[:only].include?(k) : true }.to_hash
2025
+ end
2026
+
2027
+ ##
2028
+ # Clone a repository.
2029
+ #
2030
+ # Here is what this does, pretty much:
2031
+ # % amp init monkey
2032
+ # % cd monkey
2033
+ # % amp pull http://monkey
2034
+ #
2035
+ # It's so simple it's not even funny.
2036
+ #
2037
+ # @param [Amp::Repository] remote repository to pull from
2038
+ # @param [Array<String>] heads list of revs to clone (forces use of pull)
2039
+ # @param [Boolean] stream do we stream from the remote source?
2040
+ def clone(remote, opts={:revs => [], :stream => false})
2041
+ # now, all clients that can request uncompressed clones can
2042
+ # read repo formats supported by all servers that can serve
2043
+ # them.
2044
+
2045
+ # The streaming case:
2046
+ # if revlog format changes, client will have to check version
2047
+ # and format flags on "stream" capability, and use
2048
+ # uncompressed only if compatible.
2049
+ if opts[:stream] && opts[:revs].any? && remote.capable?('stream')
2050
+ stream_in remote
2051
+ else
2052
+ pull remote, :revs => opts[:revs]
2053
+ end
2054
+ end
2055
+
2056
+ ##
2057
+ # Stream in the data from +remote+.
2058
+ #
2059
+ # @param [Amp::Repository] remote repository to pull from
2060
+ # @return [Integer] the number of heads in the repository minus 1
2061
+ def stream_in(remote)
2062
+ remote.stream_out do |f|
2063
+ l = f.gets # this should be the server code
2064
+
2065
+ unless Integer(l)
2066
+ raise ResponseError.new("Unexpected response from server: #{l}")
2067
+ end
2068
+
2069
+ case l.to_i
2070
+ when 1
2071
+ raise RepoError.new("operation forbidden by server")
2072
+ when 2
2073
+ raise RepoError.new("locking the remote repository failed")
2074
+ end
2075
+
2076
+ UI::status "streaming all changes"
2077
+
2078
+ l = f.gets # this is effectively [total_files, total_bytes].join ' '
2079
+ total_files, total_bytes = *l.split(' ').map {|i| i.to_i }[0..1]
2080
+ UI::status "#{total_files} file#{total_files == 1 ? '' : 's' } to transfer, #{total_bytes.to_human} of data"
2081
+
2082
+ start = Time.now
2083
+ total_files.times do |i|
2084
+ l = f.gets
2085
+ name, size = *l.split("\0")[0..1]
2086
+ size = size.to_i
2087
+ UI::debug "adding #{name} (#{size.to_human})"
2088
+
2089
+ @store.opener.open do |store_file|
2090
+ chunk = f.read size # will return nil if at EOF
2091
+ store_file.write chunk if chunk
2092
+ end
2093
+ end
2094
+
2095
+ elapsed = Time.now - start
2096
+ elapsed = 0.001 if elapsed <= 0
2097
+
2098
+ UI::status("transferred #{total_bytes.to_human} in #{elapsed}" +
2099
+ "second#{elapsed == 1.0 ? '' : 's' } (#{total_bytes.to_f / elapsed}/sec)")
2100
+
2101
+ invalidate!
2102
+ heads.size - 1
2103
+ end
2104
+ end
2105
+
2106
+ ##
2107
+ # Invalidate the repository: delete things and reset others.
2108
+ def invalidate!
2109
+ @changelog = nil
2110
+ @manifest = nil
2111
+
2112
+ invalidate_tag_cache!
2113
+ invalidate_branch_cache!
2114
+ end
2115
+
2116
+ ##
2117
+ # Commits a changeset or set of files to the repository. You will quite often
2118
+ # use this method since it's basically the basis of version control systems.
2119
+ #
2120
+ # @param [Hash] opts the options to this method are all optional, so it's a very
2121
+ # flexible method. Options listed below.
2122
+ # @option [Array] opts :files ([]) the specific files to commit - if this is
2123
+ # not provided, the current status of the working directory is used.
2124
+ # @option [Hash] opts :extra ({}) any extra data, such as "close" => true
2125
+ # will close the active branch.
2126
+ # @option [String] opts :message ("") the message for the commit. An editor
2127
+ # will be opened if this is not provided.
2128
+ # @option [Boolean] opts :force (false) Forces the commit, ignoring minor details
2129
+ # like when you try to commit when no files have been changed.
2130
+ # @option [Match] opts :match (nil) A match object to specify how to pick files
2131
+ # to commit. These are useful so you don't accidentally commit ignored files,
2132
+ # for example.
2133
+ # @option [Boolean] opts :empty_ok (false) Is an empty commit message a-ok?
2134
+ # @option [Boolean] opts :force_editor (false) Do we force the editor to be
2135
+ # opened, even if :message is provided?
2136
+ # @option [String] opts :user ($USER) the username to associate with the commit.
2137
+ # Defaults to AmpConfig#username.
2138
+ # @option [DateTime, Time, Date] opts :date (Time.now) the date to mark with
2139
+ # the commit. Useful if you miss a deadline and want to pretend that you actually
2140
+ # made it!
2141
+ # @return [String] the digest referring to this entry in the revlog
2142
+ def commit(opts={:message => "", :extra => {}, :files => []})
2143
+ opts[:extra] ||= {}
2144
+ opts[:force] = true if opts[:extra]["close"]
2145
+ opts[:files] ||= []
2146
+ opts[:files].uniq!
2147
+
2148
+ use_dirstate = opts[:p1] == nil
2149
+ changes = {}
2150
+ lock_working_and_store do
2151
+ if use_dirstate
2152
+ p1, p2 = dirstate.parents
2153
+ update_dirstate = true
2154
+
2155
+ tests = [opts[:force] ,
2156
+ p2 != NULL_ID,
2157
+ opts[:match] ]
2158
+
2159
+ raise StandardError("cannot partially commit a merge") if tests.all?
2160
+
2161
+ if opts[:files].any?
2162
+ changes = {:modified => [], :removed => []}
2163
+
2164
+ # split the files up so we can deal with them appropriately
2165
+ opts[:files].each do |file|
2166
+ state = dirstate[file]
2167
+ if state.normal? || state.merged? || state.added?
2168
+ changes[:modified] << file
2169
+ elsif state.removed?
2170
+ changes[:removed] << file
2171
+ elsif state.untracked?
2172
+ UI.warn "#{file} not tracked!"
2173
+ else
2174
+ UI.err "#{file} has unknown state #{state[0]}"
2175
+ end
2176
+ end
2177
+
2178
+ else
2179
+ changes = status(:match => opts[:match])
2180
+ end
2181
+ else
2182
+ p1, p2 = opts[:p1], (opts[:p2] || NULL_ID)
2183
+ update_dirstate = dirstate.parents[0] == p1
2184
+ changes = {:modified => files}
2185
+ end
2186
+
2187
+
2188
+ merge_state = Amp::Merges::MergeState.new self # merge state!
2189
+
2190
+ changes[:modified].each do |file|
2191
+ if merge_state[file] && merge_state[file] == "u"
2192
+ raise StandardError.new("unresolved merge conflicts (see `amp resolve`)")
2193
+ end
2194
+ end
2195
+
2196
+ changeset = WorkingDirectoryChangeset.new self, :parents => [p1, p2] ,
2197
+ :text => opts[:message],
2198
+ :user => opts[:user] ,
2199
+ :date => opts[:date] ,
2200
+ :extra => opts[:extra] ,
2201
+ :changes => changes
2202
+
2203
+ revision = commit_changeset changeset, :force => opts[:force] ,
2204
+ :force_editor => opts[:force_editor],
2205
+ :empty_ok => opts[:empty_ok] ,
2206
+ :use_dirstate => use_dirstate ,
2207
+ :update_dirstate => update_dirstate
2208
+
2209
+ merge_state.reset
2210
+ return revision
2211
+ end
2212
+ end
2213
+
2214
+ ##
2215
+ # Commits the given changeset to the repository.
2216
+ #
2217
+ # @param changeset the changeset to commit. Could be working dir, for
2218
+ # example.
2219
+ # @param opts the options for committing the changeset.
2220
+ # @option [Boolean] opts :force (false) force the commit, even though
2221
+ # nothing has changed.
2222
+ # @option [Boolean] opts :force_editor (false) force the user to open
2223
+ # their editor, even though they provided a message already
2224
+ # @option [Boolean] opts :empty_ok (false) is it ok if they have no
2225
+ # description of the commit?
2226
+ # @option [Boolean] opts :use_dirstate (true) use the DirState for this
2227
+ # commit? Used if you're committing the working directory (typical)
2228
+ # @option [Boolean] opts :update_dirstate (true) should we update the
2229
+ # DirState after the commit? Used if you're committing the working
2230
+ # directory.
2231
+ # @return [String] the digest referring to this entry in the revlog
2232
+ def commit_changeset(changeset, opts = {:use_dirstate => true,
2233
+ :update_dirstate => true})
2234
+ journal = nil
2235
+ valid = false #don't update the DirState if this is set!
2236
+
2237
+ commit = ((changeset.modified || []) + (changeset.added || [])).sort
2238
+ remove = changeset.removed
2239
+ extra = changeset.extra.dup
2240
+ branchname = extra["branch"]
2241
+ user = changeset.user
2242
+ text = changeset.description
2243
+
2244
+ p1, p2 = changeset.parents.map {|p| p.node}
2245
+ c1 = changelog.read(p1) # 1 parent's changeset as an array
2246
+ c2 = changelog.read(p2) # 2nd parent's changeset as an array
2247
+ m1 = manifest.read(c1[0]).dup # 1st parent's manifest
2248
+ m2 = manifest.read(c2[0]) # 2nd parent's manifest
2249
+
2250
+ if opts[:use_dirstate]
2251
+ oldname = c1[5]["branch"]
2252
+ tests = [ commit.empty?, remove.empty?, ! opts[:force],
2253
+ p2 == NULL_ID, branchname = oldname ]
2254
+
2255
+ if tests.all?
2256
+ UI::status "nothing changed"
2257
+ return nil
2258
+ end
2259
+ end
2260
+
2261
+ xp1 = p1.hexlify
2262
+ xp2 = (p2 == NULL_ID) ? "" : p2.hexlify
2263
+
2264
+ run_hook :pre_commit
2265
+ journal = Journal.new
2266
+
2267
+ fresh = {} # new = reserved haha
2268
+ changed = []
2269
+ link_rev = self.size
2270
+
2271
+ (commit + (remove || [])).each {|file| UI::status file }
2272
+
2273
+ #Amp::Logger.info("<changeset commit>").indent
2274
+
2275
+ commit.each do |file|
2276
+ # begin
2277
+
2278
+ versioned_file = changeset.get_file(file)
2279
+ newflags = versioned_file.flags
2280
+
2281
+ fresh[file] = commit_file(versioned_file, m1, m2, link_rev,
2282
+ journal, changed)
2283
+ if [ changed.empty? || changed.last != file,
2284
+ m2[file] != fresh[file] ].all?
2285
+ changed << file if m1.flags[file] != newflags
2286
+ end
2287
+ m1.flags[file] = newflags
2288
+
2289
+ dirstate.normal file if opts[:use_dirstate]
2290
+ #Amp::Logger.section("committing: #{file}") do
2291
+ #Amp::Logger.info("flags: #{newflags.inspect}")
2292
+ #Amp::Logger.info("total changes: #{changed.inspect}")
2293
+ #end
2294
+ # rescue
2295
+ # if opts[:use_dirstate]
2296
+ # UI.warn("trouble committing #{file}")
2297
+ # raise
2298
+ # else
2299
+ # remove << file
2300
+ # end
2301
+ # end
2302
+ end
2303
+
2304
+ updated, added = [], []
2305
+ changed.sort.each do |file|
2306
+ if m1[file] || m2[file]
2307
+ updated << file
2308
+ else
2309
+ added << file
2310
+ end
2311
+ end
2312
+
2313
+ m1.merge!(fresh)
2314
+
2315
+ removed = remove.sort.select {|f| m1[f] || m2[f]}
2316
+ removed_1 = []
2317
+ removed.select {|f| m1[f]}.each do |f|
2318
+ m1.delete f
2319
+ removed_1 << f
2320
+ #Amp::Logger.info("Removed: #{f}")
2321
+ end
2322
+
2323
+ fresh = fresh.map {|k, v| (v) ? k : nil}.reject {|k| k.nil? }
2324
+ man_entry = manifest.add(m1, journal, link_rev, c1[0], c2[0],
2325
+ [fresh, removed_1])
2326
+ #Amp::Logger.info("Adding/modifying: #{fresh.inspect}")
2327
+ #Amp::Logger.info("Removing: #{removed_1.inspect}")
2328
+ #Amp::Logger.section("New Manifest") do
2329
+ #manifest.read(:tip).each do |file, _|
2330
+ #Amp::Logger.info(file)
2331
+ #end
2332
+ #end
2333
+ if !opts[:empty_ok] && !text
2334
+ template_opts = {:added => added, :updated => updated,
2335
+ :removed => removed, :template_type => :commit }
2336
+ edit_text = changeset.to_templated_s(template_opts)
2337
+ text = UI.edit(edit_text, user)
2338
+ end
2339
+
2340
+ lines = text.rstrip.split("\n").map {|r| r.rstrip}.reject {|l| l.empty?}
2341
+ if lines.empty? && opts[:use_dirstate]
2342
+ raise abort("empty commit message")
2343
+ end
2344
+ text = lines.join("\n")
2345
+
2346
+ changelog.delay_update
2347
+ n = changelog.add(man_entry, changed + removed_1, text, journal, p1, p2, user,
2348
+ changeset.date, extra)
2349
+ #Amp::Logger.section("changelog") do
2350
+ #Amp::Logger.info("manifest entry: #{man_entry.inspect}")
2351
+ #Amp::Logger.info("files: #{(changed + removed_1).inspect}")
2352
+ #Amp::Logger.info("text: #{text.inspect}")
2353
+ #Amp::Logger.info("p1: #{p1.inspect}")
2354
+ #Amp::Logger.info("p2: #{p2.inspect}")
2355
+ #Amp::Logger.info("user: #{user.inspect}")
2356
+ #Amp::Logger.info("date: #{changeset.date.inspect}")
2357
+ #Amp::Logger.info("extra: #{extra.inspect}")
2358
+ #end
2359
+ self.changelog.write_pending()
2360
+ changelog.finalize(journal)
2361
+ #Amp::Logger.outdent.info("</changeset commit>")
2362
+ # branchtags
2363
+
2364
+ if opts[:use_dirstate] || opts[:update_dirstate]
2365
+ dirstate.parents = n
2366
+ removed.each {|f| dirstate.forget(f) } if opts[:use_dirstate]
2367
+ dirstate.write
2368
+ end
2369
+
2370
+ valid = true
2371
+ journal.close
2372
+ run_hook :post_commit, :added => added, :modified => updated, :removed => removed,
2373
+ :user => user, :date => changeset.date, :text => text,
2374
+ :revision => changelog.index_size
2375
+ return n
2376
+ rescue StandardError => e
2377
+ if !valid
2378
+ dirstate.invalidate!
2379
+ end
2380
+ if e.kind_of?(AbortError)
2381
+ UI::warn "Abort: #{e}"
2382
+ else
2383
+ UI::warn "Got exception while committing. #{e}"
2384
+ UI::warn e.backtrace.join("\n")
2385
+ end
2386
+ journal.delete if journal
2387
+ end
2388
+
2389
+
2390
+ ##
2391
+ # Commits a file as part of a larger transaction.
2392
+ #
2393
+ # @param file the versioned-file to commit
2394
+ # @param manifest1 the manifest of the first parent
2395
+ # @param manifest2 the manifest of the second parent
2396
+ # @param link_revision the revision index we'll be adding this under
2397
+ # @param journal the journal for aborting failed commits
2398
+ # @param change_list the list of all the files changed during the commit
2399
+ #
2400
+ def commit_file(file, manifest1, manifest2, link_revision, journal, change_list)
2401
+ filename = file.path
2402
+ text = file.data
2403
+ curfile = self.file filename
2404
+
2405
+ fp1 = manifest1[filename] || NULL_ID
2406
+ fp2 = manifest2[filename] || NULL_ID
2407
+
2408
+ metadata = {}
2409
+ copied = file.renamed
2410
+ if copied && copied[0] != filename
2411
+ # Mark the new revision of this file as a copy of another
2412
+ # file. This copy data will effectively act as a parent
2413
+ # of this new revision. If this is a merge, the first
2414
+ # parent will be the nullid (meaning "look up the copy data")
2415
+ # and the second one will be the other parent. For example:
2416
+ #
2417
+ # 0 --- 1 --- 3 rev1 changes file foo
2418
+ # \ / rev2 renames foo to bar and changes it
2419
+ # \- 2 -/ rev3 should have bar with all changes and
2420
+ # should record that bar descends from
2421
+ # bar in rev2 and foo in rev1
2422
+ #
2423
+ # this allows this merge to succeed:
2424
+ #
2425
+ # 0 --- 1 --- 3 rev4 reverts the content change from rev2
2426
+ # \ / merging rev3 and rev4 should use bar@rev2
2427
+ # \- 2 --- 4 as the merge base
2428
+
2429
+ copied_file = copied[0]
2430
+ copied_revision = manifest1[copied_file]
2431
+ new_fp = fp2
2432
+
2433
+ if manifest2 # branch merge
2434
+ if fp2 == NULL_ID || copied_revision == nil # copied on remote side
2435
+ if manifest2[copied_file]
2436
+ copied_revision = manifest2[copied_file]
2437
+ new_fp = fp1
2438
+ end
2439
+ end
2440
+ end
2441
+
2442
+ if copied_revision.nil? || copied_revision.empty?
2443
+ self["."].ancestors.each do |a|
2444
+ if a[copied_file]
2445
+ copied_revision = a[copied_file].file_node
2446
+ break
2447
+ end
2448
+ end
2449
+ end
2450
+
2451
+ UI::say "#{filename}: copy #{copied_file}:#{copied_revision.hexlify}"
2452
+ metadata["copy"] = copied_file
2453
+ metadata["copyrev"] = copied_revision.hexlify
2454
+ fp1, fp2 = NULL_ID, new_fp
2455
+ elsif fp2 != NULL_ID
2456
+ fpa = curfile.ancestor(fp1, fp2)
2457
+
2458
+ fp1, fp2 = fp2, NULL_ID if fpa == fp1
2459
+ fp2 = NULL_ID if fpa != fp2 && fpa == fp2
2460
+ end
2461
+
2462
+ if fp2 == NULL_ID && !(curfile.cmp(fp1, text)) && metadata.empty?
2463
+ return fp1
2464
+ end
2465
+
2466
+ change_list << filename
2467
+ return curfile.add(text, metadata, journal, link_revision, fp1, fp2)
2468
+ end
2469
+
2470
+ private
2471
+
2472
+ ##
2473
+ # Make the dummy changelog at .hg/00changelog.i
2474
+ def make_changelog
2475
+ @hg_opener.open "00changelog.i", "w" do |file|
2476
+ file.write "\0\0\0\2" # represents revlogv2
2477
+ file.write " dummy changelog to avoid using the old repo type"
2478
+ end
2479
+ end
2480
+
2481
+ ##
2482
+ # Write the requirements file. This returns the requirements passed
2483
+ # so that it can be the final method call in #init
2484
+ def write_requires(requirements)
2485
+ @hg_opener.open "requires", "w" do |require_file|
2486
+ requirements.each {|r| require_file.puts r }
2487
+ end
2488
+ requirements
2489
+ end
2490
+
2491
+ ##
2492
+ # Look up the files in +lookup+ to make sure
2493
+ # they're either the same or not. Normally, we can
2494
+ # just tell if two files are the same by looking at their sizes. But
2495
+ # sometimes, we can't! That's where this method comes into play; it
2496
+ # hashes the files to verify integrity.
2497
+ #
2498
+ # @param [String] lookup files to look up
2499
+ # @param node1
2500
+ # @param node2
2501
+ # @return [[String], [String], Boolean] clean files, modified files, and
2502
+ # whether or not to write the dirstate
2503
+ def fix_files(lookup, node1, node2)
2504
+ write_dirstate = false # this gets returned
2505
+ modified = [] # and this
2506
+ fixup = [] # fixup are files that haven't changed so they're being
2507
+ # marked wrong in the dirstate. this gets returned
2508
+
2509
+ lookup.each do |file|
2510
+ # this checks to see if the file has been modified after doing
2511
+ # hashes/flag checks
2512
+ tests = [ node1.include?(file) ,
2513
+ node2.flags(file) == node1.flags(file) ,
2514
+ node1[file] === node2[file] ]
2515
+
2516
+ unless tests.all?
2517
+ modified << file
2518
+ else
2519
+ fixup << file # mark the file as clean
2520
+ end
2521
+ end
2522
+
2523
+
2524
+ # mark every fixup'd file as clean in the dirstate
2525
+ begin
2526
+ lock_working do
2527
+ fixup.each do |file|
2528
+ write_dirstate = true
2529
+ dirstate.normal file
2530
+ modified.delete file
2531
+ end
2532
+ end
2533
+ rescue LockError
2534
+ end
2535
+ dirstate.write if write_dirstate
2536
+
2537
+ # the fixups are actually clean
2538
+ [fixup, modified, write_dirstate]
2539
+ end
2540
+
2541
+ ##
2542
+ # do a binary search
2543
+ # used by common_nodes
2544
+ #
2545
+ # Hash info!
2546
+ # :find => the stuff we're searching through
2547
+ # :on_find => what to do when we've got something new
2548
+ # :repo => usually the remote repo where we get new info from
2549
+ # :node_map => the nodes in the current changelog
2550
+ def binary_search(opts={})
2551
+ # I have a lot of stuff to do for scouts
2552
+ # but instead i'm doing this
2553
+ # hizzah!
2554
+ count = 0
2555
+
2556
+ until opts[:find].empty?
2557
+ new_search = []
2558
+ count += 1
2559
+
2560
+ #puts opts[:find].inspect #killme
2561
+ #puts opts[:find].inspect #killme
2562
+
2563
+ zipped = opts[:find].zip opts[:repo].between(opts[:find])
2564
+ zipped.each do |(n, list)|
2565
+ list << n[1]
2566
+ p = n[0]
2567
+ f = 1 # ??? why are these vars so NAMELESS
2568
+
2569
+ list.each do |item|
2570
+ UI::debug "narrowing #{f}:#{list.size} #{short item}"
2571
+
2572
+ if opts[:node_map].include? item
2573
+ if f <= 2
2574
+ opts[:on_find].call(p, item)
2575
+ else
2576
+ UI::debug "narrowed branch search to #{short p}:#{short item}"
2577
+ new_search << [p, item]
2578
+ end
2579
+ break
2580
+ end
2581
+
2582
+ p, f = item, f*2
2583
+ end
2584
+ end
2585
+
2586
+ opts[:find] = new_search
2587
+ end
2588
+
2589
+ [opts[:find], count]
2590
+ end
2591
+
2592
+ ##
2593
+ # this is called before every push
2594
+ # @todo -- add default values for +opts+
2595
+ def pre_push(remote, opts={})
2596
+ common = {}
2597
+ remote_heads = remote.heads
2598
+ inc = common_nodes remote, :base => common, :heads => remote_heads, :force => true
2599
+ inc = inc[1]
2600
+ update, updated_heads = find_outgoing_roots remote, :base => common, :heads => remote_heads
2601
+
2602
+ if opts[:revs]
2603
+ btw = changelog.nodes_between(update, opts[:revs])
2604
+ missing_cl, bases, heads = btw[:between], btw[:roots], btw[:heads]
2605
+ else
2606
+ bases, heads = update, changelog.heads
2607
+ end
2608
+ if bases.empty?
2609
+ UI::status 'no changes found'
2610
+ return nil, 1
2611
+ elsif !opts[:force]
2612
+ # check if we're creating new remote heads
2613
+ # to be a remote head after push, node must be either
2614
+ # - unknown locally
2615
+ # - a local outgoing head descended from update
2616
+ # - a remote head that's known locally and not
2617
+ # ancestral to an outgoing head
2618
+
2619
+ warn = false
2620
+ if remote_heads == [NULL_ID]
2621
+ warn = false
2622
+ elsif (opts[:revs].nil? || opts[:revs].empty?) and heads.size > remote_heads.size
2623
+ warn = true
2624
+ else
2625
+ new_heads = heads
2626
+ remote_heads.each do |r|
2627
+ if changelog.node_map.include? r
2628
+ desc = changelog.heads r, heads
2629
+ l = heads.select {|h| desc.include? h }
2630
+
2631
+ new_heads << r if l.empty?
2632
+ else
2633
+ new_heads << r
2634
+ end
2635
+ end
2636
+
2637
+ warn = true if new_heads.size > remote_heads.size
2638
+ end
2639
+
2640
+ if warn
2641
+ UI::status 'abort: push creates new remote heads!'
2642
+ UI::status '(did you forget to merge? use push -f to forge)'
2643
+ return nil, 0
2644
+ elsif inc.any?
2645
+ UI::note 'unsynced remote changes!'
2646
+ end
2647
+ end
2648
+
2649
+ if opts[:revs].nil?
2650
+ # use the fast path, no race possible on push
2651
+ cg = get_changegroup common.keys, :push
2652
+ else
2653
+ cg = changegroup_subset update, revs, :push
2654
+ end
2655
+
2656
+ [cg, remote_heads]
2657
+ end
2658
+
2659
+ end # localrepo
2660
+ end # repo
2661
+ end