amp 0.5.2 → 0.5.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (232) hide show
  1. data/.gitignore +12 -0
  2. data/.hgignore +3 -0
  3. data/AUTHORS +1 -1
  4. data/Manifest.txt +99 -38
  5. data/README.md +3 -3
  6. data/Rakefile +53 -18
  7. data/SCHEDULE.markdown +5 -1
  8. data/TODO.markdown +120 -149
  9. data/ampfile.rb +3 -1
  10. data/bin/amp +4 -1
  11. data/ext/amp/bz2/extconf.rb +1 -1
  12. data/ext/amp/mercurial_patch/extconf.rb +1 -1
  13. data/ext/amp/mercurial_patch/mpatch.c +4 -3
  14. data/ext/amp/priority_queue/extconf.rb +1 -1
  15. data/ext/amp/support/extconf.rb +1 -1
  16. data/ext/amp/support/support.c +1 -1
  17. data/lib/amp.rb +125 -67
  18. data/lib/amp/commands/command.rb +12 -10
  19. data/lib/amp/commands/command_support.rb +8 -1
  20. data/lib/amp/commands/commands/help.rb +2 -20
  21. data/lib/amp/commands/commands/init.rb +14 -2
  22. data/lib/amp/commands/commands/templates.rb +6 -4
  23. data/lib/amp/commands/commands/version.rb +15 -1
  24. data/lib/amp/commands/commands/workflow.rb +3 -3
  25. data/lib/amp/commands/commands/workflows/git/add.rb +3 -3
  26. data/lib/amp/commands/commands/workflows/git/copy.rb +1 -1
  27. data/lib/amp/commands/commands/workflows/git/rm.rb +4 -2
  28. data/lib/amp/commands/commands/workflows/hg/add.rb +1 -1
  29. data/lib/amp/commands/commands/workflows/hg/addremove.rb +2 -2
  30. data/lib/amp/commands/commands/workflows/hg/annotate.rb +8 -2
  31. data/lib/amp/commands/commands/workflows/hg/bisect.rb +253 -0
  32. data/lib/amp/commands/commands/workflows/hg/branch.rb +1 -1
  33. data/lib/amp/commands/commands/workflows/hg/branches.rb +3 -3
  34. data/lib/amp/commands/commands/workflows/hg/bundle.rb +3 -3
  35. data/lib/amp/commands/commands/workflows/hg/clone.rb +4 -5
  36. data/lib/amp/commands/commands/workflows/hg/commit.rb +37 -1
  37. data/lib/amp/commands/commands/workflows/hg/copy.rb +2 -1
  38. data/lib/amp/commands/commands/workflows/hg/debug/index.rb +1 -1
  39. data/lib/amp/commands/commands/workflows/hg/diff.rb +3 -8
  40. data/lib/amp/commands/commands/workflows/hg/forget.rb +5 -4
  41. data/lib/amp/commands/commands/workflows/hg/identify.rb +6 -6
  42. data/lib/amp/commands/commands/workflows/hg/import.rb +1 -1
  43. data/lib/amp/commands/commands/workflows/hg/incoming.rb +2 -2
  44. data/lib/amp/commands/commands/workflows/hg/log.rb +5 -4
  45. data/lib/amp/commands/commands/workflows/hg/merge.rb +1 -1
  46. data/lib/amp/commands/commands/workflows/hg/move.rb +5 -3
  47. data/lib/amp/commands/commands/workflows/hg/outgoing.rb +1 -1
  48. data/lib/amp/commands/commands/workflows/hg/push.rb +6 -7
  49. data/lib/amp/commands/commands/workflows/hg/remove.rb +2 -2
  50. data/lib/amp/commands/commands/workflows/hg/resolve.rb +6 -23
  51. data/lib/amp/commands/commands/workflows/hg/root.rb +1 -2
  52. data/lib/amp/commands/commands/workflows/hg/status.rb +21 -12
  53. data/lib/amp/commands/commands/workflows/hg/tag.rb +2 -2
  54. data/lib/amp/commands/commands/workflows/hg/untrack.rb +12 -0
  55. data/lib/amp/commands/commands/workflows/hg/verify.rb +13 -3
  56. data/lib/amp/commands/commands/workflows/hg/what_changed.rb +18 -0
  57. data/lib/amp/commands/dispatch.rb +12 -13
  58. data/lib/amp/dependencies/amp_support.rb +1 -1
  59. data/lib/amp/dependencies/amp_support/ruby_amp_support.rb +1 -0
  60. data/lib/amp/dependencies/maruku.rb +136 -0
  61. data/lib/amp/dependencies/maruku/attributes.rb +227 -0
  62. data/lib/amp/dependencies/maruku/defaults.rb +71 -0
  63. data/lib/amp/dependencies/maruku/errors_management.rb +92 -0
  64. data/lib/amp/dependencies/maruku/helpers.rb +260 -0
  65. data/lib/amp/dependencies/maruku/input/charsource.rb +326 -0
  66. data/lib/amp/dependencies/maruku/input/extensions.rb +69 -0
  67. data/lib/amp/dependencies/maruku/input/html_helper.rb +189 -0
  68. data/lib/amp/dependencies/maruku/input/linesource.rb +111 -0
  69. data/lib/amp/dependencies/maruku/input/parse_block.rb +615 -0
  70. data/lib/amp/dependencies/maruku/input/parse_doc.rb +234 -0
  71. data/lib/amp/dependencies/maruku/input/parse_span_better.rb +746 -0
  72. data/lib/amp/dependencies/maruku/input/rubypants.rb +225 -0
  73. data/lib/amp/dependencies/maruku/input/type_detection.rb +147 -0
  74. data/lib/amp/dependencies/maruku/input_textile2/t2_parser.rb +163 -0
  75. data/lib/amp/dependencies/maruku/maruku.rb +33 -0
  76. data/lib/amp/dependencies/maruku/output/to_ansi.rb +223 -0
  77. data/lib/amp/dependencies/maruku/output/to_html.rb +991 -0
  78. data/lib/amp/dependencies/maruku/output/to_markdown.rb +164 -0
  79. data/lib/amp/dependencies/maruku/output/to_s.rb +56 -0
  80. data/lib/amp/dependencies/maruku/string_utils.rb +191 -0
  81. data/lib/amp/dependencies/maruku/structures.rb +167 -0
  82. data/lib/amp/dependencies/maruku/structures_inspect.rb +87 -0
  83. data/lib/amp/dependencies/maruku/structures_iterators.rb +61 -0
  84. data/lib/amp/dependencies/maruku/textile2.rb +1 -0
  85. data/lib/amp/dependencies/maruku/toc.rb +199 -0
  86. data/lib/amp/dependencies/maruku/usage/example1.rb +33 -0
  87. data/lib/amp/dependencies/maruku/version.rb +40 -0
  88. data/lib/amp/dependencies/priority_queue.rb +2 -1
  89. data/lib/amp/dependencies/python_config.rb +2 -1
  90. data/lib/amp/graphs/ancestor.rb +2 -1
  91. data/lib/amp/graphs/copies.rb +236 -233
  92. data/lib/amp/help/entries/__default__.erb +31 -0
  93. data/lib/amp/help/entries/commands.erb +6 -0
  94. data/lib/amp/help/entries/mdtest.md +35 -0
  95. data/lib/amp/help/entries/silly +3 -0
  96. data/lib/amp/help/help.rb +288 -0
  97. data/lib/amp/profiling_hacks.rb +5 -3
  98. data/lib/amp/repository/abstract/abstract_changeset.rb +97 -0
  99. data/lib/amp/repository/abstract/abstract_local_repo.rb +181 -0
  100. data/lib/amp/repository/abstract/abstract_staging_area.rb +180 -0
  101. data/lib/amp/repository/abstract/abstract_versioned_file.rb +100 -0
  102. data/lib/amp/repository/abstract/common_methods/changeset.rb +75 -0
  103. data/lib/amp/repository/abstract/common_methods/local_repo.rb +277 -0
  104. data/lib/amp/repository/abstract/common_methods/staging_area.rb +233 -0
  105. data/lib/amp/repository/abstract/common_methods/versioned_file.rb +71 -0
  106. data/lib/amp/repository/generic_repo_picker.rb +78 -0
  107. data/lib/amp/repository/git/repo_format/changeset.rb +336 -0
  108. data/lib/amp/repository/git/repo_format/staging_area.rb +192 -0
  109. data/lib/amp/repository/git/repo_format/versioned_file.rb +119 -0
  110. data/lib/amp/repository/git/repositories/local_repository.rb +164 -0
  111. data/lib/amp/repository/git/repository.rb +41 -0
  112. data/lib/amp/repository/mercurial/encoding/mercurial_diff.rb +382 -0
  113. data/lib/amp/repository/mercurial/encoding/mercurial_patch.rb +1 -0
  114. data/lib/amp/repository/mercurial/encoding/patch.rb +294 -0
  115. data/lib/amp/repository/mercurial/encoding/pure_ruby/ruby_mercurial_patch.rb +124 -0
  116. data/lib/amp/repository/mercurial/merging/merge_ui.rb +327 -0
  117. data/lib/amp/repository/mercurial/merging/simple_merge.rb +452 -0
  118. data/lib/amp/repository/mercurial/repo_format/branch_manager.rb +266 -0
  119. data/lib/amp/repository/mercurial/repo_format/changeset.rb +768 -0
  120. data/lib/amp/repository/mercurial/repo_format/dir_state.rb +716 -0
  121. data/lib/amp/repository/mercurial/repo_format/journal.rb +218 -0
  122. data/lib/amp/repository/mercurial/repo_format/lock.rb +210 -0
  123. data/lib/amp/repository/mercurial/repo_format/merge_state.rb +228 -0
  124. data/lib/amp/repository/mercurial/repo_format/staging_area.rb +367 -0
  125. data/lib/amp/repository/mercurial/repo_format/store.rb +487 -0
  126. data/lib/amp/repository/mercurial/repo_format/tag_manager.rb +322 -0
  127. data/lib/amp/repository/mercurial/repo_format/updatable.rb +543 -0
  128. data/lib/amp/repository/mercurial/repo_format/updater.rb +848 -0
  129. data/lib/amp/repository/mercurial/repo_format/verification.rb +433 -0
  130. data/lib/amp/repository/mercurial/repositories/bundle_repository.rb +216 -0
  131. data/lib/amp/repository/mercurial/repositories/http_repository.rb +386 -0
  132. data/lib/amp/repository/mercurial/repositories/local_repository.rb +2034 -0
  133. data/lib/amp/repository/mercurial/repository.rb +119 -0
  134. data/lib/amp/repository/mercurial/revlogs/bundle_revlogs.rb +249 -0
  135. data/lib/amp/repository/mercurial/revlogs/changegroup.rb +217 -0
  136. data/lib/amp/repository/mercurial/revlogs/changelog.rb +339 -0
  137. data/lib/amp/repository/mercurial/revlogs/file_log.rb +152 -0
  138. data/lib/amp/repository/mercurial/revlogs/index.rb +500 -0
  139. data/lib/amp/repository/mercurial/revlogs/manifest.rb +201 -0
  140. data/lib/amp/repository/mercurial/revlogs/node.rb +20 -0
  141. data/lib/amp/repository/mercurial/revlogs/revlog.rb +1026 -0
  142. data/lib/amp/repository/mercurial/revlogs/revlog_support.rb +129 -0
  143. data/lib/amp/repository/mercurial/revlogs/versioned_file.rb +597 -0
  144. data/lib/amp/repository/repository.rb +11 -88
  145. data/lib/amp/server/extension/amp_extension.rb +3 -3
  146. data/lib/amp/server/fancy_http_server.rb +1 -1
  147. data/lib/amp/server/fancy_views/_browser.haml +1 -1
  148. data/lib/amp/server/fancy_views/_diff_file.haml +1 -8
  149. data/lib/amp/server/fancy_views/changeset.haml +2 -2
  150. data/lib/amp/server/fancy_views/file.haml +1 -1
  151. data/lib/amp/server/fancy_views/file_diff.haml +1 -1
  152. data/lib/amp/support/amp_ui.rb +13 -29
  153. data/lib/amp/support/generator.rb +1 -1
  154. data/lib/amp/support/loaders.rb +1 -2
  155. data/lib/amp/support/logger.rb +10 -16
  156. data/lib/amp/support/match.rb +18 -4
  157. data/lib/amp/support/mercurial/ignore.rb +151 -0
  158. data/lib/amp/support/openers.rb +8 -3
  159. data/lib/amp/support/support.rb +91 -46
  160. data/lib/amp/templates/{blank.commit.erb → mercurial/blank.commit.erb} +0 -0
  161. data/lib/amp/templates/{blank.log.erb → mercurial/blank.log.erb} +0 -0
  162. data/lib/amp/templates/{default.commit.erb → mercurial/default.commit.erb} +0 -0
  163. data/lib/amp/templates/{default.log.erb → mercurial/default.log.erb} +0 -0
  164. data/lib/amp/templates/template.rb +18 -18
  165. data/man/amp.1 +51 -0
  166. data/site/src/about/commands.haml +1 -1
  167. data/site/src/css/amp.css +1 -1
  168. data/site/src/index.haml +3 -3
  169. data/tasks/man.rake +39 -0
  170. data/tasks/stats.rake +1 -10
  171. data/tasks/yard.rake +1 -50
  172. data/test/dirstate_tests/test_dir_state.rb +10 -8
  173. data/test/functional_tests/annotate.out +31 -0
  174. data/test/functional_tests/test_functional.rb +155 -63
  175. data/test/localrepo_tests/ampfile.rb +12 -0
  176. data/test/localrepo_tests/test_local_repo.rb +56 -57
  177. data/test/manifest_tests/test_manifest.rb +3 -5
  178. data/test/merge_tests/test_merge.rb +3 -3
  179. data/test/revlog_tests/test_revlog.rb +14 -6
  180. data/test/store_tests/test_fncache_store.rb +19 -19
  181. data/test/test_19_compatibility.rb +46 -0
  182. data/test/test_base85.rb +2 -2
  183. data/test/test_bdiff.rb +2 -2
  184. data/test/test_changegroup.rb +59 -0
  185. data/test/test_commands.rb +2 -2
  186. data/test/test_difflib.rb +2 -2
  187. data/test/test_generator.rb +34 -0
  188. data/test/test_ignore.rb +203 -0
  189. data/test/test_journal.rb +18 -13
  190. data/test/test_match.rb +2 -2
  191. data/test/test_mdiff.rb +3 -3
  192. data/test/test_mpatch.rb +3 -3
  193. data/test/test_multi_io.rb +40 -0
  194. data/test/test_support.rb +18 -2
  195. data/test/test_templates.rb +38 -0
  196. data/test/test_ui.rb +79 -0
  197. data/test/testutilities.rb +56 -0
  198. metadata +168 -49
  199. data/ext/amp/bz2/mkmf.log +0 -38
  200. data/lib/amp/encoding/mercurial_diff.rb +0 -378
  201. data/lib/amp/encoding/mercurial_patch.rb +0 -1
  202. data/lib/amp/encoding/patch.rb +0 -292
  203. data/lib/amp/encoding/pure_ruby/ruby_mercurial_patch.rb +0 -123
  204. data/lib/amp/merges/merge_state.rb +0 -164
  205. data/lib/amp/merges/merge_ui.rb +0 -322
  206. data/lib/amp/merges/simple_merge.rb +0 -450
  207. data/lib/amp/repository/branch_manager.rb +0 -234
  208. data/lib/amp/repository/dir_state.rb +0 -950
  209. data/lib/amp/repository/journal.rb +0 -203
  210. data/lib/amp/repository/lock.rb +0 -207
  211. data/lib/amp/repository/repositories/bundle_repository.rb +0 -214
  212. data/lib/amp/repository/repositories/http_repository.rb +0 -377
  213. data/lib/amp/repository/repositories/local_repository.rb +0 -2661
  214. data/lib/amp/repository/store.rb +0 -485
  215. data/lib/amp/repository/tag_manager.rb +0 -319
  216. data/lib/amp/repository/updatable.rb +0 -532
  217. data/lib/amp/repository/verification.rb +0 -431
  218. data/lib/amp/repository/versioned_file.rb +0 -475
  219. data/lib/amp/revlogs/bundle_revlogs.rb +0 -246
  220. data/lib/amp/revlogs/changegroup.rb +0 -217
  221. data/lib/amp/revlogs/changelog.rb +0 -338
  222. data/lib/amp/revlogs/changeset.rb +0 -521
  223. data/lib/amp/revlogs/file_log.rb +0 -165
  224. data/lib/amp/revlogs/index.rb +0 -493
  225. data/lib/amp/revlogs/manifest.rb +0 -195
  226. data/lib/amp/revlogs/node.rb +0 -18
  227. data/lib/amp/revlogs/revlog.rb +0 -1045
  228. data/lib/amp/revlogs/revlog_support.rb +0 -126
  229. data/lib/amp/support/ignore.rb +0 -144
  230. data/site/Rakefile +0 -38
  231. data/test/test_amp.rb +0 -9
  232. data/test/test_helper.rb +0 -15
@@ -0,0 +1,386 @@
1
+ require 'uri'
2
+ require 'zlib'
3
+
4
+ # to shut up those fucking warnings!
5
+ # taken from http://www.5dollarwhitebox.org/drupal/node/64
6
+ class Net::HTTP
7
+ alias_method :old_initialize, :initialize
8
+ def initialize(*args)
9
+ old_initialize(*args)
10
+ require 'openssl' unless defined? OpenSSL
11
+ @ssl_context = OpenSSL::SSL::SSLContext.new
12
+ @ssl_context.verify_mode = OpenSSL::SSL::VERIFY_NONE
13
+ end
14
+ end
15
+
16
+ module Amp
17
+ module Repositories
18
+ module Mercurial
19
+
20
+ ##
21
+ # = This is the class for connecting to an HTTP[S]-based repository.
22
+ # The protocol's pretty simple - just ?cmd="command", and any other
23
+ # args you need. Should be pretty easy.
24
+ class HTTPRepository < Repository
25
+ include Amp::Mercurial::RevlogSupport::Node
26
+
27
+ DEFAULT_HEADERS = {"User-agent" => "Amp-#{Amp::VERSION}",
28
+ "Accept" => "Application/Mercurial-0.1"}
29
+
30
+ ##
31
+ # The URL we connect to for this repository
32
+ attr_reader :url
33
+
34
+ ##
35
+ # Should the repository connect via SSL?
36
+ attr_accessor :secure
37
+
38
+ ##
39
+ # Returns whether the repository is local or not. Which it isn't. Because
40
+ # we're connecting over HTTP.
41
+ #
42
+ # @return [Boolean] +false+. Because the repo isn't local.
43
+ def local?; false; end
44
+
45
+ ##
46
+ # Standard initializer for a repository. However, "create" is a no-op.
47
+ #
48
+ # @param [String] path the URL for the repository.
49
+ # @param [Boolean] create this is useless since we can't create remote repos
50
+ # @param [Amp::AmpConfig] config the configuration for Amp right now.
51
+ def initialize(path="", create=false, config=nil)
52
+ @url, @config = URI.parse(path), config
53
+ @username ||= @url.user
54
+ @password ||= @url.password
55
+ @auth_mode = :none
56
+ raise InvalidArgumentError.new("Invalid URL for an HTTP repo!") if @url.nil?
57
+ end
58
+
59
+ ##
60
+ # Loads the capabilities from the server when necessary. (Lazy loading)
61
+ #
62
+ # @return [Hash] the capabilities of the server, in the form:
63
+ # { capability => true }
64
+ # or
65
+ # { capability => "capability;settings;"}
66
+ def get_capabilities
67
+ return @capabilities if @capabilities
68
+ begin
69
+ @capabilities = {}
70
+ do_read("capabilities")[:body].split.each do |k|
71
+ if k.include? "="
72
+ key, value = k.split("=", 2)
73
+ @capabilities[key] = value
74
+ else
75
+ @capabilities[k] = true
76
+ end
77
+ end
78
+ rescue
79
+ @capabilities = []
80
+ end
81
+ @capabilities
82
+ end
83
+
84
+ ##
85
+ # Unsupported - raises an error.
86
+ def lock; raise RepoError.new("You can't lock an HTTP repo."); end
87
+
88
+ ##
89
+ # Looks up a node with the given key. The key could be a node ID (full or
90
+ # partial), an index number (though this is slightly risky as it might
91
+ # match a node ID partially), "tip", and so on. See {LocalRepository#[]}.
92
+ #
93
+ # @param [String] key the key to look up - could be node ID, revision index,
94
+ # and so on.
95
+ # @return [String] the full node ID of the requested node on the remote server
96
+ def lookup(key)
97
+ require_capability("lookup", "Look up Remote Revision")
98
+ data = do_read("lookup", :key => key)[:body]
99
+ code, data = data.chomp.split(" ", 2)
100
+
101
+ return data.unhexlify if code.to_i > 0
102
+ raise RepoError.new("Unknown Revision #{data}")
103
+ end
104
+
105
+ ##
106
+ # Gets all the heads of the repository. Returned in binary form.
107
+ #
108
+ # @return [Array<String>] the full, binary node_ids of all the heads on
109
+ # the remote server.
110
+ def heads
111
+ data = do_read("heads")[:body]
112
+ data.chomp.split(" ").map {|h| h.unhexlify }
113
+ end
114
+
115
+ ##
116
+ # Gets the node IDs of all the branch roots in the repository. Uses
117
+ # the supplied nodes to use to search for branches.
118
+ #
119
+ # @param [Array<String>] nodes the nodes to use as heads to search for
120
+ # branches. The search starts at each supplied node (or the tip, if
121
+ # left empty), and goes to that tree's root, and returns the relevant
122
+ # information for the branch.
123
+ # @return [Array<Array<String>>] An array of arrays of strings. Each array
124
+ # has 4 components: [head, root, parent1, parent2].
125
+ def branches(nodes)
126
+ n = nodes.map {|n| n.hexlify }.join(" ")
127
+ data = do_read("branches", :nodes => n)[:body]
128
+ data.split("\n").map do |b|
129
+ b.split(" ").map {|b| b.unhexlify}
130
+ end
131
+ end
132
+
133
+ ##
134
+ # Asks the server to bundle up the given nodes into a changegroup, and returns it
135
+ # uncompressed. This is for pulls.
136
+ #
137
+ # @todo figure out what the +kind+ parameter is for
138
+ # @param [Array<String>] nodes the nodes to package into the changegroup
139
+ # @param [NilClass] kind (UNUSED)
140
+ # @return [StringIO] the uncompressed changegroup as a stream
141
+ def changegroup(nodes, kind)
142
+ n = nodes.map{|i| i.hexlify }.join ' '
143
+ f = do_read('changegroup', n.empty? ? {} : {:roots => n})[:body]
144
+
145
+ s = StringIO.new "",(ruby_19? ? "w+:ASCII-8BIT" : "w+")
146
+ s.write Zlib::Inflate.inflate(f)
147
+ s.pos = 0
148
+ s
149
+ end
150
+
151
+ ##
152
+ # Asks the server to bundle up all the necessary nodes between the lists
153
+ # bases and heads. It is returned as a stream that reads it in a decompressed
154
+ # fashion. This is for pulls.
155
+ #
156
+ # @param [Array<String>] bases the base nodes of the subset we're requesting.
157
+ # Should be an array (or any Enumerable) of node ids.
158
+ # @param [Array<String>] heads the heads of the subset we're requesting.
159
+ # These nodes will be retrieved as well. Should be an array of node IDs.
160
+ # @param [NilClass] source i have no idea (UNUSED)
161
+ # @return [StringIO] the uncompressed changegroup subset as a stream.
162
+ def changegroup_subset(bases, heads, source)
163
+ #require_capability 'changegroupsubset', 'look up remote changes'
164
+ base_list = bases.map {|n| n.hexlify }.join ' '
165
+ head_list = heads.map {|n| n.hexlify }.join ' '
166
+ response = do_read("changegroupsubset", :bases => base_list, :heads => head_list)
167
+
168
+ s = StringIO.new "",(ruby_19? ? "w+:ASCII-8BIT" : "w+")
169
+ s.write Zlib::Inflate.inflate(response[:body])
170
+ s.rewind
171
+ s
172
+ end
173
+
174
+ ##
175
+ # Sends a bundled up changegroup to the server, who will add it to its repository.
176
+ # Uses the bundle format.
177
+ #
178
+ # @param [StringIO] cg the changegroup to push as a stream.
179
+ # @param [Array<String>] heads the heads of the changegroup being sent
180
+ # @param [NilClass] source no idea UNUSED
181
+ # @return [Fixnum] the response code from the server (1 indicates success)
182
+ def unbundle(cg, heads, source)
183
+ # have to stream bundle to a temp file because we do not have
184
+ # http 1.1 chunked transfer
185
+
186
+ type = ''
187
+ types = capable? 'unbundle'
188
+
189
+ # servers older than d1b16a746db6 will send 'unbundle' as a boolean
190
+ # capability
191
+ # this will be a list of allowed bundle compression types
192
+ types = types.split ',' rescue ['']
193
+
194
+ # pick a compression format
195
+ types.each do |x|
196
+ (type = x and break) if Amp::Mercurial::RevlogSupport::ChangeGroup::BUNDLE_HEADERS.include? x
197
+ end
198
+
199
+ # compress and create the bundle
200
+ data = Amp::Mercurial::RevlogSupport::ChangeGroup.write_bundle cg, type
201
+
202
+ # send the data
203
+ resp = do_read 'unbundle', :data => data.string,
204
+ :headers => {'Content-Type' => 'application/octet-stream'},
205
+ :heads => heads.map{|h| h.hexlify }.join(' ')
206
+ # parse output
207
+ resp_code, output = resp[:body].split "\n"
208
+
209
+ # make sure the reponse was in an expected format (i.e. with a response code)
210
+ unless resp_code.to_i.to_s == resp_code
211
+ raise abort("push failed (unexpected response): #{resp}")
212
+ end
213
+
214
+ # output any text from the server
215
+ UI::status output
216
+ # return 1 for success, 0 for failure
217
+ resp_code.to_i
218
+ end
219
+
220
+ def stream_out
221
+ do_cmd 'stream_out'
222
+ end
223
+
224
+ ##
225
+ # For each provided pair of nodes, return the nodes between the pair.
226
+ #
227
+ # @param [Array<Array<String>>] an array of node pairs, so an array of an array
228
+ # of strings. The first node is the head, the second node is the root of the pair.
229
+ # @return [Array<Array<String>>] for each pair, we return 1 array, which contains
230
+ # the node IDs of every node between the pair.
231
+ # add lstrip to split_newlines to fix but not cure bug
232
+ def between(pairs)
233
+ batch = 8
234
+ ret = []
235
+
236
+ (0..(pairs.size)).step(batch) do |i|
237
+ n = pairs[i..(i+batch-1)].map {|p| p.map {|k| k.hexlify }.join("-") }.join(" ")
238
+ resp = do_read("between", :pairs => n)
239
+
240
+ raise RepoError.new("unexpected code: #{code}") unless resp[:code] == 200
241
+
242
+ ret += resp[:body].lstrip.split_newlines.map {|l| (l && l.split(" ").map{|i| i.unhexlify }) || []}
243
+ end
244
+
245
+ Amp::UI.debug "between returns: #{ret.inspect}"
246
+ ret
247
+ end
248
+
249
+ private
250
+
251
+ ##
252
+ # Runs the given command by the server, gets the response. Takes the name of the command,
253
+ # the data, headers, etc. The command is assumed to be a GET request, unless args[:data] is
254
+ # set, in which case it is sent via POST.
255
+ #
256
+ # @param [String] command the command to send to the server, such as "heads"
257
+ # @param [Hash] args the arguments you need to provide - for lookup, it
258
+ # might be the revision indicies.
259
+ # @return [String] the response data from the server.
260
+ def do_cmd(command, args={})
261
+ require 'net/http'
262
+
263
+ # Be safe for recursive calls
264
+ work_args = args.dup
265
+ # grab data, but don't leave it in, or it'll be added to the query string
266
+ data = work_args.delete(:data) || nil
267
+ # and headers, but don't leave it in, or it'll be added to the query string
268
+ headers = work_args.delete(:headers) || {}
269
+
270
+ # Our query string is "cmd => command" plus any other parts of the args hash
271
+ query = { "cmd" => command }
272
+ query.merge! work_args
273
+
274
+ # break it up, make a query
275
+ host = @url.host
276
+ path = @url.path
277
+ # Was having trouble with this... should be safe now
278
+ path += "?" + URI.escape(query.map {|k,v| "#{k}=#{v}"}.join("&"), /[^-_!~*'()a-zA-Z\d;\/?:@&=+$,\[\]]/n)
279
+
280
+ # silly scoping
281
+ response = nil
282
+ # Create an HTTP object so we can send our request. static methods aren't flexible
283
+ # enough for us
284
+ sess = Net::HTTP.new host, @url.port
285
+ # Use SSL if necessary
286
+ sess.use_ssl = true if secure
287
+ # Let's send our request!
288
+ sess.start do |http|
289
+ # if we have data, it's a POST
290
+ if data
291
+ req = Net::HTTP::Post.new(path)
292
+ req.body = data
293
+ else
294
+ # otherwise, it's a GET
295
+ req = Net::HTTP::Get.new(path)
296
+ end
297
+ if @auth_mode == :digest
298
+ # Set digest headers
299
+ req.digest_auth @username, @password, @auth_digest
300
+ elsif @auth_mode == :basic
301
+ # Set basic auth headers
302
+ req.basic_auth @username, @password
303
+ end
304
+ # Copy over the default headers
305
+ DEFAULT_HEADERS.each {|k, v| req[k] = v}
306
+ # Then overwrite them (and add new ones) from our arguments
307
+ headers.each {|k, v| req[k] = v}
308
+ # And send the request!
309
+ response = http.request(req)
310
+ end
311
+ # Case on response - we'll be using the kind_of? style of switch statement
312
+ # here
313
+ case response
314
+ when Net::HTTPRedirection
315
+ # Redirect to a new URL - grab the new URL...
316
+ newurl = response["Location"]
317
+ @url = URI.parse newurl
318
+ @url.user = @username # Keep the old username/password combination.
319
+ @url.password = @password # Keep the old username/password combination.
320
+
321
+ # and try that again.
322
+ do_cmd(command, args)
323
+ when Net::HTTPUnauthorized
324
+ if @auth_mode == :digest
325
+ # no other handlers!
326
+ raise AuthorizationError.new("Failed to authenticate to local repository!")
327
+ elsif @auth_mode == :basic
328
+ # failed to authenticate via basic, so escalate to digest mode
329
+ @auth_mode = :digest
330
+ @auth_digest = response
331
+ do_cmd command, args
332
+ else
333
+ # They want a username and password. A few routes:
334
+ # First, check the URL for the username:password@host format
335
+ @username ||= @url.user
336
+ @password ||= @url.password
337
+ # and start off with basic authentication
338
+ @auth_mode = :basic
339
+ # If the URL didn't contain the username AND password, ask the user for them.
340
+ unless @username && @password
341
+ UI::say "==> HTTP Authentication Required"
342
+
343
+ @username = UI::ask 'username: '
344
+ @password = UI::ask 'password: ', :password
345
+ end
346
+
347
+ # Recursively call the command
348
+ do_cmd command, args
349
+ end
350
+ else
351
+ response
352
+ end
353
+ end
354
+
355
+ ##
356
+ # This is a helper for do_cmd - it splits up the response object into
357
+ # two relevant parts: the response body, and the response code.
358
+ #
359
+ # @param [String] command the remote command to execute, such as "heads"
360
+ # @param [Hash] args the arguments to pass to the request. Takes some special values. All
361
+ # other values are sent in the query string.
362
+ # @option args [String] :data (nil) the POST data to send
363
+ # @option args [Hash] :headers ({}) the headers to send with the request, not including
364
+ # any authentication or user-agent headers.
365
+ # @return [Hash<Symbol => String, Integer>] the response data, in the form
366
+ # {:body => body, :code => response_code}
367
+ def do_read(command, args={})
368
+ response = do_cmd(command, args)
369
+ {:body => response.body, :code => response.code.to_i}
370
+ end
371
+ end
372
+
373
+ ##
374
+ # A special form of the HTTPRepository, except that it is secured over SSL (HTTPS).
375
+ # Other than that, nothing fancy about it.
376
+ class HTTPSRepository < HTTPRepository
377
+ def initialize(*args)
378
+ require 'net/https'
379
+
380
+ super(*args)
381
+ self.secure = true
382
+ end
383
+ end
384
+ end
385
+ end
386
+ end
@@ -0,0 +1,2034 @@
1
+ require 'fileutils'
2
+ module Amp
3
+ module Repositories
4
+ module Mercurial
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::Mercurial::RevlogSupport::Node
12
+ include Repositories::Mercurial::BranchManager
13
+ include Repositories::Mercurial::TagManager
14
+ include Repositories::Mercurial::Updatable
15
+ include Repositories::Mercurial::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
26
+ attr_reader :staging_area
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
+ super(path, create, config)
39
+ @hg = working_join ".hg"
40
+ @file_opener = Amp::Opener.new @root
41
+ @file_opener.default = :open_file # these two are the same, pretty much
42
+ @hg_opener = Amp::Opener.new @root
43
+ @hg_opener.default = :open_hg # just with different defaults
44
+ @filters = {}
45
+ @changelog = nil
46
+ @manifest = nil
47
+ @dirstate = nil
48
+ @staging_area = StagingArea.new(self)
49
+ @working_lock_ref = @lock_ref = 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 join("hgrc")
70
+ end
71
+
72
+ def local?; true; end
73
+
74
+ def inspect; "#<LocalRepository @root=#{@root.inspect}>"; end
75
+
76
+ ##
77
+ # Creates this repository's folders and structure.
78
+ #
79
+ # @param [AmpConfig] config the configuration for this user so
80
+ # we know what neato features to use (like filename cache)
81
+ # @return [Array<String>] the requirements that we found are returned,
82
+ # so further configuration can go down.
83
+ def init(config=@config)
84
+ # make the directory if it's not there
85
+ super
86
+ FileUtils.makedirs @hg
87
+
88
+ requirements = ["revlogv1"]
89
+
90
+ # add some requirements
91
+ if config["format"]["usestore", Boolean] || true
92
+ FileUtils.mkdir "#{@hg}/store"
93
+ requirements << "store"
94
+ requirements << "fncache" if config["format"]["usefncache", Boolean, true]
95
+
96
+ # add the changelog
97
+ make_changelog
98
+ end
99
+
100
+ # write the requires file
101
+ write_requires requirements
102
+ end
103
+
104
+ ##
105
+ # Has the repository been changed since the last commit?
106
+ # Returns true if there are NO outstanding changes or uncommitted merges.
107
+ #
108
+ # @return [Boolean] is the repo pristine
109
+ def pristine?
110
+ dirstate.parents.last == RevlogSupport::Node::NULL_ID &&
111
+ status(:only => [:modified, :added, :removed, :deleted]).all? {|_, v| v.empty? }
112
+ end
113
+
114
+ opposite_method :changed?, :pristine?
115
+
116
+ ##
117
+ # Gets the changeset at the given revision.
118
+ #
119
+ # @param [String, Integer] rev the revision index (Integer) or
120
+ # node_id (String) that we want to access. if nil, returns
121
+ # the working directory. if the string is 'tip', it returns the
122
+ # latest head. Can be either a string or an integer;
123
+ # this shit is smart.
124
+ # @return [Changeset] the changeset at the given revision index or node
125
+ # id. Could be working directory.
126
+ def [](rev)
127
+ if rev.nil?
128
+ return Amp::Mercurial::WorkingDirectoryChangeset.new(self)
129
+ end
130
+ rev = rev.to_i if rev.to_i.to_s == rev
131
+ return Amp::Mercurial::Changeset.new(self, rev)
132
+ end
133
+
134
+ ##
135
+ # Creates a lock at the given path. At first it tries to just make it straight away.
136
+ # If this fails, we then sleep for up to a given amount of time (defaults to 10 minutes!)
137
+ # and continually try to acquire the lock.
138
+ #
139
+ # @raise [LockHeld] if the lock cannot be acquired, this exception is raised
140
+ # @param [String] lockname the name of the lock to create
141
+ # @param [Boolean] wait should we wait for the lock to be released?
142
+ # @param [Proc, #call] release_proc a proc to run when the lock is released
143
+ # @param [Proc, #call] acquire_proc a proc to run when we get the lock
144
+ # @param [String] desc the description of the lock to show if someone stomps on it
145
+ # @return [Lock] a lock at the given location.
146
+ def make_lock(lockname, wait, release_proc, acquire_proc, desc)
147
+ begin
148
+ lock = Lock.new(lockname, :timeout => 0, :release_fxn => release_proc, :desc => desc)
149
+ rescue LockHeld => err
150
+ raise unless wait
151
+ UI.warn("waiting for lock on #{desc} held by #{err.locker}")
152
+ lock = Lock.new(lockname, :timeout => @config["ui","timeout","600"].to_i,
153
+ :release_proc => release_proc, :desc => desc)
154
+ end
155
+ acquire_proc.call if acquire_proc
156
+ return lock
157
+ end
158
+
159
+ ##
160
+ # Locks the repository's .hg/store directory. Returns the lock, or if a block is given,
161
+ # runs the block with the lock, and clears the lock afterward.
162
+ #
163
+ # @yield When a block is given, that block is executed under locked
164
+ # conditions. That code can be guaranteed it is the only code running on the
165
+ # store in a destructive manner.
166
+ # @param [Boolean] wait (true) wait for the lock to expire?
167
+ # @return [Lock] the lock on the .hg/store directory
168
+ def lock_store(wait = true)
169
+ return @lock_ref if @lock_ref && @lock_ref.weakref_alive?
170
+
171
+ lock = make_lock(store_join("lock"), wait, nil, nil, "repository #{root}")
172
+ @lock_ref = WeakRef.new(lock)
173
+ if block_given?
174
+ begin
175
+ yield
176
+ ensure
177
+ @lock_ref = nil
178
+ lock.release
179
+ end
180
+ else
181
+ return lock
182
+ end
183
+ end
184
+
185
+ ##
186
+ # Locks the repository's working directory. Returns the lock, or if a block is given,
187
+ # runs the block with the lock, and clears the lock afterward.
188
+ #
189
+ # @yield When a block is given, that block is executed under locked
190
+ # conditions. That code can be guaranteed it is the only code running on the
191
+ # working directory in a destructive manner.
192
+ # @param [Boolean] wait (true) wait for the lock to expire?
193
+ # @return [Lock] the lock on the working directory
194
+ def lock_working(wait = true)
195
+ return @working_lock_ref if @working_lock_ref && @working_lock_ref.weakref_alive?
196
+
197
+ lock = make_lock(join("wlock"), wait, nil, nil, "working directory of #{root}")
198
+ @working_lock_ref = WeakRef.new(lock)
199
+ if block_given?
200
+ begin
201
+ yield
202
+ ensure
203
+ @working_lock_ref = nil
204
+ lock.release
205
+ end
206
+ else
207
+ return lock
208
+ end
209
+ end
210
+
211
+ ##
212
+ # Takes a block, and runs that block with both the store and the working directory locked.
213
+ #
214
+ # @param [Boolean] wait (true) should we wait for locks, or just give up early?
215
+ def lock_working_and_store(wait=true)
216
+ lock_store(wait) do
217
+ lock_working(wait) do
218
+ yield
219
+ end
220
+ end
221
+ end
222
+
223
+ ##
224
+ # Returns an opener object, which knows how to open objects in the repository's
225
+ # store.
226
+ def store_opener
227
+ @store.opener
228
+ end
229
+
230
+ ##
231
+ # Gets the file-log for the given path, so we can look at an individual
232
+ # file's history, for example.
233
+ #
234
+ # @param [String] f the path to the file
235
+ # @return [FileLog] a filelog (a type of revision log) for the given file
236
+ def file_log(f)
237
+ f = f[1..-1] if f[0, 1] == "/"
238
+ Amp::Mercurial::FileLog.new @store.opener, f
239
+ end
240
+ alias_method :file, :file_log
241
+
242
+ ##
243
+ # Returns the parent changesets of the specified changeset. Defaults to the
244
+ # working directory, if +change_id+ is unspecified.
245
+ #
246
+ # @param [Integer, String] change_id the ID (or index) of the requested changeset
247
+ # @return [Array<Changeset>] the parent changesets of the requested changeset
248
+ def parents(change_id=nil)
249
+ self[change_id].parents
250
+ end
251
+
252
+ ##
253
+ # Gets a versioned file for the given path, so we can look at the individual
254
+ # file's history with the file object itself.
255
+ #
256
+ # @param [String] path the path to the file
257
+ # @param [Hash] opts the options for creating the versioned file
258
+ # @option [String] opts change_id (nil) The ID of the changeset in question
259
+ # @option [String, Integer] opts file_id (nil) the revision # or node ID of
260
+ # into the file_log
261
+ def versioned_file(path, opts={})
262
+ Amp::Mercurial::VersionedFile.new(self, path, opts)
263
+ end
264
+
265
+ ##
266
+ # Gets a versioned file, but using the working directory, so we are looking
267
+ # past the last commit. Important because it uses a different class. Duh.
268
+ #
269
+ # @param [String] path the path to the file
270
+ # @param [Hash] opts the options for creating the versioned file
271
+ # @option [String] opts change_id (nil) The ID of the changeset in question
272
+ # @option [String, Integer] opts file_id (nil) the revision # or node ID of
273
+ # into the file_log
274
+ def working_file(path, opts={})
275
+ VersionedWorkingFile.new(self, path, opts)
276
+ end
277
+
278
+ ##
279
+ # Reads from a file, but in the working directory.
280
+ # Uses encoding if we are set up to do so.
281
+ #
282
+ # @param [String] filename the file to read from the working directory
283
+ # @return [String] the data read from the file, encoded if we are set
284
+ # up to do so.
285
+ def working_read(filename)
286
+ data = @file_opener.open(filename, "r") {|f| f.read }
287
+ data = @filters["encode"].call(filename, data) if @filters["encode"]
288
+ data
289
+ end
290
+
291
+ ##
292
+ # Writes to a file, but in the working directory. Uses encoding if we are
293
+ # set up to do so. Also handles symlinks and executables. Ugh.
294
+ #
295
+ # @param [String] path the path to the file to write to
296
+ # @param [String] data the data to write
297
+ # @param [String] flags the flags to set
298
+ def working_write(path, data, flags = "")
299
+ @file_opener.open(path, "w") do |file|
300
+ file.write(data)
301
+ end
302
+ if flags && flags.include?('x')
303
+ File.amp_set_executable(working_join(path), true)
304
+ end
305
+ end
306
+
307
+ ##
308
+ # Returns the changelog for this repository. This changelog basically
309
+ # is the history of all commits.
310
+ #
311
+ # @return [ChangeLog] the commit history object for the entire repo.
312
+ def changelog
313
+ return @changelog if @changelog
314
+
315
+ @changelog = Amp::Mercurial::ChangeLog.new @store.opener
316
+
317
+ if path = ENV['HG_PENDING']
318
+ if path =~ /^#{root}/
319
+ @changelog.read_pending('00changelog.i.a')
320
+ end
321
+ end
322
+
323
+ @changelog
324
+ end
325
+
326
+ ##
327
+ # Has the file been modified from node1 to node2?
328
+ #
329
+ # @param [String] file the file to check
330
+ # @param [Hash] opts needs to have :node1 and :node2
331
+ # @return [Boolean] has the +file+ been modified?
332
+ def file_modified?(file, opts={})
333
+ vf_old, vf_new = opts[:node1][file], opts[:node2][file]
334
+
335
+ tests = [vf_old.flags != vf_new.flags,
336
+ vf_old.file_node != vf_new.file_node &&
337
+ (vf_new.changeset.include?(file) || vf_old === vf_new)]
338
+ tests.any?
339
+ end
340
+
341
+
342
+ ##
343
+ # Marks a file as resolved according to the merge state. Basic form of
344
+ # merge conflict resolution that all repositories must support.
345
+ #
346
+ # @api
347
+ # @param [String] filename the file to mark resolved
348
+ def mark_resolved(filename)
349
+ merge_state.mark_resolved filename
350
+ end
351
+
352
+ ##
353
+ # Marks a file as conflicted according to the merge state. Basic form of
354
+ # merge conflict resolution that all repositories must support.
355
+ #
356
+ # @api
357
+ # @param [String] filename the file to mark as conflicted
358
+ def mark_conflicted(filename)
359
+ merge_state.mark_conflicted filename
360
+ end
361
+
362
+ ##
363
+ # Returns all files that have not been merged. In other words, if we're
364
+ # waiting for the user to fix up their merge, then return the list of files
365
+ # we need to be correct before merging.
366
+ #
367
+ # @todo think up a better name
368
+ #
369
+ # @return [Array<Array<String, Symbol>>] an array of String-Symbol pairs - the
370
+ # filename is the first entry, the status of the merge is the second.
371
+ def uncommitted_merge_files
372
+ merge_state.uncommitted_merge_files
373
+ end
374
+
375
+ ##
376
+ # Attempts to resolve the given file, according to how mercurial manages
377
+ # merges. Needed for api compliance.
378
+ #
379
+ # @api
380
+ # @param [String] filename the file to attempt to resolve
381
+ def try_resolve_conflict
382
+ # retry the merge
383
+ working_changeset = self[nil]
384
+ merge_changeset = working_changeset.parents.last
385
+
386
+ # backup the current file to a .resolve file (but retain the extension
387
+ # so editors that rely on extensions won't bug out)
388
+ path = working_join file
389
+ File.copy(path, path + ".resolve" + File.extname(path))
390
+
391
+ # try to merge the files!
392
+ merge_state.resolve(file, working_changeset, merge_changeset)
393
+
394
+ # restore the backup to .orig (overwriting the old one)
395
+ File.move(path + ".resolve" + File.extname(path), path + ".orig" + File.extname(path))
396
+ end
397
+
398
+ ##
399
+ # Returns the merge state for this repository. The merge state keeps track
400
+ # of what files need to be merged for an update to be successfully completed.
401
+ #
402
+ # @return [MergeState] the repository's merge state.
403
+ def merge_state
404
+ @merge_state ||= Amp::Merges::Mercurial::MergeState.new(self)
405
+ end
406
+
407
+ ##
408
+ # Returns the manifest for this repository. The manifest keeps track
409
+ # of what files exist at what times, and if they have certain flags
410
+ # (such as executable, or is it a symlink).
411
+ #
412
+ # @return [Manifest] the manifest for the repository
413
+ def manifest
414
+ return @manifest if @manifest
415
+
416
+ changelog #load the changelog
417
+ @manifest = Amp::Mercurial::Manifest.new @store.opener
418
+ end
419
+
420
+ ##
421
+ # Returns the dirstate for this repository. The dirstate keeps track
422
+ # of files status, such as removed, added, merged, and so on. It also
423
+ # keeps track of the working directory.
424
+ #
425
+ # @return [DirState] the dirstate for this local repository.
426
+ def dirstate
427
+ staging_area.dirstate
428
+ end
429
+
430
+ ##
431
+ # Returns the URL of this repository. Uses the "file:" scheme as such.
432
+ #
433
+ # @return [String] the URL pointing to this repo
434
+ def url; "file:#{@root}"; end
435
+
436
+ ##
437
+ # Opens a file using our opener. Can only access files in .hg/
438
+ def open(*args, &block)
439
+ @hg_opener.open(*args, &block)
440
+ end
441
+
442
+ ##
443
+ # Joins the path from this repo's path (.hg), to the file provided.
444
+ #
445
+ # @param file the file we need the path for
446
+ # @return [String] the repo's root, joined with the file's path
447
+ def join(file)
448
+ File.join(@hg, file)
449
+ end
450
+
451
+ ##
452
+ # Joins the path, with a bunch of other args, to the store's directory.
453
+ # Used for opening {FileLog}s and whatnot.
454
+ #
455
+ # @param file the path to the file
456
+ # @return [String] the path to the file from the store.
457
+ def store_join(file)
458
+ @store.join file
459
+ end
460
+
461
+ ##
462
+ # Looks up an identifier for a revision in the commit history. This
463
+ # key could be an integer (specifying a revision number), "." for
464
+ # the latest revision, "null" for the null revision, "tip" for
465
+ # the tip of the repository, a node_id (in hex or binary form) for
466
+ # a revision in the changelog. Yeah. It's a flexible method.
467
+ #
468
+ # @param key the key to lookup in the history of the repo
469
+ # @return [String] a node_id into the changelog for the requested revision
470
+ def lookup(key)
471
+ key = key.to_i if key.to_i.to_s == key.to_s # casting for things like "10"
472
+ case key
473
+ when Fixnum, Bignum, Integer
474
+ changelog.node_id_for_index(key)
475
+ when "."
476
+ dirstate.parents().first
477
+ when "null", nil
478
+ NULL_ID
479
+ when "tip"
480
+ changelog.tip
481
+ else
482
+
483
+ n = changelog.id_match(key)
484
+ return n if n
485
+
486
+ return tags[key] if tags[key]
487
+ return branch_tags[key] if branch_tags[key]
488
+
489
+ n = changelog.partial_id_match(key)
490
+ return n if n
491
+
492
+ # bail
493
+ raise RepoError.new("unknown revision #{key}")
494
+ end
495
+ end
496
+
497
+ ##
498
+ # Finds the nodes between two nodes - this algorithm is ported from the
499
+ # python for mercurial (localrepo.py:1247, for 1.2.1 source). Since this
500
+ # is used by servers, it implements their algorithm... which seems to
501
+ # intentionally not return every node between +top+ and +bottom+.
502
+ # Each one is twice as far from +top+ as the previous.
503
+ #
504
+ # @param [Array<String, String>] An array of node-id pairs, which are arrays
505
+ # of [+top+, +bottom+], which are:
506
+ # top [String] the "top" - or most recent - revision's node ID
507
+ # bottom [String] the "bottom" - or oldest - revision's node ID
508
+ #
509
+ # return [Array<String>] a list of node IDs that are between +top+ and +bottom+
510
+ def between(pairs)
511
+ pairs.map do |top, bottom|
512
+ node, list, counter = top, [], 0
513
+ add_me = 1
514
+ while node != bottom && node != NULL_ID
515
+ if counter == add_me
516
+ list << node
517
+ add_me *= 2
518
+ end
519
+ parent = changelog.parents_for_node(node).first
520
+ node = parent
521
+ counter += 1
522
+ end
523
+ list
524
+ end
525
+ end
526
+
527
+ ##
528
+ # Pull new changegroups from +remote+
529
+ # This does not apply the changes, but pulls them onto
530
+ # the local server.
531
+ #
532
+ # @param [Repository] remote_repo the remote repository object to pull from
533
+ # @param [Hash] options extra options for pulling
534
+ # @option [Array<String, Fixnum>] :heads ([]) which repository heads to pull, such as
535
+ # a branch name or a sha-1 identifier
536
+ # @option [Boolean] :force (false) force the pull, ignoring any errors or warnings
537
+ # @return [Boolean] for success/failure
538
+ def pull(remote, opts={:heads => nil, :force => nil})
539
+ lock_store do
540
+ # get the common nodes, missing nodes, and the remote heads
541
+ # this is findcommonincoming in the Python code, for those with both open
542
+ common, fetch, remote_heads = *common_nodes(remote, :heads => opts[:heads],
543
+ :force => opts[:force])
544
+
545
+ UI::status 'requesting all changes' if fetch == [NULL_ID]
546
+ if fetch.empty?
547
+ UI::status 'no changes found'
548
+ return 0
549
+ end
550
+
551
+ if (opts[:heads].nil? || opts[:heads].empty?) && remote.capable?('changegroupsubset')
552
+ opts[:heads] = remote_heads
553
+ end
554
+ opts[:heads] ||= []
555
+ cg = if opts[:heads].empty?
556
+ remote.changegroup fetch, :pull
557
+ else
558
+ # check for capabilities
559
+ unless remote.capable? 'changegroupsubset'
560
+ raise abort('Partial pull cannot be done because' +
561
+ 'the other repository doesn\'t support' +
562
+ 'changegroupsubset')
563
+ end # end unless
564
+
565
+ remote.changegroup_subset fetch, opts[:heads], :pull
566
+ end
567
+
568
+ add_changegroup cg, :pull, remote.url
569
+ end
570
+ end
571
+
572
+ ##
573
+ # Add a changegroup to the repo.
574
+ #
575
+ # Return values:
576
+ # - nothing changed or no source: 0
577
+ # - more heads than before: 1+added_heads (2..n)
578
+ # - fewer heads than before: -1-removed_heads (-2..-n)
579
+ # - number of heads stays the same: 1
580
+ #
581
+ # Don't the first and last conflict? they stay the same if
582
+ # nothing has changed...
583
+ def add_changegroup(source, type, url, opts={:empty => []})
584
+ run_hook :pre_changegroup, :throw => true, :source => type, :url => url
585
+ changesets = files = revisions = 0
586
+
587
+ return 0 if source.string.empty?
588
+
589
+ rev_map = proc {|x| changelog.revision_index_for_node x }
590
+ cs_map = proc do |x|
591
+ UI::debug "add changeset #{short x}"
592
+ changelog.size
593
+ end
594
+
595
+ # write changelog data to temp files so concurrent readers will not
596
+ # see inconsistent view
597
+ changelog.delay_update
598
+ old_heads = changelog.heads.size
599
+ new_heads = nil # scoping
600
+ changesets = nil # scoping
601
+ cor = nil # scoping
602
+ cnr = nil # scoping
603
+ heads = nil # scoping
604
+
605
+ Amp::Mercurial::Journal.start(join('journal'), :opener => @store.opener) do |journal|
606
+ UI::status 'adding changeset'
607
+
608
+ # pull of the changeset group
609
+ cor = changelog.size - 1
610
+ unless changelog.add_group(source, cs_map, journal) || opts[:empty].any?
611
+ raise abort("received changelog group is empty")
612
+ end
613
+
614
+ cnr = changelog.size - 1
615
+ changesets = cnr - cor
616
+
617
+ # pull off the manifest group
618
+ UI::status 'adding manifests'
619
+
620
+ # No need to check for empty manifest group here:
621
+ # if the result of the merge of 1 and 2 is the same in 3 and 4,
622
+ # no new manifest will be created and the manifest group will be
623
+ # empty during the pull
624
+ manifest.add_group source, rev_map, journal
625
+
626
+ # process the files
627
+ UI::status 'adding file changes'
628
+
629
+ loop do
630
+ f = Amp::Mercurial::RevlogSupport::ChangeGroup.get_chunk source
631
+ break if f.empty?
632
+
633
+ UI::debug "adding #{f} revisions"
634
+ fl = file f
635
+ o = fl.index_size
636
+ unless fl.add_group source, rev_map, journal
637
+ raise abort('received file revlog group is empty')
638
+ end
639
+ revisions += fl.index_size - o
640
+ files += 1
641
+ end # end loop
642
+
643
+ new_heads = changelog.heads.size
644
+ heads = ""
645
+
646
+ unless old_heads.zero? || new_heads == old_heads
647
+ heads = " (+#{new_heads - old_heads} heads)"
648
+ end
649
+
650
+ UI::status("added #{changesets} changesets" +
651
+ " with #{revisions} changes to #{files} files#{heads}")
652
+
653
+ if changesets > 0
654
+ changelog.write_pending
655
+ p = proc { changelog.write_pending && root or "" }
656
+ run_hook :pre_txnchangegroup, :throw => true,
657
+ :node => changelog.node_id_for_index(cor+1).hexlify,
658
+ :source => type,
659
+ :url => url
660
+ end
661
+
662
+ changelog.finalize journal
663
+
664
+ end # end Journal::start
665
+
666
+ if changesets > 0
667
+ # forcefully update the on-disk branch cache
668
+ UI::debug 'updating the branch cache'
669
+ branch_tags
670
+ run_hook :post_changegroup, :node => changelog.node_id_for_index(cor+1).hexlify, :source => type, :url => url
671
+
672
+ ((cor+1)..(cnr+1)).to_a.each do |i|
673
+ run_hook :incoming, :node => changelog.node_id_for_index(i).hexlify,
674
+ :source => type,
675
+ :url => url
676
+ end # end each
677
+ end # end if
678
+
679
+ # never return 0 here
680
+ ret = if new_heads < old_heads
681
+ new_heads - old_heads - 1
682
+ else
683
+ new_heads - old_heads + 1
684
+ end # end if
685
+
686
+ ret
687
+ end # end def
688
+
689
+ ##
690
+ # A changegroup, of some sort.
691
+ def changegroup(base_nodes, source)
692
+ changegroup_subset(base_nodes, heads, source)
693
+ end
694
+
695
+ ##
696
+ # Prints information about the changegroup we are going to receive.
697
+ #
698
+ # @param [Array<String>] nodes the list of node IDs we are receiving
699
+ # @param [Symbol] source how are we receiving the changegroup?
700
+ # @todo add more debug info
701
+ def changegroup_info(nodes, source)
702
+ # print info
703
+ if source == :bundle
704
+ UI.status("#{nodes.size} changesets found")
705
+ end
706
+ # debug stuff
707
+ end
708
+
709
+ ##
710
+ # Faster version of changegroup_subset. Useful when pushing working dir.
711
+ #
712
+ # Generate a changegruop of all nodes that we have that a recipient
713
+ # doesn't
714
+ #
715
+ # This is much easier than the previous function as we can assume that
716
+ # the recipient has any changegnode we aren't sending them.
717
+ #
718
+ # @param [[String]] common the set of common nodes between remote and self
719
+ # @param [Amp::Repository] source
720
+ def get_changegroup(common, source)
721
+ # Call the hooks
722
+ run_hook :pre_outgoing, :throw => true, :source => source
723
+
724
+ nodes = changelog.find_missing common
725
+ revset = Hash.with_keys(nodes.map {|n| changelog.rev(n)})
726
+
727
+ changegroup_info nodes, source
728
+
729
+ identity = proc {|x| x }
730
+
731
+ # ok so this method goes through the generic revlog, and looks for nodes
732
+ # in the changeset(s) we're pushing. Works by the link_rev - basically,
733
+ # the changelog says "hey we're at revision 35", and any changes to any
734
+ # files in any revision logs for that commit will have a link_revision
735
+ # of 35. So we just look for 35!
736
+ gen_node_list = proc do |log|
737
+ log.select {|r| revset[r.link_rev] }.map {|r| r.node_id }
738
+ end
739
+
740
+ # Ok.... I've tried explaining this 3 times and failed.
741
+ #
742
+ # Goal of this proc: We need to update the changed_files hash to reflect
743
+ # which files (typically file logs) have changed since the last push.
744
+ #
745
+ # How it works: it generates a proc that takes a node_id. That node_id
746
+ # will be looked up in the changelog.i file, which happens to store a
747
+ # list of files that were changed in that commit! So really, this method
748
+ # just takes a node_id, and adds filenamess to the list of changed files.
749
+ changed_file_collector = proc do |changed_fileset|
750
+ proc do |cl_node|
751
+ c = changelog.read cl_node
752
+ c[3].each {|fname| changed_fileset[fname] = true }
753
+ end
754
+ end
755
+
756
+ lookup_revlink_func = proc do |revlog|
757
+ # given a revision, return the node
758
+ # good thing the python has a description of what this does
759
+ #
760
+ # *snort*
761
+ lookup_revlink = proc do |n|
762
+ changelog.node revlog[n].link_rev
763
+ end
764
+ end
765
+
766
+ # This constructs a changegroup, or a list of all changed files.
767
+ # If you're here, looking at this code, this bears repeating:
768
+ # - Changelog
769
+ # -- ChangeSet+
770
+ #
771
+ # A Changelog (history of a branch) is an array of ChangeSets,
772
+ # and a ChangeSet is just a single revision, containing what files
773
+ # were changed, who did it, and the commit message. THIS IS JUST A
774
+ # RECEIPT!!!
775
+ #
776
+ # The REASON we construct a changegroup here is because this is called
777
+ # when we push, and we push a changelog (usually bundled to conserve
778
+ # space). This is where we make that receipt, called a changegroup.
779
+ #
780
+ # 'nuff tangent, time to fucking code
781
+ generate_group = proc do
782
+ result = []
783
+ changed_files = {}
784
+
785
+ coll = changed_file_collector[changed_files]
786
+ # get the changelog's changegroups
787
+ changelog.group(nodes, identity, coll) {|chunk| result << chunk }
788
+
789
+
790
+ node_iter = gen_node_list[manifest]
791
+ look = lookup_revlink_func[manifest]
792
+ # get the manifest's changegroups
793
+ manifest.group(node_iter, look) {|chunk| result << chunk }
794
+
795
+ changed_files.keys.sort.each do |fname|
796
+ file_revlog = file fname
797
+ # warning: useless comment
798
+ if file_revlog.index_size.zero?
799
+ raise abort("empty or missing revlog for #{fname}")
800
+ end
801
+
802
+ node_list = gen_node_list[file_revlog]
803
+
804
+ if node_list.any?
805
+ result << Amp::Mercurial::RevlogSupport::ChangeGroup.chunk_header(fname.size)
806
+ result << fname
807
+
808
+ lookup = lookup_revlink_func[file_revlog] # Proc#call
809
+ # more changegroups
810
+ file_revlog.group(node_list, lookup) {|chunk| result << chunk }
811
+ end
812
+ end
813
+ result << Amp::Mercurial::RevlogSupport::ChangeGroup.closing_chunk
814
+
815
+ run_hook :post_outgoing, :node => nodes[0].hexlify, :source => source
816
+
817
+ result
818
+ end
819
+
820
+ s = StringIO.new "",(ruby_19? ? "w+:ASCII-8BIT" : "w+")
821
+ generate_group[].each {|chunk| s.write chunk }
822
+ s.rewind
823
+ s
824
+ end
825
+
826
+ ##
827
+ # This function generates a changegroup consisting of all the nodes
828
+ # that are descendents of any of the bases, and ancestors of any of
829
+ # the heads.
830
+ #
831
+ # It is fairly complex in determining which filenodes and which
832
+ # manifest nodes need to be included for the changeset to be complete
833
+ # is non-trivial.
834
+ #
835
+ # Another wrinkle is doing the reverse, figuring out which changeset in
836
+ # the changegroup a particular filenode or manifestnode belongs to.
837
+ #
838
+ # The caller can specify some nodes that must be included in the
839
+ # changegroup using the extranodes argument. It should be a dict
840
+ # where the keys are the filenames (or 1 for the manifest), and the
841
+ # values are lists of (node, linknode) tuples, where node is a wanted
842
+ # node and linknode is the changelog node that should be transmitted as
843
+ # the linkrev.
844
+ #
845
+ # MAD SHOUTZ to Eric Hopper, who actually had the balls to document a
846
+ # good chunk of this code in the Python. He is a really great man, and
847
+ # deserves whatever thanks we can give him. *Peace*
848
+ #
849
+ # @param [String => [(String, String)]] extra_nodes the key is a filename
850
+ # and the value is a list of (node, link_node) tuples
851
+ def changegroup_subset(bases, new_heads, source, extra_nodes=nil)
852
+ unless extra_nodes
853
+ if new_heads.sort! == heads.sort!
854
+ common = []
855
+
856
+ # parents of bases are known from both sides
857
+ bases.each do |base|
858
+ changelog.parents_for_node(base).each do |parent|
859
+ common << parent unless parent.null? # == NULL_ID
860
+ end # end each
861
+ end # end each
862
+
863
+ # BAIL
864
+ return get_changegroup(common, source)
865
+ end # end if
866
+ end # end unless
867
+
868
+ run_hook :pre_outgoing, :throw => true, :source => source # call dem hooks
869
+
870
+
871
+ # missing changelog list, bases, and heads
872
+ #
873
+ # Some bases may turn out to be superfluous, and some heads may be as
874
+ # well. #nodes_between will return the minimal set of bases and heads
875
+ # necessary to recreate the changegroup.
876
+ # missing_cl_list, bases, heads = changelog.nodes_between(bases, heads)
877
+ btw = changelog.nodes_between(bases, heads)
878
+ missing_cl_list, bases, heads = btw[:between], btw[:roots], btw[:heads]
879
+ changegroup_info missing_cl_list, source
880
+
881
+ # Known heads are the list of heads about which it is assumed the recipient
882
+ # of this changegroup will know.
883
+ known_heads = []
884
+
885
+ # We assume that all parents of bases are known heads.
886
+ bases.each do |base|
887
+ changelog.parents_for_node(base).each do |parent|
888
+ known_heads << parent
889
+ end # end each
890
+ end # end each
891
+
892
+ if known_heads.any? # unless known_heads.empty?
893
+ # Now that we know what heads are known, we can compute which
894
+ # changesets are known. The recipient must know about all
895
+ # changesets required to reach the known heads from the null
896
+ # changeset.
897
+ has_cl_set = changelog.nodes_between(nil, known_heads)[:between]
898
+
899
+ # cast to a hash for latter usage
900
+ has_cl_set = Hash.with_keys has_cl_set
901
+ else
902
+ # If there were no known heads, the recipient cannot be assumed to
903
+ # know about any changesets.
904
+ has_cl_set = {}
905
+ end
906
+
907
+ # We don't know which manifests are missing yet
908
+ missing_mf_set = {}
909
+ # Nor do we know which filenodes are missing.
910
+ missing_fn_set = {}
911
+
912
+ ########
913
+ # Here are procs for further usage
914
+
915
+ # A changeset always belongs to itself, so the changenode lookup
916
+ # function for a changenode is +identity+
917
+ identity = proc {|x| x }
918
+
919
+ # A function generating function. Sets up an enviroment for the
920
+ # inner function.
921
+ cmp_by_rev_function = proc do |rvlg|
922
+ # Compare two nodes by their revision number in the environment's
923
+ # revision history. Since the revision number both represents the
924
+ # most efficient order to read the nodes in, and represents a
925
+ # topological sorting of the nodes, this function if often useful.
926
+ proc {|a, b| rvlg.rev(a) <=> rvlg.rev(b) }
927
+ end
928
+
929
+ # If we determine that a particular file or manifest node must be a
930
+ # node that the recipient of the changegroup will already have, we can
931
+ # also assume the recipient will have all the parents. This function
932
+ # prunes them from the set of missing nodes.
933
+ prune_parents = proc do |rvlg, hasses, missing|
934
+ has_list = hasses.keys
935
+ has_list.sort!(&cmp_by_rev_function(rvlg))
936
+
937
+ has_list.each do |node|
938
+ parent_list = revlog.parent_for_node(node).select {|p| p.not_null? }
939
+ end
940
+
941
+ while parent_list.any?
942
+ n = parent_list.pop
943
+ unless hasses.include? n
944
+ hasses[n] = 1
945
+ p = revlog.parent_for_node(node).select {|p| p.not_null? }
946
+ parent_list += p
947
+ end
948
+ end
949
+
950
+ hasses.each do |n|
951
+ missing.slice!(n - 1, 1) # pop(n, None)
952
+ end
953
+ end
954
+
955
+ # This is a function generating function used to set up an environment
956
+ # for the inner funciont to execute in.
957
+ manifest_and_file_collector = proc do |changed_fileset|
958
+ # This is an information gathering function that gathers
959
+ # information from each changeset node that goes out as part of
960
+ # the changegroup. The information gathered is a list of which
961
+ # manifest nodes are potentially required (the recipient may already
962
+ # have them) and total list of all files which were changed in any
963
+ # changeset in the changegroup.
964
+ #
965
+ # We also remember the first changenode we saw any manifest
966
+ # referenced by so we can later determine which changenode owns
967
+ # the manifest.
968
+
969
+ # this is what we're returning
970
+ proc do |cl_node|
971
+ c = changelog.read cl_node
972
+ c[3].each do |f|
973
+ # This is to make sure we only have one instance of each
974
+ # filename string for each filename
975
+ changed_fileset[f] ||= f
976
+ end # end each
977
+
978
+ missing_mf_set[c[0]] ||= cl_node
979
+ end # end proc
980
+ end # end proc
981
+
982
+ # Figure out which manifest nodes (of the ones we think might be part
983
+ # of the changegroup) the recipients must know about and remove them
984
+ # from the changegroup.
985
+ prune_manifest = proc do
986
+ has_mnfst_set = {}
987
+ missing_mf_set.values.each do |node|
988
+ # If a 'missing' manifest thinks it belongs to a changenode
989
+ # the recipient is assumed to have, obviously the recipient
990
+ # must have the manifest.
991
+ link_node = changelog.node manifest.link_rev(manifest.revision_index_for_node(node))
992
+ has_mnfst_set[n] = 1 if has_cl_set.include? link_node
993
+ end # end each
994
+
995
+ prune_parents[manifest, has_mnfst_set, missing_mf_set] # Proc#call
996
+ end # end proc
997
+
998
+ # Use the information collected in collect_manifests_and_files to say
999
+ # which changenode any manifestnode belongs to.
1000
+ lookup_manifest_link = proc {|node| missing_mf_set[node] }
1001
+
1002
+ # A function generating function that sets up the initial environment
1003
+ # the inner function.
1004
+ filenode_collector = proc do |changed_files|
1005
+ next_rev = []
1006
+
1007
+ # This gathers information from each manifestnode included in the
1008
+ # changegroup about which filenodes the manifest node references
1009
+ # so we can include those in the changegroup too.
1010
+ #
1011
+ # It also remembers which changenode each filenode belongs to. It
1012
+ # does this by assuming the a filenode belongs to the changenode
1013
+ # the first manifest that references it belongs to.
1014
+ collect_missing_filenodes = proc do |node|
1015
+ r = manifest.rev node
1016
+
1017
+ if r == next_rev[0]
1018
+
1019
+ # If the last rev we looked at was the one just previous,
1020
+ # we only need to see a diff.
1021
+ delta_manifest = manifest.read_delta node
1022
+
1023
+ # For each line in the delta
1024
+ delta_manifest.each do |f, fnode|
1025
+ f = changed_files[f]
1026
+
1027
+ # And if the file is in the list of files we care
1028
+ # about.
1029
+ if f
1030
+ # Get the changenode this manifest belongs to
1031
+ cl_node = missing_mf_set[node]
1032
+
1033
+ # Create the set of filenodes for the file if
1034
+ # there isn't one already.
1035
+ ndset = missing_fn_set[f] ||= {}
1036
+
1037
+ # And set the filenode's changelog node to the
1038
+ # manifest's if it hasn't been set already.
1039
+ ndset[fnode] ||= cl_node
1040
+ end
1041
+ end
1042
+ else
1043
+ # Otherwise we need a full manifest.
1044
+ m = manifest.read node
1045
+
1046
+ # For every file in we care about.
1047
+ changed_files.each do |f|
1048
+ fnode = m[f]
1049
+
1050
+ # If it's in the manifest
1051
+ if fnode
1052
+ # See comments above.
1053
+ cl_node = msng_mnfst_set[mnfstnode]
1054
+ ndset = missing_fn_set[f] ||= {}
1055
+ ndset[fnode] ||= cl_node
1056
+ end
1057
+ end
1058
+ end
1059
+
1060
+ # Remember the revision we hope to see next.
1061
+ next_rev[0] = r + 1
1062
+ end # end proc
1063
+ end # end proc
1064
+
1065
+ # We have a list of filenodes we think need for a file, let's remove
1066
+ # all those we know the recipient must have.
1067
+ prune_filenodes = proc do |f, f_revlog|
1068
+ missing_set = missing_fn_set[f]
1069
+ hasset = {}
1070
+
1071
+ # If a 'missing' filenode thinks it belongs to a changenode we
1072
+ # assume the recipient must have, the the recipient must have
1073
+ # that filenode.
1074
+ missing_set.each do |n|
1075
+ cl_node = changelog.node f_revlog[n].link_rev
1076
+ hasset[n] = true if has_cl_set.include? cl_node
1077
+ end
1078
+
1079
+ prune_parents[f_revlog, hasset, missing_set] # Proc#call
1080
+ end # end proc
1081
+
1082
+ # Function that returns a function.
1083
+ lookup_filenode_link_func = proc do |name|
1084
+ missing_set = missing_fn_set[name]
1085
+
1086
+ # lookup the changenode the filenode belongs to
1087
+ lookup_filenode_link = proc do |node|
1088
+ missing_set[node]
1089
+ end # end proc
1090
+ end # end proc
1091
+
1092
+ # add the nodes that were explicitly requested.
1093
+ add_extra_nodes = proc do |name, nodes|
1094
+ return unless extra_nodes && extra_nodes[name]
1095
+
1096
+ extra_nodes[name].each do |node, link_node|
1097
+ nodes[node] = link_node unless nodes[node]
1098
+ end
1099
+
1100
+ end
1101
+
1102
+ # Now that we have all theses utility functions to help out and
1103
+ # logically divide up the task, generate the group.
1104
+ generate_group = proc do
1105
+ changed_files = {}
1106
+ group = changelog.group(missing_cl_list, identity, &manifest_and_file_collector[changed_files])
1107
+ group.each { |chunk| yield chunk }
1108
+ prune_manifests.call
1109
+ add_extra_nodes[1, msng_mnfst_set]
1110
+ msng_mnfst_lst = msng_mnfst_set.keys
1111
+
1112
+ msng_mnfst_lst.sort!(&cmp_by_rev_function[manifest])
1113
+
1114
+ group = manifest.group(msng_mnfst_lst, lookup_filenode_link,
1115
+ filenode_collector[changed_files])
1116
+
1117
+ group.each {|chunk| yield chunk }
1118
+
1119
+ msng_mnfst_lst = nil
1120
+ msng_mnfst_set.clear
1121
+
1122
+ if extra_nodes
1123
+ extra_nodes.each do |fname|
1124
+ next if fname.kind_of?(Integer)
1125
+ msng_mnfst_set[fname] ||= {}
1126
+ changed_files[fname] = true
1127
+ end
1128
+ end
1129
+
1130
+ changed_files.sort.each do |fname|
1131
+ file_revlog = file(fname)
1132
+ unless file_revlog.size > 0
1133
+ raise abort("empty or missing revlog for #{fname}")
1134
+ end
1135
+
1136
+ if msng_mnfst_set[fname]
1137
+ prune_filenodes[fname, file_revlog]
1138
+ add_extra_nodes[fname, missing_fn_set[fname]]
1139
+ missing_fn_list = missing_fn_set[fname].keys
1140
+ else
1141
+ missing_fn_list = []
1142
+ end
1143
+
1144
+ if missing_fn_list.size > 0
1145
+ yield ChangeGroup.chunk_header(fname.size)
1146
+ yield fname
1147
+ missing_fn_list.sort!(&cmp_by_rev_function[file_revlog])
1148
+ group = file_revlog.group(missing_fn_list,
1149
+ lookup_filenode_link_func[fname])
1150
+ group.each {|chunk| yield chunk }
1151
+ end
1152
+ if missing_fn_set[fname]
1153
+ missing_fn_set.delete fname
1154
+ end
1155
+ end
1156
+
1157
+ yield ChangeGroup.close_chunk
1158
+
1159
+ if missing_cl_list
1160
+ run_hook :post_outgoing
1161
+ end
1162
+ end # end proc
1163
+
1164
+ s = StringIO.new "",(ruby_19? ? "w+:ASCII-8BIT" : "w+")
1165
+ generate_group.call do |chunk|
1166
+ s.write chunk
1167
+ end
1168
+ s.seek(0, IO::SEEK_SET)
1169
+
1170
+ end # end def
1171
+
1172
+ ##
1173
+ # Revert a file or group of files to +revision+. If +opts[:unlink]+
1174
+ # is true, then the files
1175
+ #
1176
+ # @param [Array<String>] files a list of files to revert
1177
+ # @return [Boolean] a success marker
1178
+ def revert(files=nil, opts={})
1179
+ # get the parents - used in checking if we haven an uncommitted merge
1180
+ parent, p2 = dirstate.parents
1181
+
1182
+ # get the revision
1183
+ rev = opts[:revision] || opts[:rev] || opts[:to]
1184
+
1185
+ # check to make sure it's logically possible
1186
+ unless rev || p2 == Amp::Mercurial::RevlogSupport::Node::NULL_ID
1187
+ raise abort("uncommitted merge - please provide a specific revision")
1188
+ end
1189
+
1190
+ # if we have anything here, then create a matcher
1191
+ matcher = if files
1192
+ Amp::Match.create :files => files ,
1193
+ :includer => opts[:include],
1194
+ :excluder => opts[:exclude]
1195
+ else
1196
+ # else just return nil
1197
+ # we can return nil because when it gets used in :match => matcher,
1198
+ # it will be as though it's not even there
1199
+ nil
1200
+ end
1201
+
1202
+ # the changeset we use as a guide
1203
+ changeset = self[rev]
1204
+
1205
+ # get the files that need to be changed
1206
+ stats = status :node_1 => rev, :match => matcher
1207
+
1208
+ ###
1209
+ # now make the changes
1210
+ ###
1211
+
1212
+ ##########
1213
+ # MODIFIED and DELETED
1214
+ ##########
1215
+ # Just write the old data to the files
1216
+ (stats[:modified] + stats[:deleted]).each do |path|
1217
+ File.open path, 'w' do |file|
1218
+ file.write changeset.get_file(path).data
1219
+ end
1220
+ UI::status "restored\t#{path}"
1221
+ end
1222
+
1223
+ ##########
1224
+ # REMOVED
1225
+ ##########
1226
+ # these files are set to be removed, and have thus far been dropped from the filesystem
1227
+ # we restore them and we alert the repo
1228
+ stats[:removed].each do |path|
1229
+ File.open path, 'w' do |file|
1230
+ file.write changeset.get_file(path).data
1231
+ end
1232
+
1233
+ dirstate.normal path # pretend nothing happened
1234
+ UI::status "saved\t#{path}"
1235
+ end
1236
+
1237
+ ##########
1238
+ # ADDED
1239
+ ##########
1240
+ # these files have been added SINCE +rev+
1241
+ stats[:added].each do |path|
1242
+ remove path
1243
+ UI::status "destroyed\t#{path}"
1244
+ end # pretend these files were never even there
1245
+
1246
+ true # success marker
1247
+ end
1248
+
1249
+ ##
1250
+ # Return list of roots of the subsets of missing nodes from remote
1251
+ #
1252
+ # If base dict 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
+ # All the descendants of the list returned are missing in self.
1262
+ # (and so we know that the rest of the nodes are missing in remote, see
1263
+ # outgoing)
1264
+ #
1265
+ # @return [Array<String>] the nodes that are missing from the local repository
1266
+ # but are present in the foreign repo. These are the nodes that will be
1267
+ # coming in over the wire.
1268
+ def find_incoming_roots(remote, opts={:base => nil, :heads => nil,
1269
+ :force => false, :base => nil})
1270
+ common_nodes(remote, opts)[1]
1271
+ end
1272
+
1273
+ ##
1274
+ # Find the common nodes, missing nodes, and remote heads.
1275
+ #
1276
+ # So in this code, we use opts[:base] and fetch as hashes
1277
+ # instead of arrays. We could very well use arrays, but hashes have
1278
+ # O(1) lookup time, and since these could get RFH (Really Fucking
1279
+ # Huge), we decided to take the liberty and just use hash for now.
1280
+ #
1281
+ # If opts[:base] (Hash) is specified, assume that these nodes and their parents
1282
+ # exist on the remote side and that no child of a node of base exists
1283
+ # in both remote and self.
1284
+ # Furthermore base will be updated to include the nodes that exists
1285
+ # in self and remote but no children exists in self and remote.
1286
+ # If a list of heads is specified, return only nodes which are heads
1287
+ # or ancestors of these heads.
1288
+ #
1289
+ # All the ancestors of base are in self and in remote.
1290
+ #
1291
+ # @param [Amp::Repository] remote the repository we're pulling from
1292
+ # @param [(Array<String>, Array<String>, Array<String>)] the common nodes, missing nodes, and
1293
+ # remote heads
1294
+ def common_nodes(remote, opts={:heads => nil, :force => nil, :base => nil})
1295
+ # variable prep!
1296
+ node_map = changelog.node_map
1297
+ search = []
1298
+ unknown = []
1299
+ fetch = {}
1300
+ seen = {}
1301
+ seen_branch = {}
1302
+ opts[:base] ||= {}
1303
+ opts[:heads] ||= remote.heads
1304
+
1305
+ # if we've got nothing...
1306
+ if changelog.tip == NULL_ID
1307
+ opts[:base][NULL_ID] = true # 1 is stored in the Python
1308
+
1309
+ return [NULL_ID], [NULL_ID], opts[:heads].dup unless opts[:heads] == [NULL_ID]
1310
+ return [NULL_ID], [], [] # if we didn't trip ^, we're returning this
1311
+ end
1312
+
1313
+ # assume we're closer to the tip than the root
1314
+ # and start by examining heads
1315
+ UI::status 'searching for changes'
1316
+
1317
+ opts[:heads].each do |head|
1318
+ if !node_map.include?(head)
1319
+ unknown << head
1320
+ else
1321
+ opts[:base][head] = true # 1 is stored in the Python
1322
+ end
1323
+ end
1324
+
1325
+ opts[:heads] = unknown # the ol' switcheroo
1326
+ return opts[:base].keys, [], [] if unknown.empty? # BAIL
1327
+
1328
+ # make a hash with keys of unknown
1329
+ requests = Hash.with_keys unknown
1330
+ count = 0
1331
+
1332
+ # Search through the remote branches
1333
+ # a branch here is a linear part of history, with 4 (four)
1334
+ # parts:
1335
+ #
1336
+ # head, root, first parent, second parent
1337
+ # (a branch always has two parents (or none) by definition)
1338
+ #
1339
+ # Here's where we start using the Hashes instead of Arrays
1340
+ # trick. Keep an eye out for opts[:base] and opts[:heads]!
1341
+ unknown = remote.branches(*unknown)
1342
+ until unknown.empty?
1343
+ r = []
1344
+
1345
+ while node = unknown.shift
1346
+ next if seen.include?(node[0])
1347
+ UI::debug "examining #{short node[0]}:#{short node[1]}"
1348
+
1349
+ if node[0] == NULL_ID
1350
+ # Do nothing...
1351
+ elsif seen_branch.include? node
1352
+ UI::debug 'branch already found'
1353
+ next
1354
+ elsif node_map.include? node[1]
1355
+ UI::debug "found incomplete branch #{short node[0]}:#{short node[1]}"
1356
+ search << node[0..1]
1357
+ seen_branch[node] = true # 1 in the python
1358
+ else
1359
+ unless seen.include?(node[1]) || fetch.include?(node[1])
1360
+ if node_map.include?(node[2]) and node_map.include?(node[3])
1361
+ UI::debug "found new changset #{short node[1]}"
1362
+ fetch[node[1]] = true # 1 in the python
1363
+ end # end if
1364
+
1365
+ node[2..3].each do |p|
1366
+ opts[:base][p] = true if node_map.include? p
1367
+ end
1368
+ end # end unless
1369
+
1370
+ node[2..3].each do |p|
1371
+ unless requests.include?(p) || node_map.include?(p)
1372
+ r << p
1373
+ requests[p] = true # 1 in the python
1374
+ end # end unless
1375
+ end # end each
1376
+ end # end if
1377
+
1378
+ seen[node[0]] = true # 1 in the python
1379
+ end # end while
1380
+
1381
+ unless r.empty?
1382
+ count += 1
1383
+
1384
+ UI::debug "request #{count}: #{r.map{|i| short i }}"
1385
+
1386
+ (0 .. (r.size-1)).step(10) do |p|
1387
+ remote.branches(r[p..(p+9)]).each do |b|
1388
+ UI::debug "received #{short b[0]}:#{short b[1]}"
1389
+ unknown << b
1390
+ end
1391
+ end
1392
+ end # end unless
1393
+ end # end until
1394
+
1395
+ # sorry for the ambiguous variable names
1396
+ # the python doesn't name them either, which
1397
+ # means I have no clue what these are
1398
+ find_proc = proc do |item1, item2|
1399
+ fetch[item1] = true
1400
+ opts[:base][item2] = true
1401
+ end
1402
+
1403
+ # do a binary search on the branches we found
1404
+ search, new_count = *binary_search(:find => search,
1405
+ :repo => remote,
1406
+ :node_map => node_map,
1407
+ :on_find => find_proc)
1408
+ count += new_count # keep keeping track of the total
1409
+
1410
+ # sanity check, because this method is sooooo fucking long
1411
+ fetch.keys.each do |f|
1412
+ if node_map.include? f
1413
+ raise RepoError.new("already have changeset #{short f[0..3]}")
1414
+ end
1415
+ end
1416
+
1417
+ if opts[:base].keys == [NULL_ID]
1418
+ if opts[:force]
1419
+ UI::warn 'repository is unrelated'
1420
+ else
1421
+ raise RepoError.new('repository is unrelated')
1422
+ end
1423
+ end
1424
+
1425
+ UI::debug "found new changesets starting at #{fetch.keys.map{|f| short f }.join ' '}"
1426
+ UI::debug "#{count} total queries"
1427
+
1428
+ # on with the show!
1429
+ [opts[:base].keys, fetch.keys, opts[:heads]]
1430
+ end
1431
+
1432
+ ##
1433
+ # Returns the number of revisions the repository is tracking.
1434
+ #
1435
+ # @return [Integer] how many revisions there have been
1436
+ def size
1437
+ changelog.size
1438
+ end
1439
+
1440
+ ##
1441
+ # Forgets an added file or files from the repository. Doesn't delete the
1442
+ # files, it just says "don't add this on the next commit."
1443
+ #
1444
+ # Please note that this has different semantics from {DirState#forget}
1445
+ #
1446
+ # @param [Array, String] list a file path (or list of file paths) to
1447
+ # "forget".
1448
+ # @return [Boolean] success marker
1449
+ def forget(list)
1450
+ lock_working do
1451
+ list = [*list]
1452
+
1453
+ successful = list.any? do |f|
1454
+ if dirstate[f].status != :added
1455
+ UI.warn "#{f} not being added! can't forget it"
1456
+ false
1457
+ else
1458
+ dirstate.forget f
1459
+ true
1460
+ end
1461
+ end
1462
+
1463
+ dirstate.write if successful
1464
+ end
1465
+
1466
+ true
1467
+ end
1468
+
1469
+ ##
1470
+ # Returns the parents that aren't NULL_ID
1471
+ def living_parents
1472
+ dirstate.parents.select {|p| p != NULL_ID }
1473
+ end
1474
+
1475
+ ##
1476
+ # There are two ways to push to remote repo:
1477
+ #
1478
+ # addchangegroup assumes local user can lock remote
1479
+ # repo (local filesystem, old ssh servers).
1480
+ #
1481
+ # unbundle assumes local user cannot lock remote repo (new ssh
1482
+ # servers, http servers).
1483
+ #
1484
+ # @param [Repository] remote_repo the remote repository object to push to
1485
+ # @param [Hash] options extra options for pushing
1486
+ # @option options [Boolean] :force (false) Force pushing, even if it would create
1487
+ # new heads (or some other error arises)
1488
+ # @option options [Array<Fixnum, String>] :revs ([]) specify which revisions to push
1489
+ # @return [Boolean] for success/failure
1490
+ def push(remote_repo, opts={:force => false, :revs => nil})
1491
+ if remote_repo.capable? "unbundle"
1492
+ push_unbundle remote_repo, opts
1493
+ else
1494
+ push_add_changegroup remote_repo, opts
1495
+ end
1496
+ end
1497
+
1498
+ ##
1499
+ # Push and add a changegroup
1500
+ # @todo -- add default values for +opts+
1501
+ def push_add_changegroup(remote, opts={})
1502
+ # no locking cuz we rockz
1503
+ ret = pre_push remote, opts
1504
+
1505
+ if ret[0]
1506
+ cg, remote_heads = *ret
1507
+ remote.add_changegroup cg, :push, url
1508
+ else
1509
+ ret[1]
1510
+ end
1511
+ end
1512
+
1513
+ ##
1514
+ # Push an unbundled dohickey
1515
+ # @todo -- add default values for +opts+
1516
+ def push_unbundle(remote, opts={})
1517
+ # local repo finds heads on server, finds out what revs it
1518
+ # must push. once revs transferred, if server finds it has
1519
+ # different heads (someone else won commit/push race), server
1520
+ # aborts.
1521
+
1522
+ ret = pre_push remote, opts
1523
+
1524
+ if ret[0]
1525
+ cg, remote_heads = *ret
1526
+ remote_heads = ['force'] if opts[:force]
1527
+ remote.unbundle cg, remote_heads, :push
1528
+ else
1529
+ ret[1]
1530
+ end
1531
+ end
1532
+
1533
+ ##
1534
+ # Return list of nodes that are roots of subsets not in remote
1535
+ #
1536
+ # If base dict is specified, assume that these nodes and their parents
1537
+ # exist on the remote side.
1538
+ # If a list of heads is specified, return only nodes which are heads
1539
+ # or ancestors of these heads, and return a second element which
1540
+ # contains all remote heads which get new children.
1541
+ def find_outgoing_roots(remote, opts={:base => nil, :heads => nil, :force => false})
1542
+ base, heads, force = opts[:base], opts[:heads], opts[:force]
1543
+ if base.nil?
1544
+ base = {}
1545
+ find_incoming_roots remote, :base => base, :heads => heads, :force => force
1546
+ end
1547
+
1548
+ UI::debug("common changesets up to "+base.keys.map {|k| k.short_hex}.join(" "))
1549
+
1550
+ remain = Hash.with_keys changelog.node_map.keys, nil
1551
+
1552
+ # prune everything remote has from the tree
1553
+ remain.delete NULL_ID
1554
+ remove = base.keys
1555
+ while remove.any?
1556
+ node = remove.shift
1557
+ if remain.include? node
1558
+ remain.delete node
1559
+ changelog.parents_for_node(node).each {|p| remove << p }
1560
+ end
1561
+ end
1562
+
1563
+ # find every node whose parents have been pruned
1564
+ subset = []
1565
+ # find every remote head that will get new children
1566
+ updated_heads = {}
1567
+ remain.keys.each do |n|
1568
+ p1, p2 = changelog.parents_for_node n
1569
+ subset << n unless remain.include?(p1) || remain.include?(p2)
1570
+ if heads && heads.any?
1571
+ updated_heads[p1] = true if heads.include? p1
1572
+ updated_heads[p2] = true if heads.include? p2
1573
+ end
1574
+ end
1575
+
1576
+ # this is the set of all roots we have to push
1577
+ if heads && heads.any?
1578
+ return subset, updated_heads.keys
1579
+ else
1580
+ return subset
1581
+ end
1582
+ end
1583
+
1584
+ ##
1585
+ # The branches available in this repository.
1586
+ #
1587
+ # @param [Array<String>] nodes the list of nodes. this can be optionally left empty
1588
+ # @return [Array<String>] the branches, active and inactive!
1589
+ def branches(*nodes)
1590
+ branches = []
1591
+ nodes = [changelog.tip] if nodes.empty?
1592
+
1593
+ # for each node, find its first parent (adam and eve, basically)
1594
+ # -- that's our branch!
1595
+ nodes.each do |node|
1596
+ t = node
1597
+ # traverse the tree, staying to the left side
1598
+ # node
1599
+ # / \
1600
+ # parent1 parent2
1601
+ # .... ....
1602
+ # This will get us the first parent. When it's finally NULL_ID,
1603
+ # we have a root -- this is the basis for our branch.
1604
+ loop do
1605
+ parents = changelog.parents_for_node t
1606
+ if parents[1] != NULL_ID || parents[0] == NULL_ID
1607
+ branches << [node, t, *parents]
1608
+ break
1609
+ end
1610
+ t = parents.first # get the first parent and start again
1611
+ end
1612
+ end
1613
+
1614
+ branches
1615
+ end
1616
+
1617
+ ##
1618
+ # Undelete a file. For instance, if you remove something and then
1619
+ # find out that you NEED that file, you can use this command.
1620
+ #
1621
+ # @param [[String]] list the files to be undeleted
1622
+ def undelete(list)
1623
+ manifests = living_parents.map do |p|
1624
+ manifest.read changelog.read(p).first
1625
+ end
1626
+
1627
+ # now we actually restore the files
1628
+ list.each do |file|
1629
+ unless dirstate[file].removed?
1630
+ UI.warn "#{file} isn't being removed!"
1631
+ else
1632
+ m = manifests[0] || manifests[1]
1633
+ data = file(f).read m[f]
1634
+ add_file file, data, m.flags(f) # add_file is wwrite in the python
1635
+ dirstate.normal f # we know it's clean, we just restored it
1636
+ end
1637
+ end
1638
+ end
1639
+ alias_method :restore, :undelete
1640
+
1641
+ ##
1642
+ # Write data to a file in the CODE repo, not the .hg
1643
+ #
1644
+ # @param [String] file_name
1645
+ # @param [String] data (no trailing newlines are appended)
1646
+ # @param [[String]] flags we're really just looking for links
1647
+ # and executables, here
1648
+ def add_file(file_name, data, flags)
1649
+ data = filter "decode", file_name, data
1650
+ path = working_join file_name
1651
+
1652
+ File.unlink path rescue nil
1653
+
1654
+ if flags.include? 'l' # if it's a link
1655
+ @file_opener.symlink path, data
1656
+ else
1657
+ @file_opener.open(path, 'w') {|f| f.write data }
1658
+ File.set_flag path, false, true if flags.include? 'x'
1659
+ end
1660
+ end
1661
+
1662
+ ##
1663
+ # Returns the node_id's of the heads of the repository.
1664
+ def heads(start=nil, options={:closed => true})
1665
+ heads = changelog.heads(start)
1666
+ should_show = lambda do |head|
1667
+ return true if options[:closed]
1668
+
1669
+ extras = changelog.read(head)[5]
1670
+ return !(extras["close"])
1671
+ end
1672
+ heads = heads.select {|h| should_show[h] }
1673
+ heads.map! {|h| [changelog.rev(h), h] }
1674
+ heads.sort! {|arr1, arr2| arr2[0] <=> arr1[0] }
1675
+ heads.map! {|r, n| n}
1676
+ end
1677
+
1678
+ ##
1679
+ # Walk recursively through the directory tree (or a changeset)
1680
+ # finding all files matched by the match function
1681
+ #
1682
+ # @param [String, Integer] node selects which changeset to walk
1683
+ # @param [Amp::Match] match the matcher decides how to pick the files
1684
+ # @param [Array<String>] an array of filenames
1685
+ def walk(node=nil, match = Match.create({}) { true })
1686
+ self[node].walk match # calls Changeset#walk
1687
+ end
1688
+
1689
+ ##
1690
+ # Returns the requested file at the given revision annotated by
1691
+ # line number, so you can see who committed which lines in the file's
1692
+ # history.
1693
+ #
1694
+ # @param file The name of the file to annotate
1695
+ # @param [Integer, String] rev (nil) The revision to look at for
1696
+ # annotation
1697
+ def annotate(file, revision=nil, opts={})
1698
+ changeset = self[revision]
1699
+ changeset[file].annotate opts[:follow_copies], opts[:line_numbers]
1700
+ end
1701
+
1702
+ ##
1703
+ # Clone a repository.
1704
+ #
1705
+ # Here is what this does, pretty much:
1706
+ # % amp init monkey
1707
+ # % cd monkey
1708
+ # % amp pull http://monkey
1709
+ #
1710
+ # It's so simple it's not even funny.
1711
+ #
1712
+ # @param [Amp::Repository] remote repository to pull from
1713
+ # @param [Array<String>] heads list of revs to clone (forces use of pull)
1714
+ # @param [Boolean] stream do we stream from the remote source?
1715
+ def clone(remote, opts={:revs => [], :stream => false})
1716
+ # now, all clients that can request uncompressed clones can
1717
+ # read repo formats supported by all servers that can serve
1718
+ # them.
1719
+
1720
+ # The streaming case:
1721
+ # if revlog format changes, client will have to check version
1722
+ # and format flags on "stream" capability, and use
1723
+ # uncompressed only if compatible.
1724
+ if opts[:stream] && opts[:revs].any? && remote.capable?('stream')
1725
+ stream_in remote
1726
+ else
1727
+ pull remote, :revs => opts[:revs]
1728
+ end
1729
+ end
1730
+
1731
+ ##
1732
+ # Stream in the data from +remote+.
1733
+ #
1734
+ # @param [Amp::Repository] remote repository to pull from
1735
+ # @return [Integer] the number of heads in the repository minus 1
1736
+ def stream_in(remote)
1737
+ remote.stream_out do |f|
1738
+ l = f.gets # this should be the server code
1739
+
1740
+ unless Integer(l)
1741
+ raise ResponseError.new("Unexpected response from server: #{l}")
1742
+ end
1743
+
1744
+ case l.to_i
1745
+ when 1
1746
+ raise RepoError.new("operation forbidden by server")
1747
+ when 2
1748
+ raise RepoError.new("locking the remote repository failed")
1749
+ end
1750
+
1751
+ UI::status "streaming all changes"
1752
+
1753
+ l = f.gets # this is effectively [total_files, total_bytes].join ' '
1754
+ total_files, total_bytes = *l.split(' ').map {|i| i.to_i }[0..1]
1755
+ UI::status "#{total_files} file#{total_files == 1 ? '' : 's' } to transfer, #{total_bytes.to_human} of data"
1756
+
1757
+ start = Time.now
1758
+ total_files.times do |i|
1759
+ l = f.gets
1760
+ name, size = *l.split("\0")[0..1]
1761
+ size = size.to_i
1762
+ UI::debug "adding #{name} (#{size.to_human})"
1763
+
1764
+ @store.opener.open do |store_file|
1765
+ chunk = f.read size # will return nil if at EOF
1766
+ store_file.write chunk if chunk
1767
+ end
1768
+ end
1769
+
1770
+ elapsed = Time.now - start
1771
+ elapsed = 0.001 if elapsed <= 0
1772
+
1773
+ UI::status("transferred #{total_bytes.to_human} in #{elapsed}" +
1774
+ "second#{elapsed == 1.0 ? '' : 's' } (#{total_bytes.to_f / elapsed}/sec)")
1775
+
1776
+ invalidate!
1777
+ heads.size - 1
1778
+ end
1779
+ end
1780
+
1781
+ ##
1782
+ # Invalidate the repository: delete things and reset others.
1783
+ def invalidate!
1784
+ @changelog = nil
1785
+ @manifest = nil
1786
+
1787
+ invalidate_tag_cache!
1788
+ invalidate_branch_cache!
1789
+ end
1790
+
1791
+ ##
1792
+ # Commits a changeset or set of files to the repository. You will quite often
1793
+ # use this method since it's basically the basis of version control systems.
1794
+ #
1795
+ # @api
1796
+ # @param [Hash] opts the options to this method are all optional, so it's a very
1797
+ # flexible method. Options listed below.
1798
+ # @option opts [Array] :modified ([]) which files have been added or modified
1799
+ # that you want to be added as a changeset.
1800
+ # @option opts [Array] :removed ([]) which files should be removed in this
1801
+ # commit?
1802
+ # @option opts [Hash] :extra ({}) any extra data, such as "close" => true
1803
+ # will close the active branch.
1804
+ # @option opts [String] :message ("") the message for the commit. An editor
1805
+ # will be opened if this is not provided.
1806
+ # @option opts [Boolean] :force (false) Forces the commit, ignoring minor details
1807
+ # like when you try to commit when no files have been changed.
1808
+ # @option opts [Match] :match (nil) A match object to specify how to pick files
1809
+ # to commit. These are useful so you don't accidentally commit ignored files,
1810
+ # for example.
1811
+ # @option opts [Array<String>] :parents (nil) the node IDs of the parents under
1812
+ # which this changeset will be committed. No more than 2 for mercurial.
1813
+ # @option opts [Boolean] :empty_ok (false) Is an empty commit message a-ok?
1814
+ # @option opts [Boolean] :force_editor (false) Do we force the editor to be
1815
+ # opened, even if :message is provided?
1816
+ # @option opts [String] :user (ENV["HGUSER"]) the username to associate with the commit.
1817
+ # Defaults to AmpConfig#username.
1818
+ # @option opts [DateTime, Time, Date] :date (Time.now) the date to mark with
1819
+ # the commit. Useful if you miss a deadline and want to pretend that you actually
1820
+ # made it!
1821
+ # @return [String] the digest referring to this entry in the changelog
1822
+ def commit(options={})
1823
+ pre_commit(options) {|changeset, opts| changeset.commit opts }
1824
+ end
1825
+
1826
+ ##
1827
+ # Prepares a local changeset to be committed. It must take a block
1828
+ # and yield to it the changeset and any options to be passed to
1829
+ # {AbstractChangeset#commit}. This is only ever called by
1830
+ # {AbstractLocalRepository#commit}.
1831
+ #
1832
+ # @example def pre_commit(opts={})
1833
+ # cs = MyChangeset.new :text => 'asdf', :diff => "asdfasd"
1834
+ # raise "fail" if something_happens
1835
+ # yield cs, opts # well MTV, dis where da magic happen
1836
+ # true
1837
+ # end
1838
+ #
1839
+ # @yield [String] the result of {AbstractChangeset#commit}
1840
+ # @yieldparam [AbstractChangeset] the changeset to commit
1841
+ # @yieldparam [Hash] any options to be passed to {AbstractChangeset#commit}
1842
+ # @return [Boolean] success/failure
1843
+ def pre_commit(opts={})
1844
+ [:parents, :modified, :removed].each {|sym| opts[sym] ||= [] }
1845
+ opts[:extra] ||= {}
1846
+ opts[:force] = true if opts[:extra]["close"]
1847
+
1848
+ # lock the working directory and store
1849
+ lock_working_and_store do
1850
+ opts[:use_dirstate] = opts[:parents][0].nil?
1851
+
1852
+ # do we use the dirstate?
1853
+ if opts[:use_dirstate]
1854
+ p1, p2 = dirstate.parents
1855
+
1856
+ if opts[:force] && # we're forcing the commit
1857
+ p2 != NULL_ID && # but we're merging two branches
1858
+ opts[:match] # and we have a partial matcher
1859
+ raise StandardError("cannot partially commit a merge")
1860
+ end
1861
+
1862
+ opts[:update_dirstate] = opts[:modified].any?
1863
+ else
1864
+ p1, p2 = opts[:parents]
1865
+ p2 ||= NULL_ID
1866
+
1867
+ opts[:update_dirstate] = dirstate.parents[0] == p1
1868
+ end
1869
+
1870
+
1871
+ opts[:modified].each do |file|
1872
+ if merge_state.unresolved? file
1873
+ raise StandardError.new("unresolved merge conflicts (see `amp resolve`)")
1874
+ end
1875
+ end
1876
+
1877
+ changes = {:modified => opts[:modified], :removed => opts[:removed]}
1878
+ changeset = Amp::Mercurial::WorkingDirectoryChangeset.new self, :parents => [p1, p2] ,
1879
+ :text => opts[:message],
1880
+ :user => opts[:user] ,
1881
+ :date => opts[:date] ,
1882
+ :extra => opts[:extra] ,
1883
+ :changes => changes
1884
+
1885
+ tailored_hash = opts.only :force, :force_editor, :empty_ok,
1886
+ :use_dirstate, :update_dirstate
1887
+ revision = yield changeset, tailored_hash
1888
+
1889
+ merge_state.reset
1890
+ return revision
1891
+ end #unlock working dir + store
1892
+ end
1893
+
1894
+ private
1895
+
1896
+ ##
1897
+ # Make the dummy changelog at .hg/00changelog.i
1898
+ def make_changelog
1899
+ @hg_opener.open "00changelog.i", "w" do |file|
1900
+ file.write "\0\0\0\2" # represents revlogv2
1901
+ file.write " dummy changelog to avoid using the old repo type"
1902
+ end
1903
+ end
1904
+
1905
+ ##
1906
+ # Write the requirements file. This returns the requirements passed
1907
+ # so that it can be the final method call in #init
1908
+ def write_requires(requirements)
1909
+ @hg_opener.open "requires", "w" do |require_file|
1910
+ requirements.each {|r| require_file.puts r }
1911
+ end
1912
+ requirements
1913
+ end
1914
+
1915
+
1916
+ ##
1917
+ # do a binary search
1918
+ # used by common_nodes
1919
+ #
1920
+ # Hash info!
1921
+ # :find => the stuff we're searching through
1922
+ # :on_find => what to do when we've got something new
1923
+ # :repo => usually the remote repo where we get new info from
1924
+ # :node_map => the nodes in the current changelog
1925
+ def binary_search(opts={})
1926
+ # I have a lot of stuff to do for scouts
1927
+ # but instead i'm doing this
1928
+ # hizzah!
1929
+ count = 0
1930
+
1931
+ until opts[:find].empty?
1932
+ new_search = []
1933
+ count += 1
1934
+
1935
+ zipped = opts[:find].zip opts[:repo].between(opts[:find])
1936
+ zipped.each do |(n, list)|
1937
+ list << n[1]
1938
+ p = n[0]
1939
+ f = 1 # ??? why are these vars so NAMELESS
1940
+
1941
+ list.each do |item|
1942
+ UI::debug "narrowing #{f}:#{list.size} #{short item}"
1943
+
1944
+ if opts[:node_map].include? item
1945
+ if f <= 2
1946
+ opts[:on_find].call(p, item)
1947
+ else
1948
+ UI::debug "narrowed branch search to #{short p}:#{short item}"
1949
+ new_search << [p, item]
1950
+ end
1951
+ break
1952
+ end
1953
+
1954
+ p, f = item, f*2
1955
+ end
1956
+ end
1957
+
1958
+ opts[:find] = new_search
1959
+ end
1960
+
1961
+ [opts[:find], count]
1962
+ end
1963
+
1964
+ ##
1965
+ # this is called before every push
1966
+ # @todo -- add default values for +opts+
1967
+ def pre_push(remote, opts={})
1968
+ common = {}
1969
+ remote_heads = remote.heads
1970
+ inc = common_nodes remote, :base => common, :heads => remote_heads, :force => true
1971
+ inc = inc[1]
1972
+ update, updated_heads = find_outgoing_roots remote, :base => common, :heads => remote_heads
1973
+
1974
+ if opts[:revs]
1975
+ btw = changelog.nodes_between(update, opts[:revs])
1976
+ missing_cl, bases, heads = btw[:between], btw[:roots], btw[:heads]
1977
+ else
1978
+ bases, heads = update, changelog.heads
1979
+ end
1980
+ if bases.empty?
1981
+ UI::status 'no changes found'
1982
+ return nil, 1
1983
+ elsif !opts[:force]
1984
+ # check if we're creating new remote heads
1985
+ # to be a remote head after push, node must be either
1986
+ # - unknown locally
1987
+ # - a local outgoing head descended from update
1988
+ # - a remote head that's known locally and not
1989
+ # ancestral to an outgoing head
1990
+
1991
+ warn = false
1992
+ if remote_heads == [NULL_ID]
1993
+ warn = false
1994
+ elsif (opts[:revs].nil? || opts[:revs].empty?) and heads.size > remote_heads.size
1995
+ warn = true
1996
+ else
1997
+ new_heads = heads
1998
+ remote_heads.each do |r|
1999
+ if changelog.node_map.include? r
2000
+ desc = changelog.heads r, heads
2001
+ l = heads.select {|h| desc.include? h }
2002
+
2003
+ new_heads << r if l.empty?
2004
+ else
2005
+ new_heads << r
2006
+ end
2007
+ end
2008
+
2009
+ warn = true if new_heads.size > remote_heads.size
2010
+ end
2011
+
2012
+ if warn
2013
+ UI::status 'abort: push creates new remote heads!'
2014
+ UI::status '(did you forget to merge? use push -f to forge)'
2015
+ return nil, 0
2016
+ elsif inc.any?
2017
+ UI::note 'unsynced remote changes!'
2018
+ end
2019
+ end
2020
+
2021
+ if opts[:revs].nil?
2022
+ # use the fast path, no race possible on push
2023
+ cg = get_changegroup common.keys, :push
2024
+ else
2025
+ cg = changegroup_subset update, revs, :push
2026
+ end
2027
+
2028
+ [cg, remote_heads]
2029
+ end
2030
+
2031
+ end # localrepo
2032
+ end # repo
2033
+ end
2034
+ end