citizen-scripts 1.1.0

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.
@@ -0,0 +1,195 @@
1
+ require 'json'
2
+ require 'tempfile'
3
+
4
+ module CitizenCodeScripts
5
+ class Pivotal
6
+ attr_reader :token
7
+
8
+ def initialize(token)
9
+ @token = token
10
+ end
11
+
12
+ def request(url, method: "GET", body: nil)
13
+ cmd = %Q{curl --silent -X #{method} -H "X-TrackerToken: #{token}"}
14
+ cmd += %Q{ -H "Content-Type: application/json"}
15
+
16
+ if body
17
+ cmd += %Q{ -d '#{body}'}
18
+ end
19
+
20
+ cmd += %Q{ "#{url}"}
21
+
22
+ result = `#{cmd}`
23
+
24
+ JSON.parse(result.strip)
25
+ end
26
+ end
27
+
28
+ class Begin < Base
29
+ def self.description
30
+ "Starts your Pivotal Tracker story and opens a new PR on Github"
31
+ end
32
+
33
+ def self.help
34
+ <<-EOF
35
+ citizen begin #{colorize(:light_blue, "[story id] [branch name]")}
36
+
37
+ Example: $ #{colorize(:light_blue, 'citizen begin "#133717234"')}
38
+
39
+ This command will start your Pivotal Tracker story for you, open a pull
40
+ request on Github, and copy over the Pivotal Tracker story description to
41
+ the Github pull request description. As well, any tasks in your
42
+ Pivotal Tracker story will automatically become [x] checkbox tasks on the
43
+ Github PR.
44
+
45
+ * All branch names will be auto-converted to
46
+ kebab-case, lowercase
47
+
48
+ * Passing story id/branch name as arguments are optional - if
49
+ they are missing, you'll be prompted
50
+ EOF
51
+ end
52
+
53
+ def run
54
+ story_id, branch_name = argv
55
+
56
+ if !command?("hub")
57
+ abort <<-EOF
58
+ You need to install `hub` before you can use this program.
59
+
60
+ brew install hub
61
+ EOF
62
+ end
63
+
64
+ pivotal = Pivotal.new(git_config("user.pivotalApiToken"))
65
+ config_abort_if_blank!("user.pivotalApiToken", pivotal.token)
66
+
67
+ project_id = git_config("user.pivotalProjectId")
68
+ config_abort_if_blank!("user.pivotalProjectId", project_id)
69
+
70
+ story_id ||= prompt("Please paste the Pivotal story ID here")
71
+ story_id = story_id.gsub(/[^\d]/, '')
72
+
73
+ story_url = "https://www.pivotaltracker.com/services/v5"\
74
+ "/projects/#{project_id}/stories/#{story_id}"
75
+
76
+ story = pivotal.request(story_url)
77
+
78
+ default_branch_name = normalized_branch_name(story['name'])
79
+
80
+ branch_name ||= prompt("Please enter a branch name (#{default_branch_name})")
81
+
82
+ if branch_name.strip == ""
83
+ branch_name = default_branch_name
84
+ else
85
+ branch_name = normalized_branch_name(branch_name)
86
+ end
87
+
88
+ # Start the story
89
+ pivotal.request(story_url, method: "PUT", body: '{ "current_state":"started" }')
90
+
91
+ silent "git checkout master",
92
+ "git fetch origin",
93
+ "git reset --hard origin/master"
94
+
95
+ puts "==> Checking out #{branch_name}"
96
+
97
+ silent "git checkout -b #{branch_name}",
98
+ 'git commit --allow-empty -m "Initial commit for story #' + story_id + '"',
99
+ "git push origin #{branch_name}",
100
+ "git branch --set-upstream #{branch_name} origin/#{branch_name}"
101
+
102
+ tasks = pivotal.request(
103
+ "https://www.pivotaltracker.com/services/v5"\
104
+ "/projects/#{project_id}/stories/#{story_id}/tasks"
105
+ )
106
+
107
+ story_description = story['description']
108
+
109
+ if story_description.nil?
110
+ story_description = "_No story description given in Pivotal_"
111
+ end
112
+
113
+ description = <<-EOF
114
+ #{story['name']}
115
+
116
+ #{story['url']}
117
+
118
+ # Description
119
+
120
+ #{story_description}
121
+ EOF
122
+
123
+ description.strip!
124
+
125
+ unless tasks.empty?
126
+ description += "\n\n## TODO\n\n"
127
+
128
+ tasks.each do |task|
129
+ description += "- [ ] #{task['description']}\n"
130
+ end
131
+
132
+ description.strip!
133
+ end
134
+
135
+ puts "==> Opening pull request on GitHub"
136
+
137
+ tempfile = Tempfile.new('begin_pull_request')
138
+
139
+ begin
140
+ tempfile.write(description)
141
+ tempfile.close
142
+
143
+ labels = ['WIP', story['story_type']].join(',')
144
+
145
+ url = `hub pull-request -F #{tempfile.path} -l "#{labels}" -a "" -o`
146
+
147
+ # Copy it to your clipboard
148
+ system("echo #{url} | pbcopy")
149
+ puts url
150
+ ensure
151
+ tempfile.unlink
152
+ end
153
+ end
154
+
155
+ private
156
+
157
+ def silent(*cmds)
158
+ cmds.each { |cmd| system("#{cmd} >/dev/null 2>&1") }
159
+ end
160
+
161
+ def command?(name)
162
+ `which #{name}`
163
+ $?.success?
164
+ end
165
+
166
+ def git_config(key)
167
+ `git config --local --get #{key}`.strip
168
+ end
169
+
170
+ def config_abort_if_blank!(key, value)
171
+ if value.strip == ""
172
+ abort <<-EOF
173
+ You need to set the #{key} value in your git config!
174
+
175
+ git config --local --add #{key} [value]
176
+ EOF
177
+ end
178
+ end
179
+
180
+ def prompt(msg)
181
+ print "#{msg} > "
182
+ value = STDIN.gets.strip
183
+ puts
184
+ value
185
+ end
186
+
187
+ def normalized_branch_name(branch_name)
188
+ branch_name
189
+ .gsub(/[^\w\s-]/, '')
190
+ .gsub(/\s+/, '-')
191
+ .downcase
192
+ .gsub(/-*$/, '') # trailing dashes
193
+ end
194
+ end
195
+ end
@@ -0,0 +1,39 @@
1
+ module CitizenCodeScripts::Colorize
2
+ extend self
3
+
4
+ def self.included(base)
5
+ colorize = self
6
+
7
+ base.class_eval do
8
+ extend colorize
9
+ end
10
+ end
11
+
12
+ COLOR_CODES = {
13
+ black: 30,
14
+ blue: 34,
15
+ brown: 33,
16
+ cyan: 36,
17
+ dark_gray: 90,
18
+ green: 32,
19
+ light_blue: 94,
20
+ light_cyan: 96,
21
+ light_gray: 37,
22
+ light_green: 92,
23
+ light_purple: 95,
24
+ light_red: 91,
25
+ light_yellow: 93,
26
+ purple: 35,
27
+ red: 31,
28
+ white: 97,
29
+ yellow: 33,
30
+
31
+ command: 96,
32
+ error: 91,
33
+ info: 93,
34
+ }
35
+
36
+ def colorize(color, string)
37
+ "\e[#{COLOR_CODES[color]}m#{string}\e[0m"
38
+ end
39
+ end
@@ -0,0 +1,179 @@
1
+ class CitizenCodeScripts::Doctor < CitizenCodeScripts::Base
2
+ class Check
3
+ include CitizenCodeScripts::Colorize
4
+
5
+ attr_reader :name, :command, :remedy, :problems
6
+
7
+ def initialize(name:, command:, remedy:)
8
+ @name = name
9
+ @command = command
10
+ @remedy = remedy
11
+ @problems = []
12
+ end
13
+
14
+ def run!
15
+ print "Checking: #{name}... "
16
+
17
+ success = if command.respond_to?(:call)
18
+ command.call
19
+ else
20
+ system "#{command} > /dev/null 2>&1"
21
+ end
22
+
23
+ if success
24
+ puts 'OK'
25
+ else
26
+ print colorize(:error, 'F')
27
+ fix = remedy.respond_to?(:join) ? remedy.join(" ") : remedy
28
+ puts "\n To fix: #{fix}\n\n"
29
+
30
+ problems << name
31
+ end
32
+ end
33
+ end
34
+
35
+ def self.description
36
+ "Checks the health of your development environment"
37
+ end
38
+
39
+ def initialize(*args)
40
+ super
41
+ @checks = []
42
+ end
43
+
44
+ def run
45
+ case argv.first
46
+ when "list"
47
+ list_default_checks
48
+ else
49
+ run_doctor
50
+ end
51
+ end
52
+
53
+ def self.help
54
+ "doctor - helps you diagnose any setup issues with this application\n"
55
+ end
56
+
57
+ def self.help_subcommands
58
+ {
59
+ "citizen doctor" => "runs health checks and gives a report",
60
+ "citizen doctor list" => "prints a list of default checks you can use when overriding doctor checks in your app"
61
+ }
62
+ end
63
+
64
+ def run_doctor
65
+ run_checks
66
+ report
67
+ end
68
+
69
+ def list_default_checks
70
+ puts "These default checks are available for use in your overrides:"
71
+ puts
72
+
73
+ default_checks.each do |name|
74
+ puts " - #{colorize(:light_blue, name)}"
75
+ end
76
+
77
+ puts
78
+ end
79
+
80
+ def check(**options)
81
+ check = Check.new(options)
82
+ @checks << check
83
+
84
+ check.run!
85
+ end
86
+
87
+ private
88
+
89
+ def run_checks
90
+ run_default_checks
91
+ end
92
+
93
+ def run_default_checks
94
+ default_checks.each do |check|
95
+ send(check)
96
+ end
97
+ end
98
+
99
+ def default_checks
100
+ %i[
101
+ check_postgres_launchctl
102
+ check_postgres_running
103
+ check_postgres_role
104
+ check_db_migrated
105
+ check_direnv_installed
106
+ check_phantomjs_installed
107
+ check_gemfile_dependencies
108
+ check_envrc_file_exists
109
+ ]
110
+ end
111
+
112
+ def check_postgres_launchctl
113
+ check \
114
+ name: "postgres launchctl script is linked",
115
+ command: "ls -1 ~/Library/LaunchAgents/homebrew.mxcl.postgresql.plist",
116
+ remedy: command("ln -sfv /usr/local/opt/postgresql/*.plist ~/Library/LaunchAgents")
117
+ end
118
+
119
+ def check_postgres_running
120
+ check \
121
+ name: "postgres is running",
122
+ command: "psql -l",
123
+ remedy: command("launchctl load ~/Library/LaunchAgents/homebrew.mxcl.postgresql.plist")
124
+ end
125
+
126
+ def check_postgres_role
127
+ check \
128
+ name: "postgres role exists",
129
+ command: "psql -U postgres -l",
130
+ remedy: command("createuser --superuser postgres")
131
+ end
132
+
133
+ def check_gemfile_dependencies
134
+ check \
135
+ name: "Gemfile dependencies are up to date",
136
+ command: "bundle check",
137
+ remedy: command("bundle")
138
+ end
139
+
140
+ def check_db_migrated
141
+ check \
142
+ name: "DB is migrated",
143
+ command: "source .envrc && rails runner 'ActiveRecord::Migration.check_pending!'",
144
+ remedy: command("rake db:migrate")
145
+ end
146
+
147
+ def check_direnv_installed
148
+ check \
149
+ name: "direnv installed",
150
+ command: "which direnv",
151
+ remedy: command("brew install direnv")
152
+ end
153
+
154
+ def check_phantomjs_installed
155
+ check \
156
+ name: "PhantomJS installed",
157
+ command: "which phantomjs",
158
+ remedy: command("brew install phantomjs")
159
+ end
160
+
161
+ def check_envrc_file_exists
162
+ check \
163
+ name: "envrc",
164
+ command: "stat .envrc",
165
+ remedy: "get your .envrc file from 1password"
166
+ end
167
+
168
+ def problems
169
+ @checks.map(&:problems).flatten
170
+ end
171
+
172
+ def report
173
+ exit problems.size
174
+ end
175
+
176
+ def command(s)
177
+ "run #{colorize :command, s}"
178
+ end
179
+ end
@@ -0,0 +1,53 @@
1
+ class CitizenCodeScripts::Help < CitizenCodeScripts::Base
2
+ def self.help
3
+ "Inception was a lame movie"
4
+ end
5
+
6
+ def self.help_subcommands
7
+ {}
8
+ end
9
+
10
+ def self.description
11
+ "Prints this message out"
12
+ end
13
+
14
+ def run
15
+ specific_script = argv[0]
16
+
17
+ if CitizenCodeScripts::Base.script_names.include?(specific_script)
18
+ script = CitizenCodeScripts::Base.scripts[specific_script]
19
+ puts full_help(script)
20
+ elsif specific_script
21
+ puts colorize(:red, "\"#{specific_script}\" does not exist, cannot display help")
22
+ else
23
+ basic_usage
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def full_help(script)
30
+ (script.help || "") + script.help_subcommands.map do |cmd, description|
31
+ " - #{colorize(:light_blue, cmd)} - #{description}"
32
+ end.join("\n")
33
+ end
34
+
35
+ def basic_usage
36
+ puts "Usage: citizen #{colorize(:light_blue, '[script name]')}"
37
+ puts
38
+ puts "Specify a specific script to run, options are: "
39
+ puts
40
+
41
+ names_and_descriptions = CitizenCodeScripts::Base.scripts.map do |name, script|
42
+ [colorize(:light_green, name), colorize(:light_blue, script.description)]
43
+ end
44
+
45
+ padding = names_and_descriptions.map { |name, _| name.length }.max
46
+
47
+ names_and_descriptions.each do |name, description|
48
+ puts " %-#{padding}s %s" % [name, description]
49
+ end
50
+
51
+ puts
52
+ end
53
+ end
@@ -0,0 +1,58 @@
1
+ require_relative './doctor'
2
+
3
+ class HerokuDoctor < CitizenCodeScripts::Doctor
4
+ def self.description
5
+ "Checks the health of your Heroku config"
6
+ end
7
+
8
+ def self.help
9
+ <<~TEXT
10
+ citizen heroku-doctor #{colorize(:light_blue, "environment")} = #{colorize(:yellow, "staging")}
11
+ TEXT
12
+ end
13
+
14
+ def run_checks
15
+ @env = argv[0] || "staging"
16
+ @heroku_app_name = app_names[@env.to_s]
17
+
18
+ puts "Environment: #{@env}"
19
+ puts "Heroku app: #{@heroku_app_name}"
20
+ puts
21
+
22
+ # this one should always be first - it will NEVER pass for the citizen-rails project which is OKAY!
23
+ check(
24
+ name: "The citizen.yml file has been configured properly",
25
+ command: "! grep 'citizen-rails-staging' citizen.yml",
26
+ remedy: "configure your citizen.yml file to have the correct app names set for all your Heroku environments"
27
+ )
28
+
29
+ check(
30
+ name: "app #{@heroku_app_name} exists",
31
+ command: "cat .git/config | grep git@heroku.com:#{@heroku_app_name}.git",
32
+ remedy: [command("heroku apps:create #{@heroku_app_name}"), "and/or", command("git remote add staging git@heroku.com:#{@heroku_app_name}.git")]
33
+ )
34
+
35
+ check_env("DEPLOY_TASKS", "db:migrate")
36
+ check_env("RAILS_ENV", "production")
37
+ check_env("DATABASE_URL", "postgres://", "go to https://dashboard.heroku.com/apps/#{@heroku_app_name}/resources and add the Heroku Postgress add-on")
38
+
39
+ check_buildpack("https://github.com/heroku/heroku-buildpack-ruby")
40
+ check_buildpack("https://github.com/gunpowderlabs/buildpack-ruby-rake-deploy-tasks")
41
+ end
42
+
43
+ def check_env(env_var, value, remedy=nil)
44
+ check(
45
+ name: env_var,
46
+ command: "heroku config:get #{env_var} -a #{@heroku_app_name} | grep '#{value}'",
47
+ remedy: remedy || command("heroku config:set #{env_var}=#{value} -a #{@heroku_app_name}")
48
+ )
49
+ end
50
+
51
+ def check_buildpack(url)
52
+ check(
53
+ name: url.split("/").last,
54
+ command: "heroku buildpacks -a #{@heroku_app_name} | grep '#{url}'",
55
+ remedy: command("heroku buildpacks:add #{url} -a #{@heroku_app_name}")
56
+ )
57
+ end
58
+ end
@@ -0,0 +1,22 @@
1
+ class CitizenCodeScripts::KillDbSessions < CitizenCodeScripts::Base
2
+ def self.description
3
+ "Kills active Postgres sessions"
4
+ end
5
+
6
+ def run
7
+ print "Loading Rails... "
8
+ require app_root.join("./config/environment")
9
+
10
+ puts "done"
11
+
12
+ print "Killing DB sessions... "
13
+ ActiveRecord::Base.connection.execute(<<-SQL)
14
+ SELECT pg_terminate_backend(pg_stat_activity.pid)
15
+ FROM pg_stat_activity
16
+ WHERE datname = current_database()
17
+ AND pid <> pg_backend_pid()
18
+ SQL
19
+
20
+ puts "done"
21
+ end
22
+ end
@@ -0,0 +1,40 @@
1
+ class Levenstein
2
+ def self.edit_distance(s, t)
3
+ m = s.length
4
+ n = t.length
5
+ return m if n == 0
6
+ return n if m == 0
7
+ d = Array.new(m+1) { Array.new(n+1) }
8
+
9
+ (0..m).each { |i| d[i][0] = i }
10
+ (0..n).each { |j| d[0][j] = j }
11
+ (1..n).each do |j|
12
+ (1..m).each do |i|
13
+ d[i][j] = if s[i-1] == t[j-1] # adjust index into string
14
+ d[i-1][j-1] # no operation required
15
+ else
16
+ [d[i-1][j]+1, # deletion
17
+ d[i][j-1]+1, # insertion
18
+ d[i-1][j-1]+1, # substitution
19
+ ].min
20
+ end
21
+ end
22
+ end
23
+ d[m][n]
24
+ end
25
+
26
+ def self.closest_match(needle, haystack)
27
+ min_distance = haystack.map(&:size).max
28
+ results = nil
29
+ haystack.each do |value|
30
+ distance = edit_distance(needle, value)
31
+ if distance < min_distance
32
+ min_distance = distance
33
+ results = [value]
34
+ elsif distance == min_distance
35
+ results << value
36
+ end
37
+ end
38
+ results
39
+ end
40
+ end
@@ -0,0 +1,23 @@
1
+ class CitizenCodeScripts::Pushit < CitizenCodeScripts::Base
2
+ def self.description
3
+ "Pulls code, runs test, pushes your code"
4
+ end
5
+
6
+ def self.help
7
+ <<-EOF
8
+ citizen pushit
9
+
10
+ Pulls the latest code, restarts, runs the tests, and pushes
11
+ your new code up.
12
+ EOF
13
+ end
14
+
15
+ def run
16
+ CitizenCodeScripts::Update.run
17
+ CitizenCodeScripts::Test.run
18
+
19
+ step "Pushing" do
20
+ system('git push')
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,19 @@
1
+ class CitizenCodeScripts::Rspec < CitizenCodeScripts::Base
2
+ def self.description
3
+ "Runs your RSpec suite"
4
+ end
5
+
6
+ def run
7
+ begin
8
+ load(File.expand_path("./spring", __FILE__))
9
+ rescue LoadError
10
+ end
11
+
12
+ require 'bundler/setup'
13
+
14
+ step "Running RSpec" do
15
+ command = [Gem.bin_path('rspec-core', 'rspec')] + argv
16
+ system!(command.join(" "))
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,17 @@
1
+ class CitizenCodeScripts::Test < CitizenCodeScripts::Base
2
+ def self.description
3
+ "Runs all test suites for CI/pushit"
4
+ end
5
+
6
+ def run
7
+ step "Running test suite" do
8
+ rspec
9
+ end
10
+ end
11
+
12
+ private
13
+
14
+ def rspec
15
+ CitizenCodeScripts::Rspec.run
16
+ end
17
+ end
@@ -0,0 +1,29 @@
1
+ class CitizenCodeScripts::TodayI < CitizenCodeScripts::Base
2
+ def self.help
3
+ <<-EOF
4
+ citizen today-i
5
+
6
+ Prints out a list of commit message names that you worked on today.
7
+ EOF
8
+ end
9
+
10
+ def self.description
11
+ "Prints a list of commit msgs from today"
12
+ end
13
+
14
+ def run
15
+ date_string = Time.now.to_s.split(' ')[0]
16
+
17
+ lines = %x{
18
+ git log \
19
+ --date=local \
20
+ --oneline \
21
+ --after="#{date_string} 00:00" \
22
+ --before="#{date_string} 23:59"
23
+ }
24
+
25
+ lines.each_line do |line|
26
+ puts line.split(" ", 2)[1]
27
+ end
28
+ end
29
+ end