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
@@ -0,0 +1,89 @@
1
+ require 'hustle_and_flow/issue_trackers/github/issue'
2
+ require 'hustle_and_flow/formatters/issue_table_formatter'
3
+
4
+ module HustleAndFlow
5
+ module IssueTrackers
6
+ class Github
7
+ class Issues
8
+ include Enumerable
9
+
10
+ attr_accessor :tracker,
11
+ :query,
12
+ :issues
13
+
14
+ def initialize(tracker:, issues: nil, query: { state: :all })
15
+ self.tracker = tracker
16
+ self.query = query
17
+ self.issues = issues
18
+ end
19
+
20
+ def self.start(tracker:, issues: nil, io:, me:, issue_data:)
21
+ new(tracker: tracker,
22
+ issues: issues).
23
+ start(io: io,
24
+ me: me,
25
+ issue_data: issue_data)
26
+ end
27
+
28
+ def start(io:, me:, issue_data:)
29
+ issue_data = issue_data.merge(status: 'open')
30
+
31
+ unless issue_specified?(issue_data)
32
+ applicable_issues = filter_by(issue_data)
33
+
34
+ io.print_formatted_table \
35
+ data: Formatters::IssueTableFormatter.new(applicable_issues).to_ary,
36
+ title: 'Available Issues'
37
+
38
+ issue_data[:number] = io.choose_issue until issue_specified?(issue_data)
39
+ end
40
+
41
+ find_or_create(issue_data).start(me: me)
42
+ end
43
+
44
+ def find_or_create(**args)
45
+ find(**args) || Issue.create(tracker: tracker, **args)
46
+ end
47
+
48
+ def each
49
+ issues.each do |issue|
50
+ yield issue
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ def filter_by(*args)
57
+ self.class.new(tracker: tracker,
58
+ issues: issues.select do |issue|
59
+ issue if issue.match?(*args)
60
+ end)
61
+ end
62
+
63
+ def find(type: nil, title: nil, number: nil, **args)
64
+ issues.find do |issue|
65
+ issue.match?(type: type, title: title) ||
66
+ issue.match?(number: number.to_i)
67
+ end
68
+ end
69
+
70
+ def issues
71
+ @issues ||= client.
72
+ issues(tracker.repo_name, query).
73
+ map do |issue_data|
74
+ Issue.new(tracker: tracker,
75
+ data: issue_data)
76
+ end
77
+ end
78
+
79
+ def issue_specified?(data)
80
+ (data[:type] && data[:title]) || data[:number]
81
+ end
82
+
83
+ def client
84
+ tracker.client
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,71 @@
1
+ require 'octokit'
2
+ require 'hustle_and_flow/issue_trackers/github/issue'
3
+ require 'hustle_and_flow/issue_trackers/github/issues'
4
+
5
+ module HustleAndFlow
6
+ module IssueTrackers
7
+ class Github
8
+ attr_accessor :client,
9
+ :repo,
10
+ :source
11
+
12
+ def initialize(repo:)
13
+ Octokit.auto_paginate = true
14
+
15
+ self.client = Octokit::Client.new(netrc: true)
16
+ self.repo = repo
17
+ end
18
+
19
+ def repo_name
20
+ repo.base_name
21
+ end
22
+
23
+ def start(io:, me:, issue_data:)
24
+ Issues.start(tracker: self,
25
+ io: io,
26
+ me: me,
27
+ issue_data: issue_data)
28
+ end
29
+
30
+ # def ready_for_review(branch)
31
+ # #
32
+ # # No issue number
33
+ # # Find by title
34
+ # # Has issue number
35
+ # # Find by number
36
+ # #
37
+ # # Issue found
38
+ # # Issue closed
39
+ # # Ask to reopen issue
40
+ # # Issue open
41
+ # # Issue being reviewed
42
+ # # Issue not being reviewed
43
+ # # Issue not found
44
+ # # Ask to create issue
45
+ # #
46
+ # # Lookup Pull Request from Issue and Version Number
47
+ # #
48
+ # # Pull Request Found
49
+ # # How are PR's found?
50
+ # #
51
+ # # PR Title Format: #15 v1 - My Issue Title
52
+ # #
53
+ # # Pull Request Closed
54
+ # # Reopen PR
55
+ # # Pull Request Open
56
+ # # Use found PR
57
+ # # Pull Request Not Found
58
+ # # Create New PR
59
+ # #
60
+ # pull_request = PullRequest.find_or_create_from_branch_name(branch.name)
61
+ # issue = Issue.find_or_create_from_branch_name(branch.name)
62
+ #
63
+ # issue.ready_for_review(reviewer_names: branch.overwritten_authors)
64
+ # end
65
+ #
66
+ # def update_pull_request(branch)
67
+ # client.create_pull_request()
68
+ # end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,25 @@
1
+ module HustleAndFlow
2
+ module Utils
3
+ module String
4
+ def underscore(camel_cased_word)
5
+ word = camel_cased_word.to_s.gsub('::', '/')
6
+ word.gsub!(/(?:([A-Za-z\d])|^)(?=\b|[^a-z])/) { "#{$1}#{$1 && '_'}" }
7
+ word.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2')
8
+ word.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
9
+ word.tr!('-', '_')
10
+ word.chomp!('_')
11
+ word.downcase!
12
+ word
13
+ end
14
+
15
+ def titleize(other)
16
+ result = other.to_s.dup
17
+ result = underscore(result)
18
+ result = result.tr('_', ' ')
19
+ result = result.downcase
20
+
21
+ result.gsub(/\b(?<!['’`])[a-z]/) { $&.capitalize }
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,28 @@
1
+ ###
2
+ # Public: Responsible for converting strings into URL Slugs
3
+ #
4
+ module HustleAndFlow
5
+ module Utils
6
+ class UrlSlug
7
+ def initialize(original_string)
8
+ self.original_string = original_string || ''
9
+ end
10
+
11
+ def to_s
12
+ original_string.
13
+ downcase. # Lowercase the string
14
+ strip. # Remove all leading and trailing spaces
15
+ gsub(/-+\z/, ''). # Remove any dashes at the end
16
+ gsub(/\A-+/, ''). # Remove any dashes at the beginning
17
+ gsub(/[^\w\-\s]|_/, ''). # Remove any non-alphanumerics, dashes or spaces
18
+ gsub(/\s+/, '-'). # Consolidate remaining consecutive space and
19
+ # convert to dashes
20
+ gsub(/-+/, '-') # Consolidate consecutive dashes into one dash
21
+ end
22
+
23
+ protected
24
+
25
+ attr_accessor :original_string
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,19 @@
1
+ require 'hustle_and_flow/version_control/git/repository'
2
+
3
+ module HustleAndFlow
4
+ class VcsRepository
5
+ attr_accessor :adapter
6
+
7
+ def initialize(**args)
8
+ self.adapter = HustleAndFlow::
9
+ VersionControl::
10
+ Git::
11
+ Repository.
12
+ new(path: args[:path])
13
+ end
14
+
15
+ def method_missing(name, *args)
16
+ adapter.public_send(name, *args)
17
+ end
18
+ end
19
+ end
@@ -1,3 +1,3 @@
1
1
  module HustleAndFlow
2
- VERSION = '0.0.1'
2
+ VERSION = '0.0.2'
3
3
  end
@@ -0,0 +1,136 @@
1
+ module HustleAndFlow
2
+ module VersionControl
3
+ module Git
4
+ class Branch
5
+ attr_accessor :repo,
6
+ :branch
7
+
8
+ def initialize(repo:, branch:)
9
+ self.repo = repo
10
+ self.branch = branch
11
+ end
12
+
13
+ def current?
14
+ local? && branch.current
15
+ end
16
+
17
+ def local?
18
+ !remote?
19
+ end
20
+
21
+ def remote?
22
+ !branch.remote.to_s.empty?
23
+ end
24
+
25
+ def has_tracking_branch?
26
+ local? && upstream
27
+ end
28
+
29
+ def author
30
+ latest_commit.author.name
31
+ end
32
+
33
+ def name
34
+ branch.name
35
+ end
36
+
37
+ def start
38
+ # WIP if necessary
39
+
40
+ repo.with_branch(name) do
41
+ ensure_upstream
42
+ end
43
+
44
+ # if selected_branch.has_wip_commit? && yes?("There is a WIP commit on this bra
45
+ # nch, would you like to apply it locally?")
46
+ # repo.rollback
47
+ #
48
+ # say "The WIP commit has been applied."
49
+ #
50
+ # if yes?("Would you like to remove the commit from origin?")
51
+ # repo.push(branch: selected_branch, force: true)
52
+ # end
53
+ # end
54
+ end
55
+
56
+ def version_number
57
+ name[/v(\d+)\z/, 1].to_i
58
+ end
59
+
60
+ private
61
+
62
+ # def has_wip_commit?
63
+ # latest_commit.message.include? 'WIP:'
64
+ # end
65
+
66
+ # def <=>(other)
67
+ # name <=> other.name
68
+ # end
69
+
70
+ # def overwritten_authors
71
+ # names = `git overwritten`
72
+ # names.first(3)
73
+ # end
74
+
75
+ def ensure_upstream
76
+ self.upstream = default_upstream_branch_name unless has_tracking_branch?
77
+ end
78
+
79
+ def latest_commit
80
+ branch.gcommit
81
+ end
82
+
83
+ def upstream=(other)
84
+ remote, branch = parse_name(other)
85
+
86
+ self.upstream_remote = remote
87
+ self.upstream_branch = "refs/heads/#{branch}"
88
+ end
89
+
90
+ def upstream
91
+ return nil if upstream_remote.empty?
92
+
93
+ "#{upstream_remote}/#{name}"
94
+ end
95
+
96
+ def upstream_remote
97
+ repo.upstream_remote_for_branch(name)
98
+ end
99
+
100
+ def upstream_remote=(other)
101
+ repo.upstream_remote_for_branch(name, other)
102
+ end
103
+
104
+ def upstream_branch
105
+ repo.upstream_branch_for_branch(name)
106
+ end
107
+
108
+ def upstream_branch=(other)
109
+ repo.upstream_branch_for_branch(name, other)
110
+ end
111
+
112
+ def default_upstream_branch_name
113
+ "origin/#{name}"
114
+ end
115
+
116
+ def parse_name(name)
117
+ name = name.to_s
118
+
119
+ if name.match(%r{^(?:remotes/)?([^/]+)/(.+)})
120
+ return [$1, $2]
121
+ end
122
+
123
+ return [nil, name]
124
+ end
125
+
126
+ def branch=(other)
127
+ @branch = if other.is_a? self.class
128
+ other.branch
129
+ else
130
+ other
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,93 @@
1
+ require 'hustle_and_flow/utils/url_slug'
2
+ require 'hustle_and_flow/version_control/git/branch'
3
+
4
+ module HustleAndFlow
5
+ module VersionControl
6
+ module Git
7
+ class Branches
8
+ include Enumerable
9
+
10
+ attr_accessor :repo,
11
+ :branches
12
+
13
+ def initialize(repo:, branches:)
14
+ self.repo = repo
15
+ self.branches = branches
16
+ end
17
+
18
+ def filter_by_issue(number:, title:)
19
+ self.class.new(repo: repo,
20
+ branches: (branches_matching_issue_number(number) +
21
+ branches_matching_issue_title(title)).uniq)
22
+ end
23
+
24
+ def valid_number?(number)
25
+ return false unless number.to_s.match(/\A\d+\z/)
26
+
27
+ (1..branches.count).include?(number.to_i)
28
+ end
29
+
30
+ def from_number(number)
31
+ branches[number.to_i - 1]
32
+ end
33
+
34
+ def next_branch_name(template:)
35
+ template.sub('*****', next_version_number.to_s)
36
+ end
37
+
38
+ def selectable
39
+ tracking_branches = branches.map do |branch|
40
+ branch.name if branch.has_tracking_branch?
41
+ end
42
+
43
+ self.class.new(repo: repo,
44
+ branches: branches.reject do |branch|
45
+ branch.remote? &&
46
+ tracking_branches.include?(branch.name)
47
+ end)
48
+ end
49
+
50
+ def each
51
+ branches.each do |branch|
52
+ yield branch
53
+ end
54
+ end
55
+
56
+ private
57
+
58
+ def next_version_number
59
+ latest_branch = branches.max do |first_branch, second_branch|
60
+ first_branch.version_number <=> second_branch.version_number
61
+ end
62
+
63
+ latest_branch ? latest_branch.version_number + 1 : 1
64
+ end
65
+
66
+ def branches_matching_issue_number(issue_number)
67
+ return [] unless issue_number
68
+
69
+ branches.select do |branch|
70
+ branch.name.match(/-#{issue_number.to_i}\b/)
71
+ end
72
+ end
73
+
74
+ def branches_matching_issue_title(issue_title)
75
+ return [] unless issue_title.to_s.length > 0
76
+
77
+ title_slug = Utils::UrlSlug.new(issue_title).to_s
78
+
79
+ branches.select do |branch|
80
+ branch.name.include? title_slug
81
+ end
82
+ end
83
+
84
+ def branches=(other)
85
+ @branches = other.map do |branch|
86
+ Branch.new(repo: repo,
87
+ branch: branch)
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,99 @@
1
+ require 'git'
2
+ require 'hustle_and_flow/version_control/git/branches'
3
+ require 'hustle_and_flow/formatters/branch_table_formatter'
4
+
5
+ module HustleAndFlow
6
+ module VersionControl
7
+ module Git
8
+ class Repository
9
+ attr_accessor :repo
10
+
11
+ def initialize(path:)
12
+ self.repo = ::Git.open(path)
13
+ end
14
+
15
+ def start(io:, number:, title:, branch_template:)
16
+ available_branches = branches.
17
+ selectable.
18
+ filter_by_issue(number: number,
19
+ title: title)
20
+
21
+ next_branch_name = available_branches.next_branch_name(template: branch_template)
22
+
23
+ selected = if available_branches.count.zero?
24
+ Branch.new(repo: self, branch: repo.checkout(next_branch_name,
25
+ new_branch: true))
26
+ else
27
+ branch_number = nil
28
+
29
+ io.print_formatted_table \
30
+ title: 'Possible Existing Branches',
31
+ data: Formatters::BranchTableFormatter.new(available_branches).
32
+ to_ary
33
+
34
+ io.print_new_branch_prompt(next_branch_name)
35
+
36
+ until available_branches.valid_number? branch_number
37
+ branch_number = io.choose_branch
38
+ end
39
+
40
+ available_branches.from_number(branch_number)
41
+ end
42
+
43
+ selected.start
44
+ end
45
+
46
+ def user
47
+ repo.config('github.user')
48
+ end
49
+
50
+ def base_name
51
+ origin_url[%r{github\.com.([\w\-]+/[\w\-]+)}, 1]
52
+ end
53
+
54
+ def upstream_remote_for_branch(branch_name, value = nil)
55
+ repo.config(upstream_remote_config_key(branch_name), value)
56
+ end
57
+
58
+ def upstream_branch_for_branch(branch_name, value = nil)
59
+ repo.config(upstream_merge_config_key(branch_name), value)
60
+ end
61
+
62
+ def with_branch(branch_name)
63
+ repo.checkout(branch_name)
64
+
65
+ yield
66
+
67
+ repo.push('origin', branch_name)
68
+ end
69
+
70
+ private
71
+
72
+ def branches
73
+ Branches.new(repo: self,
74
+ branches: repo.branches)
75
+ end
76
+
77
+ def new_branch_name(branch_template)
78
+ branch_template.sub('*****', branches.next_version_number)
79
+ end
80
+
81
+ def origin_url
82
+ origin.url
83
+ end
84
+
85
+ def origin
86
+ @origin ||= repo.remote('origin')
87
+ end
88
+
89
+ def upstream_remote_config_key(branch_name)
90
+ "branch.#{branch_name}.remote"
91
+ end
92
+
93
+ def upstream_merge_config_key(branch_name)
94
+ "branch.#{branch_name}.merge"
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
@@ -1,5 +1,4 @@
1
- require "hustle_and_flow/version"
1
+ require 'hustle_and_flow/version'
2
2
 
3
3
  module HustleAndFlow
4
- # Your code goes here...
5
4
  end
@@ -0,0 +1,44 @@
1
+ require 'spec_helper'
2
+ require 'hustle_and_flow/formatters/branch_table_formatter'
3
+
4
+ module HustleAndFlow
5
+ module Formatters
6
+ describe BranchTableFormatter do
7
+ it 'can output branch information as columns' do
8
+ branches = [
9
+ OpenStruct.new(current?: true,
10
+ local?: true,
11
+ remote?: true,
12
+ has_tracking_branch?: true,
13
+ author: 'Michael Myers',
14
+ name: 'feature/add-new-feature-closes-42-v2'),
15
+ OpenStruct.new(current?: false,
16
+ local?: false,
17
+ remote?: true,
18
+ has_tracking_branch?: false,
19
+ author: 'Jeff Has a Really Really Long Name',
20
+ name: 'add-new-feature-42'),
21
+ ]
22
+
23
+ columnizer = BranchTableFormatter.new(branches)
24
+
25
+ expect(columnizer.to_ary).to eql([
26
+ [
27
+ { format: [:white, :bold], value: 1 },
28
+ { format: [:yellow, :bold], value: '*' },
29
+ { format: [:yellow, :bold], value: 'T' },
30
+ { format: [:white, :bold], value: 'Michael Myers' },
31
+ { format: [:green, :bold], value: 'feature/add-new-feature-closes-42-v2' },
32
+ ],
33
+ [
34
+ { format: [:white, :bold], value: 2 },
35
+ { format: [:yellow, :bold], value: ' ' },
36
+ { format: [:magenta, :bold], value: 'R' },
37
+ { format: [:white, :bold], value: 'Jeff Has a Really Really Long Name' },
38
+ { format: [:green, :bold], value: 'add-new-feature-42' },
39
+ ],
40
+ ])
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,49 @@
1
+ require 'spec_helper'
2
+ require 'hustle_and_flow/formatters/issue_detail_formatter'
3
+ require 'hustle_and_flow/issue_trackers/github/issue'
4
+
5
+ module HustleAndFlow
6
+ module Formatters
7
+ describe IssueDetailFormatter do
8
+ it 'can output the details of an issue as an array' do
9
+ issue = IssueTrackers::Github::Issue.new(
10
+ tracker: 'foo',
11
+ data: {
12
+ status: 'open',
13
+ assignee: {
14
+ login: 'neo' },
15
+ number: 42,
16
+ title: 'My Awesome Title',
17
+ body: 'My Body is a Wonderland' })
18
+
19
+ expect(IssueDetailFormatter.new(issue).to_hash).to eql(
20
+ header: { format: [:green, :bold], value: '#42 - My Awesome Title' },
21
+ divider: { format: [:black, :bold], value: nil },
22
+ contact: { format: [:blue, :bold], value: 'By: neo' },
23
+ body: { format: [:white, :bold], value: 'My Body is a Wonderland' }
24
+ )
25
+ end
26
+
27
+ it 'can output the category next to the title if there is one' do
28
+ issue = IssueTrackers::Github::Issue.new(
29
+ tracker: 'foo',
30
+ data: {
31
+ status: 'open',
32
+ assignee: {
33
+ login: 'neo' },
34
+ number: 42,
35
+ labels: [
36
+ { name: 'feature' },
37
+ ],
38
+ title: 'My Awesome Title' })
39
+
40
+ expect(IssueDetailFormatter.new(issue).to_hash).to eql(
41
+ header: { format: [:green, :bold], value: '#42 - Feature: My Awesome Title' },
42
+ divider: { format: [:black, :bold], value: nil },
43
+ contact: { format: [:blue, :bold], value: 'By: neo' },
44
+ body: { format: [:white, :bold], value: nil }
45
+ )
46
+ end
47
+ end
48
+ end
49
+ end