Module: GitTopic

Includes:
Comment, Git, Naming
Defined in:
lib/git_topic.rb,
lib/git_topic/cli.rb,
lib/git_topic/git.rb,
lib/git_topic/logger.rb,
lib/git_topic/naming.rb,
lib/git_topic/comment.rb

Defined Under Namespace

Modules: Comment, Git, Logger, Naming

Constant Summary collapse

GlobalOptKeys =
[
  :verbose, :help, :verbose_given, :version, :completion_help,
  :completion_help_given, :no_log
]
SubCommands =
%w(
  work-on done abandon status review comment comments accept reject
  install-aliases
)
Version =
lambda {
  h = YAML::load_file( "#{File.dirname( __FILE__ )}/../../VERSION.yml" )
  if h.is_a? Hash
    [h[:major], h[:minor], h[:patch], h[:build]].compact.join( "." )
  end
}.call

Class Attribute Summary collapse

Class Method Summary collapse

Methods included from Comment

included

Methods included from Naming

included

Methods included from Git

included

Class Attribute Details

.global_optsObject

Returns the value of attribute global_opts.



3
4
5
# File 'lib/git_topic/git.rb', line 3

def global_opts
  @global_opts
end

Class Method Details

.abandon(topic = nil, opts = {}) ⇒ Object

Delete topic locally and remotely. Defaults to current topic if unspecified.



78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
# File 'lib/git_topic.rb', line 78

def abandon topic=nil, opts={}
  local_branch =
    if topic.nil?
      parts = topic_parts current_branch
      if parts && parts[:namespace] == "wip" && parts[:user] == user
        topic = current_topic
        current_branch
      else
        raise "Cannot abandon #{current_branch}."
      end
    else
      wip_branch  topic
    end

  unless rb = remote_branch( topic, :strip_remote => true )
    raise "No such topic #{topic}."
  end

  git [
    ( "checkout master"           if current_branch == local_branch ),
    ( "branch -D #{local_branch}" if branches.include? local_branch ),
    "push origin :#{rb}",
  ].compact

  report "Topic #{topic} abandoned."
end

.accept(topic = nil, opts = {}) ⇒ Object

Accept the branch currently being reviewed.



252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
# File 'lib/git_topic.rb', line 252

def accept  topic=nil, opts={}
  raise "Must be on a review branch." unless on_review_branch?
  raise "Working tree must be clean" unless working_tree_clean?
  
  # switch to master
  # merge review branch, assuming FF
  # push master, destroy remote
  # destroy local
  user, topic           = user_topic_name( current_branch )

  local_review_branch   = current_branch
  ff_merge = git [
      "checkout master",
      "merge --ff-only #{local_review_branch}",
  ]

  unless ff_merge
    git "checkout #{local_review_branch}"
    raise "
      review branch is not up to date: merge not a fast-forward.  Either
      rebase or reject this branch.
    ".unindent
  end

  rem_review_branch   = find_remote_review_branch( topic ).gsub( %r{^origin/}, '' )
  git [
    "push origin master :refs/heads/#{rem_review_branch}",
    "branch -d #{local_review_branch}"
  ]

  report  "Accepted topic #{user}/#{topic}."
end

.comment(opts = {}) ⇒ Object



286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
# File 'lib/git_topic.rb', line 286

def comment opts={}
  diff_legal = 
    git( "diff --diff-filter=ACDRTUXB --quiet" )          && 
    git( "diff --cached --diff-filter=ACDRTUXB --quiet" )

  raise "
    Diffs are not comments.  Files have been added, deleted or had their
    modes changed.  See
      git diff --diff-filter=ACDRTUXB
    for a list of changes preventing git-topic comment from saving your
    comments.
  ".unindent unless diff_legal


  diff_empty          = git( "diff --diff-filter=M --quiet" )

  added_comments = 
    case current_namespace
    when "wip"
      if existing_comments?
        raise "
          diff → comments not allowed when replying.  Please make sure your
          working tree is completely clean and then invoke git-topic comment
          again.
        ".oneline unless diff_empty

        notes_from_reply_to_comments
      else
        puts "No comments to reply to.  See git-topic comment --help for usage."
        return
      end
    when "review"
      if existing_comments?
        if opts[ :force_update ]
          notes_from_initial_comments( "edit" )
        else
          raise "
            diff → comments not allowed when replying.  Please make sure your
            working tree is completely clean and then invoke git-topic comment
            again.
          ".oneline unless diff_empty

          notes_from_reply_to_comments
        end
      else
        notes_from_initial_comments
      end
    else
      raise "Inappropriate namespace for comments: [#{namespace}]"
    end

  if added_comments
    report "Your comments have been saved."
  else
    report "You did not write any comments.  Nothing to save."
  end
end

.comments(spec = nil, opts = {}) ⇒ Object



344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
# File 'lib/git_topic.rb', line 344

def comments  spec=nil, opts={}
  args = [ spec ].compact
  if args.empty? && current_branch.nil?
    if guess = guess_branch
      args << guess_branch

      puts "
        You are not on a branch and no topic branch was specified.  Using
        alternate name for HEAD, #{guess}.
      ".oneline
    else
      puts "
        You are not on a branch and no topic branch was specified.  I could
        not find an appropriate name for HEAD to guess.
      "
      return
    end
  end

  unless existing_comments? *args
    puts "There are no comments on this branch."
    return
  end

  range = "origin/master..#{remote_branch *args}"
  git "log #{range} --show-notes=#{notes_ref *args} --no-standard-notes",
      :show => true
end

.done(topic = nil, opts = {}) ⇒ Object

Done with the given topic. If none is specified, then topic is assumed to be the current branch (if it’s a topic branch).



107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
# File 'lib/git_topic.rb', line 107

def done  topic=nil, opts={}
  if topic.nil?
    raise "
      Current branch is not a topic branch.  Switch to a topic branch or
      supply an argument.
    ".oneline if current_topic.nil?

    topic = current_topic
  else
    raise "
      Specified topic #{topic} does not refer to a topic branch.
    " unless branches.include? wip_branch( topic )
  end
  raise "Working tree must be clean" unless working_tree_clean?


  wb = wip_branch( topic )
  rb = review_branch( topic )
  refspecs = [
    "refs/heads/#{wb}:refs/heads/#{rb}",
    ":refs/heads/#{wb}",
    "refs/notes/reviews/*:refs/notes/reviews/*"
  ].join( " " )
  git [
    "push origin #{refspecs}",
    ("checkout master" if strip_namespace( topic ) == current_topic),
    "branch -D #{wip_branch( topic )}"
  ].compact

  report "Completed topic #{topic}.  It has been pushed for review."
end

.install_aliases(opts = {}) ⇒ Object



435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
# File 'lib/git_topic.rb', line 435

def install_aliases opts={}
  opts.assert_valid_keys  :local, :local_given, *GlobalOptKeys

  flags = "--global" unless opts[:local]

  git [
    "config #{flags} alias.work-on  'topic work-on'",
    "config #{flags} alias.done     'topic done'",
    "config #{flags} alias.review   'topic review'",
    "config #{flags} alias.accept   'topic accept'",
    "config #{flags} alias.reject   'topic reject'",
    "config #{flags} alias.comment  'topic comment'",
    "config #{flags} alias.comments 'topic comments'",
  ]

  report  "Aliases installed Successfully.",
          "
            Error installing aliases.  re-run with --verbose flag for
            details.
          ".oneline
end

.reject(topic_or_opts = nil, opts = {}) ⇒ Object

Reject the branch currently being reviewed.



375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
# File 'lib/git_topic.rb', line 375

def reject  topic_or_opts=nil, opts={}
  if topic_or_opts.is_a? Hash
    topic = nil
    opts = topic_or_opts
  else
    topic = topic_or_opts
  end

  raise "Must be on a review branch." unless on_review_branch?
  unless working_tree_clean?
    if opts[:save_comments]
      comment
    else
      raise "Working tree must be clean without --save-comments."
    end
  end

  # switch to master
  # push to rejected, destroy remote
  # destroy local
  user, topic = user_topic_name( current_branch )

  rem_review_branch   = find_remote_review_branch( topic ).gsub( %r{^origin/}, '' )
  rem_rej_branch      = remote_rejected_branch( topic, user )

  refspecs = [
    "refs/heads/#{current_branch}:refs/heads/#{rem_rej_branch}",
    ":refs/heads/#{rem_review_branch}",
    "refs/notes/reviews/*:refs/notes/reviews/*"
  ].join( " " )
  git [
    "checkout master",
    "push origin #{refspecs}",
    "branch -D #{current_branch}"
  ]

  report  "Rejected topic #{user}/#{topic}"
end

.review(ref = nil, opts = {}) ⇒ Object

Switch to a review branch to check somebody else’s code.



203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
# File 'lib/git_topic.rb', line 203

def review  ref=nil, opts={}
  rb = remote_branches_organized
  review_branches = rb[:review]

  if ref.nil?
    # select the oldest (by HEAD) topic, if any exist
    if review_branches.empty?
      puts "nothing to review."
      return
    else
      user, topic = oldest_review_user_topic
    end
  else
    p             = topic_parts( ref )
    user, topic   = p[:user], p[:topic]
  end

  if remote_topic_branch = find_remote_review_branch( topic )
    # Get the actual user/topic, e.g. to get the user if ref only specifies
    # the topic.
    real_user, real_topic = user_topic_name( remote_topic_branch )
    git [
      switch_to_branch(
        review_branch( real_topic, real_user ),
        remote_topic_branch )]
  else
    raise "No review topic found matching ‘#{ref}"
  end
  report  "Reviewing topic #{user}/#{topic}."

  unless rebased_to_master?
    git "rebase --quiet master"
    report(
      "
        #{user}/#{topic} was not rebased to master, but has successfully
        been automatically rebased.
      ".unindent,
      "
        #{user}/#{topic} was not rebased to master, and I could not
        automatically rebase it.  You will not be able to accept this topic.
        Either
          1) rebase the topic manually or
          2) reject the topic and ask the author to rebase.
      ".unindent )
  end
end

.runObject



25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
# File 'lib/git_topic/cli.rb', line 25

def run
  global_opts = self.global_opts = Trollop::options do
    banner "
      git-topic #{Version}
      Manage a topic/review workflow.

      see <http://github.com/hjdivad/git-topic>

      Commands are:
        #{SubCommands.join( "
        " )}

        Global Options:
    ".unindent
    version Version

    opt :verbose,   
        "Verbose output, including complete traces on errors."
    opt :completion_help,
        "View instructions for setting up autocompletion."
    opt :no_log,
        "Disable logging."

    stop_on         SubCommands
  end


  if global_opts[:completion_help]
    root = File.expand_path( "#{File.dirname( __FILE__ )}/../.." )
    completion_bash_file  = "#{root}/share/completion.bash"
    puts %Q{
      To make use of bash autocompletion, you must do the following:

        1.  Make sure you source
              #{completion_bash_file}
            before you source git's completion.

        2.  Optionally, install git-topic with the --no-wrappers option.
            This is to sidestep ruby issue 3465 which makes loading gems
            through the generated wrapper far too slow for autocompletion.
            For more information, see:

              http://redmine.ruby-lang.org/issues/show/3465
    }.unindent
    exit 0
  end

  info      ''
  info      ARGV.join( " " )
  cmd       = ARGV.shift
  cmd_opts  = Trollop::options do
    case cmd
    when "work-on"
      banner "
        git[-topic] work-on <topic> [<upstream> | --continue]

        Switches to a local work-in-progress (wip) branch for <topic>.  The
        branch (and a matching remote branch) is created if necessary.

        If this is a rejected topic, work will continue from the state of
        the rejected topic branch.  Similarly, if this is a review topic,
        the review will be pulled and work will continue on that topic.

        <topic>'s branch's HEAD will point to <upstream>, if supplied.  If
        --continue is supplied instead, HEAD will point to the most recent
        review (i.e. submitted) of your topic branches.  If you have just
        submitted a topic with git done, git work-on next-topic --continue
        would begin the next topic starting from where you had left off.
       
        If both <upstream> and --continue are omitted, <topic>'s branch's
        HEAD will default to the current HEAD.

        Options:
      ".unindent

      opt   :continue,
            "Use latest review branch as <upstream>",
            :default => false
    when "abandon"
      banner "
        git[-topic] abandon [<topic>]

        Deletes <topic> locally and remotely.  Defaults to current topic if unspecified.
      ".unindent
    when /done(-with)?/
      banner "
        git[-topic] done

        Indicate that this topic branch is ready for review.  Push to a
        remote review branch and switch back to master.

        Options:
      ".unindent
    when "status"
      banner "
        git st
        git-topic status

        Print a status, showing rejected branches to work on and branches
        that can be reviewed.

        Options:
      ".unindent
      opt   :prepended,
            "
              Prepend status to git status output (for a complete view of
              status).
            ".oneline,
            :default => false
    when "review"
      banner "
        git[-topic] review [<topic>]

        Review <topic>.  If <topic> is unspecified, review the oldest (by HEAD) topic.

        Options:
      ".unindent
    when "comment"
      banner "
        git[-topic] comment

        Add your comments to the current topic.  If this is the first time
        you are reviwing <topic> you can set initial comments (see
        INITIAL_COMMENTS below).  Otherwise, your GIT_EDITOR will open to
        let you enter your replies to the comments.

        Similarly, if you are working on a rejected branch, git-topic
        comment will open your GIT_EDITOR so you can reply to the reviewer's
        comments.
       
        INITIAL_COMMENTS

        For the initial set of comments, you can edit the files in your
        working tree to include any file specific comments.  Simply ensure
        that all such comments are prefixed with a ‘#’.  git-topic comment
        will convert your changes to a list of file-specific comments.

        In order to use this feature, there are several requirements about
        the output of git diff.

        1.  It must only have file modifications.  i.e., no deletions,
            additions or mode changes.

        2.  Those modifications must only have line additions.  i.e. no line
            deletions.

        3.  Those line additions must all begin with any amount of
            whitespace followed by a ‘#’ character.  i.e. they should be
            comments.

        Options:
      ".unindent

      opt   :force_update,
            "
              If you are commenting on the initial review and you wish to
              edit your comments, you can pass this flag to do so.
            ".oneline
    when "comments"
      banner "
        git[-topic] comments [<topic>]

        View the comments for <topic>, which defaults to the current topic.
        If your branch was rejected, you should read these comments so you
        know what to do to appease the reviewer.

        Options:
      ".unindent
    when "accept"
      banner "
        git[-topic] accept

        Accept the current in-review topic, merging it to master and
        cleaning up the remote branch.  This will fail if the branch does
        not merge as a fast-forward in master.  If that happens, the topic
        should either be rejected, or you can manually rebase.

        Options:
      ".unindent
    when "reject"
      banner "
        git[-topic] reject

        Reject the current in-review topic.

        Options:
      ".unindent

      opt   :save_comments,
            "
              If the current diff includes your comments (see git-topic
              comment --help), this flag will autosave those comments before
              rejecting the branch.
            ".oneline
    when "install-aliases"
      banner "
        git-topic install-aliases

        Install aliases to make git topic nicer to work with.  The aliases are as follows:

        w[ork-on]   topic work-on
        done        topic done
        r[eview]    topic review
        accept      topic accept
        reject      topic reject

        st          topic status --prepended

        Options:
      ".unindent

      opt   :local,
            "
              Install aliases non-globally (i.e. in .git/config instead of
              $HOME/.gitconfig
            ".oneline,
            :default => false
    end
  end

  check_for_setup unless cmd == "setup"

  opts = global_opts.merge( cmd_opts )
  display_git_output! if opts[:verbose]

  case cmd
  when "work-on"
    topic             = ARGV.shift
    upstream          = ARGV.shift
    opts.merge!({
      :upstream       => upstream
    })
    work_on           topic, opts
  when "abandon"
    topic             = ARGV.shift
    abandon           topic
  when /done(-with)?/
    topic             = ARGV.shift
    done              topic, opts
  when "status"
    status            opts
  when "review"
    spec              = ARGV.shift
    review            spec, opts
  when "comment"
    comment           opts
  when "comments"
    spec              = ARGV.shift
    comments          spec, opts
  when "accept"
    topic             = ARGV.shift
    accept            topic, opts
  when "reject"
    topic             = ARGV.shift
    reject            topic, opts
  when "install-aliases"
    install_aliases   opts
  when "setup"
    setup             opts
  end
rescue => error
  puts "Error: #{error.message}"
  puts error.backtrace.join( "\n" ) if opts[:verbose]
end

.setup(opts = {}) ⇒ Object

Setup .git/config.

This means setting up:

1. refspecs for origin fetching for review comments.
2. notes.rewriteRef for copying review comments on rebase.


421
422
423
424
425
426
427
428
429
430
431
432
433
# File 'lib/git_topic.rb', line 421

def setup opts={}
  cmds = []

  cmds <<(
    "config --add remote.origin.fetch +refs/notes/reviews/*:refs/notes/reviews/*"
  ) unless has_setup_refspec?

  cmds <<(
    "config --add notes.rewriteRef refs/notes/reviews/*"
  ) unless has_setup_notes_rewrite?

  git cmds.compact
end

.status(opts = {}) ⇒ Object

Produce status like

# There are 2 topics you can review.
#
# from davidjh:
#   zombies
#   pirates
# from king-julian:
#   fish
#   whales
#
# 2 of your topics were rejected.
#   dragons
#   liches


154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
# File 'lib/git_topic.rb', line 154

def status  opts={}
  opts.assert_valid_keys  :prepended, :prepended_given, *GlobalOptKeys

  sb = ''
  rb = remote_branches_organized
  # Only others' branche should appear as ‘topics you can review’
  review_ut   = rb[:review].select{ |u, t| u != user }
  rejected_ut = rb[:rejected]

  unless review_ut.empty?
    topic_count = review_ut.inject( 0 ){ |a, uts| a + uts.size }
    prep = topic_count == 1 ? "is 1" : "are #{topic_count}"
    sb << "# There #{prep} #{'topic'.pluralize( topic_count )} you can review.\n\n"

    sb << review_ut.map do |user, topics|
      sb2 = "  from #{user}:\n"
      sb2 << topics.map do |t|
        age = ref_age( review_branch( t, user, :remote => true ))
        age_str = " (#{age} days)" if age && age > 0
        "    #{t}#{age_str}"
      end.join( "\n" )
      sb2
    end.join( "\n" )
  end

  rejected_topics = (rejected_ut[ user ] || []).dup
  rejected_topics.map! do |topic|
    suffix = " (reviewer comments) "
    "#{topic}#{suffix if existing_comments?( "#{user}/#{topic}" )}"
  end
  unless rejected_topics.empty?
    sb << "\n" unless review_ut.empty?
    verb = rejected_topics.size  == 1 ? 'is' : 'are'
    sb << "\n#{rejected_topics.size} of your topics #{verb} rejected.\n    "
    sb << rejected_topics.join( "\n    " )
  end

  sb.gsub! "\n", "\n# "
  sb << "\n" unless sb.empty?
  print sb

  if opts[ :prepended ]
    print "#\n" unless sb.empty?
    git "status", :show => true
  end
end

.work_on(topic, opts = {}) ⇒ Object

Switch to a branch for the given topic.



30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# File 'lib/git_topic.rb', line 30

def work_on topic, opts={}
  opts.assert_valid_keys  :continue, :continue_given, :upstream, *GlobalOptKeys
  raise "Topic must be specified" if topic.nil?

  upstream =
    if opts[:upstream] && opts[:continue]
      raise "upstream and continue options mutually exclusive."
    elsif opts[:upstream]
      opts[:upstream]
    elsif opts[:continue]
      newest_pending_branch
    end


  # setup a remote branch, if necessary
  wb = wip_branch( topic )
  git(
    "push origin HEAD:refs/heads/#{wb}"
  ) unless remote_branches.include? "origin/#{wb}"
  # switch to the new branch
  git [ switch_to_branch( wb, "origin/#{wb}" )]
  

  # Check for rejected or review branch
  [ rejected_branch( topic ), review_branch( topic ) ].each do |b|
    if remote_branches.include? "origin/#{b}"
      git [
        "reset --hard origin/#{b}",
        "push origin :refs/heads/#{b} +HEAD:refs/heads/#{wb}",
      ]
    end
  end

  # Reset upstream, if specified
  if upstream
    git "reset --hard #{upstream}"
  end

  report "Switching branches to work on #{topic}."
  if existing_comments?
    report "You have reviewer comments on this topic."
  end
end