esr-rim 1.1.5

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 (48) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG +40 -0
  3. data/README.md +3 -0
  4. data/Rakefile +56 -0
  5. data/bin/rim +3 -0
  6. data/lib/rim/command/command.rb +37 -0
  7. data/lib/rim/command/status.rb +110 -0
  8. data/lib/rim/command/sync.rb +69 -0
  9. data/lib/rim/command/upload.rb +33 -0
  10. data/lib/rim/command_helper.rb +119 -0
  11. data/lib/rim/dirty_check.rb +111 -0
  12. data/lib/rim/file_helper.rb +58 -0
  13. data/lib/rim/file_logger.rb +21 -0
  14. data/lib/rim/git.rb +339 -0
  15. data/lib/rim/manifest/helper.rb +82 -0
  16. data/lib/rim/manifest/json_reader.rb +40 -0
  17. data/lib/rim/manifest/manifest.json +7 -0
  18. data/lib/rim/manifest/model.rb +33 -0
  19. data/lib/rim/manifest/repo_reader.rb +61 -0
  20. data/lib/rim/module_helper.rb +52 -0
  21. data/lib/rim/module_info.rb +30 -0
  22. data/lib/rim/processor.rb +126 -0
  23. data/lib/rim/rev_status.rb +61 -0
  24. data/lib/rim/rim.rb +93 -0
  25. data/lib/rim/rim_exception.rb +15 -0
  26. data/lib/rim/rim_info.rb +129 -0
  27. data/lib/rim/status_builder.rb +219 -0
  28. data/lib/rim/sync_helper.rb +121 -0
  29. data/lib/rim/sync_module_helper.rb +115 -0
  30. data/lib/rim/upload_helper.rb +67 -0
  31. data/lib/rim/upload_module_helper.rb +152 -0
  32. data/lib/rim/version.rb +10 -0
  33. data/test/dirty_check_test.rb +210 -0
  34. data/test/file_helper_test.rb +132 -0
  35. data/test/git_test.rb +49 -0
  36. data/test/manifest_helper_test.rb +29 -0
  37. data/test/manifest_test_dir/manifest.rim +9 -0
  38. data/test/manifest_test_dir/subdir/only_to_keep_folder_in_git.txt +0 -0
  39. data/test/processor_test.rb +32 -0
  40. data/test/rim_info_test.rb +93 -0
  41. data/test/status_builder_test.rb +488 -0
  42. data/test/sync_helper_test.rb +193 -0
  43. data/test/sync_module_helper_test.rb +96 -0
  44. data/test/test_helper.rb +39 -0
  45. data/test/unit_tests.rb +14 -0
  46. data/test/upload_helper_test.rb +338 -0
  47. data/test/upload_module_helper_test.rb +92 -0
  48. metadata +110 -0
@@ -0,0 +1,111 @@
1
+ require 'digest'
2
+ require 'pathname'
3
+ require 'rim/rim_info'
4
+ require 'rim/file_helper'
5
+
6
+ module RIM
7
+
8
+ # Module dirty state checker.
9
+ #
10
+ # Provides means to mark modules as "clean" and check if they become "dirty" later on.
11
+ #
12
+ # Once a module has been marked as being clean, it will become dirty if
13
+ # any of the following is true:
14
+ #
15
+ # * Number of contained files has changed
16
+ # * File names or location have changed
17
+ # * File contents have changed
18
+ # * One of the RIM info attributes listed in ChecksumAttributes has changed
19
+ # * The RIM info file is missing or became invalid
20
+ #
21
+ # Ignored files are not considered by this check.
22
+ #
23
+ class DirtyCheck
24
+
25
+ # raised when there is not enough info for checksum calculation
26
+ class MissingInfoException < Exception
27
+ end
28
+
29
+ # attributes to be included into checksum calculation
30
+ # checksum calculation fails if those attributes are not present
31
+ ChecksumAttributes = [
32
+ :remote_url,
33
+ :revision_sha1
34
+ ]
35
+
36
+ # rim info must exist in dir and must be valid
37
+ # it also must contain attributes listed in ChecksumAttributes
38
+ # otherwise a MissingInfoException is raised
39
+ def self.mark_clean(dir)
40
+ mi = RimInfo.from_dir(dir)
41
+ cs = self.new.calc_checksum(mi, dir)
42
+ raise MissingInfoException unless cs
43
+ mi.checksum = cs
44
+ mi.to_dir(dir)
45
+ end
46
+
47
+ def self.dirty?(dir)
48
+ mi = RimInfo.from_dir(dir)
49
+ # always fails if there is no checksum
50
+ !mi.checksum || mi.checksum != self.new.calc_checksum(mi, dir)
51
+ end
52
+
53
+ # returns nil if checksum can't be calculated due to missing info
54
+ def calc_checksum(mi, dir)
55
+ if check_required_attributes(mi)
56
+ sha1 = Digest::SHA1.new
57
+ # all files and directories within dir
58
+ files = FileHelper.find_matching_files(dir, false, "/**/*", File::FNM_DOTMATCH)
59
+ # Dir.glob with FNM_DOTMATCH might return . and ..
60
+ files.delete(".")
61
+ files.delete("..")
62
+ # ignore the info file itself
63
+ files.delete(RimInfo::InfoFileName)
64
+ # ignores defined by user
65
+ files -= FileHelper.find_matching_files(dir, false, mi.ignores)
66
+ # order of files makes a difference
67
+ # sort to eliminate platform specific glob behavior
68
+ files.sort!
69
+ files.each do |fn|
70
+ update_file(sha1, dir, fn)
71
+ end
72
+ ChecksumAttributes.each do |a|
73
+ sha1.update(mi.send(a))
74
+ end
75
+ sha1.hexdigest
76
+ else
77
+ # can't calc checksum
78
+ nil
79
+ end
80
+ end
81
+
82
+ private
83
+
84
+ def ignored_files(mi, dir)
85
+ find_matching_files(dir, mi.ignores)
86
+ end
87
+
88
+ def update_file(sha1, dir, filename)
89
+ fn = dir+"/"+filename
90
+ if File.directory?(fn)
91
+ if Dir.entries(fn).size > 2 # 2 for . and ..
92
+ # add directory names but only for non-empty directories
93
+ sha1.update(filename)
94
+ end
95
+ else
96
+ # file name
97
+ sha1.update(filename)
98
+ # file contents
99
+ File.open(fn, "rb") do |f|
100
+ sha1.update(f.read.gsub("\r\n", "\n"))
101
+ end
102
+ end
103
+ end
104
+
105
+ def check_required_attributes(mi)
106
+ ChecksumAttributes.all?{|a| mi.send(a)}
107
+ end
108
+
109
+ end
110
+
111
+ end
@@ -0,0 +1,58 @@
1
+ require 'pathname'
2
+ require 'fileutils'
3
+
4
+ module RIM
5
+
6
+ class FileHelper
7
+
8
+ def self.get_relative_path(path, base)
9
+ Pathname.new(get_absolute_path(path)).relative_path_from(Pathname.new(base)).to_s
10
+ end
11
+
12
+ def self.get_absolute_path(path)
13
+ File.expand_path(path)
14
+ end
15
+
16
+ def self.find_matching_files(dir, absolute = true, patterns = "**/*", flags = 0)
17
+ files = []
18
+ dirpath = Pathname.new(dir)
19
+ normalize_patterns(patterns).each do |i|
20
+ Dir.glob(File.join(dir, i), flags) do |f|
21
+ if absolute
22
+ files.push(f)
23
+ else
24
+ files.push(Pathname.new(f).relative_path_from(dirpath).to_s)
25
+ end
26
+ end
27
+ end
28
+ files.sort.uniq
29
+ end
30
+
31
+ def self.remove_empty_dirs(dir, exclude = ".")
32
+ exclude = File.join(File.expand_path(exclude), "") if exclude
33
+ Dir.glob(File.join(dir, "/*/**/")).reverse_each do |d|
34
+ Dir.rmdir d if Dir.entries(d).size == 2 && (!exclude || !exclude.start_with?(d))
35
+ end
36
+ end
37
+
38
+ def self.make_empty_dir(dir)
39
+ FileUtils.rm_rf dir
40
+ FileUtils.mkdir_p(dir)
41
+ end
42
+
43
+ private
44
+
45
+ def self.normalize_patterns(patterns = [])
46
+ if patterns.is_a?(String)
47
+ return patterns.split(",").each do |p|
48
+ p.strip!
49
+ end
50
+ elsif !patterns
51
+ patterns = []
52
+ end
53
+ patterns
54
+ end
55
+
56
+ end
57
+
58
+ end
@@ -0,0 +1,21 @@
1
+ require 'logger'
2
+
3
+ module RIM
4
+
5
+ class FileLogger < Logger
6
+
7
+ def initialize(name, file)
8
+ super(name)
9
+ FileUtils.mkdir_p(File.dirname(file))
10
+ @file_logger = Logger.new(file)
11
+ @file_logger.level = Logger::DEBUG
12
+ end
13
+
14
+ def add(severity, message = nil, progname = nil, &block)
15
+ @file_logger.add(severity, message, progname)
16
+ super(severity, message, progname)
17
+ end
18
+
19
+ end
20
+
21
+ end
@@ -0,0 +1,339 @@
1
+ require 'tmpdir'
2
+ require 'logger'
3
+
4
+ module RIM
5
+
6
+ # raised when there is an error emitted by git call
7
+ class GitException < Exception
8
+ attr_reader :cmd, :exitstatus, :out
9
+ def initialize(cmd, exitstatus, out)
10
+ super("git \"#{cmd}\" => #{exitstatus}\n#{out}")
11
+ @cmd = cmd
12
+ @exitstatus = exitstatus
13
+ @out = out
14
+ end
15
+ end
16
+
17
+ class GitSession
18
+
19
+ attr_reader :execute_dir
20
+
21
+ def initialize(logger, execute_dir, arg = {})
22
+ @execute_dir = execute_dir
23
+ if arg.is_a?(Hash)
24
+ @work_dir = arg.has_key?(:work_dir) ? arg[:work_dir] : ""
25
+ @git_dir = arg.has_key?(:git_dir) ? arg[:git_dir] : ""
26
+ end
27
+ @logger = logger
28
+ end
29
+
30
+ def self.logger=(logger)
31
+ @logger = logger
32
+ end
33
+
34
+ def self.open(execute_dir, options = {})
35
+ log = @logger || Logger.new($stdout)
36
+ self.new(log, execute_dir, options)
37
+ end
38
+
39
+ def self.next_invocation_id
40
+ @invocation_id ||= 0
41
+ @invocation_id += 1
42
+ end
43
+
44
+ class Status
45
+ attr_accessor :lines
46
+
47
+ # X Y Meaning
48
+ # -------------------------------------------------
49
+ # [MD] not updated
50
+ # M [ MD] updated in index
51
+ # A [ MD] added to index
52
+ # D [ M] deleted from index
53
+ # R [ MD] renamed in index
54
+ # C [ MD] copied in index
55
+ # [MARC] index and work tree matches
56
+ # [ MARC] M work tree changed since index
57
+ # [ MARC] D deleted in work tree
58
+ # -------------------------------------------------
59
+ # D D unmerged, both deleted
60
+ # A U unmerged, added by us
61
+ # U D unmerged, deleted by them
62
+ # U A unmerged, added by them
63
+ # D U unmerged, deleted by us
64
+ # A A unmerged, both added
65
+ # U U unmerged, both modified
66
+ # -------------------------------------------------
67
+ # ? ? untracked
68
+ # ! ! ignored
69
+ # -------------------------------------------------
70
+ class Line
71
+ attr_accessor :istat, :wstat, :file, :rename
72
+
73
+ def untracked?
74
+ istat == "?" && wstat == "?"
75
+ end
76
+
77
+ def ignored?
78
+ istat == "!" && wstat == "!"
79
+ end
80
+
81
+ def unmerged?
82
+ istat == "D" && wstat == "D" ||
83
+ istat == "A" && wstat == "A" ||
84
+ istat == "U" ||
85
+ wstat == "U"
86
+ end
87
+
88
+ end
89
+ end
90
+
91
+ # returns the current branch
92
+ def current_branch
93
+ out = execute "git branch"
94
+ out.split("\n").each do |l|
95
+ if l =~ /^\*\s+(\S+)/
96
+ return $1
97
+ end
98
+ end
99
+ nil
100
+ end
101
+
102
+ # check whether branch exists
103
+ def has_branch?(branch)
104
+ execute("git show-ref refs/heads/#{branch}") do |b, e|
105
+ return !e
106
+ end
107
+ end
108
+
109
+ # check whether remote branch exists
110
+ def has_remote_branch?(branch)
111
+ out = execute("git ls-remote --heads")
112
+ out.split("\n").each do |l|
113
+ return true if l.split(/\s+/)[1] == "refs/heads/#{branch}"
114
+ end
115
+ false
116
+ end
117
+
118
+ # check whether remote repository is valid
119
+ def has_valid_remote_repository?()
120
+ execute("git ls-remote") do |b, e|
121
+ return !e
122
+ end
123
+ end
124
+
125
+ # checks whether the first (ancestor) revision is is ancestor of the second (child) revision
126
+ def is_ancestor?(ancestor, child)
127
+ execute("git merge-base --is-ancestor #{ancestor} #{child}") do |b, e|
128
+ return !e
129
+ end
130
+ end
131
+
132
+ # returns the parent commits of rev as SHA-1s
133
+ # returns an empty array if there are no parents (e.g. orphan or initial)
134
+ def parent_revs(rev)
135
+ out = execute "git rev-list -n 1 --parents #{rev} --"
136
+ out.strip.split[1..-1]
137
+ end
138
+
139
+ # returns the SHA-1 representation of rev
140
+ def rev_sha1(rev)
141
+ sha1 = nil
142
+ execute "git rev-list -n 1 #{rev} --" do |out, e|
143
+ sha1 = out.strip if !e
144
+ end
145
+ sha1
146
+ end
147
+
148
+ # returns the SHA-1 representations of the heads of all remote branches
149
+ def remote_branch_revs
150
+ out = execute "git show-ref"
151
+ out.split("\n").collect { |l|
152
+ if l =~ /refs\/remotes\//
153
+ l.split[0]
154
+ else
155
+ nil
156
+ end
157
+ }.compact
158
+ end
159
+
160
+ # all commits reachable from rev which are not ancestors of remote branches
161
+ def all_reachable_non_remote_revs(rev)
162
+ out = execute "git rev-list #{rev} --not --remotes --"
163
+ out.split("\n")
164
+ end
165
+
166
+ # export file contents of rev to dir
167
+ # if +paths+ is given and non-empty, checks out only those parts of the filesystem tree
168
+ # does not remove any files from dir which existed before
169
+ def export_rev(rev, dir, paths=[])
170
+ paths = paths.dup
171
+ loop do
172
+ path_args = ""
173
+ # max command line length on Windows XP and higher is 8191
174
+ # consider the following extra characters which will be added:
175
+ # up to 3 paths in execute, 1 path for tar, max path length 260 = 1040
176
+ # plus some "glue" characters, plus the last path item with 260 max;
177
+ # use 6000 to be on the safe side
178
+ while !paths.empty? && path_args.size < 6000
179
+ path_args << " "
180
+ path_args << paths.shift
181
+ end
182
+ execute "git archive --format tar #{rev} #{path_args} | tar -x -C #{dir}"
183
+ break if paths.empty?
184
+ end
185
+ end
186
+
187
+ # checks out rev to a temporary directory and yields this directory to the given block
188
+ # if +paths+ is given and non-empty, checks out only those parts of the filesystem tree
189
+ # returns the value returned by the block
190
+ def within_exported_rev(rev, paths=[])
191
+ Dir.mktmpdir("rim") do |d|
192
+ c = File.join(d, "content")
193
+ FileUtils.mkdir(c)
194
+ export_rev(rev, c, paths)
195
+ # return contents of yielded block
196
+ # mktmpdir returns value return by our block
197
+ yield c
198
+ FileUtils.rm_rf(c)
199
+ # retry to delete if it hasn't been deleted yet
200
+ # this could be due to Windows keeping the files locked for some time
201
+ # this is especially a problem if the machine is at its limits
202
+ retries = 600
203
+ while File.exist?(c) && retries > 0
204
+ sleep(0.1)
205
+ FileUtils.rm_rf(c)
206
+ retries -= 1
207
+ end
208
+ if File.exist?(c)
209
+ @logger.warn "could not delete temp dir: #{c}"
210
+ end
211
+ end
212
+ end
213
+
214
+ def uncommited_changes?
215
+ # either no status lines are all of them due to ignored items
216
+ !status.lines.all?{|l| l.ignored?}
217
+ end
218
+
219
+ def current_branch_name
220
+ out = execute "git rev-parse --abbrev-ref HEAD"
221
+ out.strip
222
+ end
223
+
224
+ ChangedFile = Struct.new(:path, :kind)
225
+
226
+ # returns a list of all files which changed in commit +rev+
227
+ # together with the kind of the change (:modified, :deleted, :added)
228
+ #
229
+ # if +from_rev+ is given, lists changes between +from_rev and +rev+
230
+ # with one argument only, no changes will be returned for merge commits
231
+ # use the two argument variant for merge commits and decide for one parent
232
+ def changed_files(rev, rev_from=nil)
233
+ out = execute "git diff-tree -r --no-commit-id #{rev_from} #{rev}"
234
+ out.split("\n").collect do |l|
235
+ cols = l.split
236
+ path = cols[5]
237
+ kind = case cols[4]
238
+ when "M"
239
+ :modified
240
+ when "A"
241
+ :added
242
+ when "D"
243
+ :deleted
244
+ else
245
+ nil
246
+ end
247
+ ChangedFile.new(path, kind)
248
+ end
249
+ end
250
+
251
+ # 3 most significant numbers of git version of nil if it can't be determined
252
+ def git_version
253
+ out = execute("git --version")
254
+ if out =~ /^git version (\d+\.\d+\.\d+)/
255
+ $1
256
+ else
257
+ nil
258
+ end
259
+ end
260
+
261
+ def status(dir = nil)
262
+ # -s short format
263
+ # --ignored show ignored
264
+ out = execute "git status -s --ignored #{dir}"
265
+ parse_status(out)
266
+ end
267
+
268
+ def execute(cmd)
269
+ raise "git command has to start with 'git'" unless cmd.start_with? "git "
270
+ cmd.slice!("git ")
271
+ # remove any newlines as they will cause the command line to end prematurely
272
+ cmd.gsub!("\n", "")
273
+ options = ((!@execute_dir || @execute_dir == ".") ? "" : " -C #{@execute_dir}") \
274
+ + (@work_dir.empty? ? "" : " --work-tree=#{File.expand_path(@work_dir)}") \
275
+ + (@git_dir.empty? ? "" : " --git-dir=#{File.expand_path(@git_dir)}")
276
+ cmd = "git#{options} #{cmd} 2>&1"
277
+
278
+ out = `#{cmd}`
279
+ # make sure we don't run into any encoding misinterpretation issues
280
+ out.force_encoding("binary")
281
+
282
+ exitstatus = $?.exitstatus
283
+
284
+ invid = self.class.next_invocation_id.to_s.ljust(4)
285
+ @logger.debug "git##{invid} \"#{cmd}\" => #{exitstatus}"
286
+
287
+ out.split(/\r?\n/).each do |ol|
288
+ @logger.debug "git##{invid} out : #{ol}"
289
+ end
290
+
291
+ exception = exitstatus != 0 ? GitException.new(cmd, exitstatus, out) : nil
292
+
293
+ if block_given?
294
+ yield out, exception
295
+ elsif exception
296
+ raise exception
297
+ end
298
+
299
+ out
300
+ end
301
+
302
+ private
303
+
304
+ def parse_status(out)
305
+ status = Status.new
306
+ status.lines = []
307
+ out.split(/\r?\n/).each do |l|
308
+ sl = Status::Line.new
309
+ sl.istat, sl.wstat = l[0], l[1]
310
+ f1, f2 = l[3..-1].split(" -> ")
311
+ f1 = unquote(f1)
312
+ f2 = unquote(f2) if f2
313
+ sl.file = f1
314
+ sl.rename = f2
315
+ status.lines << sl
316
+ end
317
+ status
318
+ end
319
+
320
+ def unquote(s)
321
+ if s[0] == "\"" && s[-1] == "\""
322
+ s = s[1..-2]
323
+ s.gsub!("\\\\", "\\")
324
+ s.gsub!("\\\"", "\"")
325
+ s.gsub!("\\t", "\t")
326
+ s.gsub!("\\r", "\r")
327
+ s.gsub!("\\n", "\n")
328
+ end
329
+ s
330
+ end
331
+
332
+ end
333
+
334
+ def RIM.git_session(execute_dir, options = {})
335
+ s = GitSession.open(execute_dir, options)
336
+ yield s
337
+ end
338
+
339
+ end