git-review 0.4.1

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 (4) hide show
  1. data/LICENSE +20 -0
  2. data/bin/git-review +8 -0
  3. data/lib/git-review.rb +315 -0
  4. metadata +112 -0
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Scott Chacon
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/bin/git-review ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+ require 'rubygems'
3
+
4
+ $LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
5
+
6
+ require 'git-review'
7
+
8
+ GitReview.start(ARGV)
data/lib/git-review.rb ADDED
@@ -0,0 +1,315 @@
1
+ require 'json'
2
+ require 'launchy'
3
+ require 'octokit'
4
+
5
+ class GitReview
6
+
7
+ REVIEW_CACHE_FILE = '.git/review_cache.json'
8
+
9
+ def initialize(args)
10
+ @command = args.shift
11
+ @user, @repo = repo_info
12
+ @args = args
13
+ end
14
+
15
+ def self.start(args)
16
+ GitReview.new(args).run
17
+ end
18
+
19
+ def run
20
+ configure
21
+ if @command && self.respond_to?(@command)
22
+ update
23
+ self.send @command
24
+ elsif %w(-h --help).include?(@command)
25
+ usage
26
+ else
27
+ help
28
+ end
29
+ end
30
+
31
+ ## COMMANDS ##
32
+
33
+ def help
34
+ puts "No command: #{@command}"
35
+ puts "Try: update, list, show, merge, browse, create"
36
+ puts "or call with '-h' for usage information"
37
+ end
38
+
39
+ def usage
40
+ puts <<-USAGE
41
+ Usage: git review list [--reverse]
42
+ or: git review show <number> [--full]
43
+ or: git review browse <number>
44
+ or: git review merge <number>
45
+ or: git review create
46
+ USAGE
47
+ end
48
+
49
+ def merge
50
+ num = @args.shift
51
+ option = @args.shift
52
+ if p = pull_num(num)
53
+ if p['head']['repository']
54
+ o = p['head']['repository']['owner']
55
+ r = p['head']['repository']['name']
56
+ else # they deleted the source repo
57
+ o = p['head']['user']['login']
58
+ purl = p['patch_url']
59
+ puts "Sorry, #{o} deleted the source repository, git-review doesn't support this."
60
+ puts "You can manually patch your repo by running:"
61
+ puts
62
+ puts " curl #{purl} | git am"
63
+ puts
64
+ puts "Tell the contributor not to do this."
65
+ return false
66
+ end
67
+ s = p['head']['sha']
68
+
69
+ message = "Merge pull request ##{num} from #{o}/#{r}\n\n---\n\n"
70
+ message += p['body'].gsub("'", '')
71
+ cmd = ''
72
+ if option == '--log'
73
+ message += "\n\n---\n\nMerge Log:\n"
74
+ puts cmd = "git merge --no-ff --log -m '#{message}' #{s}"
75
+ else
76
+ puts cmd = "git merge --no-ff -m '#{message}' #{s}"
77
+ end
78
+ exec(cmd)
79
+ else
80
+ puts "No such number"
81
+ end
82
+ end
83
+
84
+ def show
85
+ num = @args.shift
86
+ option = @args.shift
87
+ if p = pull_num(num)
88
+ puts "Number : #{p['number']}"
89
+ puts "Label : #{p['head']['label']}"
90
+ puts "Created : #{p['created_at']}"
91
+ puts "Votes : #{p['votes']}"
92
+ puts "Comments : #{p['comments']}"
93
+ puts
94
+ puts "Title : #{p['title']}"
95
+ puts "Body :"
96
+ puts
97
+ puts p['body']
98
+ puts
99
+ puts '------------'
100
+ puts
101
+ if option == '--full'
102
+ exec "git diff --color=always HEAD...#{p['head']['sha']}"
103
+ else
104
+ puts "cmd: git diff HEAD...#{p['head']['sha']}"
105
+ puts git("diff --stat --color=always HEAD...#{p['head']['sha']}")
106
+ end
107
+ else
108
+ puts "No such number"
109
+ end
110
+ end
111
+
112
+ def browse
113
+ num = @args.shift
114
+ if p = pull_num(num)
115
+ Launchy.open(p['html_url'])
116
+ else
117
+ puts "No such number"
118
+ end
119
+ end
120
+
121
+ def list
122
+ option = @args.shift
123
+ puts "Open Pull Requests for #{@user}/#{@repo}"
124
+ pulls = get_pull_info
125
+ pulls.reverse! if option == '--reverse'
126
+ count = 0
127
+ pulls.each do |pull|
128
+ line = []
129
+ line << l(pull['number'], 4)
130
+ line << l(Date.parse(pull['created_at']).strftime("%m/%d"), 5)
131
+ line << l(pull['comments'], 2)
132
+ line << l(pull['title'], 35)
133
+ line << l(pull['head']['label'], 20)
134
+ sha = pull['head']['sha']
135
+ if not_merged?(sha)
136
+ puts line.join ' '
137
+ count += 1
138
+ end
139
+ end
140
+ if count == 0
141
+ puts ' -- no open pull requests --'
142
+ end
143
+ end
144
+
145
+ def update
146
+ cache_pull_info
147
+ fetch_stale_forks
148
+ end
149
+
150
+ def create
151
+ repo = "#{@user}/#{@repo}"
152
+ to_branch = 'master'
153
+ from_branch = get_from_branch_title
154
+ title = 'my title'
155
+ body = 'my body'
156
+
157
+ Octokit.create_pull_request(repo, to_branch, from_branch, title, body)
158
+ end
159
+
160
+ def get_from_branch_title
161
+ git('branch', false).match(/\*(.*)/)[0][2..-1]
162
+ end
163
+
164
+ def fetch_stale_forks
165
+ pulls = get_pull_info
166
+ repos = {}
167
+ pulls.each do |pull|
168
+ next if pull['head']['repository'].nil? # Fork has been deleted
169
+ o = pull['head']['repository']['owner']
170
+ r = pull['head']['repository']['name']
171
+ s = pull['head']['sha']
172
+ if !has_sha(s)
173
+ repo = "#{o}/#{r}"
174
+ repos[repo] = true
175
+ end
176
+ end
177
+ if github_credentials_provided?
178
+ endpoint = "git@github.com:"
179
+ else
180
+ endpoint = github_endpoint + "/"
181
+ end
182
+ repos.each do |repo, bool|
183
+ puts "fetching #{repo}"
184
+ git("fetch #{endpoint}#{repo}.git +refs/heads/*:refs/pr/#{repo}/*")
185
+ end
186
+ end
187
+
188
+ def has_sha(sha)
189
+ git("show #{sha} 2>&1")
190
+ $?.exitstatus == 0
191
+ end
192
+
193
+ def not_merged?(sha)
194
+ commits = git("rev-list #{sha} ^HEAD 2>&1")
195
+ commits.split("\n").size > 0
196
+ end
197
+
198
+ # DISPLAY HELPER FUNCTIONS #
199
+
200
+ def l(info, size)
201
+ clean(info)[0, size].ljust(size)
202
+ end
203
+
204
+ def r(info, size)
205
+ clean(info)[0, size].rjust(size)
206
+ end
207
+
208
+ def clean(info)
209
+ info.to_s.gsub("\n", ' ')
210
+ end
211
+
212
+ # PRIVATE REPOSITORIES ACCESS
213
+
214
+ def configure
215
+ Octokit.configure do |config|
216
+ config.login = github_login
217
+ config.token = github_token
218
+ config.endpoint = github_endpoint
219
+ end
220
+ end
221
+
222
+ def github_login
223
+ git("config --get-all github.user")
224
+ end
225
+
226
+ def github_token
227
+ git("config --get-all github.token")
228
+ end
229
+
230
+ def github_endpoint
231
+ host = git("config --get-all github.host")
232
+ if host.size > 0
233
+ host
234
+ else
235
+ 'https://github.com'
236
+ end
237
+ end
238
+
239
+ # API/DATA HELPER FUNCTIONS #
240
+
241
+ def github_credentials_provided?
242
+ if github_token.empty? && github_login.empty?
243
+ return false
244
+ end
245
+ true
246
+ end
247
+
248
+ def get_pull_info
249
+ get_data(REVIEW_CACHE_FILE)['review']
250
+ end
251
+
252
+ def get_data(file)
253
+ data = JSON.parse(File.read(file))
254
+ end
255
+
256
+ def cache_pull_info
257
+ response = Octokit.pull_requests("#{@user}/#{@repo}")
258
+ save_data({'review' => response}, REVIEW_CACHE_FILE)
259
+ end
260
+
261
+ def save_data(data, file)
262
+ File.open(file, "w+") do |f|
263
+ f.puts data.to_json
264
+ end
265
+ end
266
+
267
+ def pull_num(num)
268
+ data = get_pull_info
269
+ data.select { |p| p['number'].to_s == num.to_s }.first
270
+ end
271
+
272
+ def github_insteadof_matching(c, u)
273
+ first = c.collect {|k,v| [v, /url\.(.*github\.com.*)\.insteadof/.match(k)]}.
274
+ find {|v,m| u.index(v) and m != nil}
275
+ if first
276
+ return first[0], first[1][1]
277
+ end
278
+ return nil, nil
279
+ end
280
+
281
+ def github_user_and_proj(u)
282
+ # Trouble getting optional ".git" at end to work, so put that logic below
283
+ m = /github\.com.(.*?)\/(.*)/.match(u)
284
+ if m
285
+ return m[1], m[2].sub(/\.git\Z/, "")
286
+ end
287
+ return nil, nil
288
+ end
289
+
290
+ def repo_info
291
+ c = {}
292
+ config = git('config --list')
293
+ config.split("\n").each do |line|
294
+ k, v = line.split('=')
295
+ c[k] = v
296
+ end
297
+ u = c['remote.origin.url']
298
+
299
+ user, proj = github_user_and_proj(u)
300
+ if !(user and proj)
301
+ short, base = github_insteadof_matching(c, u)
302
+ if short and base
303
+ u = u.sub(short, base)
304
+ user, proj = github_user_and_proj(u)
305
+ end
306
+ end
307
+ [user, proj]
308
+ end
309
+
310
+ def git(command, chomp=true)
311
+ s = `git #{command}`
312
+ s.chomp! if chomp
313
+ s
314
+ end
315
+ end
metadata ADDED
@@ -0,0 +1,112 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: git-review
3
+ version: !ruby/object:Gem::Version
4
+ hash: 13
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 4
9
+ - 1
10
+ version: 0.4.1
11
+ platform: ruby
12
+ authors:
13
+ - Cristian Messel, Dominik Bamberger
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-07-12 00:00:00 +02:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: json
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 3
30
+ segments:
31
+ - 0
32
+ version: "0"
33
+ type: :runtime
34
+ version_requirements: *id001
35
+ - !ruby/object:Gem::Dependency
36
+ name: launchy
37
+ prerelease: false
38
+ requirement: &id002 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ hash: 3
44
+ segments:
45
+ - 0
46
+ version: "0"
47
+ type: :runtime
48
+ version_requirements: *id002
49
+ - !ruby/object:Gem::Dependency
50
+ name: octokit
51
+ prerelease: false
52
+ requirement: &id003 !ruby/object:Gem::Requirement
53
+ none: false
54
+ requirements:
55
+ - - "="
56
+ - !ruby/object:Gem::Version
57
+ hash: 9
58
+ segments:
59
+ - 0
60
+ - 5
61
+ - 1
62
+ version: 0.5.1
63
+ type: :runtime
64
+ version_requirements: *id003
65
+ description: git-review facilitates github code reviews.
66
+ email: bamberger.dominik@gmail.com
67
+ executables:
68
+ - git-review
69
+ extensions: []
70
+
71
+ extra_rdoc_files: []
72
+
73
+ files:
74
+ - LICENSE
75
+ - lib/git-review.rb
76
+ - bin/git-review
77
+ has_rdoc: true
78
+ homepage: http://github.com/b4mboo/git-review
79
+ licenses: []
80
+
81
+ post_install_message:
82
+ rdoc_options: []
83
+
84
+ require_paths:
85
+ - lib
86
+ required_ruby_version: !ruby/object:Gem::Requirement
87
+ none: false
88
+ requirements:
89
+ - - ">="
90
+ - !ruby/object:Gem::Version
91
+ hash: 3
92
+ segments:
93
+ - 0
94
+ version: "0"
95
+ required_rubygems_version: !ruby/object:Gem::Requirement
96
+ none: false
97
+ requirements:
98
+ - - ">="
99
+ - !ruby/object:Gem::Version
100
+ hash: 3
101
+ segments:
102
+ - 0
103
+ version: "0"
104
+ requirements: []
105
+
106
+ rubyforge_project:
107
+ rubygems_version: 1.5.2
108
+ signing_key:
109
+ specification_version: 3
110
+ summary: facilitates github code reviews
111
+ test_files: []
112
+