twig 1.6 → 1.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -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