twig 1.2.1 → 1.3
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.
- data/HISTORY.md +14 -1
- data/README.md +11 -5
- data/bin/twig +2 -2
- data/bin/twig-gh-open +3 -2
- data/bin/twig-gh-open-issue +2 -1
- data/lib/twig.rb +13 -12
- data/lib/twig/cli.rb +77 -33
- data/lib/twig/display.rb +55 -33
- data/lib/twig/github.rb +10 -7
- data/lib/twig/options.rb +59 -7
- data/lib/twig/util.rb +4 -0
- data/lib/twig/version.rb +1 -1
- data/spec/twig/cli_spec.rb +106 -13
- data/spec/twig/display_spec.rb +131 -23
- data/spec/twig/github_spec.rb +299 -0
- data/spec/twig/options_spec.rb +108 -7
- data/spec/twig/util_spec.rb +24 -0
- data/spec/twig_spec.rb +41 -27
- data/twig.gemspec +4 -3
- metadata +26 -9
data/HISTORY.md
CHANGED
@@ -1,9 +1,22 @@
|
|
1
1
|
Twig
|
2
2
|
====
|
3
3
|
|
4
|
+
1.3 (2013-05-22)
|
5
|
+
----------------
|
6
|
+
* ENHANCEMENT: Add `--branch-width` and `--<property>-width` options for setting
|
7
|
+
custom column widths.
|
8
|
+
* ENHANCEMENT: Add `--reverse` option for listing least recently updated
|
9
|
+
branches first. This can be used in a config file as `reverse: true`.
|
10
|
+
* ENHANCEMENT: Make `gh-open` and `gh-open-issue` work cross-platform.
|
11
|
+
(GH-18. Thanks [ixti](https://github.com/ixti)!)
|
12
|
+
* FIX: Allow getting, setting, and unsetting properties for branches older than
|
13
|
+
the `max-days-old` option, if given.
|
14
|
+
* FIX: Abort `twig gh-*` subcommands early if working in a non-Github
|
15
|
+
repository.
|
16
|
+
|
4
17
|
1.2.1 (2013-05-04)
|
5
18
|
------------------
|
6
|
-
* FIX: Add User-Agent string to `twig
|
19
|
+
* FIX: Add User-Agent string to `twig gh-update` GitHub requests to comply with
|
7
20
|
GitHub API v3.
|
8
21
|
|
9
22
|
1.2 (2013-03-21)
|
data/README.md
CHANGED
@@ -43,13 +43,21 @@ chronologically with their properties.
|
|
43
43
|
* `twig <property> -b <branch>`: Get property for any branch
|
44
44
|
* `twig <property> <value> -b <branch>`: Set property for any branch
|
45
45
|
* `twig --unset <property> -b <branch>`: Unset property for any branch
|
46
|
-
* `twig --header-style <format>`: Change the header style, e.g., "red", "green bold"
|
47
46
|
* `twig init-completion`: Set up tab completion for `-b` and `--branch`
|
48
47
|
* `twig --help`: More info
|
49
48
|
|
50
49
|
|
51
|
-
|
52
|
-
|
50
|
+
Display options
|
51
|
+
---------------
|
52
|
+
|
53
|
+
* `twig --header-style <format>`: Change the header style, e.g., "red", "green bold"
|
54
|
+
* `twig --branch-width <number>`: Set the character width for the `branch` column
|
55
|
+
* `twig --<property>-width <number>`: Set the character width for a specific property column
|
56
|
+
* `twig --reverse`: List oldest branches first
|
57
|
+
|
58
|
+
|
59
|
+
Filtering options
|
60
|
+
-----------------
|
53
61
|
|
54
62
|
Twig lists all of your branches by default (newest first), but you can filter
|
55
63
|
them by age, name, and custom properties:
|
@@ -154,7 +162,6 @@ a browser window if possible:
|
|
154
162
|
|
155
163
|
$ twig gh-open
|
156
164
|
GitHub URL: https://github.com/myname/myproject
|
157
|
-
# Also opens a browser window (OS X only).
|
158
165
|
|
159
166
|
If you're working on an issue for a GitHub repository, the `gh-update`
|
160
167
|
subcommand syncs issue statuses with GitHub:
|
@@ -193,7 +200,6 @@ subcommand to view that issue on GitHub:
|
|
193
200
|
# Current branch:
|
194
201
|
$ twig gh-open-issue
|
195
202
|
GitHub issue URL: https://github.com/myname/myproject/issues/111
|
196
|
-
# Also opens a browser window (OS X only).
|
197
203
|
|
198
204
|
# Any branch:
|
199
205
|
$ twig gh-open-issue -b <branch name>
|
data/bin/twig
CHANGED
@@ -2,13 +2,13 @@
|
|
2
2
|
|
3
3
|
require File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib', 'twig'))
|
4
4
|
|
5
|
-
twig = Twig.new
|
6
5
|
repo_required = ARGV != ['--help'] && ARGV != ['help'] && ARGV != ['--version']
|
7
6
|
|
8
|
-
if repo_required && !
|
7
|
+
if repo_required && !Twig.repo?
|
9
8
|
abort "Current directory is not a git repository."
|
10
9
|
end
|
11
10
|
|
12
11
|
# Gettin' twiggy wit' it.
|
12
|
+
twig = Twig.new
|
13
13
|
twig.read_config_file!
|
14
14
|
twig.read_cli_args!(ARGV)
|
data/bin/twig-gh-open
CHANGED
@@ -5,10 +5,11 @@
|
|
5
5
|
# Author: Ron DeVera <http://rondevera.com>
|
6
6
|
|
7
7
|
require 'rubygems'
|
8
|
-
require 'twig
|
8
|
+
require 'twig'
|
9
|
+
require 'launchy'
|
9
10
|
|
10
11
|
Twig::GithubRepo.new do |gh_repo|
|
11
12
|
url = "https://github.com/#{gh_repo.username}/#{gh_repo.repository}"
|
12
13
|
puts "GitHub URL: #{url}"
|
13
|
-
|
14
|
+
Launchy.open(url) rescue nil
|
14
15
|
end
|
data/bin/twig-gh-open-issue
CHANGED
@@ -10,6 +10,7 @@
|
|
10
10
|
|
11
11
|
require 'rubygems'
|
12
12
|
require 'twig'
|
13
|
+
require 'launchy'
|
13
14
|
|
14
15
|
Twig::GithubRepo.new do |gh_repo|
|
15
16
|
twig = Twig.new
|
@@ -25,5 +26,5 @@ Twig::GithubRepo.new do |gh_repo|
|
|
25
26
|
url << "/issues/#{issue_id}"
|
26
27
|
|
27
28
|
puts "GitHub issue URL: #{url}"
|
28
|
-
|
29
|
+
Launchy.open(url) rescue nil
|
29
30
|
end
|
data/lib/twig.rb
CHANGED
@@ -18,6 +18,11 @@ class Twig
|
|
18
18
|
`#{command}`.strip
|
19
19
|
end
|
20
20
|
|
21
|
+
def self.repo?
|
22
|
+
Twig.run('git rev-parse 2>&1')
|
23
|
+
$?.success?
|
24
|
+
end
|
25
|
+
|
21
26
|
def initialize
|
22
27
|
self.options = {}
|
23
28
|
|
@@ -25,14 +30,8 @@ class Twig
|
|
25
30
|
set_option(:header_style, DEFAULT_HEADER_COLOR.to_s)
|
26
31
|
end
|
27
32
|
|
28
|
-
def repo?
|
29
|
-
Twig.run('git rev-parse 2>&1')
|
30
|
-
$?.success?
|
31
|
-
end
|
32
|
-
|
33
33
|
def current_branch_name
|
34
|
-
@_current_branch_name ||=
|
35
|
-
Twig.run('git symbolic-ref -q HEAD').sub(%r{^#{ REF_PREFIX }}, '')
|
34
|
+
@_current_branch_name ||= Twig.run('git rev-parse --abbrev-ref HEAD')
|
36
35
|
end
|
37
36
|
|
38
37
|
def all_branches
|
@@ -84,8 +83,8 @@ class Twig
|
|
84
83
|
end
|
85
84
|
end
|
86
85
|
|
87
|
-
def
|
88
|
-
|
86
|
+
def all_branch_names
|
87
|
+
all_branches.map { |branch| branch.name }
|
89
88
|
end
|
90
89
|
|
91
90
|
|
@@ -103,9 +102,11 @@ class Twig
|
|
103
102
|
|
104
103
|
out = "\n" << branch_list_headers(options)
|
105
104
|
|
106
|
-
# List
|
107
|
-
listable_branches =
|
108
|
-
|
105
|
+
# List least recently modified branches first
|
106
|
+
listable_branches = branches.sort_by { |branch| branch.last_commit_time }
|
107
|
+
if options[:reverse] != true
|
108
|
+
listable_branches.reverse! # List most recently modified branches first
|
109
|
+
end
|
109
110
|
|
110
111
|
branch_lines = listable_branches.inject([]) do |result, branch|
|
111
112
|
result << branch_list_line(branch)
|
data/lib/twig/cli.rb
CHANGED
@@ -12,7 +12,7 @@ class Twig
|
|
12
12
|
for your Git branches.
|
13
13
|
})
|
14
14
|
|
15
|
-
<<-BANNER.gsub(/^[ ]+/, '')
|
15
|
+
intro = <<-BANNER.gsub(/^[ ]+/, '')
|
16
16
|
|
17
17
|
#{version_string}
|
18
18
|
#{'-' * version_string.size}
|
@@ -20,8 +20,9 @@ class Twig
|
|
20
20
|
#{intro}
|
21
21
|
|
22
22
|
#{Twig::HOMEPAGE}
|
23
|
-
|
24
23
|
BANNER
|
24
|
+
|
25
|
+
intro + ' ' # Force extra blank line
|
25
26
|
end
|
26
27
|
|
27
28
|
def help_separator(option_parser, text, options={})
|
@@ -54,11 +55,47 @@ class Twig
|
|
54
55
|
lines
|
55
56
|
end
|
56
57
|
|
58
|
+
def help_description_for_custom_property(option_parser, desc_lines)
|
59
|
+
indent = ' '
|
60
|
+
left_column_width = 29
|
61
|
+
|
62
|
+
help_desc = desc_lines.inject('') do |desc, (left_column, right_column)|
|
63
|
+
desc + indent +
|
64
|
+
sprintf("%-#{left_column_width}s", left_column) + right_column + "\n"
|
65
|
+
end
|
66
|
+
|
67
|
+
help_separator(option_parser, help_desc, :trailing => "\n")
|
68
|
+
end
|
69
|
+
|
57
70
|
def help_paragraph(text)
|
58
71
|
help_description(text, :width => 80).join("\n")
|
59
72
|
end
|
60
73
|
|
74
|
+
def help_line_for_custom_property?(line)
|
75
|
+
is_custom_property_except = (
|
76
|
+
line.include?('--except-') &&
|
77
|
+
!line.include?('--except-branch') &&
|
78
|
+
!line.include?('--except-PROPERTY')
|
79
|
+
)
|
80
|
+
is_custom_property_only = (
|
81
|
+
line.include?('--only-') &&
|
82
|
+
!line.include?('--only-branch') &&
|
83
|
+
!line.include?('--only-PROPERTY')
|
84
|
+
)
|
85
|
+
is_custom_property_width = (
|
86
|
+
line =~ /--.+-width/ &&
|
87
|
+
!line.include?('--branch-width') &&
|
88
|
+
!line.include?('--PROPERTY-width')
|
89
|
+
)
|
90
|
+
|
91
|
+
is_custom_property_except ||
|
92
|
+
is_custom_property_only ||
|
93
|
+
is_custom_property_width
|
94
|
+
end
|
95
|
+
|
61
96
|
def read_cli_options!(args)
|
97
|
+
custom_properties = Twig::Branch.all_properties
|
98
|
+
|
62
99
|
option_parser = OptionParser.new do |opts|
|
63
100
|
opts.banner = help_intro
|
64
101
|
opts.summary_indent = ' ' * 2
|
@@ -82,22 +119,17 @@ class Twig
|
|
82
119
|
|
83
120
|
desc = 'Show this help content.'
|
84
121
|
opts.on('--help', *help_description(desc)) do
|
85
|
-
summary_lines = opts.
|
122
|
+
summary_lines = opts.to_s.split("\n")
|
86
123
|
|
87
|
-
# Filter out
|
124
|
+
# Filter out custom property lines
|
125
|
+
prev_line = nil
|
88
126
|
summary_lines.each do |line|
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
)
|
94
|
-
is_custom_property_except = (
|
95
|
-
line.include?('--except-') &&
|
96
|
-
!line.include?('--except-branch') &&
|
97
|
-
!line.include?('--except-PROPERTY')
|
98
|
-
)
|
99
|
-
unless is_custom_property_only || is_custom_property_except
|
127
|
+
# Squash successive blank lines
|
128
|
+
next if line == "\n" && prev_line == "\n"
|
129
|
+
|
130
|
+
unless help_line_for_custom_property?(line)
|
100
131
|
puts line
|
132
|
+
prev_line = line
|
101
133
|
end
|
102
134
|
end
|
103
135
|
|
@@ -129,16 +161,10 @@ class Twig
|
|
129
161
|
end
|
130
162
|
|
131
163
|
desc = 'Do not list branches whose name matches a given pattern.'
|
132
|
-
opts.on(
|
133
|
-
'--except-branch PATTERN',
|
134
|
-
*help_description(desc, :add_separator => false)
|
135
|
-
# `:add_separator => false` skips the extra line generated by the
|
136
|
-
# custom property filters below.
|
137
|
-
) do |pattern|
|
164
|
+
opts.on('--except-branch PATTERN', *help_description(desc)) do |pattern|
|
138
165
|
set_option(:property_except, :branch => pattern)
|
139
166
|
end
|
140
167
|
|
141
|
-
custom_properties = Twig::Branch.all_properties
|
142
168
|
custom_properties.each do |property_name|
|
143
169
|
opts.on("--only-#{property_name} PATTERN") do |pattern|
|
144
170
|
set_option(:property_only, property_name.to_sym => pattern)
|
@@ -148,18 +174,14 @@ class Twig
|
|
148
174
|
set_option(:property_except, property_name.to_sym => pattern)
|
149
175
|
end
|
150
176
|
end
|
151
|
-
|
152
|
-
custom_properties_desc_lines = [
|
177
|
+
help_description_for_custom_property(opts, [
|
153
178
|
['--only-PROPERTY PATTERN', 'Only list branches with a given property'],
|
154
179
|
['', 'that matches a given pattern.'],
|
155
|
-
|
180
|
+
])
|
181
|
+
help_description_for_custom_property(opts, [
|
156
182
|
['--except-PROPERTY PATTERN', 'Do not list branches with a given property'],
|
157
183
|
['', 'that matches a given pattern.']
|
158
|
-
]
|
159
|
-
custom_properties_desc = custom_properties_desc_lines.inject('') do |desc, line_parts|
|
160
|
-
desc + sprintf(' %-29s', line_parts[0]) + line_parts[1] + "\n"
|
161
|
-
end
|
162
|
-
help_separator(opts, custom_properties_desc, :trailing => "\n")
|
184
|
+
])
|
163
185
|
|
164
186
|
desc =
|
165
187
|
'Lists all branches regardless of other filtering options. ' +
|
@@ -175,11 +197,25 @@ class Twig
|
|
175
197
|
|
176
198
|
help_separator(opts, 'Listing branches:')
|
177
199
|
|
200
|
+
desc = 'Set the width for the `branch` column.'
|
201
|
+
opts.on('--branch-width NUMBER', *help_description(desc)) do |width|
|
202
|
+
set_option(:property_width, :branch => width)
|
203
|
+
end
|
204
|
+
|
205
|
+
custom_properties.each do |property_name|
|
206
|
+
opts.on("--#{property_name}-width NUMBER") do |width|
|
207
|
+
set_option(:property_width, property_name.to_sym => width)
|
208
|
+
end
|
209
|
+
end
|
210
|
+
help_description_for_custom_property(opts, [
|
211
|
+
['--PROPERTY-width NUMBER', "Set the width for a given property's column."]
|
212
|
+
])
|
213
|
+
|
178
214
|
colors = Twig::Display::COLORS.keys.map do |value|
|
179
|
-
format_string(value,
|
215
|
+
format_string(value, :color => value)
|
180
216
|
end.join(', ')
|
181
217
|
weights = Twig::Display::WEIGHTS.keys.map do |value|
|
182
|
-
format_string(value,
|
218
|
+
format_string(value, :weight => value)
|
183
219
|
end.join(' and ')
|
184
220
|
default_color = format_string(
|
185
221
|
Twig::DEFAULT_HEADER_COLOR.to_s,
|
@@ -190,10 +226,18 @@ class Twig
|
|
190
226
|
Valid colors are #{colors}. Valid weights are #{weights}.
|
191
227
|
The default is "#{default_color}".
|
192
228
|
DESC
|
193
|
-
opts.on(
|
229
|
+
opts.on(
|
230
|
+
'--header-style "STYLE"',
|
231
|
+
*help_description(desc, :add_separator => true)
|
232
|
+
) do |style|
|
194
233
|
set_option(:header_style, style)
|
195
234
|
end
|
196
235
|
|
236
|
+
desc = 'Show oldest branches first.'
|
237
|
+
opts.on('--reverse', *help_description(desc)) do
|
238
|
+
set_option(:reverse, true)
|
239
|
+
end
|
240
|
+
|
197
241
|
|
198
242
|
|
199
243
|
help_separator(opts, help_paragraph(%{
|
data/lib/twig/display.rb
CHANGED
@@ -14,41 +14,58 @@ class Twig
|
|
14
14
|
:normal => 0,
|
15
15
|
:bold => 1
|
16
16
|
}
|
17
|
+
DEFAULT_PROPERTY_COLUMN_WIDTH = 16
|
18
|
+
DEFAULT_BRANCH_COLUMN_WIDTH = 48
|
17
19
|
CURRENT_BRANCH_INDICATOR = '* '
|
18
20
|
EMPTY_BRANCH_PROPERTY_INDICATOR = '-'
|
19
21
|
|
20
|
-
def column(string
|
22
|
+
def column(string, options = {})
|
21
23
|
# Returns `string` with an exact fixed width. If `string` is too wide, it
|
22
24
|
# is truncated with an ellipsis and a trailing space to separate columns.
|
23
25
|
#
|
24
|
-
# `
|
26
|
+
# `options`:
|
25
27
|
# - `:color`: `nil` by default. Accepts a key from `COLORS`.
|
26
28
|
# - `:weight`: `nil` by default. Accepts a key from `WEIGHTS`.
|
27
29
|
# - `:width`: 8 (characters) by default.
|
28
30
|
|
29
|
-
|
30
|
-
|
31
|
-
new_string
|
32
|
-
omission
|
31
|
+
string ||= ' '
|
32
|
+
width = options[:width] || 8
|
33
|
+
new_string = string[0, width]
|
34
|
+
omission = '...'
|
33
35
|
|
34
|
-
if string.size
|
36
|
+
if string.size > width
|
35
37
|
new_string[-omission.size, omission.size] = omission
|
36
38
|
else
|
37
|
-
new_string = ' ' *
|
39
|
+
new_string = ' ' * width
|
38
40
|
new_string[0, string.size] = string
|
39
41
|
end
|
40
42
|
|
41
43
|
new_string = format_string(
|
42
44
|
new_string,
|
43
|
-
|
45
|
+
options.reject { |k, v| ![:color, :weight].include?(k) }
|
44
46
|
)
|
45
47
|
|
46
48
|
new_string
|
47
49
|
end
|
48
50
|
|
51
|
+
def date_time_column_width; 35; end
|
52
|
+
def column_gutter; ' '; end
|
53
|
+
|
54
|
+
def property_column_width(property_name = nil)
|
55
|
+
if property_name && options[:property_width]
|
56
|
+
width = options[:property_width][property_name.to_sym]
|
57
|
+
end
|
58
|
+
|
59
|
+
if width
|
60
|
+
width
|
61
|
+
elsif property_name == :branch
|
62
|
+
Twig::Display::DEFAULT_BRANCH_COLUMN_WIDTH
|
63
|
+
else
|
64
|
+
Twig::Display::DEFAULT_PROPERTY_COLUMN_WIDTH
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
49
68
|
def branch_list_headers(header_options = {})
|
50
|
-
columns_for_date_time = 5
|
51
|
-
columns_per_property = 2
|
52
69
|
branch_indicator_padding = ' ' * CURRENT_BRANCH_INDICATOR.size
|
53
70
|
|
54
71
|
header_options.merge!(
|
@@ -62,22 +79,22 @@ class Twig
|
|
62
79
|
end
|
63
80
|
)
|
64
81
|
|
65
|
-
out =
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
out <<
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
82
|
+
out = column(' ', :width => date_time_column_width) << column_gutter
|
83
|
+
out << Twig::Branch.all_properties.map do |property|
|
84
|
+
width = property_column_width(property)
|
85
|
+
column(property, header_options.merge(:width => width)) << column_gutter
|
86
|
+
end.join
|
87
|
+
out << column(branch_indicator_padding + 'branch', header_options)
|
88
|
+
out << "\n"
|
89
|
+
|
90
|
+
out << column(' ', :width => date_time_column_width) << column_gutter
|
91
|
+
out << Twig::Branch.all_properties.map do |property|
|
92
|
+
width = property_column_width(property)
|
93
|
+
underline = '-' * property.size
|
94
|
+
column(underline, header_options.merge(:width => width)) << column_gutter
|
95
|
+
end.join
|
96
|
+
out << column(branch_indicator_padding + '------', header_options)
|
97
|
+
out << "\n"
|
81
98
|
|
82
99
|
out
|
83
100
|
end
|
@@ -87,24 +104,29 @@ class Twig
|
|
87
104
|
|
88
105
|
properties = Twig::Branch.all_properties.inject({}) do |result, property_name|
|
89
106
|
property = (get_branch_property(branch.name, property_name) || '').strip
|
90
|
-
property =
|
107
|
+
property = EMPTY_BRANCH_PROPERTY_INDICATOR if property.empty?
|
91
108
|
property.gsub!(/[\n\r]+/, ' ')
|
92
109
|
result.merge(property_name => property)
|
93
110
|
end
|
94
111
|
|
95
|
-
line = column(branch.last_commit_time.to_s,
|
112
|
+
line = column(branch.last_commit_time.to_s, :width => date_time_column_width)
|
113
|
+
line << column_gutter
|
96
114
|
|
97
115
|
line <<
|
98
116
|
Twig::Branch.all_properties.map do |property_name|
|
99
|
-
|
100
|
-
|
117
|
+
property_value = properties[property_name] || ''
|
118
|
+
width = property_column_width(property_name)
|
119
|
+
column(property_value, :width => width) << column_gutter
|
101
120
|
end.join
|
102
121
|
|
122
|
+
branch_column_width = property_column_width(:branch)
|
123
|
+
branch_column = column(branch.to_s, :width => branch_column_width)
|
124
|
+
branch_column.strip! # Strip final column
|
103
125
|
line <<
|
104
126
|
if is_current_branch
|
105
|
-
CURRENT_BRANCH_INDICATOR +
|
127
|
+
CURRENT_BRANCH_INDICATOR + branch_column
|
106
128
|
else
|
107
|
-
(' ' * CURRENT_BRANCH_INDICATOR.size) +
|
129
|
+
(' ' * CURRENT_BRANCH_INDICATOR.size) + branch_column
|
108
130
|
end
|
109
131
|
|
110
132
|
line = format_string(line, :weight => :bold) if is_current_branch
|