autobuild 1.8.3 → 1.9.0.b1

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 +4 -4
  2. data/Manifest.txt +16 -7
  3. data/Rakefile +2 -0
  4. data/lib/autobuild/config.rb +21 -6
  5. data/lib/autobuild/configurable.rb +2 -2
  6. data/lib/autobuild/environment.rb +52 -27
  7. data/lib/autobuild/exceptions.rb +48 -22
  8. data/lib/autobuild/import/archive.rb +37 -16
  9. data/lib/autobuild/import/cvs.rb +26 -28
  10. data/lib/autobuild/import/darcs.rb +9 -8
  11. data/lib/autobuild/import/git.rb +324 -217
  12. data/lib/autobuild/import/hg.rb +6 -9
  13. data/lib/autobuild/import/svn.rb +190 -47
  14. data/lib/autobuild/importer.rb +80 -35
  15. data/lib/autobuild/package.rb +16 -35
  16. data/lib/autobuild/packages/autotools.rb +8 -8
  17. data/lib/autobuild/packages/cmake.rb +18 -12
  18. data/lib/autobuild/packages/genom.rb +1 -1
  19. data/lib/autobuild/packages/gnumake.rb +11 -12
  20. data/lib/autobuild/packages/orogen.rb +1 -1
  21. data/lib/autobuild/packages/ruby.rb +9 -5
  22. data/lib/autobuild/reporting.rb +10 -6
  23. data/lib/autobuild/subcommand.rb +110 -50
  24. data/lib/autobuild/test.rb +104 -0
  25. data/lib/autobuild/timestamps.rb +3 -3
  26. data/lib/autobuild/tools.rb +1 -1
  27. data/lib/autobuild/utility.rb +22 -10
  28. data/lib/autobuild/version.rb +1 -1
  29. data/test/data/gitrepo-with-extra-commit-and-tag.tar +0 -0
  30. data/test/data/gitrepo.tar +0 -0
  31. data/test/data/gitrepo/test +0 -0
  32. data/test/data/gitrepo/test2 +0 -0
  33. data/test/data/gitrepo/test3 +0 -0
  34. data/test/data/svnroot.tar +0 -0
  35. data/test/import/test_cvs.rb +51 -0
  36. data/test/import/test_git.rb +364 -0
  37. data/test/import/test_svn.rb +144 -0
  38. data/test/import/test_tar.rb +76 -0
  39. data/test/suite.rb +7 -0
  40. data/test/test_config.rb +1 -5
  41. data/test/test_environment.rb +88 -0
  42. data/test/test_reporting.rb +2 -14
  43. data/test/test_subcommand.rb +7 -22
  44. metadata +17 -14
  45. data/test/test_import_cvs.rb +0 -59
  46. data/test/test_import_svn.rb +0 -56
  47. data/test/test_import_tar.rb +0 -83
  48. data/test/tools.rb +0 -44
@@ -16,7 +16,6 @@ module Autobuild
16
16
  raise ArgumentError, "no module given"
17
17
  end
18
18
 
19
- @program = Autobuild.tool('cvs')
20
19
  @options_up = cvsopts[:cvsup] || '-dP'
21
20
  @options_up = Array[*@options_up]
22
21
  @options_co = cvsopts[:cvsco] || '-P'
@@ -34,31 +33,32 @@ module Autobuild
34
33
 
35
34
  private
36
35
 
37
- def update(package,only_local=false) # :nodoc:
38
- if only_local
39
- Autobuild.warn "The importer #{self.class} does not support local updates, skipping #{self}"
36
+ def update(package, options = Hash.new) # :nodoc:
37
+ if options[:only_local]
38
+ package.warn "%s: the CVS importer does not support local updates, skipping"
40
39
  return
41
40
  end
42
- Dir.chdir(package.srcdir) do
43
- if !File.exists?("#{package.srcdir}/CVS/Root")
44
- raise ConfigException.new(package, 'import'), "#{package.srcdir} is not a CVS working copy"
45
- end
46
41
 
47
- root = File.open("#{package.srcdir}/CVS/Root") { |io| io.read }.chomp
48
- mod = File.open("#{package.srcdir}/CVS/Repository") { |io| io.read }.chomp
42
+ if !File.exist?("#{package.srcdir}/CVS/Root")
43
+ raise ConfigException.new(package, 'import'), "#{package.srcdir} is not a CVS working copy"
44
+ end
45
+
46
+ root = File.open("#{package.srcdir}/CVS/Root") { |io| io.read }.chomp
47
+ mod = File.open("#{package.srcdir}/CVS/Repository") { |io| io.read }.chomp
49
48
 
50
- # Remove any :ext: in front of the root
51
- root = root.gsub /^:ext:/, ''
52
- expected_root = @root.gsub /^:ext:/, ''
53
- # Remove the optional ':' between the host and the path
54
- root = root.gsub /:/, ''
55
- expected_root = expected_root.gsub /:/, ''
49
+ # Remove any :ext: in front of the root
50
+ root = root.gsub(/^:ext:/, '')
51
+ expected_root = @root.gsub(/^:ext:/, '')
52
+ # Remove the optional ':' between the host and the path
53
+ root = root.gsub(/:/, '')
54
+ expected_root = expected_root.gsub(/:/, '')
56
55
 
57
- if root != expected_root || mod != @module
58
- raise ConfigException.new(package, 'import'), "checkout in #{package.srcdir} is from #{root}:#{mod}, was expecting #{expected_root}:#{@module}"
59
- end
60
- Subprocess.run(package, :import, @program, 'up', *@options_up)
61
- end
56
+ if root != expected_root || mod != @module
57
+ raise ConfigException.new(package, 'import'),
58
+ "checkout in #{package.srcdir} is from #{root}:#{mod}, was expecting #{expected_root}:#{@module}"
59
+ end
60
+ package.run(:import, Autobuild.tool(:cvs), 'up', *@options_up,
61
+ retry: true, working_directory: package.importdir)
62
62
  end
63
63
 
64
64
  def checkout(package) # :nodoc:
@@ -66,21 +66,19 @@ module Autobuild
66
66
  cvsroot = @root
67
67
 
68
68
  FileUtils.mkdir_p(head) if !File.directory?(head)
69
- Dir.chdir(head) do
70
- options = [ @program, '-d', cvsroot, 'co', '-d', tail ] + @options_co + [ modulename ]
71
- Subprocess.run(package, :import, *options)
72
- end
69
+ package.run(:import, Autobuild.tool(:cvs), '-d', cvsroot, 'co', '-d', tail, *@options_co, modulename,
70
+ retry: true, working_directory: head)
73
71
  end
74
72
  end
75
73
 
76
74
  # Returns the CVS importer which will get the +name+ module in repository
77
75
  # +repo+. The allowed values in +options+ are described in CVSImporter.new.
78
- def self.cvs(module_name, options = {}, backward_compatibility = nil)
76
+ def self.cvs(root, options = {}, backward_compatibility = nil)
79
77
  if backward_compatibility
80
78
  backward_compatibility[:module] = options
81
- CVSImporter.new(module_name, backward_compatibility)
79
+ CVSImporter.new(root, backward_compatibility)
82
80
  else
83
- CVSImporter.new(module_name, options)
81
+ CVSImporter.new(root, options)
84
82
  end
85
83
  end
86
84
  end
@@ -22,17 +22,18 @@ module Autobuild
22
22
 
23
23
  private
24
24
 
25
- def update(package,only_local=false) # :nodoc:
26
- if only_local
27
- Autobuild.warn "The importer #{self.class} does not support local updates, skipping #{self}"
25
+ def update(package, options = Hash.new) # :nodoc:
26
+ if options[:only_local]
27
+ package.warn "%s: the darcs importer does not support local updates, skipping"
28
28
  return
29
29
  end
30
30
  if !File.directory?( File.join(package.srcdir, '_darcs') )
31
- raise ConfigException.new(package, 'import'), "#{package.srcdir} is not a Darcs repository"
31
+ raise ConfigException.new(package, 'import'),
32
+ "#{package.srcdir} is not a Darcs repository"
32
33
  end
33
34
 
34
- Subprocess.run(package, :import, @program,
35
- 'pull', '--all', "--repodir=#{package.srcdir}", '--set-scripts-executable', @source, *@pull)
35
+ package.run(:import, @program,
36
+ 'pull', '--all', "--repodir=#{package.srcdir}", '--set-scripts-executable', @source, *@pull, retry: true)
36
37
  end
37
38
 
38
39
  def checkout(package) # :nodoc:
@@ -41,8 +42,8 @@ module Autobuild
41
42
  FileUtils.mkdir_p(basedir)
42
43
  end
43
44
 
44
- Subprocess.run(package, :import, @program,
45
- 'get', '--set-scripts-executable', @source, package.srcdir, *@get)
45
+ package.run(:import, @program,
46
+ 'get', '--set-scripts-executable', @source, package.srcdir, *@get, retry: true)
46
47
  end
47
48
  end
48
49
 
@@ -35,6 +35,46 @@ module Autobuild
35
35
  end
36
36
  end
37
37
 
38
+ # Returns the git version as a string
39
+ #
40
+ # @return [String]
41
+ def self.version
42
+ version = Subprocess.run('git', 'setup', Autobuild.tool(:git), '--version').first
43
+ if version =~ /^git version (\d[\d\.]+)/
44
+ $1.split(".").map { |i| Integer(i) }
45
+ else
46
+ raise ArgumentError, "cannot parse git version string #{version}, was expecting something looking like 'git version 2.1.0'"
47
+ end
48
+ end
49
+
50
+ # Helper method to compare two (partial) versions represented as array
51
+ # of integers
52
+ #
53
+ # @return [Integer] -1 if actual is greater than required,
54
+ # 0 if equal, and 1 if actual is smaller than required
55
+ def self.compare_versions(actual, required)
56
+ if actual.size > required.size
57
+ return -compare_versions(required, actual)
58
+ end
59
+
60
+ actual += [0] * (required.size - actual.size)
61
+ actual.zip(required).each do |v_act, v_req|
62
+ if v_act > v_req then return -1
63
+ elsif v_act < v_req then return 1
64
+ end
65
+ end
66
+ 0
67
+ end
68
+
69
+ # Tests the git version
70
+ #
71
+ # @param [Array<Integer>] version the git version as an array of integer
72
+ # @return [Boolean] true if the git version is at least the requested
73
+ # one, and false otherwise
74
+ def self.at_least_version(*version)
75
+ compare_versions(self.version, version) <= 0
76
+ end
77
+
38
78
  # Creates an importer which tracks the given repository
39
79
  # and branch. +source+ is [repository, branch]
40
80
  #
@@ -47,8 +87,7 @@ module Autobuild
47
87
  @git_dir_cache = Array.new
48
88
 
49
89
  if branch.respond_to?(:to_hash)
50
- options = branch.to_hash
51
- branch = nil
90
+ branch, options = nil, branch.to_hash
52
91
  end
53
92
 
54
93
  if branch
@@ -64,27 +103,19 @@ module Autobuild
64
103
  branch: nil,
65
104
  tag: nil,
66
105
  commit: nil,
106
+ repository_id: nil,
107
+ source_id: nil,
67
108
  with_submodules: false
68
109
  if gitopts[:branch] && branch
69
110
  raise ConfigException, "git branch specified with both the option hash and the explicit parameter"
70
111
  end
71
-
72
- sourceopts, common = Kernel.filter_options common,
73
- :repository_id, :source_id
112
+ gitopts[:branch] ||= branch
74
113
 
75
114
  super(common)
76
115
 
77
- @push_to = gitopts[:push_to]
78
- @with_submodules = gitopts[:with_submodules]
79
- branch = gitopts[:branch] || branch
80
- tag = gitopts[:tag]
81
- commit = gitopts[:commit]
82
-
83
- @branch = branch || 'master'
84
- @tag = tag
85
- @commit = commit
116
+ @with_submodules = gitopts.delete(:with_submodules)
86
117
  @remote_name = 'autobuild'
87
- relocate(repository, sourceopts)
118
+ relocate(repository, gitopts)
88
119
  end
89
120
 
90
121
  # The name of the remote that should be set up by the importer
@@ -159,12 +190,12 @@ module Autobuild
159
190
  # The tag we are pointing to. It is a tag name.
160
191
  #
161
192
  # If set, both branch and commit have to be nil.
162
- attr_reader :tag
193
+ attr_accessor :tag
163
194
 
164
195
  # The commit we are pointing to. It is a commit ID.
165
196
  #
166
197
  # If set, both branch and tag have to be nil.
167
- attr_reader :commit
198
+ attr_accessor :commit
168
199
 
169
200
  # True if it is allowed to merge remote updates automatically. If false
170
201
  # (the default), the import will fail if the updates do not resolve as
@@ -192,7 +223,7 @@ module Autobuild
192
223
  # :bare or :normal, or nil if path is not a git repository.
193
224
  def self.resolve_git_dir(path)
194
225
  dir = File.join(path, '.git')
195
- if !File.exists?(dir)
226
+ if !File.exist?(dir)
196
227
  dir = path
197
228
  end
198
229
 
@@ -213,74 +244,108 @@ module Autobuild
213
244
  end
214
245
 
215
246
  @git_dir_cache = [package.importdir, dir, style]
247
+ self.class.validate_git_dir(package, require_working_copy, dir, style)
248
+ dir
249
+ end
250
+
251
+ def self.git_dir(package, require_working_copy)
252
+ dir, style = Git.resolve_git_dir(package.importdir)
253
+ validate_git_dir(package, require_working_copy, dir, style)
254
+ dir
255
+ end
256
+
257
+ # Validates the return value of {resolve_git_dir}
258
+ #
259
+ # @param [Package] package the package we are working on
260
+ # @param [Boolean] require_working_copy if false, a bare repository will
261
+ # be considered as valid, otherwise not
262
+ # @param [String,nil] dir the path to the repository's git directory, or nil
263
+ # if the target is not a valid repository (see the documentation of
264
+ # {resolve_git_dir}
265
+ # @param [Symbol,nil] style either :normal for a git checkout with
266
+ # working copy, :bare for a bare repository or nil if {resolve_git_dir}
267
+ # did not detect a git repository
268
+ #
269
+ # @return [void]
270
+ # @raise ConfigException if dir/style are nil, or if
271
+ # require_working_copy is true and style is :bare
272
+ def self.validate_git_dir(package, require_working_copy, dir, style)
216
273
  if !style
217
- raise ConfigException.new(package, 'import'), "while importing #{package.name}, #{package.importdir} does not point to a git repository"
274
+ raise ConfigException.new(package, 'import', retry: false),
275
+ "while importing #{package.name}, #{package.importdir} does not point to a git repository"
218
276
  elsif require_working_copy && (style == :bare)
219
- raise ConfigException.new(package, 'import'), "while importing #{package.name}, #{package.importdir} points to a bare git repository but a working copy was required"
220
- else
221
- return dir
277
+ raise ConfigException.new(package, 'import', retry: false),
278
+ "while importing #{package.name}, #{package.importdir} points to a bare git repository but a working copy was required"
222
279
  end
223
280
  end
224
281
 
225
282
  # Computes the merge status for this package between two existing tags
226
283
  # Raises if a tag is unknown
227
284
  def delta_between_tags(package, from_tag, to_tag)
228
- Dir.chdir(package.importdir) do
229
- pkg_tags = tags(package)
230
- if not pkg_tags.has_key?(from_tag)
231
- raise ArgumentError, "tag '#{from_tag}' is unknown to #{package.name} -- known tags are: #{pkg_tags.keys}"
232
- end
233
- if not pkg_tags.has_key?(to_tag)
234
- raise ArgumentError, "tag '#{to_tag}' is unknown to #{package.name} -- known tags are: #{pkg_tags.keys}"
235
- end
285
+ pkg_tags = tags(package)
286
+ if not pkg_tags.has_key?(from_tag)
287
+ raise ArgumentError, "tag '#{from_tag}' is unknown to #{package.name} -- known tags are: #{pkg_tags.keys}"
288
+ end
289
+ if not pkg_tags.has_key?(to_tag)
290
+ raise ArgumentError, "tag '#{to_tag}' is unknown to #{package.name} -- known tags are: #{pkg_tags.keys}"
291
+ end
236
292
 
237
- from_commit = pkg_tags[from_tag]
238
- to_commit = pkg_tags[to_tag]
293
+ from_commit = pkg_tags[from_tag]
294
+ to_commit = pkg_tags[to_tag]
239
295
 
240
- merge_status(package, to_commit, from_commit)
241
- end
296
+ merge_status(package, to_commit, from_commit)
242
297
  end
243
298
 
244
299
  # Retrieve the tags of this packages as a hash mapping to the commit id
245
300
  def tags(package)
246
- Dir.chdir(package.importdir) do
247
- `git fetch --tags -q`
248
- tag_list = `git show-ref --tags`
249
- tag_list = tag_list.split("\n")
250
- @tags ||= Hash.new
251
- tag_list.each do |entry|
252
- commit_to_tag = entry.split(" ")
253
- @tags[commit_to_tag[1].sub("refs/tags/","")] = commit_to_tag[0]
254
- end
255
- @tags
301
+ run_git_bare(package, 'fetch', '--tags')
302
+ tag_list = run_git_bare(package, 'show-ref', '--tags').map(&:strip)
303
+ tags = Hash.new
304
+ tag_list.each do |entry|
305
+ commit_to_tag = entry.split(" ")
306
+ tags[commit_to_tag[1].sub("refs/tags/","")] = commit_to_tag[0]
307
+ end
308
+ tags
309
+ end
310
+
311
+ def run_git(package, *args)
312
+ self.class.run_git(package, *args)
313
+ end
314
+
315
+ def self.run_git(package, *args)
316
+ options = Hash.new
317
+ if args.last.kind_of?(Hash)
318
+ options = args.pop
256
319
  end
320
+
321
+ working_directory = File.dirname(git_dir(package, true))
322
+ package.run(:import, Autobuild.tool(:git), *args,
323
+ Hash[working_directory: working_directory].merge(options))
324
+ end
325
+
326
+ def run_git_bare(package, *args)
327
+ self.class.run_git_bare(package, *args)
257
328
  end
258
329
 
259
- def update_cache(package, cache_dir, phase)
260
- remote_name = package.name.gsub(/[^\w]/, '_')
261
- Subprocess.run(*git, "remote.#{remote_name}.url", repository)
262
- Subprocess.run(*git, "remote.#{remote_name}.fetch", "+refs/heads/*:refs/remotes/#{remote_name}/*")
263
- Subprocess.run(*git, 'fetch', '--tags', remote_name)
330
+ def self.run_git_bare(package, *args)
331
+ package.run(:import, Autobuild.tool(:git), '--git-dir', git_dir(package, false), *args)
264
332
  end
265
333
 
266
334
  # Updates the git repository's configuration for the target remote
267
- def update_remotes_configuration(package, phase)
268
- git = [package, phase, Autobuild.tool(:git), '--git-dir', git_dir(package, false), 'config', '--replace-all']
269
- Subprocess.run(*git, "remote.#{remote_name}.url", repository)
270
- if push_to
271
- Subprocess.run(*git, "remote.#{remote_name}.pushurl", push_to)
272
- end
273
- Subprocess.run(*git, "remote.#{remote_name}.fetch", "+refs/heads/*:refs/remotes/#{remote_name}/*")
335
+ def update_remotes_configuration(package)
336
+ run_git_bare(package, 'config', '--replace-all', "remote.#{remote_name}.url", repository)
337
+ run_git_bare(package, 'config', '--replace-all', "remote.#{remote_name}.pushurl", push_to || repository)
338
+ run_git_bare(package, 'config', '--replace-all', "remote.#{remote_name}.fetch", "+refs/heads/*:refs/remotes/#{remote_name}/*")
274
339
 
275
340
  if remote_branch && local_branch
276
- Subprocess.run(*git, "remote.#{remote_name}.push", "refs/heads/#{local_branch}:refs/heads/#{remote_branch}")
341
+ run_git_bare(package, 'config', '--replace-all', "remote.#{remote_name}.push", "refs/heads/#{local_branch}:refs/heads/#{remote_branch}")
277
342
  else
278
- Subprocess.run(*git, "remote.#{remote_name}.push", "refs/heads/*:refs/heads/*")
343
+ run_git_bare(package, 'config', '--replace-all', "remote.#{remote_name}.push", "refs/heads/*:refs/heads/*")
279
344
  end
280
345
 
281
346
  if local_branch
282
- Subprocess.run(*git, "branch.#{local_branch}.remote", remote_name)
283
- Subprocess.run(*git, "branch.#{local_branch}.merge", "refs/heads/#{local_branch}")
347
+ run_git_bare(package, 'config', '--replace-all', "branch.#{local_branch}.remote", remote_name)
348
+ run_git_bare(package, 'config', '--replace-all', "branch.#{local_branch}.merge", "refs/heads/#{local_branch}")
284
349
  end
285
350
  end
286
351
 
@@ -290,7 +355,6 @@ module Autobuild
290
355
  def fetch_remote(package)
291
356
  validate_importdir(package)
292
357
  git_dir = git_dir(package, false)
293
- git = [package, :import, Autobuild.tool('git'), '--git-dir', git_dir]
294
358
 
295
359
  # If we are checking out a specific commit, we don't know which
296
360
  # branch to refer to in git fetch. So, we have to set up the
@@ -303,9 +367,9 @@ module Autobuild
303
367
  # configuration parameters only if the repository and branch are
304
368
  # OK (i.e. we keep old working configuration instead)
305
369
  refspec = [branch || tag].compact
306
- Subprocess.run(*git, 'fetch', '--tags', repository, *refspec)
370
+ run_git_bare(package, 'fetch', '--tags', repository, *refspec, retry: true)
307
371
 
308
- update_remotes_configuration(package, :import)
372
+ update_remotes_configuration(package)
309
373
 
310
374
  # Now get the actual commit ID from the FETCH_HEAD file, and
311
375
  # return it
@@ -319,85 +383,117 @@ module Autobuild
319
383
 
320
384
  # Update the remote tag if needs be
321
385
  if branch && commit_id
322
- Subprocess.run(*git, 'update-ref',
323
- "-m", "updated by autobuild", "refs/remotes/#{remote_name}/#{remote_branch}", commit_id)
386
+ run_git_bare(package, 'update-ref', "-m", "updated by autobuild", "refs/remotes/#{remote_name}/#{remote_branch}", commit_id)
324
387
  end
325
388
 
326
389
  commit_id
327
390
  end
328
391
 
329
392
  def self.has_uncommitted_changes?(package, with_untracked_files = false)
330
- Dir.chdir(package.importdir) do
331
- status = `git status --porcelain`.split("\n").map(&:strip)
332
- if with_untracked_files
333
- !status.empty?
334
- else
335
- status.any? { |l| l[0, 2] !~ /^\?\?|^ / }
393
+ status = run_git(package, 'status', '--porcelain').map(&:strip)
394
+ if with_untracked_files
395
+ !status.empty?
396
+ else
397
+ status.any? { |l| l[0, 2] !~ /^\?\?|^ / }
398
+ end
399
+ end
400
+
401
+ # Returns the commit ID of what we should consider being the remote
402
+ # commit
403
+ #
404
+ # @param [Package] package
405
+ # @param [Boolean] only_local if true, no remote access should be
406
+ # performed, in which case the current known state of the remote will be
407
+ # used. If false, we access the remote repository to fetch the actual
408
+ # commit ID
409
+ # @return [String] the commit ID as a string
410
+ def current_remote_commit(package, only_local = false)
411
+ if only_local
412
+ begin
413
+ run_git_bare(package, 'show-ref', '-s', "refs/remotes/#{remote_name}/#{remote_branch}").first.strip
414
+ rescue SubcommandFailed
415
+ raise PackageException.new(package, "import"), "cannot resolve remote HEAD #{remote_name}/#{remote_branch}"
416
+ end
417
+ else
418
+ begin fetch_remote(package)
419
+ rescue Exception => e
420
+ return fallback(e, package, :status, package, only_local)
336
421
  end
337
422
  end
338
423
  end
339
424
 
425
+
340
426
  # Returns a Importer::Status object that represents the status of this
341
427
  # package w.r.t. the root repository
342
428
  def status(package, only_local = false)
343
- Dir.chdir(package.importdir) do
344
- validate_importdir(package)
345
- remote_commit = nil
346
- if only_local
347
- remote_commit = `git show-ref -s refs/remotes/#{remote_name}/#{remote_branch}`.chomp
348
- else
349
- remote_commit =
350
- begin fetch_remote(package)
351
- rescue Exception => e
352
- return fallback(e, package, :status, package, only_local)
353
- end
354
-
355
- if !remote_commit
356
- return
357
- end
358
- end
429
+ validate_importdir(package)
430
+ remote_commit = current_remote_commit(package, only_local)
431
+ status = merge_status(package, remote_commit)
432
+ status.uncommitted_code = self.class.has_uncommitted_changes?(package)
433
+ status
434
+ end
359
435
 
360
- status = merge_status(package, remote_commit)
361
- status.uncommitted_code = self.class.has_uncommitted_changes?(package)
362
- status
436
+ def has_commit?(package, commit_id)
437
+ run_git_bare(package, 'rev-parse', '-q', '--verify', "#{commit_id}^{commit}")
438
+ true
439
+ rescue SubcommandFailed => e
440
+ if e.status == 1
441
+ false
442
+ else raise
363
443
  end
444
+ end
364
445
 
446
+ def has_branch?(package, branch_name)
447
+ run_git_bare(package, 'show-ref', '-q', '--verify', "refs/heads/#{branch_name}")
448
+ true
449
+ rescue SubcommandFailed => e
450
+ if e.status == 1
451
+ false
452
+ else raise
453
+ end
365
454
  end
366
455
 
367
- def has_local_branch?
368
- `git show-ref -q --verify refs/heads/#{local_branch}`
369
- $?.exitstatus == 0
456
+ def has_local_branch?(package)
457
+ has_branch?(package, local_branch)
370
458
  end
371
459
 
372
- def detached_head?
373
- `git symbolic-ref HEAD -q`
374
- return ($?.exitstatus != 0)
460
+ def detached_head?(package)
461
+ current_branch(package).nil?
375
462
  end
376
463
 
377
- def current_branch
378
- current_branch = `git symbolic-ref HEAD -q`.chomp
379
- if $?.exitstatus != 0
380
- # HEAD cannot be resolved as a symbol. We are probably on a
381
- # detached HEAD
464
+ # Returns the branch HEAD is pointing to
465
+ #
466
+ # @return [String,nil] the full ref HEAD is pointing to (i.e.
467
+ # refs/heads/master), or nil if HEAD is detached
468
+ # @raises SubcommandFailed if git failed
469
+ def current_branch(package)
470
+ run_git_bare(package, 'symbolic-ref', 'HEAD', '-q').first.strip
471
+ rescue SubcommandFailed => e
472
+ if e.status == 1
382
473
  return
474
+ else raise
383
475
  end
384
- return current_branch
385
476
  end
386
477
 
387
478
  # Checks if the current branch is the target branch. Expects that the
388
479
  # current directory is the package's directory
389
- def on_target_branch?
390
- if current_branch = self.current_branch
480
+ def on_local_branch?(package)
481
+ if current_branch = self.current_branch(package)
391
482
  current_branch == "refs/heads/#{local_branch}"
392
483
  end
393
484
  end
394
485
 
486
+ # @deprecated use on_local_branch? instead
487
+ def on_target_branch?(package)
488
+ on_local_branch?(package)
489
+ end
490
+
395
491
  class Status < Importer::Status
396
492
  attr_reader :fetch_commit
397
493
  attr_reader :head_commit
398
494
  attr_reader :common_commit
399
495
 
400
- def initialize(status, remote_commit, local_commit, common_commit)
496
+ def initialize(package, status, remote_commit, local_commit, common_commit)
401
497
  super()
402
498
  @status = status
403
499
  @fetch_commit = fetch_commit
@@ -405,10 +501,10 @@ module Autobuild
405
501
  @common_commit = common_commit
406
502
 
407
503
  if remote_commit != common_commit
408
- @remote_commits = log(common_commit, remote_commit)
504
+ @remote_commits = log(package, common_commit, remote_commit)
409
505
  end
410
506
  if local_commit != common_commit
411
- @local_commits = log(common_commit, local_commit)
507
+ @local_commits = log(package, common_commit, local_commit)
412
508
  end
413
509
  end
414
510
 
@@ -416,26 +512,42 @@ module Autobuild
416
512
  status == Status::NEEDS_MERGE || status == Status::SIMPLE_UPDATE
417
513
  end
418
514
 
419
- def log(from, to)
420
- log = `git log --encoding=UTF-8 --pretty=format:"%h %cr %cn %s" #{from}..#{to}`.chomp
421
-
422
- if log.respond_to?(:encode)
423
- log = log.encode
424
- end
425
-
426
- encodings = ['UTF-8', 'iso8859-1']
427
- begin
428
- log.split("\n")
429
- rescue
430
- if encodings.empty?
431
- return "[some log messages have invalid characters, cannot display/parse them]"
432
- end
433
- log.force_encoding(encodings.pop)
434
- retry
515
+ def log(package, from, to)
516
+ log = package.importer.run_git_bare(package, 'log', '--encoding=UTF-8', "--pretty=format:%h %cr %cn %s", "#{from}..#{to}")
517
+ log.map do |line|
518
+ line.strip.encode
435
519
  end
436
520
  end
437
521
  end
438
522
 
523
+ def rev_parse(package, name)
524
+ run_git_bare(package, 'rev-parse', name).first
525
+ rescue Autobuild::SubcommandFailed
526
+ raise PackageException.new(package, 'import'), "failed to resolve #{name}. Are you sure this commit, branch or tag exists ?"
527
+ end
528
+
529
+ def show(package, commit, path)
530
+ run_git_bare(package, 'show', "#{commit}:#{path}").join("\n")
531
+ rescue Autobuild::SubcommandFailed
532
+ raise PackageException.new(package, 'import'), "failed to either resolve commit #{commit} or file #{path}"
533
+ end
534
+
535
+ # Tests whether a commit is already present in a given history
536
+ #
537
+ # @param [Package] the package we are working on
538
+ # @param [String] commit the commit ID we want to verify the presence of
539
+ # @param [String] reference the reference commit. The method tests that
540
+ # 'commit' is present in the history of 'reference'
541
+ #
542
+ # @return [Boolean]
543
+ def commit_present_in?(package, commit, reference)
544
+ merge_base = run_git_bare(package, 'merge-base', commit, reference).first
545
+ merge_base == commit
546
+
547
+ rescue Exception
548
+ raise PackageException.new(package, 'import'), "failed to find the merge-base between #{commit} and #{reference}. Are you sure these commits exist ?"
549
+ end
550
+
439
551
  # Computes the update status to update a branch whose tip is at
440
552
  # reference_commit (which can be a symbolic reference) using the
441
553
  # fetch_commit commit
@@ -446,16 +558,15 @@ module Autobuild
446
558
  # git merge fetch_commit
447
559
  #
448
560
  def merge_status(package, fetch_commit, reference_commit = "HEAD")
449
- common_commit = `git merge-base #{reference_commit} #{fetch_commit}`.chomp
450
- if $?.exitstatus != 0
561
+ begin
562
+ common_commit = run_git_bare(package, 'merge-base', reference_commit, fetch_commit).first.strip
563
+ rescue Exception
451
564
  raise PackageException.new(package, 'import'), "failed to find the merge-base between #{reference_commit} and #{fetch_commit}. Are you sure these commits exist ?"
452
565
  end
453
- head_commit = `git rev-parse #{reference_commit}`.chomp
454
- if $?.exitstatus != 0
455
- raise PackageException.new(package, 'import'), "failed to resolve #{reference_commit}. Are you sure this commit, branch or tag exists ?"
456
- end
566
+ remote_commit = rev_parse(package, fetch_commit)
567
+ head_commit = rev_parse(package, reference_commit)
457
568
 
458
- status = if common_commit != fetch_commit
569
+ status = if common_commit != remote_commit
459
570
  if common_commit == head_commit
460
571
  Status::SIMPLE_UPDATE
461
572
  else
@@ -469,7 +580,7 @@ module Autobuild
469
580
  end
470
581
  end
471
582
 
472
- Status.new(status, fetch_commit, head_commit, common_commit)
583
+ Status.new(package, status, fetch_commit, head_commit, common_commit)
473
584
  end
474
585
 
475
586
  # Updates the git alternates file in the already checked out package to
@@ -506,78 +617,96 @@ module Autobuild
506
617
  end
507
618
  end
508
619
 
509
- def update(package,only_local = false)
620
+ def commit_pinning(package, target_commit, fetch_commit)
621
+ current_head = rev_parse(package, 'HEAD')
622
+
623
+ # Check whether the current HEAD is present on the remote
624
+ # repository. We'll refuse resetting if there are uncommitted
625
+ # changes
626
+ head_to_remote = merge_status(package, fetch_commit, current_head)
627
+ status_to_remote = head_to_remote.status
628
+ if status_to_remote == Status::ADVANCED || status_to_remote == Status::NEEDS_MERGE
629
+ raise ImporterCannotReset.new(package, 'import'), "branch #{local_branch} of #{package.name} contains commits that do not seem to be present on the branch #{remote_branch} of the remote repository. I can't go on as it could make you loose some stuff. Update the remote branch in your overrides, push your changes or reset to the remote commit manually before trying again"
630
+ end
631
+
632
+ head_to_target = merge_status(package, target_commit, current_head)
633
+ status_to_target = head_to_target.status
634
+ if status_to_target == Status::UP_TO_DATE
635
+ return
636
+ end
637
+
638
+ package.message " %%s: resetting branch %s to %s" % [local_branch, target_commit.to_s]
639
+ # I don't use a reset --hard here as it would add even more
640
+ # restrictions on when we can do the operation (as we would refuse
641
+ # doing it if there are local changes). The checkout creates a
642
+ # detached HEAD, but makes sure that applying uncommitted changes is
643
+ # fine (it would abort otherwise). The rest then updates HEAD and
644
+ # the local_branch ref to match the required target commit
645
+ resolved_target_commit = rev_parse(package, "#{target_commit}^{commit}")
646
+ begin
647
+ run_git(package, 'checkout', target_commit)
648
+ run_git(package, 'update-ref', "refs/heads/#{local_branch}", resolved_target_commit)
649
+ run_git(package, 'symbolic-ref', "HEAD", "refs/heads/#{local_branch}")
650
+ rescue ::Exception
651
+ run_git(package, 'symbolic-ref', "HEAD", target_commit)
652
+ run_git(package, 'update-ref', "refs/heads/#{local_branch}", current_head)
653
+ run_git(package, 'checkout', local_branch)
654
+ raise
655
+ end
656
+ end
657
+
658
+ # @option (see Package#update)
659
+ def update(package, options = Hash.new)
510
660
  validate_importdir(package)
661
+
511
662
  # This is really really a hack to workaround how broken the
512
663
  # importdir thing is
513
664
  if package.importdir == package.srcdir
514
665
  update_alternates(package)
515
666
  end
516
- Dir.chdir(package.importdir) do
517
- #Checking if we should only merge our repro to remotes/HEAD without updateing from the remote side...
518
- if !only_local
519
- fetch_commit = fetch_remote(package)
520
- end
521
667
 
522
- # If we are tracking a commit/tag, just check it out and return
523
- if commit || tag
524
- target_commit = (commit || tag)
525
- status_to_head = merge_status(package, target_commit, "HEAD")
526
- if status_to_head.status == Status::UP_TO_DATE
527
- # Check if by any chance we could switch back to a
528
- # proper branch instead of having a detached HEAD
529
- if detached_head?
530
- status_to_remote = merge_status(package, target_commit, fetch_commit)
531
- if status_to_remote.status != Status::UP_TO_DATE
532
- package.message " the package is on a detached HEAD because of commit pinning"
533
- return
534
- end
535
- else
536
- return
537
- end
538
- elsif status_to_head.status != Status::SIMPLE_UPDATE
539
- raise PackageException.new(package, 'import'), "checking out the specified commit #{target_commit} would be a non-simple operation (i.e. the current state of the repository is not a linear relationship with the specified commit), do it manually"
540
- end
541
-
542
- status_to_remote = merge_status(package, target_commit, fetch_commit)
543
- if status_to_remote.status != Status::UP_TO_DATE
544
- # Try very hard to avoid creating a detached HEAD
545
- if local_branch
546
- status_to_branch = merge_status(package, target_commit, local_branch)
547
- if status_to_branch.status == Status::UP_TO_DATE # Checkout the branch
548
- package.message " checking out specific commit %s for %s. It will checkout branch %s." % [target_commit.to_s, package.name, local_branch]
549
- Subprocess.run(package, :import, Autobuild.tool('git'), 'checkout', local_branch)
550
- return
551
- end
552
- end
553
- package.message " checking out specific commit %s for %s. This will create a detached HEAD." % [target_commit.to_s, package.name]
554
- Subprocess.run(package, :import, Autobuild.tool('git'), 'checkout', target_commit)
668
+ # Check whether we are already at the requested state
669
+ pinned_state = (commit || tag)
670
+ if pinned_state && has_commit?(package, pinned_state)
671
+ pinned_state = rev_parse(package, pinned_state)
672
+ current_head = rev_parse(package, 'HEAD')
673
+ if options[:reset]
674
+ if current_head == pinned_state
555
675
  return
556
676
  end
557
- end
558
-
559
- if !fetch_commit
677
+ elsif commit_present_in?(package, pinned_state, current_head)
560
678
  return
561
679
  end
680
+ end
681
+ fetch_commit = current_remote_commit(package, options[:only_local])
562
682
 
563
- if !on_target_branch?
564
- # Check if the target branch already exists. If it is the
565
- # case, check it out. Otherwise, create it.
566
- if system("git", "show-ref", "--verify", "--quiet", "refs/heads/#{local_branch}")
567
- package.message " switching branch of %s to %s" % [package.name, local_branch]
568
- Subprocess.run(package, :import, Autobuild.tool('git'), 'checkout', local_branch)
569
- else
570
- package.message " checking out branch %s for %s" % [local_branch, package.name]
571
- Subprocess.run(package, :import, Autobuild.tool('git'), 'checkout', '-b', local_branch, "FETCH_HEAD")
572
- end
683
+ target_commit =
684
+ if commit then commit
685
+ elsif tag then "refs/tags/#{tag}"
686
+ else fetch_commit
573
687
  end
574
688
 
575
- status = merge_status(package, fetch_commit)
689
+ # If we are tracking a commit/tag, just check it out and return
690
+ if !has_local_branch?(package)
691
+ package.message "%%s: checking out branch %s" % [local_branch]
692
+ run_git(package, 'checkout', '-b', local_branch, target_commit)
693
+ return
694
+ end
695
+
696
+ if !on_target_branch?(package)
697
+ package.message "%%s: switching to branch %s" % [local_branch]
698
+ run_git(package, 'checkout', local_branch)
699
+ end
700
+
701
+ if options[:reset]
702
+ commit_pinning(package, target_commit, fetch_commit)
703
+ else
704
+ status = merge_status(package, target_commit)
576
705
  if status.needs_update?
577
706
  if !merge? && status.status == Status::NEEDS_MERGE
578
707
  raise PackageException.new(package, 'import'), "the local branch '#{local_branch}' and the remote branch #{branch} of #{package.name} have diverged, and I therefore refuse to update automatically. Go into #{package.importdir} and either reset the local branch or merge the remote changes"
579
708
  end
580
- Subprocess.run(package, :import, Autobuild.tool('git'), 'merge', fetch_commit)
709
+ run_git(package, 'merge', target_commit)
581
710
  end
582
711
  end
583
712
  end
@@ -600,49 +729,27 @@ module Autobuild
600
729
  FileUtils.mkdir_p base_dir
601
730
  end
602
731
 
603
- clone_options = ['-o', remote_name]
604
-
732
+ clone_options = Array.new
605
733
  if with_submodules?
606
734
  clone_options << '--recurse-submodules'
607
735
  end
608
736
  each_alternate_path(package) do |path|
609
737
  clone_options << '--reference' << path
610
738
  end
611
- Subprocess.run(package, :import,
612
- Autobuild.tool('git'), 'clone', *clone_options, repository, package.importdir)
613
-
614
- Dir.chdir(package.importdir) do
615
- if push_to
616
- Subprocess.run(package, :import, Autobuild.tool('git'), 'config',
617
- "--replace-all", "remote.#{remote_name}.pushurl", push_to)
618
- end
619
- if local_branch && remote_branch
620
- Subprocess.run(package, :import, Autobuild.tool('git'), 'config',
621
- "--replace-all", "remote.#{remote_name}.push", "refs/heads/#{local_branch}:refs/heads/#{remote_branch}")
622
- end
739
+ package.run(:import,
740
+ Autobuild.tool('git'), 'clone', '-o', remote_name, *clone_options, repository, package.importdir, retry: true)
623
741
 
624
- # If we are tracking a commit/tag, just check it out
625
- if commit || tag
626
- status = merge_status(package, commit || tag)
627
- if status.status != Status::UP_TO_DATE
628
- package.message " checking out specific commit for %s. This will create a detached HEAD." % [package.name]
629
- Subprocess.run(package, :import, Autobuild.tool('git'), 'checkout', commit || tag)
630
- end
631
- else
632
- current_branch = `git symbolic-ref HEAD`.chomp
633
- if current_branch == "refs/heads/#{local_branch}"
634
- Subprocess.run(package, :import, Autobuild.tool('git'),
635
- 'reset', '--hard', "#{remote_name}/#{branch}")
636
- else
637
- Subprocess.run(package, :import, Autobuild.tool('git'),
638
- 'checkout', '-b', local_branch, "#{remote_name}/#{branch}")
639
- end
640
- end
641
- end
742
+ update_remotes_configuration(package)
743
+ update(package, only_local: true)
642
744
  end
643
745
 
644
746
  # Changes the repository this importer is pointing to
645
747
  def relocate(repository, options = Hash.new)
748
+ @push_to = options[:push_to] || @push_to
749
+ @branch = options[:branch] || @branch || 'master'
750
+ @tag = options[:tag] || @tag
751
+ @commit = options[:commit] || @commit
752
+
646
753
  @repository = repository.to_str
647
754
  @repository_id = options[:repository_id] ||
648
755
  "git:#{@repository}"