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,108 @@
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
+ module GitProc
14
+
15
+ #
16
+ # The status of the Git repository.
17
+ #
18
+ # @!attribute [r] unmerged
19
+ # @return [Enumerable] a sorted list of unmerged files
20
+ # @!attribute [r] modified
21
+ # @return [Enumerable] a sorted list of modified files
22
+ # @!attribute [r] deleted
23
+ # @return [Enumerable] a sorted list of deleted files
24
+ # @!attribute [r] added
25
+ # @return [Enumerable] a sorted list of files that have been added
26
+ # @!attribute [r] unknown
27
+ # @return [Enumerable] a sorted list of unknown files
28
+ class GitStatus
29
+ attr_reader :unmerged, :modified, :deleted, :added, :unknown
30
+
31
+
32
+ def initialize(lib)
33
+ unmerged = []
34
+ modified = []
35
+ deleted = []
36
+ added = []
37
+ unknown = []
38
+
39
+ stats = lib.porcelain_status.split("\n")
40
+
41
+ stats.each do |s|
42
+ stat = s[0..1]
43
+ file = s[3..-1]
44
+ #puts "stat #{stat} - #{file}"
45
+ f = unquote(file)
46
+ case stat
47
+ when 'U ', ' U'
48
+ unmerged << f
49
+ when 'UU'
50
+ unmerged << f
51
+ modified << f
52
+ when 'M ', ' M', 'MM'
53
+ modified << f
54
+ when 'MD'
55
+ modified << f
56
+ deleted << f
57
+ when 'D ', ' D', 'DD'
58
+ deleted << f
59
+ when 'DU', 'UD'
60
+ deleted << f
61
+ unmerged << f
62
+ when 'A ', ' A'
63
+ added << f
64
+ when 'AD'
65
+ added << f
66
+ deleted << f
67
+ when 'AA', 'AU', 'UA'
68
+ added << f
69
+ unmerged << f
70
+ when 'AM', 'MA'
71
+ added << f
72
+ modified << f
73
+ when '??', '!!'
74
+ unknown << f
75
+ when 'R '
76
+ old_file, new_file = file.split(' -> ')
77
+ deleted << unquote(old_file)
78
+ added << unquote(new_file)
79
+ when 'C '
80
+ old_file, new_file = file.split(' -> ')
81
+ added << unquote(old_file)
82
+ added << unquote(new_file)
83
+ else
84
+ raise "Do not know what to do with status #{stat} - #{file}"
85
+ end
86
+ end
87
+
88
+ @unmerged = unmerged.sort.uniq.freeze
89
+ @modified = modified.sort.uniq.freeze
90
+ @deleted = deleted.sort.uniq.freeze
91
+ @added = added.sort.uniq.freeze
92
+ @unknown = unknown.sort.uniq.freeze
93
+ end
94
+
95
+
96
+ def unquote(file)
97
+ file.match(/^"?(.*?)"?$/)[1]
98
+ end
99
+
100
+
101
+ # @return [Boolean] are there any changes in the index or working directory?
102
+ def clean?
103
+ @unmerged.empty? and @modified.empty? and @deleted.empty? and @added.empty? and @unknown.empty?
104
+ end
105
+
106
+ end
107
+
108
+ end
@@ -0,0 +1,298 @@
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_lib'
14
+ require 'highline/import'
15
+ require 'octokit'
16
+ require 'uri'
17
+
18
+
19
+ #
20
+ # Provides methods related to GitHub configuration
21
+ #
22
+ module GitHubService
23
+
24
+ class Configuration
25
+
26
+ attr_reader :git_config
27
+
28
+
29
+ #
30
+ # @param [GitProc::GitConfig] git_config
31
+ # @param [Hash] opts
32
+ # @option opts [String] :remote_name (#remote_name) The "remote" name to use (e.g., 'origin')
33
+ # @option opts [String] :user the username to authenticate with
34
+ # @option opts [String] :password (#password) the password to authenticate with
35
+ #
36
+ # @return [String] the OAuth token
37
+ #
38
+ def initialize(git_config, opts = {})
39
+ @git_config = git_config
40
+ @user = opts[:user]
41
+ @password = opts[:password]
42
+ @remote_name = opts[:remote_name]
43
+ end
44
+
45
+
46
+ # @return [String]
47
+ def remote_name
48
+ unless @remote_name
49
+ @remote_name = gitlib.remote.name
50
+ raise NoRemoteRepository.new('No remote repository is defined') unless @remote_name
51
+ end
52
+ @remote_name
53
+ end
54
+
55
+
56
+ # @return [String]
57
+ def user
58
+ @user ||= Configuration.ask_for_user(gitlib)
59
+ end
60
+
61
+
62
+ # @return [String]
63
+ def password
64
+ @password ||= Configuration.ask_for_password
65
+ end
66
+
67
+
68
+ # @return [Octokit::Client]
69
+ def client
70
+ create_client
71
+ end
72
+
73
+
74
+ # @return [GitProc::GitLib]
75
+ def gitlib
76
+ @git_config.gitlib
77
+ end
78
+
79
+
80
+ # @return [Octokit::Client]
81
+ def create_client(opts = {})
82
+ logger.debug { "Creating GitHub client for user #{user} using token '#{auth_token}'" }
83
+
84
+ base_url = opts[:base_url] || base_github_api_url_for_remote
85
+
86
+ configure_octokit(:base_url => base_url)
87
+
88
+ Octokit::Client.new(:login => user, :oauth_token => auth_token)
89
+ end
90
+
91
+
92
+ #
93
+ # Configures Octokit to use the appropriate URLs for GitHub server.
94
+ #
95
+ # @param [Hash] opts the options to create a message with
96
+ # @option opts [String] :base_url The base URL to use for the GitHub server
97
+ #
98
+ # @return [void]
99
+ #
100
+ def configure_octokit(opts = {})
101
+ base_url = opts[:base_url] || base_github_api_url_for_remote
102
+ Octokit.configure do |c|
103
+ c.api_endpoint = api_endpoint(base_url)
104
+ c.web_endpoint = web_endpoint(base_url)
105
+ c.faraday_config do |f|
106
+ #f.response :logger
107
+ end
108
+ end
109
+ end
110
+
111
+
112
+ #
113
+ # Determines the URL used for using the GitHub REST interface based
114
+ # on a "base" URL.
115
+ #
116
+ # If the "base_url" is not provided, then it assumes that this object
117
+ # has a "remote_name" property that it can ask.
118
+ #
119
+ # @param [String] base_url the base GitHub URL
120
+ # @return [String] the GitHub REST API URL
121
+ #
122
+ def api_endpoint(base_url = nil)
123
+ base_url ||= base_github_api_url_for_remote
124
+ if /github.com/ !~ base_url
125
+ "#{base_url}/api/v3"
126
+ else
127
+ Octokit::Configuration::DEFAULT_API_ENDPOINT
128
+ end
129
+ end
130
+
131
+
132
+ #
133
+ # Determines the URL used for using the GitHub web interface based
134
+ # on a "base" URL.
135
+ #
136
+ # If the "base_url" is not provided, then it assumes that this object
137
+ # has a "remote_name" property that it can ask.
138
+ #
139
+ # @param [String] base_url the base GitHub URL
140
+ # @return [String] the GitHub web URL
141
+ #
142
+ def web_endpoint(base_url = nil)
143
+ base_url ||= base_github_api_url_for_remote
144
+ if /github.com/ !~ base_url
145
+ base_url
146
+ else
147
+ Octokit::Configuration::DEFAULT_WEB_ENDPOINT
148
+ end
149
+ end
150
+
151
+
152
+ #
153
+ # Determines the base URL for GitHub API calls.
154
+ #
155
+ # @return [String] the base GitHub API URL
156
+ #
157
+ def base_github_api_url_for_remote
158
+ url = gitlib.remote.expanded_url(remote_name)
159
+ Configuration.url_to_base_github_api_url(url)
160
+ end
161
+
162
+
163
+ #
164
+ # Translate any "git known" URL to the HTTP(S) URL needed for
165
+ # GitHub API calls.
166
+ #
167
+ # @param url [String] the URL to translate
168
+ # @return [String] the base GitHub API URL
169
+ #
170
+ def self.url_to_base_github_api_url(url)
171
+ uri = URI.parse(url)
172
+ host = uri.host
173
+
174
+ if /github.com$/ =~ host
175
+ 'https://api.github.com'
176
+ else
177
+ scheme = uri.scheme
178
+ scheme = 'https' unless scheme.start_with?('http')
179
+ host = 'unknown-host' unless host
180
+ "#{scheme}://#{host}"
181
+ end
182
+ end
183
+
184
+
185
+ #
186
+ # Create a GitHub client using username and password specifically.
187
+ # Meant to be used to get an OAuth token for "regular" client calls.
188
+ #
189
+ # @param [Hash] opts the options to create a message with
190
+ # @option opts [String] :base_url The base URL to use for the GitHub server
191
+ # @option opts [String] :remote_name (#remote_name) The "remote" name to use (e.g., 'origin')
192
+ # @option opts [String] :user the username to authenticate with
193
+ # @option opts [String] :password (#password) the password to authenticate with
194
+ #
195
+ def create_pw_client(opts = {})
196
+ usr = opts[:user] || user()
197
+ pw = opts[:password] || password()
198
+
199
+ logger.debug { "Creating GitHub client for user #{usr} using BasicAuth w/ password" }
200
+
201
+ configure_octokit(opts)
202
+
203
+ Octokit::Client.new(:login => usr, :password => pw)
204
+ end
205
+
206
+
207
+ #
208
+ # Returns to OAuth token. If it's in .git/config, returns that.
209
+ # Otherwise it connects to GitHub to get the authorization token.
210
+ #
211
+ # @param [Hash] opts
212
+ # @option opts [String] :base_url The base URL to use for the GitHub server
213
+ # @option opts [String] :remote_name (#remote_name) The "remote" name to use (e.g., 'origin')
214
+ # @option opts [String] :user the username to authenticate with
215
+ # @option opts [String] :password (#password) the password to authenticate with
216
+ #
217
+ # @return [String]
218
+ #
219
+ def auth_token(opts = {})
220
+ get_config_auth_token() || create_authorization(opts)
221
+ end
222
+
223
+
224
+ #
225
+ # Connects to GitHub to get an OAuth token.
226
+ #
227
+ # @param [Hash] opts
228
+ # @option opts [String] :base_url The base URL to use for the GitHub server
229
+ # @option opts [String] :remote_name (#remote_name) The "remote" name to use (e.g., 'origin')
230
+ # @option opts [String] :user the username to authenticate with
231
+ # @option opts [String] :password (#password) the password to authenticate with
232
+ #
233
+ # @return [String] the OAuth token
234
+ #
235
+ def create_authorization(opts = {})
236
+ username = opts[:user] || self.user
237
+ remote = opts[:remote_name] || self.remote_name
238
+ logger.info("Authorizing #{username} to work with #{remote}.")
239
+
240
+ auth = create_pw_client(opts).create_authorization(
241
+ :scopes => %w(repo user gist),
242
+ :note => 'Git-Process',
243
+ :note_url => 'http://jdigger.github.com/git-process')
244
+
245
+ config_auth_token = auth['token']
246
+
247
+ # remember it for next time
248
+ gitlib.config['gitProcess.github.authToken'] = config_auth_token
249
+
250
+ config_auth_token
251
+ end
252
+
253
+
254
+ # @return [String]
255
+ def get_config_auth_token
256
+ c_auth_token = gitlib.config['gitProcess.github.authToken']
257
+ (c_auth_token.nil? or c_auth_token.empty?) ? nil : c_auth_token
258
+ end
259
+
260
+
261
+ def logger
262
+ gitlib.logger
263
+ end
264
+
265
+
266
+ private
267
+
268
+
269
+ def self.ask_for_user(gitlib)
270
+ user = gitlib.config['github.user']
271
+ if user.nil? or user.empty?
272
+ user = ask("Your <%= color('GitHub', [:bold, :blue]) %> username: ") do |q|
273
+ q.validate = /^\w\w+$/
274
+ end
275
+ gitlib.config['github.user'] = user
276
+ end
277
+ user
278
+ end
279
+
280
+
281
+ def self.ask_for_password
282
+ ask("Your <%= color('GitHub', [:bold, :blue]) %> password: ") do |q|
283
+ q.validate = /^\S\S+$/
284
+ q.echo = 'x'
285
+ end
286
+ end
287
+
288
+ end
289
+
290
+
291
+ class Error < ::StandardError
292
+ end
293
+
294
+
295
+ class NoRemoteRepository < Error
296
+ end
297
+
298
+ end
@@ -0,0 +1,165 @@
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/github_configuration'
14
+ require 'octokit'
15
+ require 'octokit/repository'
16
+
17
+
18
+ module GitHub
19
+
20
+ class PullRequest
21
+ attr_reader :gitlib, :repo, :remote_name, :client, :configuration
22
+
23
+ MAX_RESEND = 5
24
+
25
+ def initialize(lib, remote_name, repo, opts = {})
26
+ @gitlib = lib
27
+ @repo = repo
28
+ @remote_name = remote_name
29
+ @configuration = GitHubService::Configuration.new(gitlib.config, :user => opts[:user], :password => opts[:password])
30
+ end
31
+
32
+
33
+ def client
34
+ @client ||= @configuration.create_client
35
+ end
36
+
37
+
38
+ def pull_requests(state = 'open', opts = {})
39
+ @pull_requests ||= client.pull_requests(repo, state, opts)
40
+ end
41
+
42
+
43
+ def create(base, head, title, body)
44
+ logger.info { "Creating a pull request asking for '#{head}' to be merged into '#{base}' on #{repo}." }
45
+ begin
46
+ client.create_pull_request(repo, base, head, title, body)
47
+ rescue Octokit::UnprocessableEntity => exp
48
+ pull = pull_requests.find { |p| p[:head][:ref] == head and p[:base][:ref] == base }
49
+ if pull
50
+ logger.warn { "Pull request already exists. See #{pull[:html_url]}" }
51
+ else
52
+ logger.warn { "UnprocessableEntity: #{exp}" }
53
+ end
54
+ pull
55
+ end
56
+ end
57
+
58
+
59
+ def logger
60
+ @gitlib.logger
61
+ end
62
+
63
+
64
+ def pull_request(pr_number)
65
+ client.pull_request(repo, pr_number)
66
+ end
67
+
68
+
69
+ #
70
+ # Find the pull request (PR) that matches the 'head' and 'base'.
71
+ #
72
+ # @param [String] base what the PR is merging into
73
+ # @param [String] head the branch of the PR
74
+ #
75
+ # @return [Hash]
76
+ # @raise [NotFoundError] if the pull request does not exist
77
+ #
78
+ def get_pull_request(base, head)
79
+ find_pull_request(base, head, true)
80
+ end
81
+
82
+
83
+ #
84
+ # Find the pull request (PR) that matches the 'head' and 'base'.
85
+ #
86
+ # @param [String] base what the PR is merging into
87
+ # @param [String] head the branch of the PR
88
+ # @param [boolean] error_if_missing should this error-out if the PR is not found?
89
+ #
90
+ # @return [Hash, nil]
91
+ # @raise [NotFoundError] if the pull request does not exist and 'error_if_missing' is true
92
+ #
93
+ def find_pull_request(base, head, error_if_missing = false)
94
+ logger.info { "Looking for a pull request asking for '#{head}' to be merged into '#{base}' on #{repo}." }
95
+
96
+ json = pull_requests
97
+ pr = json.find { |p| p[:head][:ref] == head and p[:base][:ref] == base }
98
+
99
+ raise NotFoundError.new(base, head, repo, json) if error_if_missing && pr.nil?
100
+
101
+ pr
102
+ end
103
+
104
+
105
+ def close(*args)
106
+ pull_number = if args.size == 2
107
+ get_pull_request(args[0], args[1])[:number]
108
+ elsif args.size == 1
109
+ args[0]
110
+ else
111
+ raise ArgumentError.new('close(..) needs 1 or 2 arguments')
112
+ end
113
+
114
+ logger.info { "Closing a pull request \##{pull_number} on #{repo}." }
115
+
116
+ send_close_req(pull_number, 1)
117
+ end
118
+
119
+
120
+ class NotFoundError < StandardError
121
+ attr_reader :base, :head, :repo
122
+
123
+
124
+ def initialize(base, head, repo, pull_requests_json)
125
+ @base = base
126
+ @head = head
127
+ @repo = repo
128
+
129
+ @pull_requests = pull_requests_json.map do |p|
130
+ {:head => p[:head][:ref], :base => p[:base][:ref]}
131
+ end
132
+
133
+ msg = "Could not find a pull request for '#{head}' to be merged with '#{base}' on #{repo}."
134
+ msg += "\n\nExisting Pull Requests:"
135
+ msg = pull_requests.inject(msg) { |a, v| "#{a}\n #{v[:head]} -> #{v[:base]}" }
136
+
137
+ super(msg)
138
+ end
139
+
140
+
141
+ def pull_requests
142
+ @pull_requests
143
+ end
144
+
145
+ end
146
+
147
+
148
+ private
149
+
150
+
151
+ def send_close_req(pull_number, count)
152
+ begin
153
+ client.patch("repos/#{Octokit::Repository.new(repo)}/pulls/#{pull_number}", {:state => 'closed'})
154
+ rescue Octokit::UnprocessableEntity => exp
155
+ if count > MAX_RESEND
156
+ raise exp
157
+ end
158
+ logger.warn { "Retrying closing a pull request \##{pull_number} on #{repo} - try \##{count + 1}" }
159
+ send_close_req(pull_number, count + 1)
160
+ end
161
+ end
162
+
163
+ end
164
+
165
+ end