autobuild 1.8.3 → 1.9.0.b1

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 +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}"