hustle_and_flow 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/bin/hustle +5 -0
  3. data/lib/hustle_and_flow/binary/runner.rb +35 -0
  4. data/lib/hustle_and_flow/commands/start.rb +36 -0
  5. data/lib/hustle_and_flow/errors/unknown_issue_action_error.rb +6 -0
  6. data/lib/hustle_and_flow/formatters/branch_table_formatter.rb +81 -0
  7. data/lib/hustle_and_flow/formatters/issue_detail_formatter.rb +74 -0
  8. data/lib/hustle_and_flow/formatters/issue_table_formatter.rb +80 -0
  9. data/lib/hustle_and_flow/io/shell.rb +102 -0
  10. data/lib/hustle_and_flow/issue_tracker.rb +18 -0
  11. data/lib/hustle_and_flow/issue_trackers/github/issue.rb +192 -0
  12. data/lib/hustle_and_flow/issue_trackers/github/issues.rb +89 -0
  13. data/lib/hustle_and_flow/issue_trackers/github.rb +71 -0
  14. data/lib/hustle_and_flow/utils/string.rb +25 -0
  15. data/lib/hustle_and_flow/utils/url_slug.rb +28 -0
  16. data/lib/hustle_and_flow/vcs_repository.rb +19 -0
  17. data/lib/hustle_and_flow/version.rb +1 -1
  18. data/lib/hustle_and_flow/version_control/git/branch.rb +136 -0
  19. data/lib/hustle_and_flow/version_control/git/branches.rb +93 -0
  20. data/lib/hustle_and_flow/version_control/git/repository.rb +99 -0
  21. data/lib/hustle_and_flow.rb +1 -2
  22. data/spec/lib/hustle_and_flow/formatters/branch_table_formatter_spec.rb +44 -0
  23. data/spec/lib/hustle_and_flow/formatters/issue_detail_formatter_spec.rb +49 -0
  24. data/spec/lib/hustle_and_flow/formatters/issue_table_formatter_spec.rb +89 -0
  25. data/spec/lib/hustle_and_flow/io/shell_spec.rb +154 -0
  26. data/spec/lib/hustle_and_flow/issue_tracker_spec.rb +7 -0
  27. data/spec/lib/hustle_and_flow/issue_trackers/github/issue_spec.rb +354 -0
  28. data/spec/lib/hustle_and_flow/issue_trackers/github/issues_spec.rb +91 -0
  29. data/spec/lib/hustle_and_flow/issue_trackers/github_spec.rb +51 -0
  30. data/spec/lib/hustle_and_flow/vcs_repository_spec.rb +15 -0
  31. data/spec/lib/hustle_and_flow/version_control/git/branch_spec.rb +101 -0
  32. data/spec/lib/hustle_and_flow/version_control/git/branches_spec.rb +57 -0
  33. data/spec/lib/hustle_and_flow/version_control/git/repository_spec.rb +36 -0
  34. data/spec/lib/utils/url_slug_spec.rb +74 -0
  35. data/spec/spec_helper.rb +3 -0
  36. data/spec/support/git_repo.rb +43 -0
  37. metadata +128 -16
  38. data/.gitignore +0 -19
  39. data/.ruby-version +0 -1
  40. data/.travis.yml +0 -13
  41. data/Gemfile +0 -4
  42. data/Gemfile.lock +0 -56
  43. data/Rakefile +0 -2
  44. data/hustle_and_flow.gemspec +0 -37
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 2ad538991817d27a8eecc569f8623079cd8da45d
4
- data.tar.gz: 855fbfd13d91261bc5353074360f2ca6709735cf
3
+ metadata.gz: 09c8b0e8909c7e772f202f9c4a8d5d51ef65423b
4
+ data.tar.gz: 29362f1b7e998396294e1925c69d939ba66dc2a2
5
5
  SHA512:
6
- metadata.gz: 9f0359c6e32ea999dd46ae83847e708fb93514cd34fd1bd885e16365f33e8ee9d810723ad2408146048bd5eaff245932a9446134bf81b6ca1140d06134c4ee7f
7
- data.tar.gz: 74e82c9b0f1064bc6831e4d019f474e07bc7cbc4d4ad0ba32a44bba57aa595f264f03ed20bc39b030b5f5a17e4b3b16532d1d57b6ef7957be515ab735315da2c
6
+ metadata.gz: 623381f2772547914f6a2c26604542131eeec13d0c82351b0cd17e4bec7ba5c592ea646f0584f9cfb2032bda48ba839bb2f96d59ce435f2c1924f593ce4f95ba
7
+ data.tar.gz: 2f3f3906195c4043b503ab2939c8cc0625effb86badebf5c781a1cd6e928a5632bd18bb96ca0f1448f6aa062aadfca8c818811bb6d69bb24675b54a121a3e3fd
data/bin/hustle ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'hustle_and_flow/binary/runner'
4
+
5
+ HustleAndFlow::Binary::Runner.start
@@ -0,0 +1,35 @@
1
+ require 'thor'
2
+ require 'hustle_and_flow/commands/start'
3
+
4
+ module HustleAndFlow
5
+ module Binary
6
+ class Runner < Thor
7
+ include Thor::Actions
8
+
9
+ desc 'start', 'For generating new, or working on existing issues'
10
+ method_option :create,
11
+ type: :boolean,
12
+ aliases: '-c',
13
+ desc: 'Whether or not to create the issue if one cannot be found'
14
+ method_option :type,
15
+ type: :string,
16
+ aliases: '-t',
17
+ desc: 'The type of issue that should be searched for or created (eg feature, bug, chore, etc)'
18
+ method_option :status,
19
+ type: :string,
20
+ aliases: '-u',
21
+ desc: 'When searching for issues, it is the status of the issues you would like to view.'
22
+ method_option :subject,
23
+ type: :string,
24
+ aliases: '-s',
25
+ desc: 'The subject (or title) of the issue which should be searched for or created'
26
+ method_option :number,
27
+ type: :string,
28
+ aliases: '-n',
29
+ desc: 'The number of the issue which should be searched for.'
30
+ def start
31
+ Commands::Start.new(io: HustleAndFlow::Io::Shell.new, **options).call
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,36 @@
1
+ require 'thor/shell/basic'
2
+ require 'hustle_and_flow/vcs_repository'
3
+ require 'hustle_and_flow/issue_tracker'
4
+
5
+ module HustleAndFlow
6
+ module Commands
7
+ class Start
8
+ attr_accessor :repo,
9
+ :tracker,
10
+ :me,
11
+ :io,
12
+ :issue_data
13
+
14
+ def initialize(io:, **args)
15
+ self.repo = VcsRepository.new(path: Dir.pwd)
16
+ self.tracker = IssueTracker.new(repo: repo)
17
+ self.me = repo.user
18
+ self.io = io
19
+ self.issue_data = args.select { |_k, v| v }
20
+ end
21
+
22
+ def call
23
+ issue = tracker.start(io: io,
24
+ me: me,
25
+ issue_data: issue_data)
26
+ _branch = repo.start(io: io,
27
+ number: issue.number,
28
+ title: issue.title,
29
+ branch_template: issue.to_branch_name(version: '*****'))
30
+ rescue Git::GitExecuteError => e
31
+ io.say 'There was a problem running a git command.'
32
+ io.say e.message
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,6 @@
1
+ module HustleAndFlow
2
+ module Errors
3
+ class UnknownIssueActionError < RuntimeError
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,81 @@
1
+ module HustleAndFlow
2
+ module Formatters
3
+ class BranchTableFormatter
4
+ attr_accessor :branches
5
+
6
+ def initialize(branches)
7
+ self.branches = branches
8
+ end
9
+
10
+ def to_ary
11
+ number = 0
12
+
13
+ branches.map do |branch|
14
+ [
15
+ {
16
+ format: number_formatting(branch),
17
+ value: number += 1,
18
+ },
19
+ {
20
+ format: current_branch_formatting(branch),
21
+ value: branch.current? ? '*' : ' ',
22
+ },
23
+ {
24
+ format: status_formatting(branch),
25
+ value: status(branch),
26
+ },
27
+ {
28
+ format: author_formatting(branch),
29
+ value: branch.author,
30
+ },
31
+ {
32
+ format: name_formatting(branch),
33
+ value: branch.name,
34
+ },
35
+ ]
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ def status(branch)
42
+ if branch.has_tracking_branch?
43
+ 'T'
44
+ elsif branch.remote?
45
+ 'R'
46
+ elsif branch.local?
47
+ 'L'
48
+ else
49
+ '?'
50
+ end
51
+ end
52
+
53
+ def number_formatting(_branch)
54
+ [:white, :bold]
55
+ end
56
+
57
+ def name_formatting(_branch)
58
+ [:green, :bold]
59
+ end
60
+
61
+ def author_formatting(_branch)
62
+ [:white, :bold]
63
+ end
64
+
65
+ def current_branch_formatting(_branch)
66
+ [:yellow, :bold]
67
+ end
68
+
69
+ def status_formatting(branch)
70
+ case
71
+ when branch.has_tracking_branch?
72
+ [:yellow, :bold]
73
+ when branch.remote?
74
+ [:magenta, :bold]
75
+ when branch.local?
76
+ [:blue, :bold]
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,74 @@
1
+ require 'hustle_and_flow/utils/string'
2
+
3
+ module HustleAndFlow
4
+ module Formatters
5
+ class IssueDetailFormatter
6
+ include HustleAndFlow::Utils::String
7
+
8
+ attr_accessor :issue
9
+
10
+ def initialize(issue)
11
+ self.issue = issue
12
+ end
13
+
14
+ def to_hash
15
+ {
16
+ header: {
17
+ format: issue_header_formatting,
18
+ value: issue_header,
19
+ },
20
+ divider: {
21
+ format: issue_divider_formatting,
22
+ value: nil,
23
+ },
24
+ contact: {
25
+ format: issue_contact_formatting,
26
+ value: issue_contact,
27
+ },
28
+ body: {
29
+ format: issue_body_formatting,
30
+ value: issue.body,
31
+ },
32
+ }
33
+ end
34
+
35
+ private
36
+
37
+ def issue_number
38
+ "##{issue.number}"
39
+ end
40
+
41
+ def issue_header
42
+ category = if issue.category
43
+ "#{titleize(issue.category)}: "
44
+ end
45
+
46
+ "#{issue_number} - #{category}#{issue.title}"
47
+ end
48
+
49
+ def issue_header_formatting
50
+ if issue.closed?
51
+ [:red, :bold]
52
+ else
53
+ [:green, :bold]
54
+ end
55
+ end
56
+
57
+ def issue_divider_formatting
58
+ [:black, :bold]
59
+ end
60
+
61
+ def issue_contact
62
+ "By: #{issue.contact}"
63
+ end
64
+
65
+ def issue_contact_formatting
66
+ [:blue, :bold]
67
+ end
68
+
69
+ def issue_body_formatting
70
+ [:white, :bold]
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,80 @@
1
+ require 'hustle_and_flow/utils/string'
2
+
3
+ module HustleAndFlow
4
+ module Formatters
5
+ class IssueTableFormatter
6
+ include HustleAndFlow::Utils::String
7
+
8
+ attr_accessor :issues,
9
+ :title
10
+
11
+ def initialize(issues)
12
+ self.issues = issues
13
+ end
14
+
15
+ def to_ary
16
+ issues.map do |issue|
17
+ [
18
+ {
19
+ format: number_formatting(issue),
20
+ value: "##{issue.number}",
21
+ },
22
+ {
23
+ format: contact_formatting(issue),
24
+ value: issue.contact,
25
+ },
26
+ {
27
+ format: category_formatting(issue),
28
+ value: titleize(issue.category),
29
+ },
30
+ {
31
+ format: title_formatting(issue),
32
+ value: issue.title,
33
+ },
34
+ ]
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ def number_formatting(issue)
41
+ if issue.closed?
42
+ [:red]
43
+ else
44
+ [:green]
45
+ end
46
+ end
47
+
48
+ def contact_formatting(_issue)
49
+ [:white]
50
+ end
51
+
52
+ def category_formatting(issue)
53
+ case issue.category
54
+ when 'feature'
55
+ [:green, :bold]
56
+ when 'bug'
57
+ [:red, :bold]
58
+ when 'refactor'
59
+ [:yellow, :bold]
60
+ when 'test'
61
+ [:cyan, :bold]
62
+ when 'style'
63
+ [:magenta, :bold]
64
+ when 'chore'
65
+ [:white, :bold]
66
+ when 'docs'
67
+ [:blue, :bold]
68
+ when 'spike'
69
+ [:white, :bold]
70
+ else
71
+ [:white, :bold]
72
+ end
73
+ end
74
+
75
+ def title_formatting(_issue)
76
+ [:white]
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,102 @@
1
+ require 'thor/shell/color'
2
+ require 'hustle_and_flow/formatters/issue_detail_formatter'
3
+
4
+ module HustleAndFlow
5
+ module Io
6
+ class Shell < Thor::Shell::Color
7
+ def correct_issue?(issue)
8
+ if issue.new?
9
+ Kernel.system("open #{issue.url}")
10
+ else
11
+ stdout.puts
12
+ print_issue Formatters::IssueDetailFormatter.new(issue).to_hash
13
+
14
+ yes?('Does this issue look correct?')
15
+ end
16
+ end
17
+
18
+ def reassign_issue?(from:)
19
+ yes?("Do you want to reassign this issue from #{from} to yourself?")
20
+ end
21
+
22
+ def choose_issue
23
+ stdout.puts
24
+
25
+ ask 'Enter the number of the issue you would like to work on:'
26
+ end
27
+
28
+ def choose_branch
29
+ stdout.puts
30
+ ask 'Enter the number of the branch you would like to work on:'
31
+ end
32
+
33
+ def print_new_branch_prompt(new_branch_name)
34
+ stdout.puts
35
+ say "Entering 'c' creates a new branch named '#{GREEN}#{new_branch_name}#{CLEAR}'"
36
+ end
37
+
38
+ def print_formatted_table(data:, title: nil)
39
+ data = data.map do |row|
40
+ row.map do |column|
41
+ set_color(column[:value], *column[:format])
42
+ end
43
+ end
44
+
45
+ stdout.puts
46
+
47
+ if title
48
+ stdout.puts title.center(terminal_width)
49
+ stdout.puts '-' * terminal_width
50
+ end
51
+
52
+ print_table(data, truncate: true)
53
+ end
54
+
55
+ def print_issue(data)
56
+ data[:header][:format].each do |format|
57
+ stdout.print lookup_color(format)
58
+ end
59
+
60
+ print_wrapped(data[:header][:value])
61
+
62
+ stdout.puts set_color('-' * terminal_width, *data[:divider][:format])
63
+ stdout.puts set_color(data[:contact][:value], *data[:contact][:format])
64
+
65
+ data[:body][:format].each do |format|
66
+ stdout.print lookup_color(format)
67
+ end
68
+
69
+ if data[:body][:value]
70
+ stdout.puts
71
+ print_wrapped(data[:body][:value])
72
+ end
73
+ end
74
+
75
+ # To be removed once our PR lands
76
+ def print_wrapped(message, options = {})
77
+ indent = options[:indent] || 0
78
+ width = 80
79
+ paras = message.split("\n\n")
80
+
81
+ paras.map! do |unwrapped|
82
+ unwrapped.
83
+ strip.
84
+ gsub(/\n/, ' ').
85
+ squeeze(' ').
86
+ gsub(/.{1,#{width}}(?:\s|\Z)/) do
87
+ ($& + 5.chr).
88
+ gsub(/\n\005/, "\n").
89
+ gsub(/\005/, "\n")
90
+ end
91
+ end
92
+
93
+ paras.each do |para|
94
+ para.split("\n").each do |line|
95
+ stdout.puts line.insert(0, ' ' * indent)
96
+ end
97
+ stdout.puts unless para == paras.last
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,18 @@
1
+ require 'hustle_and_flow/issue_trackers/github'
2
+
3
+ module HustleAndFlow
4
+ class IssueTracker
5
+ attr_accessor :adapter
6
+
7
+ def initialize(**args)
8
+ self.adapter = HustleAndFlow::
9
+ IssueTrackers::
10
+ Github.
11
+ new(repo: args[:repo])
12
+ end
13
+
14
+ def method_missing(name, *args)
15
+ adapter.public_send(name, *args)
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,192 @@
1
+ require 'hustle_and_flow/utils/url_slug'
2
+ require 'hustle_and_flow/errors/unknown_issue_action_error'
3
+ require 'hustle_and_flow/io/shell'
4
+
5
+ module HustleAndFlow
6
+ module IssueTrackers
7
+ class Github
8
+ class Issue
9
+ DEFAULT_CATEGORY_LABELS = %w{feature bug refactor test style chore docs spike}
10
+ ISSUE_ACTIONS = %w{closes references fixes}
11
+
12
+ attr_accessor :tracker,
13
+ :io,
14
+ :data
15
+
16
+ def initialize(tracker:, io: nil, data:)
17
+ self.tracker = tracker
18
+ self.io = io || HustleAndFlow::Io::Shell.new
19
+ self.data = data
20
+ end
21
+
22
+ def self.create(tracker: nil, io: nil, title:, body: '', **args)
23
+ new(io: io,
24
+ tracker: tracker,
25
+ data: tracker.client.create_issue(tracker.repo_name,
26
+ title,
27
+ body,
28
+ **args))
29
+ end
30
+
31
+ def start(me: nil)
32
+ return unless io.correct_issue?(self)
33
+
34
+ reopen.
35
+ assign_issue? to: me
36
+ end
37
+
38
+ def assign(to:)
39
+ reassigned_issue_data = client.update_issue(tracker.repo_name,
40
+ number,
41
+ title,
42
+ body,
43
+ assignee: to)
44
+
45
+ Issue.new(tracker: tracker,
46
+ data: reassigned_issue_data)
47
+ end
48
+
49
+ def category
50
+ @category ||= labels.find do |label|
51
+ DEFAULT_CATEGORY_LABELS.include? label
52
+ end
53
+ end
54
+
55
+ def new?
56
+ five_minutes_ago = Time.now.utc - (5 * 60)
57
+
58
+ five_minutes_ago <= created_at
59
+ end
60
+
61
+ def has_label?(other)
62
+ labels.include?(other)
63
+ end
64
+
65
+ def has_body?
66
+ !(body.nil? || body.empty?)
67
+ end
68
+
69
+ def assign_issue?(to:)
70
+ return self unless needs_reassignment?(assignee_to_assign_to: to)
71
+
72
+ assign to: to
73
+ end
74
+
75
+ def closed?
76
+ status == 'closed'
77
+ end
78
+
79
+ def unassigned?
80
+ assigned_to? 'unassigned'
81
+ end
82
+
83
+ def assigned_to?(other)
84
+ assignee == other
85
+ end
86
+
87
+ def match?(type: nil, **args)
88
+ result = true
89
+ conditions = args
90
+
91
+ conditions.each do |key, value|
92
+ result &&= public_send(key) == value
93
+ end
94
+
95
+ type ? (result && has_label?(type)) : result
96
+ end
97
+
98
+ def contact
99
+ assignee_data[:login] || user_data[:login]
100
+ end
101
+
102
+ def title
103
+ data[:title]
104
+ end
105
+
106
+ def number
107
+ data[:number]
108
+ end
109
+
110
+ def body
111
+ data[:body]
112
+ end
113
+
114
+ def created_at
115
+ data[:created_at]
116
+ end
117
+
118
+ def url
119
+ data[:html_url]
120
+ end
121
+
122
+ def status
123
+ data[:state]
124
+ end
125
+
126
+ def to_branch_name(action: 'closes',
127
+ version: 1)
128
+ branch_name = []
129
+ branch_name << category
130
+ branch_name << base_branch_name(action: action,
131
+ version: version)
132
+
133
+ branch_name.compact!
134
+
135
+ branch_name.join('/')
136
+ end
137
+
138
+ private
139
+
140
+ def reopen
141
+ reopened_issue_data = client.reopen_issue(tracker.repo_name, number)
142
+
143
+ closed? ? Issue.new(tracker: tracker, data: reopened_issue_data) : self
144
+ end
145
+
146
+ def labels
147
+ return [] unless data[:labels]
148
+
149
+ @labels ||= data[:labels].map { |l| l[:name] }
150
+ end
151
+
152
+ def assignee
153
+ assignee_data[:login] || 'unassigned'
154
+ end
155
+
156
+ def assignee_data
157
+ data[:assignee] || {}
158
+ end
159
+
160
+ def user_data
161
+ data[:user] || {}
162
+ end
163
+
164
+ def client
165
+ tracker.client
166
+ end
167
+
168
+ def needs_reassignment?(assignee_to_assign_to:)
169
+ unassigned? ||
170
+ (
171
+ !assigned_to?(assignee_to_assign_to) &&
172
+ io.reassign_issue?(from: assignee)
173
+ )
174
+ end
175
+
176
+ def base_branch_name(action:, version:)
177
+ fail Errors::UnknownIssueActionError unless ISSUE_ACTIONS.include? action.downcase
178
+
179
+ base = []
180
+ base << Utils::UrlSlug.new(title).to_s
181
+ base << action if number
182
+ base << number
183
+ base << "v#{version}" if version
184
+
185
+ base.compact!
186
+
187
+ base.join('-').downcase
188
+ end
189
+ end
190
+ end
191
+ end
192
+ end