amp 0.5.2 → 0.5.3
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 +12 -0
- data/.hgignore +3 -0
- data/AUTHORS +1 -1
- data/Manifest.txt +99 -38
- data/README.md +3 -3
- data/Rakefile +53 -18
- data/SCHEDULE.markdown +5 -1
- data/TODO.markdown +120 -149
- data/ampfile.rb +3 -1
- data/bin/amp +4 -1
- data/ext/amp/bz2/extconf.rb +1 -1
- data/ext/amp/mercurial_patch/extconf.rb +1 -1
- data/ext/amp/mercurial_patch/mpatch.c +4 -3
- data/ext/amp/priority_queue/extconf.rb +1 -1
- data/ext/amp/support/extconf.rb +1 -1
- data/ext/amp/support/support.c +1 -1
- data/lib/amp.rb +125 -67
- data/lib/amp/commands/command.rb +12 -10
- data/lib/amp/commands/command_support.rb +8 -1
- data/lib/amp/commands/commands/help.rb +2 -20
- data/lib/amp/commands/commands/init.rb +14 -2
- data/lib/amp/commands/commands/templates.rb +6 -4
- data/lib/amp/commands/commands/version.rb +15 -1
- data/lib/amp/commands/commands/workflow.rb +3 -3
- data/lib/amp/commands/commands/workflows/git/add.rb +3 -3
- data/lib/amp/commands/commands/workflows/git/copy.rb +1 -1
- data/lib/amp/commands/commands/workflows/git/rm.rb +4 -2
- data/lib/amp/commands/commands/workflows/hg/add.rb +1 -1
- data/lib/amp/commands/commands/workflows/hg/addremove.rb +2 -2
- data/lib/amp/commands/commands/workflows/hg/annotate.rb +8 -2
- data/lib/amp/commands/commands/workflows/hg/bisect.rb +253 -0
- data/lib/amp/commands/commands/workflows/hg/branch.rb +1 -1
- data/lib/amp/commands/commands/workflows/hg/branches.rb +3 -3
- data/lib/amp/commands/commands/workflows/hg/bundle.rb +3 -3
- data/lib/amp/commands/commands/workflows/hg/clone.rb +4 -5
- data/lib/amp/commands/commands/workflows/hg/commit.rb +37 -1
- data/lib/amp/commands/commands/workflows/hg/copy.rb +2 -1
- data/lib/amp/commands/commands/workflows/hg/debug/index.rb +1 -1
- data/lib/amp/commands/commands/workflows/hg/diff.rb +3 -8
- data/lib/amp/commands/commands/workflows/hg/forget.rb +5 -4
- data/lib/amp/commands/commands/workflows/hg/identify.rb +6 -6
- data/lib/amp/commands/commands/workflows/hg/import.rb +1 -1
- data/lib/amp/commands/commands/workflows/hg/incoming.rb +2 -2
- data/lib/amp/commands/commands/workflows/hg/log.rb +5 -4
- data/lib/amp/commands/commands/workflows/hg/merge.rb +1 -1
- data/lib/amp/commands/commands/workflows/hg/move.rb +5 -3
- data/lib/amp/commands/commands/workflows/hg/outgoing.rb +1 -1
- data/lib/amp/commands/commands/workflows/hg/push.rb +6 -7
- data/lib/amp/commands/commands/workflows/hg/remove.rb +2 -2
- data/lib/amp/commands/commands/workflows/hg/resolve.rb +6 -23
- data/lib/amp/commands/commands/workflows/hg/root.rb +1 -2
- data/lib/amp/commands/commands/workflows/hg/status.rb +21 -12
- data/lib/amp/commands/commands/workflows/hg/tag.rb +2 -2
- data/lib/amp/commands/commands/workflows/hg/untrack.rb +12 -0
- data/lib/amp/commands/commands/workflows/hg/verify.rb +13 -3
- data/lib/amp/commands/commands/workflows/hg/what_changed.rb +18 -0
- data/lib/amp/commands/dispatch.rb +12 -13
- data/lib/amp/dependencies/amp_support.rb +1 -1
- data/lib/amp/dependencies/amp_support/ruby_amp_support.rb +1 -0
- data/lib/amp/dependencies/maruku.rb +136 -0
- data/lib/amp/dependencies/maruku/attributes.rb +227 -0
- data/lib/amp/dependencies/maruku/defaults.rb +71 -0
- data/lib/amp/dependencies/maruku/errors_management.rb +92 -0
- data/lib/amp/dependencies/maruku/helpers.rb +260 -0
- data/lib/amp/dependencies/maruku/input/charsource.rb +326 -0
- data/lib/amp/dependencies/maruku/input/extensions.rb +69 -0
- data/lib/amp/dependencies/maruku/input/html_helper.rb +189 -0
- data/lib/amp/dependencies/maruku/input/linesource.rb +111 -0
- data/lib/amp/dependencies/maruku/input/parse_block.rb +615 -0
- data/lib/amp/dependencies/maruku/input/parse_doc.rb +234 -0
- data/lib/amp/dependencies/maruku/input/parse_span_better.rb +746 -0
- data/lib/amp/dependencies/maruku/input/rubypants.rb +225 -0
- data/lib/amp/dependencies/maruku/input/type_detection.rb +147 -0
- data/lib/amp/dependencies/maruku/input_textile2/t2_parser.rb +163 -0
- data/lib/amp/dependencies/maruku/maruku.rb +33 -0
- data/lib/amp/dependencies/maruku/output/to_ansi.rb +223 -0
- data/lib/amp/dependencies/maruku/output/to_html.rb +991 -0
- data/lib/amp/dependencies/maruku/output/to_markdown.rb +164 -0
- data/lib/amp/dependencies/maruku/output/to_s.rb +56 -0
- data/lib/amp/dependencies/maruku/string_utils.rb +191 -0
- data/lib/amp/dependencies/maruku/structures.rb +167 -0
- data/lib/amp/dependencies/maruku/structures_inspect.rb +87 -0
- data/lib/amp/dependencies/maruku/structures_iterators.rb +61 -0
- data/lib/amp/dependencies/maruku/textile2.rb +1 -0
- data/lib/amp/dependencies/maruku/toc.rb +199 -0
- data/lib/amp/dependencies/maruku/usage/example1.rb +33 -0
- data/lib/amp/dependencies/maruku/version.rb +40 -0
- data/lib/amp/dependencies/priority_queue.rb +2 -1
- data/lib/amp/dependencies/python_config.rb +2 -1
- data/lib/amp/graphs/ancestor.rb +2 -1
- data/lib/amp/graphs/copies.rb +236 -233
- data/lib/amp/help/entries/__default__.erb +31 -0
- data/lib/amp/help/entries/commands.erb +6 -0
- data/lib/amp/help/entries/mdtest.md +35 -0
- data/lib/amp/help/entries/silly +3 -0
- data/lib/amp/help/help.rb +288 -0
- data/lib/amp/profiling_hacks.rb +5 -3
- data/lib/amp/repository/abstract/abstract_changeset.rb +97 -0
- data/lib/amp/repository/abstract/abstract_local_repo.rb +181 -0
- data/lib/amp/repository/abstract/abstract_staging_area.rb +180 -0
- data/lib/amp/repository/abstract/abstract_versioned_file.rb +100 -0
- data/lib/amp/repository/abstract/common_methods/changeset.rb +75 -0
- data/lib/amp/repository/abstract/common_methods/local_repo.rb +277 -0
- data/lib/amp/repository/abstract/common_methods/staging_area.rb +233 -0
- data/lib/amp/repository/abstract/common_methods/versioned_file.rb +71 -0
- data/lib/amp/repository/generic_repo_picker.rb +78 -0
- data/lib/amp/repository/git/repo_format/changeset.rb +336 -0
- data/lib/amp/repository/git/repo_format/staging_area.rb +192 -0
- data/lib/amp/repository/git/repo_format/versioned_file.rb +119 -0
- data/lib/amp/repository/git/repositories/local_repository.rb +164 -0
- data/lib/amp/repository/git/repository.rb +41 -0
- data/lib/amp/repository/mercurial/encoding/mercurial_diff.rb +382 -0
- data/lib/amp/repository/mercurial/encoding/mercurial_patch.rb +1 -0
- data/lib/amp/repository/mercurial/encoding/patch.rb +294 -0
- data/lib/amp/repository/mercurial/encoding/pure_ruby/ruby_mercurial_patch.rb +124 -0
- data/lib/amp/repository/mercurial/merging/merge_ui.rb +327 -0
- data/lib/amp/repository/mercurial/merging/simple_merge.rb +452 -0
- data/lib/amp/repository/mercurial/repo_format/branch_manager.rb +266 -0
- data/lib/amp/repository/mercurial/repo_format/changeset.rb +768 -0
- data/lib/amp/repository/mercurial/repo_format/dir_state.rb +716 -0
- data/lib/amp/repository/mercurial/repo_format/journal.rb +218 -0
- data/lib/amp/repository/mercurial/repo_format/lock.rb +210 -0
- data/lib/amp/repository/mercurial/repo_format/merge_state.rb +228 -0
- data/lib/amp/repository/mercurial/repo_format/staging_area.rb +367 -0
- data/lib/amp/repository/mercurial/repo_format/store.rb +487 -0
- data/lib/amp/repository/mercurial/repo_format/tag_manager.rb +322 -0
- data/lib/amp/repository/mercurial/repo_format/updatable.rb +543 -0
- data/lib/amp/repository/mercurial/repo_format/updater.rb +848 -0
- data/lib/amp/repository/mercurial/repo_format/verification.rb +433 -0
- data/lib/amp/repository/mercurial/repositories/bundle_repository.rb +216 -0
- data/lib/amp/repository/mercurial/repositories/http_repository.rb +386 -0
- data/lib/amp/repository/mercurial/repositories/local_repository.rb +2034 -0
- data/lib/amp/repository/mercurial/repository.rb +119 -0
- data/lib/amp/repository/mercurial/revlogs/bundle_revlogs.rb +249 -0
- data/lib/amp/repository/mercurial/revlogs/changegroup.rb +217 -0
- data/lib/amp/repository/mercurial/revlogs/changelog.rb +339 -0
- data/lib/amp/repository/mercurial/revlogs/file_log.rb +152 -0
- data/lib/amp/repository/mercurial/revlogs/index.rb +500 -0
- data/lib/amp/repository/mercurial/revlogs/manifest.rb +201 -0
- data/lib/amp/repository/mercurial/revlogs/node.rb +20 -0
- data/lib/amp/repository/mercurial/revlogs/revlog.rb +1026 -0
- data/lib/amp/repository/mercurial/revlogs/revlog_support.rb +129 -0
- data/lib/amp/repository/mercurial/revlogs/versioned_file.rb +597 -0
- data/lib/amp/repository/repository.rb +11 -88
- data/lib/amp/server/extension/amp_extension.rb +3 -3
- data/lib/amp/server/fancy_http_server.rb +1 -1
- data/lib/amp/server/fancy_views/_browser.haml +1 -1
- data/lib/amp/server/fancy_views/_diff_file.haml +1 -8
- data/lib/amp/server/fancy_views/changeset.haml +2 -2
- data/lib/amp/server/fancy_views/file.haml +1 -1
- data/lib/amp/server/fancy_views/file_diff.haml +1 -1
- data/lib/amp/support/amp_ui.rb +13 -29
- data/lib/amp/support/generator.rb +1 -1
- data/lib/amp/support/loaders.rb +1 -2
- data/lib/amp/support/logger.rb +10 -16
- data/lib/amp/support/match.rb +18 -4
- data/lib/amp/support/mercurial/ignore.rb +151 -0
- data/lib/amp/support/openers.rb +8 -3
- data/lib/amp/support/support.rb +91 -46
- data/lib/amp/templates/{blank.commit.erb → mercurial/blank.commit.erb} +0 -0
- data/lib/amp/templates/{blank.log.erb → mercurial/blank.log.erb} +0 -0
- data/lib/amp/templates/{default.commit.erb → mercurial/default.commit.erb} +0 -0
- data/lib/amp/templates/{default.log.erb → mercurial/default.log.erb} +0 -0
- data/lib/amp/templates/template.rb +18 -18
- data/man/amp.1 +51 -0
- data/site/src/about/commands.haml +1 -1
- data/site/src/css/amp.css +1 -1
- data/site/src/index.haml +3 -3
- data/tasks/man.rake +39 -0
- data/tasks/stats.rake +1 -10
- data/tasks/yard.rake +1 -50
- data/test/dirstate_tests/test_dir_state.rb +10 -8
- data/test/functional_tests/annotate.out +31 -0
- data/test/functional_tests/test_functional.rb +155 -63
- data/test/localrepo_tests/ampfile.rb +12 -0
- data/test/localrepo_tests/test_local_repo.rb +56 -57
- data/test/manifest_tests/test_manifest.rb +3 -5
- data/test/merge_tests/test_merge.rb +3 -3
- data/test/revlog_tests/test_revlog.rb +14 -6
- data/test/store_tests/test_fncache_store.rb +19 -19
- data/test/test_19_compatibility.rb +46 -0
- data/test/test_base85.rb +2 -2
- data/test/test_bdiff.rb +2 -2
- data/test/test_changegroup.rb +59 -0
- data/test/test_commands.rb +2 -2
- data/test/test_difflib.rb +2 -2
- data/test/test_generator.rb +34 -0
- data/test/test_ignore.rb +203 -0
- data/test/test_journal.rb +18 -13
- data/test/test_match.rb +2 -2
- data/test/test_mdiff.rb +3 -3
- data/test/test_mpatch.rb +3 -3
- data/test/test_multi_io.rb +40 -0
- data/test/test_support.rb +18 -2
- data/test/test_templates.rb +38 -0
- data/test/test_ui.rb +79 -0
- data/test/testutilities.rb +56 -0
- metadata +168 -49
- data/ext/amp/bz2/mkmf.log +0 -38
- data/lib/amp/encoding/mercurial_diff.rb +0 -378
- data/lib/amp/encoding/mercurial_patch.rb +0 -1
- data/lib/amp/encoding/patch.rb +0 -292
- data/lib/amp/encoding/pure_ruby/ruby_mercurial_patch.rb +0 -123
- data/lib/amp/merges/merge_state.rb +0 -164
- data/lib/amp/merges/merge_ui.rb +0 -322
- data/lib/amp/merges/simple_merge.rb +0 -450
- data/lib/amp/repository/branch_manager.rb +0 -234
- data/lib/amp/repository/dir_state.rb +0 -950
- data/lib/amp/repository/journal.rb +0 -203
- data/lib/amp/repository/lock.rb +0 -207
- data/lib/amp/repository/repositories/bundle_repository.rb +0 -214
- data/lib/amp/repository/repositories/http_repository.rb +0 -377
- data/lib/amp/repository/repositories/local_repository.rb +0 -2661
- data/lib/amp/repository/store.rb +0 -485
- data/lib/amp/repository/tag_manager.rb +0 -319
- data/lib/amp/repository/updatable.rb +0 -532
- data/lib/amp/repository/verification.rb +0 -431
- data/lib/amp/repository/versioned_file.rb +0 -475
- data/lib/amp/revlogs/bundle_revlogs.rb +0 -246
- data/lib/amp/revlogs/changegroup.rb +0 -217
- data/lib/amp/revlogs/changelog.rb +0 -338
- data/lib/amp/revlogs/changeset.rb +0 -521
- data/lib/amp/revlogs/file_log.rb +0 -165
- data/lib/amp/revlogs/index.rb +0 -493
- data/lib/amp/revlogs/manifest.rb +0 -195
- data/lib/amp/revlogs/node.rb +0 -18
- data/lib/amp/revlogs/revlog.rb +0 -1045
- data/lib/amp/revlogs/revlog_support.rb +0 -126
- data/lib/amp/support/ignore.rb +0 -144
- data/site/Rakefile +0 -38
- data/test/test_amp.rb +0 -9
- data/test/test_helper.rb +0 -15
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
module Amp
|
|
2
|
+
module Repositories
|
|
3
|
+
module Mercurial
|
|
4
|
+
class StagingArea < AbstractStagingArea
|
|
5
|
+
|
|
6
|
+
attr_reader :dirstate
|
|
7
|
+
attr_reader :repo
|
|
8
|
+
alias_method :repository, :repo
|
|
9
|
+
attr_accessor :vcs_dir
|
|
10
|
+
|
|
11
|
+
def initialize(repo)
|
|
12
|
+
@ignore_all = nil
|
|
13
|
+
@repo = repo
|
|
14
|
+
@check_exec = false
|
|
15
|
+
@vcs_dir = '.hg'
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
######### API Methods #################################################
|
|
19
|
+
|
|
20
|
+
##
|
|
21
|
+
# Adds a list of file paths to the repository for the next commit.
|
|
22
|
+
#
|
|
23
|
+
# @api
|
|
24
|
+
# @param [String, Array<String>] paths the paths of the files we need to
|
|
25
|
+
# add to the next commit
|
|
26
|
+
# @return [Array<String>] which files WEREN'T added
|
|
27
|
+
def add(*files)
|
|
28
|
+
rejected = []
|
|
29
|
+
files.flatten!
|
|
30
|
+
|
|
31
|
+
repo.lock_working do
|
|
32
|
+
files.each do |file|
|
|
33
|
+
path = repo.working_join file
|
|
34
|
+
stat = File.exist?(path) && File.lstat(path)
|
|
35
|
+
|
|
36
|
+
if !stat
|
|
37
|
+
UI.warn "#{file} does not exist!"
|
|
38
|
+
rejected << file
|
|
39
|
+
elsif File.ftype(path) != 'file' && File.ftype(path) != 'link'
|
|
40
|
+
UI.warn "#{file} not added: only files and symlinks supported. Type is #{File.ftype path}"
|
|
41
|
+
rejected << path
|
|
42
|
+
else
|
|
43
|
+
if stat.size > 10.mb
|
|
44
|
+
UI.warn "#{file}: files over 10MB may cause memory and performance problems\n" +
|
|
45
|
+
"(use 'amp revert #{file}' to unadd the file)\n"
|
|
46
|
+
end
|
|
47
|
+
dirstate.add file
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
dirstate.write unless rejected.size == files.size
|
|
51
|
+
end
|
|
52
|
+
rejected
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
##
|
|
56
|
+
# Removes the file (or files) from the repository. Marks them as removed
|
|
57
|
+
# in the DirState, and if the :unlink option is provided, the files are
|
|
58
|
+
# deleted from the filesystem.
|
|
59
|
+
#
|
|
60
|
+
# @api
|
|
61
|
+
# @param list the list of files. Could also just be 1 file as a string.
|
|
62
|
+
# should be paths.
|
|
63
|
+
# @param opts the options for this removal.
|
|
64
|
+
# @option [Boolean] opts :unlink (false) whether or not to delete the
|
|
65
|
+
# files from the filesystem after marking them as removed from the
|
|
66
|
+
# DirState.
|
|
67
|
+
# @return [Boolean] success?
|
|
68
|
+
def remove(*args)
|
|
69
|
+
list = args.last.is_a?(Hash) ? args[0..-2].flatten : args[0..-1].flatten
|
|
70
|
+
opts = args.last.is_a?(Hash) ? args.last : {}
|
|
71
|
+
# Should we delete the filez?
|
|
72
|
+
if opts[:unlink]
|
|
73
|
+
FileUtils.safe_unlink list.map {|f| repo.working_join(f)}
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
repo.lock_working do
|
|
77
|
+
# Save ourselves a dirstate write
|
|
78
|
+
successful = list.any? do |f|
|
|
79
|
+
if opts[:unlink] && File.exists?(repo.working_join(f))
|
|
80
|
+
# Uh, why is the file still there? Don't remove it from the dirstate
|
|
81
|
+
UI.warn("#{f} still exists!")
|
|
82
|
+
false # no success
|
|
83
|
+
else
|
|
84
|
+
dirstate.remove f
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Write 'em out boss
|
|
89
|
+
dirstate.write if successful
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
true
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
##
|
|
96
|
+
# Set +file+ as normal and clean. Un-removes any files marked as removed, and
|
|
97
|
+
# un-adds any files marked as added.
|
|
98
|
+
#
|
|
99
|
+
# @param [Array<String>] files the name of the files to mark as normal
|
|
100
|
+
# @return [Boolean] success marker
|
|
101
|
+
def normal(*files)
|
|
102
|
+
files.each do |file|
|
|
103
|
+
dirstate.normal(file)
|
|
104
|
+
end
|
|
105
|
+
dirstate.write
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
##
|
|
109
|
+
# Copies a file from +source+ to +destination+, while being careful of the
|
|
110
|
+
# specified options. This method will perform all necessary file manipulation
|
|
111
|
+
# and dirstate changes and so forth. Just give 'er a source and a destination.
|
|
112
|
+
#
|
|
113
|
+
# @api
|
|
114
|
+
# @param [String] source the path to the source file
|
|
115
|
+
# @param [String] destination the path to the destination file
|
|
116
|
+
# @param [Hash] opts the options for the copy
|
|
117
|
+
# @option [Boolean] opts :after (false) should the file be deleted?
|
|
118
|
+
# @return [Boolean] success?
|
|
119
|
+
def copy(source, destination, opts)
|
|
120
|
+
# Traverse repository subdirectories
|
|
121
|
+
src = repo.relative_join source
|
|
122
|
+
target = repo.relative_join destination
|
|
123
|
+
|
|
124
|
+
# Is there a tracked file at our destination? If so, get its state.
|
|
125
|
+
state = dirstate[target].status
|
|
126
|
+
# abstarget is the full path to the target. Needed for system calls
|
|
127
|
+
# (just to be safe)
|
|
128
|
+
abstarget = repo.working_join target
|
|
129
|
+
|
|
130
|
+
# If true, we're copying into a directory, so be smart about it.
|
|
131
|
+
if File.directory? abstarget
|
|
132
|
+
abstarget = File.join abstarget, File.basename(src)
|
|
133
|
+
target = File.join target, File.basename(src)
|
|
134
|
+
end
|
|
135
|
+
abssrc = repo.working_join(src)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
exists = File.exist? abstarget
|
|
139
|
+
# If the file's there, and we aren't forcing the copy, then we should let
|
|
140
|
+
# the user know they might overwrite an existing file in the repo.
|
|
141
|
+
if (!opts[:after] && exists || opts[:after] && [:merged, :normal].include?(state))
|
|
142
|
+
unless opts[:force]
|
|
143
|
+
Amp::UI.warn "#{target} not overwriting, file exists"
|
|
144
|
+
return false
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
return if opts[:after] && !exists
|
|
149
|
+
unless opts[:"dry-run"]
|
|
150
|
+
# Performs actual file copy from one locatino to another.
|
|
151
|
+
# Overwrites file if it's there.
|
|
152
|
+
begin
|
|
153
|
+
File.safe_unlink(abstarget) if exists
|
|
154
|
+
|
|
155
|
+
target_dir = File.dirname abstarget
|
|
156
|
+
File.makedirs target_dir unless File.directory? target_dir
|
|
157
|
+
File.copy(abssrc, abstarget)
|
|
158
|
+
rescue Errno::ENOENT
|
|
159
|
+
# This happens if the file has been deleted between the check up above
|
|
160
|
+
# (exists = File.exist? abstarget) and the call to File.safe_unlink.
|
|
161
|
+
Amp::UI.warn("#{target}: deleted in working copy in the last 2 microseconds")
|
|
162
|
+
rescue StandardError => e
|
|
163
|
+
Amp::UI.warn("#{target} - cannot copy: #{e}")
|
|
164
|
+
return false
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Be nice and give the user some output
|
|
169
|
+
if opts[:verbose] || opts[:"dry-run"]
|
|
170
|
+
action = opts[:rename] ? "moving" : "copying"
|
|
171
|
+
Amp::UI.status("#{action} #{src} to #{target}")
|
|
172
|
+
end
|
|
173
|
+
return false if opts[:"dry-run"]
|
|
174
|
+
|
|
175
|
+
# in case the source of the copy is marked as the destination of a
|
|
176
|
+
# different copy (that hasn't yet been committed either), we should
|
|
177
|
+
# do some extra handling
|
|
178
|
+
origsrc = dirstate.copy_map[src] || src
|
|
179
|
+
if target == origsrc
|
|
180
|
+
# We're copying back to our original location! D'oh.
|
|
181
|
+
unless [:merged, :normal].include?(state)
|
|
182
|
+
dirstate.maybe_dirty target
|
|
183
|
+
end
|
|
184
|
+
else
|
|
185
|
+
if dirstate[origsrc].added? && origsrc == src
|
|
186
|
+
# we copying an added (but uncommitted) file?
|
|
187
|
+
UI.warn("#{origsrc} has not been committed yet, so no copy data" +
|
|
188
|
+
"will be stored for #{target}")
|
|
189
|
+
if [:untracked, :removed].include?(dirstate[target].status)
|
|
190
|
+
add target
|
|
191
|
+
end
|
|
192
|
+
else
|
|
193
|
+
dirstate_copy src, target
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Clean up if we're doing a move, and not a copy.
|
|
198
|
+
remove(src, :unlink => !(opts[:after])) if opts[:rename]
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
##
|
|
202
|
+
# Copy a file from +source+ to +dest+. Really simple, peeps.
|
|
203
|
+
# The reason this shit is even *slightly* complicated because
|
|
204
|
+
# it deals with file types. Otherwise I could write this
|
|
205
|
+
# in, what, 3 lines?
|
|
206
|
+
#
|
|
207
|
+
# @param [String] source the from
|
|
208
|
+
# @param [String] dest the to
|
|
209
|
+
def dirstate_copy(source, dest)
|
|
210
|
+
path = repo.working_join dest
|
|
211
|
+
|
|
212
|
+
if !File.exist?(path) || File.ftype(path) == 'link'
|
|
213
|
+
UI::warn "#{dest} doesn't exist!"
|
|
214
|
+
elsif not (File.ftype(path) == 'file' || File.ftype(path) == 'link')
|
|
215
|
+
UI::warn "copy failed: #{dest} is neither a file nor a symlink"
|
|
216
|
+
else
|
|
217
|
+
repo.lock_working do
|
|
218
|
+
# HOME FREE!!!!!!! i love getting out of school before noon :-D
|
|
219
|
+
# add it if it makes sense (like it was previously removed or untracked)
|
|
220
|
+
# and then copy da hoe
|
|
221
|
+
state = dirstate[dest].status
|
|
222
|
+
dirstate.add dest if [:untracked, :removed].include?(state)
|
|
223
|
+
dirstate.copy source => dest
|
|
224
|
+
dirstate.write
|
|
225
|
+
|
|
226
|
+
#Amp::Logger.info("copy #{source} -> #{dest}")
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
##
|
|
232
|
+
# Marks a modified file to be included in the next commit.
|
|
233
|
+
# If your VCS does this implicitly, this should be defined as a no-op.
|
|
234
|
+
#
|
|
235
|
+
# Mercurial: This is a no-op unless the specified files are not already
|
|
236
|
+
# in the repository, so we should add them to the repo in that case.
|
|
237
|
+
#
|
|
238
|
+
# @api
|
|
239
|
+
# @param [[String]] filenames a list of files to include for committing
|
|
240
|
+
# @return [Boolean] true for success, false for failure
|
|
241
|
+
def include(*filenames)
|
|
242
|
+
to_add = []
|
|
243
|
+
|
|
244
|
+
filenames.each do |filename|
|
|
245
|
+
unless dirstate[filename]
|
|
246
|
+
to_add << filename
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
add to_add if to_add.any?
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
##
|
|
254
|
+
# Returns a Symbol.
|
|
255
|
+
# Possible results:
|
|
256
|
+
# :added (subset of :included)
|
|
257
|
+
# :removed
|
|
258
|
+
# :untracked
|
|
259
|
+
# :included
|
|
260
|
+
# :normal
|
|
261
|
+
#
|
|
262
|
+
def file_status(filename)
|
|
263
|
+
dirstate[filename].status
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
##
|
|
267
|
+
# Returns whether or not the repository is tracking the given file.
|
|
268
|
+
#
|
|
269
|
+
# @param [String] filename the file to look up
|
|
270
|
+
# @return [Boolean] are we tracking the given file?
|
|
271
|
+
def tracking?(filename)
|
|
272
|
+
dirstate.tracking? filename
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
##
|
|
276
|
+
# Returns all files tracked by the repository *for the working directory* - not
|
|
277
|
+
# to be confused with the most recent changeset.
|
|
278
|
+
#
|
|
279
|
+
# @return [Array<String>] all files tracked by the repository at this moment in
|
|
280
|
+
# time, including just-added files (for example) that haven't been committed yet.
|
|
281
|
+
def all_files
|
|
282
|
+
dirstate.all_files
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
######### Optional API Methods ########################################
|
|
286
|
+
|
|
287
|
+
##
|
|
288
|
+
# Returns whether the given directory is being ignored. Optional method - defaults to
|
|
289
|
+
# +false+ at all times.
|
|
290
|
+
#
|
|
291
|
+
# @api-optional
|
|
292
|
+
# @param [String] directory the directory to check against ignoring rules
|
|
293
|
+
# @return [Boolean] are we ignoring this directory?
|
|
294
|
+
def ignoring_directory?(directory)
|
|
295
|
+
return true if @ignore_all
|
|
296
|
+
return false if @ignore_all == false
|
|
297
|
+
dirstate.ignoring_directory? directory
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
##
|
|
301
|
+
# Returns whether the given file is being ignored. Optional method - defaults to
|
|
302
|
+
# +false+ at all times.
|
|
303
|
+
#
|
|
304
|
+
# @api-optional
|
|
305
|
+
# @param [String] file the file to check against ignoring rules
|
|
306
|
+
# @return [Boolean] are we ignoring this file?
|
|
307
|
+
def ignoring_file?(file)
|
|
308
|
+
return true if @ignore_all
|
|
309
|
+
return false if @ignore_all == false
|
|
310
|
+
dirstate.ignore file
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
##
|
|
314
|
+
# Retrives the dirstate from the staging_area. The staging area is reponsible
|
|
315
|
+
# for properly maintaining the dirstate.
|
|
316
|
+
#
|
|
317
|
+
# @return [DirState]
|
|
318
|
+
def dirstate
|
|
319
|
+
return @dirstate if @dirstate ||= nil # the "||= nil" kills undefined ivar warning
|
|
320
|
+
|
|
321
|
+
opener = Amp::Opener.new repo.root
|
|
322
|
+
opener.default = :open_hg
|
|
323
|
+
|
|
324
|
+
@dirstate = DirState.new(repo.root, repo.config, opener)
|
|
325
|
+
@dirstate.read!
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
##
|
|
329
|
+
# Calculates the difference (in bytes) between a file and its last tracked state.
|
|
330
|
+
#
|
|
331
|
+
# Supplements the built-in #status method so that its output will include deltas.
|
|
332
|
+
#
|
|
333
|
+
# @apioptional
|
|
334
|
+
# @param [String] file the filename to look up
|
|
335
|
+
# @param [File::Stats] st the current results of File.lstat(file)
|
|
336
|
+
# @return [Fixnum] the number of bytes difference between the file and
|
|
337
|
+
# its last tracked state.
|
|
338
|
+
def calculate_delta(file, st)
|
|
339
|
+
state, mode, size, time = dirstate.files[file].to_a
|
|
340
|
+
st && size >= 0 ? (size - st.size).abs : 0 # increase the delta, but don't forget to check that it's not nil
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
##
|
|
344
|
+
# Does a detailed look at a file, to see if it is clean, modified, or needs to have its
|
|
345
|
+
# content checked precisely.
|
|
346
|
+
#
|
|
347
|
+
# Supplements the built-in #status method so that its output will be more
|
|
348
|
+
# accurate.
|
|
349
|
+
#
|
|
350
|
+
# @param [String] file the filename to look up
|
|
351
|
+
# @param [File::Stats] st the current results of File.lstat(file)
|
|
352
|
+
# @return [Symbol] a symbol representing the current file's status
|
|
353
|
+
def file_precise_status(file, st)
|
|
354
|
+
state, mode, size, time = dirstate.files[file].to_a
|
|
355
|
+
if (size >= 0 && (size != st.size || ((mode ^ st.mode) & 0100 and @check_exec))) || size == -2 || dirstate.copy_map[file]
|
|
356
|
+
return :modified
|
|
357
|
+
elsif time != st.mtime.to_i # DOH - we have to remember that times are stored as fixnums
|
|
358
|
+
return :lookup
|
|
359
|
+
else
|
|
360
|
+
return :clean
|
|
361
|
+
end
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
end
|
|
367
|
+
end
|
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
module Amp
|
|
2
|
+
module Repositories
|
|
3
|
+
module Mercurial
|
|
4
|
+
module Stores
|
|
5
|
+
extend self
|
|
6
|
+
class StoreError < StandardError; end
|
|
7
|
+
# Picks which store to use, given a list of requirements.
|
|
8
|
+
def pick(requirements, path, opener, pathjoiner=nil)
|
|
9
|
+
pathjoiner ||= proc {|*args| File.join(args) }
|
|
10
|
+
if requirements.include? 'store'
|
|
11
|
+
if requirements.include? 'fncache'
|
|
12
|
+
return FilenameCacheStore.new(path, opener, pathjoiner)
|
|
13
|
+
else
|
|
14
|
+
return EncodedStore.new(path, EncodedOpener, pathjoiner)
|
|
15
|
+
end
|
|
16
|
+
else
|
|
17
|
+
return BasicStore.new(path, opener, pathjoiner)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
##
|
|
22
|
+
# = BasicStore
|
|
23
|
+
# This class is the one from which all other stores derive. It implements
|
|
24
|
+
# basic methods #walk, #join, #datafiles, and #copy_list which are the
|
|
25
|
+
# public methods for all stores. All others are basically internal.
|
|
26
|
+
class BasicStore
|
|
27
|
+
BASIC_DATA_FILES = %W(data 00manifest.d 00manifest.i 00changelog.d 00changelog.i)
|
|
28
|
+
|
|
29
|
+
attr_accessor :path_joiner
|
|
30
|
+
attr_reader :path
|
|
31
|
+
attr_reader :opener
|
|
32
|
+
attr_reader :create_mode
|
|
33
|
+
|
|
34
|
+
def initialize(path, openerklass, pathjoiner)
|
|
35
|
+
@path_joiner, @path = pathjoiner, path
|
|
36
|
+
@create_mode = calculate_mode path
|
|
37
|
+
@opener = openerklass.new(@path)
|
|
38
|
+
@opener.create_mode = @create_mode
|
|
39
|
+
#@opener.default = :open_hg
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
##
|
|
43
|
+
# Joins the file _f_ to the store's base path using the path-joiner.
|
|
44
|
+
#
|
|
45
|
+
# @param [String] f the filename to join to the store's base path
|
|
46
|
+
# @return the combined base path and file path
|
|
47
|
+
def join(f)
|
|
48
|
+
@path_joiner.call(@path, f)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
##
|
|
52
|
+
# Iterates over every file tracked in the store and yield it.
|
|
53
|
+
#
|
|
54
|
+
# @yield [file] every file in the store
|
|
55
|
+
# @yieldparam [String] file the filepath to an entry in the store
|
|
56
|
+
def walk
|
|
57
|
+
datafiles do |x|
|
|
58
|
+
yield x
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
meta = do_walk '', false
|
|
62
|
+
meta.reverse.each do |x|
|
|
63
|
+
yield x
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
##
|
|
68
|
+
# Returns all the data files in the store.
|
|
69
|
+
def datafiles
|
|
70
|
+
do_walk('data', true)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
##
|
|
74
|
+
# Basic walker that is not very smart at all. It can recursively search
|
|
75
|
+
# for data files, but it actually uses a queue to do its searching.
|
|
76
|
+
#
|
|
77
|
+
# @param [String] relpath the base path to search
|
|
78
|
+
# @param [Boolean] recurse (false) whether or not to recursively
|
|
79
|
+
# search each discovered directory.
|
|
80
|
+
# @return [(String, String, Fixnum)] Each entry is returned in the form
|
|
81
|
+
# [filepath, filepath, filesize]
|
|
82
|
+
def do_walk(relpath, recurse=false)
|
|
83
|
+
path = join relpath
|
|
84
|
+
stripped_len = path.size + File::SEPARATOR.size - 1
|
|
85
|
+
list = []
|
|
86
|
+
if File.directory?(path)
|
|
87
|
+
to_visit = [path]
|
|
88
|
+
while to_visit.any?
|
|
89
|
+
p = to_visit.shift
|
|
90
|
+
Dir.stat_list(p, true) do |file, kind, stat|
|
|
91
|
+
fp = join(file)
|
|
92
|
+
if kind =~ /file/ && ['.d','.i'].include?(file[-2..-1])
|
|
93
|
+
n = fp[stripped_len..-1]
|
|
94
|
+
list << [n, n, stat.size]
|
|
95
|
+
elsif kind =~ /directory/ && recurse
|
|
96
|
+
to_visit << fp
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
list.sort
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
##
|
|
105
|
+
# Calculates the mode for the user on the file at the given path.
|
|
106
|
+
# I guess this saves some wasted chmods.
|
|
107
|
+
#
|
|
108
|
+
# @param [String] path the path to calculate the mode for
|
|
109
|
+
# @return [Fixnum] the mode to use for chmod. Octal, like 0777
|
|
110
|
+
def calculate_mode(path)
|
|
111
|
+
begin
|
|
112
|
+
mode = File.stat(path).mode
|
|
113
|
+
if (0777 & ~Amp::Support.UMASK) == (0777 & mode)
|
|
114
|
+
mode = nil
|
|
115
|
+
end
|
|
116
|
+
rescue
|
|
117
|
+
mode = nil
|
|
118
|
+
end
|
|
119
|
+
mode
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
##
|
|
123
|
+
# Returns the list of basic files that are crucial for the store to
|
|
124
|
+
# function.
|
|
125
|
+
#
|
|
126
|
+
# @return [Array<String>] the list of basic files crucial to this class
|
|
127
|
+
def copy_list
|
|
128
|
+
['requires'] + BASIC_DATA_FILES
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
##
|
|
133
|
+
# = EncodedOpener
|
|
134
|
+
# This opener uses the Stores' encoding function to modify the filename
|
|
135
|
+
# before it is loaded.
|
|
136
|
+
class EncodedOpener < Amp::Opener
|
|
137
|
+
|
|
138
|
+
##
|
|
139
|
+
# Overrides the normal opener method to use encoded filenames.
|
|
140
|
+
def open(f, mode="r", &block)
|
|
141
|
+
super(Stores.encode_filename(f), mode, &block)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
##
|
|
146
|
+
# = EncodedStore
|
|
147
|
+
# This version of the store uses encoded file paths to preserve
|
|
148
|
+
# consistency across platforms.
|
|
149
|
+
class EncodedStore < BasicStore
|
|
150
|
+
|
|
151
|
+
##
|
|
152
|
+
# over-ride the datafiles block so that it decodes filenames before
|
|
153
|
+
# it returns them.
|
|
154
|
+
#
|
|
155
|
+
# @see BasicStore
|
|
156
|
+
def datafiles
|
|
157
|
+
do_walk('data', true) do |a, b, size|
|
|
158
|
+
a = decode_filename(a) || nil
|
|
159
|
+
yield [a, b, size] if block_given?
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
##
|
|
164
|
+
# Encode the filename before joining
|
|
165
|
+
def join
|
|
166
|
+
@path_joiner.call @path, encode_filename(f)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
##
|
|
170
|
+
# We've got a new required file so let's include it
|
|
171
|
+
def copy_list
|
|
172
|
+
BASIC_DATA_FILES.inject ['requires', '00changelog.i'] do |a, f|
|
|
173
|
+
a + @path_joiner.call('store', f)
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
##
|
|
179
|
+
# = FilenameCache
|
|
180
|
+
# This module handles dealing with Filename Caches - namely, parsing
|
|
181
|
+
# them.
|
|
182
|
+
module FilenameCache
|
|
183
|
+
|
|
184
|
+
##
|
|
185
|
+
# Parses the filename cache, given an object capable of opening
|
|
186
|
+
# a file relative to the right directory.
|
|
187
|
+
#
|
|
188
|
+
# @param [Amp::Opener] opener An opener initialized to the repo's
|
|
189
|
+
# directory.
|
|
190
|
+
def self.parse(opener)
|
|
191
|
+
return unless File.exist? opener.join("fncache")
|
|
192
|
+
opener.open 'fncache', 'r' do |fp|
|
|
193
|
+
# error handling?
|
|
194
|
+
i = 0
|
|
195
|
+
fp.each_line do |line| #this is how we parse it
|
|
196
|
+
if line.size < 2 || line[-1,1] != "\n"
|
|
197
|
+
raise StoreError.new("invalid fncache entry, line #{i}")
|
|
198
|
+
end
|
|
199
|
+
yield line.chomp
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
##
|
|
205
|
+
# = FilenameCacheOpener
|
|
206
|
+
# This opener handles a cache of filenames that we are currently
|
|
207
|
+
# tracking. This way we don't need to recursively walk though
|
|
208
|
+
# the folders every single time. To use this class, you pass in
|
|
209
|
+
# the real Opener object (that responds to #open and returns a file
|
|
210
|
+
# pointer). then just treat it like any other opener. It will handle
|
|
211
|
+
# the behind-the-scenes work itself.
|
|
212
|
+
class FilenameCacheOpener < Amp::Opener
|
|
213
|
+
|
|
214
|
+
##
|
|
215
|
+
# Initializes a new FNCacheOpener. Requires a normal object capable
|
|
216
|
+
# of opening files.
|
|
217
|
+
#
|
|
218
|
+
# @param [Amp::Opener] opener an opener object initialized to the
|
|
219
|
+
# appropriate root directory.
|
|
220
|
+
def initialize(opener)
|
|
221
|
+
@opener = opener
|
|
222
|
+
@entries = nil
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def path; @opener.path; end
|
|
226
|
+
alias_method :root, :path
|
|
227
|
+
|
|
228
|
+
##
|
|
229
|
+
# Parses the filename cache and loads it into an ivar.
|
|
230
|
+
def load_filename_cache
|
|
231
|
+
@entries = {}
|
|
232
|
+
FilenameCache.parse @opener do |f|
|
|
233
|
+
@entries[f] = true
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
##
|
|
238
|
+
# Opens a file while being sure to write the filename if we haven't
|
|
239
|
+
# seen it before. Just like the normal Opener's open() method.
|
|
240
|
+
#
|
|
241
|
+
# @param [String] path the path to the file
|
|
242
|
+
# @param [Fixnum] mode the read/write/append mode
|
|
243
|
+
# @param block the block to pass to it (optional)
|
|
244
|
+
def open(path, mode='r', &block)
|
|
245
|
+
|
|
246
|
+
if mode !~ /r/ && path =~ /data\//
|
|
247
|
+
load_filename_cache if @entries.nil?
|
|
248
|
+
if @entries[path].nil?
|
|
249
|
+
@opener.open('fncache','ab') {|f| f.puts path }
|
|
250
|
+
@entries[path] = true
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
begin
|
|
255
|
+
@opener.open(Stores.hybrid_encode(path), mode, &block)
|
|
256
|
+
rescue Errno::ENOENT
|
|
257
|
+
raise
|
|
258
|
+
rescue
|
|
259
|
+
raise unless mode == 'r'
|
|
260
|
+
end
|
|
261
|
+
rescue
|
|
262
|
+
raise
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
##
|
|
269
|
+
# = FilenameCacheStore
|
|
270
|
+
# This version of the store uses a "Filename Cache", which is just a file
|
|
271
|
+
# that names all the tracked files in the store. It also uses an even more
|
|
272
|
+
# advanced "hybrid" encoding for filenames that again ensure consistency across
|
|
273
|
+
# platforms. However, this encoding is non-reversible - but since we're just
|
|
274
|
+
# doing file lookups anyway, that's just ducky.
|
|
275
|
+
class FilenameCacheStore < BasicStore
|
|
276
|
+
|
|
277
|
+
##
|
|
278
|
+
# Initializes the store. Sets up the cache right away.
|
|
279
|
+
#
|
|
280
|
+
# @see BasicStore
|
|
281
|
+
def initialize(path, openerklass, pathjoiner)
|
|
282
|
+
@path_joiner = pathjoiner
|
|
283
|
+
@path = pathjoiner.call(path, 'store')
|
|
284
|
+
@create_mode = calculate_mode @path
|
|
285
|
+
@_op = openerklass.new(@path)
|
|
286
|
+
@_op.create_mode = @create_mode
|
|
287
|
+
@_op.default = :open_file
|
|
288
|
+
|
|
289
|
+
@opener = FilenameCache::FilenameCacheOpener.new(@_op)
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
##
|
|
293
|
+
# Properly joins the path, but hybrid-encodes the file's path
|
|
294
|
+
# first.
|
|
295
|
+
def join(f)
|
|
296
|
+
@path_joiner.call(@path, Stores.hybrid_encode(f))
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
##
|
|
300
|
+
# Here's how we walk through the files now. Oh, look, we don't need
|
|
301
|
+
# to do annoying directory traversal anymore! But we do have to
|
|
302
|
+
# maintain a consistent fnstore file. I think I can live with that.
|
|
303
|
+
def datafiles
|
|
304
|
+
rewrite = false
|
|
305
|
+
existing = []
|
|
306
|
+
pjoin = @path_joiner
|
|
307
|
+
spath = @path
|
|
308
|
+
result = []
|
|
309
|
+
FilenameCache.parse(@_op) do |f|
|
|
310
|
+
|
|
311
|
+
ef = Stores.hybrid_encode f
|
|
312
|
+
begin
|
|
313
|
+
st = File.stat(@path_joiner.call(spath, ef))
|
|
314
|
+
yield [f, ef, st.size] if block_given?
|
|
315
|
+
result << [f, ef, st.size] unless block_given?
|
|
316
|
+
existing << f
|
|
317
|
+
rescue Errno::ENOENT
|
|
318
|
+
rewrite = true
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
if rewrite
|
|
322
|
+
fp = @_op.open('fncache', 'wb')
|
|
323
|
+
existing.each do |p|
|
|
324
|
+
fp.write(p + "\n")
|
|
325
|
+
end
|
|
326
|
+
fp.close
|
|
327
|
+
end
|
|
328
|
+
result
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
##
|
|
332
|
+
# A more advanced list of files we need, properly joined and whatnot.
|
|
333
|
+
def copy_list
|
|
334
|
+
d = BASIC_DATA_FILES + ['dh', 'fncache']
|
|
335
|
+
d.inject ['requires', '00changelog.i'] do |a, f|
|
|
336
|
+
a + @path_joiner.call('store', f)
|
|
337
|
+
end
|
|
338
|
+
result
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
#############################################
|
|
345
|
+
############ Encoding formats ###############
|
|
346
|
+
#############################################
|
|
347
|
+
|
|
348
|
+
##
|
|
349
|
+
# Gets the basic character map that maps disallowed letters to
|
|
350
|
+
# allowable substitutes.
|
|
351
|
+
#
|
|
352
|
+
# @param [Boolean] underscore Should underscores be inserted in front of
|
|
353
|
+
# capital letters before we downcase them? (e.g. if true, "A" => "_a")
|
|
354
|
+
def illegal_character_map(underscore=true)
|
|
355
|
+
e = '_'
|
|
356
|
+
win_reserved = "\\:*?\"<>|".split("").map {|x| x.ord}
|
|
357
|
+
cmap = {}; 0.upto(126) {|x| cmap[x.chr] = x.chr}
|
|
358
|
+
((0..31).to_a + (126..255).to_a + win_reserved).each do |x|
|
|
359
|
+
cmap[x.chr] = "~%02x" % x
|
|
360
|
+
end
|
|
361
|
+
((("A".ord)..("Z".ord)).to_a + [e.ord]).each do |x|
|
|
362
|
+
cmap[x.chr] = e + x.chr.downcase if underscore
|
|
363
|
+
cmap[x.chr] = x.chr.downcase unless underscore
|
|
364
|
+
end
|
|
365
|
+
cmap
|
|
366
|
+
end
|
|
367
|
+
memoize_method :illegal_character_map, true
|
|
368
|
+
|
|
369
|
+
##
|
|
370
|
+
# Reversible encoding of the filename
|
|
371
|
+
#
|
|
372
|
+
# @param [String] s a file's path you wish to encode
|
|
373
|
+
# @param [Boolean] underscore should we insert underscores when
|
|
374
|
+
# downcasing letters? (e.g. if true, "A" => "_a")
|
|
375
|
+
# @return [String] an encoded file path
|
|
376
|
+
def encode_filename(s, underscore=true)
|
|
377
|
+
cmap = illegal_character_map underscore
|
|
378
|
+
s.split("").map {|c| cmap[c]}.join
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
##
|
|
382
|
+
# Decodes an encoding performed by encode_filename
|
|
383
|
+
#
|
|
384
|
+
# @param [String] s an encoded file path
|
|
385
|
+
# @param [String] the decoded file path
|
|
386
|
+
def decode_filename(s)
|
|
387
|
+
cmap = illegal_character_map true
|
|
388
|
+
dmap = {}
|
|
389
|
+
cmap.each do |k, v|
|
|
390
|
+
dmap[v] = k
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
i = 0
|
|
394
|
+
result = []
|
|
395
|
+
while i < s.size
|
|
396
|
+
1.upto(3) do |l|
|
|
397
|
+
if dmap[s[i..(i+l-1)]]
|
|
398
|
+
result << dmap[s[i..(i+l-1)]]
|
|
399
|
+
i += l
|
|
400
|
+
break
|
|
401
|
+
end
|
|
402
|
+
end
|
|
403
|
+
end
|
|
404
|
+
result.join
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
# can't name a file one of these on windows, apparently
|
|
408
|
+
WINDOWS_RESERVED_FILENAMES = %w(con prn aux nul com1
|
|
409
|
+
com2 com3 com4 com5 com6 com7 com8 com8 lpt1 lpt2
|
|
410
|
+
lpt3 lpt4 lpt5 lpt6 lpt7 lpt8 lpt9)
|
|
411
|
+
|
|
412
|
+
##
|
|
413
|
+
# Copypasta
|
|
414
|
+
def auxilliary_encode(path)
|
|
415
|
+
res = []
|
|
416
|
+
path.split('/').each do |n|
|
|
417
|
+
if n.any?
|
|
418
|
+
base = n.split('.')[0]
|
|
419
|
+
if !(base.nil?) && base.any? && WINDOWS_RESERVED_FILENAMES.include?(base)
|
|
420
|
+
ec = "~%02x" % n[2,1].ord
|
|
421
|
+
n = n[0..1] + ec + n[3..-1]
|
|
422
|
+
end
|
|
423
|
+
if ['.',' '].include? n[-1,1]
|
|
424
|
+
n = n[0..-2] + ("~%02x" % n[-1,1].ord)
|
|
425
|
+
end
|
|
426
|
+
end
|
|
427
|
+
res << n
|
|
428
|
+
end
|
|
429
|
+
res.join("/")
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
##
|
|
433
|
+
# Normal encoding, but without extra underscores in the filenames.
|
|
434
|
+
def lower_encode(s)
|
|
435
|
+
encode_filename s, false
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
MAX_PATH_LEN_IN_HGSTORE = 120
|
|
439
|
+
DIR_PREFIX_LEN = 8
|
|
440
|
+
MAX_SHORTENED_DIRS_LEN = 8 * (DIR_PREFIX_LEN + 1) - 4
|
|
441
|
+
|
|
442
|
+
##
|
|
443
|
+
# uber encoding that's straight up crazy.
|
|
444
|
+
# Max length of 120 means we have a non-reversible encoding,
|
|
445
|
+
# but since the FilenameCache only cares about name lookups, one-way
|
|
446
|
+
# is really all that matters!
|
|
447
|
+
#
|
|
448
|
+
# @param [String] path the path to encode
|
|
449
|
+
# @return [String] an encoded path, with a maximum length of 120.
|
|
450
|
+
def hybrid_encode(path)
|
|
451
|
+
return path unless path =~ /data\//
|
|
452
|
+
ndpath = path["data/".size..-1]
|
|
453
|
+
res = "data/" + auxilliary_encode(encode_filename(ndpath))
|
|
454
|
+
if res.size > MAX_PATH_LEN_IN_HGSTORE
|
|
455
|
+
digest = path.sha1.hexdigest
|
|
456
|
+
aep = auxilliary_encode(lower_encode(ndpath))
|
|
457
|
+
root, ext = File.amp_split_extension aep
|
|
458
|
+
parts = aep.split('/')
|
|
459
|
+
basename = File.basename aep
|
|
460
|
+
sdirs = []
|
|
461
|
+
parts[0..-2].each do |p|
|
|
462
|
+
d = p[0..(DIR_PREFIX_LEN-1)]
|
|
463
|
+
|
|
464
|
+
d = d[0..-2] + "_" if " .".include?(d[-1,1])
|
|
465
|
+
|
|
466
|
+
t = sdirs.join("/") + "/" + d
|
|
467
|
+
break if t.size > MAX_SHORTENED_DIRS_LEN
|
|
468
|
+
|
|
469
|
+
sdirs << d
|
|
470
|
+
end
|
|
471
|
+
dirs = sdirs.join("/")
|
|
472
|
+
dirs += "/" if dirs.size > 0
|
|
473
|
+
|
|
474
|
+
res = "dh/" + dirs + digest + ext
|
|
475
|
+
space_left = MAX_PATH_LEN_IN_HGSTORE - res.size
|
|
476
|
+
if space_left > 0
|
|
477
|
+
filler = basename[0..(space_left-1)]
|
|
478
|
+
res = "dh/" + dirs + filler + digest + ext
|
|
479
|
+
end
|
|
480
|
+
end
|
|
481
|
+
return res
|
|
482
|
+
|
|
483
|
+
end
|
|
484
|
+
end
|
|
485
|
+
end
|
|
486
|
+
end
|
|
487
|
+
end
|