git-process-lib 2.0.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.
- data/CHANGELOG.md +123 -0
- data/Gemfile +21 -0
- data/Gemfile.lock +57 -0
- data/LICENSE +193 -0
- data/README.md +342 -0
- data/Rakefile +32 -0
- data/bin/git-new-fb +39 -0
- data/bin/git-pull-request +63 -0
- data/bin/git-sync +38 -0
- data/bin/git-to-master +44 -0
- data/docs/git-new-fb.1.adoc +83 -0
- data/docs/git-process.1.adoc +227 -0
- data/docs/git-pull-request.1.adoc +166 -0
- data/docs/git-sync.1.adoc +120 -0
- data/docs/git-to-master.1.adoc +172 -0
- data/git-new-fb.gemspec +20 -0
- data/git-process-lib.gemspec +25 -0
- data/git-process.gemspec +22 -0
- data/git-pull-request.gemspec +20 -0
- data/git-sync.gemspec +20 -0
- data/git-to-master.gemspec +20 -0
- data/lib/git-process/abstract_error_builder.rb +53 -0
- data/lib/git-process/changed_file_helper.rb +115 -0
- data/lib/git-process/git_abstract_merge_error_builder.rb +130 -0
- data/lib/git-process/git_branch.rb +105 -0
- data/lib/git-process/git_branches.rb +81 -0
- data/lib/git-process/git_config.rb +135 -0
- data/lib/git-process/git_lib.rb +646 -0
- data/lib/git-process/git_logger.rb +84 -0
- data/lib/git-process/git_merge_error.rb +28 -0
- data/lib/git-process/git_process.rb +159 -0
- data/lib/git-process/git_process_error.rb +18 -0
- data/lib/git-process/git_process_options.rb +101 -0
- data/lib/git-process/git_rebase_error.rb +30 -0
- data/lib/git-process/git_remote.rb +222 -0
- data/lib/git-process/git_status.rb +108 -0
- data/lib/git-process/github_configuration.rb +298 -0
- data/lib/git-process/github_pull_request.rb +165 -0
- data/lib/git-process/new_fb.rb +49 -0
- data/lib/git-process/parked_changes_error.rb +41 -0
- data/lib/git-process/pull_request.rb +136 -0
- data/lib/git-process/pull_request_error.rb +25 -0
- data/lib/git-process/rebase_to_master.rb +148 -0
- data/lib/git-process/sync_process.rb +55 -0
- data/lib/git-process/syncer.rb +157 -0
- data/lib/git-process/uncommitted_changes_error.rb +23 -0
- data/lib/git-process/version.rb +22 -0
- data/local-build.rb +24 -0
- data/spec/FileHelpers.rb +19 -0
- data/spec/GitRepoHelper.rb +123 -0
- data/spec/changed_file_helper_spec.rb +127 -0
- data/spec/git_abstract_merge_error_builder_spec.rb +64 -0
- data/spec/git_branch_spec.rb +123 -0
- data/spec/git_config_spec.rb +45 -0
- data/spec/git_lib_spec.rb +176 -0
- data/spec/git_logger_spec.rb +66 -0
- data/spec/git_process_spec.rb +208 -0
- data/spec/git_remote_spec.rb +227 -0
- data/spec/git_status_spec.rb +122 -0
- data/spec/github_configuration_spec.rb +152 -0
- data/spec/github_pull_request_spec.rb +117 -0
- data/spec/github_test_helper.rb +49 -0
- data/spec/new_fb_spec.rb +126 -0
- data/spec/pull_request_helper.rb +94 -0
- data/spec/pull_request_spec.rb +137 -0
- data/spec/rebase_to_master_spec.rb +362 -0
- data/spec/spec_helper.rb +21 -0
- data/spec/sync_spec.rb +1474 -0
- 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
|