greenhouse 0.0.3

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