git_compound 0.0.9

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