esr-rim 1.1.5

Sign up to get free protection for your applications and to get access to all the features.
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