git_compound 0.0.9

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 (63) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +10 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +8 -0
  5. data/Compoundfile +4 -0
  6. data/Gemfile +3 -0
  7. data/LICENSE +21 -0
  8. data/README.md +291 -0
  9. data/Rakefile +13 -0
  10. data/bin/console +7 -0
  11. data/bin/setup +5 -0
  12. data/exe/gitcompound +5 -0
  13. data/git_compound.gemspec +33 -0
  14. data/lib/git_compound/builder.rb +93 -0
  15. data/lib/git_compound/command/options.rb +55 -0
  16. data/lib/git_compound/command.rb +113 -0
  17. data/lib/git_compound/component/destination.rb +45 -0
  18. data/lib/git_compound/component/source.rb +53 -0
  19. data/lib/git_compound/component/version/branch.rb +30 -0
  20. data/lib/git_compound/component/version/gem_version.rb +45 -0
  21. data/lib/git_compound/component/version/sha.rb +33 -0
  22. data/lib/git_compound/component/version/tag.rb +30 -0
  23. data/lib/git_compound/component/version/version_strategy.rb +45 -0
  24. data/lib/git_compound/component.rb +78 -0
  25. data/lib/git_compound/dsl/component_dsl.rb +60 -0
  26. data/lib/git_compound/dsl/manifest_dsl.rb +27 -0
  27. data/lib/git_compound/exceptions.rb +16 -0
  28. data/lib/git_compound/lock.rb +64 -0
  29. data/lib/git_compound/logger/colors.rb +123 -0
  30. data/lib/git_compound/logger/core_ext/string.rb +5 -0
  31. data/lib/git_compound/logger.rb +43 -0
  32. data/lib/git_compound/manifest.rb +39 -0
  33. data/lib/git_compound/node.rb +17 -0
  34. data/lib/git_compound/repository/git_command.rb +33 -0
  35. data/lib/git_compound/repository/git_repository.rb +79 -0
  36. data/lib/git_compound/repository/git_version.rb +43 -0
  37. data/lib/git_compound/repository/remote_file/git_archive_strategy.rb +30 -0
  38. data/lib/git_compound/repository/remote_file/github_strategy.rb +54 -0
  39. data/lib/git_compound/repository/remote_file/remote_file_strategy.rb +27 -0
  40. data/lib/git_compound/repository/remote_file.rb +34 -0
  41. data/lib/git_compound/repository/repository_local.rb +81 -0
  42. data/lib/git_compound/repository/repository_remote.rb +12 -0
  43. data/lib/git_compound/repository.rb +19 -0
  44. data/lib/git_compound/task/task.rb +28 -0
  45. data/lib/git_compound/task/task_all.rb +27 -0
  46. data/lib/git_compound/task/task_each.rb +18 -0
  47. data/lib/git_compound/task/task_single.rb +21 -0
  48. data/lib/git_compound/task.rb +22 -0
  49. data/lib/git_compound/version.rb +5 -0
  50. data/lib/git_compound/worker/circular_dependency_checker.rb +31 -0
  51. data/lib/git_compound/worker/component_builder.rb +30 -0
  52. data/lib/git_compound/worker/component_replacer.rb +25 -0
  53. data/lib/git_compound/worker/component_update_dispatcher.rb +64 -0
  54. data/lib/git_compound/worker/component_updater.rb +24 -0
  55. data/lib/git_compound/worker/components_collector.rb +20 -0
  56. data/lib/git_compound/worker/conflicting_dependency_checker.rb +31 -0
  57. data/lib/git_compound/worker/local_changes_guard.rb +47 -0
  58. data/lib/git_compound/worker/name_constraint_checker.rb +20 -0
  59. data/lib/git_compound/worker/pretty_print.rb +18 -0
  60. data/lib/git_compound/worker/task_runner.rb +12 -0
  61. data/lib/git_compound/worker/worker.rb +20 -0
  62. data/lib/git_compound.rb +112 -0
  63. metadata +193 -0
@@ -0,0 +1,39 @@
1
+ require 'digest'
2
+
3
+ module GitCompound
4
+ # Manifest
5
+ #
6
+ class Manifest < Node
7
+ attr_accessor :name, :components, :tasks
8
+
9
+ FILENAMES = %w(Compoundfile .gitcompound)
10
+
11
+ def initialize(contents, parent = nil)
12
+ @contents = contents
13
+ @parent = parent
14
+ @name = ''
15
+ @components = {}
16
+ @tasks = {}
17
+ DSL::ManifestDSL.new(self, contents) if contents
18
+ end
19
+
20
+ def process(*workers)
21
+ workers.each { |worker| worker.visit_manifest(self) }
22
+ components.each_value { |component| component.process(*workers) }
23
+ tasks.each_value { |task| workers.each { |worker| worker.visit_task(task) } }
24
+ end
25
+
26
+ def ==(other)
27
+ return false unless other.instance_of? Manifest
28
+ md5sum == other.md5sum
29
+ end
30
+
31
+ def exists?
32
+ @contents ? true : false
33
+ end
34
+
35
+ def md5sum
36
+ Digest::MD5.hexdigest(@contents) if exists?
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,17 @@
1
+ module GitCompound
2
+ # Abstract node class
3
+ #
4
+ class Node
5
+ attr_reader :parent
6
+
7
+ def process(*_workers)
8
+ raise NotImplementedError
9
+ end
10
+
11
+ def ancestors
12
+ return [] if @parent.nil? || @parent.parent.nil?
13
+ ancestor = @parent.parent
14
+ ancestor.ancestors.dup << ancestor
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,33 @@
1
+ require 'English'
2
+
3
+ module GitCompound
4
+ module Repository
5
+ # Execute git command
6
+ #
7
+ class GitCommand
8
+ attr_reader :output, :status, :command
9
+
10
+ def initialize(cmd, args, workdir = nil)
11
+ @command = "git #{cmd} #{args} 2>&1"
12
+ @workdir = workdir
13
+ end
14
+
15
+ def execute!
16
+ path = @workdir ? @workdir : Dir.pwd
17
+ Dir.chdir(path) { @output = `#{@command}` }
18
+ @status = $CHILD_STATUS.exitstatus
19
+ @output.sub!(/\n\Z/, '')
20
+ end
21
+
22
+ def execute
23
+ execute!
24
+ raise GitCommandError, @output unless valid?
25
+ @output
26
+ end
27
+
28
+ def valid?
29
+ @status == 0
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,79 @@
1
+ module GitCompound
2
+ module Repository
3
+ # Git repository base class
4
+ #
5
+ class GitRepository
6
+ def initialize(source)
7
+ @source = source
8
+ end
9
+
10
+ def clone(destination, options = nil, source = @source)
11
+ args = "#{source} #{destination}"
12
+ args.prepend(options + ' ') if options
13
+ GitCommand.new(:clone, args).execute
14
+ end
15
+
16
+ def versions
17
+ git_versions = tags.map { |tag, sha| GitVersion.new(tag, sha) }
18
+ git_versions.select(&:valid?)
19
+ end
20
+
21
+ def refs
22
+ @refs ||= GitCommand.new('ls-remote', @source).execute
23
+ @refs.scan(%r{^(\b[0-9a-f]{5,40}\b)\srefs\/(heads|tags)\/(.+)})
24
+ rescue GitCommandError => e
25
+ raise RepositoryUnreachableError, "Could not reach repository: #{e.message}"
26
+ end
27
+
28
+ def branches
29
+ refs_select('heads')
30
+ end
31
+
32
+ def tags
33
+ all = refs_select('tags')
34
+ annotated = all.select { |tag, _| tag =~ /\^\{\}$/ }
35
+ annotated.each_pair do |annotated_tag, annotated_tag_sha|
36
+ tag = annotated_tag.sub(/\^\{\}$/, '')
37
+ all.delete(annotated_tag)
38
+ all[tag] = annotated_tag_sha
39
+ end
40
+ all
41
+ end
42
+
43
+ def ref_exists?(ref)
44
+ matching = refs.select { |refs_a| refs_a.include?(ref.to_s) }
45
+ matching.any?
46
+ end
47
+
48
+ # Returns contents of first file found
49
+ #
50
+ def files_contents(files, ref)
51
+ files.each do |file|
52
+ begin
53
+ return file_contents(file, ref)
54
+ rescue FileNotFoundError
55
+ next
56
+ end
57
+ end
58
+ raise FileNotFoundError,
59
+ "Couldn't find any of #{files} files"
60
+ end
61
+
62
+ def file_contents(_file, _ref)
63
+ raise NotImplementedError
64
+ end
65
+
66
+ def file_exists?(_file, _ref)
67
+ raise NotImplementedError
68
+ end
69
+
70
+ private
71
+
72
+ def refs_select(name)
73
+ selected_refs = refs.select { |ref| ref[1] == name }
74
+ selected_refs.collect! { |r| [r.last, r.first] }
75
+ Hash[selected_refs]
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,43 @@
1
+ module GitCompound
2
+ module Repository
3
+ # GitVersion represents tagged version inside Git repository
4
+ #
5
+ class GitVersion
6
+ attr_reader :tag, :sha, :version
7
+
8
+ def initialize(tag, sha)
9
+ @tag = tag
10
+ @sha = sha
11
+ @version = tag.sub(/^v/, '')
12
+ end
13
+
14
+ def to_gem_version
15
+ Gem::Version.new(@version)
16
+ end
17
+
18
+ def valid?
19
+ @tag.match(/^v?#{Gem::Version::VERSION_PATTERN}$/)
20
+ end
21
+
22
+ def matches?(requirement)
23
+ dependency = Gem::Dependency.new('component', requirement)
24
+ dependency.match?('component', to_gem_version, true)
25
+ end
26
+
27
+ def <=>(other)
28
+ to_gem_version <=> other.to_gem_version
29
+ end
30
+
31
+ def ==(other)
32
+ case other
33
+ when String
34
+ version == other
35
+ when GitVersion
36
+ version == other.version
37
+ else
38
+ false
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,30 @@
1
+ module GitCompound
2
+ module Repository
3
+ class RemoteFile
4
+ # Git archive strategy
5
+ #
6
+ class GitArchiveStrategy < RemoteFileStrategy
7
+ def initialize(source, ref, file)
8
+ super
9
+ opts = "--format=tar --remote=#{@source} #{@ref} -- #{@file} | tar -O -xf -"
10
+ @command = GitCommand.new(:archive, opts)
11
+ @command.execute!
12
+ end
13
+
14
+ def contents
15
+ raise FileUnreachableError unless reachable?
16
+ raise FileNotFoundError unless exists?
17
+ @command.output
18
+ end
19
+
20
+ def reachable?
21
+ @command.valid? || @command.output.include?('did not match any files')
22
+ end
23
+
24
+ def exists?
25
+ @command.valid?
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,54 @@
1
+ require 'net/http'
2
+ require 'uri'
3
+
4
+ module GitCompound
5
+ module Repository
6
+ class RemoteFile
7
+ # Git archive strategy
8
+ #
9
+ class GithubStrategy < RemoteFileStrategy
10
+ GITHUB_URI = 'https://raw.githubusercontent.com'
11
+ GITHUB_PATTERN = 'git@github.com:|https:\/\/github.com'
12
+
13
+ def initialize(source, ref, file)
14
+ super
15
+ @uri = github_uri
16
+ end
17
+
18
+ def contents
19
+ raise FileUnreachableError unless reachable?
20
+ raise FileNotFoundError unless exists?
21
+ @response.body
22
+ end
23
+
24
+ def reachable?
25
+ @source.match(/#{GITHUB_PATTERN}/) ? true : false
26
+ end
27
+
28
+ def exists?
29
+ @response ||= http_response(@uri)
30
+ @response.code == 200.to_s
31
+ end
32
+
33
+ private
34
+
35
+ def github_uri
36
+ github_location = @source.sub(/^#{GITHUB_PATTERN}/, '')
37
+ github_location.gsub!(%r{^/|/$}, '')
38
+ file_uri = "#{GITHUB_URI}/#{github_location}/#{@ref}/#{@file}"
39
+ URI.parse(file_uri)
40
+ end
41
+
42
+ def http_response(uri)
43
+ http = Net::HTTP.new(uri.host, uri.port)
44
+ http.use_ssl = true
45
+ http.open_timeout = 4
46
+ http.read_timeout = 4
47
+ params = { 'User-Agent' => 'git_compound' }
48
+ req = Net::HTTP::Get.new(uri.request_uri, params)
49
+ http.request(req)
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,27 @@
1
+ module GitCompound
2
+ module Repository
3
+ class RemoteFile
4
+ # Base interface for strategies
5
+ #
6
+ class RemoteFileStrategy
7
+ def initialize(source, ref, file)
8
+ @source = source
9
+ @ref = ref
10
+ @file = file
11
+ end
12
+
13
+ def contents
14
+ raise NotImplementedError
15
+ end
16
+
17
+ def reachable?
18
+ raise NotImplementedError
19
+ end
20
+
21
+ def exists?
22
+ raise NotImplementedError
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,34 @@
1
+ module GitCompound
2
+ module Repository
3
+ # Remote file loader based on strategies
4
+ #
5
+ class RemoteFile
6
+ def initialize(source, ref, file, strategies = nil)
7
+ @source = source
8
+ @ref = ref
9
+ @file = file
10
+ @strategies = strategies
11
+ end
12
+
13
+ def contents
14
+ @strategies ||= strategies_available
15
+ @strategies.each do |remote_file_strategy|
16
+ remote_file = remote_file_strategy.new(@source, @ref, @file)
17
+ next unless remote_file.reachable?
18
+ return remote_file.contents
19
+ end
20
+ raise FileUnreachableError,
21
+ "Couldn't reach file #{@file} after trying #{@strategies.count} stategies"
22
+ end
23
+
24
+ def strategies_available
25
+ # Strategies ordered by #reachable? method overhead
26
+ # More general strategies should be placed at lower positions,
27
+ # but this also depends on #reachable? overhead.
28
+ #
29
+ [GithubStrategy,
30
+ GitArchiveStrategy]
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,81 @@
1
+ module GitCompound
2
+ module Repository
3
+ # Local git repository implementation
4
+ #
5
+ class RepositoryLocal < GitRepository
6
+ def initialize(source)
7
+ super
8
+ raise RepositoryUnreachableError unless
9
+ File.directory?("#{@source}/.git")
10
+ end
11
+
12
+ def clone(destination, options = nil)
13
+ # Prefer ^file:/// instead of ^/ as latter does not work with --depth
14
+ source = @source.sub(%r{^\/}, 'file:///')
15
+ super(destination, options, source)
16
+ end
17
+
18
+ def checkout(ref)
19
+ GitCommand.new(:checkout, ref, @source).execute
20
+ end
21
+
22
+ def fetch
23
+ GitCommand.new(:fetch, '', @source).execute
24
+ GitCommand.new(:fetch, '--tags', @source).execute
25
+ end
26
+
27
+ def merge(mergeable = 'FETCH_HEAD')
28
+ GitCommand.new(:merge, mergeable, @source).execute
29
+ end
30
+
31
+ def file_exists?(file, ref)
32
+ cmd = GitCommand.new(:show, "#{ref}:#{file}", @source)
33
+ cmd.execute!
34
+ cmd.valid?
35
+ end
36
+
37
+ def file_contents(file, ref)
38
+ raise FileNotFoundError unless file_exists?(file, ref)
39
+ GitCommand.new(:show, "#{ref}:#{file}", @source).execute
40
+ end
41
+
42
+ def origin_remote
43
+ origin = GitCommand.new(:remote, '-v', @source).execute.match(/origin\t(.*?)\s/)
44
+ origin.captures.first if origin
45
+ end
46
+
47
+ def untracked_files?(exclude = nil)
48
+ untracked =
49
+ GitCommand.new('ls-files', '--exclude-standard --others', @source).execute
50
+ return (untracked.length > 0) unless exclude
51
+
52
+ untracked = untracked.split("\n")
53
+ untracked.delete_if do |file|
54
+ exclude.include?(file) || exclude.include?(file.split(File::SEPARATOR).first)
55
+ end
56
+
57
+ untracked.any?
58
+ end
59
+
60
+ def uncommited_changes?
61
+ GitCommand.new('update-index', '-q --refresh', @source).execute
62
+ unstaged = GitCommand.new('diff-files', '--quiet', @source)
63
+ uncommited = GitCommand.new('diff-index', '--cached --quiet HEAD', @source)
64
+
65
+ [unstaged, uncommited].any? do |cmd|
66
+ cmd.execute!
67
+ !cmd.valid?
68
+ end
69
+ end
70
+
71
+ def unpushed_commits?
72
+ unpushed = GitCommand.new('rev-list', '@{u}..', @source)
73
+ unpushed.execute.length > 0
74
+ end
75
+
76
+ def head_sha
77
+ GitCommand.new('rev-parse', 'HEAD', @source).execute
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,12 @@
1
+ module GitCompound
2
+ module Repository
3
+ # Remote git repository implementation
4
+ #
5
+ class RepositoryRemote < GitRepository
6
+ def file_contents(file, ref)
7
+ remote_file = RemoteFile.new(@source, ref, file)
8
+ remote_file.contents
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,19 @@
1
+ module GitCompound
2
+ # Git repositories module, also repository factory
3
+ #
4
+ module Repository
5
+ extend self
6
+
7
+ def factory(source)
8
+ if local?(source)
9
+ RepositoryLocal.new(source)
10
+ else
11
+ RepositoryRemote.new(remote = source) # rubocop:disable Lint/UselessAssignment
12
+ end
13
+ end
14
+
15
+ def local?(source)
16
+ source.match(%r{(^\/|file:\/\/).*})
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,28 @@
1
+ module GitCompound
2
+ module Task
3
+ # Base abstract class for task
4
+ #
5
+ class Task
6
+ attr_reader :name
7
+
8
+ def initialize(name, manifest, &block)
9
+ raise GitCompoundError,
10
+ "Block not given for task `#{name}`" unless block
11
+
12
+ @name = name
13
+ @manifest = manifest
14
+ @block = block
15
+ end
16
+
17
+ def execute
18
+ raise NotImplementedError
19
+ end
20
+
21
+ private
22
+
23
+ def execute_on(directory, component)
24
+ @block.call(directory, component)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,27 @@
1
+ module GitCompound
2
+ module Task
3
+ # Task for all descendant components in manifest
4
+ #
5
+ class TaskAll < Task
6
+ def initialize(name, manifest, &block)
7
+ super
8
+ @components = components_collect!
9
+ end
10
+
11
+ def execute
12
+ @components.each_value do |component|
13
+ execute_on(component.destination_path, component)
14
+ end
15
+ end
16
+
17
+ private
18
+
19
+ def components_collect!
20
+ components = {}
21
+ @manifest.process(Worker::CircularDependencyChecker.new,
22
+ Worker::ComponentsCollector.new(components))
23
+ components
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,18 @@
1
+ module GitCompound
2
+ module Task
3
+ # Task for each component defined in manifest
4
+ #
5
+ class TaskEach < Task
6
+ def initialize(name, manifest, &block)
7
+ super
8
+ @components = manifest.components
9
+ end
10
+
11
+ def execute
12
+ @components.each_value do |component|
13
+ execute_on(component.destination_path, component)
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,21 @@
1
+ module GitCompound
2
+ module Task
3
+ # Single task for single component
4
+ #
5
+ class TaskSingle < Task
6
+ def initialize(name, manifest, &block)
7
+ super
8
+ @component = @manifest.parent
9
+ end
10
+
11
+ def execute
12
+ if @component
13
+ execute_on(@component.destination_path, @component.manifest)
14
+ else
15
+ # Root manifest without parent
16
+ execute_on(Dir.pwd, @manifest)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,22 @@
1
+ module GitCompound
2
+ # Task module and factory
3
+ #
4
+ module Task
5
+ extend self
6
+
7
+ def factory(name, type, manifest, &block)
8
+ case
9
+ # manifest task
10
+ when type.nil? || type == :manfiest then task_class = TaskSingle
11
+ # task for each component defined in manifest
12
+ when type == :each then task_class = TaskEach
13
+ # task for all descendant components of manifest
14
+ when type == :all then task_class = TaskAll
15
+ else
16
+ raise GitCompoundError, "Unrecognized task type `#{type}`"
17
+ end
18
+
19
+ task_class.new(name, manifest, &block)
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,5 @@
1
+ # GitCompound
2
+ #
3
+ module GitCompound
4
+ VERSION = '0.0.9'
5
+ end
@@ -0,0 +1,31 @@
1
+ module GitCompound
2
+ module Worker
3
+ # Worker that checks if unwanted circular dependency exists
4
+ #
5
+ class CircularDependencyChecker < Worker
6
+ def visit_component(component)
7
+ @element = component
8
+ raise_error if circular_dependency_exists?
9
+ end
10
+
11
+ def visit_manifest(manifest)
12
+ @element = manifest
13
+ raise_error if circular_dependency_exists?
14
+ end
15
+
16
+ private
17
+
18
+ def circular_dependency_exists?
19
+ @element.ancestors.include?(@element)
20
+ end
21
+
22
+ def raise_error
23
+ name = @element.name
24
+ type = @element.class.name.downcase
25
+
26
+ raise CircularDependencyError,
27
+ "Circular dependency detected in #{type} `#{name}`!"
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,30 @@
1
+ module GitCompound
2
+ module Worker
3
+ # Worker that builds components
4
+ #
5
+ class ComponentBuilder < Worker
6
+ def initialize(lock = nil)
7
+ @lock = lock
8
+ @print = PrettyPrint.new
9
+ end
10
+
11
+ def visit_component(component)
12
+ raise GitCompoundError,
13
+ "Destination directory `#{component.destination_path}` " \
14
+ 'already exists !' if component.destination_exists?
15
+
16
+ Logger.inline 'Building: '
17
+ @print.visit_component(component)
18
+
19
+ component.build
20
+
21
+ raise GitCompoundError,
22
+ "Destination `#{component.destination_path}` " \
23
+ 'verification failed !' unless component.destination_exists?
24
+
25
+ return unless @lock
26
+ @lock.lock_component(component) unless @lock.find(component)
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,25 @@
1
+ module GitCompound
2
+ module Worker
3
+ # Worker that replaces components if necessary
4
+ #
5
+ class ComponentReplacer < Worker
6
+ def initialize(lock)
7
+ @lock = lock
8
+ @print = PrettyPrint.new
9
+ end
10
+
11
+ def visit_component(component)
12
+ raise "Component `#{component.name}` is not built !" unless
13
+ component.destination_exists?
14
+
15
+ Logger.inline 'Replacing: '
16
+ @print.visit_component(component)
17
+
18
+ component.remove!
19
+ component.build
20
+
21
+ @lock.lock_component(component)
22
+ end
23
+ end
24
+ end
25
+ end