solano 1.31.0
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 +15 -0
- data/bin/solano +29 -0
- data/bin/tddium +29 -0
- data/lib/solano.rb +19 -0
- data/lib/solano/agent.rb +3 -0
- data/lib/solano/agent/solano.rb +128 -0
- data/lib/solano/cli.rb +25 -0
- data/lib/solano/cli/api.rb +368 -0
- data/lib/solano/cli/commands/account.rb +50 -0
- data/lib/solano/cli/commands/activate.rb +16 -0
- data/lib/solano/cli/commands/api.rb +15 -0
- data/lib/solano/cli/commands/config.rb +78 -0
- data/lib/solano/cli/commands/console.rb +85 -0
- data/lib/solano/cli/commands/describe.rb +104 -0
- data/lib/solano/cli/commands/find_failing.rb +64 -0
- data/lib/solano/cli/commands/heroku.rb +17 -0
- data/lib/solano/cli/commands/hg.rb +48 -0
- data/lib/solano/cli/commands/keys.rb +81 -0
- data/lib/solano/cli/commands/login.rb +37 -0
- data/lib/solano/cli/commands/logout.rb +14 -0
- data/lib/solano/cli/commands/password.rb +26 -0
- data/lib/solano/cli/commands/rerun.rb +59 -0
- data/lib/solano/cli/commands/server.rb +21 -0
- data/lib/solano/cli/commands/spec.rb +401 -0
- data/lib/solano/cli/commands/status.rb +117 -0
- data/lib/solano/cli/commands/stop.rb +19 -0
- data/lib/solano/cli/commands/suite.rb +110 -0
- data/lib/solano/cli/commands/support.rb +24 -0
- data/lib/solano/cli/commands/web.rb +29 -0
- data/lib/solano/cli/config.rb +246 -0
- data/lib/solano/cli/params_helper.rb +66 -0
- data/lib/solano/cli/prompt.rb +128 -0
- data/lib/solano/cli/show.rb +136 -0
- data/lib/solano/cli/solano.rb +208 -0
- data/lib/solano/cli/suite.rb +104 -0
- data/lib/solano/cli/text_helper.rb +16 -0
- data/lib/solano/cli/timeformat.rb +21 -0
- data/lib/solano/cli/util.rb +132 -0
- data/lib/solano/constant.rb +581 -0
- data/lib/solano/scm.rb +18 -0
- data/lib/solano/scm/configure.rb +37 -0
- data/lib/solano/scm/git.rb +349 -0
- data/lib/solano/scm/git_log_parser.rb +67 -0
- data/lib/solano/scm/hg.rb +263 -0
- data/lib/solano/scm/hg_log_parser.rb +66 -0
- data/lib/solano/scm/scm.rb +119 -0
- data/lib/solano/scm/scm_stub.rb +9 -0
- data/lib/solano/scm/url.rb +75 -0
- data/lib/solano/script.rb +12 -0
- data/lib/solano/script/git-remote-hg +1258 -0
- data/lib/solano/ssh.rb +66 -0
- data/lib/solano/util.rb +63 -0
- data/lib/solano/version.rb +5 -0
- metadata +413 -0
data/lib/solano/scm.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# Copyright (c) 2011-2015 Solano Labs All Rights Reserved
|
2
|
+
|
3
|
+
module Solano
|
4
|
+
class SCM
|
5
|
+
SCMS = %w(git hg)
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
require 'solano/scm/git_log_parser'
|
10
|
+
require 'solano/scm/hg_log_parser'
|
11
|
+
|
12
|
+
require 'solano/scm/configure'
|
13
|
+
require 'solano/scm/url'
|
14
|
+
|
15
|
+
require 'solano/scm/scm'
|
16
|
+
require 'solano/scm/scm_stub'
|
17
|
+
require 'solano/scm/git'
|
18
|
+
require 'solano/scm/hg'
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# Copyright (c) 2011-2015 Solano Labs All Rights Reserved
|
2
|
+
|
3
|
+
module Solano
|
4
|
+
class SCM
|
5
|
+
def self.configure
|
6
|
+
scm = nil
|
7
|
+
ok = false
|
8
|
+
scms = [::Solano::Git, ::Solano::Hg]
|
9
|
+
|
10
|
+
# Select SCM based on command availability and current repo type
|
11
|
+
scms.each do |scm_class|
|
12
|
+
sniff_scm = scm_class.new
|
13
|
+
if sniff_scm.repo? && scm_class.version_ok then
|
14
|
+
ok = true
|
15
|
+
scm = sniff_scm
|
16
|
+
break
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# Fall back to first SCM type that is available
|
21
|
+
if !ok then
|
22
|
+
scms.each do |scm_class|
|
23
|
+
sniff_scm = scm_class.new
|
24
|
+
if scm_class.version_ok then
|
25
|
+
ok = true
|
26
|
+
scm = sniff_scm
|
27
|
+
break
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Default to a null SCM implementation
|
33
|
+
scm ||= ::Solano::StubSCM.new
|
34
|
+
return [scm, ok]
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,349 @@
|
|
1
|
+
# Copyright (c) 2011-2016 Solano Labs All Rights Reserved
|
2
|
+
|
3
|
+
module Solano
|
4
|
+
class Git < SCM
|
5
|
+
include SolanoConstant
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
super
|
9
|
+
end
|
10
|
+
|
11
|
+
def scm_name
|
12
|
+
return 'git'
|
13
|
+
end
|
14
|
+
|
15
|
+
def repo?
|
16
|
+
if File.directory?('.git') then
|
17
|
+
return true
|
18
|
+
end
|
19
|
+
ignore = `git status 2>&1`
|
20
|
+
ok = $?.success?
|
21
|
+
return ok
|
22
|
+
end
|
23
|
+
|
24
|
+
def root
|
25
|
+
root = `git rev-parse --show-toplevel 2>&1`
|
26
|
+
if $?.exitstatus == 0 then
|
27
|
+
root.chomp! if root
|
28
|
+
return root
|
29
|
+
end
|
30
|
+
return Dir.pwd
|
31
|
+
end
|
32
|
+
|
33
|
+
def mirror_path
|
34
|
+
return nil
|
35
|
+
end
|
36
|
+
|
37
|
+
def repo_name
|
38
|
+
return File.basename(self.root)
|
39
|
+
end
|
40
|
+
|
41
|
+
def origin_url
|
42
|
+
return @default_origin_url if @default_origin_url
|
43
|
+
|
44
|
+
result = `git config --get remote.origin.url`
|
45
|
+
return nil unless $?.success?
|
46
|
+
|
47
|
+
result = result.strip
|
48
|
+
|
49
|
+
# no slashes before first colon
|
50
|
+
# [user@]host.xz:path/to/repo.git/
|
51
|
+
scp_pat = /^([A-Za-z0-9_]+@)?([A-Za-z0-9._-]+):\/?([^\/].*)/
|
52
|
+
if m = scp_pat.match(result) then
|
53
|
+
result = "ssh://#{m[1]}#{m[2]}/#{m[3]}"
|
54
|
+
end
|
55
|
+
|
56
|
+
return result
|
57
|
+
end
|
58
|
+
|
59
|
+
def ignore_path
|
60
|
+
path = File.join(self.root, Config::GIT_IGNORE)
|
61
|
+
return path
|
62
|
+
end
|
63
|
+
|
64
|
+
def current_branch
|
65
|
+
`git symbolic-ref HEAD`.gsub("\n", "").split("/")[2..-1].join("/")
|
66
|
+
end
|
67
|
+
|
68
|
+
def default_branch
|
69
|
+
`git remote show origin | grep HEAD | awk '{print $3}'`.gsub("\n", "")
|
70
|
+
end
|
71
|
+
|
72
|
+
# XXX DANGER: This method will edit the current workspace. It's meant to
|
73
|
+
# be run to make a git mirror up-to-date.
|
74
|
+
def checkout(branch, options={})
|
75
|
+
if !!options[:update] then
|
76
|
+
`git fetch origin`
|
77
|
+
return false if !$?.success?
|
78
|
+
end
|
79
|
+
|
80
|
+
cmd = "git checkout "
|
81
|
+
if !!options[:force] then
|
82
|
+
cmd += "-f "
|
83
|
+
end
|
84
|
+
cmd += Shellwords.shellescape(branch)
|
85
|
+
`#{cmd}`
|
86
|
+
|
87
|
+
return false if !$?.success?
|
88
|
+
|
89
|
+
`git reset --hard origin/#{branch}`
|
90
|
+
return $?.success?
|
91
|
+
end
|
92
|
+
|
93
|
+
def changes?(options={})
|
94
|
+
return Solano::Git.git_changes?(:exclude=>".gitignore")
|
95
|
+
end
|
96
|
+
|
97
|
+
def push_latest(session_data, suite_details, options={})
|
98
|
+
branch = options[:branch] || self.current_branch
|
99
|
+
remote_branch = options[:remote_branch] || branch
|
100
|
+
git_repo_uri = if options[:git_repo_uri] then
|
101
|
+
options[:git_repo_uri]
|
102
|
+
elsif options[:use_private_uri] then
|
103
|
+
suite_details["git_repo_private_uri"] || suite_details["git_repo_uri"]
|
104
|
+
else
|
105
|
+
suite_details["git_repo_uri"]
|
106
|
+
end
|
107
|
+
this_ref = (session_data['commit_data'] || {})['git_ref']
|
108
|
+
refs = this_ref ? ["HEAD:#{this_ref}"] : []
|
109
|
+
|
110
|
+
if options[:git_repo_origin_uri] then
|
111
|
+
Solano::Git.git_set_remotes(options[:git_repo_origin_uri], 'origin')
|
112
|
+
end
|
113
|
+
|
114
|
+
Solano::Git.git_set_remotes(git_repo_uri)
|
115
|
+
return Solano::Git.git_push(branch, refs, remote_branch)
|
116
|
+
end
|
117
|
+
|
118
|
+
def current_commit
|
119
|
+
`git rev-parse --verify HEAD`.strip
|
120
|
+
end
|
121
|
+
|
122
|
+
def commits
|
123
|
+
commits = GitCommitLogParser.new(self.latest_commit).commits
|
124
|
+
return commits
|
125
|
+
end
|
126
|
+
|
127
|
+
def number_of_commits(id_from, id_to)
|
128
|
+
result = `git log --pretty='%H' #{id_from}..#{id_to}`
|
129
|
+
result.split("\n").length
|
130
|
+
end
|
131
|
+
|
132
|
+
def offer_snapshot_creation(session_id, options={})
|
133
|
+
say Text::Process::ASK_FOR_SNAPSHOT
|
134
|
+
answer = STDIN.gets.chomp
|
135
|
+
if /Y/.match(answer) then
|
136
|
+
create_snapshot(session_id, options.merge({ :force=>true }))
|
137
|
+
else
|
138
|
+
raise Text::Error::ANSWER_NOT_Y
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def create_snapshot(session_id, options={})
|
143
|
+
api = options[:api]
|
144
|
+
res = api.request_snapshot_url({:session_id => session_id})
|
145
|
+
auth_url = res['auth_url']
|
146
|
+
|
147
|
+
say Text::Process::SNAPSHOT_URL % auth_url
|
148
|
+
|
149
|
+
unique = SecureRandom.hex(10)
|
150
|
+
snaphot_path = File.join(Dir.tmpdir,".solano-#{unique}-snapshot")
|
151
|
+
file = File.join(Dir.tmpdir, "solano-#{unique}-snapshot.tar")
|
152
|
+
|
153
|
+
if !options[:force] then
|
154
|
+
#git default branch
|
155
|
+
branch = options[:default_branch]
|
156
|
+
branch ||= self.default_branch
|
157
|
+
if branch.nil? then
|
158
|
+
raise Text::Error::DEFAULT_BRANCH
|
159
|
+
end
|
160
|
+
if branch == (`git rev-parse --abbrev-ref HEAD`).strip && !/Your branch is up-to-date with/.match(`git status`).nil? then
|
161
|
+
raise Text::Error::NEED_TO_FORCE % branch
|
162
|
+
end
|
163
|
+
say Text::Process::CREATING_REPO_SNAPSHOT_BRANCH % [root, branch]
|
164
|
+
out = `git clone --mirror -b #{branch} #{root} #{snaphot_path}`
|
165
|
+
if !$?.success? then
|
166
|
+
raise Text::Error::FAILED_TO_CREATE_SNAPSHOT % out
|
167
|
+
end
|
168
|
+
else
|
169
|
+
say Text::Process::CREATING_REPO_SNAPSHOT % root
|
170
|
+
out = `git clone --mirror #{root} #{snaphot_path}`
|
171
|
+
if !$?.success? then
|
172
|
+
raise Text::Error::FAILED_TO_CREATE_SNAPSHOT % out
|
173
|
+
end
|
174
|
+
end
|
175
|
+
out = `tar -C #{snaphot_path} -czpf #{file} .`
|
176
|
+
upload_file(auth_url, file)
|
177
|
+
Dir.chdir(snaphot_path){
|
178
|
+
@snap_id = (`git rev-parse HEAD`).strip
|
179
|
+
}
|
180
|
+
|
181
|
+
desc = {"url" => auth_url.gsub(/\?.*/,''),
|
182
|
+
"size" => File.stat(file).size,
|
183
|
+
"sha1"=> Digest::SHA1.file(file).hexdigest.upcase,
|
184
|
+
"commit_id"=> @snap_id,
|
185
|
+
"session_id" => session_id,
|
186
|
+
}
|
187
|
+
api.update_snapshot({:repo_snapshot => desc})
|
188
|
+
ensure
|
189
|
+
FileUtils.rm_rf(snaphot_path) if snaphot_path && File.exists?(snaphot_path)
|
190
|
+
FileUtils.rm_f(file) if file && File.exists?(file)
|
191
|
+
end
|
192
|
+
|
193
|
+
def create_patch(session_id, options={})
|
194
|
+
#oldest version of git that has been tested with diff patching
|
195
|
+
if !check_version('1.7.12.4') then
|
196
|
+
say Text::Warning::SAME_SNAPSHOT_COMMIT
|
197
|
+
warn(Text::Warning::GIT_VERSION_FOR_PATCH)
|
198
|
+
raise
|
199
|
+
end
|
200
|
+
api = options[:api]
|
201
|
+
patch_base_sha = options[:commit]
|
202
|
+
if "#{patch_base_sha}" == self.current_commit then
|
203
|
+
say Text::Warning::SAME_SNAPSHOT_COMMIT
|
204
|
+
return
|
205
|
+
end
|
206
|
+
#check if snapshot commit is known locally
|
207
|
+
`git branch -q --contains #{patch_base_sha}`
|
208
|
+
if !$?.success? then
|
209
|
+
#try and create a patch from upstream instread of repo snapshot
|
210
|
+
upstream = self.origin_url
|
211
|
+
reg = Regexp.new('([^\s]*)\s*' + upstream.to_s + '\s*\(fetch\)')
|
212
|
+
if !upstream.nil? && (reg_match = reg.match(`git remote -v`)) then
|
213
|
+
origin_name = reg_match[1]
|
214
|
+
end
|
215
|
+
origin_name ||= "origin"
|
216
|
+
say Text::Process::ATTEMPT_UPSTREAM_PATCH % upstream
|
217
|
+
#should be the remote name
|
218
|
+
patch_base_sha = `git rev-parse #{origin_name}`.to_s.strip
|
219
|
+
if !$?.success? then
|
220
|
+
say Text::Error::PATCH_CREATION_ERROR % patch_base_sha
|
221
|
+
offer_snapshot_creation(session_id, :api=>api)
|
222
|
+
return
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
file_name = "solano-#{SecureRandom.hex(10)}.patch"
|
227
|
+
file_path = File.join(Dir.tmpdir, file_name)
|
228
|
+
say Text::Process::CREATING_PATCH % [patch_base_sha, self.current_commit]
|
229
|
+
out = ` git diff-index -p --minimal #{patch_base_sha} > #{file_path}`
|
230
|
+
if !$?.success? then
|
231
|
+
say Text::Error::FAILED_TO_CREATE_PATCH % [patch_base_sha, out]
|
232
|
+
offer_snapshot_creation(session_id, :api=>api)
|
233
|
+
return
|
234
|
+
end
|
235
|
+
|
236
|
+
file_size = File.size(file_path)
|
237
|
+
if file_size != 0 then
|
238
|
+
|
239
|
+
file_sha1 = Digest::SHA1.file(file_path).hexdigest.upcase
|
240
|
+
|
241
|
+
#upload patch
|
242
|
+
say Text::Process::REQUST_PATCH_URL
|
243
|
+
res = api.request_patch_url({:session_id => session_id})
|
244
|
+
if (auth_url = res['auth_url']) then
|
245
|
+
say Text::Process::UPLOAD_PATCH % auth_url
|
246
|
+
upload_file(auth_url, file_path)
|
247
|
+
else
|
248
|
+
raise Text::Error::NO_PATCH_URL
|
249
|
+
end
|
250
|
+
|
251
|
+
args = { :session_id => session_id,
|
252
|
+
:sha1 => file_sha1,
|
253
|
+
:size => file_size,
|
254
|
+
:base_commit => patch_base_sha,
|
255
|
+
:git_version_used => current_version,
|
256
|
+
:cli_version_used => Solano::VERSION,
|
257
|
+
}
|
258
|
+
|
259
|
+
api.upload_session_patch(args)
|
260
|
+
else
|
261
|
+
say Text::Warning::EMPTY_PATCH
|
262
|
+
return
|
263
|
+
end
|
264
|
+
|
265
|
+
ensure
|
266
|
+
FileUtils.rm_rf(file_path) if file_path && File.exists?(file_path)
|
267
|
+
end
|
268
|
+
|
269
|
+
def current_version
|
270
|
+
`git --version`.strip.match(Dependency::VERSION_REGEXP)[0] rescue nil
|
271
|
+
end
|
272
|
+
|
273
|
+
def check_version(allowed_version)
|
274
|
+
Gem::Version.new(allowed_version) <= Gem::Version.new(current_version)
|
275
|
+
end
|
276
|
+
|
277
|
+
protected
|
278
|
+
|
279
|
+
def latest_commit
|
280
|
+
`git log --pretty='%H%n%s%n%aN%n%aE%n%at%n%cN%n%cE%n%ct%n' -1`
|
281
|
+
end
|
282
|
+
|
283
|
+
class << self
|
284
|
+
include SolanoConstant
|
285
|
+
|
286
|
+
def git_changes?(options={})
|
287
|
+
options[:exclude] ||= []
|
288
|
+
options[:exclude] = [options[:exclude]] unless options[:exclude].is_a?(Array)
|
289
|
+
cmd = "git status --porcelain -uno"
|
290
|
+
p = IO.popen(cmd)
|
291
|
+
changes = false
|
292
|
+
while line = p.gets do
|
293
|
+
line = line.strip
|
294
|
+
status, name = line.split(/\s+/)
|
295
|
+
next if options[:exclude].include?(name)
|
296
|
+
if status !~ /^\?/ then
|
297
|
+
changes = true
|
298
|
+
break
|
299
|
+
end
|
300
|
+
end
|
301
|
+
unless $?.success? then
|
302
|
+
warn(Text::Warning::SCM_UNABLE_TO_DETECT)
|
303
|
+
return false
|
304
|
+
end
|
305
|
+
return changes
|
306
|
+
end
|
307
|
+
|
308
|
+
def git_set_remotes(git_repo_uri, remote_name=nil)
|
309
|
+
remote_name ||= Config::REMOTE_NAME
|
310
|
+
|
311
|
+
unless `git remote show -n #{remote_name}` =~ /#{git_repo_uri}/
|
312
|
+
IO.popen("git remote rm #{remote_name}") {} # Discard output on *nix & windows
|
313
|
+
`git remote add #{remote_name} #{git_repo_uri.shellescape}`
|
314
|
+
end
|
315
|
+
end
|
316
|
+
|
317
|
+
def git_push(this_branch, additional_refs=[], remote_branch=nil)
|
318
|
+
say Text::Process::SCM_PUSH
|
319
|
+
remote_branch ||= this_branch
|
320
|
+
refs = ["#{this_branch}:#{remote_branch}"]
|
321
|
+
refs += additional_refs
|
322
|
+
refspec = refs.map(&:shellescape).join(" ")
|
323
|
+
cmd = "git push -f #{Config::REMOTE_NAME} #{refspec}"
|
324
|
+
say "Running '#{cmd}'"
|
325
|
+
system(cmd)
|
326
|
+
end
|
327
|
+
|
328
|
+
def version_ok
|
329
|
+
version = nil
|
330
|
+
begin
|
331
|
+
version_string = `git --version`
|
332
|
+
m = version_string.match(Dependency::VERSION_REGEXP)
|
333
|
+
version = m[0] unless m.nil?
|
334
|
+
rescue Errno
|
335
|
+
rescue Exception
|
336
|
+
end
|
337
|
+
if version.nil? || version.empty? then
|
338
|
+
return false
|
339
|
+
end
|
340
|
+
version_parts = version.split(".")
|
341
|
+
if version_parts[0].to_i < 1 ||
|
342
|
+
(version_parts[0].to_i < 2 && version_parts[1].to_i == 1 && version_parts[1].to_i < 7) then
|
343
|
+
warn(Text::Warning::GIT_VERSION % version)
|
344
|
+
end
|
345
|
+
true
|
346
|
+
end
|
347
|
+
end
|
348
|
+
end
|
349
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# Copyright (c) 2011, 2012, 2013, 2014 Solano Labs All Rights Reserved
|
2
|
+
|
3
|
+
# this is not namespaced under Solano because we want to eventually move this out into another gem
|
4
|
+
class GitCommitLogParser
|
5
|
+
attr_accessor :commit_log
|
6
|
+
|
7
|
+
# example commit_log generated by
|
8
|
+
# `git log --pretty='%H%n%s%n%aN%n%aE%n%at%n%cN%n%cE%n%ct%n' HEAD^..HEAD`
|
9
|
+
|
10
|
+
# 15e8cbd88d68d210953d51c28e26c6b9944a313b
|
11
|
+
# ignore .ruby-version for rvm
|
12
|
+
# Bob Smith
|
13
|
+
# bob@example.com
|
14
|
+
# 1367556311
|
15
|
+
# Fred Smith
|
16
|
+
# fred@example.com
|
17
|
+
# 1367556311
|
18
|
+
#
|
19
|
+
|
20
|
+
def initialize(commit_log)
|
21
|
+
@commit_log = commit_log
|
22
|
+
end
|
23
|
+
|
24
|
+
# Returns a list of commits in the following format
|
25
|
+
# [{
|
26
|
+
# "id" => "15e8cbd88d68d210953d51c28e26c6b9944a313b",
|
27
|
+
# "author" => {"name"=>"Bob Smith", "email"=>"bob@example.com"},
|
28
|
+
# "committer" => {"name"=>"Fred Smith", "email"=>"fred@example.com"},
|
29
|
+
# "summary" => "ignore .ruby-version for rvm",
|
30
|
+
# "date" => 1380603292
|
31
|
+
# }]
|
32
|
+
|
33
|
+
def commits
|
34
|
+
record = []
|
35
|
+
commits = []
|
36
|
+
commit_log.lines.each do |line|
|
37
|
+
line.strip!
|
38
|
+
line.sanitize!
|
39
|
+
if line.empty?
|
40
|
+
c = parse_commit(record)
|
41
|
+
commits.push(c)
|
42
|
+
record = []
|
43
|
+
else
|
44
|
+
record.push(line)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
commits
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def parse_commit(record)
|
54
|
+
time = record[4].to_i
|
55
|
+
author = build_user(record[2], record[3])
|
56
|
+
committer = build_user(record[5], record[6])
|
57
|
+
build_commit(record[0], author, committer, record[1], time)
|
58
|
+
end
|
59
|
+
|
60
|
+
def build_user(name, email)
|
61
|
+
{"name" => name, "email" => email}
|
62
|
+
end
|
63
|
+
|
64
|
+
def build_commit(sha, author, committer, summary, date)
|
65
|
+
{"id" => sha, "author" => author, "committer" => committer, "summary" => summary, "date" => date}
|
66
|
+
end
|
67
|
+
end
|