hustle_and_flow 0.0.1 → 0.0.2

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 (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