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