jigit 1.0.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.
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