git-process-lib 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (69) hide show
  1. data/CHANGELOG.md +123 -0
  2. data/Gemfile +21 -0
  3. data/Gemfile.lock +57 -0
  4. data/LICENSE +193 -0
  5. data/README.md +342 -0
  6. data/Rakefile +32 -0
  7. data/bin/git-new-fb +39 -0
  8. data/bin/git-pull-request +63 -0
  9. data/bin/git-sync +38 -0
  10. data/bin/git-to-master +44 -0
  11. data/docs/git-new-fb.1.adoc +83 -0
  12. data/docs/git-process.1.adoc +227 -0
  13. data/docs/git-pull-request.1.adoc +166 -0
  14. data/docs/git-sync.1.adoc +120 -0
  15. data/docs/git-to-master.1.adoc +172 -0
  16. data/git-new-fb.gemspec +20 -0
  17. data/git-process-lib.gemspec +25 -0
  18. data/git-process.gemspec +22 -0
  19. data/git-pull-request.gemspec +20 -0
  20. data/git-sync.gemspec +20 -0
  21. data/git-to-master.gemspec +20 -0
  22. data/lib/git-process/abstract_error_builder.rb +53 -0
  23. data/lib/git-process/changed_file_helper.rb +115 -0
  24. data/lib/git-process/git_abstract_merge_error_builder.rb +130 -0
  25. data/lib/git-process/git_branch.rb +105 -0
  26. data/lib/git-process/git_branches.rb +81 -0
  27. data/lib/git-process/git_config.rb +135 -0
  28. data/lib/git-process/git_lib.rb +646 -0
  29. data/lib/git-process/git_logger.rb +84 -0
  30. data/lib/git-process/git_merge_error.rb +28 -0
  31. data/lib/git-process/git_process.rb +159 -0
  32. data/lib/git-process/git_process_error.rb +18 -0
  33. data/lib/git-process/git_process_options.rb +101 -0
  34. data/lib/git-process/git_rebase_error.rb +30 -0
  35. data/lib/git-process/git_remote.rb +222 -0
  36. data/lib/git-process/git_status.rb +108 -0
  37. data/lib/git-process/github_configuration.rb +298 -0
  38. data/lib/git-process/github_pull_request.rb +165 -0
  39. data/lib/git-process/new_fb.rb +49 -0
  40. data/lib/git-process/parked_changes_error.rb +41 -0
  41. data/lib/git-process/pull_request.rb +136 -0
  42. data/lib/git-process/pull_request_error.rb +25 -0
  43. data/lib/git-process/rebase_to_master.rb +148 -0
  44. data/lib/git-process/sync_process.rb +55 -0
  45. data/lib/git-process/syncer.rb +157 -0
  46. data/lib/git-process/uncommitted_changes_error.rb +23 -0
  47. data/lib/git-process/version.rb +22 -0
  48. data/local-build.rb +24 -0
  49. data/spec/FileHelpers.rb +19 -0
  50. data/spec/GitRepoHelper.rb +123 -0
  51. data/spec/changed_file_helper_spec.rb +127 -0
  52. data/spec/git_abstract_merge_error_builder_spec.rb +64 -0
  53. data/spec/git_branch_spec.rb +123 -0
  54. data/spec/git_config_spec.rb +45 -0
  55. data/spec/git_lib_spec.rb +176 -0
  56. data/spec/git_logger_spec.rb +66 -0
  57. data/spec/git_process_spec.rb +208 -0
  58. data/spec/git_remote_spec.rb +227 -0
  59. data/spec/git_status_spec.rb +122 -0
  60. data/spec/github_configuration_spec.rb +152 -0
  61. data/spec/github_pull_request_spec.rb +117 -0
  62. data/spec/github_test_helper.rb +49 -0
  63. data/spec/new_fb_spec.rb +126 -0
  64. data/spec/pull_request_helper.rb +94 -0
  65. data/spec/pull_request_spec.rb +137 -0
  66. data/spec/rebase_to_master_spec.rb +362 -0
  67. data/spec/spec_helper.rb +21 -0
  68. data/spec/sync_spec.rb +1474 -0
  69. metadata +249 -0
@@ -0,0 +1,81 @@
1
+ # Licensed under the Apache License, Version 2.0 (the "License");
2
+ # you may not use this file except in compliance with the License.
3
+ # You may obtain a copy of the License at
4
+ #
5
+ # http://www.apache.org/licenses/LICENSE-2.0
6
+ #
7
+ # Unless required by applicable law or agreed to in writing, software
8
+ # distributed under the License is distributed on an "AS IS" BASIS,
9
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10
+ # See the License for the specific language governing permissions and
11
+ # limitations under the License.
12
+
13
+ require 'set'
14
+ require 'git-process/git_branch'
15
+
16
+ module GitProc
17
+
18
+ class GitBranches
19
+ include Enumerable
20
+
21
+
22
+ def initialize(lib, opt = {})
23
+ @lib = lib
24
+ branch_opts = {:no_color => true}
25
+ if opt[:remote]
26
+ branch_opts[:remote] = true
27
+ elsif opt[:local]
28
+ branch_opts[:local] = true
29
+ else
30
+ branch_opts[:all] = true
31
+ end
32
+ branch_lines = lib.branch(nil, branch_opts).split("\n")
33
+ @items = SortedSet.new
34
+ branch_lines.each do |bl|
35
+ @items << GitBranch.new(bl[2..-1], bl[0..0] == '*', lib)
36
+ end
37
+ end
38
+
39
+
40
+ def <<(item)
41
+ @items << item
42
+ end
43
+
44
+
45
+ def each(&block)
46
+ @items.each { |b| block.call(b) }
47
+ end
48
+
49
+
50
+ def names
51
+ @items.map { |b| b.name }
52
+ end
53
+
54
+
55
+ def current
56
+ @items.find { |b| b.current? }
57
+ end
58
+
59
+
60
+ def parking
61
+ @items.find { |b| b.name == '_parking_' }
62
+ end
63
+
64
+
65
+ def include?(branch_name)
66
+ @items.find { |b| b.name == branch_name } != nil
67
+ end
68
+
69
+
70
+ def [](branch_name)
71
+ branch_name = current.name if branch_name == 'HEAD'
72
+ br = @items.find { |b| b.name == branch_name }
73
+ if br.nil? and branch_name !~ /origin\// and branch_name != '_parking_'
74
+ @lib.logger.warn { "Could not find '#{branch_name}' in #{@items.map { |i| i.name }.join(',')}" }
75
+ end
76
+ br
77
+ end
78
+
79
+ end
80
+
81
+ end
@@ -0,0 +1,135 @@
1
+ # Licensed under the Apache License, Version 2.0 (the "License");
2
+ # you may not use this file except in compliance with the License.
3
+ # You may obtain a copy of the License at
4
+ #
5
+ # http://www.apache.org/licenses/LICENSE-2.0
6
+ #
7
+ # Unless required by applicable law or agreed to in writing, software
8
+ # distributed under the License is distributed on an "AS IS" BASIS,
9
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10
+ # See the License for the specific language governing permissions and
11
+ # limitations under the License.
12
+
13
+ require 'git-process/git_logger'
14
+ require 'git-process/git_branch'
15
+ require 'git-process/git_branches'
16
+ require 'git-process/git_status'
17
+ require 'git-process/git_process_error'
18
+
19
+
20
+ class String
21
+
22
+ def to_boolean
23
+ return false if self == false || self.nil? || self =~ (/(false|f|no|n|0)$/i)
24
+ return true if self == true || self =~ (/(true|t|yes|y|1)$/i)
25
+ raise ArgumentError.new("invalid value for Boolean: \"#{self}\"")
26
+ end
27
+
28
+ end
29
+
30
+
31
+ class NilClass
32
+ def to_boolean
33
+ false
34
+ end
35
+ end
36
+
37
+
38
+ module GitProc
39
+
40
+ #
41
+ # Provides Git configuration
42
+ #
43
+ class GitConfig
44
+
45
+ def initialize(lib)
46
+ @lib = lib
47
+ end
48
+
49
+
50
+ def [](key)
51
+ value = config_hash[key]
52
+ unless value
53
+ value = @lib.command(:config, ['--get', key])
54
+ value = nil if value.empty?
55
+ config_hash[key] = value unless config_hash.empty?
56
+ end
57
+ value
58
+ end
59
+
60
+
61
+ def []=(key, value)
62
+ @lib.command(:config, [key, value])
63
+ config_hash[key] = value unless config_hash.empty?
64
+ value
65
+ end
66
+
67
+
68
+ def set_global(key, value)
69
+ @lib.command(:config, ['--global', key, value])
70
+ config_hash[key] = value unless config_hash.empty?
71
+ value
72
+ end
73
+
74
+
75
+ def gitlib
76
+ @lib
77
+ end
78
+
79
+
80
+ def logger
81
+ gitlib.logger
82
+ end
83
+
84
+
85
+ #
86
+ # @return true if no value has been set; the value of the config otherwise
87
+ def default_rebase_sync?
88
+ val = self['gitProcess.defaultRebaseSync']
89
+ val.nil? or val.to_boolean
90
+ end
91
+
92
+
93
+ def default_rebase_sync(re, global = true)
94
+ if global
95
+ set_global('gitProcess.defaultRebaseSync', re)
96
+ else
97
+ self['gitProcess.defaultRebaseSync'] = re
98
+ end
99
+ end
100
+
101
+
102
+ def default_rebase_sync=(re)
103
+ default_rebase_sync(re, false)
104
+ end
105
+
106
+
107
+ def master_branch
108
+ @master_branch ||= self['gitProcess.integrationBranch'] || 'master'
109
+ end
110
+
111
+
112
+ def remote_master_branch
113
+ remote.master_branch_name
114
+ end
115
+
116
+
117
+ def integration_branch
118
+ remote.exists? ? remote_master_branch : self.master_branch
119
+ end
120
+
121
+
122
+ private
123
+
124
+ def remote
125
+ gitlib.remote
126
+ end
127
+
128
+
129
+ def config_hash
130
+ @config_hash ||= {}
131
+ end
132
+
133
+ end
134
+
135
+ end
@@ -0,0 +1,646 @@
1
+ # Licensed under the Apache License, Version 2.0 (the "License");
2
+ # you may not use this file except in compliance with the License.
3
+ # You may obtain a copy of the License at
4
+ #
5
+ # http://www.apache.org/licenses/LICENSE-2.0
6
+ #
7
+ # Unless required by applicable law or agreed to in writing, software
8
+ # distributed under the License is distributed on an "AS IS" BASIS,
9
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10
+ # See the License for the specific language governing permissions and
11
+ # limitations under the License.
12
+
13
+ require 'logger'
14
+ require 'git-process/git_branch'
15
+ require 'git-process/git_branches'
16
+ require 'git-process/git_remote'
17
+ require 'git-process/git_status'
18
+ require 'git-process/git_process_error'
19
+
20
+
21
+ module GitProc
22
+
23
+ class GitExecuteError < GitProcessError
24
+ end
25
+
26
+
27
+ #
28
+ # Provides Git commands
29
+ #
30
+ #noinspection RubyTooManyMethodsInspection
31
+ class GitLib
32
+
33
+ # @param [Dir] dir
34
+ def initialize(dir, opts)
35
+ self.log_level = GitLib.log_level(opts)
36
+ self.workdir = dir
37
+ end
38
+
39
+
40
+ def logger
41
+ if @logger.nil?
42
+ @logger = GitLogger.new(log_level)
43
+ end
44
+ @logger
45
+ end
46
+
47
+
48
+ def self.log_level(opts)
49
+ if opts[:log_level]
50
+ opts[:log_level]
51
+ elsif opts[:quiet]
52
+ Logger::ERROR
53
+ elsif opts[:verbose]
54
+ Logger::DEBUG
55
+ else
56
+ Logger::INFO
57
+ end
58
+ end
59
+
60
+
61
+ def log_level
62
+ @log_level || Logger::WARN
63
+ end
64
+
65
+
66
+ def log_level=(lvl)
67
+ @log_level = lvl
68
+ end
69
+
70
+
71
+ def workdir
72
+ @workdir
73
+ end
74
+
75
+
76
+ def workdir=(dir)
77
+ workdir = GitLib.find_workdir(dir)
78
+ if workdir.nil?
79
+ @workdir = dir
80
+ logger.info { "Initializing new repository at #{dir}" }
81
+ command(:init)
82
+ else
83
+ @workdir = workdir
84
+ logger.debug { "Opening existing repository at #{dir}" }
85
+ end
86
+ end
87
+
88
+
89
+ def self.find_workdir(dir)
90
+ if dir == File::SEPARATOR
91
+ nil
92
+ elsif File.directory?(File.join(dir, '.git'))
93
+ dir
94
+ else
95
+ find_workdir(File.expand_path("#{dir}#{File::SEPARATOR}.."))
96
+ end
97
+ end
98
+
99
+
100
+ def fetch_remote_changes(remote_name = nil)
101
+ if remote.exists?
102
+ fetch(remote_name || remote.name)
103
+ else
104
+ logger.debug 'Can not fetch latest changes because there is no remote defined'
105
+ end
106
+ end
107
+
108
+
109
+ def proc_rebase(base, opts = {})
110
+ begin
111
+ rebase(base, opts)
112
+ rescue GitExecuteError => rebase_error
113
+ raise RebaseError.new(rebase_error.message, self)
114
+ end
115
+ end
116
+
117
+
118
+ def proc_merge(base, opts = {})
119
+ begin
120
+ merge(base, opts)
121
+ rescue GitExecuteError => merge_error
122
+ raise MergeError.new(merge_error.message, self)
123
+ end
124
+ end
125
+
126
+
127
+ # @return [String] the previous remote sha ONLY IF it is not the same as the new remote sha; otherwise nil
128
+ def previous_remote_sha(current_branch, remote_branch)
129
+ return nil unless has_a_remote?
130
+ return nil unless remote_branches.include?(remote_branch)
131
+
132
+ control_file_sha = read_sync_control_file(current_branch)
133
+ old_sha = control_file_sha || remote_branch_sha(remote_branch)
134
+ fetch_remote_changes
135
+ new_sha = remote_branch_sha(remote_branch)
136
+
137
+ if old_sha != new_sha
138
+ logger.info('The remote branch has changed since the last time')
139
+ old_sha
140
+ else
141
+ logger.debug 'The remote branch has not changed since the last time'
142
+ nil
143
+ end
144
+ end
145
+
146
+
147
+ def remote_branch_sha(remote_branch)
148
+ logger.debug {"getting sha for remotes/#{remote_branch}"}
149
+ rev_parse("remotes/#{remote_branch}") rescue ''
150
+ end
151
+
152
+
153
+ def is_parked?
154
+ mybranches = self.branches()
155
+ mybranches.parking == mybranches.current
156
+ end
157
+
158
+
159
+ def push_to_server(local_branch, remote_branch, opts = {})
160
+ if opts[:local]
161
+ logger.debug('Not pushing to the server because the user selected local-only.')
162
+ elsif not has_a_remote?
163
+ logger.debug('Not pushing to the server because there is no remote.')
164
+ elsif local_branch == config.master_branch
165
+ logger.warn('Not pushing to the server because the current branch is the mainline branch.')
166
+ else
167
+ opts[:prepush].call if opts[:prepush]
168
+
169
+ push(remote.name, local_branch, remote_branch, :force => opts[:force])
170
+
171
+ opts[:postpush].call if opts[:postpush]
172
+ end
173
+ end
174
+
175
+
176
+ def config
177
+ if @config.nil?
178
+ @config = GitConfig.new(self)
179
+ end
180
+ @config
181
+ end
182
+
183
+
184
+ def remote
185
+ if @remote.nil?
186
+ @remote = GitProc::GitRemote.new(config)
187
+ end
188
+ @remote
189
+ end
190
+
191
+
192
+ # @return [Boolean] does this have a remote defined?
193
+ def has_a_remote?
194
+ remote.exists?
195
+ end
196
+
197
+
198
+ def add(file)
199
+ logger.info { "Adding #{[*file].join(', ')}" }
200
+ command(:add, ['--', file])
201
+ end
202
+
203
+
204
+ def commit(msg = nil)
205
+ logger.info 'Committing changes'
206
+ command(:commit, msg.nil? ? nil : ['-m', msg])
207
+ end
208
+
209
+
210
+ def rebase(upstream, opts = {})
211
+ args = []
212
+ if opts[:interactive]
213
+ logger.info { "Interactively rebasing #{branches.current.name} against #{upstream}" }
214
+ args << '-i'
215
+ args << upstream
216
+ elsif opts[:oldbase]
217
+ logger.info { "Doing rebase from #{opts[:oldbase]} against #{upstream} on #{branches.current.name}" }
218
+ args << '--onto' << upstream << opts[:oldbase] << branches.current.name
219
+ else
220
+ logger.info { "Rebasing #{branches.current.name} against #{upstream}" }
221
+ args << upstream
222
+ end
223
+ command('rebase', args)
224
+ end
225
+
226
+
227
+ def merge(base, opts= {})
228
+ logger.info { "Merging #{branches.current.name} with #{base}" }
229
+ args = []
230
+ args << '-s' << opts[:merge_strategy] if opts[:merge_strategy]
231
+ args << base
232
+ command(:merge, args)
233
+ end
234
+
235
+
236
+ def fetch(name = remote.name)
237
+ logger.info 'Fetching the latest changes from the server'
238
+ output = self.command(:fetch, ['-p', name])
239
+
240
+ log_fetch_changes(fetch_changes(output))
241
+
242
+ output
243
+ end
244
+
245
+
246
+ # @param [Hash] changes a hash of the changes that were made
247
+ #
248
+ # @return [void]
249
+ def log_fetch_changes(changes)
250
+ changes.each do |key, v|
251
+ unless v.empty?
252
+ logger.info { " #{key.to_s.sub(/_/, ' ')}: #{v.join(', ')}" }
253
+ end
254
+ end
255
+ end
256
+
257
+
258
+ # @return [Hash]
259
+ def fetch_changes(output)
260
+ changed = output.split("\n")
261
+
262
+ changes = {:new_branch => [], :new_tag => [], :force_updated => [], :deleted => [], :updated => []}
263
+
264
+ line = changed.shift
265
+
266
+ until line.nil? do
267
+ case line
268
+ when /^\s\s\s/
269
+ m = /^\s\s\s(\S+)\s+(\S+)\s/.match(line)
270
+ changes[:updated] << "#{m[2]} (#{m[1]})"
271
+ when /^\s\*\s\[new branch\]/
272
+ m = /^\s\*\s\[new branch\]\s+(\S+)\s/.match(line)
273
+ changes[:new_branch] << m[1]
274
+ when /^\s\*\s\[new tag\]/
275
+ m = /^\s\*\s\[new tag\]\s+(\S+)\s/.match(line)
276
+ changes[:new_tag] << m[1]
277
+ when /^\sx\s/
278
+ m = /^\sx\s\[deleted\]\s+\(none\)\s+->\s+[^\/]+\/(\S+)/.match(line)
279
+ changes[:deleted] << m[1]
280
+ when /^\s\+\s/
281
+ m = /^\s\+\s(\S+)\s+(\S+)\s/.match(line)
282
+ changes[:force_updated] << "#{m[2]} (#{m[1]})"
283
+ else
284
+ # ignore the line
285
+ end
286
+ line = changed.shift
287
+ end
288
+
289
+ changes
290
+ end
291
+
292
+
293
+ def branches
294
+ GitProc::GitBranches.new(self)
295
+ end
296
+
297
+
298
+ def remote_branches
299
+ GitProc::GitBranches.new(self, :remote => true)
300
+ end
301
+
302
+
303
+ #
304
+ # Does branch manipulation.
305
+ #
306
+ # @param [String] branch_name the name of the branch
307
+ #
308
+ # @option opts [Boolean] :delete delete the remote branch
309
+ # @option opts [Boolean] :force force the update
310
+ # @option opts [Boolean] :all list all branches, local and remote
311
+ # @option opts [Boolean] :no_color force not using any ANSI color codes
312
+ # @option opts [String] :rename the new name for the branch
313
+ # @option opts [String] :upstream the new branch to track
314
+ # @option opts [String] :base_branch the branch to base the new branch off of;
315
+ # defaults to 'master'
316
+ #
317
+ # @return [String] the output of running the git command
318
+ def branch(branch_name, opts = {})
319
+ if opts[:delete]
320
+ delete_branch(branch_name, opts[:force])
321
+ elsif opts[:rename]
322
+ rename_branch(branch_name, opts[:rename])
323
+ elsif opts[:upstream]
324
+ set_upstream_branch(branch_name, opts[:upstream])
325
+ elsif branch_name
326
+ if opts[:force]
327
+ change_branch(branch_name, opts[:base_branch])
328
+ else
329
+ create_branch(branch_name, opts[:base_branch])
330
+ end
331
+ else
332
+ #list_branches(opts)
333
+ list_branches(opts[:all], opts[:remote], opts[:no_color])
334
+ end
335
+ end
336
+
337
+
338
+ #
339
+ # Pushes the given branch to the server.
340
+ #
341
+ # @param [String] remote_name the repository name; nil -> 'origin'
342
+ # @param [String] local_branch the local branch to push; nil -> the current branch
343
+ # @param [String] remote_branch the name of the branch to push to; nil -> same as local_branch
344
+ #
345
+ # @option opts [Boolean, String] :delete delete the remote branch
346
+ # @option opts [Boolean] :force force the update, even if not a fast-forward
347
+ #
348
+ # @return [void]
349
+ #
350
+ # @raise [ArgumentError] if :delete is true, but no branch name is given
351
+ def push(remote_name, local_branch, remote_branch, opts = {})
352
+ remote_name ||= 'origin'
353
+
354
+ args = [remote_name]
355
+
356
+ if opts[:delete]
357
+ if remote_branch
358
+ rb = remote_branch
359
+ elsif local_branch
360
+ rb = local_branch
361
+ elsif !(opts[:delete].is_a? TrueClass)
362
+ rb = opts[:delete]
363
+ else
364
+ raise ArgumentError.new('Need a branch name to delete.')
365
+ end
366
+
367
+ int_branch = config.master_branch
368
+ if rb == int_branch
369
+ raise GitProc::GitProcessError.new("Can not delete the integration branch '#{int_branch}'")
370
+ end
371
+
372
+ logger.info { "Deleting remote branch '#{rb}' on '#{remote_name}'." }
373
+ args << '--delete' << rb
374
+ else
375
+ local_branch ||= branches.current
376
+ remote_branch ||= local_branch
377
+ args << '-f' if opts[:force]
378
+
379
+ logger.info do
380
+ if local_branch == remote_branch
381
+ "Pushing to '#{remote_branch}' on '#{remote_name}'."
382
+ else
383
+ "Pushing #{local_branch} to '#{remote_branch}' on '#{remote_name}'."
384
+ end
385
+ end
386
+
387
+ args << "#{local_branch}:#{remote_branch}"
388
+ end
389
+ command(:push, args)
390
+ end
391
+
392
+
393
+ def rebase_continue
394
+ command(:rebase, '--continue')
395
+ end
396
+
397
+
398
+ def stash_save
399
+ command(:stash, %w(save))
400
+ end
401
+
402
+
403
+ def stash_pop
404
+ command(:stash, %w(pop))
405
+ end
406
+
407
+
408
+ def show(refspec)
409
+ command(:show, refspec)
410
+ end
411
+
412
+
413
+ # @param [String] branch_name the name of the branch to checkout/create
414
+ # @option opts [Boolean] :no_track do not track the base branch
415
+ # @option opts [String] :new_branch the name of the base branch
416
+ #
417
+ # @return [void]
418
+ def checkout(branch_name, opts = {})
419
+ args = []
420
+ args << '--no-track' if opts[:no_track]
421
+ args << '-b' if opts[:new_branch]
422
+ args << branch_name
423
+ args << opts[:new_branch] if opts[:new_branch]
424
+ branches = branches()
425
+ command(:checkout, args)
426
+
427
+ branches << GitBranch.new(branch_name, opts[:new_branch] != nil, self)
428
+
429
+ if block_given?
430
+ yield
431
+ command(:checkout, branches.current.name)
432
+ branches.current
433
+ else
434
+ branches[branch_name]
435
+ end
436
+ end
437
+
438
+
439
+ def log_count
440
+ command(:log, '--oneline').split(/\n/).length
441
+ end
442
+
443
+
444
+ def remove(files, opts = {})
445
+ args = []
446
+ args << '-f' if opts[:force]
447
+ args << [*files]
448
+ command(:rm, args)
449
+ end
450
+
451
+
452
+ #
453
+ # Returns the status of the git repository.
454
+ #
455
+ # @return [Status]
456
+ def status
457
+ GitStatus.new(self)
458
+ end
459
+
460
+
461
+ # @return [String] the raw porcelain status string
462
+ def porcelain_status
463
+ command(:status, '--porcelain')
464
+ end
465
+
466
+
467
+ def reset(rev_name, opts = {})
468
+ args = []
469
+ args << '--hard' if opts[:hard]
470
+ args << rev_name
471
+
472
+ logger.info { "Resetting #{opts[:hard] ? '(hard)' : ''} to #{rev_name}" }
473
+
474
+ command(:reset, args)
475
+ end
476
+
477
+
478
+ def rev_list(start_revision, end_revision, opts ={})
479
+ args = []
480
+ args << "-#{opts[:num_revs]}" if opts[:num_revs]
481
+ args << '--oneline' if opts[:oneline]
482
+ args << "#{start_revision}..#{end_revision}"
483
+ command('rev-list', args)
484
+ end
485
+
486
+
487
+ def rev_parse(name)
488
+ command('rev-parse', name)
489
+ end
490
+
491
+
492
+ alias sha rev_parse
493
+
494
+
495
+ # @return [String]
496
+ def command(cmd, opts = [], chdir = true, redirect = '', &block)
497
+ ENV['GIT_INDEX_FILE'] = File.join(workdir, '.git', 'index')
498
+ ENV['GIT_DIR'] = File.join(workdir, '.git')
499
+ ENV['GIT_WORK_TREE'] = workdir
500
+ path = workdir
501
+
502
+ git_cmd = create_git_command(cmd, opts, redirect)
503
+
504
+ out = command_git_cmd(path, git_cmd, chdir, block)
505
+
506
+ if logger
507
+ logger.debug(git_cmd)
508
+ logger.debug(out)
509
+ end
510
+
511
+ handle_exitstatus($?, git_cmd, out)
512
+ end
513
+
514
+
515
+ def write_sync_control_file(branch_name)
516
+ latest_sha = rev_parse(branch_name)
517
+ filename = sync_control_filename(branch_name)
518
+ logger.debug { "Writing sync control file, #{filename}, with #{latest_sha}" }
519
+ File.open(filename, 'w') { |f| f.puts latest_sha }
520
+ end
521
+
522
+
523
+ def read_sync_control_file(branch_name)
524
+ filename = sync_control_filename(branch_name)
525
+ if File.exists?(filename)
526
+ sha = File.new(filename).readline.chop
527
+ logger.debug "Read sync control file, #{filename}: #{sha}"
528
+ sha
529
+ else
530
+ logger.debug "Sync control file, #{filename}, was not found"
531
+ nil
532
+ end
533
+ end
534
+
535
+
536
+ def delete_sync_control_file!(branch_name)
537
+ filename = sync_control_filename(branch_name)
538
+ logger.debug { "Deleting sync control file, #{filename}" }
539
+ File.delete(filename)
540
+ end
541
+
542
+
543
+ def sync_control_file_exists?(branch_name)
544
+ filename = sync_control_filename(branch_name)
545
+ File.exist?(filename)
546
+ end
547
+
548
+
549
+ private
550
+
551
+
552
+ def create_git_command(cmd, opts, redirect)
553
+ opts = [opts].flatten.map { |s| escape(s) }.join(' ')
554
+ "git #{cmd} #{opts} #{redirect} 2>&1"
555
+ end
556
+
557
+
558
+ def command_git_cmd(path, git_cmd, chdir, block)
559
+ out = nil
560
+ if chdir and (Dir.getwd != path)
561
+ Dir.chdir(path) { out = run_command(git_cmd, &block) }
562
+ else
563
+ out = run_command(git_cmd, &block)
564
+ end
565
+ out
566
+ end
567
+
568
+
569
+ # @return [String]
570
+ def handle_exitstatus(proc_status, git_cmd, out)
571
+ if proc_status.exitstatus > 0
572
+ unless proc_status.exitstatus == 1 && out == ''
573
+ raise GitProc::GitExecuteError.new(git_cmd + ':' + out.to_s)
574
+ end
575
+ end
576
+ out
577
+ end
578
+
579
+
580
+ def run_command(git_cmd, &block)
581
+ if block_given?
582
+ IO.popen(git_cmd, &block)
583
+ else
584
+ `#{git_cmd}`.chomp
585
+ end
586
+ end
587
+
588
+
589
+ def escape(s)
590
+ escaped = s.to_s.gsub('\'', '\'\\\'\'')
591
+ %Q{"#{escaped}"}
592
+ end
593
+
594
+
595
+ def change_branch(branch_name, base_branch)
596
+ raise ArgumentError.new('Need :base_branch when using :force for a branch.') unless base_branch
597
+ logger.info { "Changing branch '#{branch_name}' to point to '#{base_branch}'." }
598
+
599
+ command(:branch, ['-f', branch_name, base_branch])
600
+ end
601
+
602
+
603
+ def create_branch(branch_name, base_branch)
604
+ logger.info { "Creating new branch '#{branch_name}' based on '#{base_branch}'." }
605
+
606
+ command(:branch, [branch_name, (base_branch || 'master')])
607
+ end
608
+
609
+
610
+ def list_branches(all_branches, remote_branches, no_color)
611
+ args = []
612
+ args << '-a' if all_branches
613
+ args << '-r' if remote_branches
614
+ args << '--no-color' if no_color
615
+ command(:branch, args)
616
+ end
617
+
618
+
619
+ def delete_branch(branch_name, force)
620
+ logger.info { "Deleting local branch '#{branch_name}'." } unless branch_name == '_parking_'
621
+
622
+ command(:branch, [force ? '-D' : '-d', branch_name])
623
+ end
624
+
625
+
626
+ def rename_branch(branch_name, new_name)
627
+ logger.info { "Renaming branch '#{branch_name}' to '#{new_name}'." }
628
+
629
+ command(:branch, ['-m', branch_name, new_name])
630
+ end
631
+
632
+
633
+ def set_upstream_branch(branch_name, upstream)
634
+ logger.info { "Setting upstream/tracking for branch '#{branch_name}' to '#{upstream}'." }
635
+
636
+ command(:branch, ['--set-upstream-to', upstream, branch_name])
637
+ end
638
+
639
+
640
+ def sync_control_filename(branch_name)
641
+ File.join(File.join(workdir, '.git'), "gitprocess-sync-#{remote.name}--#{branch_name}")
642
+ end
643
+
644
+ end
645
+
646
+ end