amp-core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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