twig 1.5 → 1.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,12 +1,22 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- # Usage:
3
+ # Synopsis:
4
4
  #
5
- # - `twig gh-open-issue`:
6
- # Opens a browser window for the GitHub issue, if any, for the current branch.
5
+ # twig gh-open-issue [-b|--branch <branch>]
7
6
  #
8
- # - `twig gh-open-issue -b <branch>`:
9
- # Opens the GitHub issue, if any, for the specified branch.
7
+ # Description:
8
+ #
9
+ # Opens a browser window the GitHub issue, if any, for the current branch.
10
+ #
11
+ # To customize the GitHub URI prefix (e.g., for GitHub Enterprise
12
+ # installations), set GitHub options in `~/.twigconfig`:
13
+ #
14
+ # github-uri-prefix: http://example-enterprise.github.com
15
+ # github-api-uri-prefix: http://example-enterprise.github.com
16
+ #
17
+ # Options:
18
+ #
19
+ # `-b` or `--branch`: Opens the GitHub issue, if any, for the given branch.
10
20
  #
11
21
  # Subcommand for Twig: <http://rondevera.github.io/twig/>
12
22
  # Author: Ron DeVera <http://rondevera.com>
@@ -16,15 +26,12 @@ require 'twig'
16
26
  require 'launchy'
17
27
 
18
28
  Twig::GithubRepo.new do |gh_repo|
19
- twig = Twig.new
20
- twig.read_config_file!
21
- twig.read_cli_options!(ARGV)
22
-
23
- branch_name = twig.options[:branch] || twig.current_branch_name
24
- issue_id = twig.get_branch_property(branch_name, 'issue')
29
+ twig = Twig.new(:read_options => true)
30
+ target_branch_name = twig.target_branch_name
31
+ issue_id = twig.get_branch_property(target_branch_name, 'issue')
25
32
 
26
33
  unless issue_id
27
- abort %{The branch "#{branch_name}" doesn't have an "issue" property.}
34
+ abort %{The branch "#{target_branch_name}" doesn't have an "issue" property.}
28
35
  end
29
36
 
30
37
  gh_uri_prefix = twig.options[:github_uri_prefix]
@@ -1,6 +1,18 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- # Updates each branch with the latest issue status on GitHub.
3
+ # Synopsis:
4
+ #
5
+ # twig gh-update
6
+ #
7
+ # Description:
8
+ #
9
+ # Updates each branch with the latest issue status on GitHub.
10
+ #
11
+ # To customize the GitHub URI prefix (e.g., for GitHub Enterprise
12
+ # installations), set GitHub options in `~/.twigconfig`:
13
+ #
14
+ # github-uri-prefix: http://example-enterprise.github.com
15
+ # github-api-uri-prefix: http://example-enterprise.github.com
4
16
  #
5
17
  # Subcommand for Twig: <http://rondevera.github.io/twig/>
6
18
  # Author: Ron DeVera <http://rondevera.com>
@@ -11,9 +23,7 @@ require 'json'
11
23
  require 'net/https'
12
24
  require 'uri'
13
25
 
14
- twig = Twig.new
15
- twig.read_config_file!
16
- twig.read_cli_options!(ARGV)
26
+ twig = Twig.new(:read_options => true)
17
27
 
18
28
  Twig::GithubRepo.new do |gh_repo|
19
29
  $stdout.sync = true
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Synopsis:
4
+ #
5
+ # twig init
6
+ #
7
+ # Description:
8
+ #
9
+ # Runs all initialization commands. This is only needed once after installing
10
+ # Twig, and affects all Git repositories.
11
+ #
12
+ # Subcommand for Twig: <http://rondevera.github.io/twig/>
13
+ # Author: Ron DeVera <http://rondevera.com>
14
+
15
+ exec('twig init-completion')
@@ -1,7 +1,19 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- # Initializes tab completion for Twig. To use this, run
4
- # `twig init-completion` and follow the instructions.
3
+ # Synopsis:
4
+ #
5
+ # twig init-completion [--force]
6
+ #
7
+ # Description:
8
+ #
9
+ # Initializes tab completion for Twig. Instead of running this directly,
10
+ # run `twig init` to run all setup tasks.
11
+ #
12
+ # Options:
13
+ #
14
+ # `--force`: By default, `twig init-completion` preserves the existing tab
15
+ # completion script, if any. If this option is used, the command will
16
+ # overwrite any existing tab completion script.
5
17
  #
6
18
  # Subcommand for Twig: <http://rondevera.github.io/twig/>
7
19
  # Author: Ron DeVera <http://rondevera.com>
@@ -1,7 +1,14 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- # Initializes bash tab completion for Twig. To use this, run
4
- # `twig init-completion` and follow the instructions.
3
+ # Synopsis:
4
+ #
5
+ # twig init-completion-bash
6
+ #
7
+ # Description:
8
+ #
9
+ # Initializes bash tab completion for Twig. Instead of running this directly,
10
+ # run `twig init` to run all setup tasks for the current shell. Uses all
11
+ # options available for `twig init-completion`.
5
12
  #
6
13
  # Subcommand for Twig: <http://rondevera.github.io/twig/>
7
14
  # Author: Ron DeVera <http://rondevera.com>
@@ -84,7 +91,8 @@ full_script_path = File.expand_path(script_path)
84
91
  script_exists = File.exists?(full_script_path)
85
92
 
86
93
  if script_exists && !force
87
- puts "The file `#{script_path}` already exists."
94
+ twig = Twig.new
95
+ puts twig.format_string("The file `#{script_path}` already exists.", :color => :red)
88
96
  puts "To overwrite it, run `twig init-completion --force`."
89
97
  else
90
98
  File.open(full_script_path, 'w') do |file|
@@ -1,24 +1,27 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- # Summary:
3
+ # Synopsis:
4
4
  #
5
- # `twig rebase [<branch>] [<options>]`
5
+ # twig rebase [<branch>] [<options>]
6
6
  #
7
- # Usage:
7
+ # Description:
8
8
  #
9
- # - `twig rebase`:
10
9
  # Rebases the current branch onto the branch in its `diff-branch` property.
10
+ # All options are passed through to `git-rebase`.
11
11
  #
12
- # - `twig rebase <options>`:
13
- # Rebases the current branch onto its `diff-branch`, and passes options
14
- # through to `git-rebase`, e.g., `twig rebase -i`.
12
+ # Examples:
15
13
  #
16
- # - `twig rebase <branch>`:
17
- # Rebases the given branch onto its `diff-branch`.
14
+ # Rebase the current branch onto its `diff-branch` interactively:
18
15
  #
19
- # - `twig rebase <branch> <options>`:
20
- # Rebases the given branch and its `diff-branch`, and passes options
21
- # through to `git-rebase`, e.g., `twig rebase my_branch -i`.
16
+ # twig rebase -i
17
+ #
18
+ # Rebase the given branch onto its `diff-branch`:
19
+ #
20
+ # twig rebase my_branch
21
+ #
22
+ # Rebase the given branch onto its `diff-branch` interactively:
23
+ #
24
+ # twig rebase my_branch -i
22
25
  #
23
26
  # Subcommand for Twig: <http://rondevera.github.io/twig/>
24
27
  # Author: Ron DeVera <http://rondevera.com>
@@ -1,4 +1,15 @@
1
- Dir[File.join(File.dirname(__FILE__), 'twig', '*.rb')].each { |file| require file }
1
+ $LOAD_PATH.unshift(File.dirname(__FILE__)) # For gem development
2
+ require 'twig/branch'
3
+ require 'twig/cli'
4
+ require 'twig/commit_time'
5
+ require 'twig/display'
6
+ require 'twig/github'
7
+ require 'twig/homepage'
8
+ require 'twig/options'
9
+ require 'twig/subcommands'
10
+ require 'twig/system'
11
+ require 'twig/util'
12
+ require 'twig/version'
2
13
  require 'time'
3
14
 
4
15
  # The main class.
@@ -26,37 +37,32 @@ class Twig
26
37
  $?.success?
27
38
  end
28
39
 
29
- def initialize
40
+ def initialize(options = {})
30
41
  self.options = {}
31
42
 
32
43
  # Set defaults
33
44
  set_option(:github_api_uri_prefix, DEFAULT_GITHUB_API_URI_PREFIX)
34
45
  set_option(:github_uri_prefix, DEFAULT_GITHUB_URI_PREFIX)
35
46
  set_option(:header_style, DEFAULT_HEADER_COLOR.to_s)
47
+
48
+ if options[:read_options]
49
+ read_config_file!
50
+ read_cli_options!(ARGV)
51
+ end
36
52
  end
37
53
 
38
54
  def current_branch_name
55
+ # Returns the name of the branch that is currently checked out in Git.
39
56
  @_current_branch_name ||= Twig.run('git rev-parse --abbrev-ref HEAD')
40
57
  end
41
58
 
42
- def all_branches
43
- @_all_branches ||= begin
44
- branch_tuples = Twig.
45
- run(%{git for-each-ref #{ REF_PREFIX } --format="#{ REF_FORMAT }"}).
46
- split("\n")
47
-
48
- branch_tuples.inject([]) do |result, branch_tuple|
49
- ref, time_string, time_ago = branch_tuple.split(REF_FORMAT_SEPARATOR)
50
- time = Time.parse(time_string)
51
- commit_time = Twig::CommitTime.new(time, time_ago)
52
- branch = Branch.new(ref, :last_commit_time => commit_time)
53
- result << branch
54
- end
55
- end
59
+ def target_branch_name
60
+ # Returns the name of the branch to work on, e.g., for setting a property.
61
+ options[:branch] || current_branch_name
56
62
  end
57
63
 
58
64
  def branches
59
- branches = all_branches
65
+ branches = Twig::Branch.all_branches
60
66
  now = Time.now
61
67
  max_days_old = options[:max_days_old]
62
68
  max_seconds_old = max_days_old * 86400 if max_days_old
@@ -99,8 +105,22 @@ class Twig
99
105
  branches
100
106
  end
101
107
 
102
- def all_branch_names
103
- all_branches.map { |branch| branch.name }
108
+ def property_names
109
+ @_property_names ||= begin
110
+ property_names = Twig::Branch.all_property_names
111
+ only_name = options[:property_only_name]
112
+ except_name = options[:property_except_name]
113
+
114
+ if only_name
115
+ property_names = property_names.select { |name| name =~ only_name }
116
+ end
117
+
118
+ if except_name
119
+ property_names = property_names.select { |name| name !~ except_name }
120
+ end
121
+
122
+ property_names
123
+ end
104
124
  end
105
125
 
106
126
 
@@ -109,7 +129,7 @@ class Twig
109
129
 
110
130
  def list_branches
111
131
  if branches.empty?
112
- if all_branches.any?
132
+ if Twig::Branch.all_branches.any?
113
133
  return 'There are no branches matching your selected options.'
114
134
  else
115
135
  return 'This repository has no branches.'
@@ -5,7 +5,7 @@ class Twig
5
5
 
6
6
  EMPTY_PROPERTY_NAME_ERROR = 'Branch property names cannot be empty strings.'
7
7
  PROPERTY_NAME_FROM_GIT_CONFIG = /^branch\.[^.]+\.([^=]+)=.*$/
8
- RESERVED_BRANCH_PROPERTY_NAMES = %w[branch merge rebase remote]
8
+ RESERVED_BRANCH_PROPERTY_NAMES = %w[branch merge property rebase remote]
9
9
 
10
10
  class EmptyPropertyNameError < ArgumentError
11
11
  def initialize(message = nil)
@@ -17,14 +17,34 @@ class Twig
17
17
 
18
18
  attr_accessor :name, :last_commit_time
19
19
 
20
+ def self.all_branches
21
+ @_all_branches ||= begin
22
+ branch_tuples = Twig.
23
+ run(%{git for-each-ref #{ REF_PREFIX } --format="#{ REF_FORMAT }"}).
24
+ split("\n")
25
+
26
+ branch_tuples.inject([]) do |result, branch_tuple|
27
+ name, time_string, time_ago = branch_tuple.split(REF_FORMAT_SEPARATOR)
28
+ time = Time.parse(time_string)
29
+ commit_time = Twig::CommitTime.new(time, time_ago)
30
+ branch = Branch.new(name, :last_commit_time => commit_time)
31
+ result << branch
32
+ end
33
+ end
34
+ end
35
+
36
+ def self.all_branch_names
37
+ @_all_branch_names ||= self.all_branches.map { |branch| branch.name }
38
+ end
39
+
20
40
  def self.all_property_names
21
41
  @_all_property_names ||= begin
22
42
  config_lines = Twig.run('git config --list').split("\n")
23
43
 
24
44
  properties = config_lines.map do |line|
25
- # Split by rightmost `=`, allowing branch names to contain `=`:
26
- key = value = nil
27
- line.match(/(.+)=(.+)/).tap { |md| key, value = md[1..2] if md }
45
+ # Split by rightmost `=`, allowing branch names to contain `=`
46
+ match_data = line.match(/(.+)=(.+)/)
47
+ key, value = match_data[1..2] if match_data
28
48
  next unless key
29
49
 
30
50
  key_parts = key.split('.')
@@ -35,6 +55,10 @@ class Twig
35
55
  end
36
56
  end
37
57
 
58
+ def self.validate_property_name(property_name)
59
+ raise EmptyPropertyNameError if property_name.empty?
60
+ end
61
+
38
62
  def initialize(name, attrs = {})
39
63
  self.name = name
40
64
  raise ArgumentError, '`name` is required' if name.empty?
@@ -44,13 +68,11 @@ class Twig
44
68
 
45
69
  def to_s ; name ; end
46
70
 
47
- def to_hash
48
- all_property_names = Twig::Branch.all_property_names
49
-
71
+ def to_hash(property_names)
50
72
  {
51
73
  'name' => name,
52
74
  'last-commit-time' => last_commit_time.iso8601,
53
- 'properties' => get_properties(all_property_names)
75
+ 'properties' => get_properties(property_names)
54
76
  }
55
77
  end
56
78
 
@@ -61,7 +83,7 @@ class Twig
61
83
  def escaped_property_names(property_names)
62
84
  property_names.map do |property_name|
63
85
  property_name = sanitize_property(property_name)
64
- raise EmptyPropertyNameError if property_name.empty?
86
+ Branch.validate_property_name(property_name)
65
87
  Regexp.escape(property_name)
66
88
  end
67
89
  end
@@ -103,10 +125,9 @@ class Twig
103
125
  def set_property(property_name, value)
104
126
  property_name = sanitize_property(property_name)
105
127
  value = value.to_s.strip
128
+ Branch.validate_property_name(property_name)
106
129
 
107
- if property_name.empty?
108
- raise EmptyPropertyNameError
109
- elsif RESERVED_BRANCH_PROPERTY_NAMES.include?(property_name)
130
+ if RESERVED_BRANCH_PROPERTY_NAMES.include?(property_name)
110
131
  raise ArgumentError,
111
132
  %{Can't modify the reserved property "#{property_name}".}
112
133
  elsif value.empty?
@@ -126,7 +147,7 @@ class Twig
126
147
 
127
148
  def unset_property(property_name)
128
149
  property_name = sanitize_property(property_name)
129
- raise EmptyPropertyNameError if property_name.empty?
150
+ Branch.validate_property_name(property_name)
130
151
 
131
152
  value = get_property(property_name)
132
153
 
@@ -4,13 +4,33 @@ require 'rbconfig'
4
4
  class Twig
5
5
  module Cli
6
6
 
7
+ def self.prompt_with_choices(prompt, choices)
8
+ # Prints the given string `prompt` and the array `choices` numbered, and
9
+ # prompts the user to enter a number. Returns the matching value, or nil
10
+ # if the user input is invalid.
11
+
12
+ if choices.size < 2
13
+ raise ArgumentError, 'At least two choices required'
14
+ end
15
+
16
+ puts prompt
17
+ choices.each_with_index do |choice, index|
18
+ puts "#{sprintf('%3s', index + 1)}. #{choice}"
19
+ end
20
+ print '> '
21
+
22
+ input = $stdin.gets.to_i
23
+ choices[input - 1]
24
+ end
25
+
7
26
  def help_intro
8
27
  version_string = "Twig v#{Twig::VERSION}"
9
28
 
10
29
  intro = help_paragraph(%{
11
- Twig is your personal Git branch assistant. It shows you your most
12
- recent branches, and tracks issue tracker ids, tasks, and other metadata
13
- for your Git branches.
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.
14
34
  })
15
35
 
16
36
  intro = <<-BANNER.gsub(/^[ ]+/, '')
@@ -78,11 +98,13 @@ class Twig
78
98
  is_custom_property_except = (
79
99
  line.include?('--except-') &&
80
100
  !line.include?('--except-branch') &&
101
+ !line.include?('--except-property') &&
81
102
  !line.include?('--except-PROPERTY')
82
103
  )
83
104
  is_custom_property_only = (
84
105
  line.include?('--only-') &&
85
106
  !line.include?('--only-branch') &&
107
+ !line.include?('--only-property') &&
86
108
  !line.include?('--only-PROPERTY')
87
109
  )
88
110
  is_custom_property_width = (
@@ -212,7 +234,7 @@ class Twig
212
234
  end
213
235
  help_description_for_custom_property(opts, [
214
236
  ['--only-PROPERTY PATTERN', 'Only list branches with a given property'],
215
- ['', 'that matches a given pattern.'],
237
+ ['', 'that matches a given pattern.']
216
238
  ], :trailing => '')
217
239
  help_description_for_custom_property(opts, [
218
240
  ['--except-PROPERTY PATTERN', 'Do not list branches with a given property'],
@@ -260,6 +282,28 @@ class Twig
260
282
  ['', "(Default: #{Twig::DEFAULT_PROPERTY_COLUMN_WIDTH})"]
261
283
  ])
262
284
 
285
+ desc = <<-DESC
286
+ Only include properties where the property name matches the given
287
+ regular expression.
288
+ DESC
289
+ opts.on(
290
+ '--only-property PATTERN',
291
+ *help_description(desc, :add_separator => true)
292
+ ) do |pattern|
293
+ set_option(:property_only_name, pattern)
294
+ end
295
+
296
+ desc = <<-DESC
297
+ Exclude properties where the property name matches the given regular
298
+ expression.
299
+ DESC
300
+ opts.on(
301
+ '--except-property PATTERN',
302
+ *help_description(desc, :add_separator => true)
303
+ ) do |pattern|
304
+ set_option(:property_except_name, pattern)
305
+ end
306
+
263
307
  colors = Twig::Display::COLORS.keys.map do |value|
264
308
  format_string(value, :color => value)
265
309
  end.join(', ')
@@ -326,7 +370,7 @@ class Twig
326
370
  ' except-branch: staging',
327
371
  ' header-style: green bold',
328
372
  ' max-days-old: 30',
329
- ' reverse: true',
373
+ ' reverse: true'
330
374
  ].join("\n"), :trailing => '')
331
375
 
332
376
  help_separator(opts, help_paragraph(%{
@@ -343,9 +387,8 @@ class Twig
343
387
  end
344
388
 
345
389
  def abort_for_option_exception(exception)
346
- puts exception.message
347
- puts 'For a list of options, run `twig --help`.'
348
- exit
390
+ puts exception.message + "\nFor a list of options, run `twig --help`."
391
+ exit 1
349
392
  end
350
393
 
351
394
  def exec_subcommand_if_any(args)
@@ -362,7 +405,7 @@ class Twig
362
405
  exec_subcommand_if_any(args) if args.any?
363
406
 
364
407
  args = read_cli_options!(args)
365
- branch_name = options[:branch] || current_branch_name
408
+ branch_name = target_branch_name
366
409
  format = options.delete(:format)
367
410
  property_to_unset = options.delete(:unset_property)
368
411