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