greenhouse 0.0.3

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 (51) hide show
  1. checksums.yaml +7 -0
  2. data/bin/greenhouse +9 -0
  3. data/lib/greenhouse/cli.rb +88 -0
  4. data/lib/greenhouse/commands/add.rb +32 -0
  5. data/lib/greenhouse/commands/command.rb +88 -0
  6. data/lib/greenhouse/commands/configure.rb +9 -0
  7. data/lib/greenhouse/commands/help.rb +45 -0
  8. data/lib/greenhouse/commands/init.rb +93 -0
  9. data/lib/greenhouse/commands/launch.rb +113 -0
  10. data/lib/greenhouse/commands/new.rb +42 -0
  11. data/lib/greenhouse/commands/pull.rb +45 -0
  12. data/lib/greenhouse/commands/purge.rb +91 -0
  13. data/lib/greenhouse/commands/push.rb +45 -0
  14. data/lib/greenhouse/commands/remove.rb +32 -0
  15. data/lib/greenhouse/commands/start.rb +21 -0
  16. data/lib/greenhouse/commands/status.rb +50 -0
  17. data/lib/greenhouse/commands/sync.rb +42 -0
  18. data/lib/greenhouse/commands.rb +32 -0
  19. data/lib/greenhouse/projects/application.rb +19 -0
  20. data/lib/greenhouse/projects/collection.rb +23 -0
  21. data/lib/greenhouse/projects/engine.rb +6 -0
  22. data/lib/greenhouse/projects/gem.rb +6 -0
  23. data/lib/greenhouse/projects/project.rb +77 -0
  24. data/lib/greenhouse/projects/repository.rb +197 -0
  25. data/lib/greenhouse/projects.rb +86 -0
  26. data/lib/greenhouse/resources/dotenv_file.rb +69 -0
  27. data/lib/greenhouse/resources/file_resource.rb +50 -0
  28. data/lib/greenhouse/resources/ignore_file.rb +115 -0
  29. data/lib/greenhouse/resources/procfile.rb +144 -0
  30. data/lib/greenhouse/resources/projects_file.rb +44 -0
  31. data/lib/greenhouse/resources.rb +10 -0
  32. data/lib/greenhouse/scripts/argument.rb +36 -0
  33. data/lib/greenhouse/scripts/arguments.rb +28 -0
  34. data/lib/greenhouse/scripts/invalid_argument.rb +6 -0
  35. data/lib/greenhouse/scripts/script.rb +109 -0
  36. data/lib/greenhouse/scripts.rb +4 -0
  37. data/lib/greenhouse/tasks/add_project.rb +57 -0
  38. data/lib/greenhouse/tasks/generate_procfile.rb +22 -0
  39. data/lib/greenhouse/tasks/project_status.rb +37 -0
  40. data/lib/greenhouse/tasks/project_task.rb +363 -0
  41. data/lib/greenhouse/tasks/pull_project.rb +14 -0
  42. data/lib/greenhouse/tasks/purge_project.rb +13 -0
  43. data/lib/greenhouse/tasks/push_project.rb +14 -0
  44. data/lib/greenhouse/tasks/remove_greenhouse_files.rb +58 -0
  45. data/lib/greenhouse/tasks/remove_project.rb +15 -0
  46. data/lib/greenhouse/tasks/sync_project.rb +19 -0
  47. data/lib/greenhouse/tasks/task.rb +25 -0
  48. data/lib/greenhouse/tasks.rb +20 -0
  49. data/lib/greenhouse/version.rb +20 -0
  50. data/lib/greenhouse.rb +12 -0
  51. metadata +165 -0
@@ -0,0 +1,45 @@
1
+ module Greenhouse
2
+ module Commands
3
+ class Push
4
+ include Command
5
+ command_summary "Push local branches for all projects to their git remotes"
6
+ project_arguments *Projects::projects.map(&:to_arg)
7
+
8
+ class << self
9
+ def usage
10
+ puts <<USAGE
11
+ usage: #{::Greenhouse::CLI.command_name} #{command_name} [<project>] #{valid_arguments.to_s}
12
+
13
+ Projects:
14
+ #{project_arguments.to_help}
15
+ USAGE
16
+ end
17
+ end
18
+
19
+ def push_all
20
+ if Projects.projects.empty?
21
+ puts "No projects defined."
22
+ return
23
+ end
24
+
25
+ Projects.projects.each do |project|
26
+ Tasks::PushProject.perform(project)
27
+ end
28
+ end
29
+
30
+ def push_project(project)
31
+ unless project.exists?
32
+ puts "Project \e[36m#{project.title}\e[0m does not exist. Try initializing it with `greenhouse init`"
33
+ return
34
+ end
35
+
36
+ Tasks::PushProject.perform(project)
37
+ end
38
+
39
+ def run
40
+ project = Projects::projects.select { |project| arguments.map(&:key).include?(project.name) }.first
41
+ project.nil? ? push_all : push_project(project)
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,32 @@
1
+ module Greenhouse
2
+ module Commands
3
+ class Remove
4
+ include Command
5
+ command_summary "Purge a project and remove it from the ecosystem .projects file"
6
+ project_arguments *Projects::projects.map(&:to_arg)
7
+
8
+ class << self
9
+ def usage
10
+ puts <<USAGE
11
+ usage: #{::Greenhouse::CLI.command_name} #{command_name} <project>
12
+
13
+ Projects:
14
+ #{project_arguments.to_help}
15
+ USAGE
16
+ end
17
+ end
18
+
19
+ def run
20
+ if arguments.empty?
21
+ puts "You must provide the name of the project you want to remove from your ecosystem."
22
+ usage
23
+ return
24
+ end
25
+
26
+ project = Projects::projects.select { |proj| proj.name == arguments[0].key }.first
27
+ Tasks::PurgeProject.perform(project)
28
+ Tasks::RemoveProject.perform(project)
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,21 @@
1
+ module Greenhouse
2
+ module Commands
3
+ class Start
4
+ include Command
5
+
6
+ command_summary "Startup the entire ecosystem of apps using your Procfile (aliases `foreman start`)"
7
+
8
+ class << self
9
+ def usage
10
+ puts "usage: #{::Greenhouse::CLI.command_name} #{command_name} #{valid_arguments.to_s}"
11
+ end
12
+ end
13
+
14
+ def run
15
+ Dir.chdir(Projects::path) do
16
+ exec 'foreman start'
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,50 @@
1
+ module Greenhouse
2
+ module Commands
3
+ class Status
4
+ include Command
5
+ command_summary "List projects and their current status"
6
+ valid_arguments Scripts::Argument.new("-v, --verbose", :summary => "Print out detailed information about project status (local changes, ahead/behind/diverged branches, etc.)")
7
+ project_arguments *Projects::projects.map(&:to_arg)
8
+
9
+ class << self
10
+ def usage
11
+ puts <<USAGE
12
+ usage: #{::Greenhouse::CLI.command_name} #{command_name} [<project>] #{valid_arguments.to_s}
13
+
14
+ Arguments:
15
+ #{valid_arguments.to_help}
16
+
17
+ Projects:
18
+ #{project_arguments.to_help}
19
+ USAGE
20
+ end
21
+ end
22
+
23
+ def verbose
24
+ arguments.map(&:key).include?("-v")
25
+ end
26
+
27
+ def ecosystem_status
28
+ if Projects::projects.empty?
29
+ puts "No projects configured."
30
+ return
31
+ end
32
+
33
+ puts "The following projects are configured in your ecosystem: "
34
+ Projects::projects.each do |project|
35
+ puts
36
+ Tasks::ProjectStatus.perform(project, verbose)
37
+ end
38
+ end
39
+
40
+ def project_status(project)
41
+ Tasks::ProjectStatus.perform(project, verbose)
42
+ end
43
+
44
+ def run
45
+ project = Projects::projects.select { |project| arguments.map(&:key).include?(project.name) }.first
46
+ project.nil? ? ecosystem_status : project_status(project)
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,42 @@
1
+ module Greenhouse
2
+ module Commands
3
+ class Sync
4
+ include Command
5
+ command_summary "Sync all projects with their git remotes"
6
+ project_arguments *Projects::projects.map(&:to_arg)
7
+
8
+ class << self
9
+ def usage
10
+ puts <<USAGE
11
+ usage: #{::Greenhouse::CLI.command_name} #{command_name} [<project>] #{valid_arguments.to_s}
12
+
13
+ Projects:
14
+ #{project_arguments.to_help}
15
+ USAGE
16
+ end
17
+ end
18
+
19
+ def sync_all
20
+ if Projects.projects.empty?
21
+ puts "No projects defined."
22
+ return
23
+ end
24
+
25
+ Projects.projects.each do |project|
26
+ Tasks::SyncProject.perform(project)
27
+ end
28
+ Tasks::GenerateProcfile.perform
29
+ end
30
+
31
+ def sync_project(project)
32
+ Tasks::SyncProject.perform(project)
33
+ Tasks::GenerateProcfile.perform if project.type == 'application'
34
+ end
35
+
36
+ def run
37
+ project = Projects::projects.select { |project| arguments.map(&:key).include?(project.name) }.first
38
+ project.nil? ? sync_all : sync_project(project)
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,32 @@
1
+ module Greenhouse
2
+ module Commands
3
+ def self.commands
4
+ @commands ||= []
5
+ @commands
6
+ end
7
+
8
+ def self.exists?(cmd)
9
+ commands.map(&:command_name).include?(cmd.underscore.to_s)
10
+ end
11
+
12
+ def self.command(cmd)
13
+ raise "Command does not exist: #{cmd}" unless exists?(cmd)
14
+ commands.select { |command| command.command_name == cmd.underscore.to_s }.first
15
+ end
16
+ end
17
+ end
18
+
19
+ require 'greenhouse/commands/command'
20
+ require 'greenhouse/commands/new'
21
+ require 'greenhouse/commands/init'
22
+ require 'greenhouse/commands/configure'
23
+ require 'greenhouse/commands/add'
24
+ require 'greenhouse/commands/status'
25
+ require 'greenhouse/commands/launch'
26
+ require 'greenhouse/commands/start'
27
+ require 'greenhouse/commands/push'
28
+ require 'greenhouse/commands/pull'
29
+ require 'greenhouse/commands/sync'
30
+ require 'greenhouse/commands/purge'
31
+ require 'greenhouse/commands/remove'
32
+ require 'greenhouse/commands/help'
@@ -0,0 +1,19 @@
1
+ module Greenhouse
2
+ module Projects
3
+ class Application < Project
4
+ attr_reader :dotenv, :procfile
5
+
6
+ def configured?
7
+ @dotenv.exists?
8
+ end
9
+
10
+ protected
11
+
12
+ def initialize(name, args={})
13
+ super
14
+ @procfile = Resources::Procfile.new("#{path}/Procfile")
15
+ @dotenv = Resources::DotenvFile.new("#{path}/.env")
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,23 @@
1
+ module Greenhouse
2
+ module Projects
3
+ class Collection < Array
4
+ protected
5
+
6
+ def initialize(*args)
7
+ super
8
+
9
+ # DEPRECATED - moving this to Projects itself
10
+ each do |project|
11
+ meth = project.class.name.pluralize.underscore.split("/").last.to_sym
12
+ next if respond_to?(meth)
13
+
14
+ self.class.instance_eval do
15
+ define_method meth do
16
+ select { |proj| proj.class.name.pluralize.underscore.split("/").last.to_sym == meth }
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,6 @@
1
+ module Greenhouse
2
+ module Projects
3
+ class Engine < Project
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ module Greenhouse
2
+ module Projects
3
+ class Gem < Project
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,77 @@
1
+ module Greenhouse
2
+ module Projects
3
+ class Project
4
+ attr_accessor :name, :repository, :title
5
+
6
+ class << self
7
+ attr_reader :subclasses
8
+
9
+ # Keep track of inheriting classes (to use as project "types")
10
+ def inherited(subclass)
11
+ (@subclasses ||= [self]) << subclass
12
+ end
13
+ end
14
+
15
+
16
+ def initialize(name, args={})
17
+ @name = name
18
+ @title = args.delete('title') || name.camelize
19
+ @ignored = (args.has_key?('ignore') ? [args.delete('ignore')] : []).flatten
20
+ @repository = Repository.new(name, args)
21
+ @ignore_file = Resources::IgnoreFile.new("#{path}/.ignore")
22
+ end
23
+
24
+ def ignored
25
+ Projects.ignored.concat(@ignore_file.ignored).concat(@ignored)
26
+ end
27
+
28
+ def chdir(&block)
29
+ Dir.chdir(path, &block)
30
+ end
31
+
32
+ # Return the local path to the project repo
33
+ def path
34
+ @repository.path
35
+ end
36
+
37
+ def gemfile
38
+ return nil unless gemfile?
39
+ "#{path}/Gemfile"
40
+ end
41
+
42
+ def gemfile?
43
+ chdir { return File.exists?("Gemfile") }
44
+ end
45
+
46
+ # Check if the repository exists
47
+ def exists?
48
+ @repository.exists?
49
+ end
50
+
51
+ def type
52
+ self.class.name.underscore.split('/').last
53
+ end
54
+
55
+ def to_arg
56
+ Scripts::Argument.new(name, :summary => "#{title} (#{type.capitalize})")
57
+ end
58
+
59
+ # Go into the local directory and run Bundler
60
+ def bundle(cmd='install')
61
+ raise "Directory does not exist: #{path}" unless exists?
62
+ Dir.chdir(path) do
63
+ Bundler.with_clean_env do
64
+ # TODO look into using Bundler to install instead of executing system cmd
65
+ Greenhouse::CLI::exec "bundle #{cmd.to_s} 2>&1"
66
+ end
67
+ end
68
+ end
69
+
70
+ # Remove the project directory
71
+ def destroy
72
+ @repository.destroy # use the repository object to destroy itself/directory
73
+ end
74
+
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,197 @@
1
+ module Greenhouse
2
+ module Projects
3
+ class Repository
4
+ attr_accessor :local, :remote
5
+ alias_method :path, :local
6
+
7
+ def method_missing(meth, *args)
8
+ return git.send(meth, *args) if git.respond_to?(meth)
9
+ super
10
+ end
11
+
12
+ # Clone the remote into the local path
13
+ def clone
14
+ raise "Repository already exists: #{@local}" if cloned?
15
+ Git.clone(@remote, @local.split("/").last)
16
+ end
17
+
18
+ # Check if the remote has been cloned locally
19
+ def cloned?
20
+ File.exists?(@local) && @git ||= Git.open(@local)
21
+ end
22
+ alias_method :exists?, :cloned?
23
+
24
+ # Check whether there are any uncommited local changes
25
+ def changes?(untracked=true)
26
+ raise "Repository does not exist: #{@local}" unless cloned?
27
+ !changes(untracked).empty?
28
+ end
29
+
30
+ # Get a list of all local changes (modified, added, deleted & untracked)
31
+ def changes(include_untracked=true)
32
+ raise "Repository does not exist: #{@local}" unless cloned?
33
+ changes = changed.merge(git.status.added).merge(git.status.deleted)
34
+ changes.merge!(untracked) if include_untracked
35
+ changes
36
+ end
37
+
38
+ def changed?
39
+ !changed.empty?
40
+ end
41
+
42
+ def changed
43
+ git.status.changed.select { |name,file| !git.diff('HEAD', '--').path(name).to_s.empty? }
44
+ end
45
+
46
+ def untracked
47
+ git.status.untracked.select { |name,file| !name.match(/\Atmp\/.*\Z/) } # temporary hack to avoid untracked tmp files, since they're not being properly ignored(?)
48
+ end
49
+
50
+ def untracked?
51
+ !untracked.empty?
52
+ end
53
+
54
+ def staged
55
+ changed.merge(git.status.added).merge(git.status.deleted).delete_if { |name,file| file.sha_index.empty? || file.sha_index == '0000000000000000000000000000000000000000' }
56
+ end
57
+
58
+ def staged?
59
+ !staged.empty?
60
+ end
61
+
62
+ def unstaged
63
+ changed.merge(untracked).select { |name,file| file.sha_index.empty? || file.sha_index == '0000000000000000000000000000000000000000' }
64
+ end
65
+
66
+ def unstaged?
67
+ !unstaged.empty?
68
+ end
69
+
70
+ # Return the results of `ls-files --others` to list ignored/untracked files
71
+ def others
72
+ git.chdir do
73
+ return `git ls-files --others`.split("\n")
74
+ end
75
+ end
76
+
77
+ # Check whether local branches are synced with the remotes
78
+ def synced?
79
+ unsynced.empty?
80
+ end
81
+
82
+ # Return any unsynced branches for all remotes
83
+ def unsynced
84
+ branches = []
85
+ git.branches.local.each do |branch|
86
+ git.remotes.each do |remote|
87
+ lcommit = git.object(branch.name).log.first
88
+ rcommit = git.object("#{remote.name}/#{branch.name}").log.first
89
+ next if lcommit.sha == rcommit.sha
90
+ branches << [branch, remote]
91
+ end
92
+ end
93
+ branches
94
+ end
95
+
96
+ # Check whether there are unpushed local changes on all branches/remotes
97
+ def ahead?
98
+ !ahead.empty?
99
+ end
100
+
101
+ # Return any unpushed local changes on all branches/remotes
102
+ def ahead
103
+ unsynced.select do |branch|
104
+ lbranch = git.object(branch[0].name)
105
+ rbranch = git.object("#{branch[1].name}/#{branch[0].name}")
106
+ lcommit = lbranch.log.first
107
+ rcommit = rbranch.log.first
108
+
109
+ !rbranch.log.map(&:sha).include?(lcommit.sha) && lbranch.log.map(&:sha).include?(rcommit.sha)
110
+
111
+ #lcommit.date <= rcommit.date
112
+ end
113
+ end
114
+
115
+ # Check if there are any unpulled changes on all branches/remotes
116
+ def behind?
117
+ !behind.empty?
118
+ end
119
+
120
+ # Return any unpulled changes on all branches/remotes
121
+ def behind
122
+ unsynced.select do |branch|
123
+ lbranch = git.object(branch[0].name)
124
+ rbranch = git.object("#{branch[1].name}/#{branch[0].name}")
125
+ lcommit = lbranch.log.first
126
+ rcommit = rbranch.log.first
127
+
128
+ rbranch.log.map(&:sha).include?(lcommit.sha) && !lbranch.log.map(&:sha).include?(rcommit.sha)
129
+
130
+ #lcommit.date >= rcommit.date
131
+ end
132
+ end
133
+
134
+ def diverged?
135
+ !diverged.empty?
136
+ end
137
+
138
+ def diverged
139
+ unsynced.select do |branch|
140
+ lbranch = git.object(branch[0].name)
141
+ rbranch = git.object("#{branch[1].name}/#{branch[0].name}")
142
+ lcommit = lbranch.log.first
143
+ rcommit = rbranch.log.first
144
+
145
+ !rbranch.log.map(&:sha).include?(lcommit.sha) && !lbranch.log.map(&:sha).include?(rcommit.sha)
146
+ end
147
+ end
148
+
149
+ def up_to_date?
150
+ !out_of_sync?
151
+ end
152
+
153
+ def out_of_sync?
154
+ behind? || diverged?
155
+ end
156
+
157
+ def out_of_sync
158
+ unsynced.select do |branch|
159
+ lbranch = git.object(branch[0].name)
160
+ rbranch = git.object("#{branch[1].name}/#{branch[0].name}")
161
+ lcommit = lbranch.log.first
162
+ rcommit = rbranch.log.first
163
+
164
+ (rbranch.log.map(&:sha).include?(lcommit.sha) && !lbranch.log.map(&:sha).include?(rcommit.sha)) ||
165
+ (!rbranch.log.map(&:sha).include?(lcommit.sha) && !lbranch.log.map(&:sha).include?(rcommit.sha))
166
+ end
167
+ end
168
+
169
+ def stash
170
+ git.chdir { `git stash 2>&1` }
171
+ end
172
+
173
+ def pop_stash
174
+ git.chdir { `git stash pop 2>&1` }
175
+ end
176
+
177
+ # Remove the local repository
178
+ def destroy
179
+ FileUtils.rm_rf @local
180
+ end
181
+
182
+ def git
183
+ @git ||= Git.open(@local)
184
+ @git
185
+ end
186
+
187
+ protected
188
+
189
+ def initialize(name, args={})
190
+ raise "A git remote is required." unless args.has_key?(:remote)
191
+ @local = File.expand_path(args[:local] || name)
192
+ @remote = args[:remote]# || "git@github.com:Graphicly/#{name}.git"
193
+ end
194
+
195
+ end
196
+ end
197
+ end
@@ -0,0 +1,86 @@
1
+ require 'yaml'
2
+ require 'git'
3
+ require 'greenhouse/projects/repository'
4
+ require 'greenhouse/projects/collection'
5
+ require 'greenhouse/projects/project'
6
+ require 'greenhouse/projects/engine'
7
+ require 'greenhouse/projects/application'
8
+ require 'greenhouse/projects/gem'
9
+
10
+ module Greenhouse
11
+ module Projects
12
+ @@path = nil
13
+ @@procfile = nil
14
+ @@ignore_file = nil
15
+ @@projects_file = nil
16
+ @@dotenv = nil
17
+ @@projects = nil
18
+
19
+ def self.method_missing(meth, *args)
20
+ if Project.subclasses.map { |subclass| subclass.name.pluralize.underscore.split("/").last.to_sym }.include?(meth.to_sym)
21
+ projects.select { |proj| proj.class.name.pluralize.underscore.split("/").last.to_sym == meth.to_sym }
22
+ else
23
+ super
24
+ end
25
+ end
26
+
27
+ def self.projects
28
+ @@projects = Collection.new
29
+ return @@projects unless projects_file.exists?
30
+
31
+ projects_file.projects.each do |name,project|
32
+ type = (project.has_key?('type') ? project['type'] : 'project')
33
+ projargs = project.merge({:remote => (project['remote'] || project['git'])})
34
+ classname = "Greenhouse::Projects::#{type.singularize.camelize}"
35
+ @@projects << (defined?(classname.constantize) ? classname.constantize.new(name, projargs) : Greenhouse::Projects::Project.new(name, projargs))
36
+ end
37
+ @@projects
38
+ end
39
+
40
+ # Attempts to look for and returns the path of the root projects directory
41
+ #
42
+ # Looks up the tree from the current directory, currently checking for a .projects
43
+ # file (this might change in the future).
44
+ #
45
+ # If no projects path is found, the current directory is returned.
46
+ def self.path
47
+ return @@path unless @@path.nil?
48
+ dir = Dir.pwd
49
+ while dir != "" do
50
+ if File.exists?("#{dir}/.projects")
51
+ @@path = dir
52
+ return @@path
53
+ end
54
+ dir = dir.gsub(/\/[^\/]*\Z/,'')
55
+ end
56
+ @@path = Dir.pwd # if we haven't found a .projects file, this must be where they want to work
57
+ @@path
58
+ end
59
+
60
+ def self.ignore_file
61
+ return @@ignore_file unless @@ignore_file.nil?
62
+ @@ignore_file = Resources::IgnoreFile.new("#{path}/.ignore")
63
+ end
64
+
65
+ def self.projects_file
66
+ return @@projects_file unless @@projects_file.nil?
67
+ @@projects_file = Resources::ProjectsFile.new("#{path}/.projects")
68
+ end
69
+
70
+ def self.procfile
71
+ return @@procfile unless @@procfile.nil?
72
+ @@procfile = Resources::Procfile.new("#{path}/Procfile")
73
+ end
74
+
75
+ def self.dotenv
76
+ return @@dotenv unless @@dotenv.nil?
77
+ @@dotenv = Resources::DotenvFile.new("#{path}/.env")
78
+ end
79
+
80
+ def self.ignored
81
+ return [] unless ignore_file.exists?
82
+ ignore_file.ignored
83
+ end
84
+
85
+ end
86
+ end