codeunion 0.0.2 → 0.0.3
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.
- checksums.yaml +4 -4
- data/.gitignore +2 -0
- data/.rubocop.yml +35 -0
- data/Makefile +101 -0
- data/README.md +13 -0
- data/bin/codeunion +9 -1
- data/codeunion.gemspec +6 -0
- data/lib/codeunion/api.rb +16 -0
- data/lib/codeunion/command.rb +21 -0
- data/lib/codeunion/command/base.rb +3 -3
- data/lib/codeunion/command/config.rb +15 -28
- data/lib/codeunion/command/feedback.rb +39 -0
- data/lib/codeunion/command/main.rb +2 -2
- data/lib/codeunion/command/search.rb +34 -0
- data/lib/codeunion/config.rb +66 -4
- data/lib/codeunion/feedback_request.rb +97 -0
- data/lib/codeunion/github_api.rb +19 -0
- data/lib/codeunion/helpers/text.rb +38 -0
- data/lib/codeunion/http_client.rb +55 -0
- data/lib/codeunion/search.rb +51 -0
- data/lib/codeunion/version.rb +1 -1
- data/libexec/codeunion-config +116 -0
- data/libexec/codeunion-examples +37 -0
- data/libexec/codeunion-feedback +98 -0
- data/libexec/codeunion-projects +37 -0
- data/libexec/codeunion-search +42 -0
- data/test/features.rb +4 -0
- data/test/features/config_test.rb +27 -0
- data/test/fixtures/codeunion_api_search.rb +33 -0
- data/test/unit.rb +5 -0
- data/test/unit/config_test.rb +39 -0
- data/test/unit/feedback_request_test.rb +119 -0
- data/test/unit/fixtures/sample_config +3 -0
- data/test/unit/search_test.rb +59 -0
- metadata +118 -5
- data/bin/codeunion-config +0 -13
@@ -0,0 +1,97 @@
|
|
1
|
+
require "uri"
|
2
|
+
require "codeunion/github_api"
|
3
|
+
require "addressable/uri"
|
4
|
+
|
5
|
+
module CodeUnion
|
6
|
+
# Sends feedback requests to CodeUnion for review
|
7
|
+
class FeedbackRequest
|
8
|
+
WEB_URL_REGEX = /\A#{URI.regexp(['http', 'https'])}\z/
|
9
|
+
|
10
|
+
GIT_URL_REGEX = %r{\A(git://)?git@github.com:(.*)(.git)?\z}
|
11
|
+
REPO_CAPTURE_INDEX = 1
|
12
|
+
|
13
|
+
MISSING_ARTIFACT = "You must provide something to provide feedback on"
|
14
|
+
INVALID_ARTIFACT = "The artifact provided was not a web URL. We only provide feedback on code hosted online."
|
15
|
+
|
16
|
+
ISSUE_TITLE = "Please give my code feedback. Submitted #{Time.now}"
|
17
|
+
|
18
|
+
attr_reader :artifact
|
19
|
+
|
20
|
+
def initialize(artifact, github_token, feedback_repository, options = {})
|
21
|
+
@github_api = options.fetch(:github_api, GithubAPI).new(github_token)
|
22
|
+
@artifact = artifact
|
23
|
+
@feedback_repository = FeedbackRepository.coerce_to_proper_url(feedback_repository)
|
24
|
+
end
|
25
|
+
|
26
|
+
def send!
|
27
|
+
@github_api.create_issue(ISSUE_TITLE, @artifact, @feedback_repository)
|
28
|
+
end
|
29
|
+
|
30
|
+
def valid?
|
31
|
+
errors.empty?
|
32
|
+
end
|
33
|
+
|
34
|
+
def errors
|
35
|
+
errors = []
|
36
|
+
errors.push(MISSING_ARTIFACT) if artifact.empty?
|
37
|
+
errors.push(INVALID_ARTIFACT) unless url?(artifact)
|
38
|
+
|
39
|
+
errors.join("\n")
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def url?(url)
|
45
|
+
url =~ WEB_URL_REGEX
|
46
|
+
end
|
47
|
+
|
48
|
+
# Ensures a feedback repository location string is usable via the github api
|
49
|
+
class FeedbackRepository
|
50
|
+
DEFAULT_OWNER = "codeunion"
|
51
|
+
|
52
|
+
def self.coerce_to_proper_url(location)
|
53
|
+
new(location).to_s
|
54
|
+
end
|
55
|
+
|
56
|
+
def initialize(location)
|
57
|
+
@location = CLEAN.call(location)
|
58
|
+
end
|
59
|
+
|
60
|
+
def to_s
|
61
|
+
extract_owner_and_repo
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
CLEAN = ->(location) { location.gsub(/^\//, "").gsub(".git", "") }
|
67
|
+
EXTRACTORS = [
|
68
|
+
{
|
69
|
+
:match => ->(location) { location =~ GIT_URL_REGEX },
|
70
|
+
:extract => lambda do |location|
|
71
|
+
match = location.match(GIT_URL_REGEX)
|
72
|
+
CLEAN.call(match.captures[REPO_CAPTURE_INDEX])
|
73
|
+
end
|
74
|
+
},
|
75
|
+
{
|
76
|
+
:match => ->(location) { location =~ WEB_URL_REGEX },
|
77
|
+
:extract => ->(location) { CLEAN.call(URI(location).path) }
|
78
|
+
},
|
79
|
+
{
|
80
|
+
:match => ->(location) { location.split(/\//).length == 2 },
|
81
|
+
:extract => ->(location) { location }
|
82
|
+
},
|
83
|
+
{
|
84
|
+
:match => ->(_) { true },
|
85
|
+
:extract => ->(location) { "#{DEFAULT_OWNER}/#{location}" }
|
86
|
+
}
|
87
|
+
]
|
88
|
+
def extract_owner_and_repo
|
89
|
+
EXTRACTORS.each do |extractor|
|
90
|
+
if extractor[:match].call(@location)
|
91
|
+
return extractor[:extract].call(@location)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require "codeunion/http_client"
|
2
|
+
|
3
|
+
module CodeUnion
|
4
|
+
# Intent-revealing methods for interacting with Github with interfaces
|
5
|
+
# that aren't tied to the api calls.
|
6
|
+
class GithubAPI
|
7
|
+
def initialize(access_token)
|
8
|
+
@access_token = access_token
|
9
|
+
@http_client = HTTPClient.new("https://api.github.com")
|
10
|
+
end
|
11
|
+
|
12
|
+
def create_issue(title, content, repository)
|
13
|
+
@http_client.post("repos/#{repository}/issues", {
|
14
|
+
"title" => title,
|
15
|
+
"body" => content
|
16
|
+
}, { "access_token" => @access_token })
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require "codeunion"
|
2
|
+
require "io/console"
|
3
|
+
|
4
|
+
module CodeUnion
|
5
|
+
module Helpers
|
6
|
+
# Formats text for presenting to a console
|
7
|
+
module Text
|
8
|
+
def format_output(text)
|
9
|
+
_rows, cols = IO.console.winsize
|
10
|
+
line_length = [cols, 80].min
|
11
|
+
|
12
|
+
indent(wrap(text, line_length))
|
13
|
+
end
|
14
|
+
|
15
|
+
def wrap(text, max_width = IO.console.winsize.last)
|
16
|
+
return "" if text.empty?
|
17
|
+
|
18
|
+
words = text.split(/\s+/)
|
19
|
+
output = words.shift
|
20
|
+
chars_left = max_width - output.length
|
21
|
+
|
22
|
+
words.inject(output) do |output_builder, word|
|
23
|
+
if word.length + 1 > chars_left
|
24
|
+
chars_left = max_width - word.length
|
25
|
+
output_builder << "\n" + word
|
26
|
+
else
|
27
|
+
chars_left -= word.length + 1
|
28
|
+
output_builder << " " + word
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def indent(lines, level = 1)
|
34
|
+
lines.split("\n").map { |line| (" " * level) + line }.join("\n")
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require "faraday"
|
2
|
+
require "faraday_middleware"
|
3
|
+
require "multi_json"
|
4
|
+
require "addressable/template"
|
5
|
+
|
6
|
+
module CodeUnion
|
7
|
+
# More friendly RESTful interactions via Faraday and Addressable
|
8
|
+
class HTTPClient
|
9
|
+
def initialize(api_host)
|
10
|
+
@api_host = api_host
|
11
|
+
end
|
12
|
+
|
13
|
+
def get(endpoint, params = {})
|
14
|
+
connection.get(request_url(endpoint, params)).body
|
15
|
+
end
|
16
|
+
|
17
|
+
def post(endpoint, body = {}, params = {})
|
18
|
+
response = connection.post do |req|
|
19
|
+
req.url request_url(endpoint, params)
|
20
|
+
req.headers["Content-Type"] = "application/json"
|
21
|
+
req.body = body.to_json
|
22
|
+
end
|
23
|
+
|
24
|
+
unless (200..300).cover?(response.status)
|
25
|
+
message = "POST to #{request_url(endpoint, params)} with " +
|
26
|
+
"#{body} responded with #{response.status} and " +
|
27
|
+
"#{response.body}"
|
28
|
+
fail(HTTPClient::RequestError, message)
|
29
|
+
end
|
30
|
+
response.body
|
31
|
+
end
|
32
|
+
|
33
|
+
def request_url(endpoint, params = {})
|
34
|
+
expand_url({ :endpoint => endpoint, :params => params })
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def expand_url(options = {})
|
40
|
+
template.expand(options)
|
41
|
+
end
|
42
|
+
|
43
|
+
def template
|
44
|
+
@template ||= Addressable::Template.new("{+endpoint}{?params*}")
|
45
|
+
end
|
46
|
+
|
47
|
+
def connection
|
48
|
+
@connection ||= Faraday.new(@api_host) do |conn|
|
49
|
+
conn.response :json
|
50
|
+
conn.adapter Faraday.default_adapter
|
51
|
+
end
|
52
|
+
end
|
53
|
+
class RequestError < StandardError; end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require "rainbow"
|
2
|
+
require "codeunion/helpers/text"
|
3
|
+
|
4
|
+
module CodeUnion
|
5
|
+
class Search
|
6
|
+
# Prepares a search result for presentation in the CLI
|
7
|
+
class ResultPresenter
|
8
|
+
include CodeUnion::Helpers::Text
|
9
|
+
EXCERPT_REGEX = /<match>([^<]*)<\/match>/
|
10
|
+
|
11
|
+
def initialize(result)
|
12
|
+
@result = result
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_s
|
16
|
+
[:name, :description, :url, :tags, :excerpt].map do |attribute|
|
17
|
+
format_output(send(attribute))
|
18
|
+
end.join("\n")
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def name
|
24
|
+
"#{category}: " + Rainbow(@result["name"]).color(:blue)
|
25
|
+
end
|
26
|
+
|
27
|
+
def category
|
28
|
+
Rainbow(@result["category"].gsub(/s$/, "").capitalize).color(:red)
|
29
|
+
end
|
30
|
+
|
31
|
+
def description
|
32
|
+
Rainbow(@result["description"]).color(:yellow)
|
33
|
+
end
|
34
|
+
|
35
|
+
def excerpt
|
36
|
+
"Excerpt: " + @result["excerpt"].gsub(EXCERPT_REGEX) do
|
37
|
+
query_term = Regexp.last_match[1]
|
38
|
+
Rainbow(query_term).color(:blue)
|
39
|
+
end + "..."
|
40
|
+
end
|
41
|
+
|
42
|
+
def url
|
43
|
+
Rainbow(@result["url"]).color(:green)
|
44
|
+
end
|
45
|
+
|
46
|
+
def tags
|
47
|
+
"tags: " + @result["tags"].sort.join(", ")
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
data/lib/codeunion/version.rb
CHANGED
@@ -0,0 +1,116 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# encoding: UTF-8
|
3
|
+
|
4
|
+
# Resolve the pathname for this executable
|
5
|
+
require "pathname"
|
6
|
+
bin_file = Pathname.new(__FILE__).realpath
|
7
|
+
|
8
|
+
# Add the gem's "lib" directory to library path
|
9
|
+
$LOAD_PATH.unshift File.expand_path("../../lib", bin_file)
|
10
|
+
|
11
|
+
require "codeunion/command/config"
|
12
|
+
require "optparse"
|
13
|
+
|
14
|
+
options = {}
|
15
|
+
subtext = <<HELP
|
16
|
+
Commands are:
|
17
|
+
get NAME - Retrieves and prints the named configuration value.
|
18
|
+
set NAME VALUE - Sets the named configuration to the given value.
|
19
|
+
unset NAME - Removes the named configuration value.
|
20
|
+
|
21
|
+
See 'codeunion config COMMAND --help' for more information on a specific command.
|
22
|
+
HELP
|
23
|
+
parser = OptionParser.new do |opts|
|
24
|
+
opts.instance_exec do
|
25
|
+
self.banner = "Usage: codeunion config COMMAND"
|
26
|
+
separator ""
|
27
|
+
|
28
|
+
separator subtext
|
29
|
+
separator ""
|
30
|
+
|
31
|
+
separator "Global options:"
|
32
|
+
on_tail("-d", "--[no-]debug", "Adds debug output.") do |debug|
|
33
|
+
options[:debug] = debug
|
34
|
+
end
|
35
|
+
|
36
|
+
on_tail("-h", "--help", "Print this help message") do
|
37
|
+
puts self
|
38
|
+
exit
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
commands = {}
|
44
|
+
get_subtext = <<HELP
|
45
|
+
Displays config variable NAME. NAME may be a top-level key (i.e. 'codeunion')
|
46
|
+
or nested, (i.e. 'codeunion.slack.username')
|
47
|
+
HELP
|
48
|
+
commands["get"] = OptionParser.new do |opts|
|
49
|
+
opts.instance_exec do
|
50
|
+
self.banner = "Usage: codeunion config get NAME"
|
51
|
+
|
52
|
+
separator ""
|
53
|
+
separator get_subtext
|
54
|
+
separator ""
|
55
|
+
|
56
|
+
separator "Options:"
|
57
|
+
on_tail("-h", "--help", "Print this help message") do
|
58
|
+
puts self
|
59
|
+
exit
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
set_subtext = <<HELP
|
65
|
+
Sets config variable NAME to VALUE. NAME may be a top-level key (i.e. 'codeunion')
|
66
|
+
or nested, (i.e. 'codeunion.slack.username').
|
67
|
+
HELP
|
68
|
+
|
69
|
+
commands["set"] = OptionParser.new do |opts|
|
70
|
+
opts.instance_exec do
|
71
|
+
self.banner = "Usage: codeunion config set NAME VALUE"
|
72
|
+
|
73
|
+
separator ""
|
74
|
+
separator set_subtext
|
75
|
+
separator ""
|
76
|
+
|
77
|
+
separator "Options:"
|
78
|
+
on_tail("-h", "--help", "Print this help message") do
|
79
|
+
puts self
|
80
|
+
exit
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
unset_subtext = <<HELP
|
86
|
+
Unsets config variable NAME. NAME may be a top-level key (i.e. 'codeunion') or
|
87
|
+
nested, (i.e. 'codeunion.slack.username').
|
88
|
+
HELP
|
89
|
+
commands["unset"] = OptionParser.new do |opts|
|
90
|
+
opts.instance_exec do
|
91
|
+
self.banner = "Usage: codeunion config unset NAME"
|
92
|
+
|
93
|
+
separator ""
|
94
|
+
separator unset_subtext
|
95
|
+
separator ""
|
96
|
+
|
97
|
+
separator "Options:"
|
98
|
+
on_tail("-h", "--help", "Print this help message") do
|
99
|
+
puts self
|
100
|
+
exit
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
parser.order!
|
106
|
+
command = ARGV.shift
|
107
|
+
if !command || !commands[command]
|
108
|
+
puts parser
|
109
|
+
exit
|
110
|
+
else
|
111
|
+
commands[command].order!
|
112
|
+
options[:command] = command
|
113
|
+
options[:input] = ARGV
|
114
|
+
result = CodeUnion::Command::Config.new(options).run
|
115
|
+
puts result if result
|
116
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# encoding: UTF-8
|
3
|
+
|
4
|
+
# Resolve the pathname for this executable
|
5
|
+
require "pathname"
|
6
|
+
bin_file = Pathname.new(__FILE__).realpath
|
7
|
+
|
8
|
+
# Add the gem's "lib" directory to library path
|
9
|
+
$LOAD_PATH.unshift File.expand_path("../../lib", bin_file)
|
10
|
+
|
11
|
+
require "codeunion/command/search"
|
12
|
+
require "optparse"
|
13
|
+
|
14
|
+
options = {}
|
15
|
+
parser = OptionParser.new do |opts|
|
16
|
+
opts.instance_exec do
|
17
|
+
self.banner = "Usage: codeunion examples <terms>"
|
18
|
+
|
19
|
+
on_tail("-h", "--help", "Print this help message") do
|
20
|
+
puts self
|
21
|
+
exit
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
if ARGV.empty?
|
27
|
+
puts parser
|
28
|
+
exit
|
29
|
+
else
|
30
|
+
parser.parse!(ARGV)
|
31
|
+
end
|
32
|
+
|
33
|
+
options[:category] = "examples"
|
34
|
+
options[:query] = ARGV
|
35
|
+
|
36
|
+
result = CodeUnion::Command::Search.new(options).run
|
37
|
+
puts result if result
|
@@ -0,0 +1,98 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# encoding: UTF-8
|
3
|
+
|
4
|
+
# Resolve the pathname for this executable
|
5
|
+
require "pathname"
|
6
|
+
bin_file = Pathname.new(__FILE__).realpath
|
7
|
+
|
8
|
+
# Add the gem's "lib" directory to library path
|
9
|
+
$LOAD_PATH.unshift File.expand_path("../../lib", bin_file)
|
10
|
+
|
11
|
+
require "codeunion/command/feedback"
|
12
|
+
require "optparse"
|
13
|
+
|
14
|
+
options = {}
|
15
|
+
commands = {}
|
16
|
+
subtext = <<HELP
|
17
|
+
Commonly used command are:
|
18
|
+
request URL - (Default) - Sends a request for feedback to codeunion.
|
19
|
+
|
20
|
+
See 'codeunion feedback COMMAND --help' for more information on a specific command.
|
21
|
+
|
22
|
+
Make sure you `codeunion config set` the following:
|
23
|
+
github.access_token - Allows codeunion-client to interact with Github as you.
|
24
|
+
See: https://help.github.com/articles/creating-an-access-token-for-command-line-use/
|
25
|
+
|
26
|
+
feedback.repository - The github repository to submit feedback requests in.
|
27
|
+
(I.e. codeunion/feedback-requests-web-fundamentals)
|
28
|
+
HELP
|
29
|
+
|
30
|
+
top_level_parser = OptionParser.new do |opts|
|
31
|
+
opts.instance_exec do
|
32
|
+
self.banner = "Usage: codeunion feedback COMMAND"
|
33
|
+
|
34
|
+
separator ""
|
35
|
+
separator subtext
|
36
|
+
|
37
|
+
separator ""
|
38
|
+
separator "Global options:"
|
39
|
+
on_tail("-d", "--[no-]debug", "Adds debug output.") do |debug|
|
40
|
+
options[:debug] = debug
|
41
|
+
end
|
42
|
+
|
43
|
+
on_tail("-h", "--help", "Print this help message") do
|
44
|
+
puts self
|
45
|
+
exit
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
request_subtext = <<HELP
|
51
|
+
URL should be a pull request or a specific commit.
|
52
|
+
|
53
|
+
* How to get the URL for a commit: http://andrew.yurisich.com/work/2014/07/16/dont-link-that-line-number/
|
54
|
+
* How to create a pull request: https://help.github.com/articles/creating-a-pull-request/
|
55
|
+
HELP
|
56
|
+
|
57
|
+
commands["request"] = OptionParser.new do |opts|
|
58
|
+
opts.instance_exec do
|
59
|
+
self.banner = "Usage: codeunion feedback request URL"
|
60
|
+
|
61
|
+
separator ""
|
62
|
+
separator request_subtext
|
63
|
+
|
64
|
+
separator ""
|
65
|
+
separator "Options:"
|
66
|
+
|
67
|
+
on_tail("-h", "--help", "Print this help message") do
|
68
|
+
puts self
|
69
|
+
exit
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
top_level_parser.order!
|
75
|
+
command = ARGV.shift
|
76
|
+
|
77
|
+
unless command
|
78
|
+
puts top_level_parser
|
79
|
+
exit
|
80
|
+
end
|
81
|
+
|
82
|
+
if command && !commands[command]
|
83
|
+
options[:input] = [command]
|
84
|
+
command = "request"
|
85
|
+
else
|
86
|
+
commands[command].order!
|
87
|
+
options[:input] = ARGV
|
88
|
+
end
|
89
|
+
begin
|
90
|
+
CodeUnion::Command::Feedback.new(options).run
|
91
|
+
rescue CodeUnion::Command::InvalidInput => e
|
92
|
+
puts "Woops! Error(s) found!"
|
93
|
+
puts e.message
|
94
|
+
puts commands[command]
|
95
|
+
rescue CodeUnion::Command::MissingConfig => e
|
96
|
+
puts "Woops! You need to configure this command"
|
97
|
+
puts e.message
|
98
|
+
end
|