twig 1.6 → 1.7

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.
@@ -0,0 +1,174 @@
1
+ class Twig
2
+ module Cli
3
+ # Handles printing help output for `twig help`.
4
+ module Help
5
+ def self.console_width
6
+ 80
7
+ end
8
+
9
+ def self.intro
10
+ version_string = "Twig v#{Twig::VERSION}"
11
+
12
+ intro = Help.paragraph(%{
13
+ Twig is your personal Git branch assistant. It's a command-line tool
14
+ for listing your most recent branches, and for remembering branch
15
+ details for you, like issue tracker ids and todos. It also supports
16
+ subcommands, like automatically fetching statuses from your issue
17
+ tracking system.
18
+ })
19
+
20
+ intro = <<-BANNER.gsub(/^[ ]+/, '')
21
+
22
+ #{'=' * version_string.size}
23
+ #{version_string}
24
+ #{'=' * version_string.size}
25
+
26
+ #{intro}
27
+
28
+ #{Twig::HOMEPAGE}
29
+ BANNER
30
+
31
+ intro + ' ' # Force extra blank line
32
+ end
33
+
34
+ def self.description(text, options = {})
35
+ defaults = {
36
+ :add_blank_line => false,
37
+ :width => 40
38
+ }
39
+ options = defaults.merge(options)
40
+
41
+ width = options[:width]
42
+ words = text.gsub(/\n?\s+/, ' ').strip.split(' ')
43
+ lines = []
44
+
45
+ # Split words into lines
46
+ while words.any?
47
+ current_word = words.shift
48
+ current_word_size = Display.unformat_string(current_word).size
49
+ last_line = lines.last
50
+ last_line_size = last_line && Display.unformat_string(last_line).size
51
+
52
+ if last_line_size && (last_line_size + current_word_size + 1 <= width)
53
+ last_line << ' ' << current_word
54
+ elsif current_word_size >= width
55
+ lines << current_word[0...width]
56
+ words.unshift(current_word[width..-1])
57
+ else
58
+ lines << current_word
59
+ end
60
+ end
61
+
62
+ lines << ' ' if options[:add_blank_line]
63
+ lines
64
+ end
65
+
66
+ def self.description_for_custom_property(option_parser, desc_lines, options = {})
67
+ options[:trailing] ||= "\n"
68
+ indent = ' '
69
+ left_column_width = 29
70
+
71
+ help_desc = desc_lines.inject('') do |desc, (left_column, right_column)|
72
+ desc + indent +
73
+ sprintf("%-#{left_column_width}s", left_column) + right_column + "\n"
74
+ end
75
+
76
+ Help.print_section(option_parser, help_desc, :trailing => options[:trailing])
77
+ end
78
+
79
+ def self.line_for_custom_property?(line)
80
+ is_custom_property_except = (
81
+ line.include?('--except-') &&
82
+ !line.include?('--except-branch') &&
83
+ !line.include?('--except-property') &&
84
+ !line.include?('--except-PROPERTY')
85
+ )
86
+ is_custom_property_only = (
87
+ line.include?('--only-') &&
88
+ !line.include?('--only-branch') &&
89
+ !line.include?('--only-property') &&
90
+ !line.include?('--only-PROPERTY')
91
+ )
92
+ is_custom_property_width = (
93
+ line =~ /--.+-width/ &&
94
+ !line.include?('--branch-width') &&
95
+ !line.include?('--PROPERTY-width')
96
+ )
97
+
98
+ is_custom_property_except ||
99
+ is_custom_property_only ||
100
+ is_custom_property_width
101
+ end
102
+
103
+ def self.paragraph(text)
104
+ Help.description(text, :width => console_width).join("\n")
105
+ end
106
+
107
+ def self.print_line(option_parser, text)
108
+ # Prints a single line of text without line breaks.
109
+ option_parser.separator(text)
110
+ end
111
+
112
+ def self.print_paragraph(option_parser, text, separator_options = {})
113
+ # Prints a long chunk of text with automatic word wrapping and a leading
114
+ # line break.
115
+
116
+ separator_options[:trailing] ||= ''
117
+ Help.print_section(option_parser, Help.paragraph(text), separator_options)
118
+ end
119
+
120
+ def self.print_section(option_parser, text, options = {})
121
+ # Prints text with leading and trailing line breaks.
122
+
123
+ options[:trailing] ||= ''
124
+ option_parser.separator "\n#{text}#{options[:trailing]}"
125
+ end
126
+
127
+ def self.subcommand_descriptions
128
+ descs = {
129
+ 'checkout-child' => 'Checks out a branch\'s child branch, if any.',
130
+ 'checkout-parent' => 'Checks out a branch\'s parent branch.',
131
+ 'create-branch' => 'Creates a branch and sets its `diff-branch` property to the previous branch name.',
132
+ 'diff' => 'Shows the diff between a branch and its parent branch (`diff-branch`).',
133
+ 'gh-open' => 'Opens a browser window for the current GitHub repository.',
134
+ 'gh-open-issue' => 'Opens a browser window for a branch\'s GitHub issue, if any.',
135
+ 'gh-update' => 'Updates each branch with the latest issue status on GitHub.',
136
+ 'help' => 'Provides help for Twig and its subcommands.',
137
+ 'init' => 'Runs all Twig setup commands.',
138
+ 'init-completion' => 'Initializes tab completion for Twig. Runs as part of `twig init`.',
139
+ 'init-config' => 'Creates a default `~/.twigconfig` file. Runs as part of `twig init`.',
140
+ 'rebase' => 'Rebases a branch onto its parent branch (`diff-branch`).'
141
+ }
142
+
143
+ line_prefix = '- '
144
+ gutter_width = 2 # Space between columns
145
+ names = descs.keys.sort
146
+ max_name_width = names.map { |name| name.length }.max
147
+ names_width = max_name_width + gutter_width
148
+ descs_width = Help.console_width - line_prefix.length - names_width
149
+ desc_indent = ' ' * (names_width + line_prefix.length)
150
+
151
+ names.map do |name|
152
+ line_prefix +
153
+ sprintf("%-#{names_width}s", name) +
154
+ Help.description(descs[name], :width => descs_width).join("\n" + desc_indent)
155
+ end
156
+ end
157
+
158
+ def self.header(option_parser, text, separator_options = {}, header_options = {})
159
+ separator_options[:trailing] ||= "\n\n"
160
+ header_options[:underline] ||= '='
161
+
162
+ Help.print_section(
163
+ option_parser,
164
+ text + "\n" + (header_options[:underline] * text.size),
165
+ separator_options
166
+ )
167
+ end
168
+
169
+ def self.subheader(option_parser, text, separator_options = {})
170
+ header(option_parser, text, separator_options, :underline => '-')
171
+ end
172
+ end
173
+ end
174
+ end
@@ -2,23 +2,79 @@ class Twig
2
2
 
3
3
  # Stores a branch's last commit time and its relative time representation.
4
4
  class CommitTime
5
+ def self.now
6
+ Time.now
7
+ end
5
8
 
6
- def initialize(time, time_ago)
9
+ def initialize(time)
7
10
  @time = time
11
+ suffix = 'ago'
12
+
13
+ # Cache calculations against current time
14
+ years_ago = count_years_ago
15
+ months_ago = count_months_ago
16
+ weeks_ago = count_weeks_ago
17
+ days_ago = count_days_ago
18
+ hours_ago = count_hours_ago
19
+ minutes_ago = count_minutes_ago
20
+ seconds_ago = count_seconds_ago
21
+
22
+ @time_ago =
23
+ if years_ago > 0
24
+ "#{years_ago}y"
25
+ elsif months_ago > 0 and weeks_ago > 4
26
+ "#{months_ago}mo"
27
+ elsif weeks_ago > 0
28
+ "#{weeks_ago}w"
29
+ elsif days_ago > 0
30
+ "#{days_ago}d"
31
+ elsif hours_ago > 0
32
+ "#{hours_ago}h"
33
+ elsif minutes_ago > 0
34
+ "#{minutes_ago}m"
35
+ else
36
+ "#{seconds_ago}s"
37
+ end
38
+ @time_ago << ' ' << suffix
39
+ end
40
+
41
+ def count_years_ago
42
+ seconds_in_a_year = 60 * 60 * 24 * 365
43
+ seconds = CommitTime.now - @time
44
+ seconds < seconds_in_a_year ? 0 : (seconds / seconds_in_a_year).round
45
+ end
46
+
47
+ def count_months_ago
48
+ now = CommitTime.now
49
+ (now.year * 12 + now.month) - (@time.year * 12 + @time.month)
50
+ end
8
51
 
9
- # Shorten relative time
10
- @time_ago = time_ago.
11
- sub(/ years?/, 'y').
12
- sub(' months', 'mo').
13
- sub(' weeks', 'w').
14
- sub(' days', 'd').
15
- sub(' hours', 'h').
16
- sub(' minutes', 'm').
17
- sub(' seconds', 's')
52
+ def count_weeks_ago
53
+ seconds_in_a_week = 60 * 60 * 24 * 7
54
+ seconds = CommitTime.now - @time
55
+ seconds < seconds_in_a_week ? 0 : (seconds / seconds_in_a_week).round
56
+ end
57
+
58
+ def count_days_ago
59
+ seconds_in_a_day = 60 * 60 * 24
60
+ seconds = CommitTime.now - @time
61
+ seconds < seconds_in_a_day ? 0 : (seconds / seconds_in_a_day).round
62
+ end
63
+
64
+ def count_hours_ago
65
+ seconds_in_an_hour = 60 * 60
66
+ seconds = CommitTime.now - @time
67
+ seconds < seconds_in_an_hour ? 0 : (seconds / seconds_in_an_hour).round
68
+ end
69
+
70
+ def count_minutes_ago
71
+ seconds_in_a_minute = 60
72
+ seconds = CommitTime.now - @time
73
+ seconds < seconds_in_a_minute ? 0 : (seconds / seconds_in_a_minute).round
74
+ end
18
75
 
19
- # Keep only the most significant units in the relative time
20
- time_ago_parts = @time_ago.split(/\s+/)
21
- @time_ago = "#{time_ago_parts[0]} #{time_ago_parts[-1]}".gsub(/,/, '')
76
+ def count_seconds_ago
77
+ (CommitTime.now - @time).to_i
22
78
  end
23
79
 
24
80
  def to_i
@@ -37,6 +93,5 @@ class Twig
37
93
  def <=>(other)
38
94
  to_i <=> other.to_i
39
95
  end
40
-
41
96
  end
42
97
  end
data/lib/twig/display.rb CHANGED
@@ -1,4 +1,7 @@
1
1
  class Twig
2
+
3
+ # Handles displaying matching branches as a command-line table or as
4
+ # serialized data.
2
5
  module Display
3
6
  COLORS = {
4
7
  :black => 30,
@@ -19,6 +22,11 @@ class Twig
19
22
  CURRENT_BRANCH_INDICATOR = '* '
20
23
  EMPTY_BRANCH_PROPERTY_INDICATOR = '-'
21
24
 
25
+ def self.unformat_string(string)
26
+ # Returns a copy of the given string without color/weight markers.
27
+ string.gsub(/\e\[[0-9]+(;[0-9]+)?m/, '')
28
+ end
29
+
22
30
  def column(string, options = {})
23
31
  # Returns `string` with an exact fixed width. If `string` is too wide, it
24
32
  # is truncated with an ellipsis and a trailing space to separate columns.
@@ -42,7 +50,7 @@ class Twig
42
50
 
43
51
  new_string = format_string(
44
52
  new_string,
45
- options.reject { |k, v| ![:color, :weight].include?(k) }
53
+ options.reject { |key, value| ![:color, :weight].include?(key) }
46
54
  )
47
55
 
48
56
  new_string
@@ -144,11 +152,20 @@ class Twig
144
152
  data.to_json
145
153
  end
146
154
 
155
+ def format_strings?
156
+ !Twig::System.windows?
157
+ end
158
+
147
159
  def format_string(string, options)
148
160
  # Options:
149
161
  # - `:color`: `nil` by default. Accepts a key from `COLORS`.
150
162
  # - `:weight`: `nil` by default. Accepts a key from `WEIGHTS`.
151
163
 
164
+ # Unlike `::unformat_string`, this is an instance method so that it can
165
+ # handle config options, e.g., globally disabling color.
166
+
167
+ return string unless format_strings?
168
+
152
169
  string_options = []
153
170
  string_options << COLORS[options[:color]] if options[:color]
154
171
  string_options << WEIGHTS[options[:weight]] if options[:weight]
@@ -159,9 +176,5 @@ class Twig
159
176
 
160
177
  open_format + string.to_s + close_format
161
178
  end
162
-
163
- def unformat_string(string)
164
- string.gsub(/\e\[[0-9]+(;[0-9]+)?m/, '')
165
- end
166
- end # module Display
179
+ end
167
180
  end
data/lib/twig/github.rb CHANGED
@@ -1,14 +1,20 @@
1
1
  require 'uri'
2
2
 
3
3
  class Twig
4
+
5
+ # Represents a Git repository that is hosted on GitHub. Usage:
6
+ #
7
+ # Twig::GithubRepo.new do |gh_repo|
8
+ # puts gh_repo.username
9
+ # puts gh_repo.repository
10
+ # end
11
+ #
4
12
  class GithubRepo
5
13
  def initialize
6
- unless Twig.repo?
7
- abort 'Current directory is not a git repository.'
8
- end
14
+ abort 'Current directory is not a git repository.' unless Twig.repo?
9
15
 
10
16
  if origin_url.empty? || !github_repo? || username.empty? || repository.empty?
11
- abort_for_non_github_repo
17
+ abort 'This does not appear to be a GitHub repository.'
12
18
  end
13
19
 
14
20
  yield(self)
@@ -35,9 +41,5 @@ class Twig
35
41
  def repository
36
42
  @repo ||= origin_url_parts[-1].sub(/\.git$/, '') || ''
37
43
  end
38
-
39
- def abort_for_non_github_repo
40
- abort 'This does not appear to be a GitHub repository.'
41
- end
42
44
  end
43
45
  end
data/lib/twig/options.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  class Twig
2
- module Options
3
2
 
3
+ # Handles reading options from command-line switches and config files.
4
+ module Options
4
5
  CONFIG_PATH = '~/.twigconfig'
5
6
  DEPRECATED_CONFIG_PATH = '~/.twigrc'
6
7
  MIN_PROPERTY_WIDTH = 3
@@ -8,7 +9,7 @@ class Twig
8
9
  def readable_config_file_path
9
10
  config_path = File.expand_path(CONFIG_PATH)
10
11
 
11
- if File.exists?(config_path)
12
+ if File.exist?(config_path)
12
13
  unless File.readable?(config_path)
13
14
  $stderr.puts "Warning: #{CONFIG_PATH} is not readable."
14
15
  return # Stop if file exists but is not readable
@@ -16,12 +17,12 @@ class Twig
16
17
  else
17
18
  config_path = File.expand_path(DEPRECATED_CONFIG_PATH)
18
19
 
19
- if File.exists?(config_path)
20
+ if File.exist?(config_path)
20
21
  if File.readable?(config_path)
21
- $stderr.puts "DEPRECATED: #{DEPRECATED_CONFIG_PATH} is deprecated. " <<
22
+ $stderr.puts "DEPRECATED: #{DEPRECATED_CONFIG_PATH} is deprecated. " \
22
23
  "Please rename it to #{CONFIG_PATH}."
23
24
  else
24
- $stderr.puts "DEPRECATED: #{DEPRECATED_CONFIG_PATH} is deprecated. " <<
25
+ $stderr.puts "DEPRECATED: #{DEPRECATED_CONFIG_PATH} is deprecated. " \
25
26
  "Please rename it to #{CONFIG_PATH} and make it readable."
26
27
  return # Stop if file exists but is not readable
27
28
  end
@@ -50,8 +51,8 @@ class Twig
50
51
  if !key.empty? && value
51
52
  opts[key] = value.strip
52
53
  elsif !line.empty?
53
- $stderr.puts %{Warning: Invalid line "#{line}" in #{config_path}. } <<
54
- %{Expected format: `key: value`}
54
+ $stderr.puts %{Warning: Invalid line "#{line}" in #{config_path}. } \
55
+ 'Expected format: `key: value`'
55
56
  end
56
57
 
57
58
  opts
@@ -99,6 +100,10 @@ class Twig
99
100
  when 'github-uri-prefix'
100
101
  set_option(:github_uri_prefix, value)
101
102
 
103
+ # Subcommands:
104
+ when 'twig-rebase-autoconfirm'
105
+ set_option(:twig_rebase_autoconfirm, value)
106
+
102
107
  end
103
108
  end
104
109
  end
@@ -148,6 +153,9 @@ class Twig
148
153
  when :reverse
149
154
  options[:reverse] = Twig::Util.truthy?(value)
150
155
 
156
+ when :twig_rebase_autoconfirm
157
+ options[:twig_rebase_autoconfirm] = Twig::Util.truthy?(value)
158
+
151
159
  when :unset_property
152
160
  options[key] = value
153
161
  end
@@ -187,11 +195,12 @@ class Twig
187
195
  min_property_value = [property_name_width, MIN_PROPERTY_WIDTH].max
188
196
 
189
197
  if property_value < min_property_value
190
- min_desc = if property_value < property_name_width
191
- %{#{property_name_width} (width of "#{property_name}")}
192
- else
193
- %{#{MIN_PROPERTY_WIDTH}}
194
- end
198
+ min_desc =
199
+ if property_value < property_name_width
200
+ %{#{property_name_width} (width of "#{property_name}")}
201
+ else
202
+ MIN_PROPERTY_WIDTH.to_s
203
+ end
195
204
 
196
205
  error = %{The value `--#{property_name}-width=#{property_value}` } +
197
206
  %{is too low. The minimum is #{min_desc}.}
@@ -206,6 +215,5 @@ class Twig
206
215
  def unset_option(key)
207
216
  options.delete(key)
208
217
  end
209
-
210
- end # module Options
218
+ end
211
219
  end