twig 1.6 → 1.7

Sign up to get free protection for your applications and to get access to all the features.
data/lib/twig/branch.rb CHANGED
@@ -2,14 +2,14 @@ class Twig
2
2
 
3
3
  # Represents a Git branch.
4
4
  class Branch
5
-
6
- EMPTY_PROPERTY_NAME_ERROR = 'Branch property names cannot be empty strings.'
5
+ PARENT_PROPERTY = 'diff-branch'
7
6
  PROPERTY_NAME_FROM_GIT_CONFIG = /^branch\.[^.]+\.([^=]+)=.*$/
8
7
  RESERVED_BRANCH_PROPERTY_NAMES = %w[branch merge property rebase remote]
9
8
 
10
9
  class EmptyPropertyNameError < ArgumentError
10
+ DEFAULT_MESSAGE = 'Branch property names cannot be empty strings.'
11
11
  def initialize(message = nil)
12
- message ||= EMPTY_PROPERTY_NAME_ERROR
12
+ message ||= DEFAULT_MESSAGE
13
13
  super
14
14
  end
15
15
  end
@@ -24,9 +24,9 @@ class Twig
24
24
  split("\n")
25
25
 
26
26
  branch_tuples.inject([]) do |result, branch_tuple|
27
- name, time_string, time_ago = branch_tuple.split(REF_FORMAT_SEPARATOR)
27
+ name, time_string = branch_tuple.split(REF_FORMAT_SEPARATOR)
28
28
  time = Time.parse(time_string)
29
- commit_time = Twig::CommitTime.new(time, time_ago)
29
+ commit_time = Twig::CommitTime.new(time)
30
30
  branch = Branch.new(name, :last_commit_time => commit_time)
31
31
  result << branch
32
32
  end
@@ -34,7 +34,7 @@ class Twig
34
34
  end
35
35
 
36
36
  def self.all_branch_names
37
- @_all_branch_names ||= self.all_branches.map { |branch| branch.name }
37
+ @_all_branch_names ||= all_branches.map { |branch| branch.name }
38
38
  end
39
39
 
40
40
  def self.all_property_names
@@ -59,6 +59,10 @@ class Twig
59
59
  raise EmptyPropertyNameError if property_name.empty?
60
60
  end
61
61
 
62
+ def self.shellescape_property_value(property_value)
63
+ Shellwords.escape(property_value).gsub('\\ ', ' ')
64
+ end
65
+
62
66
  def initialize(name, attrs = {})
63
67
  self.name = name
64
68
  raise ArgumentError, '`name` is required' if name.empty?
@@ -76,6 +80,10 @@ class Twig
76
80
  }
77
81
  end
78
82
 
83
+ def parent_name
84
+ get_property(PARENT_PROPERTY)
85
+ end
86
+
79
87
  def sanitize_property(property_name)
80
88
  property_name.gsub(/[ _]+/, '')
81
89
  end
@@ -92,7 +100,7 @@ class Twig
92
100
  return {} if property_names.empty?
93
101
 
94
102
  property_names_regexp = escaped_property_names(property_names).join('|')
95
- git_config_regexp = "branch\.#{name}\.(#{ property_names_regexp })$"
103
+ git_config_regexp = "branch\.#{Shellwords.escape(name)}\.(#{ property_names_regexp })$"
96
104
  cmd = %{git config --get-regexp "#{git_config_regexp}"}
97
105
 
98
106
  git_result = Twig.run(cmd) || ''
@@ -114,7 +122,6 @@ class Twig
114
122
  properties.merge(property_name => property_value)
115
123
  end
116
124
  end
117
-
118
125
  end
119
126
 
120
127
  def get_property(property_name)
@@ -134,8 +141,9 @@ class Twig
134
141
  raise ArgumentError,
135
142
  %{Can't set a branch property to an empty string.}
136
143
  else
137
- git_config = "branch.#{name}.#{property_name}"
138
- Twig.run(%{git config #{git_config} "#{value}"})
144
+ git_config = "branch.#{name.shellescape}.#{property_name}"
145
+ escaped_value = Branch.shellescape_property_value(value)
146
+ Twig.run(%{git config #{git_config} "#{escaped_value}"})
139
147
  result_body = %{property "#{property_name}" as "#{value}" for branch "#{name}".}
140
148
  if $?.success?
141
149
  "Saved #{result_body}"
@@ -152,7 +160,7 @@ class Twig
152
160
  value = get_property(property_name)
153
161
 
154
162
  if value
155
- git_config = "branch.#{name}.#{property_name}"
163
+ git_config = "branch.#{name.shellescape}.#{property_name}"
156
164
  Twig.run(%{git config --unset #{git_config}})
157
165
  %{Removed property "#{property_name}" for branch "#{name}".}
158
166
  else
@@ -160,6 +168,5 @@ class Twig
160
168
  %{The branch "#{name}" does not have the property "#{property_name}".}
161
169
  end
162
170
  end
163
-
164
171
  end
165
172
  end
data/lib/twig/cli.rb CHANGED
@@ -1,17 +1,16 @@
1
1
  require 'optparse'
2
2
  require 'rbconfig'
3
+ require File.expand_path(File.join(File.dirname(__FILE__), 'cli', 'help'))
3
4
 
4
5
  class Twig
6
+ # Handles raw input from the command-line interface.
5
7
  module Cli
6
-
7
8
  def self.prompt_with_choices(prompt, choices)
8
9
  # Prints the given string `prompt` and the array `choices` numbered, and
9
10
  # prompts the user to enter a number. Returns the matching value, or nil
10
11
  # if the user input is invalid.
11
12
 
12
- if choices.size < 2
13
- raise ArgumentError, 'At least two choices required'
14
- end
13
+ raise ArgumentError, 'At least two choices required' if choices.size < 2
15
14
 
16
15
  puts prompt
17
16
  choices.each_with_index do |choice, index|
@@ -23,106 +22,11 @@ class Twig
23
22
  choices[input - 1]
24
23
  end
25
24
 
26
- def help_intro
27
- version_string = "Twig v#{Twig::VERSION}"
28
-
29
- intro = help_paragraph(%{
30
- Twig is your personal Git branch assistant. It's a command-line tool for
31
- listing your most recent branches, and for remembering branch details
32
- for you, like issue tracker ids and todos. It also supports subcommands,
33
- like automatically fetching statuses from your issue tracking system.
34
- })
35
-
36
- intro = <<-BANNER.gsub(/^[ ]+/, '')
37
-
38
- #{version_string}
39
- #{'-' * version_string.size}
40
-
41
- #{intro}
42
-
43
- #{Twig::HOMEPAGE}
44
- BANNER
45
-
46
- intro + ' ' # Force extra blank line
47
- end
48
-
49
- def help_separator(option_parser, text, options = {})
50
- options[:trailing] ||= "\n\n"
51
- option_parser.separator "\n#{text}#{options[:trailing]}"
52
- end
53
-
54
- def help_description(text, options = {})
55
- width = options[:width] || 40
56
- words = text.gsub(/\n?\s+/, ' ').strip.split(' ')
57
- lines = []
58
-
59
- # Split words into lines
60
- while words.any?
61
- current_word = words.shift
62
- current_word_size = unformat_string(current_word).size
63
- last_line = lines.last
64
- last_line_size = last_line && unformat_string(last_line).size
65
-
66
- if last_line_size && (last_line_size + current_word_size + 1 <= width)
67
- last_line << ' ' << current_word
68
- elsif current_word_size >= width
69
- lines << current_word[0...width]
70
- words.unshift(current_word[width..-1])
71
- else
72
- lines << current_word
73
- end
74
- end
75
-
76
- lines << ' ' if options[:add_separator]
77
- lines
78
- end
79
-
80
- def help_description_for_custom_property(option_parser, desc_lines, options = {})
81
- options[:trailing] ||= "\n"
82
- indent = ' '
83
- left_column_width = 29
84
-
85
- help_desc = desc_lines.inject('') do |desc, (left_column, right_column)|
86
- desc + indent +
87
- sprintf("%-#{left_column_width}s", left_column) + right_column + "\n"
88
- end
89
-
90
- help_separator(option_parser, help_desc, :trailing => options[:trailing])
91
- end
92
-
93
- def help_paragraph(text)
94
- help_description(text, :width => 80).join("\n")
95
- end
96
-
97
- def help_line_for_custom_property?(line)
98
- is_custom_property_except = (
99
- line.include?('--except-') &&
100
- !line.include?('--except-branch') &&
101
- !line.include?('--except-property') &&
102
- !line.include?('--except-PROPERTY')
103
- )
104
- is_custom_property_only = (
105
- line.include?('--only-') &&
106
- !line.include?('--only-branch') &&
107
- !line.include?('--only-property') &&
108
- !line.include?('--only-PROPERTY')
109
- )
110
- is_custom_property_width = (
111
- line =~ /--.+-width/ &&
112
- !line.include?('--branch-width') &&
113
- !line.include?('--PROPERTY-width')
114
- )
115
-
116
- is_custom_property_except ||
117
- is_custom_property_only ||
118
- is_custom_property_width
119
- end
120
-
121
25
  def run_pager
122
26
  # Starts a pager so that all following STDOUT output is paginated.
123
27
  # Based on: http://nex-3.com/posts/73-git-style-automatic-paging-in-ruby
124
28
 
125
- return if Twig::System.windows? || !$stdout.tty?
29
+ return if Twig::System.windows? || !$stdout.tty? || !Kernel.respond_to?(:fork)
126
30
 
127
31
  read_io, write_io = IO.pipe
128
32
 
@@ -152,28 +56,28 @@ class Twig
152
56
  custom_properties = Twig::Branch.all_property_names
153
57
 
154
58
  option_parser = OptionParser.new do |opts|
155
- opts.banner = help_intro
59
+ opts.banner = Help.intro
156
60
  opts.summary_indent = ' ' * 2
157
61
  opts.summary_width = 32
158
62
 
63
+ ###
159
64
 
160
-
161
- help_separator(opts, 'Common options:')
65
+ Help.header(opts, 'Common options')
162
66
 
163
67
  desc = 'Use a specific branch.'
164
68
  opts.on(
165
- '-b BRANCH', '--branch BRANCH', *help_description(desc)
69
+ '-b BRANCH', '--branch BRANCH', *Help.description(desc)
166
70
  ) do |branch|
167
71
  set_option(:branch, branch)
168
72
  end
169
73
 
170
74
  desc = 'Unset a branch property.'
171
- opts.on('--unset PROPERTY', *help_description(desc)) do |property_name|
75
+ opts.on('--unset PROPERTY', *Help.description(desc)) do |property_name|
172
76
  set_option(:unset_property, property_name)
173
77
  end
174
78
 
175
79
  desc = 'Show this help content.'
176
- opts.on('--help', *help_description(desc)) do
80
+ opts.on('--help', *Help.description(desc)) do
177
81
  summary_lines = opts.to_s.split("\n")
178
82
  run_pager
179
83
 
@@ -183,7 +87,7 @@ class Twig
183
87
  # Squash successive blank lines
184
88
  next if line == "\n" && prev_line == "\n"
185
89
 
186
- unless help_line_for_custom_property?(line)
90
+ unless Help.line_for_custom_property?(line)
187
91
  puts line
188
92
  prev_line = line
189
93
  end
@@ -193,17 +97,18 @@ class Twig
193
97
  end
194
98
 
195
99
  desc = 'Show Twig version.'
196
- opts.on('--version', *help_description(desc)) do
197
- puts Twig::VERSION; exit
100
+ opts.on('--version', *Help.description(desc)) do
101
+ puts Twig::VERSION
102
+ exit
198
103
  end
199
104
 
105
+ ###
200
106
 
201
-
202
- help_separator(opts, 'Filtering branches:')
107
+ Help.header(opts, 'Filtering branches')
203
108
 
204
109
  desc = 'Only list branches below a given age.'
205
110
  opts.on(
206
- '--max-days-old AGE', *help_description(desc, :add_separator => true)
111
+ '--max-days-old AGE', *Help.description(desc, :add_blank_line => true)
207
112
  ) do |age|
208
113
  set_option(:max_days_old, age)
209
114
  end
@@ -211,13 +116,13 @@ class Twig
211
116
  desc = 'Only list branches whose name matches a given pattern.'
212
117
  opts.on(
213
118
  '--only-branch PATTERN',
214
- *help_description(desc, :add_separator => true)
119
+ *Help.description(desc, :add_blank_line => true)
215
120
  ) do |pattern|
216
121
  set_option(:property_only, :branch => pattern)
217
122
  end
218
123
 
219
124
  desc = 'Do not list branches whose name matches a given pattern.'
220
- opts.on('--except-branch PATTERN', *help_description(desc)) do |pattern|
125
+ opts.on('--except-branch PATTERN', *Help.description(desc)) do |pattern|
221
126
  set_option(:property_except, :branch => pattern)
222
127
  end
223
128
 
@@ -232,43 +137,43 @@ class Twig
232
137
  set_option(:property_except, property_name_sym => pattern)
233
138
  end
234
139
  end
235
- help_description_for_custom_property(opts, [
140
+ Help.description_for_custom_property(opts, [
236
141
  ['--only-PROPERTY PATTERN', 'Only list branches with a given property'],
237
142
  ['', 'that matches a given pattern.']
238
143
  ], :trailing => '')
239
- help_description_for_custom_property(opts, [
144
+ Help.description_for_custom_property(opts, [
240
145
  ['--except-PROPERTY PATTERN', 'Do not list branches with a given property'],
241
146
  ['', 'that matches a given pattern.']
242
147
  ])
243
148
 
244
149
  desc =
245
- 'Print branch properties in a format that can be used by other ' +
150
+ 'Print branch properties in a format that can be used by other ' \
246
151
  'tools. Currently, the only supported value is `json`.'
247
152
  opts.on(
248
- '--format FORMAT', *help_description(desc, :add_separator => true)
153
+ '--format FORMAT', *Help.description(desc, :add_blank_line => true)
249
154
  ) do |format|
250
155
  set_option(:format, format)
251
156
  end
252
157
 
253
158
  desc =
254
- 'Lists all branches regardless of other filtering options. ' +
159
+ 'Lists all branches regardless of other filtering options. ' \
255
160
  'Useful for overriding options in ' +
256
161
  File.basename(Twig::Options::CONFIG_PATH) + '.'
257
- opts.on('--all', *help_description(desc)) do |pattern|
162
+ opts.on('--all', *Help.description(desc)) do |pattern|
258
163
  unset_option(:max_days_old)
259
164
  unset_option(:property_except)
260
165
  unset_option(:property_only)
261
166
  end
262
167
 
168
+ ###
263
169
 
264
-
265
- help_separator(opts, 'Listing branches:')
170
+ Help.header(opts, 'Listing branches')
266
171
 
267
172
  desc = <<-DESC
268
173
  Set the width for the `branch` column.
269
174
  (Default: #{Twig::DEFAULT_BRANCH_COLUMN_WIDTH})
270
175
  DESC
271
- opts.on('--branch-width NUMBER', *help_description(desc)) do |width|
176
+ opts.on('--branch-width NUMBER', *Help.description(desc)) do |width|
272
177
  set_option(:property_width, :branch => width)
273
178
  end
274
179
 
@@ -277,7 +182,7 @@ class Twig
277
182
  set_option(:property_width, property_name.to_sym => width)
278
183
  end
279
184
  end
280
- help_description_for_custom_property(opts, [
185
+ Help.description_for_custom_property(opts, [
281
186
  ['--PROPERTY-width NUMBER', "Set the width for a given property's column."],
282
187
  ['', "(Default: #{Twig::DEFAULT_PROPERTY_COLUMN_WIDTH})"]
283
188
  ])
@@ -288,7 +193,7 @@ class Twig
288
193
  DESC
289
194
  opts.on(
290
195
  '--only-property PATTERN',
291
- *help_description(desc, :add_separator => true)
196
+ *Help.description(desc, :add_blank_line => true)
292
197
  ) do |pattern|
293
198
  set_option(:property_only_name, pattern)
294
199
  end
@@ -299,17 +204,17 @@ class Twig
299
204
  DESC
300
205
  opts.on(
301
206
  '--except-property PATTERN',
302
- *help_description(desc, :add_separator => true)
207
+ *Help.description(desc, :add_blank_line => true)
303
208
  ) do |pattern|
304
209
  set_option(:property_except_name, pattern)
305
210
  end
306
211
 
307
- colors = Twig::Display::COLORS.keys.map do |value|
308
- format_string(value, :color => value)
309
- end.join(', ')
310
- weights = Twig::Display::WEIGHTS.keys.map do |value|
311
- format_string(value, :weight => value)
312
- end.join(' and ')
212
+ colors = Twig::Display::COLORS.keys.
213
+ map { |value| format_string(value, :color => value) }.
214
+ join(', ')
215
+ weights = Twig::Display::WEIGHTS.keys.
216
+ map { |value| format_string(value, :weight => value) }.
217
+ join(' and ')
313
218
  default_header_style = format_string(
314
219
  Twig::DEFAULT_HEADER_COLOR.to_s,
315
220
  :color => Twig::DEFAULT_HEADER_COLOR
@@ -321,19 +226,19 @@ class Twig
321
226
  DESC
322
227
  opts.on(
323
228
  '--header-style "STYLE"',
324
- *help_description(desc, :add_separator => true)
229
+ *Help.description(desc, :add_blank_line => true)
325
230
  ) do |style|
326
231
  set_option(:header_style, style)
327
232
  end
328
233
 
329
234
  desc = 'Show oldest branches first. (Default: false)'
330
- opts.on('--reverse', *help_description(desc)) do
235
+ opts.on('--reverse', *Help.description(desc)) do
331
236
  set_option(:reverse, true)
332
237
  end
333
238
 
239
+ ###
334
240
 
335
-
336
- help_separator(opts, 'GitHub integration:')
241
+ Help.header(opts, 'GitHub integration')
337
242
 
338
243
  desc = <<-DESC
339
244
  Set a custom GitHub API URI prefix, e.g.,
@@ -342,7 +247,7 @@ class Twig
342
247
  DESC
343
248
  opts.on(
344
249
  '--github-api-uri-prefix PREFIX',
345
- *help_description(desc, :add_separator => true)
250
+ *Help.description(desc, :add_blank_line => true)
346
251
  ) do |prefix|
347
252
  set_option(:github_api_uri_prefix, prefix)
348
253
  end
@@ -352,31 +257,78 @@ class Twig
352
257
  https://github-enterprise.example.com.
353
258
  (Default: "#{Twig::DEFAULT_GITHUB_URI_PREFIX}")
354
259
  DESC
355
- opts.on(
356
- '--github-uri-prefix PREFIX',
357
- *help_description(desc, :add_separator => true)
358
- ) do |prefix|
260
+ opts.on('--github-uri-prefix PREFIX', *Help.description(desc)) do |prefix|
359
261
  set_option(:github_uri_prefix, prefix)
360
262
  end
361
263
 
264
+ ###
265
+
266
+ Help.header(opts, 'Config files and tab completion', :trailing => '')
362
267
 
268
+ Help.print_paragraph(opts, %{
269
+ Twig can automatically set up a config file for you, where you can put
270
+ your most frequently used options for filtering and listing branches.
271
+ To get started, run `twig init` and follow the instructions. This does
272
+ two things:
273
+ })
363
274
 
364
- help_separator(opts, help_paragraph(%{
365
- You can put your most frequently used options for filtering and
366
- listing branches into #{Twig::Options::CONFIG_PATH}. For example:
367
- }), :trailing => '')
275
+ Help.print_paragraph(opts, %{
276
+ * Creates #{Twig::Options::CONFIG_PATH}, where you can put your
277
+ favorite options, e.g.:
278
+ })
368
279
 
369
- help_separator(opts, [
280
+ Help.print_section(opts, [
370
281
  ' except-branch: staging',
371
282
  ' header-style: green bold',
372
283
  ' max-days-old: 30',
373
284
  ' reverse: true'
285
+ ].join("\n"))
286
+
287
+ Help.print_paragraph(opts, %{
288
+ * Enables tab completion for Twig subcommands and branch names, e.g.:
289
+ })
290
+
291
+ Help.print_section(opts, [
292
+ ' `twig cre<tab>` -> `twig create-branch`',
293
+ ' `twig status -b my-br<tab>` -> `twig status -b my-branch`'
374
294
  ].join("\n"), :trailing => '')
375
295
 
376
- help_separator(opts, help_paragraph(%{
377
- To enable tab completion for Twig, run `twig init-completion` and
378
- follow the instructions.
379
- }))
296
+ ###
297
+
298
+ Help.header(opts, 'Subcommands', :trailing => '')
299
+
300
+ Help.print_paragraph(opts, "Twig comes with these subcommands:", :trailing => "\n\n")
301
+
302
+ Help.subcommand_descriptions.each do |desc|
303
+ Help.print_line(opts, desc)
304
+ end
305
+
306
+ Help.subheader(opts, 'Writing a subcommand', :trailing => '')
307
+
308
+ Help.print_paragraph(opts, %{
309
+ You can write any Twig subcommand that fits your own Git workflow. To
310
+ write a Twig subcommand:
311
+ })
312
+
313
+ Help.print_section(opts, [
314
+ '1. Write a script; any language will do. (If you want to take',
315
+ ' advantage of Twig\'s option parsing and branch processing, you\'ll',
316
+ ' need Ruby. See `twig-checkout-parent` for an example.)'
317
+ ].join("\n"))
318
+
319
+ Help.print_section(opts, [
320
+ '2. Save it with the `twig-` prefix in your `$PATH`,',
321
+ ' e.g., `~/bin/twig-my-subcommand`.'
322
+ ].join("\n"))
323
+
324
+ Help.print_section(opts, [
325
+ '3. Make it executable: `chmod ugo+x ~/bin/twig-my-subcommand`'
326
+ ].join("\n"))
327
+
328
+ Help.print_section(opts, [
329
+ '4. Run your subcommand: `twig my-subcommand` (with a *space* after',
330
+ ' `twig`'
331
+ ].join("\n"))
380
332
  end
381
333
 
382
334
  option_parser.parse!(args)
@@ -387,22 +339,12 @@ class Twig
387
339
  end
388
340
 
389
341
  def abort_for_option_exception(exception)
390
- puts exception.message + "\nFor a list of options, run `twig --help`."
342
+ puts exception.message + "\nFor a list of options, run `twig help`."
391
343
  exit 1
392
344
  end
393
345
 
394
- def exec_subcommand_if_any(args)
395
- # Run subcommand binary, if any, and exit here
396
- possible_subcommand_name = Twig::Subcommands::BIN_PREFIX + args[0]
397
- command_path = Twig.run("which #{possible_subcommand_name} 2>/dev/null")
398
- unless command_path.empty?
399
- command = ([command_path] + args[1..-1]).join(' ')
400
- exec(command)
401
- end
402
- end
403
-
404
346
  def read_cli_args!(args)
405
- exec_subcommand_if_any(args) if args.any?
347
+ Twig::Subcommands.exec_subcommand_if_any(args) if args.any?
406
348
 
407
349
  args = read_cli_options!(args)
408
350
  branch_name = target_branch_name
@@ -436,34 +378,27 @@ class Twig
436
378
  end
437
379
 
438
380
  def get_branch_property_for_cli(branch_name, property_name)
439
- begin
440
- value = get_branch_property(branch_name, property_name)
441
- if value
442
- puts value
443
- else
444
- raise Twig::Branch::MissingPropertyError,
445
- %{The branch "#{branch_name}" does not have the property "#{property_name}".}
446
- end
447
- rescue ArgumentError, Twig::Branch::MissingPropertyError => exception
448
- abort exception.message
381
+ value = get_branch_property(branch_name, property_name)
382
+ if value
383
+ puts value
384
+ else
385
+ raise Twig::Branch::MissingPropertyError,
386
+ %{The branch "#{branch_name}" does not have the property "#{property_name}".}
449
387
  end
388
+ rescue ArgumentError, Twig::Branch::MissingPropertyError => exception
389
+ abort exception.message
450
390
  end
451
391
 
452
392
  def set_branch_property_for_cli(branch_name, property_name, property_value)
453
- begin
454
- puts set_branch_property(branch_name, property_name, property_value)
455
- rescue ArgumentError, RuntimeError => exception
456
- abort exception.message
457
- end
393
+ puts set_branch_property(branch_name, property_name, property_value)
394
+ rescue ArgumentError, RuntimeError => exception
395
+ abort exception.message
458
396
  end
459
397
 
460
398
  def unset_branch_property_for_cli(branch_name, property_name)
461
- begin
462
- puts unset_branch_property(branch_name, property_name)
463
- rescue ArgumentError, Twig::Branch::MissingPropertyError => exception
464
- abort exception.message
465
- end
399
+ puts unset_branch_property(branch_name, property_name)
400
+ rescue ArgumentError, Twig::Branch::MissingPropertyError => exception
401
+ abort exception.message
466
402
  end
467
-
468
403
  end
469
404
  end