git-topic 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. data/.gitignore +4 -0
  2. data/.gvimrc +23 -0
  3. data/.rspec +1 -0
  4. data/.rvmrc +1 -0
  5. data/.vimproject +26 -0
  6. data/.vimrc +1 -0
  7. data/Gemfile +19 -0
  8. data/Gemfile.lock +63 -0
  9. data/History.txt +6 -0
  10. data/LICENSE +20 -0
  11. data/README.rdoc +192 -0
  12. data/Rakefile +62 -0
  13. data/VERSION +1 -0
  14. data/autotest/discover.rb +1 -0
  15. data/bin/git-topic +143 -0
  16. data/git-topic.gemspec +127 -0
  17. data/lib/git-topic.rb +375 -0
  18. data/lib/util.rb +34 -0
  19. data/spec/git-topic_spec.rb +378 -0
  20. data/spec/spec_helper.rb +56 -0
  21. data/spec/template/origin/HEAD +1 -0
  22. data/spec/template/origin/config +7 -0
  23. data/spec/template/origin/description +1 -0
  24. data/spec/template/origin/hooks/applypatch-msg.sample +15 -0
  25. data/spec/template/origin/hooks/commit-msg.sample +24 -0
  26. data/spec/template/origin/hooks/post-commit.sample +8 -0
  27. data/spec/template/origin/hooks/post-receive.sample +15 -0
  28. data/spec/template/origin/hooks/post-update.sample +8 -0
  29. data/spec/template/origin/hooks/pre-applypatch.sample +14 -0
  30. data/spec/template/origin/hooks/pre-commit.sample +46 -0
  31. data/spec/template/origin/hooks/pre-rebase.sample +169 -0
  32. data/spec/template/origin/hooks/prepare-commit-msg.sample +36 -0
  33. data/spec/template/origin/hooks/update.sample +128 -0
  34. data/spec/template/origin/info/exclude +6 -0
  35. data/spec/template/origin/objects/0a/da6d051b94cd0df50f5a0b7229aec26f0d2cdf +0 -0
  36. data/spec/template/origin/objects/0c/e06c616769768f09f5e629cfcc68eabe3dee81 +0 -0
  37. data/spec/template/origin/objects/20/049991cdafdce826f5a3c01e10ffa84d6997ec +0 -0
  38. data/spec/template/origin/objects/33/1d827fd47fb234af54e3a4bbf8c6705e9116cc +3 -0
  39. data/spec/template/origin/objects/41/51899b742fd6b1c873b177b9d13451682089bc +0 -0
  40. data/spec/template/origin/objects/44/ffd9c9c8b52b201659e3ad318cdad6ec836b46 +0 -0
  41. data/spec/template/origin/objects/4b/825dc642cb6eb9a060e54bf8d69288fbee4904 +0 -0
  42. data/spec/template/origin/objects/55/eeb01bdf874d1a35870bcf24a970c475c63344 +0 -0
  43. data/spec/template/origin/objects/8d/09f9b8d80ce282218125cb0cbf53cccf022203 +0 -0
  44. data/spec/template/origin/objects/b4/8e68d5cac189af36abe48e893d11c24b7b2a19 +0 -0
  45. data/spec/template/origin/objects/c0/838ed2ee8f2e83c8bda859fc5e332b92f0a5a3 +1 -0
  46. data/spec/template/origin/objects/cd/f7b9dbc4911a0d1404db54cde2ed448f6a6afd +0 -0
  47. data/spec/template/origin/objects/d2/6b33daea1ed9823a189992bba38fbc913483c1 +0 -0
  48. data/spec/template/origin/objects/fe/4e254557e19f338f40ccfdc00a7517771db880 +0 -0
  49. data/spec/template/origin/refs/heads/master +1 -0
  50. data/spec/template/origin/refs/heads/rejected/davidjh/krakens +1 -0
  51. data/spec/template/origin/refs/heads/review/davidjh/pirates +1 -0
  52. data/spec/template/origin/refs/heads/review/user24601/ninja-basic +1 -0
  53. data/spec/template/origin/refs/heads/review/user24601/zombie-basic +1 -0
  54. metadata +158 -0
data/git-topic.gemspec ADDED
@@ -0,0 +1,127 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{git-topic}
8
+ s.version = "0.1.1"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["David J. Hamilton"]
12
+ s.date = %q{2010-07-08}
13
+ s.default_executable = %q{git-topic}
14
+ s.description = %q{
15
+ gem command around reviewed topic branches. Supports workflow of the form:
16
+
17
+ # alexander:
18
+ git work-on <topic>
19
+ git done
20
+
21
+ # bismarck:
22
+ git status # notice a review branch
23
+ git review <topic>
24
+ # happy, merge into master, push and cleanup
25
+ git accept
26
+
27
+ git review <topic2>
28
+ # unhappy
29
+ git reject
30
+
31
+ # alexander:
32
+ git status # notice rejected topic
33
+ git work-on <topic>
34
+
35
+ see README.rdoc for more (any) details.
36
+ }
37
+ s.email = %q{git-topic@hjdivad.com}
38
+ s.executables = ["git-topic"]
39
+ s.extra_rdoc_files = [
40
+ "LICENSE",
41
+ "README.rdoc"
42
+ ]
43
+ s.files = [
44
+ ".gitignore",
45
+ ".gvimrc",
46
+ ".rspec",
47
+ ".rvmrc",
48
+ ".vimproject",
49
+ ".vimrc",
50
+ "Gemfile",
51
+ "Gemfile.lock",
52
+ "History.txt",
53
+ "LICENSE",
54
+ "README.rdoc",
55
+ "Rakefile",
56
+ "VERSION",
57
+ "autotest/discover.rb",
58
+ "bin/git-topic",
59
+ "git-topic.gemspec",
60
+ "lib/git-topic.rb",
61
+ "lib/util.rb",
62
+ "spec/git-topic_spec.rb",
63
+ "spec/spec_helper.rb",
64
+ "spec/template/origin/HEAD",
65
+ "spec/template/origin/config",
66
+ "spec/template/origin/description",
67
+ "spec/template/origin/hooks/applypatch-msg.sample",
68
+ "spec/template/origin/hooks/commit-msg.sample",
69
+ "spec/template/origin/hooks/post-commit.sample",
70
+ "spec/template/origin/hooks/post-receive.sample",
71
+ "spec/template/origin/hooks/post-update.sample",
72
+ "spec/template/origin/hooks/pre-applypatch.sample",
73
+ "spec/template/origin/hooks/pre-commit.sample",
74
+ "spec/template/origin/hooks/pre-rebase.sample",
75
+ "spec/template/origin/hooks/prepare-commit-msg.sample",
76
+ "spec/template/origin/hooks/update.sample",
77
+ "spec/template/origin/info/exclude",
78
+ "spec/template/origin/objects/0a/da6d051b94cd0df50f5a0b7229aec26f0d2cdf",
79
+ "spec/template/origin/objects/0c/e06c616769768f09f5e629cfcc68eabe3dee81",
80
+ "spec/template/origin/objects/20/049991cdafdce826f5a3c01e10ffa84d6997ec",
81
+ "spec/template/origin/objects/33/1d827fd47fb234af54e3a4bbf8c6705e9116cc",
82
+ "spec/template/origin/objects/41/51899b742fd6b1c873b177b9d13451682089bc",
83
+ "spec/template/origin/objects/44/ffd9c9c8b52b201659e3ad318cdad6ec836b46",
84
+ "spec/template/origin/objects/4b/825dc642cb6eb9a060e54bf8d69288fbee4904",
85
+ "spec/template/origin/objects/55/eeb01bdf874d1a35870bcf24a970c475c63344",
86
+ "spec/template/origin/objects/8d/09f9b8d80ce282218125cb0cbf53cccf022203",
87
+ "spec/template/origin/objects/b4/8e68d5cac189af36abe48e893d11c24b7b2a19",
88
+ "spec/template/origin/objects/c0/838ed2ee8f2e83c8bda859fc5e332b92f0a5a3",
89
+ "spec/template/origin/objects/cd/f7b9dbc4911a0d1404db54cde2ed448f6a6afd",
90
+ "spec/template/origin/objects/d2/6b33daea1ed9823a189992bba38fbc913483c1",
91
+ "spec/template/origin/objects/fe/4e254557e19f338f40ccfdc00a7517771db880",
92
+ "spec/template/origin/refs/heads/master",
93
+ "spec/template/origin/refs/heads/rejected/davidjh/krakens",
94
+ "spec/template/origin/refs/heads/review/davidjh/pirates",
95
+ "spec/template/origin/refs/heads/review/user24601/ninja-basic",
96
+ "spec/template/origin/refs/heads/review/user24601/zombie-basic"
97
+ ]
98
+ s.homepage = %q{http://github.com/hjdivad/git-topic}
99
+ s.rdoc_options = ["--charset=UTF-8"]
100
+ s.require_paths = ["lib"]
101
+ s.rubygems_version = %q{1.3.7}
102
+ s.summary = %q{git command around reviewed topic branches}
103
+ s.test_files = [
104
+ "spec/spec_helper.rb",
105
+ "spec/git-topic_spec.rb"
106
+ ]
107
+
108
+ if s.respond_to? :specification_version then
109
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
110
+ s.specification_version = 3
111
+
112
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
113
+ s.add_development_dependency(%q<rspec>, [">= 1.2.9"])
114
+ s.add_development_dependency(%q<yard>, [">= 0"])
115
+ s.add_development_dependency(%q<cucumber>, [">= 0"])
116
+ else
117
+ s.add_dependency(%q<rspec>, [">= 1.2.9"])
118
+ s.add_dependency(%q<yard>, [">= 0"])
119
+ s.add_dependency(%q<cucumber>, [">= 0"])
120
+ end
121
+ else
122
+ s.add_dependency(%q<rspec>, [">= 1.2.9"])
123
+ s.add_dependency(%q<yard>, [">= 0"])
124
+ s.add_dependency(%q<cucumber>, [">= 0"])
125
+ end
126
+ end
127
+
data/lib/git-topic.rb ADDED
@@ -0,0 +1,375 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: utf-8
3
+
4
+ require 'active_support'
5
+ require 'active_support/core_ext/hash/keys'
6
+
7
+ require 'util'
8
+
9
+
10
+ module GitTopic
11
+ GlobalOptKeys = [ :verbose, :help, :verbose_given ]
12
+
13
+ class << self
14
+
15
+ # Switch to a branch for the given topic.
16
+ def work_on( topic, opts={} )
17
+ raise "Topic must be specified" if topic.nil?
18
+
19
+ # setup a remote branch, if necessary
20
+ wb = wip_branch( topic )
21
+ git(
22
+ "push origin HEAD:#{wb}"
23
+ ) unless remote_branches.include? "origin/#{wb}"
24
+ # switch to the new branch
25
+ git [ switch_to_branch( wb, "origin/#{wb}" )]
26
+
27
+ # Check for rejected branch
28
+ rej_branch = rejected_branch( topic )
29
+ if remote_branches.include? "origin/#{rej_branch}"
30
+ git [
31
+ "reset --hard origin/#{rej_branch}",
32
+ "push origin :#{rej_branch} HEAD:#{wb}",
33
+ ]
34
+ end
35
+ end
36
+
37
+ # Done with the given topic. If none is specified, then topic is assumed to
38
+ # be the current branch (if it's a topic branch).
39
+ def done( topic=nil, opts={} )
40
+ raise(
41
+ "Branch must be a topic branch"
42
+ ) unless current_branch =~ %r{^wip/}
43
+ raise(
44
+ "Working tree must be clean"
45
+ ) unless working_tree_clean?
46
+
47
+ topic = current_topic if topic.nil?
48
+
49
+ wb = wip_branch( topic )
50
+ rb = review_branch( topic )
51
+ git [
52
+ "push origin #{wb}:#{rb} :#{wb}",
53
+ "checkout master",
54
+ "branch -D #{wip_branch( topic )}"
55
+ ]
56
+ end
57
+
58
+ # Produce status like
59
+ #
60
+ # # There are 2 topics you can review.
61
+ # #
62
+ # # from davidjh:
63
+ # # zombies
64
+ # # pirates
65
+ # # from king-julian:
66
+ # # fish
67
+ # # whales
68
+ # #
69
+ # # 2 of your topics were rejected.
70
+ # # dragons
71
+ # # liches
72
+ def status( opts={} )
73
+ opts.assert_valid_keys :prepended, :prepended_given, *GlobalOptKeys
74
+
75
+ sb = ''
76
+ rb = remote_branches_organized
77
+ review_ut = rb[:review]
78
+ rejected_ut = rb[:rejected]
79
+
80
+ unless review_ut.empty?
81
+ prep = review_ut.size == 1 ? "is 1" : "are #{review_ut.size}"
82
+ sb << "# There #{prep} #{'topic'.pluralize( review_ut.size )} you can review.\n\n"
83
+
84
+ sb << review_ut.map do |user, topics|
85
+ sb2 = " from #{user}:\n"
86
+ sb2 << topics.map{|t| " #{t}"}.join( "\n" )
87
+ sb2
88
+ end.join( "\n" )
89
+ end
90
+
91
+ rejected_topics = rejected_ut[ user ] || []
92
+ unless rejected_topics.empty?
93
+ sb << "\n" unless review_ut.empty?
94
+ verb = rejected_topics.size == 1 ? 'is' : 'are'
95
+ sb << "\n#{rejected_topics.size} of your topics #{verb} rejected.\n "
96
+ sb << rejected_topics.join( "\n " )
97
+ end
98
+
99
+ sb.gsub! "\n", "\n# "
100
+ sb << "\n" unless sb.empty?
101
+ print sb
102
+
103
+ if opts[ :prepended ]
104
+ print "#\n" unless sb.empty?
105
+ git "status", :show => true
106
+ end
107
+ end
108
+
109
+ # Switch to a review branch to check somebody else's code.
110
+ def review( spec=nil, opts={} )
111
+ rb = remote_branches_organized
112
+ review_branches = rb[:review]
113
+
114
+ if spec.nil?
115
+ # select the oldest (by HEAD) topic, if any exist
116
+ if review_branches.empty?
117
+ puts "nothing to review."
118
+ return
119
+ end
120
+
121
+ user, topic = oldest_review_user_topic
122
+ else
123
+ user, topic = spec.split( '/' )
124
+ end
125
+
126
+ if remote_topic_branch = find_remote_review_branch( topic )
127
+ git [
128
+ switch_to_branch(
129
+ review_branch( topic, user ),
130
+ remote_topic_branch )]
131
+ else
132
+ raise "No review topic found matching ‘#{spec}’"
133
+ end
134
+ end
135
+
136
+ # Accept the branch currently being reviewed.
137
+ def accept( topic=nil, opts={} )
138
+ raise "Must be on a review branch." unless on_review_branch?
139
+
140
+ # switch to master
141
+ # merge review branch, assuming FF
142
+ # push master, destroy remote
143
+ # destroy local
144
+ user, topic = user_topic_name( current_branch )
145
+
146
+ local_review_branch = current_branch
147
+ ff_merge = git [
148
+ "checkout master",
149
+ "merge --ff-only #{local_review_branch}",
150
+ ]
151
+
152
+ unless ff_merge
153
+ git "checkout #{local_review_branch}"
154
+ raise "
155
+ review branch is not up to date: merge not a fast-forward. Either
156
+ rebase or reject this branch.
157
+ ".cleanup
158
+ end
159
+
160
+ rem_review_branch = find_remote_review_branch( topic ).gsub( %r{^origin/}, '' )
161
+ git [
162
+ "push origin :#{rem_review_branch}",
163
+ "branch -d #{local_review_branch}"
164
+ ]
165
+ end
166
+
167
+ # Reject the branch currently being reviewed.
168
+ def reject( topic=nil, opts={} )
169
+ raise "Must be on a review branch." unless on_review_branch?
170
+
171
+ # switch to master
172
+ # push to rejected, destroy remote
173
+ # destroy local
174
+ user, topic = user_topic_name( current_branch )
175
+
176
+ rem_review_branch = find_remote_review_branch( topic ).gsub( %r{^origin/}, '' )
177
+ rem_rej_branch = remote_rejected_branch( topic, user )
178
+ git [
179
+ "checkout master",
180
+ "push origin #{current_branch}:#{rem_rej_branch} :#{rem_review_branch}",
181
+ "branch -D #{current_branch}"
182
+ ]
183
+ end
184
+
185
+ def install_aliases( opts={} )
186
+ opts.assert_valid_keys :local, :local_given, *GlobalOptKeys
187
+
188
+ flags = "--global" unless opts[:local]
189
+
190
+ git [
191
+ "config #{flags} alias.work-on 'topic work-on'",
192
+ "config #{flags} alias.done 'topic done'",
193
+ "config #{flags} alias.review 'topic review'",
194
+ "config #{flags} alias.accept 'topic accept'",
195
+ "config #{flags} alias.reject 'topic reject'",
196
+
197
+ "config #{flags} alias.w 'topic work-on'",
198
+ "config #{flags} alias.r 'topic review'",
199
+ "config #{flags} alias.st 'topic status --prepended'",
200
+ ]
201
+ end
202
+
203
+
204
+ private
205
+
206
+
207
+ def backup_branch( topic )
208
+ "backup/#{user}/#{topic}"
209
+ end
210
+
211
+ def wip_branch( topic )
212
+ "wip/#{user}/#{topic}"
213
+ end
214
+
215
+ def rejected_branch( topic )
216
+ "rejected/#{user}/#{topic}"
217
+ end
218
+
219
+ def review_branch( topic, user=user )
220
+ "review/#{user}/#{topic}"
221
+ end
222
+
223
+ def remote_rejected_branch( topic, user=user )
224
+ "rejected/#{user}/#{topic}"
225
+ end
226
+
227
+
228
+ def find_remote_review_branch( topic )
229
+ others_review_branches.find{|b| b.index topic}
230
+ end
231
+
232
+
233
+ def user_topic_name( branch )
234
+ if branch =~ %r{^origin}
235
+ branch =~ %r{^\S*?/\S*?/(\S*?)/(\S*)}
236
+ [$1, $2]
237
+ else
238
+ branch =~ %r{^\S*?/(\S*?)/(\S*)}
239
+ [$1, $2]
240
+ end
241
+ end
242
+
243
+
244
+ def user
245
+ @@user ||= (ENV['USER'] || `whoami`)
246
+ end
247
+
248
+ def current_topic
249
+ current_branch =~ %r{wip/\S*?/(\S*)}
250
+ $1
251
+ end
252
+
253
+ def current_branch
254
+ @@current_branch ||= capture_git( "branch --no-color" ).split( "\n" ).find do |b|
255
+ b =~ %r{^\*}
256
+ end[ 2..-1 ]
257
+ end
258
+
259
+ def branches
260
+ @@branches ||= capture_git( "branch --no-color" ).split( "\n" ).map{|b| b[2..-1]}
261
+ end
262
+
263
+ def remote_branches
264
+ @@remote_branches ||= capture_git( "branch -r --no-color" ).split( "\n" ).map{|b| b[2..-1]}
265
+ end
266
+
267
+ def others_review_branches
268
+ remote_branches.select do
269
+ |b| b =~ %r{/review/}
270
+ end.reject do |b|
271
+ b =~ %r{/#{user}/}
272
+ end
273
+ end
274
+
275
+ def remote_branches_organized
276
+ @@remote_branches_organized ||= (
277
+ rb = remote_branches.dup
278
+ # Convert a bunch of remote branch names, like
279
+ # origin/HEAD -> origin/masterr
280
+ # origin/master
281
+ # origin/review/user1/topic1
282
+ # origin/something-else
283
+ # origin/rejected/user2/topic2
284
+ #
285
+ # Into a hash with keys 'review' and 'rejected' pointing to hashes of
286
+ # user-topic(s) pairs.
287
+ rb.map!{|s| s.gsub( /->.*/, '')}
288
+ rb.map!{|s| s.strip.split( '/' )}
289
+ namespace_ut = rb.group_by{|remote, namespace, user, topic| namespace if topic}
290
+ namespace_ut.reject!{|k,v| not %w(rejected review).include? k}
291
+
292
+ namespace_ut.each do |k,v|
293
+ v.each{|a| a.shift( 2 )}
294
+ v = namespace_ut[k] = v.group_by{|user, topic| user if topic}
295
+ v.each{|kk,vv| vv.each(&:shift); vv.flatten!}
296
+ end
297
+
298
+ namespace_ut.symbolize_keys!
299
+ namespace_ut[:review] ||= {}
300
+ namespace_ut[:rejected] ||= {}
301
+
302
+ namespace_ut[:review].reject!{|k,v| k == user}
303
+ namespace_ut
304
+ )
305
+ end
306
+
307
+ def oldest_review_branch
308
+ return nil if others_review_branches.empty?
309
+
310
+ commits_by_age = capture_git([
311
+ "log --date-order --reverse --pretty=format:%d",
312
+ "^origin/master #{others_review_branches.join( ' ' )}",
313
+ ].join( " " )).split( "\n" )
314
+
315
+ commits_by_age.find do |ref|
316
+ # no ‘,’, i.e. only one ref matches the commit
317
+ ref.index( ',' ).nil?
318
+ end.strip[ 1..-2 ] # chomp the leading and trailing parenthesis
319
+ end
320
+
321
+ def oldest_review_user_topic
322
+ user_topic_name( oldest_review_branch )
323
+ end
324
+
325
+ def on_review_branch?
326
+ current_branch =~ %r{^review/}
327
+ end
328
+
329
+ def working_tree_clean?
330
+ git [ "diff --quiet", "diff --quiet --cached" ]
331
+ $?.success?
332
+ end
333
+
334
+ def working_tree_dirty?
335
+ not working_tree_clean?
336
+ end
337
+
338
+
339
+ def display_git_output?
340
+ @@display_git_output ||= false
341
+ end
342
+
343
+ def display_git_output!
344
+ @@display_git_output = true
345
+ end
346
+
347
+
348
+ def switch_to_branch( branch, tracking=nil )
349
+ if branches.include?( branch )
350
+ "checkout #{branch}"
351
+ else
352
+ "checkout -b #{branch} #{tracking}"
353
+ end
354
+ end
355
+
356
+ def cmd_redirect_suffix( opts )
357
+ if !opts[:show] && !display_git_output?
358
+ "> /dev/null 2> /dev/null"
359
+ end
360
+ end
361
+
362
+ def git( cmds=[], opts={} )
363
+ cmds = [cmds] if cmds.is_a? String
364
+ redir = cmd_redirect_suffix( opts )
365
+ system cmds.map{|c| "git #{c} #{redir}"}.join( " && " )
366
+ end
367
+
368
+ def capture_git( cmds=[] )
369
+ cmds = [cmds] if cmds.is_a? String
370
+ redir = "2> /dev/null" unless display_git_output?
371
+ `#{cmds.map{|c| "git #{c} #{redir}"}.join( " && " )}`
372
+ end
373
+ end
374
+ end
375
+
data/lib/util.rb ADDED
@@ -0,0 +1,34 @@
1
+ # encoding: utf-8
2
+
3
+ require 'active_support'
4
+ require 'active_support/inflector'
5
+ require 'active_support/core_ext/module/aliasing'
6
+ require 'active_support/core_ext/string/inflections'
7
+
8
+
9
+ class String
10
+ def cleanup
11
+ indent = (index /^([ \t]+)/; $1) || ''
12
+ regex = /^#{Regexp::escape( indent )}/
13
+ strip.gsub regex, ''
14
+ end
15
+
16
+ def oneline
17
+ strip.gsub( /\n\s+/, '' )
18
+ end
19
+
20
+ # Annoyingly, the useful version of pluralize in texthelpers isn't in the
21
+ # string core extensions.
22
+ def pluralize_with_count( count )
23
+ count > 1 ? pluralize_without_count : singularize
24
+ end
25
+ alias_method_chain :pluralize, :count
26
+ end
27
+
28
+
29
+ class Object
30
+ # Deep duplicate via remarshaling. Not always applicable.
31
+ def ddup
32
+ Marshal.load( Marshal.dump( self ))
33
+ end
34
+ end