right_git 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.
- data/CHANGELOG.rdoc +3 -0
- data/LICENSE +20 -0
- data/README.rdoc +13 -0
- data/Rakefile +48 -0
- data/VERSION +1 -0
- data/lib/right_git/git/branch.rb +137 -0
- data/lib/right_git/git/branch_collection.rb +107 -0
- data/lib/right_git/git/commit.rb +76 -0
- data/lib/right_git/git/repository.rb +368 -0
- data/lib/right_git/git/tag.rb +82 -0
- data/lib/right_git/git.rb +36 -0
- data/lib/right_git/shell/default.rb +191 -0
- data/lib/right_git/shell/interface.rb +74 -0
- data/lib/right_git/shell.rb +33 -0
- data/lib/right_git.rb +29 -0
- data/right_git.gemspec +61 -0
- metadata +191 -0
@@ -0,0 +1,368 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (c) 2013 RightScale Inc
|
3
|
+
#
|
4
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
5
|
+
# a copy of this software and associated documentation files (the
|
6
|
+
# "Software"), to deal in the Software without restriction, including
|
7
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
8
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
9
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
10
|
+
# the following conditions:
|
11
|
+
#
|
12
|
+
# The above copyright notice and this permission notice shall be
|
13
|
+
# included in all copies or substantial portions of the Software.
|
14
|
+
#
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
17
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
18
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
19
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
20
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
21
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
22
|
+
|
23
|
+
# ancestor
|
24
|
+
require 'right_git/git'
|
25
|
+
|
26
|
+
module RightGit::Git
|
27
|
+
|
28
|
+
# Provides an API for managing a git repository that is suitable for
|
29
|
+
# automation. It is assumed that gestures like creating a new repository,
|
30
|
+
# branch or tag are manual tasks beyond the scope of automation so those are
|
31
|
+
# not covered here. What is provided are APIs for cloning, fetching, listing
|
32
|
+
# and grooming git-related objects.
|
33
|
+
class Repository
|
34
|
+
COMMIT_SHA1_REGEX = /^commit ([0-9a-fA-F]{40})$/
|
35
|
+
|
36
|
+
SUBMODULE_STATUS_REGEX = /^([+\- ])([0-9a-fA-F]{40}) (.*) (.*)$/
|
37
|
+
|
38
|
+
attr_reader :repo_dir, :logger, :shell
|
39
|
+
|
40
|
+
# @param [String] repo_dir for git actions or '.'
|
41
|
+
# @param [Hash] options for repository
|
42
|
+
# @option options [Object] :shell for git command execution (default = DefaultShell)
|
43
|
+
# @option options [Logger] :logger for logging (default = STDOUT)
|
44
|
+
def initialize(repo_dir, options = {})
|
45
|
+
options = {
|
46
|
+
:shell => nil,
|
47
|
+
:logger => nil
|
48
|
+
}.merge(options)
|
49
|
+
if repo_dir && ::File.directory?(repo_dir)
|
50
|
+
@repo_dir = ::File.expand_path(repo_dir)
|
51
|
+
else
|
52
|
+
raise ::ArgumentError.new('A valid repo_dir is required')
|
53
|
+
end
|
54
|
+
@shell = options[:shell] || ::RightGit::Shell::Default
|
55
|
+
@logger = options[:logger] || ::RightGit::Shell::Default.default_logger
|
56
|
+
end
|
57
|
+
|
58
|
+
# Factory method to clone the repo given by URL to the given destination and
|
59
|
+
# return a new Repository object.
|
60
|
+
#
|
61
|
+
# Note that cloning to the default working directory-relative location is
|
62
|
+
# not currently supported.
|
63
|
+
#
|
64
|
+
# @param [String] repo_url to clone
|
65
|
+
# @param [String] destination path where repo is cloned
|
66
|
+
# @param [Hash] options for repository
|
67
|
+
#
|
68
|
+
# @return [Repository] new repository
|
69
|
+
def self.clone_to(repo_url, destination, options = {})
|
70
|
+
destination = ::File.expand_path(destination)
|
71
|
+
git_args = ['clone', '--', repo_url, destination]
|
72
|
+
expected_git_dir = ::File.join(destination, '.git')
|
73
|
+
if ::File.directory?(expected_git_dir)
|
74
|
+
raise ::ArgumentError,
|
75
|
+
"Destination is already a git repository: #{destination.inspect}"
|
76
|
+
end
|
77
|
+
repo = self.new('.', options)
|
78
|
+
repo.vet_output(git_args)
|
79
|
+
if ::File.directory?(expected_git_dir)
|
80
|
+
repo.instance_variable_set(:@repo_dir, destination)
|
81
|
+
else
|
82
|
+
raise GitError,
|
83
|
+
"Failed to clone #{repo_url.inspect} to #{destination.inspect}"
|
84
|
+
end
|
85
|
+
repo
|
86
|
+
end
|
87
|
+
|
88
|
+
# Fetches using the given options, if any.
|
89
|
+
#
|
90
|
+
# @param [Array] args for fetch
|
91
|
+
#
|
92
|
+
# @return [TrueClass] always true
|
93
|
+
def fetch(*args)
|
94
|
+
vet_output(['fetch', args])
|
95
|
+
true
|
96
|
+
end
|
97
|
+
|
98
|
+
# Fetches branch and tag information from remote origin.
|
99
|
+
#
|
100
|
+
# @param [Hash] options for fetch all
|
101
|
+
# @option options [TrueClass|FalseClass] :prune as true to prune dead branches
|
102
|
+
#
|
103
|
+
# @return [TrueClass] always true
|
104
|
+
def fetch_all(options = {})
|
105
|
+
options = { :prune => false }.merge(options)
|
106
|
+
git_args = ['--all']
|
107
|
+
git_args << '--prune' if options[:prune]
|
108
|
+
fetch(git_args)
|
109
|
+
fetch('--tags') # need a separate call for tags or else you don't get all the tags
|
110
|
+
true
|
111
|
+
end
|
112
|
+
|
113
|
+
# Factory method for a branch object referencing this repository.
|
114
|
+
#
|
115
|
+
# @param [String] branch_name for reference
|
116
|
+
#
|
117
|
+
# @return [Branch] new branch
|
118
|
+
def branch_for(branch_name)
|
119
|
+
Branch.new(self, branch_name)
|
120
|
+
end
|
121
|
+
|
122
|
+
# Generates a list of known (checked-out) branches from the current git
|
123
|
+
# directory.
|
124
|
+
#
|
125
|
+
# @param [Hash] options for branches
|
126
|
+
# @option options [TrueClass|FalseClass] :all is true to include remote branches (default), else local only
|
127
|
+
#
|
128
|
+
# @return [Array] list of branches
|
129
|
+
def branches(options = {})
|
130
|
+
options = {
|
131
|
+
:all => true
|
132
|
+
}.merge(options)
|
133
|
+
git_args = ['branch']
|
134
|
+
git_args << '-a' if options[:all] # note older versions of git don't accept --all
|
135
|
+
branches = BranchCollection.new(self)
|
136
|
+
git_output(git_args).lines.each do |line|
|
137
|
+
# ignore the no-branch branch that git helpfully provides when current
|
138
|
+
# HEAD is a tag or otherwise not-a-branch.
|
139
|
+
unless line.strip == '* (no branch)'
|
140
|
+
branch = Branch.new(self, line)
|
141
|
+
branches << branch if branch
|
142
|
+
end
|
143
|
+
end
|
144
|
+
branches
|
145
|
+
end
|
146
|
+
|
147
|
+
# Factory method for a tag object referencing this repository.
|
148
|
+
#
|
149
|
+
# @param [String] tag_name for reference
|
150
|
+
#
|
151
|
+
# @return [Branch] new branch
|
152
|
+
def tag_for(tag_name)
|
153
|
+
Tag.new(self, tag_name)
|
154
|
+
end
|
155
|
+
|
156
|
+
# Generates a list of known (fetched) tags from the current git directory.
|
157
|
+
#
|
158
|
+
# @return [Array] list of tags
|
159
|
+
def tags
|
160
|
+
git_output('tag').lines.map { |line| Tag.new(self, line.strip) }
|
161
|
+
end
|
162
|
+
|
163
|
+
# Generates a list of commits using the given 'git log' arguments.
|
164
|
+
#
|
165
|
+
# @param [String] revision to log or nil
|
166
|
+
# @param [Hash] options for log
|
167
|
+
# @option options [Integer] :skip as lines of most recent history to skip (Default = include most recent)
|
168
|
+
# @option options [Integer] :tail as max history of log
|
169
|
+
# @option options [TrueClass|FalseClass] :no_merges as true to exclude merge commits
|
170
|
+
# @option options [TrueClass|FalseClass] :full_hashes as true show full hashes, false for (7-character) abbreviations
|
171
|
+
#
|
172
|
+
# @return [Array] list of commits
|
173
|
+
def log(revision, options = {})
|
174
|
+
options = {
|
175
|
+
:skip => nil,
|
176
|
+
:tail => 1_000,
|
177
|
+
:no_merges => false,
|
178
|
+
:full_hashes => false,
|
179
|
+
}.merge(options)
|
180
|
+
skip = options[:skip]
|
181
|
+
git_args = [
|
182
|
+
'log',
|
183
|
+
"-n#{options[:tail]}",
|
184
|
+
"--format=\"%#{options[:full_hashes] ? 'H' : 'h'} %at %aE\"" # double-quotes are Windows friendly
|
185
|
+
]
|
186
|
+
git_args << "--skip #{skip}" if skip
|
187
|
+
git_args << "--no-merges" if options[:no_merges]
|
188
|
+
git_args << revision if revision
|
189
|
+
git_output(git_args).lines.map { |line| Commit.new(self, line) }
|
190
|
+
end
|
191
|
+
|
192
|
+
# Cleans the current repository of untracked files.
|
193
|
+
#
|
194
|
+
# @param [Array] args for clean
|
195
|
+
#
|
196
|
+
# @return [TrueClass] always true
|
197
|
+
def clean(*args)
|
198
|
+
git_args = ['clean', args]
|
199
|
+
spit_output(git_args)
|
200
|
+
true
|
201
|
+
end
|
202
|
+
|
203
|
+
# Cleans everything and optionally cleans .gitignored files.
|
204
|
+
#
|
205
|
+
# @param [Hash] options for checkout
|
206
|
+
# @option options [TrueClass|FalseClass] :directories as true to clean untracked directories (but not untracked submodules)
|
207
|
+
# @option options [TrueClass|FalseClass] :gitignored as true to clean gitignored (untracked) files
|
208
|
+
# @option options [TrueClass|FalseClass] :submodules as true to clean untracked submodules (requires force)
|
209
|
+
#
|
210
|
+
# @return [TrueClass] always true
|
211
|
+
def clean_all(options = {})
|
212
|
+
options = {
|
213
|
+
:directories => false,
|
214
|
+
:gitignored => false,
|
215
|
+
:submodules => false,
|
216
|
+
}.merge(options)
|
217
|
+
git_args = ['-f'] # force is required or else -n only lists files.
|
218
|
+
git_args << '-f' if options[:submodules] # double-tap -f to kill untracked submodules
|
219
|
+
git_args << '-d' if options[:directories]
|
220
|
+
git_args << '-x' if options[:gitignored]
|
221
|
+
clean(git_args)
|
222
|
+
true
|
223
|
+
end
|
224
|
+
|
225
|
+
# Checkout.
|
226
|
+
#
|
227
|
+
# @param [String] revision for checkout
|
228
|
+
# @param [Hash] options for checkout
|
229
|
+
# @option options [TrueClass|FalseClass] :force as true to force checkout
|
230
|
+
#
|
231
|
+
# @return [TrueClass] always true
|
232
|
+
def checkout_to(revision, options = {})
|
233
|
+
options = {
|
234
|
+
:force => false
|
235
|
+
}.merge(options)
|
236
|
+
git_args = ['checkout', revision]
|
237
|
+
git_args << '--force' if options[:force]
|
238
|
+
vet_output(git_args)
|
239
|
+
true
|
240
|
+
end
|
241
|
+
|
242
|
+
# Performs a hard reset to the given revision, if given, or else the last
|
243
|
+
# checked-out SHA.
|
244
|
+
#
|
245
|
+
# @param [String] revision as target for hard reset or nil for hard reset to HEAD
|
246
|
+
#
|
247
|
+
# @return [TrueClass] always true
|
248
|
+
def hard_reset_to(revision)
|
249
|
+
git_args = ['reset', '--hard']
|
250
|
+
git_args << revision if revision
|
251
|
+
vet_output(git_args)
|
252
|
+
true
|
253
|
+
end
|
254
|
+
|
255
|
+
# Queries the recursive list of submodule paths for the current workspace.
|
256
|
+
#
|
257
|
+
# @param [Hash] options for submodules
|
258
|
+
# @option options [TrueClass|FalseClass] :recursive as true to recursively get submodule paths
|
259
|
+
#
|
260
|
+
# @return [Array] list of submodule paths or empty
|
261
|
+
def submodule_paths(options = {})
|
262
|
+
options = {
|
263
|
+
:recursive => false
|
264
|
+
}.merge(options)
|
265
|
+
git_args = ['submodule', 'status']
|
266
|
+
git_args << '--recursive' if options[:recursive]
|
267
|
+
git_output(git_args).lines.map do |line|
|
268
|
+
data = line.chomp
|
269
|
+
if matched = SUBMODULE_STATUS_REGEX.match(data)
|
270
|
+
matched[3]
|
271
|
+
else
|
272
|
+
raise GitError,
|
273
|
+
"Unexpected output from submodule status: #{data.inspect}"
|
274
|
+
end
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
# Updates submodules for the current workspace.
|
279
|
+
#
|
280
|
+
# @param [Hash] options for submodules
|
281
|
+
# @option options [TrueClass|FalseClass] :recursive as true to recursively update submodules
|
282
|
+
#
|
283
|
+
# @return [TrueClass] always true
|
284
|
+
def update_submodules(options = {})
|
285
|
+
options = {
|
286
|
+
:recursive => false
|
287
|
+
}.merge(options)
|
288
|
+
git_args = ['submodule', 'update', '--init']
|
289
|
+
git_args << '--recursive' if options[:recursive]
|
290
|
+
spit_output(git_args)
|
291
|
+
true
|
292
|
+
end
|
293
|
+
|
294
|
+
# Determines the SHA referenced by the given revision. Raises on failure.
|
295
|
+
#
|
296
|
+
# @param [String] revision or nil for current SHA
|
297
|
+
#
|
298
|
+
# @return [String] SHA for revision
|
299
|
+
def sha_for(revision)
|
300
|
+
# note that 'git show-ref' produces easier-to-parse output but it matches
|
301
|
+
# both local and remote branch to a simple branch name whereas 'git show'
|
302
|
+
# matches at-most-one and requires origin/ for remote branches.
|
303
|
+
git_args = ['show', revision].compact
|
304
|
+
result = nil
|
305
|
+
git_output(git_args).lines.each do |line|
|
306
|
+
if matched = COMMIT_SHA1_REGEX.match(line.strip)
|
307
|
+
result = matched[1]
|
308
|
+
break
|
309
|
+
end
|
310
|
+
end
|
311
|
+
unless result
|
312
|
+
raise GitError, 'Unable to locate commit in show output.'
|
313
|
+
end
|
314
|
+
result
|
315
|
+
end
|
316
|
+
|
317
|
+
# Executes and returns the output for a git command. Raises on failure.
|
318
|
+
#
|
319
|
+
# @param [String|Array] args to execute
|
320
|
+
#
|
321
|
+
# @return [String] output
|
322
|
+
def git_output(*args)
|
323
|
+
inner_execute(:output_for, args)
|
324
|
+
end
|
325
|
+
|
326
|
+
# Prints the output for a git command. Raises on failure.
|
327
|
+
#
|
328
|
+
# @param [String|Array] args to execute
|
329
|
+
#
|
330
|
+
# @return [TrueClass] always true
|
331
|
+
def spit_output(*args)
|
332
|
+
inner_execute(:execute, args)
|
333
|
+
end
|
334
|
+
|
335
|
+
# msysgit on Windows exits zero even when checkout|reset|fetch fails so we
|
336
|
+
# need to scan the output for error or fatal messages. it does no harm to do
|
337
|
+
# the same on Linux even though the exit code works properly there.
|
338
|
+
#
|
339
|
+
# @param [String|Array] args to execute
|
340
|
+
#
|
341
|
+
# @return [TrueClass] always true
|
342
|
+
def vet_output(*args)
|
343
|
+
last_output = git_output(*args).strip
|
344
|
+
logger.info(last_output) unless last_output.empty?
|
345
|
+
if last_output.downcase =~ /^(error|fatal):/
|
346
|
+
raise GitError, "Git exited zero but an error was detected in output."
|
347
|
+
end
|
348
|
+
true
|
349
|
+
end
|
350
|
+
|
351
|
+
private
|
352
|
+
|
353
|
+
# git defaults to working in the current directory but is sensitive to
|
354
|
+
# GIT_ env vars. we prefer the working directory so ensure any GIT_ that
|
355
|
+
# override the working directory are cleared.
|
356
|
+
CLEAR_GIT_ENV_VARS = ['GIT_DIR', 'GIT_INDEX_FILE', 'GIT_WORK_TREE'].freeze
|
357
|
+
|
358
|
+
def inner_execute(shell_method, git_args)
|
359
|
+
shell.send(
|
360
|
+
shell_method,
|
361
|
+
['git', git_args].flatten.join(' '),
|
362
|
+
:logger => logger,
|
363
|
+
:directory => @repo_dir,
|
364
|
+
:clear_env_vars => CLEAR_GIT_ENV_VARS)
|
365
|
+
end
|
366
|
+
|
367
|
+
end # Repository
|
368
|
+
end # RightGit
|
@@ -0,0 +1,82 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (c) 2013 RightScale Inc
|
3
|
+
#
|
4
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
5
|
+
# a copy of this software and associated documentation files (the
|
6
|
+
# "Software"), to deal in the Software without restriction, including
|
7
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
8
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
9
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
10
|
+
# the following conditions:
|
11
|
+
#
|
12
|
+
# The above copyright notice and this permission notice shall be
|
13
|
+
# included in all copies or substantial portions of the Software.
|
14
|
+
#
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
17
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
18
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
19
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
20
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
21
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
22
|
+
|
23
|
+
# ancestor
|
24
|
+
require 'right_git/git'
|
25
|
+
|
26
|
+
module RightGit::Git
|
27
|
+
|
28
|
+
# A tag in a Git repository.
|
29
|
+
class Tag
|
30
|
+
attr_reader :repo, :name
|
31
|
+
|
32
|
+
class TagError < GitError; end
|
33
|
+
|
34
|
+
# @param [Repository] repo hosting tag
|
35
|
+
# @param [String] name of tag
|
36
|
+
def initialize(repo, name)
|
37
|
+
# TEAL FIX: only invalid characters seem to be whitespace and some file
|
38
|
+
# system special characters; need to locate a definitive schema for tag
|
39
|
+
# names.
|
40
|
+
if name.index(/\s|[:\\?*<>\|]/)
|
41
|
+
raise TagError, 'name is invalid'
|
42
|
+
end
|
43
|
+
@repo = repo
|
44
|
+
@name = name
|
45
|
+
end
|
46
|
+
|
47
|
+
# @return [String] stringized
|
48
|
+
def to_s
|
49
|
+
"#{self.class.name}: #{@name.inspect}"
|
50
|
+
end
|
51
|
+
alias inspect to_s
|
52
|
+
|
53
|
+
# @param [Tag] other
|
54
|
+
# @return [TrueClass|FalseClass] true if equivalent
|
55
|
+
def ==(other)
|
56
|
+
if other.kind_of?(self.class)
|
57
|
+
@name == other.name
|
58
|
+
else
|
59
|
+
false
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# @param [Tag] other
|
64
|
+
# @return [Integer] comparison value
|
65
|
+
def <=>(other)
|
66
|
+
if other.kind_of?(self.class)
|
67
|
+
@name <=> other.name
|
68
|
+
else
|
69
|
+
raise ::ArgumentError, 'Wrong type'
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# Deletes this tag.
|
74
|
+
#
|
75
|
+
# @return [TrueClass] always true
|
76
|
+
def delete
|
77
|
+
@repo.vet_output("tag -d #{@name}")
|
78
|
+
true
|
79
|
+
end
|
80
|
+
|
81
|
+
end # Tag
|
82
|
+
end # RightGit
|
@@ -0,0 +1,36 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (c) 2013 RightScale Inc
|
3
|
+
#
|
4
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
5
|
+
# a copy of this software and associated documentation files (the
|
6
|
+
# "Software"), to deal in the Software without restriction, including
|
7
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
8
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
9
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
10
|
+
# the following conditions:
|
11
|
+
#
|
12
|
+
# The above copyright notice and this permission notice shall be
|
13
|
+
# included in all copies or substantial portions of the Software.
|
14
|
+
#
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
17
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
18
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
19
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
20
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
21
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
22
|
+
|
23
|
+
# ancestor
|
24
|
+
require 'right_git'
|
25
|
+
|
26
|
+
module RightGit
|
27
|
+
module Git
|
28
|
+
class GitError < RightGitError; end
|
29
|
+
|
30
|
+
autoload :Branch, 'right_git/git/branch'
|
31
|
+
autoload :BranchCollection, 'right_git/git/branch_collection'
|
32
|
+
autoload :Commit, 'right_git/git/commit'
|
33
|
+
autoload :Repository, 'right_git/git/repository'
|
34
|
+
autoload :Tag, 'right_git/git/tag'
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,191 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (c) 2013 RightScale Inc
|
3
|
+
#
|
4
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
5
|
+
# a copy of this software and associated documentation files (the
|
6
|
+
# "Software"), to deal in the Software without restriction, including
|
7
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
8
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
9
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
10
|
+
# the following conditions:
|
11
|
+
#
|
12
|
+
# The above copyright notice and this permission notice shall be
|
13
|
+
# included in all copies or substantial portions of the Software.
|
14
|
+
#
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
17
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
18
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
19
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
20
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
21
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
22
|
+
|
23
|
+
# ancestor
|
24
|
+
require 'right_git/shell'
|
25
|
+
|
26
|
+
# local
|
27
|
+
require 'logger'
|
28
|
+
require 'stringio'
|
29
|
+
require 'singleton'
|
30
|
+
|
31
|
+
module RightGit::Shell
|
32
|
+
|
33
|
+
# Default shell singleton implementation.
|
34
|
+
class Default
|
35
|
+
include ::RightGit::Shell::Interface
|
36
|
+
include ::Singleton
|
37
|
+
|
38
|
+
def self.respond_to?(*arguments)
|
39
|
+
instance.respond_to?(*arguments) || super
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.method_missing(method_sym, *arguments, &block)
|
43
|
+
if instance.respond_to?(method_sym)
|
44
|
+
instance.send(method_sym, *arguments, &block)
|
45
|
+
else
|
46
|
+
super
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Implements execute interface.
|
51
|
+
def execute(cmd, options = {})
|
52
|
+
options = {
|
53
|
+
:directory => nil,
|
54
|
+
:logger => nil,
|
55
|
+
:outstream => nil,
|
56
|
+
:raise_on_failure => true,
|
57
|
+
:set_env_vars => nil,
|
58
|
+
:clear_env_vars => nil,
|
59
|
+
}.merge(options)
|
60
|
+
logger = options[:logger] || default_logger
|
61
|
+
outstream = options[:outstream]
|
62
|
+
|
63
|
+
# build execution block.
|
64
|
+
exitstatus = nil
|
65
|
+
executioner = lambda do
|
66
|
+
logger.info("+ #{cmd}")
|
67
|
+
::IO.popen("#{cmd} 2>&1", 'r') do |output|
|
68
|
+
output.sync = true
|
69
|
+
done = false
|
70
|
+
while !done
|
71
|
+
begin
|
72
|
+
data = output.readline
|
73
|
+
if outstream
|
74
|
+
outstream << data
|
75
|
+
else
|
76
|
+
logger.info(data.strip)
|
77
|
+
end
|
78
|
+
rescue ::EOFError
|
79
|
+
done = true
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
exitstatus = $?.exitstatus
|
84
|
+
if (!$?.success? && options[:raise_on_failure])
|
85
|
+
raise ShellError, "Execution failed with exitstatus #{exitstatus}"
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# configure and invoke.
|
90
|
+
configure_executioner(executioner, options).call
|
91
|
+
|
92
|
+
return exitstatus
|
93
|
+
end
|
94
|
+
|
95
|
+
# Implements output_for interface.
|
96
|
+
def output_for(cmd, options = {})
|
97
|
+
output = StringIO.new
|
98
|
+
execute(cmd, options.merge(:outstream => output))
|
99
|
+
output.string
|
100
|
+
end
|
101
|
+
|
102
|
+
# Encapsulates the given executioner with child-process-modifying behavior
|
103
|
+
# based on options. Builds the executioner as a series of callbacks.
|
104
|
+
#
|
105
|
+
# @param [Proc] executioner to configure
|
106
|
+
# @param [Hash] options for execution
|
107
|
+
#
|
108
|
+
# @return [Proc] configured executioner
|
109
|
+
def configure_executioner(executioner, options)
|
110
|
+
# set specific environment variables, if requested.
|
111
|
+
sev = options[:set_env_vars]
|
112
|
+
if (sev && !sev.empty?)
|
113
|
+
executioner = lambda do |e|
|
114
|
+
lambda { set_env_vars(sev) { e.call } }
|
115
|
+
end.call(executioner)
|
116
|
+
end
|
117
|
+
|
118
|
+
# clear specific environment variables, if requested.
|
119
|
+
cev = options[:clear_env_vars]
|
120
|
+
if (cev && !cev.empty?)
|
121
|
+
executioner = lambda do |e|
|
122
|
+
lambda { clear_env_vars(cev) { e.call } }
|
123
|
+
end.call(executioner)
|
124
|
+
end
|
125
|
+
|
126
|
+
# working directory.
|
127
|
+
if directory = options[:directory]
|
128
|
+
executioner = lambda do |e, d|
|
129
|
+
lambda { ::Dir.chdir(d) { e.call } }
|
130
|
+
end.call(executioner, directory)
|
131
|
+
end
|
132
|
+
executioner
|
133
|
+
end
|
134
|
+
|
135
|
+
# Sets the given list of environment variables while
|
136
|
+
# executing the given block.
|
137
|
+
#
|
138
|
+
# === Parameters
|
139
|
+
# @param [Hash] variables to set
|
140
|
+
#
|
141
|
+
# === Yield
|
142
|
+
# @yield [] called with environment set
|
143
|
+
#
|
144
|
+
# === Return
|
145
|
+
# @return [TrueClass] always true
|
146
|
+
def set_env_vars(variables)
|
147
|
+
save_vars = {}
|
148
|
+
variables.each do |k, v|
|
149
|
+
k = k.to_s
|
150
|
+
save_vars[k] = ENV[k]
|
151
|
+
ENV[k] = v.nil? ? v : v.to_s
|
152
|
+
end
|
153
|
+
begin
|
154
|
+
yield
|
155
|
+
ensure
|
156
|
+
variables.each_key do |k|
|
157
|
+
k = k.to_s
|
158
|
+
ENV[k] = save_vars[k]
|
159
|
+
end
|
160
|
+
end
|
161
|
+
true
|
162
|
+
end
|
163
|
+
|
164
|
+
# Clears (set-to-nil) the given list of environment variables while
|
165
|
+
# executing the given block.
|
166
|
+
#
|
167
|
+
# @param [Array] names of variables to clear
|
168
|
+
#
|
169
|
+
# @yield [] called with environment cleared
|
170
|
+
#
|
171
|
+
# @return [TrueClass] always true
|
172
|
+
def clear_env_vars(names, &block)
|
173
|
+
save_vars = {}
|
174
|
+
names.each do |k|
|
175
|
+
k = k.to_s
|
176
|
+
save_vars[k] = ENV[k]
|
177
|
+
ENV[k] = nil
|
178
|
+
end
|
179
|
+
begin
|
180
|
+
yield
|
181
|
+
ensure
|
182
|
+
names.each do |k|
|
183
|
+
k = k.to_s
|
184
|
+
ENV[k] = save_vars[k]
|
185
|
+
end
|
186
|
+
end
|
187
|
+
true
|
188
|
+
end
|
189
|
+
|
190
|
+
end # Default
|
191
|
+
end # RightGit::Shell
|