lita-jls 0.0.11
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 +7 -0
- data/.gitignore +19 -0
- data/Gemfile +3 -0
- data/LICENSE +19 -0
- data/README.md +24 -0
- data/Rakefile +6 -0
- data/lib/lita-jls.rb +8 -0
- data/lib/lita-jls/bot_builder.rb +240 -0
- data/lib/lita-jls/github_url_parser.rb +43 -0
- data/lib/lita-jls/repository.rb +57 -0
- data/lib/lita-jls/util.rb +259 -0
- data/lib/lita/handlers/jls.rb +321 -0
- data/lita-jls.gemspec +41 -0
- data/locales/en.yml +4 -0
- data/spec/fixtures/bad_project/Gemfile +2 -0
- data/spec/fixtures/logstash-codec-edn/logstash-codec-edn.gemspec +29 -0
- data/spec/fixtures/logstash.gemspec +3 -0
- data/spec/fixtures/project_with_version_file/lib/testmore/version.rb +3 -0
- data/spec/fixtures/project_with_version_file/project_with_version_file.gemspec +9 -0
- data/spec/fixtures/vcr_cassettes/fetch_version_of_logstash-output-s3.yml +96 -0
- data/spec/fixtures/vcr_cassettes/gem_doesnt_exist.yml +57 -0
- data/spec/fixtures/vcr_cassettes/successful_clacheck.yml +127 -0
- data/spec/fixtures/vcr_cassettes/successful_clacheck_long_commit.yml +133 -0
- data/spec/fixtures/vcr_cassettes/successful_migrate_pr.yml +81 -0
- data/spec/lita-jls/bot_builder_spec.rb +158 -0
- data/spec/lita-jls/github_url_parser_spec.rb +80 -0
- data/spec/lita/handlers/jls_spec.rb +211 -0
- data/spec/spec_helper.rb +31 -0
- metadata +338 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 82fcfffe36fd74ada588e3e69026933c65307904
|
4
|
+
data.tar.gz: e7e5e78740affb2ec4e9de2eec9345eb84f77da3
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: aab3bdc7a67337e64cac4acd57de18f70ba3713adc38915462dca2e60e85b97c41f95c40e8616763992e098d6d1c82b7fa7ce07ba80931a33f2cd026cf84397d
|
7
|
+
data.tar.gz: 78b56db4fe290dd3ae558cdc3c84a3c0a99d0cc6fd54050c22de2fa83746c334191a697e25250462b730e0031ecfe0fab459edbb421ee01752eacfd3a86782f7
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright (c) 2014 Jordan Sissel
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
5
|
+
in the Software without restriction, including without limitation the rights
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# lita-jls
|
2
|
+
|
3
|
+
TODO: Add a description of the plugin.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add lita-jls to your Lita instance's Gemfile:
|
8
|
+
|
9
|
+
``` ruby
|
10
|
+
gem "lita-jls"
|
11
|
+
```
|
12
|
+
|
13
|
+
|
14
|
+
## Configuration
|
15
|
+
|
16
|
+
TODO: Describe any configuration attributes the plugin exposes.
|
17
|
+
|
18
|
+
## Usage
|
19
|
+
|
20
|
+
TODO: Describe the plugin's features and how to use them.
|
21
|
+
|
22
|
+
## License
|
23
|
+
|
24
|
+
[MIT](http://opensource.org/licenses/MIT)
|
data/Rakefile
ADDED
data/lib/lita-jls.rb
ADDED
@@ -0,0 +1,240 @@
|
|
1
|
+
require 'lita-jls/util'
|
2
|
+
require 'rubygems'
|
3
|
+
require 'open3'
|
4
|
+
require 'gems'
|
5
|
+
require 'semverly'
|
6
|
+
|
7
|
+
module LitaJLS
|
8
|
+
module Reporter
|
9
|
+
class HipChat
|
10
|
+
def initialize(build_results)
|
11
|
+
@build_results = build_results
|
12
|
+
end
|
13
|
+
|
14
|
+
def format(message)
|
15
|
+
formatted_message = []
|
16
|
+
|
17
|
+
@build_results.each do |build_result|
|
18
|
+
if build_result.status == :ok
|
19
|
+
formatted_message << " - (success) #{build_result.message}"
|
20
|
+
else
|
21
|
+
if build_result.full_message
|
22
|
+
formatted_message << " - (stare) #{build_result.message} \nstacktrace: #{build_result.error || build_result.full_message}"
|
23
|
+
else
|
24
|
+
formatted_message << " - (stare) #{build_result.message}"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
message.reply(formatted_message.join("\n"))
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class BuildResult
|
35
|
+
attr_reader :status, :full_message, :error, :message
|
36
|
+
|
37
|
+
def initialize(options = {})
|
38
|
+
@status = options[:status]
|
39
|
+
@full_message = options[:full_message]
|
40
|
+
@error = options[:error]
|
41
|
+
@message = options[:message]
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
class BotBuilder
|
46
|
+
include LitaJLS::Logger
|
47
|
+
|
48
|
+
class ConfigurationError < StandardError; end
|
49
|
+
|
50
|
+
GEM_TO_EXCLUDE = ["logstash"].freeze
|
51
|
+
|
52
|
+
TASKS_ORDER = ['bundle install',
|
53
|
+
'bundle exec rake vendor',
|
54
|
+
'bundle exec rspec',
|
55
|
+
'bundle exec rake publish_gem'].freeze
|
56
|
+
|
57
|
+
GEM_CREDENTIALS_FILE = '~/.gem/credentials'
|
58
|
+
|
59
|
+
attr_reader :current_path, :project_name, :ruby_version
|
60
|
+
|
61
|
+
def initialize(path, options = {})
|
62
|
+
@cache_commands = {}
|
63
|
+
@current_path = File.expand_path(path)
|
64
|
+
@project_name = File.basename(current_path)
|
65
|
+
@ruby_version = options.fetch(:ruby_version, nil)
|
66
|
+
@tasks_order = options[:tasks_order] || TASKS_ORDER
|
67
|
+
end
|
68
|
+
|
69
|
+
def is_gem?
|
70
|
+
File.exists?(find_gemspec)
|
71
|
+
end
|
72
|
+
|
73
|
+
def find_gemspec
|
74
|
+
file = [project_name, "gemspec"].join('.')
|
75
|
+
File.join(current_path, file)
|
76
|
+
end
|
77
|
+
|
78
|
+
def gem_specification
|
79
|
+
# HACK: if you are using the `real` bundler way of creating gem
|
80
|
+
# You have to create a version.rb file containing the version number
|
81
|
+
# and require the file in the gemspec.
|
82
|
+
# Ruby will cache this require and not reload it again in a long running
|
83
|
+
# process like the bot.
|
84
|
+
cmd = "ruby -e \"spec = Gem::Specification.load('#{find_gemspec}'); puts [spec.name, spec.version].join(',')\""
|
85
|
+
results = execute_command_with_ruby(cmd)
|
86
|
+
|
87
|
+
if run_successfully?(results)
|
88
|
+
name, version = results.stdout.strip.split(',')
|
89
|
+
gemspec = Struct.new(:name, :version)
|
90
|
+
return gemspec.new(name, version)
|
91
|
+
else
|
92
|
+
raise results.stderr
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def publishable?
|
97
|
+
if is_gem?
|
98
|
+
return !GEM_TO_EXCLUDE.include?(gem_specification.name)
|
99
|
+
else
|
100
|
+
return false
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def run_successfully?(task_result)
|
105
|
+
logger.debug("Check if run run_successfully", :exit_code => task_result.status.inspect, :task_result => task_result.inspect)
|
106
|
+
task_result.status.success?
|
107
|
+
end
|
108
|
+
|
109
|
+
def execution_report(task_result)
|
110
|
+
if run_successfully?(task_result)
|
111
|
+
report_ok(task_result.cmd, task_result.stdout)
|
112
|
+
else
|
113
|
+
report_error(task_result.cmd, task_result.stdout, task_result.stderr)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def cache_command(cmd, options = {})
|
118
|
+
@cache_commands[cmd] ||= execute_command(cmd, options)
|
119
|
+
end
|
120
|
+
|
121
|
+
def report_error(message, full_message = nil, error = nil)
|
122
|
+
BuildResult.new(:status => :error, :message => message, :full_message => full_message, :error => error)
|
123
|
+
end
|
124
|
+
|
125
|
+
def report_ok(message, full_message = nil)
|
126
|
+
BuildResult.new(:status => :ok, :message => message, :full_message => full_message)
|
127
|
+
end
|
128
|
+
|
129
|
+
def fetch_last_released_version(name)
|
130
|
+
# Assume you have correctly configured the ~/gem/credentials file
|
131
|
+
credentials_file = File.expand_path(GEM_CREDENTIALS_FILE)
|
132
|
+
|
133
|
+
if File.exist?(credentials_file)
|
134
|
+
response = Gems.versions(name)
|
135
|
+
if response != 'This rubygem could not be found.'
|
136
|
+
return response.first.fetch('number', nil)
|
137
|
+
else
|
138
|
+
return nil
|
139
|
+
end
|
140
|
+
else
|
141
|
+
raise ConfigurationError.new("Missing rubygems credentials in #{credentials_file}")
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def local_version
|
146
|
+
SemVer.parse(gem_specification.version.to_s)
|
147
|
+
end
|
148
|
+
|
149
|
+
def rubygems_version
|
150
|
+
rubygems_version = fetch_last_released_version(project_name)
|
151
|
+
|
152
|
+
if rubygems_version.nil?
|
153
|
+
return SemVer.new(0, 0, 0)
|
154
|
+
else
|
155
|
+
return SemVer.parse(rubygems_version)
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
def execute_command_with_ruby(cmd)
|
160
|
+
Dir.chdir(current_path) do
|
161
|
+
Bundler.with_clean_env do
|
162
|
+
environment_variables = {}
|
163
|
+
|
164
|
+
if ruby_version
|
165
|
+
if using_rvm?
|
166
|
+
cmd = "rvm #{ruby_version} do #{cmd}"
|
167
|
+
elsif using_rbenv?
|
168
|
+
raise ConfigurationError.new('RBENV is currently not supported')
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
return cache_command(cmd, environment_variables)
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
def execute_command(cmd, environment_variables = {})
|
178
|
+
logger.debug("Running command", :cmd => cmd, :path => current_path)
|
179
|
+
Open3.popen3(environment_variables, cmd, :chdir => current_path) do |input, stdout, stderr, thr|
|
180
|
+
return OpenStruct.new(:stdout => stdout.read,
|
181
|
+
:status => thr.value,
|
182
|
+
:stderr => stderr.read,
|
183
|
+
:cmd => cmd)
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
def using_rvm?
|
188
|
+
run_successfully?(cache_command('which rvm'))
|
189
|
+
end
|
190
|
+
|
191
|
+
def using_rbenv?
|
192
|
+
run_successfully?(cache_command('which rbenv'))
|
193
|
+
end
|
194
|
+
|
195
|
+
def run_tasks
|
196
|
+
messages = []
|
197
|
+
|
198
|
+
@tasks_order.each do |task|
|
199
|
+
result = execute_command_with_ruby(task)
|
200
|
+
messages << execution_report(result)
|
201
|
+
break unless run_successfully?(result)
|
202
|
+
end
|
203
|
+
messages
|
204
|
+
end
|
205
|
+
|
206
|
+
def build
|
207
|
+
messages = []
|
208
|
+
|
209
|
+
if is_gem?
|
210
|
+
if publishable?
|
211
|
+
if local_version < rubygems_version
|
212
|
+
logger.debug("Remote version is higher on rubygems, we dont do anything")
|
213
|
+
|
214
|
+
messages << report_error("Higher version on rubygems (#{rubygems_version}) than the local version (#{local_version}), see http://rubygems.org/gems/#{project_name}")
|
215
|
+
elsif local_version == rubygems_version
|
216
|
+
logger.debug("Same version on rubygems", :local_version => local_version, :rubygems_version => rubygems_version)
|
217
|
+
|
218
|
+
messages << report_error("Local version and rubygems version are the same (#{local_version}|#{rubygems_version}), see http://rubygems.org/gems/#{project_name}")
|
219
|
+
else
|
220
|
+
logger.debug("Start the build process")
|
221
|
+
|
222
|
+
messages.concat(run_tasks)
|
223
|
+
|
224
|
+
if local_version == rubygems_version
|
225
|
+
messages << report_ok("version on rubygems match local version, published #{local_version} see http://rubygems.org/gems/#{project_name}")
|
226
|
+
else
|
227
|
+
messages << report_error("versions on rubygems doesn't match see http://rubygems.org/gems/#{project_name}")
|
228
|
+
end
|
229
|
+
end
|
230
|
+
else
|
231
|
+
messages << report_error("#{project_name} is blacklisted, you cannot deploy it with this tool.")
|
232
|
+
end
|
233
|
+
else
|
234
|
+
messages << report_error("#{project_name} doesn't have a gemspec")
|
235
|
+
end
|
236
|
+
|
237
|
+
messages
|
238
|
+
end
|
239
|
+
end
|
240
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module LitaJLS
|
2
|
+
class GithubUrlParser
|
3
|
+
attr_reader :user, :project, :pr, :url
|
4
|
+
|
5
|
+
URL_BASE = "https://github.com"
|
6
|
+
|
7
|
+
def initialize(git_url, options)
|
8
|
+
@url = git_url
|
9
|
+
full_path = URI.parse(@url).path
|
10
|
+
_, @user, @project, _, @pr = full_path.split('/')
|
11
|
+
|
12
|
+
@options = { :link => :repository }.merge(options)
|
13
|
+
end
|
14
|
+
|
15
|
+
def valid_url?
|
16
|
+
url =~ /^#{URL_BASE.gsub('/', '\/')}/
|
17
|
+
end
|
18
|
+
|
19
|
+
def validate_repository!
|
20
|
+
if user.nil? || project.nil? || !valid_url?
|
21
|
+
raise "Invalid URL. Expected something like: #{URL_BASE}/elasticsearch/snacktime/"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def validate_pull_request!
|
26
|
+
if user.nil? || project.nil? || pr.nil? || !valid_url?
|
27
|
+
raise "Invalid URL. Expected something like: #{URL_BASE}/elasticsearch/snacktime/pull/12345"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def validate!
|
32
|
+
send("validate_#{@options[:link]}!")
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.parse(git_url, options = {})
|
36
|
+
new(git_url, options)
|
37
|
+
end
|
38
|
+
|
39
|
+
def git_url
|
40
|
+
"#{URL_BASE}/#{user}/#{project}"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'lita-jls/util'
|
2
|
+
|
3
|
+
module LitaJLS
|
4
|
+
class Repository
|
5
|
+
include LitaJLS::Util
|
6
|
+
include LitaJLS::Logger
|
7
|
+
|
8
|
+
REMOTE = 'origin'
|
9
|
+
|
10
|
+
def initialize(parsed_url)
|
11
|
+
@parsed_url = parsed_url
|
12
|
+
end
|
13
|
+
|
14
|
+
def clone
|
15
|
+
clone_at(@parsed_url.git_url, git_path)
|
16
|
+
git(git_path, "am", "--abort") if unfinished_rebase?
|
17
|
+
end
|
18
|
+
|
19
|
+
def unfinished_rebase?
|
20
|
+
File.directory?(".git/rebase-apply")
|
21
|
+
end
|
22
|
+
|
23
|
+
def git_path
|
24
|
+
@git_path ||= gitdir(@parsed_url.project)
|
25
|
+
end
|
26
|
+
|
27
|
+
def switch_branch(branch, create_new=false)
|
28
|
+
if create_new
|
29
|
+
logger.info("Creating and switching branches", :branch => branch, :repo => git_path)
|
30
|
+
git(git_path, "checkout", "-b", branch)
|
31
|
+
git(git_path, "reset", "--hard", "#{REMOTE}/master")
|
32
|
+
else
|
33
|
+
logger.info("Switching branches", :branch => branch, :repo => git_path)
|
34
|
+
git(git_path, "checkout", branch)
|
35
|
+
git(git_path, "reset", "--hard", "#{REMOTE}/#{branch}")
|
36
|
+
git(git_path, "pull", "--ff-only")
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def git_patch(patch_file)
|
41
|
+
raise "Previous patch apply had failed. Please resolve it before continuing" if File.directory?(".git/rebase-apply")
|
42
|
+
git(git_path, "am", "--3way", patch_file)
|
43
|
+
end
|
44
|
+
|
45
|
+
def git_push_branch(local_branch)
|
46
|
+
git(git_path, "push", "origin", local_branch)
|
47
|
+
end
|
48
|
+
|
49
|
+
def delete_local_branch(branch, ignore_error=false)
|
50
|
+
begin
|
51
|
+
git(git_path, "branch", "-D", branch)
|
52
|
+
rescue => e
|
53
|
+
raise e unless ignore_error
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,259 @@
|
|
1
|
+
require "cabin"
|
2
|
+
require "fileutils"
|
3
|
+
require "uri"
|
4
|
+
require "faraday" # for cla check
|
5
|
+
require "json" # for cla check
|
6
|
+
#
|
7
|
+
# TODO(sissel): This code needs some suuuper serious refactoring and testing improvements.
|
8
|
+
|
9
|
+
module LitaJLS
|
10
|
+
module Logger
|
11
|
+
def logger
|
12
|
+
return @logger if @logger
|
13
|
+
@logger = Cabin::Channel.get
|
14
|
+
@logger.level = :debug if ENV["DEBUG"]
|
15
|
+
@logger
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
module Util
|
20
|
+
include Logger
|
21
|
+
|
22
|
+
CLANotSigned = Class.new(StandardError)
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
# This method requires @cla_uri being set before it'll work.
|
27
|
+
def cla?(repository, pr)
|
28
|
+
raise "No @cla_uri set. Cannot check CLA signature." unless @cla_uri
|
29
|
+
uri = URI.parse(@cla_uri)
|
30
|
+
conn = Faraday.new(:url => "#{uri.scheme}://#{uri.host}")
|
31
|
+
conn.basic_auth(uri.user, uri.password)
|
32
|
+
response = conn.get(uri.path, :repository => repository, :number => pr)
|
33
|
+
check = JSON.parse(response.body)
|
34
|
+
# TODO(sissel): json exception? .get exception?
|
35
|
+
|
36
|
+
if check["status"] == "error"
|
37
|
+
raise CLANotSigned, check["message"]
|
38
|
+
end
|
39
|
+
|
40
|
+
true
|
41
|
+
end # cla?
|
42
|
+
|
43
|
+
def logstash_team?(user)
|
44
|
+
["electrical",
|
45
|
+
"jordansissel",
|
46
|
+
"ph",
|
47
|
+
"colinsurprenant",
|
48
|
+
"jsvd",
|
49
|
+
"untergeek",
|
50
|
+
"talevy",
|
51
|
+
"kurtado",
|
52
|
+
"suyograo",
|
53
|
+
"purbon"].include?(user.downcase)
|
54
|
+
end
|
55
|
+
|
56
|
+
# Clone a git url into a local path.
|
57
|
+
#
|
58
|
+
# This caches a remote git repo and performs a clone against that in
|
59
|
+
# order to make subsequent clones faster. It will try to provide the
|
60
|
+
# latest (via 'git fetch') after cloning is complete.
|
61
|
+
def clone_at(url, gitpath)
|
62
|
+
# TODO(sissel): Refactor this into two 'clone' calls.
|
63
|
+
logger.debug("clone_at", :url => url, :gitpath => gitpath)
|
64
|
+
|
65
|
+
# Cache a remote git url so that we can clone it more quickly in the
|
66
|
+
# future.
|
67
|
+
cache = File.join(gitdir(File.join("_")), File.basename(gitpath))
|
68
|
+
FileUtils.mkdir_p(cache) unless File.directory?(cache)
|
69
|
+
|
70
|
+
logger.info("Cloning to cache", :url => url, :cache => cache)
|
71
|
+
begin
|
72
|
+
git(".", "clone", url, cache)
|
73
|
+
rescue => e
|
74
|
+
logger.debug("clone_at failed, trying to open repo instead", :cache => cache, :error => e)
|
75
|
+
git(cache, "log", "-n0") # Verify some kind of git works here
|
76
|
+
end
|
77
|
+
remote = "origin"
|
78
|
+
|
79
|
+
# Update from remote if already cloned
|
80
|
+
logger.debug("Fetching a remote", :cache => cache, :remote => remote)
|
81
|
+
git(cache, "fetch", remote)
|
82
|
+
|
83
|
+
# Clone from cache.
|
84
|
+
# This allows us to have multiple local working/clones and just keep
|
85
|
+
# cloning from the local cache. The alternative is to clone from the
|
86
|
+
# remote every time we do a new clone_at, and that would be slow through
|
87
|
+
# github or gitlab.
|
88
|
+
logger.info("Cloning from cache", :cache => cache, :gitpath => gitpath)
|
89
|
+
begin
|
90
|
+
git(".", "clone", cache, gitpath)
|
91
|
+
rescue => e
|
92
|
+
logger.info(e)
|
93
|
+
logger.debug("clone_at from cache failed, trying to open repo instead", :repo => gitpath, :cache => cache)
|
94
|
+
end
|
95
|
+
git(gitpath, "remote", "set-url", remote, url)
|
96
|
+
|
97
|
+
uri = URI.parse(url)
|
98
|
+
push_url = "git@github.com:#{uri.path}.git"
|
99
|
+
git(gitpath, "remote", "set-url", "--push", remote, push_url)
|
100
|
+
git(gitpath, "fetch")
|
101
|
+
|
102
|
+
# TODO(sissel): pull --ff-only?
|
103
|
+
gitpath
|
104
|
+
end # def clone_at
|
105
|
+
|
106
|
+
def gitdir(project)
|
107
|
+
if !@gitdir
|
108
|
+
@gitdir = workdir("gitbase")
|
109
|
+
FileUtils.mkdir_p(@gitdir) unless File.directory?(@gitdir)
|
110
|
+
logger.debug("Git dir", :path => @gitdir)
|
111
|
+
end
|
112
|
+
path = File.join(@gitdir, project)
|
113
|
+
Dir.mkdir(path) unless File.directory?(path)
|
114
|
+
path
|
115
|
+
end # def gitdir
|
116
|
+
|
117
|
+
def workdir(path=nil)
|
118
|
+
return File.join(@workdir, path) if @workdir
|
119
|
+
@workdir = File.join(Dir.tmpdir, "lita-jls")
|
120
|
+
Dir.mkdir(@workdir) unless File.directory?(@workdir)
|
121
|
+
if path.nil?
|
122
|
+
@workdir
|
123
|
+
else
|
124
|
+
File.join(@workdir, path)
|
125
|
+
end
|
126
|
+
end # def workdir
|
127
|
+
|
128
|
+
def apply_patch(repo, patch_body, &block)
|
129
|
+
require "mbox"
|
130
|
+
require "time"
|
131
|
+
# The github '.patch' format is an mbox containing one mail+patch per
|
132
|
+
# commit.
|
133
|
+
mbox = Mbox.new(patch_body)
|
134
|
+
mbox.each_with_index do |mail, i|
|
135
|
+
commit = apply_commit(repo, mail, &block)
|
136
|
+
logger.info("Created commit", :commit => commit, :patch => i)
|
137
|
+
end
|
138
|
+
end # def apply_patch
|
139
|
+
|
140
|
+
def apply_commit(repo, mail, &block)
|
141
|
+
from_re = /([^<]+) (<[^>]+>)/
|
142
|
+
match = from_re.match(mail.headers["from"])
|
143
|
+
name = match[1].gsub(/(^")|("$)/, "")
|
144
|
+
email = match[2].gsub(/^<|>$/, "")
|
145
|
+
if name.nil? || email.nil?
|
146
|
+
raise "Unable to parse name and email from '#{mail.headers["from"]}'. Cannot continue"
|
147
|
+
end
|
148
|
+
time = Time.parse(mail.headers["date"])
|
149
|
+
#File.write("/tmp/x", patch)
|
150
|
+
|
151
|
+
# Take the email subject but strip [PATCH] or [PATCH N/M] out.
|
152
|
+
subject = mail.headers["subject"].gsub(/^\[PATCH[^\]]*\] /, "")
|
153
|
+
# The email body (minus the patch itself) is the rest of the commit message
|
154
|
+
description = /^(?<description>.*\n)?---\n.*$/m.match(mail.content.first.content)["description"] || ""
|
155
|
+
|
156
|
+
if subject.empty? && description.empty?
|
157
|
+
raise "Empty commit message (no subject or description). Refusing to continue."
|
158
|
+
end
|
159
|
+
|
160
|
+
#patch = mail.content.first.content.gsub(/^(?:.*\n)?---\n.*?\n\n/m, "")
|
161
|
+
#patch += "\n" if patch[-1,1] != "\n"
|
162
|
+
# Patch must have a trailing newline.
|
163
|
+
patch = [mail.headers, mail.content, ""].join("\n")
|
164
|
+
|
165
|
+
# Apply the code change to the git index
|
166
|
+
Dir.chdir(repo) do
|
167
|
+
cmd = ["git", "am", "--3way"]
|
168
|
+
File.write("/tmp/patch", patch)
|
169
|
+
IO.popen(cmd, "w+") do |io|
|
170
|
+
io.write(patch)
|
171
|
+
io.close_write
|
172
|
+
logger.pipe(io => :debug)
|
173
|
+
end
|
174
|
+
status = $?
|
175
|
+
if !status.success?
|
176
|
+
logger.warn("Git am failed", :code => status.exitstatus, :command => cmd, :pwd => Dir.pwd)
|
177
|
+
git(repo, "am", "--abort")
|
178
|
+
raise "Git am failed: #{cmd.join(" ")}"
|
179
|
+
end
|
180
|
+
logger.info("Git am successful!", :code => status.exitstatus, :command => cmd, :pwd => Dir.pwd)
|
181
|
+
end
|
182
|
+
|
183
|
+
# Combine subject + description for the full commit message
|
184
|
+
message = "#{subject}\n\n#{description}"
|
185
|
+
|
186
|
+
commit_settings = {
|
187
|
+
# TODO(sissel): override the committer with whoever
|
188
|
+
#:author => { :email => email, :name => name, :time => time },
|
189
|
+
#:committer => { :email => "jls@semicomplete.com", :name => "Jordan Sissel", :time => Time.now },
|
190
|
+
:message => message
|
191
|
+
}
|
192
|
+
|
193
|
+
# Allow any modifications to the commit object itself.
|
194
|
+
block.call(commit_settings)
|
195
|
+
|
196
|
+
Dir.chdir(repo) do
|
197
|
+
cmd = ["git", "commit", "--amend", "-F-"]
|
198
|
+
IO.popen(cmd, "w+") do |io|
|
199
|
+
io.write(commit_settings[:message])
|
200
|
+
io.close_write
|
201
|
+
logger.pipe(io => :debug)
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
end # def apply_commit
|
206
|
+
|
207
|
+
def system!(*args)
|
208
|
+
logger.debug("Running command", :args => args)
|
209
|
+
# TODO(sissel): use Open4
|
210
|
+
IO.popen(args, "r") do |io|
|
211
|
+
logger.pipe(io => :debug)
|
212
|
+
end
|
213
|
+
status = $?
|
214
|
+
return if status.success?
|
215
|
+
raise "Command failed; #{args.inspect}"
|
216
|
+
end
|
217
|
+
|
218
|
+
def github_client
|
219
|
+
require "octokit"
|
220
|
+
# This requires you have ~/.netrc setup correctly
|
221
|
+
# I don't know if it works with 2FA
|
222
|
+
@client ||= Octokit::Client.new(:netrc => true).tap do |client|
|
223
|
+
client.login
|
224
|
+
client.auto_paginate = true
|
225
|
+
end
|
226
|
+
end # def client
|
227
|
+
|
228
|
+
def github_issue_label(project, issue, labels)
|
229
|
+
logger.debug('Adding label to a specific issue', :project => project, :issue => issue, :labels => labels)
|
230
|
+
github_client.add_labels_to_an_issue(project, issue, labels) unless labels.empty?
|
231
|
+
rescue => e
|
232
|
+
raise e.class, "Failed adding label '#{labels}' to issue #{issue} on #{project}: #{e}"
|
233
|
+
end # def github_issue_label
|
234
|
+
|
235
|
+
def github_issue_comment(project, issue, comment)
|
236
|
+
logger.debug('Adding a comment to an given issue', :project => project, :issue => issue, :comment => comment)
|
237
|
+
github_client.add_comment(project, issue, comment) unless comment.empty?
|
238
|
+
rescue => e
|
239
|
+
raise e.class, "Failed adding a comment #{comment} to issue #{issue} on #{project}: #{e}"
|
240
|
+
|
241
|
+
end # def github_issue_comment
|
242
|
+
|
243
|
+
def github_create_pr(project, branch, title, body)
|
244
|
+
logger.debug('Creating a pull request', :project => project, :branch => branch)
|
245
|
+
github_client.create_pull_request(project, "master", branch, title, body)
|
246
|
+
end
|
247
|
+
|
248
|
+
def github_get_pr(project, pr_num)
|
249
|
+
github_client.pull_request(project, pr_num)
|
250
|
+
end
|
251
|
+
|
252
|
+
def git(gitdir, *args)
|
253
|
+
Dir.chdir(gitdir) do
|
254
|
+
system!("git", *args)
|
255
|
+
end
|
256
|
+
end
|
257
|
+
end # module Util
|
258
|
+
|
259
|
+
end # module LitaJLS
|