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.
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