shanty 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 472f2d824a07db1b5c47ec13ddca184cad7478db
4
- data.tar.gz: 6a45c1b9a654584efbd06d87d2c7202667095b43
3
+ metadata.gz: c1dc7e7d6baa3b74a90fb389dee1d33fc8959af0
4
+ data.tar.gz: 597c9fa320428c48c3ca7e100048e7fdb50a982b
5
5
  SHA512:
6
- metadata.gz: 92abe04b4e6d65338f17a637c03ff0079297b3777c3e23726d4d8f85438121ece70a02806feac64e149e24103816f734518247e6f07ee53c460b0778a802b31a
7
- data.tar.gz: 7b7e75add8f40176c1510ffeb96453678194a8e4f7f6a6fafb9f266d1e3a211d00d5d144285ee50d72ebd0e63d8c3276d9931d6017121705ca2ab8b2feecf3f0
6
+ metadata.gz: ab037eae56ab017500c24a0b3e23e178763953736e43a115201851aefba6f38b009c77638281608bdb2c8181d035049ac1ef136a2fb1fe1fd2a99fa38acb1b66
7
+ data.tar.gz: 43122c363848e085b35dcbd76956a0e51127ff9ebf38bdeaac49b81138a4d42a1e78a9bd5859bd38d66d3383d27811254b0c152180b5a6b771fc9af176fdc905
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Shanty [![Build Status](https://travis-ci.org/shantytown/shanty.svg?branch=master)](https://travis-ci.org/shantytown/shanty)[![Coverage Status](https://coveralls.io/repos/shantytown/shanty/badge.png?branch=master)](https://coveralls.io/r/shantytown/shanty?branch=master)
1
+ # Shanty [![Build Status](https://travis-ci.org/shantytown/shanty.svg?branch=master)](https://travis-ci.org/shantytown/shanty) [![Coverage Status](https://coveralls.io/repos/shantytown/shanty/badge.png?branch=master)](https://coveralls.io/r/shantytown/shanty?branch=master)
2
2
 
3
3
  **Shanty** is a project orchestration tool. It is designed to make it easy for you to build dependencies between projects, and execute tasks across this tree of relationships, regardless of what language or technology these projects use. The aim is consistency in the way you manage, build, test and deploy your projects.
4
4
 
@@ -31,7 +31,7 @@ We welcome any contribution, whether big or small! We're busy importing our plan
31
31
 
32
32
  The MIT License (MIT)
33
33
 
34
- Copyright (c) 2014 Chris Jansen, Nathan Kleyn
34
+ Copyright (c) 2015 Chris Jansen, Nathan Kleyn
35
35
 
36
36
  Permission is hereby granted, free of charge, to any person obtaining a copy
37
37
  of this software and associated documentation files (the "Software"), to deal
@@ -4,23 +4,27 @@ require 'pry'
4
4
 
5
5
  require 'shanty/cli'
6
6
  require 'shanty/discoverers/shantyfile'
7
+ require 'shanty/discoverers/rubygem'
8
+ require 'shanty/env'
7
9
  require 'shanty/graph'
8
- require 'shanty/global'
9
- require 'shanty/mutators/git'
10
- require 'shanty/tasks/basic'
10
+ require 'shanty/mutators/bundler'
11
+ require 'shanty/mutators/changed'
12
+ require 'shanty/plugins/rspec'
13
+ require 'shanty/plugins/rubocop'
14
+ require 'shanty/task_env'
15
+ require 'shanty/task_sets/basic'
11
16
 
12
17
  module Shanty
13
18
  # Main shanty class
14
19
  class Shanty
20
+ # This is the root directory where the Shanty gem is located. Do not confuse this with the root of the repository
21
+ # in which Shanty is operating, which is available via the TaskEnv class.
15
22
  GEM_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..'))
16
23
 
17
24
  def start!
18
25
  setup_i18n
19
- Cli.new(graph).run
20
- end
21
26
 
22
- def graph
23
- @graph ||= construct_project_graph
27
+ Cli.new(TaskEnv.new(Env.new)).run
24
28
  end
25
29
 
26
30
  private
@@ -29,19 +33,5 @@ module Shanty
29
33
  I18n.enforce_available_locales = true
30
34
  I18n.load_path = Dir[File.join(GEM_ROOT, 'translations', '*.yml')]
31
35
  end
32
-
33
- def construct_project_graph
34
- project_templates = Dir.chdir(Global.root) do
35
- Discoverer.new.discover_all
36
- end
37
-
38
- projects = project_templates.map do |project_template|
39
- project_template.type.new(project_template)
40
- end
41
-
42
- graph = Graph.new(projects)
43
-
44
- Mutator.new.apply_mutations(graph)
45
- end
46
36
  end
47
37
  end
@@ -1,6 +1,6 @@
1
1
  require 'commander'
2
2
  require 'i18n'
3
- require 'shanty/task'
3
+ require 'shanty/task_set'
4
4
 
5
5
  module Shanty
6
6
  # Public: Handle the CLI interface between the user and the registered tasks
@@ -8,13 +8,13 @@ module Shanty
8
8
  class Cli
9
9
  include Commander::Methods
10
10
 
11
- def initialize(graph)
12
- @graph = graph
11
+ def initialize(task_env)
12
+ @task_env = task_env
13
13
  end
14
14
 
15
15
  def tasks
16
- Task.tasks.reduce({}) do |acc, task|
17
- acc.merge(task.task_definitions)
16
+ TaskSet.task_sets.reduce({}) do |acc, task_set|
17
+ acc.merge(task_set.tasks)
18
18
  end
19
19
  end
20
20
 
@@ -36,28 +36,44 @@ module Shanty
36
36
  end
37
37
 
38
38
  def setup_task(name, task)
39
- command name do |c|
39
+ command(name) do |c|
40
40
  c.description = I18n.t(task[:desc], default: task[:desc])
41
+ c.syntax = task[:syntax]
42
+ add_options_to_command(task, c)
43
+ add_action_to_command(name, task, c)
44
+ end
45
+ end
41
46
 
42
- task[:options].each do |option_name, option|
43
- c.option(syntax_for_option(option_name, option), I18n.t(option[:desc], default: option[:desc]))
44
- end
47
+ def add_options_to_command(task, command)
48
+ task[:options].each do |option_name, option|
49
+ command.option(syntax_for_option(option_name, option), I18n.t(option[:desc], default: option[:desc]))
50
+ end
51
+ end
45
52
 
46
- c.action do |args, options|
47
- execute_task(name, args, options)
48
- end
53
+ def add_action_to_command(name, task, command)
54
+ command.action do |args, options|
55
+ task = tasks[name]
56
+ options.default(Hash[defaults_for_options(task)])
57
+ execute_task(name, task, args, options)
49
58
  end
50
59
  end
51
60
 
52
- def execute_task(name, args, options)
53
- task = tasks[name]
61
+ def execute_task(name, task, args, options)
62
+ # We use allocate here beccause we do not want this to blow up because the class defines a constructor.
63
+ # We cannot and do not support taskset classes needing constructors.
64
+ klass = task[:klass].allocate
65
+ arity = klass.method(name).arity
54
66
 
55
- default_option_pairs = task[:options].map do |option_name, option|
67
+ args.unshift(@task_env) if arity >= 2
68
+ args.unshift(options) if arity >= 1
69
+
70
+ klass.send(name, *args)
71
+ end
72
+
73
+ def defaults_for_options(task)
74
+ task[:options].map do |option_name, option|
56
75
  [option_name, default_for_type(option)]
57
76
  end
58
- options.default(Hash[default_option_pairs])
59
-
60
- task[:klass].new.send(name, options, @graph, *args)
61
77
  end
62
78
 
63
79
  def syntax_for_option(name, option)
@@ -65,7 +81,7 @@ module Shanty
65
81
  when :boolean
66
82
  "--#{name}"
67
83
  else
68
- "--#{name} #{option[:type].upcase}"
84
+ "--#{name} #{(option[:type] || 'string').upcase}"
69
85
  end
70
86
 
71
87
  option[:required] ? "[#{syntax}]" : syntax
@@ -10,6 +10,12 @@ module Shanty
10
10
  attr_reader :discoverers
11
11
  end
12
12
 
13
+ attr_reader :env
14
+
15
+ def initialize(env)
16
+ @env = env
17
+ end
18
+
13
19
  def self.inherited(discoverer)
14
20
  Util.logger.debug("Detected project discoverer #{discoverer}")
15
21
  @discoverers ||= []
@@ -18,14 +24,14 @@ module Shanty
18
24
 
19
25
  def discover_all
20
26
  self.class.discoverers.flat_map do |discoverer|
21
- discoverer.new.discover
27
+ discoverer.new(env).discover
22
28
  end
23
29
  end
24
30
 
25
31
  private
26
32
 
27
- def create_project(*args)
28
- ProjectTemplate.new(*args)
33
+ def create_project_template(*args)
34
+ ProjectTemplate.new(Dir.pwd, *args).tap { |pt| yield pt }
29
35
  end
30
36
  end
31
37
  end
@@ -0,0 +1,17 @@
1
+ require 'shanty/discoverer'
2
+ require 'shanty/projects/rubygem'
3
+
4
+ module Shanty
5
+ # Public: Discoverer for Shantyfiles
6
+ # will create a a project for every Shantyfile it finds in
7
+ # a directory
8
+ class RubygemDiscoverer < Discoverer
9
+ def discover
10
+ Dir[File.join(env.root, '**', '*.gemspec')].map do |path|
11
+ create_project_template(File.absolute_path(File.dirname(path))) do |project_template|
12
+ project_template.type = RubygemProject
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -5,10 +5,17 @@ module Shanty
5
5
  # Public: Discoverer for Shantyfiles
6
6
  # will create a a project for every Shantyfile it finds in
7
7
  # a directory
8
+ #
9
+ # Note that this does not execute the Shantyfile. That
10
+ # happens inside the ProjectTemplate as we may have projects
11
+ # discovered by other discoverers that still need
12
+ # customisation.
8
13
  class ShantyfileDiscoverer < Discoverer
9
14
  def discover
10
- Dir['**/Shantyfile'].map do |path|
11
- create_project(File.absolute_path(File.dirname(path)))
15
+ Dir[File.join(env.root, '**', 'Shantyfile')].map do |path|
16
+ create_project_template(File.absolute_path(File.dirname(path))) do |project_template|
17
+ project_template.priority = -1
18
+ end
12
19
  end
13
20
  end
14
21
  end
@@ -0,0 +1,51 @@
1
+ require 'deep_merge'
2
+ require 'yaml'
3
+
4
+ module Shanty
5
+ #
6
+ class Env
7
+ CONFIG_FILE = '.shanty.yml'
8
+ DEFAULT_CONFIG = {}
9
+
10
+ def initialize
11
+ Dir.chdir(root) do
12
+ (config['require'] || {}).each do |requirement|
13
+ requirement = "#{requirement}/**/*.rb" unless requirement.include?('.rb')
14
+ Dir[requirement].each { |f| require(File.join(root, f)) }
15
+ end
16
+ end
17
+ end
18
+
19
+ def environment
20
+ @environment = ENV['SHANTY_ENV'] || 'local'
21
+ end
22
+
23
+ def root
24
+ @root ||= find_root
25
+ end
26
+
27
+ private
28
+
29
+ def config
30
+ return @config unless @config.nil?
31
+
32
+ file_config = YAML.load_file("#{root}/#{CONFIG_FILE}") || {}
33
+ @config = DEFAULT_CONFIG.deep_merge!(file_config[environment])
34
+ end
35
+
36
+ def find_root
37
+ if root_dir.nil?
38
+ fail "Could not find a #{CONFIG_FILE} file in this or any parent directories. \
39
+ Please run `shanty init` in the directory you want to be the root of your project structure."
40
+ end
41
+
42
+ root_dir
43
+ end
44
+
45
+ def root_dir
46
+ Pathname.new(Dir.pwd).ascend do |d|
47
+ return d if d.join(CONFIG_FILE).exist?
48
+ end
49
+ end
50
+ end
51
+ end
@@ -1,4 +1,6 @@
1
1
  require 'algorithms'
2
+ require 'forwardable'
3
+ require 'tsort'
2
4
 
3
5
  module Shanty
4
6
  # Public: Represents the link graph of projects in the repository. This class is
@@ -7,20 +9,22 @@ module Shanty
7
9
  # projects need to be build by combining a Git diff with the dependency graph
8
10
  # to resolve to a list of projects required to be built.
9
11
  class Graph
10
- attr_reader :projects
12
+ extend Forwardable
13
+ include TSort, Enumerable
14
+
15
+ def_delegators :@projects, :<<, :length, :add, :remove
16
+ def_delegators :sorted_projects, :each, :values, :[]
11
17
 
12
18
  # Public: Initialise a ProjectLinkGraph.
13
19
  #
14
- # projects - An array of project instances to take and link together into
15
- # a graph structure of dependencies.
16
- def initialize(projects)
17
- @projects = sort_projects(link_projects(projects))
18
-
20
+ # project_templates - An array of project templates to take, instantiate and
21
+ # link together into a graph structure of dependencies.
22
+ def initialize(project_templates)
19
23
  @project_path_trie = Containers::Trie.new
24
+ @project_templates = project_templates
25
+ @projects = projects_by_path.values
20
26
 
21
- @projects.each do |project|
22
- @project_path_trie[project.path] = project
23
- end
27
+ link_projects
24
28
  end
25
29
 
26
30
  # Public: All the changed projects in the current repository.
@@ -28,7 +32,7 @@ module Shanty
28
32
  # Returns an Array of Project subclasses, one for each project in the
29
33
  # repository.
30
34
  def changed
31
- @projects.select { |project| project.changed? }
35
+ select(&:changed?)
32
36
  end
33
37
 
34
38
  # Public: Returns a project, if any, with the given name.
@@ -37,7 +41,7 @@ module Shanty
37
41
  #
38
42
  # Returns an instance of a Project subclass if found, otherwise nil.
39
43
  def by_name(name)
40
- @projects.find { |project| project.name == name }
44
+ find { |project| project.name == name }
41
45
  end
42
46
 
43
47
  # Public: Returns all projects of the given types.
@@ -47,7 +51,7 @@ module Shanty
47
51
  # Returns an Array of Project subclasses, one for each project in the
48
52
  # repository.
49
53
  def all_of_type(*types)
50
- @projects.select { |project| types.include?(project.class) }
54
+ select { |project| types.include?(project.class) }
51
55
  end
52
56
 
53
57
  # Public: Returns all the changed projects of the given types.
@@ -85,6 +89,26 @@ module Shanty
85
89
 
86
90
  private
87
91
 
92
+ def sorted_projects
93
+ @sorted_projects ||= tsort
94
+ end
95
+
96
+ def tsort_each_node
97
+ @projects.each { |p| yield p }
98
+ end
99
+
100
+ def tsort_each_child(project)
101
+ projects_by_path[project.path].parents.each { |p| yield p }
102
+ end
103
+
104
+ def projects_by_path
105
+ @projects_by_path ||= Hash[@project_templates.map do |project_template|
106
+ project = project_template.type.new(project_template)
107
+ @project_path_trie[project.path] = project
108
+ [project.path, project]
109
+ end]
110
+ end
111
+
88
112
  # Private: Given a list of projects, construct the parent/child
89
113
  # relationships between them given a list of their parents/children by name
90
114
  # as defined on the project instances.
@@ -92,31 +116,18 @@ module Shanty
92
116
  # projects - An array of Project subclasses to link together.
93
117
  #
94
118
  # Returns an Array of linked projects.
95
- def link_projects(projects)
96
- projects_by_name = projects.each_with_object({}) { |project, acc| acc[project.name] = project }
119
+ def link_projects
120
+ projects_by_path.each_value do |project|
121
+ project.parents_by_path.each do |parent_path|
122
+ parent_dependency = projects_by_path[parent_path]
123
+ if parent_dependency.nil?
124
+ fail("Cannot find project at path #{parent_path}, which was specified as a dependency for #{project}")
125
+ end
97
126
 
98
- projects.each do |project|
99
- parent_dependencies = project.parents_by_name.map { |parent_name| projects_by_name[parent_name] }.compact
100
-
101
- parent_dependencies.each do |parent_dependency|
102
127
  project.add_parent(parent_dependency)
103
128
  parent_dependency.add_child(project)
104
129
  end
105
130
  end
106
-
107
- projects
108
- end
109
-
110
- # Private: Given a list of Project subclasses, sort them by their distance
111
- # from the root node (that is, the topmost node). This order is important
112
- # because it matches the order in which things should be build to avoid
113
- # missing dependencies.
114
- #
115
- # projects - An array of Project subclasses to sort.
116
- #
117
- # Returns a sorted Array of Project subclasses.
118
- def sort_projects(projects)
119
- projects.sort { |a, b| a.distance_from_root <=> b.distance_from_root }
120
131
  end
121
132
  end
122
133
  end
@@ -66,16 +66,6 @@ module Shanty
66
66
  def all_parents
67
67
  parents + parents.map(&:all_parents).flatten
68
68
  end
69
-
70
- # Public: Calculate the maximum number of traverses that need to be made to
71
- # reach the root from this node.
72
- #
73
- # Returns a Fixnum representing the traverses to the root. Note that a return
74
- # value of 0 means this is the root (ie. it has no parents).
75
- def distance_from_root
76
- return 0 if parents.empty?
77
- parents.map(&:distance_from_root).max + 1
78
- end
79
69
  end
80
70
  end
81
71
  end
@@ -1,5 +1,3 @@
1
- require 'hooks'
2
-
3
1
  module Shanty
4
2
  module Mixins
5
3
  # A mixin to implement publish/subscribe style callbacks in the class that
@@ -15,24 +13,38 @@ module Shanty
15
13
  # to call extend and include for this mixin. You'll see this idiom everywhere
16
14
  # in the Ruby/Rails world, so we use it too.
17
15
  def self.included(cls)
18
- cls.include(Hooks)
19
16
  cls.extend(ClassMethods)
20
17
  end
21
18
 
22
19
  # Common methods inherited by all classes
23
20
  module ClassMethods
21
+ def class_callbacks
22
+ @class_callbacks ||= Hash.new { |h, k| h[k] = [] }
23
+ end
24
+
24
25
  def subscribe(*names, sym)
25
26
  names.each do |name|
26
- define_hook(name, halts_on_falsey: true) unless respond_to?(name)
27
- send(name, sym)
27
+ class_callbacks[name] << sym
28
28
  end
29
29
  end
30
30
  end
31
31
 
32
+ def callbacks
33
+ @callbacks ||= Hash.new { |h, k| h[k] = [] }
34
+ end
35
+
36
+ def subscribe(*names, sym)
37
+ names.each do |name|
38
+ callbacks[name] << sym
39
+ end
40
+ end
41
+
32
42
  def publish(name, *args)
33
- return true if self.class.callbacks_for_hook(name).nil?
34
- results = run_hook(name, *args)
35
- !results.halted?
43
+ class_callback_methods = self.class.class_callbacks[name]
44
+ callback_methods = callbacks[name]
45
+
46
+ (class_callback_methods + callback_methods).each { |method| return false unless send(method, *args) }
47
+ true
36
48
  end
37
49
  end
38
50
  end