amp-core 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. data/.document +5 -0
  2. data/.gitignore +23 -0
  3. data/Gemfile +11 -0
  4. data/LICENSE +20 -0
  5. data/README.rdoc +17 -0
  6. data/Rakefile +67 -0
  7. data/VERSION +1 -0
  8. data/features/amp-core.feature +9 -0
  9. data/features/step_definitions/amp-core_steps.rb +0 -0
  10. data/features/support/env.rb +4 -0
  11. data/lib/amp-core.rb +53 -0
  12. data/lib/amp-core/command_ext/repository_loading.rb +31 -0
  13. data/lib/amp-core/repository/abstract/abstract_changeset.rb +113 -0
  14. data/lib/amp-core/repository/abstract/abstract_local_repo.rb +208 -0
  15. data/lib/amp-core/repository/abstract/abstract_staging_area.rb +202 -0
  16. data/lib/amp-core/repository/abstract/abstract_versioned_file.rb +116 -0
  17. data/lib/amp-core/repository/abstract/common_methods/changeset.rb +185 -0
  18. data/lib/amp-core/repository/abstract/common_methods/local_repo.rb +293 -0
  19. data/lib/amp-core/repository/abstract/common_methods/staging_area.rb +248 -0
  20. data/lib/amp-core/repository/abstract/common_methods/versioned_file.rb +87 -0
  21. data/lib/amp-core/repository/generic_repo_picker.rb +94 -0
  22. data/lib/amp-core/repository/repository.rb +41 -0
  23. data/lib/amp-core/support/encoding_utils.rb +46 -0
  24. data/lib/amp-core/support/platform_utils.rb +92 -0
  25. data/lib/amp-core/support/rooted_opener.rb +143 -0
  26. data/lib/amp-core/support/string_utils.rb +86 -0
  27. data/lib/amp-core/templates/git/blank.log.erb +18 -0
  28. data/lib/amp-core/templates/git/default.log.erb +18 -0
  29. data/lib/amp-core/templates/mercurial/blank.commit.erb +23 -0
  30. data/lib/amp-core/templates/mercurial/blank.log.erb +18 -0
  31. data/lib/amp-core/templates/mercurial/default.commit.erb +23 -0
  32. data/lib/amp-core/templates/mercurial/default.log.erb +26 -0
  33. data/lib/amp-core/templates/template.rb +202 -0
  34. data/spec/amp-core_spec.rb +11 -0
  35. data/spec/command_ext_specs/repository_loading_spec.rb +64 -0
  36. data/spec/command_ext_specs/spec_helper.rb +1 -0
  37. data/spec/repository_specs/repository_spec.rb +41 -0
  38. data/spec/repository_specs/spec_helper.rb +1 -0
  39. data/spec/spec.opts +1 -0
  40. data/spec/spec_helper.rb +14 -0
  41. data/spec/support_specs/encoding_utils_spec.rb +69 -0
  42. data/spec/support_specs/platform_utils_spec.rb +33 -0
  43. data/spec/support_specs/spec_helper.rb +1 -0
  44. data/spec/support_specs/string_utils_spec.rb +44 -0
  45. data/test/test_templates.rb +81 -0
  46. metadata +157 -0
@@ -0,0 +1,293 @@
1
+ ##################################################################
2
+ # Licensing Information #
3
+ # #
4
+ # The following code is licensed, as standalone code, under #
5
+ # the Ruby License, unless otherwise directed within the code. #
6
+ # #
7
+ # For information on the license of this code when distributed #
8
+ # with and used in conjunction with the other modules in the #
9
+ # Amp project, please see the root-level LICENSE file. #
10
+ # #
11
+ # © Michael J. Edgar and Ari Brown, 2009-2010 #
12
+ # #
13
+ ##################################################################
14
+
15
+ module Amp
16
+ module Core
17
+ module Repositories
18
+
19
+ ##
20
+ # = CommonLocalRepoMethods
21
+ #
22
+ # These methods are common to all repositories, and this module is mixed into
23
+ # the AbstractLocalRepository class. This guarantees that all repositories will
24
+ # have these methods.
25
+ #
26
+ # No methods should be placed into this module unless it relies on methods in the
27
+ # general API for repositories.
28
+ module CommonLocalRepoMethods
29
+ include Enumerable
30
+ attr_accessor :config
31
+
32
+ ##
33
+ # Initializes a new directory to the given path, and with the current
34
+ # configuration.
35
+ #
36
+ # @param [String] path a path to the Repository.
37
+ # @param [Boolean] create Should we create a new one? Usually for
38
+ # the "amp init" command.
39
+ # @param [Amp::AmpConfig] config the configuration loaded from the user's
40
+ # system. Will have some settings overwritten by the repo's hgrc.
41
+ def initialize(path="", create=false, config=nil)
42
+ @capabilities = {}
43
+ @root = File.expand_path path.chomp("/")
44
+ @config = config
45
+ end
46
+
47
+ ##
48
+ # Initializes a new repository in the given directory. We recommend
49
+ # calling this at some point in your repository subclass as it will
50
+ # do amp-specific initialization, though you will need to do all the
51
+ # hard stuff yourself.
52
+ def init(config=@config)
53
+ FileUtils.makedirs root
54
+ working_write "Ampfile", <<-EOF
55
+ # Any ruby code here will be executed before Amp loads a repository and
56
+ # dispatches a command.
57
+ #
58
+ # Example command:
59
+ #
60
+ # command "echo" do |c|
61
+ # c.opt :"no-newline", "Don't print a trailing newline character", :short => "-n"
62
+ # c.on_run do |opts, args|
63
+ # print args.join(" ")
64
+ # print "\\n" unless opts[:"no-newline"]
65
+ # end
66
+ # end
67
+ EOF
68
+
69
+ end
70
+
71
+ ##
72
+ # Joins the path to the repo's root (not .hg, the working dir root)
73
+ #
74
+ # @param path the path we're joining
75
+ # @return [String] the path joined to the working directory's root
76
+ def working_join(path)
77
+ File.join(root, path)
78
+ end
79
+
80
+ ##
81
+ # Call the hooks that run under +call+
82
+ #
83
+ # @param [Symbol] call the location in the system where the hooks
84
+ # are to be called
85
+ def run_hook(call, opts={:throw => false})
86
+ Hook.run_hook(call, opts)
87
+ end
88
+
89
+ ##
90
+ # Adds a list of file paths to the repository for the next commit.
91
+ #
92
+ # @param [String, Array<String>] paths the paths of the files we need to
93
+ # add to the next commit
94
+ # @return [Array<String>] which files WEREN'T added
95
+ def add(*paths)
96
+ staging_area.add(*paths)
97
+ end
98
+
99
+ ##
100
+ # Removes the file (or files) from the repository. Marks them as removed
101
+ # in the DirState, and if the :unlink option is provided, the files are
102
+ # deleted from the filesystem.
103
+ #
104
+ # @param list the list of files. Could also just be 1 file as a string.
105
+ # should be paths.
106
+ # @param opts the options for this removal. Must be last argument or will mess
107
+ # things up.
108
+ # @option opts [Boolean] :unlink (false) whether or not to delete the
109
+ # files from the filesystem after marking them as removed from the
110
+ # DirState.
111
+ # @return [Boolean] success?
112
+ def remove(*args)
113
+ staging_area.remove(*args)
114
+ end
115
+
116
+ def relative_join(file, cur_dir=FileUtils.pwd)
117
+ @root_pathname ||= Pathname.new(root)
118
+ Pathname.new(File.expand_path(File.join(cur_dir, file))).relative_path_from(@root_pathname).to_s
119
+ end
120
+
121
+ ##
122
+ # Walk recursively through the directory tree (or a changeset)
123
+ # finding all files matched by the match function
124
+ #
125
+ # @param [String, Integer] node selects which changeset to walk
126
+ # @param [Amp::Match] match the matcher decides how to pick the files
127
+ # @param [Array<String>] an array of filenames
128
+ def walk(node=nil, match = Match.create({}) { true })
129
+ self[node].walk match # calls Changeset#walk
130
+ end
131
+
132
+ ##
133
+ # Iterates over each changeset in the repository, from oldest to newest.
134
+ #
135
+ # @yield each changeset in the repository is yielded to the caller, in order
136
+ # from oldest to newest. (Actually, lowest revision # to highest revision #)
137
+ def each(&block)
138
+ 0.upto(size - 1) { |i| yield self[i] }
139
+ end
140
+
141
+
142
+ ##
143
+ # This gives the status of the repository, comparing 2 node in
144
+ # its history. Now, with no parameters, it's going to compare the
145
+ # last revision with the working directory, which is the most common
146
+ # usage - that answers "what is the current status of the repository,
147
+ # compared to the last time a commit happened?". However, given any
148
+ # two revisions, it can compare them.
149
+ #
150
+ # @example @repo.status # => {:unknown => ['code/smthng.rb'], :added => [], ...}
151
+ # @param [Hash] opts the options for this command. there's a bunch.
152
+ # @option opts [String, Integer] :node_1 (".") an identifier for the starting
153
+ # revision
154
+ # @option opts [String, Integer] :node_2 (nil) an identifier for the ending
155
+ # revision. Defaults to the working directory.
156
+ # @option opts [Proc] :match (proc { true }) a proc that will match
157
+ # a file, so we know if we're interested in it.
158
+ # @option opts [Boolean] :ignored (false) do we want to see files we're
159
+ # ignoring?
160
+ # @option opts [Boolean] :clean (false) do we want to see files that are
161
+ # totally unchanged?
162
+ # @option opts [Boolean] :unknown (false) do we want to see files we've
163
+ # never seen before (i.e. files the user forgot to add to the repo)?
164
+ # @option opts [Boolean] :delta (false) do we want to see the overall delta?
165
+ # @return [Hash{Symbol => Array<String>}] no, I'm not kidding. the keys are:
166
+ # :modified, :added, :removed, :deleted, :unknown, :ignored, :clean, and :delta. The
167
+ # keys are the type of change, and the values are arrays of filenames
168
+ # (local to the root) that are under each key.
169
+ def status(opts={:node_1 => '.'})
170
+ run_hook :status
171
+
172
+ opts[:delta] ||= true
173
+ node1, node2, match = opts[:node_1], opts[:node_2], opts[:match]
174
+
175
+ match = Match.create({}) { true } unless match
176
+
177
+ node1 = self[node1] unless node1.kind_of? Repositories::AbstractChangeset # get changeset objects
178
+ node2 = self[node2] unless node2.kind_of? Repositories::AbstractChangeset
179
+
180
+ # are we working with working directories?
181
+ working = node2.working?
182
+ comparing_to_tip = working && node2.parents.include?(node1)
183
+
184
+ status = Hash.new {|h, k| h[k] = k == :delta ? 0 : [] }
185
+
186
+ if working
187
+ # get the dirstate's latest status
188
+ status.merge! staging_area.status(opts[:ignored], opts[:clean], opts[:unknown], match)
189
+
190
+ # this case is run about 99% of the time
191
+ # do we need to do hashes on any files to see if they've changed?
192
+ if comparing_to_tip && status[:lookup].any?
193
+ clean, modified = fix_files(status[:lookup], node1, node2)
194
+
195
+ status[:clean].concat clean
196
+ status[:modified].concat modified
197
+ end
198
+ end
199
+ # if we're working with old revisions...
200
+ unless comparing_to_tip
201
+ # get the older revision manifest
202
+ node1_file_list = node1.all_files.dup
203
+ node2_file_list = node2.all_files.dup
204
+ if working
205
+ # remove any files we've marked as removed them from the '.' manifest
206
+ status[:removed].each {|file| node2_file_list.delete file }
207
+ end
208
+
209
+ # Every file in the later revision (or working directory)
210
+ node2.all_files.each do |file|
211
+ # Does it exist in the old manifest? If so, it wasn't added.
212
+ if node1.include? file
213
+ # It's in the old manifest, so lets check if its been changed
214
+ # Else, it must be unchanged
215
+ if file_modified? file, :node1 => node1, :node2 => node2 # tests.any?
216
+ status[:modified] << file
217
+ elsif opts[:clean]
218
+ status[:clean] << file
219
+ end
220
+ # Remove that file from the old manifest, since we've checked it
221
+ node1_file_list.delete file
222
+ else
223
+ # if it's not in the old manifest, it's been added
224
+ status[:added] << file
225
+ end
226
+ end
227
+
228
+ # Anything left in the old manifest is a file we've removed since the
229
+ # first revision.
230
+ status[:removed] = node1_file_list
231
+ end
232
+
233
+ # We're done!
234
+ status.delete :lookup # because nobody cares about it
235
+ delta = status.delete :delta
236
+
237
+ status.each {|k, v| v.sort! } # sort dem fuckers
238
+ status[:delta] = delta if opts[:delta]
239
+ status.each {|k, _| status.delete k if opts[:only] && !opts[:only].include?(k) }
240
+ status
241
+ end
242
+
243
+ ##
244
+ # Look up the files in +lookup+ to make sure
245
+ # they're either the same or not. Normally, we can
246
+ # just tell if two files are the same by looking at their sizes. But
247
+ # sometimes, we can't! That's where this method comes into play; it
248
+ # hashes the files to verify integrity.
249
+ #
250
+ # @param [String] lookup files to look up
251
+ # @param node1
252
+ # @param node2
253
+ # @return [[String], [String]] clean files and modified files
254
+ def fix_files(lookup, node1, node2)
255
+ write_dirstate = false # this gets returned
256
+ modified = [] # and this
257
+ fixup = [] # fixup are files that haven't changed so they're being
258
+ # marked wrong in the dirstate. this gets returned
259
+
260
+ lookup.each do |file|
261
+ # this checks to see if the file has been modified after doing
262
+ # hashes/flag checks
263
+ tests = [ node1.include?(file) ,
264
+ node2.flags(file) == node1.flags(file) ,
265
+ node1[file] === node2[file] ]
266
+
267
+ unless tests.all?
268
+ modified << file
269
+ else
270
+ fixup << file # mark the file as clean
271
+ end
272
+ end
273
+
274
+
275
+ # mark every fixup'd file as clean in the dirstate
276
+ begin
277
+ lock_working do
278
+ staging_area.normal *fixup
279
+ fixup.each do |file|
280
+ modified.delete file
281
+ end
282
+ end
283
+ rescue LockError
284
+ end
285
+
286
+ # the fixups are actually clean
287
+ [fixup, modified]
288
+ end
289
+ end
290
+
291
+ end
292
+ end
293
+ end
@@ -0,0 +1,248 @@
1
+ ##################################################################
2
+ # Licensing Information #
3
+ # #
4
+ # The following code is licensed, as standalone code, under #
5
+ # the Ruby License, unless otherwise directed within the code. #
6
+ # #
7
+ # For information on the license of this code when distributed #
8
+ # with and used in conjunction with the other modules in the #
9
+ # Amp project, please see the root-level LICENSE file. #
10
+ # #
11
+ # © Michael J. Edgar and Ari Brown, 2009-2010 #
12
+ # #
13
+ ##################################################################
14
+
15
+ module Amp
16
+ module Core
17
+ module Repositories
18
+
19
+ ##
20
+ # = CommonStagingAreaMethods
21
+ #
22
+ # These methods are common to all staging areas, and this module is mixed into
23
+ # the AbstractStagingArea class. This guarantees that all staging areas will
24
+ # have these methods.
25
+ #
26
+ # No methods should be placed into this module unless it relies on methods in the
27
+ # general API for staging areas.
28
+ module CommonStagingAreaMethods
29
+
30
+ ##
31
+ # Returns whether or not the repository is tracking the given file.
32
+ #
33
+ # @api
34
+ # @param [String] filename the file to look up
35
+ # @return [Boolean] are we tracking the given file?
36
+ def tracking?(filename)
37
+ file_status(filename) != :untracked
38
+ end
39
+
40
+ ##
41
+ # Helper method that filters out a list of explicit filenames that do not
42
+ # belong in the repository. It then stores the File stats of the files
43
+ # it keeps, and returns any explicitly named directories.
44
+ #
45
+ # @param [Array<String>] files the files to examine
46
+ # @param [Amp::Match] match the matcher object
47
+ # @return [Array<Hash, Array<String>>] returns an array: [file_stats, directories]
48
+ def examine_named_files(files, match)
49
+ results, work = {vcs_dir => true}, [] # ignore the .hg
50
+ files.reject {|f| results[f] || f == ""}.sort.each do |file|
51
+ path = File.join(repo.root, file)
52
+
53
+ if File.exist?(path)
54
+ # we'll take it! but only if it's a directory, which means we have
55
+ # more work to do...
56
+ if File.directory?(path)
57
+ # add it to the list of dirs we have to search in
58
+ work << File.join(repo.root, file) unless ignoring_directory? file
59
+ elsif File.file?(path) || File.symlink?(path)
60
+ # ARGH WE FOUND ZE BOOTY
61
+ results[file] = File.lstat path
62
+ else
63
+ # user you are a fuckup in life please exit the world
64
+ UI::warn "#{file}: unsupported file type (type is #{File.ftype file})"
65
+ results[file] = nil if tracking? file
66
+ end
67
+ else
68
+ prefix = file + '/'
69
+
70
+ unless all_files.find { |f, _| f == file || f.start_with?(prefix) }
71
+ bad_type[file]
72
+ results[file] = nil if (tracking?(file) || !ignoring_file?(file)) && match.call(file)
73
+ end
74
+ end
75
+ end
76
+ [results, work]
77
+ end
78
+
79
+ ##
80
+ # Helper method that runs match's patterns on every non-ignored file in
81
+ # the repository's directory.
82
+ #
83
+ # @param [Hash] found_files the already found files (we don't want to search them
84
+ # again)
85
+ # @param [Array<String>] dirs the directories to search
86
+ # @param [Amp::Match] match the matcher object that runs patterns against
87
+ # filenames
88
+ # @return [Hash] the updated found_files hash
89
+ def find_with_patterns(found_files, dirs, match)
90
+ results = found_files
91
+ Find.find(*dirs) do |f|
92
+ tf = f[(repo.root.size+1)..-1]
93
+ Find.prune if results[tf]
94
+
95
+ stats = File.lstat f
96
+ match_result = match.call tf
97
+ tracked = tracking? tf
98
+
99
+ if File.directory? f
100
+ Find.prune if ignoring_file? tf
101
+ results[tf] = nil if tracked && match_result
102
+ elsif File.file?(f) || File.symlink?(f)
103
+ if match_result && (tracked || !ignoring_file?(tf))
104
+ results[tf] = stats
105
+ end
106
+ elsif tracked && match_result
107
+ results[tf] = nil
108
+ end
109
+ end
110
+ results
111
+ end
112
+
113
+ ##
114
+ # Walk recursively through the directory tree, finding all
115
+ # files matched by the regexp in match.
116
+ #
117
+ # Step 1: find all explicit files
118
+ # Step 2: visit subdirectories
119
+ # Step 3: report unseen items in the @files hash
120
+ #
121
+ # @todo this is still tied to hg
122
+ # @param [Boolean] unknown
123
+ # @param [Boolean] ignored
124
+ # @return [Hash{String => [NilClass, File::Stat]}] nil for directories and
125
+ # stuff, File::Stat for files and links
126
+ def walk(unknown, ignored, match = Amp::Match.new { true })
127
+ if ignored
128
+ @ignore_all = false
129
+ elsif not unknown
130
+ @ignore_all = true
131
+ end
132
+
133
+ files = (match.files || []).uniq
134
+
135
+ # why do we overwrite the entire array if it includes the current dir?
136
+ # we even kill posisbly good things
137
+ files = [''] if files.include?('.') # strange thing to do
138
+
139
+ # Step 1: find all explicit files
140
+ results, found_directories = examine_named_files files, match
141
+ work = [repo.root] + found_directories
142
+
143
+ # Run the patterns
144
+ results = find_with_patterns(results, work, match)
145
+
146
+ # step 3: report unseen items in @files
147
+ visit = all_files.select {|f| !results[f] && match.call(f) }.sort
148
+
149
+ visit.each do |file|
150
+ path = File.join(repo.root, file)
151
+ keep = File.exist?(path) && (File.file?(path) || File.symlink(path))
152
+ results[file] = keep ? File.lstat(path) : nil
153
+ end
154
+
155
+ results.delete vcs_dir
156
+ @ignore_all = nil # reset this
157
+ results
158
+ end
159
+
160
+ ##
161
+ # what's the current state of life, man!
162
+ # Splits up all the files into modified, clean,
163
+ # added, deleted, unknown, ignored, or lookup-needed.
164
+ #
165
+ # @param [Boolean] ignored do we collect the ignore files?
166
+ # @param [Boolean] clean do we collect the clean files?
167
+ # @param [Boolean] unknown do we collect the unknown files?
168
+ # @param [Amp::Match] match the matcher
169
+ # @return [Hash{Symbol => Array<String>}] a hash of the filestatuses and their files
170
+ def status(ignored, clean, unknown, match = Match.new { true })
171
+ list_ignored, list_clean, list_unknown = ignored, clean, unknown
172
+ lookup, modified, added, unknown, ignored = [], [], [], [], []
173
+ moved, copied, removed, deleted, clean = [], [], [], [], []
174
+ delta = 0
175
+
176
+ walk(list_unknown, list_ignored, match).each do |file, st|
177
+ next if file.nil?
178
+
179
+ unless tracking?(file)
180
+ if list_ignored && ignoring_directory?(file)
181
+ ignored << file
182
+ elsif list_unknown
183
+ unknown << file unless ignoring_file?(file)
184
+ end
185
+
186
+ next # on to the next one, don't do the rest
187
+ end
188
+
189
+ # here's where we split up the files
190
+ state = file_status file
191
+
192
+ delta += calculate_delta(file, st)
193
+ if !st && [:normal, :modified, :added].include?(state)
194
+ # add it to the deleted folder if it should be here but isn't
195
+ deleted << file
196
+ elsif state == :normal
197
+ case file_precise_status(file, st)
198
+ when :modified
199
+ modified << file
200
+ when :lookup
201
+ lookup << file
202
+ when :clean
203
+ clean << file if list_clean
204
+ end
205
+
206
+ elsif state == :merged
207
+ modified << file
208
+ elsif state == :added
209
+ added << file
210
+ elsif state == :removed
211
+ removed << file
212
+ end
213
+ end
214
+
215
+ # # This code creates the copied and moved arrays
216
+ # #
217
+ # # ugh this should be optimized
218
+ # # as in, built into the code above ^^^^^
219
+ # dirstate.copy_map.each do |dst, src|
220
+ # # assume that if +src+ is in +removed+ then +dst+ is in +added+
221
+ # # we know that this part will be COPIES
222
+ # if removed.include? src
223
+ # removed.delete src
224
+ # added.delete dst
225
+ # copied << [src, dst]
226
+ # elsif added.include? dst # these are the MOVES
227
+ # added.delete dst
228
+ # moved << [src, dst]
229
+ # end
230
+ # end
231
+
232
+ r = { :modified => modified.sort , # those that have clearly been modified
233
+ :added => added.sort , # those that are marked for adding
234
+ :removed => removed.sort , # those that are marked for removal
235
+ :deleted => deleted.sort , # those that should be here but aren't
236
+ :unknown => unknown.sort , # those that aren't being tracked
237
+ :ignored => ignored.sort , # those that are being deliberately ignored
238
+ :clean => clean.sort , # those that haven't changed
239
+ :lookup => lookup.sort , # those that need to be content-checked to see if they've changed
240
+ #:copied => copied.sort_by {|a| a[0] }, # those that have been copied
241
+ #:moved => moved.sort_by {|a| a[0] }, # those that have been moved
242
+ :delta => delta # how many bytes have been added or removed from files (not bytes that have been changed)
243
+ }
244
+ end
245
+ end
246
+ end
247
+ end
248
+ end