codeunion 0.0.2 → 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- 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
|