amp 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (295) hide show
  1. data/.gitignore +1 -0
  2. data/.hgignore +26 -0
  3. data/AUTHORS +2 -0
  4. data/History.txt +6 -0
  5. data/LICENSE +37 -0
  6. data/MANIFESTO +7 -0
  7. data/Manifest.txt +294 -0
  8. data/README.md +129 -0
  9. data/Rakefile +102 -0
  10. data/SCHEDULE.markdown +12 -0
  11. data/STYLE +27 -0
  12. data/TODO.markdown +149 -0
  13. data/ampfile.rb +47 -0
  14. data/bin/amp +30 -0
  15. data/bin/amp1.9 +30 -0
  16. data/ext/amp/bz2/README.txt +39 -0
  17. data/ext/amp/bz2/bz2.c +1582 -0
  18. data/ext/amp/bz2/extconf.rb +77 -0
  19. data/ext/amp/bz2/mkmf.log +29 -0
  20. data/ext/amp/mercurial_patch/extconf.rb +5 -0
  21. data/ext/amp/mercurial_patch/mpatch.c +405 -0
  22. data/ext/amp/priority_queue/extconf.rb +5 -0
  23. data/ext/amp/priority_queue/priority_queue.c +947 -0
  24. data/ext/amp/support/extconf.rb +5 -0
  25. data/ext/amp/support/support.c +250 -0
  26. data/lib/amp.rb +200 -0
  27. data/lib/amp/commands/command.rb +507 -0
  28. data/lib/amp/commands/command_support.rb +137 -0
  29. data/lib/amp/commands/commands/config.rb +143 -0
  30. data/lib/amp/commands/commands/help.rb +29 -0
  31. data/lib/amp/commands/commands/init.rb +10 -0
  32. data/lib/amp/commands/commands/templates.rb +137 -0
  33. data/lib/amp/commands/commands/version.rb +7 -0
  34. data/lib/amp/commands/commands/workflow.rb +28 -0
  35. data/lib/amp/commands/commands/workflows/git/add.rb +65 -0
  36. data/lib/amp/commands/commands/workflows/git/copy.rb +27 -0
  37. data/lib/amp/commands/commands/workflows/git/mv.rb +23 -0
  38. data/lib/amp/commands/commands/workflows/git/rm.rb +60 -0
  39. data/lib/amp/commands/commands/workflows/hg/add.rb +53 -0
  40. data/lib/amp/commands/commands/workflows/hg/addremove.rb +86 -0
  41. data/lib/amp/commands/commands/workflows/hg/annotate.rb +46 -0
  42. data/lib/amp/commands/commands/workflows/hg/archive.rb +126 -0
  43. data/lib/amp/commands/commands/workflows/hg/branch.rb +28 -0
  44. data/lib/amp/commands/commands/workflows/hg/branches.rb +30 -0
  45. data/lib/amp/commands/commands/workflows/hg/bundle.rb +115 -0
  46. data/lib/amp/commands/commands/workflows/hg/clone.rb +95 -0
  47. data/lib/amp/commands/commands/workflows/hg/commit.rb +42 -0
  48. data/lib/amp/commands/commands/workflows/hg/copy.rb +31 -0
  49. data/lib/amp/commands/commands/workflows/hg/debug/dirstate.rb +32 -0
  50. data/lib/amp/commands/commands/workflows/hg/debug/index.rb +36 -0
  51. data/lib/amp/commands/commands/workflows/hg/default.rb +9 -0
  52. data/lib/amp/commands/commands/workflows/hg/diff.rb +30 -0
  53. data/lib/amp/commands/commands/workflows/hg/forget.rb +11 -0
  54. data/lib/amp/commands/commands/workflows/hg/heads.rb +25 -0
  55. data/lib/amp/commands/commands/workflows/hg/identify.rb +23 -0
  56. data/lib/amp/commands/commands/workflows/hg/import.rb +135 -0
  57. data/lib/amp/commands/commands/workflows/hg/incoming.rb +85 -0
  58. data/lib/amp/commands/commands/workflows/hg/info.rb +18 -0
  59. data/lib/amp/commands/commands/workflows/hg/log.rb +21 -0
  60. data/lib/amp/commands/commands/workflows/hg/manifest.rb +13 -0
  61. data/lib/amp/commands/commands/workflows/hg/merge.rb +53 -0
  62. data/lib/amp/commands/commands/workflows/hg/move.rb +28 -0
  63. data/lib/amp/commands/commands/workflows/hg/outgoing.rb +61 -0
  64. data/lib/amp/commands/commands/workflows/hg/pull.rb +74 -0
  65. data/lib/amp/commands/commands/workflows/hg/push.rb +20 -0
  66. data/lib/amp/commands/commands/workflows/hg/remove.rb +45 -0
  67. data/lib/amp/commands/commands/workflows/hg/resolve.rb +83 -0
  68. data/lib/amp/commands/commands/workflows/hg/revert.rb +53 -0
  69. data/lib/amp/commands/commands/workflows/hg/root.rb +13 -0
  70. data/lib/amp/commands/commands/workflows/hg/serve.rb +38 -0
  71. data/lib/amp/commands/commands/workflows/hg/status.rb +116 -0
  72. data/lib/amp/commands/commands/workflows/hg/tag.rb +69 -0
  73. data/lib/amp/commands/commands/workflows/hg/tags.rb +27 -0
  74. data/lib/amp/commands/commands/workflows/hg/tip.rb +13 -0
  75. data/lib/amp/commands/commands/workflows/hg/update.rb +27 -0
  76. data/lib/amp/commands/commands/workflows/hg/verify.rb +9 -0
  77. data/lib/amp/commands/commands/workflows/hg/view.rb +36 -0
  78. data/lib/amp/commands/dispatch.rb +181 -0
  79. data/lib/amp/commands/hooks.rb +81 -0
  80. data/lib/amp/dependencies/amp_support.rb +1 -0
  81. data/lib/amp/dependencies/amp_support/ruby_amp_support.rb +103 -0
  82. data/lib/amp/dependencies/minitar.rb +979 -0
  83. data/lib/amp/dependencies/priority_queue.rb +18 -0
  84. data/lib/amp/dependencies/priority_queue/c_priority_queue.rb +1 -0
  85. data/lib/amp/dependencies/priority_queue/poor_priority_queue.rb +46 -0
  86. data/lib/amp/dependencies/priority_queue/ruby_priority_queue.rb +525 -0
  87. data/lib/amp/dependencies/python_config.rb +211 -0
  88. data/lib/amp/dependencies/trollop.rb +713 -0
  89. data/lib/amp/dependencies/zip/ioextras.rb +155 -0
  90. data/lib/amp/dependencies/zip/stdrubyext.rb +111 -0
  91. data/lib/amp/dependencies/zip/tempfile_bugfixed.rb +186 -0
  92. data/lib/amp/dependencies/zip/zip.rb +1850 -0
  93. data/lib/amp/dependencies/zip/zipfilesystem.rb +609 -0
  94. data/lib/amp/dependencies/zip/ziprequire.rb +90 -0
  95. data/lib/amp/encoding/base85.rb +97 -0
  96. data/lib/amp/encoding/binary_diff.rb +82 -0
  97. data/lib/amp/encoding/difflib.rb +166 -0
  98. data/lib/amp/encoding/mercurial_diff.rb +378 -0
  99. data/lib/amp/encoding/mercurial_patch.rb +1 -0
  100. data/lib/amp/encoding/patch.rb +292 -0
  101. data/lib/amp/encoding/pure_ruby/ruby_mercurial_patch.rb +123 -0
  102. data/lib/amp/extensions/ditz.rb +41 -0
  103. data/lib/amp/extensions/lighthouse.rb +167 -0
  104. data/lib/amp/graphs/ancestor.rb +147 -0
  105. data/lib/amp/graphs/copies.rb +261 -0
  106. data/lib/amp/merges/merge_state.rb +164 -0
  107. data/lib/amp/merges/merge_ui.rb +322 -0
  108. data/lib/amp/merges/simple_merge.rb +450 -0
  109. data/lib/amp/profiling_hacks.rb +36 -0
  110. data/lib/amp/repository/branch_manager.rb +234 -0
  111. data/lib/amp/repository/dir_state.rb +950 -0
  112. data/lib/amp/repository/journal.rb +203 -0
  113. data/lib/amp/repository/lock.rb +207 -0
  114. data/lib/amp/repository/repositories/bundle_repository.rb +214 -0
  115. data/lib/amp/repository/repositories/http_repository.rb +377 -0
  116. data/lib/amp/repository/repositories/local_repository.rb +2661 -0
  117. data/lib/amp/repository/repository.rb +94 -0
  118. data/lib/amp/repository/store.rb +485 -0
  119. data/lib/amp/repository/tag_manager.rb +319 -0
  120. data/lib/amp/repository/updatable.rb +532 -0
  121. data/lib/amp/repository/verification.rb +431 -0
  122. data/lib/amp/repository/versioned_file.rb +475 -0
  123. data/lib/amp/revlogs/bundle_revlogs.rb +246 -0
  124. data/lib/amp/revlogs/changegroup.rb +217 -0
  125. data/lib/amp/revlogs/changelog.rb +338 -0
  126. data/lib/amp/revlogs/changeset.rb +521 -0
  127. data/lib/amp/revlogs/file_log.rb +165 -0
  128. data/lib/amp/revlogs/index.rb +493 -0
  129. data/lib/amp/revlogs/manifest.rb +195 -0
  130. data/lib/amp/revlogs/node.rb +18 -0
  131. data/lib/amp/revlogs/revlog.rb +1032 -0
  132. data/lib/amp/revlogs/revlog_support.rb +126 -0
  133. data/lib/amp/server/amp_user.rb +44 -0
  134. data/lib/amp/server/extension/amp_extension.rb +396 -0
  135. data/lib/amp/server/extension/authorization.rb +201 -0
  136. data/lib/amp/server/fancy_http_server.rb +252 -0
  137. data/lib/amp/server/fancy_views/_browser.haml +28 -0
  138. data/lib/amp/server/fancy_views/_diff_file.haml +13 -0
  139. data/lib/amp/server/fancy_views/_navbar.haml +17 -0
  140. data/lib/amp/server/fancy_views/changeset.haml +31 -0
  141. data/lib/amp/server/fancy_views/commits.haml +32 -0
  142. data/lib/amp/server/fancy_views/file.haml +35 -0
  143. data/lib/amp/server/fancy_views/file_diff.haml +23 -0
  144. data/lib/amp/server/fancy_views/harshcss/all_hallows_eve.css +72 -0
  145. data/lib/amp/server/fancy_views/harshcss/amy.css +147 -0
  146. data/lib/amp/server/fancy_views/harshcss/twilight.css +138 -0
  147. data/lib/amp/server/fancy_views/stylesheet.sass +175 -0
  148. data/lib/amp/server/http_server.rb +140 -0
  149. data/lib/amp/server/repo_user_management.rb +287 -0
  150. data/lib/amp/support/amp_config.rb +164 -0
  151. data/lib/amp/support/amp_ui.rb +287 -0
  152. data/lib/amp/support/docs.rb +54 -0
  153. data/lib/amp/support/generator.rb +78 -0
  154. data/lib/amp/support/ignore.rb +144 -0
  155. data/lib/amp/support/loaders.rb +93 -0
  156. data/lib/amp/support/logger.rb +103 -0
  157. data/lib/amp/support/match.rb +151 -0
  158. data/lib/amp/support/multi_io.rb +87 -0
  159. data/lib/amp/support/openers.rb +121 -0
  160. data/lib/amp/support/ruby_19_compatibility.rb +66 -0
  161. data/lib/amp/support/support.rb +1095 -0
  162. data/lib/amp/templates/blank.commit.erb +23 -0
  163. data/lib/amp/templates/blank.log.erb +18 -0
  164. data/lib/amp/templates/default.commit.erb +23 -0
  165. data/lib/amp/templates/default.log.erb +26 -0
  166. data/lib/amp/templates/template.rb +165 -0
  167. data/site/Rakefile +24 -0
  168. data/site/src/about/ampfile.haml +57 -0
  169. data/site/src/about/commands.haml +106 -0
  170. data/site/src/about/index.haml +33 -0
  171. data/site/src/about/performance.haml +31 -0
  172. data/site/src/about/workflows.haml +34 -0
  173. data/site/src/contribute/index.haml +65 -0
  174. data/site/src/contribute/style.haml +297 -0
  175. data/site/src/css/active4d.css +114 -0
  176. data/site/src/css/all_hallows_eve.css +72 -0
  177. data/site/src/css/all_themes.css +3299 -0
  178. data/site/src/css/amp.css +260 -0
  179. data/site/src/css/amy.css +147 -0
  180. data/site/src/css/blackboard.css +88 -0
  181. data/site/src/css/brilliance_black.css +605 -0
  182. data/site/src/css/brilliance_dull.css +599 -0
  183. data/site/src/css/cobalt.css +149 -0
  184. data/site/src/css/cur_amp.css +185 -0
  185. data/site/src/css/dawn.css +121 -0
  186. data/site/src/css/eiffel.css +121 -0
  187. data/site/src/css/espresso_libre.css +109 -0
  188. data/site/src/css/idle.css +62 -0
  189. data/site/src/css/iplastic.css +80 -0
  190. data/site/src/css/lazy.css +73 -0
  191. data/site/src/css/mac_classic.css +123 -0
  192. data/site/src/css/magicwb_amiga.css +104 -0
  193. data/site/src/css/pastels_on_dark.css +188 -0
  194. data/site/src/css/reset.css +55 -0
  195. data/site/src/css/slush_poppies.css +85 -0
  196. data/site/src/css/spacecadet.css +51 -0
  197. data/site/src/css/sunburst.css +180 -0
  198. data/site/src/css/twilight.css +137 -0
  199. data/site/src/css/zenburnesque.css +91 -0
  200. data/site/src/get/index.haml +32 -0
  201. data/site/src/helpers.rb +121 -0
  202. data/site/src/images/amp_logo.png +0 -0
  203. data/site/src/images/carbonica.png +0 -0
  204. data/site/src/images/revolution.png +0 -0
  205. data/site/src/images/tab-bg.png +0 -0
  206. data/site/src/images/tab-sliding-left.png +0 -0
  207. data/site/src/images/tab-sliding-right.png +0 -0
  208. data/site/src/include/_footer.haml +22 -0
  209. data/site/src/include/_header.haml +17 -0
  210. data/site/src/index.haml +104 -0
  211. data/site/src/learn/index.haml +46 -0
  212. data/site/src/scripts/jquery-1.3.2.min.js +19 -0
  213. data/site/src/scripts/jquery.cookie.js +96 -0
  214. data/tasks/stats.rake +155 -0
  215. data/tasks/yard.rake +171 -0
  216. data/test/dirstate_tests/dirstate +0 -0
  217. data/test/dirstate_tests/hgrc +5 -0
  218. data/test/dirstate_tests/test_dir_state.rb +192 -0
  219. data/test/functional_tests/resources/.hgignore +2 -0
  220. data/test/functional_tests/resources/STYLE.txt +25 -0
  221. data/test/functional_tests/resources/command.rb +372 -0
  222. data/test/functional_tests/resources/commands/annotate.rb +57 -0
  223. data/test/functional_tests/resources/commands/experimental/lolcats.rb +17 -0
  224. data/test/functional_tests/resources/commands/heads.rb +22 -0
  225. data/test/functional_tests/resources/commands/manifest.rb +12 -0
  226. data/test/functional_tests/resources/commands/status.rb +90 -0
  227. data/test/functional_tests/resources/version2/.hgignore +5 -0
  228. data/test/functional_tests/resources/version2/STYLE.txt +25 -0
  229. data/test/functional_tests/resources/version2/command.rb +372 -0
  230. data/test/functional_tests/resources/version2/commands/annotate.rb +45 -0
  231. data/test/functional_tests/resources/version2/commands/experimental/lolcats.rb +17 -0
  232. data/test/functional_tests/resources/version2/commands/heads.rb +22 -0
  233. data/test/functional_tests/resources/version2/commands/manifest.rb +12 -0
  234. data/test/functional_tests/resources/version2/commands/status.rb +90 -0
  235. data/test/functional_tests/resources/version3/.hgignore +5 -0
  236. data/test/functional_tests/resources/version3/STYLE.txt +31 -0
  237. data/test/functional_tests/resources/version3/command.rb +376 -0
  238. data/test/functional_tests/resources/version3/commands/annotate.rb +45 -0
  239. data/test/functional_tests/resources/version3/commands/experimental/lolcats.rb +17 -0
  240. data/test/functional_tests/resources/version3/commands/heads.rb +22 -0
  241. data/test/functional_tests/resources/version3/commands/manifest.rb +12 -0
  242. data/test/functional_tests/resources/version3/commands/status.rb +90 -0
  243. data/test/functional_tests/resources/version4/.hgignore +5 -0
  244. data/test/functional_tests/resources/version4/STYLE.txt +31 -0
  245. data/test/functional_tests/resources/version4/command.rb +376 -0
  246. data/test/functional_tests/resources/version4/commands/experimental/lolcats.rb +17 -0
  247. data/test/functional_tests/resources/version4/commands/heads.rb +22 -0
  248. data/test/functional_tests/resources/version4/commands/manifest.rb +12 -0
  249. data/test/functional_tests/resources/version4/commands/stats.rb +25 -0
  250. data/test/functional_tests/resources/version4/commands/status.rb +90 -0
  251. data/test/functional_tests/resources/version5_1/.hgignore +5 -0
  252. data/test/functional_tests/resources/version5_1/STYLE.txt +2 -0
  253. data/test/functional_tests/resources/version5_1/command.rb +374 -0
  254. data/test/functional_tests/resources/version5_1/commands/experimental/lolcats.rb +17 -0
  255. data/test/functional_tests/resources/version5_1/commands/heads.rb +22 -0
  256. data/test/functional_tests/resources/version5_1/commands/manifest.rb +12 -0
  257. data/test/functional_tests/resources/version5_1/commands/stats.rb +25 -0
  258. data/test/functional_tests/resources/version5_1/commands/status.rb +90 -0
  259. data/test/functional_tests/resources/version5_2/.hgignore +5 -0
  260. data/test/functional_tests/resources/version5_2/STYLE.txt +14 -0
  261. data/test/functional_tests/resources/version5_2/command.rb +376 -0
  262. data/test/functional_tests/resources/version5_2/commands/experimental/lolcats.rb +17 -0
  263. data/test/functional_tests/resources/version5_2/commands/manifest.rb +12 -0
  264. data/test/functional_tests/resources/version5_2/commands/newz.rb +12 -0
  265. data/test/functional_tests/resources/version5_2/commands/stats.rb +25 -0
  266. data/test/functional_tests/resources/version5_2/commands/status.rb +90 -0
  267. data/test/functional_tests/test_functional.rb +604 -0
  268. data/test/localrepo_tests/test_local_repo.rb +121 -0
  269. data/test/localrepo_tests/testrepo.tar.gz +0 -0
  270. data/test/manifest_tests/00manifest.i +0 -0
  271. data/test/manifest_tests/test_manifest.rb +72 -0
  272. data/test/merge_tests/base.txt +10 -0
  273. data/test/merge_tests/expected.local.txt +16 -0
  274. data/test/merge_tests/local.txt +11 -0
  275. data/test/merge_tests/remote.txt +11 -0
  276. data/test/merge_tests/test_merge.rb +26 -0
  277. data/test/revlog_tests/00changelog.i +0 -0
  278. data/test/revlog_tests/revision_added_changelog.i +0 -0
  279. data/test/revlog_tests/test_adding_index.i +0 -0
  280. data/test/revlog_tests/test_revlog.rb +333 -0
  281. data/test/revlog_tests/testindex.i +0 -0
  282. data/test/store_tests/store.tar.gz +0 -0
  283. data/test/store_tests/test_fncache_store.rb +122 -0
  284. data/test/test_amp.rb +9 -0
  285. data/test/test_base85.rb +14 -0
  286. data/test/test_bdiff.rb +42 -0
  287. data/test/test_commands.rb +122 -0
  288. data/test/test_difflib.rb +50 -0
  289. data/test/test_helper.rb +15 -0
  290. data/test/test_journal.rb +29 -0
  291. data/test/test_match.rb +134 -0
  292. data/test/test_mdiff.rb +74 -0
  293. data/test/test_mpatch.rb +14 -0
  294. data/test/test_support.rb +24 -0
  295. metadata +385 -0
@@ -0,0 +1,234 @@
1
+ module Amp
2
+ module Repositories
3
+ ##
4
+ # = BranchManager
5
+ # Michael Scott for Amp.
6
+ #
7
+ # More seriously, this class handles reading/writing to the branch cache
8
+ # and figuring out what the head revisions are for each branch and such.
9
+ module BranchManager
10
+ include Amp::RevlogSupport::Node
11
+
12
+ ##
13
+ # Saves the branches with the given "partial" and the last_rev index.
14
+ def save_branch_cache(partial, last_rev)
15
+ tiprev = self.size - 1
16
+ # If our cache is outdated, then update it and re-write it
17
+ if last_rev != tiprev
18
+ # search for new heads
19
+ update_branches!(partial, last_rev+1, tiprev+1)
20
+ # write our data out
21
+ write_branches!(partial, self.changelog.tip, tiprev)
22
+ end
23
+ # return our mappings
24
+ partial
25
+ end
26
+
27
+ ##
28
+ # Loads the head revisions for each branch. Each branch has at least one, but
29
+ # possible more than one, head.
30
+ #
31
+ # @return [Hash] a map, where the branch names are keys and the values
32
+ # are the heads of the branch
33
+ def branch_heads
34
+ # Gets the mapping of branch names to branch heads, but uses caching to avoid
35
+ # doing IO and tedious computation over and over. As long as our tip doesn't
36
+ # change, the cache will remain valid.
37
+
38
+ # Check our current tip
39
+ tip = self.changelog.tip
40
+ # Do we have a cache, and if we do, is the saved cache == tip?
41
+ if !@branch_cache.nil? && @branch_cache_tip == tip
42
+ # if so, cache HIT
43
+ return @branch_cache
44
+ end
45
+
46
+ # nope? cache miss
47
+ # save the old tip
48
+ oldtip = @branch_cache_tip
49
+ # save the new tip
50
+ @branch_cache_tip = tip
51
+
52
+ # reset the branch cache
53
+ @branch_cache = @branch_cache.nil? ? {} : @branch_cache.clear # returns same hash, but empty
54
+ # if we didn't have an old cache, or it was invalid, read in the branches again
55
+ if oldtip.nil? || self.changelog.node_map[oldtip].nil?
56
+ partial, last, last_rev = read_branches!
57
+ else
58
+ # Otherwise, dig up the cached hash!
59
+ last_rev = self.changelog.rev(oldtip)
60
+ # Get the last branch cache
61
+ partial = @u_branch_cache
62
+ end
63
+ # Save the branch cache (updating it if we have to)
64
+ save_branch_cache(partial, last_rev)
65
+
66
+ # Cache our saved hash
67
+ @u_branch_cache = partial
68
+
69
+ # Copy the partial into the branch_cache
70
+ partial.each { |k, v| @branch_cache[k] = v }
71
+ @branch_cache
72
+ end
73
+
74
+ # Returns a dict where branch names map to the tipmost head of
75
+ # the branch, open heads come before closed
76
+ def branch_tags
77
+ tags = {}
78
+ branch_heads.each do |branch_node, local_heads|
79
+ head = nil
80
+ local_heads.reverse_each do |h| # get the tip if its a closed
81
+ if !(changelog.read(h)[5].include?("close"))
82
+ head = h
83
+ break
84
+ end
85
+ end
86
+ head = local_heads.last if head.nil?
87
+ tags[branch_node] = head # it's the tip
88
+ end
89
+ return tags
90
+ end
91
+
92
+ ##
93
+ # Saves the branches, the tip-most node, and the tip-most revision
94
+ # to the branch cache.
95
+ #
96
+ def save_branches(branches, tip, tip_revision)
97
+ write_branches!(branches, tip, tip_revision)
98
+ end
99
+
100
+ private
101
+
102
+ ##
103
+ # Reads in the branches from the branch.cache file, stored in the root
104
+ # of the repository's .hg folder. While the repository could figure out
105
+ # what each branch's heads are each time the program is run, that would
106
+ # be quite slow. So we cache them in a file, along with the tip of the
107
+ # repository, so we know if our cache has become inaccurate.
108
+ # The format is very simple:
109
+ # [tip_node_id] [tip_revision_number]
110
+ # [branch_head_node_id] [branch_name]
111
+ # [branch_head_node_id] [branch_name]
112
+ # [branch_head_node_id] [branch_name]
113
+ #
114
+ # Example:
115
+ # 0abc3135810abc3135810abc3135810abc313581 603
116
+ # 0abc3135810abc3135810abc3135810abc313581 default
117
+ # 1234567890123456789012345678901234567890 other_branch
118
+ # 0987654321098765432109876543210987654321 other_branch
119
+ #
120
+ # In the example, other_branch has 2 heads. This is acceptable. The tip of the
121
+ # repository is node 0abc3135, revision 603, which is the only head of the default
122
+ # branch.
123
+ #
124
+ # @return [[Hash, String, Integer]] The results are returned in the form of:
125
+ # [partial, tip_node_id, tip_rev_index], where +partial+ is a mapping of
126
+ # branch names to an array of their heads.
127
+ def read_branches!
128
+ partial, last, last_rev = {}, nil, nil
129
+ lines = nil
130
+ invalid = false
131
+
132
+ begin
133
+ # read in all the lines. This file should be short, so don't worry about
134
+ # performance concerns of a File.read() call (this call is actually
135
+ # Opener#read, which then calls File.read)
136
+ lines = @hg_opener.read("branchheads.cache").split("\n")
137
+ rescue SystemCallError # IO Errors, i.e. if there is no branch.cache file
138
+ return {}, NULL_ID, NULL_REV
139
+ end
140
+ # use catch, not exceptions (exceptions are more costly)
141
+ valid = catch(:invalid) do
142
+ # Read in the tip node and tip revision #
143
+ last, last_rev = lines.shift.split(" ", 2)
144
+ last, last_rev = last.unhexlify, last_rev.to_i
145
+
146
+ # if we aren't matching up with the current repo, then invalidate the cache
147
+ if last_rev > self.size || self[last_rev].node != last
148
+ throw :invalid, false
149
+ end
150
+
151
+ # Go through each next line and read in a head-branch pair
152
+ lines.each do |line|
153
+ # empty = useless line
154
+ next if line.nil? || line.empty?
155
+ # split on " ", only once so we can have a space in a branch name
156
+ node, _label = line.split(" ", 2)
157
+ # and assign to our "partial" i.e. our list of branch-heads
158
+ partial[_label.strip] ||= []
159
+ partial[_label.strip] << node.unhexlify
160
+ end
161
+ end
162
+
163
+ # if invalid was thrown.... bail
164
+ unless valid
165
+ UI.puts("invalidating branch cache (tip different)")
166
+ partial, last, last_rev = {}, NULL_ID, NULL_REV
167
+ end
168
+
169
+ # Return our results!
170
+ [partial, last, last_rev]
171
+ end
172
+
173
+ ##
174
+ # Invalidates the tag cache. Removes all ivars relating to tags.
175
+ def invalidate_branch_cache!
176
+ @branch_cache = nil
177
+ @branch_cache_tip = nil
178
+ @u_branch_cache = nil
179
+ end
180
+
181
+ ##
182
+ # Goes through from revision +start+ to revision +stop+ and searches for
183
+ # new branch heads for each branch. Annoying, yes. But necessary to keep the
184
+ # cache up to date.
185
+ #
186
+ # @param [Hash] partial the current pairing of branch names to heads. Might
187
+ # be incomplete, which is why it's called "partial"
188
+ # @param [Integer] start the revision # to start looking new branch heads
189
+ # @param [Integer] stop the last revision in which to look for branch heads
190
+ def update_branches!(partial, start, stop)
191
+ (start..(stop-1)).each do |r|
192
+ # get the changeset
193
+ changeset = self[r]
194
+ # look at its branch
195
+ branch = changeset.branch
196
+ # get that branch's partial list of heads
197
+
198
+ branch_heads = (partial[branch] ||= [])
199
+ # add this changeset
200
+ branch_heads << changeset.node
201
+ # remove our parents from this branch's list of heads if they're in there,
202
+ # because if they have children, they aren't heads.
203
+ changeset.parents.each do |parent|
204
+ # get the node_id
205
+ parent_node = parent.node
206
+ # remove the parent
207
+ branch_heads.delete parent_node if branch_heads.include? parent_node
208
+ end
209
+ end
210
+ end
211
+
212
+ ##
213
+ # Writes the branches out to the branch cache. Simple as that. See #read_branches!
214
+ # for the file format.
215
+ #
216
+ # @see read_branches!
217
+ # @param [Hash] branches a mapping of branch names to arrays of their head node IDs
218
+ # @param [String] tip the tip of this repository's node ID
219
+ # @param [Integer] tip_revision the index # of this repository's tip
220
+ def write_branches!(branches, tip, tip_revision)
221
+ @hg_opener.open "branchheads.cache", "w" do |f|
222
+ f << "#{tip.hexlify} #{tip_revision}\n"
223
+ branches.each do |_label, nodes|
224
+ nodes.each {|node| f << "#{node.hexlify} #{_label}\n" }
225
+ end
226
+ end
227
+ rescue SystemCallError
228
+
229
+ end
230
+ end
231
+ MichaelScott = BranchManager # hehehe
232
+ end
233
+ end
234
+
@@ -0,0 +1,950 @@
1
+ module Amp
2
+ module Repositories
3
+
4
+ ##
5
+ # An entry in the dirstate. Similar to IndexEntry for revlogs. Simple struct, that's
6
+ # all.
7
+ class DirStateEntry < Struct.new(:status, :mode, :size, :mtime)
8
+
9
+ ##
10
+ # shortcuts!
11
+ def removed?; self.status == :removed; end
12
+ def added?; self.status == :added; end
13
+ def untracked?; self.status == :untracked; end
14
+ def modified?; self.status == :modified; end
15
+ def merged?; self.status == :merged; end
16
+ def normal?; self.status == :normal; end
17
+ def forgotten?; self.status == :forgotten; end
18
+
19
+ ##
20
+ # Do I represent a dirty object?
21
+ #
22
+ # @return [Boolean] does this array represent a dirty object in a DirState?
23
+ def dirty?
24
+ self[-2] == -2 && self[-1] == -1 && self.normal?
25
+ end
26
+
27
+ ##
28
+ # Do I possibly represent a dirty object?
29
+ #
30
+ # @return [Boolean] does this array possibly represent a dirty object in a DirState?
31
+ def maybe_dirty?
32
+ self[-2] == -1 && self[-1] == -1 && self.normal?
33
+ end
34
+ end
35
+
36
+ ##
37
+ # = DirState
38
+ # This class handles parsing and manipulating the "dirstate" file, which is stored
39
+ # in the .hg folder. This file handles which files are marked for addition, removal,
40
+ # copies, and so on. The structure of each entry is below.
41
+ #
42
+ #
43
+ # class DirStateEntry < BitStruct
44
+ # default_options :endian => :network
45
+ #
46
+ # char :status , 8, "the state of the file"
47
+ # signed :mode , 32, "mode"
48
+ # signed :size , 32, "size"
49
+ # signed :mtime , 32, "mtime"
50
+ # signed :fname_size , 32, "filename size"
51
+ #
52
+ # end
53
+ class DirState
54
+ include Ignore
55
+ include RevlogSupport::Node
56
+
57
+ UNKNOWN = DirStateEntry.new(:untracked, 0, 0, 0)
58
+ FORMAT = "cNNNN"
59
+
60
+ class FileNotInRootError < StandardError; end
61
+ class AbsolutePathNeededError < StandardError; end
62
+
63
+ # The parents of the current state. If there's been an uncommitted merge,
64
+ # it will be two. Otherwise it will just be one parent and +NULL_ID+
65
+ attr_reader :parents
66
+
67
+ # The number of directories in each base ["dir" => #_of_dirs]
68
+ attr_reader :dirs
69
+
70
+ # The files mapped to their stats (state, mode, size, mtime)
71
+ # [state, mode, size, mtime]
72
+ attr_reader :files
73
+
74
+ # A map of files to be copied, because we want to preserve their history
75
+ # "source" => "dest"
76
+ attr_reader :copy_map
77
+
78
+ # I still don't know what this does
79
+ attr_reader :folds
80
+
81
+ # The conglomerate config object of global configs and the repo
82
+ # specific config.
83
+ attr_reader :config
84
+
85
+ # The root of the repository
86
+ attr_reader :root
87
+
88
+ # The opener to access files. The only files that will be touched lie
89
+ # in the .hg/ directory, so the default MUST be +:open_hg+.
90
+ attr_reader :opener
91
+
92
+ ##
93
+ # Creates a DirState object. This is used to represent, in memory (and
94
+ # occasionally on file) how the repository is being changed.
95
+ # It's really simple, and it is really the basis for _using_ the repo
96
+ # (contrary to how Revlog is the basis for _saving_ the repo).
97
+ #
98
+ # @param [String] root the absolute path to the root of the repository
99
+ # @param [Amp::AmpConfig] config the config file of hgrc
100
+ # @param [Amp::Opener] opener the opener to open files with
101
+ def initialize(root, config, opener)
102
+ unless root[0, 1] == "/"
103
+ raise AbsolutePathNeededError, "#{root} is not an absolute path!"
104
+ end
105
+
106
+ # root must be an aboslute path with no ending slash
107
+ @root = root[-1, 1] == "/" ? root[0..-2] : root # the root of the repo
108
+ @config = config # the config file where we get defaults
109
+ @opener = opener # opener to retrieve files (default: open_hg)
110
+ @dirty = false # has something changed, and do we need to write?
111
+ @dirty_parents = false
112
+ @parents = [NULL_ID, NULL_ID] # the parent revisions
113
+ @dirs = {} # number of directories in each base ["dir" => #_of_dirs]
114
+ @files = {} # the files mapped to their statistics
115
+ @copy_map = {} # src => dest
116
+ @ignore = [] # dirs and files to ignore
117
+ @folds = []
118
+ @check_exec = nil
119
+ generate_ignore
120
+ end
121
+
122
+ ##
123
+ # Retrieve a file's status from +@files+. If it's not there
124
+ # then return :untracked
125
+ #
126
+ # @param [String] key the path of the file
127
+ # @return [Symbol] status of the file, either :removed, :added, :untracked,
128
+ # :merged, :normal, :forgotten, or :untracked
129
+ def [](key)
130
+ lookup = @files[key]
131
+ lookup || DirStateEntry.new(:untracked)
132
+ end
133
+
134
+ ##
135
+ # Determine if +path+ is a link or an executable.
136
+ #
137
+ # @param [String] path the path to the file
138
+ # @return [String] either 'l' for a link and 'x' for an executable. Returns
139
+ # '' if neither
140
+ def flags(path)
141
+ return 'l' if File.ftype(path) == 'link'
142
+ return 'x' if File.executable? path
143
+ ''
144
+ end
145
+
146
+ ##
147
+ # just a lil' reader to find if the repo is dirty or not
148
+ # by dirty i mean "no longer in sync with the cache"
149
+ #
150
+ # @return [Boolean] is the dirstate no longer in sync with the cache located
151
+ # at .hg/branch.cache
152
+ def dirty?
153
+ @dirty
154
+ end
155
+
156
+ ##
157
+ # The directories and path matches that we're ignoringzorz. It will call
158
+ # the ignorer generated by .hgignore, but only if @ignore_all is nil (really
159
+ # only if @ignore_all isn't a Boolean value, but we set it to nil)
160
+ #
161
+ # @param [String] file the path to the file that will be checked by
162
+ # the .hgignore file
163
+ # @return [Boolean] whether we're ignoring the path or not
164
+ def ignore(file)
165
+ return true if @ignore_all == true
166
+ return false if @ignore_all == false
167
+ @ignore_matches ||= parse_ignore @root, @ignore
168
+ @ignore_matches.call file
169
+ end
170
+
171
+ ##
172
+ # Gets the current branch.
173
+ #
174
+ # @return [String] the current branch in the working directory
175
+ def branch
176
+ text = @opener.read('branch').strip
177
+ @branch ||= text.empty? ? "default" : text
178
+ rescue
179
+ @branch = "default"
180
+ end
181
+
182
+ ##
183
+ # Set the branch to +branch+.
184
+ #
185
+ # @param [#to_s] brnch the branch to switch to
186
+ # @return [String] +brnch+.to_s
187
+ def branch=(brnch)
188
+ @branch = brnch.to_s
189
+
190
+ @opener.open 'branch', 'w' do |f|
191
+ f.puts brnch.to_s
192
+ end
193
+ @branch
194
+ end
195
+
196
+ ##
197
+ # Set the parents to +p+
198
+ #
199
+ # @param [Array<String>] p the parents as binary strings
200
+ # @return [Array<String>] the parents, as will be used by the dirstate
201
+ def parents=(p)
202
+ @parents = if p.is_a? Array
203
+ p.size == 1 ? p + [NULL_ID] : p[0..1]
204
+ else
205
+ [p, NULL_ID]
206
+ end
207
+
208
+ @dirty_parents = true
209
+ @dirty = true
210
+ @parents # return this
211
+ end
212
+ alias_method :parent, :parents
213
+
214
+ ##
215
+ # Set the file as "to be added".
216
+ #
217
+ # @param [String] file the path of the file to add
218
+ # @return [Boolean] a success marker
219
+ def add(file)
220
+ add_path file, true
221
+
222
+ @dirty = true
223
+ @files[file] = DirStateEntry.new(:added, 0, -1, -1)
224
+ @copy_map.delete file
225
+ true # success
226
+ end
227
+
228
+ ##
229
+ # Set the file as "normal", meaning no changes. This is the same
230
+ # as dirstate.normal in dirstate.py, for those referencing both.
231
+ #
232
+ # @param [String] file the path of the file to clean
233
+ # @return [Boolean] a success marker
234
+ def normal(file)
235
+ @dirty = true
236
+ add_path file, true
237
+
238
+ f = File.lstat "#{@root}/#{file}"
239
+ @files[file] = DirStateEntry.new(:normal, f.mode, f.size, f.mtime.to_i)
240
+ @copy_map.delete file
241
+ true # success
242
+ end
243
+ alias_method :clean, :normal
244
+
245
+ ##
246
+ # Set the file as normal, but possibly dirty. It's like when you
247
+ # meet a cool girl, and she seems really innocent and it's a chance
248
+ # for you to maybe change yourself and make a new friend, but then
249
+ # she *might* actually be a total slut. Better milk that grapevine
250
+ # to find out the truth. Oddly specific, huh.
251
+ #
252
+ # THUS IS THE HISTORY OF THIS METHOD!
253
+ #
254
+ # And then one day you go to the movies with some other girl, and the
255
+ # original crazy slutty girl is the cashier next to you. Unsure of
256
+ # what to do, you don't do anything. Next thing you know, she's trying
257
+ # to get your attention to say hey. WTF? Anyone know what's up with this
258
+ # girl?
259
+ #
260
+ # After milking that grapevine, you find out that she's not a great person.
261
+ # There's nothing interesting there and you should just move on.
262
+ #
263
+ # *sigh* girls.
264
+ #
265
+ # @param [String] file the path of the file to mark
266
+ # @return [Boolean] a success marker
267
+ def maybe_dirty(file)
268
+ if @files[file] && @parents.last != NULL_ID
269
+ # if there's a merge happening and the file was either modified
270
+ # or dirty before being removed, restore that state.
271
+ # I'm quoting the python with that one.
272
+ # I guess what it's saying is that if a file is being removed
273
+ # by a merge, but it was altered somehow beforehand on the local
274
+ # repo, then play it safe and bring back the dead. Divine intervention
275
+ # on the side of the local repo.
276
+
277
+ # info here is a standard array of info
278
+ # [action, mode, size, mtime]
279
+ info = @files[file]
280
+
281
+ if info.removed? and [-1, -2].member? info.size
282
+ source = @copy_map[file]
283
+
284
+ # do the appropriate action
285
+ case info.size
286
+ when -1 # either merge it
287
+ merge file
288
+ when -2 # or mark it as dirty
289
+ dirty file
290
+ end
291
+
292
+ copy source => file if source
293
+ return
294
+ end
295
+
296
+ # next step... the base case!
297
+ return true if info.modified? || info.maybe_dirty? and info.size == -2
298
+ end
299
+
300
+ @dirty = true # make the repo dirty
301
+ add_path file # add the file
302
+
303
+ @files[file] = DirStateEntry.new(:normal, 0, -1, -1) # give it info
304
+ @copy_map.delete file # we're not copying it since we're adding it
305
+ true # success
306
+ end
307
+
308
+ ##
309
+ # Checks whether the dirstate is tracking the given file.
310
+ #
311
+ # @param f the file to check for
312
+ # @return [Boolean] whether or not the file is being tracked.
313
+ def include?(path)
314
+ not @files[path].nil?
315
+ end
316
+ alias_method :tracking?, :include?
317
+
318
+ ##
319
+ # Mark the file as "dirty"
320
+ #
321
+ # @param [String] file the path of the file to mark
322
+ # @return [Boolean] a success marker
323
+ def dirty(file)
324
+ @dirty = true
325
+ add_path file
326
+
327
+ @files[file] = DirStateEntry.new(:normal, 0, -2, -1)
328
+ @copy_map.delete file
329
+ true # success
330
+ end
331
+
332
+ ##
333
+ # Set the file as "to be removed"
334
+ #
335
+ # @param [String] file the path of the file to remove
336
+ # @return [Boolean] a success marker
337
+ def remove(file)
338
+ @dirty = true
339
+ drop_path file
340
+
341
+ size = 0
342
+ if @parents.last.null? && (info = @files[file])
343
+ if info.merged?
344
+ size = -1
345
+ elsif info.normal? && info.size == -2
346
+ size = -2
347
+ end
348
+ end
349
+ @files[file] = DirStateEntry.new(:removed, 0, size, 0)
350
+ @copy_map.delete file if size.zero?
351
+ true # success
352
+ end
353
+
354
+ ##
355
+ # Prepare the file to be merged
356
+ #
357
+ # @param [String] file the path of the file to merge
358
+ # @return [Boolean] a success marker
359
+ def merge(file)
360
+ @dirty = true
361
+ add_path file
362
+
363
+ stats = File.lstat "#{@root}/#{file}"
364
+ add_path file
365
+ @files[file] = DirStateEntry.new(:merged, stats.mode, stats.size, stats.mtime.to_i)
366
+ @copy_map.delete file
367
+ true # success
368
+ end
369
+
370
+ ##
371
+ # Forget the file, erase it from the repo
372
+ #
373
+ # @param [String] file the path of the file to forget
374
+ # @return [Boolean] a success marker
375
+ def forget(file)
376
+ @dirty = true
377
+ drop_path file
378
+ @files.delete file
379
+ true # success
380
+ end
381
+
382
+ ##
383
+ # Invalidates the dirstate, making it completely unusable until it is
384
+ # re-read. Should only be used in error situations.
385
+ def invalidate!
386
+ %w(@files @copy_map @folds @branch @parents @dirs @ignore).each do |ivar|
387
+ instance_variable_set(ivar, nil)
388
+ end
389
+ @dirty = false
390
+ end
391
+
392
+ ##
393
+ # Refresh the directory's state, making everything empty.
394
+ # Called by #rebuild.
395
+ #
396
+ # This is not the same as #initialize, so we can't just run
397
+ # `send :initialize` and call it a day :-(
398
+ #
399
+ # @return [Boolean] a success marker
400
+ def clear
401
+ @files = {}
402
+ @dirs = {}
403
+ @copy_map = {}
404
+ @parents = [NULL_ID, NULL_ID]
405
+ @dirty = true
406
+
407
+ true # success
408
+ end
409
+
410
+ ##
411
+ # Rebuild the directory's state. Needs Manifest, as that's
412
+ # what the files really are.
413
+ #
414
+ # @param [String] parent the binary format of the parent
415
+ # @param [ManifestEntry] files the files in a specific revision
416
+ # @return [Boolean] a success marker
417
+ def rebuild(parent, files)
418
+ clear
419
+
420
+ # alter each file according to its flags
421
+ files.each do |f|
422
+ mode = files.flags(f).include?('x') ? 0777 : 0666
423
+ @files[f] = DirStateEntry.new(:normal, mode, -1, 0)
424
+ end
425
+
426
+ @parents = [parent, NULL_ID]
427
+ @dirty_parents = true
428
+ true # success
429
+ end
430
+
431
+ ##
432
+ # Save the data to .hg/dirstate.
433
+ # Uses mode: "w", so it overwrites everything
434
+ #
435
+ # @todo watch memory usage - +si+ could grow unrestrictedly which would
436
+ # bog down the entire program
437
+ # @return [Boolean] a success marker
438
+ def write
439
+ return true unless @dirty
440
+ begin
441
+ @opener.open "dirstate", 'w' do |state|
442
+ gran = @config['dirstate']['granularity'] || 1 # self._ui.config('dirstate', 'granularity', 1)
443
+
444
+ limit = 2147483647 # sorry for the literal use...
445
+ limit = state.mtime - gran if gran > 0
446
+
447
+ si = StringIO.new "", (ruby_19? ? "w+:ASCII-8BIT" : "w+")
448
+ si.write @parents.join
449
+
450
+ @files.each do |file, info|
451
+ file = file.dup # so we don't corrupt vars
452
+ info = info.dup.to_a # UNLIKE PYTHON
453
+ info[0] = info[0].to_hg_int
454
+
455
+ # I should probably do mah physics hw. nah, i'll do it
456
+ # tomorrow during my break
457
+ # good news - i did pretty well on my physics test by using
458
+ # brian ford's name instead of my own.
459
+ file = "#{file}\0#{@copy_map[file]}" if @copy_map[file]
460
+ info = [info[0], 0, (-1).to_signed(32), (-1).to_signed(32)] if info[3].to_i > limit.to_i and info[0] == :normal
461
+ info << file.size # the final element to make it pass, which is the length of the filename
462
+ info = info.pack FORMAT # pack them their lunch
463
+ si.write info # and send them off
464
+ si.write file # to school
465
+ end
466
+
467
+ state.write si.string
468
+ @dirty = false
469
+ @dirty_parents = false
470
+
471
+ true # success
472
+ end
473
+ rescue IOError
474
+ false
475
+ end
476
+ end
477
+
478
+ ##
479
+ # Copies the files in h (represented as "source" => "dest").
480
+ #
481
+ # @param [Hash<String => String>] h the keys are sources and the values
482
+ # are dests
483
+ # @return [Boolean] a success marker
484
+ def copy(h={})
485
+ h.each do |source, dest|
486
+ next if source == dest
487
+ return true unless source
488
+
489
+ @dirty = true
490
+
491
+ if @copy_map[dest]
492
+ then @copy_map.delete dest
493
+ else @copy_map[dest] = source
494
+ end
495
+ end
496
+
497
+ true # success
498
+ end
499
+
500
+ ##
501
+ # The current directory from where the command is being called, with
502
+ # the path shortened if it's within the repo.
503
+ #
504
+ # @return [String] effectively Dir.pwd
505
+ def cwd
506
+ path = Dir.pwd
507
+ return '' if path == @root
508
+
509
+ # return a more local path if possible...
510
+ return path[@root.length..-1] if path.start_with? @root
511
+ path # else we're outside the repo
512
+ end
513
+ alias_method :pwd, :cwd
514
+
515
+ ##
516
+ # Returns the relative path from +src+ to +dest+.
517
+ #
518
+ # @param [String] src This is a directory! If this is relative,
519
+ # it is assumed to be relative to the root.
520
+ # @param [String] dest This MUST be within root! It also is a file.
521
+ # @return [String] the relative path
522
+ def path_to(src, dest)
523
+ # first, make both paths absolute, for ease of use.
524
+ # @root is guarenteed to be absolute, so we're leethax here
525
+ src = File.join @root, src
526
+ dest = File.join @root, dest
527
+
528
+ # lil' bit of error checking...
529
+ [src, dest].map do |f|
530
+ unless File.exist? f # does both files and directories...
531
+ raise FileNotInRootError, "#{f} is not in the root, #{@root}"
532
+ end
533
+ end
534
+
535
+ # now we find the differences
536
+ # these both are now arrays!!!
537
+ src = src.split '/'
538
+ dest = dest.split '/'
539
+
540
+ while src.first == dest.first
541
+ src.shift and dest.shift
542
+ end
543
+
544
+ # now, src and dest are just where they differ
545
+ path = ['..'] * src.size # we want to go back this many directories
546
+ path += dest
547
+ path.join '/' # tadah!
548
+ end
549
+
550
+ ##
551
+ # Walk recursively through the directory tree, finding all
552
+ # files matched by the regexp in match.
553
+ #
554
+ # Step 1: find all explicit files
555
+ # Step 2: visit subdirectories
556
+ # Step 3: report unseen items in the @files hash
557
+ #
558
+ # @param [Boolean] unknown
559
+ # @param [Boolean] ignored
560
+ # @return [Hash<String => [NilClass, File::Stat]>] nil for directories and
561
+ # stuff, File::Stat for files and links
562
+ def walk(unknown, ignored, match)
563
+ files = match.files
564
+
565
+ bad_type = proc do |file|
566
+ UI::warn "#{file}: unsupported file type (type is #{File.ftype file})"
567
+ end
568
+
569
+ if ignored
570
+ @ignore_all = false
571
+ elsif not unknown
572
+ @ignore_all = true
573
+ end
574
+
575
+ work = [@root]
576
+
577
+ files = match.files ? match.files.uniq : [] # because [].uniq! is a major fuckup
578
+
579
+ # why do we overwrite the entire array if it includes the current dir?
580
+ # we even kill posisbly good things
581
+ files = [''] if files.empty? || files.include?('.') # strange thing to do
582
+ results = {'.hg' => true}
583
+
584
+ # Step 1: find all explicit files
585
+ files.sort.each do |file|
586
+ next if results[file] || file == ""
587
+
588
+ begin
589
+ stats = File.lstat File.join(@root, file)
590
+ kind = File.ftype File.join(@root, file)
591
+
592
+ # we'll take it! but only if it's a directory, which means we have
593
+ # more work to do...
594
+ if kind == 'directory'
595
+ # add it to the list of dirs we have to search in
596
+ work << File.join(@root, file) unless ignoring_directory? file
597
+ elsif kind == 'file' || kind == 'link'
598
+ # ARGH WE FOUND ZE BOOTY
599
+ results[file] = stats
600
+ else
601
+ # user you are a fuckup in life please exit the world
602
+ bad_type[file]
603
+ results[file] = nil if @files[file]
604
+ end
605
+ rescue => e
606
+ keep = false
607
+ prefix = file + '/'
608
+
609
+ @files.each do |f, _|
610
+ if f == file || f.start_with?(prefix)
611
+ keep = true
612
+ break
613
+ end
614
+ end
615
+
616
+ unless keep
617
+ bad_type[file]
618
+ results[file] = nil if (@files[file] || !ignore(file)) && match.call(file)
619
+ end
620
+ end
621
+ end
622
+
623
+ # step 2: visit subdirectories in `work`
624
+ until work.empty?
625
+ dir = work.shift
626
+ skip = nil
627
+
628
+ if dir == '.'
629
+ dir = ''
630
+ else
631
+ skip = '.hg'
632
+ end
633
+
634
+
635
+ dirs = Dir.glob("#{dir}/*", File::FNM_DOTMATCH) - ["#{dir}/.", "#{dir}/.."]
636
+ entries = dirs.inject({}) do |h, f|
637
+ h.merge f => [File.ftype(f), File.lstat(f)]
638
+ end
639
+
640
+
641
+ entries.each do |f, arr|
642
+ tf = f[(@root.size+1)..-1]
643
+ kind = arr[0]
644
+ stats = arr[1]
645
+ unless results[tf]
646
+ if kind == 'directory'
647
+ work << f unless ignore tf
648
+ results[tf] = nil if @files[tf] && match.call(tf)
649
+ elsif kind == 'file' || kind == 'link'
650
+ if @files[tf]
651
+ results[tf] = stats if match.call tf
652
+ elsif match.call(tf) && !ignore(tf)
653
+ results[tf] = stats
654
+ end
655
+ elsif @files[tf] && match.call(tf)
656
+ results[tf] = nil
657
+ end
658
+ end
659
+ end
660
+ end
661
+
662
+ # step 3: report unseen items in @files
663
+ visit = @files.keys.select {|f| !results[f] && match.call(f) }.sort
664
+
665
+ # zip it to a hash of {file_name => file_stats}
666
+ hash = visit.inject({}) do |h, f|
667
+ h.merge!(f => File.stat(File.join(@root,f))) rescue h.merge!(f => nil)
668
+ end
669
+
670
+ hash.each do |file, stat|
671
+ unless stat.nil?
672
+ # because filestats can't be gathered if it's, say, a directory
673
+ stat = nil unless ['file', 'link'].include? File.ftype(File.join(@root, file))
674
+ end
675
+ results[file] = stat
676
+ end
677
+
678
+ results.delete ".hg"
679
+ @ignore_all = nil # reset this
680
+ results
681
+ end
682
+
683
+ ##
684
+ # what's the current state of life, man!
685
+ # Splits up all the files into modified, clean,
686
+ # added, deleted, unknown, ignored, or lookup-needed.
687
+ #
688
+ # @return [Hash<Symbol => Array<String>>] a hash of the filestatuses and their files
689
+ def status(ignored, clean, unknown, match = Match.new { true })
690
+ list_ignored, list_clean, list_unknown = ignored, clean, unknown
691
+ lookup, modified, added, unknown, ignored = [], [], [], [], []
692
+ removed, deleted, clean = [], [], []
693
+ delta = 0
694
+
695
+ walk(list_unknown, list_ignored, match).each do |file, st|
696
+ next if file.nil?
697
+
698
+ unless @files[file]
699
+ if list_ignored && ignoring_directory?(file)
700
+ ignored << file
701
+ elsif list_unknown
702
+ unknown << file unless ignore(file)
703
+ end
704
+
705
+ next # on to the next one, don't do the rest
706
+ end
707
+
708
+ # here's where we split up the files
709
+ state, mode, size, time = *@files[file].to_a
710
+ delta += (size - st.size).abs if st && size >= 0 # increase the delta, but don't forget to check that it's not nil
711
+ if !st && [:normal, :modified, :added].include?(state)
712
+ # add it to the deleted folder if it should be here but isn't
713
+ deleted << file
714
+ elsif state == :normal
715
+ if (size >= 0 && (size != st.size || ((mode ^ st.mode) & 0100 and @check_exec))) || size == -2 || @copy_map[file]
716
+ modified << file
717
+ elsif time != st.mtime.to_i # DOH - we have to remember that times are stored as fixnums
718
+ lookup << file
719
+ elsif list_clean
720
+ clean << file
721
+ end
722
+
723
+ elsif state == :merged
724
+ modified << file
725
+ elsif state == :added
726
+ added << file
727
+ elsif state == :removed
728
+ removed << file
729
+ end
730
+ end
731
+
732
+ r = { :modified => modified.sort , # those that have clearly been modified
733
+ :added => added.sort , # those that are marked for adding
734
+ :removed => removed.sort , # those that are marked for removal
735
+ :deleted => deleted.sort , # those that should be here but aren't
736
+ :unknown => unknown.sort , # those that aren't being tracked
737
+ :ignored => ignored.sort , # those that are being deliberately ignored
738
+ :clean => clean.sort , # those that haven't changed
739
+ :lookup => lookup.sort , # those that need to be content-checked to see if they've changed
740
+ :delta => delta # how many bytes have been added or removed from files (not bytes that have been changed)
741
+ }
742
+ end
743
+
744
+
745
+ ##
746
+ # Reads the data in the .hg folder and fills in the vars
747
+ #
748
+ # @return [Amp::DirState] self -- chainable!
749
+ def read!
750
+ @parents, @files, @copy_map = parse('dirstate')
751
+ self # chainable
752
+ end
753
+
754
+ private
755
+ ##
756
+ # Generates the @ignore array
757
+ # The array is full of paths relative to the root, which
758
+ # makes things easier for the proc-generation phase.
759
+ #
760
+ # @return [NilClass]
761
+ def generate_ignore
762
+ @ignore = @config['ui'].map do |k, v|
763
+ @ignore << "#{v}" if k == "ignore"
764
+ end
765
+
766
+ @ignore << ".hgignore"
767
+ @ignore.compact
768
+
769
+ nil
770
+ end
771
+
772
+ ##
773
+ # Perform various checks on the file before upping the content count
774
+ # for all of its parent directories. It checks for:
775
+ # * filenames containing "\n" or "\r" (newlines and carriage returns)
776
+ # * filenames with the same names as directories
777
+ # * clashing filenames
778
+ #
779
+ # It only increments the dirs' file count if the file is untracked or
780
+ # being removed.
781
+ #
782
+ # @param [String] f Should be formatted like ["action", mode, size, mtime]
783
+ # @param [Boolean] check whether to perform any of the checks
784
+ # @return [NilClass]
785
+ def add_path(f, check=false)
786
+ old_state = @files[f] || DirStateEntry.new # it's an array of info, remember
787
+
788
+ if check || old_state.removed?
789
+ raise "Bad Filename" if f.match(/\r|\n/)
790
+ raise "Directory #{f} already exists" if @dirs[f]
791
+
792
+ # make sure we don't have any files with the same name as a directory
793
+ directories_to(f).each do |d|
794
+ break if @dirs[d]
795
+
796
+ if @files[d] && !@files[d].removed?
797
+ raise "File #{d} clashes with #{f}! Fix their names"
798
+ end
799
+ end
800
+ end
801
+
802
+ # only inc the dirs if the file is untracked or being removed.
803
+ if [:untracked, :removed].include? old_state.status
804
+ # inc the number of dirs in each dir
805
+ inc_directories_to f
806
+ end
807
+
808
+ nil
809
+ end
810
+
811
+ ##
812
+ # Conditional wrapper around +dec_directories_to+. It will dec the
813
+ # directories if the file in question (+f+) is either untracked or
814
+ # being removed.
815
+ #
816
+ # @param [String] f Should be formatted like ["action", mode, size, mtime]
817
+ # @return [NilClass]
818
+ def drop_path(f)
819
+ unless [:untracked, :removed].include? f[0]
820
+ dec_directories_to(f)
821
+ end
822
+
823
+ nil
824
+ end
825
+
826
+ ##
827
+ # All directories leading up to this path
828
+ #
829
+ # @example directories_to "/Users/ari/src/monkey.txt" # =>
830
+ # ["/Users/ari/src", "/Users/ari", "/Users"]
831
+ # @param [String] path the path to the file we're examining
832
+ # @return [Array] the directories leading up to this path
833
+ def directories_to(path)
834
+ File.amp_directories_to path
835
+ end
836
+
837
+ ##
838
+ # Increment all directories' dir-count leading up to this path.
839
+ # The dir-count is the path's value in @dirs.
840
+ # This is used when adding a file.
841
+ #
842
+ # @param [String] path the path we're disecting
843
+ # @return [NilClass]
844
+ def inc_directories_to(path)
845
+ p = directories_to(path).first
846
+ @dirs[p] ||= 0
847
+ @dirs[p] += 1
848
+ nil
849
+ end
850
+
851
+ ##
852
+ # Decrement all directories' dir-count leading up to this path.
853
+ # The dir-count is the path's value in @dirs.
854
+ # This is used when removing a file.
855
+ #
856
+ # @param [String] path the path we're disecting
857
+ # @return [NilClass]
858
+ def dec_directories_to(path)
859
+ p = directories_to(path).first
860
+ # if the dir has 0, kill the dir. we don't need it anymore
861
+ if @dirs[p] && @dirs[p].zero?
862
+ @dirs.delete p
863
+ elsif @dirs[p]
864
+ @dirs[p] -= 1 # we only need to inc the latest dir
865
+ end
866
+
867
+ nil
868
+ end
869
+
870
+ ##
871
+ # I wish I knew what this did or when it was called.
872
+ #
873
+ # @todo figure out what this does
874
+ # @param [String] path the path to a file
875
+ # @return [String] All I know is that this returns a string
876
+ def normalize(path)
877
+ fold_path = @folds[path]
878
+ fold_path = path unless fold_path # if fold_path is true, then this line returns nil
879
+ fold_path # so we need an extra line here to make sure it returns a good value
880
+ end
881
+
882
+ ##
883
+ # Are we ignoring the directory?
884
+ #
885
+ # @param [String] dir the directory we're checking, either aboslute or relative
886
+ # @return [Boolean] are we ignoring the dir?
887
+ def ignoring_directory?(dir)
888
+ return true if @ignore_all
889
+ return false if @ignore_all == false
890
+ return false if dir == '.' # base cases
891
+ return true if ignore dir # base cases
892
+
893
+ !!directories_to(dir).any? {|d| ignore d }
894
+ end
895
+ alias_method :ignoring_dir?, :ignoring_directory?
896
+
897
+ ##
898
+ # Parses the dirstate file in .hg
899
+ #
900
+ # @param [String] file path to the file to parse
901
+ # @return [((String, String), Hash<String => (Integer, Integer, Integer)>, Hash<String => String>)]
902
+ # a tuple of (parents, files, copies). Parents is a tuple of the parents,
903
+ # files is a hash of filename => [mode, size, mtime], and copies is a hash of src => dest
904
+ def parse(file)
905
+ # the main data we need to return
906
+ files = {}
907
+ copies = {}
908
+ parents = []
909
+ @opener.open file, "r" do |s|
910
+
911
+ # the parents are the first 40 bytes
912
+ parent = s.read(20) || NULL_ID
913
+ parent_ = s.read(20) || NULL_ID
914
+ parents = [parent, parent_]
915
+
916
+ # 1 character + 4 32-bit ints = 17 bytes
917
+ e_size = 17
918
+
919
+ # this loop is just cycling through and reading every entry
920
+ while !s.eof?
921
+ # read 1 entry
922
+ info = s.read(e_size).unpack FORMAT
923
+
924
+ # byte swap and shizzle
925
+ info = [info[0].to_dirstate_symbol, info[1], info[2].to_signed(32), info[3].to_signed(32), info[4]]
926
+ # ^^^^ we have to sign them because otherwise they'll be hugely wrong
927
+
928
+ # read in the filename
929
+ f = s.read(info[4])
930
+
931
+ # if it has an \0, then we've moved/copied it
932
+ if f.match(/\0/)
933
+ source, dest = f.split "\0"
934
+ copies[source] = dest
935
+ f = source
936
+ end
937
+
938
+ # and put in the info for the file itself
939
+ files[f] = DirStateEntry.new(*info[0..3])
940
+ end
941
+ end
942
+
943
+ [parents, files, copies]
944
+ rescue Errno::ENOENT
945
+ # no file? easy peasy
946
+ [[NULL_ID, NULL_ID], {}, {}]
947
+ end
948
+ end
949
+ end
950
+ end