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