jigit 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +7 -0
  2. data/.gitattributes +1 -0
  3. data/.gitignore +60 -0
  4. data/.rspec +2 -0
  5. data/.rubocop.yml +117 -0
  6. data/.travis.yml +20 -0
  7. data/CHANGELOG.md +5 -0
  8. data/Dangerfile +22 -0
  9. data/Gemfile +7 -0
  10. data/Gemfile.lock +138 -0
  11. data/LICENSE +21 -0
  12. data/README.md +66 -0
  13. data/Rakefile +32 -0
  14. data/bin/jigit +5 -0
  15. data/jigit.gemspec +35 -0
  16. data/lib/jigit.rb +12 -0
  17. data/lib/jigit/commands/init.rb +309 -0
  18. data/lib/jigit/commands/issue.rb +56 -0
  19. data/lib/jigit/commands/runner.rb +22 -0
  20. data/lib/jigit/commands/start_issue.rb +58 -0
  21. data/lib/jigit/commands/stop_issue.rb +53 -0
  22. data/lib/jigit/core/jigitfile.rb +31 -0
  23. data/lib/jigit/core/jigitfile_constants.rb +15 -0
  24. data/lib/jigit/core/jigitfile_generator.rb +34 -0
  25. data/lib/jigit/git/git_hook.rb +11 -0
  26. data/lib/jigit/git/git_hook_installer.rb +60 -0
  27. data/lib/jigit/git/git_ignore_updater.rb +20 -0
  28. data/lib/jigit/git/post_checkout_hook.rb +23 -0
  29. data/lib/jigit/helpers/informator.rb +131 -0
  30. data/lib/jigit/helpers/keychain_storage.rb +19 -0
  31. data/lib/jigit/jira/jira_api_client.rb +80 -0
  32. data/lib/jigit/jira/jira_api_client_error.rb +10 -0
  33. data/lib/jigit/jira/jira_config.rb +16 -0
  34. data/lib/jigit/jira/jira_transition_finder.rb +16 -0
  35. data/lib/jigit/jira/resources/jira_issue.rb +34 -0
  36. data/lib/jigit/jira/resources/jira_status.rb +18 -0
  37. data/lib/jigit/jira/resources/jira_transition.rb +18 -0
  38. data/lib/jigit/version.rb +4 -0
  39. data/spec/fixtures/jigitfile_invalid.yaml +2 -0
  40. data/spec/fixtures/jigitfile_valid.yaml +5 -0
  41. data/spec/lib/integration/jigit/core/jigitfile_generator_spec.rb +27 -0
  42. data/spec/lib/integration/jigit/core/keychain_storage_spec.rb +35 -0
  43. data/spec/lib/integration/jigit/git/git_hook_installer_spec.rb +66 -0
  44. data/spec/lib/integration/jigit/git/git_ignore_updater_spec.rb +45 -0
  45. data/spec/lib/integration/jigit/jira/jira_api_client_spec.rb +154 -0
  46. data/spec/lib/unit/jigit/core/jigitfile_spec.rb +33 -0
  47. data/spec/lib/unit/jigit/git/post_checkout_hook_spec.rb +22 -0
  48. data/spec/lib/unit/jigit/git_hooks/post_checkout_hook_spec.rb +22 -0
  49. data/spec/lib/unit/jigit/jira/jira_config_spec.rb +23 -0
  50. data/spec/lib/unit/jigit/jira/jira_issue_spec.rb +101 -0
  51. data/spec/lib/unit/jigit/jira/jira_status_spec.rb +62 -0
  52. data/spec/lib/unit/jigit/jira/jira_transition_finder_spec.rb +70 -0
  53. data/spec/lib/unit/jigit/jira/jira_transition_spec.rb +64 -0
  54. data/spec/mock_responses/issue.json +1108 -0
  55. data/spec/mock_responses/issue_1002_transitions.json +49 -0
  56. data/spec/mock_responses/statuses.json +37 -0
  57. data/spec/spec_helper.rb +17 -0
  58. data/tasks/generate_jira_localhost.rake +20 -0
  59. metadata +293 -0
@@ -0,0 +1,32 @@
1
+ require "rubygems"
2
+ require "bundler/gem_tasks"
3
+ require "rubocop/rake_task"
4
+ require "rspec/core/rake_task"
5
+
6
+ Dir.glob("tasks/*.rake").each { |r| import r }
7
+
8
+ begin
9
+ RSpec::Core::RakeTask.new(:specs)
10
+ rescue LoadError
11
+ puts "Please use `bundle exec` to get all the rake commands"
12
+ end
13
+
14
+ task default: [:prepare, :spec, :rubocop]
15
+
16
+ desc "Prepare and run rspec tests"
17
+ task :prepare do
18
+ rsa_key = File.expand_path("rsakey.pem")
19
+ unless File.exist?(rsa_key)
20
+ raise "rsakey.pem does not exist, tests will fail. Run `r` first"
21
+ end
22
+ end
23
+
24
+ desc "Run jigit's spec tests"
25
+ RSpec::Core::RakeTask.new(:spec) do |task|
26
+ task.rspec_opts = ["--color", "--format", "doc"]
27
+ end
28
+
29
+ desc "Run RuboCop on the lib/specs directory"
30
+ RuboCop::RakeTask.new(:rubocop) do |task|
31
+ task.patterns = Dir.glob(["lib/**/*.rb", "spec/**/*.rb"]) - Dir.glob(["spec/fixtures/**/*"])
32
+ end
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ $LOAD_PATH.push File.expand_path("../../lib", __FILE__)
3
+
4
+ require "jigit"
5
+ Jigit::Runner.run ARGV
@@ -0,0 +1,35 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "jigit/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "jigit"
8
+ spec.version = Jigit::VERSION
9
+ spec.authors = ["Anton Domashnev"]
10
+ spec.email = ["antondomashnev@gmail.com"]
11
+ spec.description = Jigit::DESCRIPTION
12
+ spec.summary = "Keep the status of the JIRA issue always in sync with your local git"
13
+ spec.homepage = "https://github.com/Antondomashnev/jigit"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.required_ruby_version = ">= 2.0.0"
22
+
23
+ spec.add_runtime_dependency "ruby-keychain", "~> 0.3"
24
+ spec.add_runtime_dependency "claide", "~> 1.0"
25
+ spec.add_runtime_dependency "cork", "~> 0.1"
26
+ spec.add_runtime_dependency "jira-ruby", "~> 1.0"
27
+
28
+ spec.add_development_dependency "webmock", "~> 1.18", ">= 1.18.0"
29
+ spec.add_development_dependency "rubocop", "~> 0.42"
30
+ spec.add_development_dependency "bundler", "~> 1.3"
31
+ spec.add_development_dependency "rspec", "~> 3.0", ">= 3.0.0"
32
+ spec.add_development_dependency "simplecov-json", "~> 0.2"
33
+ spec.add_development_dependency "rake", "~> 10.3", ">= 10.3.2"
34
+ spec.add_development_dependency "simplecov", "~> 0.12.0"
35
+ end
@@ -0,0 +1,12 @@
1
+ require "jigit/version"
2
+ require "jigit/commands/runner"
3
+ require "jigit/commands/issue"
4
+ require "jigit/commands/start_issue"
5
+ require "jigit/commands/stop_issue"
6
+ require "jigit/commands/init"
7
+
8
+ require "claide"
9
+ require "cork"
10
+
11
+ module Jigit
12
+ end
@@ -0,0 +1,309 @@
1
+ require "yaml"
2
+ require "jigit/jira/jira_config"
3
+ require "jigit/jira/jira_api_client"
4
+ require "jigit/jira/resources/jira_status"
5
+ require "jigit/core/jigitfile_generator"
6
+ require "jigit/git/git_hook_installer"
7
+ require "jigit/git/git_ignore_updater"
8
+ require "jigit/git/post_checkout_hook"
9
+ require "jigit/helpers/keychain_storage"
10
+
11
+ module Jigit
12
+ # This class is heavily based on the Init command from the Danger gem
13
+ # The original link is https://github.com/danger/danger/blob/master/lib/danger/commands/init.rb
14
+ class Init < Runner
15
+ attr_accessor :current_jira_config
16
+
17
+ self.summary = "Helps you set up Jigit."
18
+ self.command = "init"
19
+ self.abstract_command = false
20
+
21
+ def self.options
22
+ [
23
+ ["--impatient", "'I've not got all day here. Don't add any thematic delays please.'"],
24
+ ["--mousey", "'Don't make me press return to continue the adventure.'"]
25
+ ].concat(super)
26
+ end
27
+
28
+ def initialize(argv)
29
+ super
30
+ ui.no_delay = argv.flag?("impatient", false)
31
+ ui.no_waiting = argv.flag?("mousey", false)
32
+ end
33
+
34
+ def run
35
+ ui.say "\nOK, thanks #{ENV['LOGNAME']}, have a seat and we'll get you started.\n".yellow
36
+ ui.pause 1
37
+
38
+ show_todo_state
39
+ ui.pause 1.4
40
+
41
+ return unless setup_access_to_jira
42
+ return unless setup_jigitfile
43
+ return unless setup_post_checkout_hook
44
+ return unless setup_gitignore
45
+
46
+ info
47
+ thanks
48
+ end
49
+
50
+ def show_todo_state
51
+ ui.say "We need to do the following:\n"
52
+ ui.pause 0.6
53
+ ui.say " - [ ] Set up an access to JIRA."
54
+ ui.pause 0.6
55
+ ui.say " - [ ] Set up a Jigit configuration file."
56
+ ui.pause 0.6
57
+ ui.say " - [ ] Set up a git hooks to automate the process."
58
+ ui.pause 0.6
59
+ ui.say " - [ ] Add private jigit's related things to .gitignore."
60
+ end
61
+
62
+ def ask_for_jira_account_email
63
+ ui.ask("What's is your JIRA's account email").strip
64
+ end
65
+
66
+ def ask_for_jira_account_password(email)
67
+ ui.ask("What's is the password for #{email}").strip
68
+ end
69
+
70
+ def ask_for_jira_host(polite)
71
+ return ui.ask("What's is the host for your JIRA server").strip unless polite
72
+
73
+ ui.say "\nThanks, and the last one is a bit tricky. Jigit needs the " + "host".green + " of your JIRA server.\n"
74
+ ui.pause 0.6
75
+ ui.say "The easiest way to get it is to go to your JIRA website and check the browser address field.\n"
76
+ ui.pause 0.6
77
+ ui.say "Usually it looks like " + "your_company_name.atlassian.net".green + ".\n"
78
+ ui.pause 0.6
79
+ ui.ask("What's is the host for your JIRA server").strip
80
+ end
81
+
82
+ def validate_jira_config?(config)
83
+ is_valid = Jigit::JiraAPIClient.new(config, nil).validate_api?
84
+ if is_valid
85
+ ui.inform "Hooray 🎉, everything is green.\n"
86
+ return true
87
+ else
88
+ ui.error "Yikes 😕\n"
89
+ ui.say "Let's try once again, you can do it 💪\n"
90
+ return false
91
+ end
92
+ rescue Jigit::JiraAPIClientError
93
+ ui.error "Yikes 😕\n"
94
+ ui.say "Let's try once again, you can do it 💪\n"
95
+ return false
96
+ end
97
+
98
+ def build_jira_config_politely(politely)
99
+ email = ask_for_jira_account_email
100
+ password = ask_for_jira_account_password(email)
101
+ host = ask_for_jira_host(politely)
102
+
103
+ ui.say "\nThanks, let's validate if the Jigit has access now...\n" if politely
104
+ config = Jigit::JiraConfig.new(email, password, host)
105
+ if validate_jira_config?(config)
106
+ config
107
+ else
108
+ build_jira_config_politely(false)
109
+ end
110
+ rescue Jigit::NetworkError => exception
111
+ ui.error "Error while validating access to JIRA API: #{exception.message}"
112
+ return nil
113
+ end
114
+
115
+ def setup_access_to_jira
116
+ ui.header "\nStep 1: Setting up an access to JIRA"
117
+ ui.pause 0.6
118
+ ui.say "In order to Jigit to be able to help you, it needs access to your JIRA account.\n"
119
+ ui.pause 0.6
120
+ ui.say "But don't worry it'll store it in a safe place.\n"
121
+ ui.pause 1
122
+
123
+ self.current_jira_config = build_jira_config_politely(true)
124
+ if self.current_jira_config
125
+ keychain_storage = Jigit::KeychainStorage.new
126
+ keychain_storage.save(self.current_jira_config.user, self.current_jira_config.password, self.current_jira_config.host)
127
+ ui.say "Let's move to next step, press return when ready..."
128
+ ui.wait_for_return
129
+ return true
130
+ else
131
+ return false
132
+ end
133
+ end
134
+
135
+ def fetch_jira_status_names
136
+ ui.say "Fetching all possible statuses from JIRA...\n"
137
+ jira_api_client = Jigit::JiraAPIClient.new(self.current_jira_config, nil)
138
+ begin
139
+ all_statuses = jira_api_client.fetch_jira_statuses
140
+ if all_statuses.nil? || all_statuses.count.zero?
141
+ ui.error "Yikes 😕\n"
142
+ ui.say "Jigit can not find any statuses for JIRA issue in your company setup.\n"
143
+ return nil
144
+ else
145
+ all_statuses.map(&:name)
146
+ end
147
+ rescue Jigit::JiraAPIClientError => exception
148
+ ui.error "Error while fetching statuses from JIRA API: #{exception.message}"
149
+ return false
150
+ rescue Jigit::NetworkError => exception
151
+ ui.error "Error while fetching statuses from JIRA API: #{exception.message}"
152
+ return false
153
+ end
154
+ end
155
+
156
+ def handle_nicely_setup_jigitfile_failure
157
+ ui.say "Unfortunately, Jigit can not proceed without that information.\n"
158
+ ui.pause 0.6
159
+ ui.say "Try to check the JIRA setup and your internet connection status.\n"
160
+ ui.pause 0.6
161
+ ui.say "If everything looks fine, try to init Jigit once egain: `bundle exec jigit init`"
162
+ end
163
+
164
+ def ask_for_in_progress_status_name(status_names)
165
+ in_progress_status_name = ui.ask_with_answers("What status do you set when work on the JIRA issue\n", status_names)
166
+ in_progress_status_name
167
+ end
168
+
169
+ def ask_for_other_status_names(status_names)
170
+ not_asked_status_names = status_names
171
+ selected_status_names = []
172
+ ui.say "Now Jigit needs to know, what status could you set when stop working on the issue.\n"
173
+ ui.pause 0.6
174
+ ui.say "We know you can have multiple, don't worry and"
175
+ ui.pause 0.6
176
+ ui.say "when you're done select 'nothing' option.\n"
177
+ ui.pause 1
178
+
179
+ selected_status_name = nil
180
+ loop do
181
+ selected_status_names << selected_status_name unless selected_status_name.nil?
182
+ break if not_asked_status_names.count.zero?
183
+ selected_status_name = ui.ask_with_answers("Which one you want to select", not_asked_status_names + ["nothing"])
184
+ break if selected_status_name == "nothing"
185
+ ui.say selected_status_name
186
+ not_asked_status_names.delete(selected_status_name)
187
+ end
188
+ return selected_status_names
189
+ end
190
+
191
+ def setup_jigitfile_into
192
+ ui.header "Step 2: Setting up a Jigit configuration file"
193
+ ui.say "In order to Jigit to be able to help you it needs to know something about your usual workflow.\n"
194
+ ui.pause 1
195
+ end
196
+
197
+ def setup_jigitfile_outro
198
+ ui.say "And the jigitfile is ready 🎉.\n"
199
+ ui.say "You can find it at './.jigit/Jigitfile.yml'"
200
+ ui.pause 0.6
201
+ ui.say "Let's move to next step, press return when ready..."
202
+ ui.wait_for_return
203
+ end
204
+
205
+ def setup_jigitfile_with_user_input
206
+ jigitfile_generator = Jigit::JigitfileGenerator.new
207
+ jigitfile_generator.write_jira_host(self.current_jira_config.host)
208
+ ui.pause 0.6
209
+
210
+ in_progress_status_name = ask_for_in_progress_status_name(jira_status_names)
211
+ jigitfile_generator.write_in_progress_status_name(in_progress_status_name)
212
+ ui.pause 0.6
213
+
214
+ selected_status_names = ask_for_other_status_names(jira_status_names)
215
+ jigitfile_generator.write_other_statuses(selected_status_names)
216
+ ui.pause 0.6
217
+
218
+ jigitfile_generator.save
219
+ end
220
+
221
+ def setup_jigitfile
222
+ setup_jigitfile_into
223
+
224
+ jira_status_names = fetch_jira_status_names
225
+ unless jira_status_names
226
+ handle_nicely_setup_jigitfile_failure
227
+ return false
228
+ end
229
+ ui.pause 0.6
230
+
231
+ setup_jigitfile_with_user_input
232
+ setup_jigitfile_outro
233
+
234
+ return true
235
+ end
236
+
237
+ def setup_post_checkout_hook_intro
238
+ ui.header "Step 3: Setting up a git hooks to automate the process."
239
+ ui.say "Jigit is going to create a post-checkout git hook."
240
+ ui.pause 0.6
241
+ ui.say "It will the 'git checkout' command and if it's a checkout to a branch."
242
+ ui.pause 0.6
243
+ ui.say "Jigit will ask it needs to put the new branch's related issue In Progress"
244
+ ui.pause 0.6
245
+ ui.say "and to update status for the old branch on JIRA"
246
+ end
247
+
248
+ def setup_post_checkout_hook_outro
249
+ ui.say "And the git hook is ready 🎉.\n"
250
+ ui.say "You can find it at './.git/hooks/post-checkout'"
251
+ ui.pause 0.6
252
+ ui.say "One last step and we're done, press return to continue..."
253
+ ui.wait_for_return
254
+ end
255
+
256
+ def setup_post_checkout_hook
257
+ setup_post_checkout_hook_intro
258
+
259
+ git_hook_installer = Jigit::GitHookInstaller.new
260
+ post_checkout_hook = Jigit::PostCheckoutHook
261
+ git_hook_installer.install(post_checkout_hook)
262
+
263
+ setup_post_checkout_hook_outro
264
+
265
+ return true
266
+ end
267
+
268
+ def setup_gitignore_intro
269
+ ui.header "Step 4: Adding private jigit's related things to .gitignore."
270
+ ui.say "Jigit has been setup for your personal usage with your personal info"
271
+ ui.pause 0.6
272
+ ui.say "therefore it can not be really used accross the team, so we need to git ignore the related files."
273
+ ui.pause 0.6
274
+ end
275
+
276
+ def setup_gitignore_outro
277
+ ui.say "And the git ignore now ignores your .jigit folder 🎉.\n"
278
+ ui.pause 0.6
279
+ ui.say "That's all to finish initialization press return"
280
+ ui.wait_for_return
281
+ end
282
+
283
+ def setup_gitignore
284
+ setup_gitignore_intro
285
+
286
+ git_hook_installer = Jigit::GitIgnoreUpdater.new
287
+ git_hook_installer.ignore(".jigit")
288
+
289
+ setup_gitignore_outro
290
+
291
+ return true
292
+ end
293
+
294
+ def info
295
+ ui.header "Useful info"
296
+ ui.say "- This project is at it's early stage and may be unstable"
297
+ ui.pause 0.6
298
+ ui.say "- If you find any bug or want to add something, you're very welcome to the repo:"
299
+ ui.link "https://github.com/Antondomashnev/jigit"
300
+ ui.pause 0.6
301
+ ui.say "- If you want to know more, follow " + "@antondomashnev".green + " on Twitter"
302
+ ui.pause 1
303
+ end
304
+
305
+ def thanks
306
+ ui.say "\n\nHave a happy coding 🎉"
307
+ end
308
+ end
309
+ end
@@ -0,0 +1,56 @@
1
+ require "jigit/commands/runner"
2
+ require "jigit/jira/jira_api_client"
3
+ require "jigit/jira/jira_config"
4
+ require "jigit/core/jigitfile"
5
+ require "jigit/helpers/keychain_storage"
6
+
7
+ module Jigit
8
+ class IssueRunner < Runner
9
+ self.abstract_command = true
10
+ self.summary = "Abstract command for commands related to JIRA issue"
11
+ self.command = "issue"
12
+
13
+ def initialize(argv)
14
+ super
15
+ jigitfile = argv.option("jigitfile")
16
+ unless jigitfile
17
+ jigitfile = try_to_find_jigitfile_path
18
+ raise "--jigitfile is a required parameter and could not be nil" if jigitfile.nil?
19
+ end
20
+ @jigitfile = Jigit::Jigitfile.new(jigitfile)
21
+ @issue_name = argv.option("name")
22
+ keychain_item = Jigit::KeychainStorage.new.load_item(@jigitfile.host)
23
+ @jira_config = Jigit::JiraConfig.new(keychain_item.account, keychain_item.password, keychain_item.service)
24
+ @jira_api_client = Jigit::JiraAPIClient.new(@jira_config, nil) if @jira_config
25
+ end
26
+
27
+ def validate!
28
+ super
29
+ help!("Please setup jira config using `jigit init` before using issue command.") unless @jira_config
30
+ help!("Please setup jigitfile using `jigit init` before using issue command.") unless @jigitfile
31
+ end
32
+
33
+ def self.options
34
+ [
35
+ ["--name=issue_name_on_jira", "Use this argument to provide a JIRA issue name. For example if the project short name is CNI, the issue name could be CNI-101"],
36
+ ["--jigitfile=path_to_jigit_file", "Use this argument to provide a path to Jigitfile, if nil will be used a default path under the './jigit/' folder"]
37
+ ].concat(super)
38
+ end
39
+
40
+ def run
41
+ self
42
+ end
43
+
44
+ private
45
+
46
+ def try_to_find_jigitfile_path
47
+ pwd_jigitfile_yaml = Pathname.pwd + ".jigit/Jigitfile.yaml"
48
+ jigitfile = pwd_jigitfile_yaml if File.exist?(pwd_jigitfile_yaml)
49
+ return jigitfile unless jigitfile.nil?
50
+ pwd_jigitfile_yml = Pathname.pwd + ".jigit/Jigitfile.yml"
51
+ jigitfile = pwd_jigitfile_yml if File.exist?(pwd_jigitfile_yml)
52
+ return jigitfile unless jigitfile.nil?
53
+ return nil
54
+ end
55
+ end
56
+ end