twig 1.0.0
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/.gitignore +3 -0
- data/.rvmrc +1 -0
- data/CONTRIBUTING.md +26 -0
- data/Gemfile +2 -0
- data/HISTORY.md +6 -0
- data/LICENSE.md +22 -0
- data/README.md +232 -0
- data/Rakefile +10 -0
- data/bin/twig +10 -0
- data/bin/twig-gh-open +41 -0
- data/bin/twig-gh-update +93 -0
- data/bin/twig-help +4 -0
- data/install +18 -0
- data/lib/twig/branch.rb +72 -0
- data/lib/twig/cli.rb +177 -0
- data/lib/twig/commit_time.rb +32 -0
- data/lib/twig/display.rb +116 -0
- data/lib/twig/options.rb +55 -0
- data/lib/twig/util.rb +9 -0
- data/lib/twig/version.rb +3 -0
- data/lib/twig.rb +119 -0
- data/spec/spec_helper.rb +1 -0
- data/spec/twig/branch_spec.rb +232 -0
- data/spec/twig/cli_spec.rb +291 -0
- data/spec/twig/commit_time_spec.rb +47 -0
- data/spec/twig/display_spec.rb +117 -0
- data/spec/twig/options_spec.rb +126 -0
- data/spec/twig/util_spec.rb +16 -0
- data/spec/twig_spec.rb +220 -0
- data/twig.gemspec +38 -0
- metadata +166 -0
data/lib/twig/cli.rb
ADDED
@@ -0,0 +1,177 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
|
3
|
+
class Twig
|
4
|
+
module Cli
|
5
|
+
|
6
|
+
def help_intro
|
7
|
+
version_string = "Twig v#{Twig::VERSION}"
|
8
|
+
|
9
|
+
<<-BANNER.gsub(/^[ ]+/, '')
|
10
|
+
|
11
|
+
#{version_string}
|
12
|
+
#{'-' * version_string.size}
|
13
|
+
|
14
|
+
Twig is your personal Git branch assistant. It shows you your most
|
15
|
+
recent branches, and tracks issue tracker ids, tasks, and other metadata
|
16
|
+
for your Git branches.
|
17
|
+
|
18
|
+
https://rondevera.github.com/twig
|
19
|
+
|
20
|
+
BANNER
|
21
|
+
end
|
22
|
+
|
23
|
+
def help_separator(option_parser, text)
|
24
|
+
option_parser.separator "\n#{text}\n\n"
|
25
|
+
end
|
26
|
+
|
27
|
+
def help_description(text, options={})
|
28
|
+
width = options[:width] || 40
|
29
|
+
text = text.dup
|
30
|
+
|
31
|
+
# Split into lines
|
32
|
+
lines = []
|
33
|
+
until text.empty?
|
34
|
+
if text.size > width
|
35
|
+
split_index = text[0..width].rindex(' ') || width
|
36
|
+
lines << text.slice!(0, split_index)
|
37
|
+
text.strip!
|
38
|
+
else
|
39
|
+
lines << text.slice!(0..-1)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
lines << ' ' if options[:add_separator]
|
44
|
+
|
45
|
+
lines
|
46
|
+
end
|
47
|
+
|
48
|
+
def read_cli_options!(args)
|
49
|
+
option_parser = OptionParser.new do |opts|
|
50
|
+
opts.banner = help_intro
|
51
|
+
opts.summary_indent = ' ' * 2
|
52
|
+
opts.summary_width = 32
|
53
|
+
|
54
|
+
|
55
|
+
|
56
|
+
help_separator(opts, 'Common options:')
|
57
|
+
|
58
|
+
desc = 'Use a specific branch.'
|
59
|
+
opts.on(
|
60
|
+
'-b BRANCH', '--branch BRANCH', *help_description(desc)
|
61
|
+
) do |branch|
|
62
|
+
set_option(:branch, branch)
|
63
|
+
end
|
64
|
+
|
65
|
+
desc = 'Unset a branch property.'
|
66
|
+
opts.on('--unset PROPERTY', *help_description(desc)) do |property_name|
|
67
|
+
set_option(:unset_property, property_name)
|
68
|
+
end
|
69
|
+
|
70
|
+
desc = 'Show this help content.'
|
71
|
+
opts.on('--help', *help_description(desc)) do
|
72
|
+
puts opts; exit
|
73
|
+
end
|
74
|
+
|
75
|
+
desc = 'Show Twig version.'
|
76
|
+
opts.on('--version', *help_description(desc)) do
|
77
|
+
puts Twig::VERSION; exit
|
78
|
+
end
|
79
|
+
|
80
|
+
|
81
|
+
|
82
|
+
help_separator(opts, 'Filtering branches:')
|
83
|
+
|
84
|
+
desc = 'Only list branches whose name matches a given pattern.'
|
85
|
+
opts.on(
|
86
|
+
'--only-branch PATTERN',
|
87
|
+
*help_description(desc, :add_separator => true)
|
88
|
+
) do |pattern|
|
89
|
+
set_option(:branch_only, pattern)
|
90
|
+
end
|
91
|
+
|
92
|
+
desc = 'Do not list branches whose name matches a given pattern.'
|
93
|
+
opts.on(
|
94
|
+
'--except-branch PATTERN',
|
95
|
+
*help_description(desc, :add_separator => true)
|
96
|
+
) do |pattern|
|
97
|
+
set_option(:branch_except, pattern)
|
98
|
+
end
|
99
|
+
|
100
|
+
desc = 'Only list branches below a given age.'
|
101
|
+
opts.on(
|
102
|
+
'--max-days-old AGE', *help_description(desc, :add_separator => true)
|
103
|
+
) do |age|
|
104
|
+
set_option(:max_days_old, age)
|
105
|
+
end
|
106
|
+
|
107
|
+
desc =
|
108
|
+
'Lists all branches regardless of age or name options. ' +
|
109
|
+
'Useful for overriding options in ' +
|
110
|
+
File.basename(Twig::Options::CONFIG_FILE) + '.'
|
111
|
+
opts.on('--all', *help_description(desc)) do |pattern|
|
112
|
+
unset_option(:max_days_old)
|
113
|
+
unset_option(:branch_except)
|
114
|
+
unset_option(:branch_only)
|
115
|
+
end
|
116
|
+
|
117
|
+
help_separator(opts, [
|
118
|
+
'You can put your most frequently used branch filtering options in',
|
119
|
+
"#{Twig::Options::CONFIG_FILE}. For example:",
|
120
|
+
'',
|
121
|
+
' except-branch: staging',
|
122
|
+
' max-days-old: 30'
|
123
|
+
].join("\n"))
|
124
|
+
end
|
125
|
+
|
126
|
+
option_parser.parse!(args)
|
127
|
+
rescue OptionParser::InvalidOption, OptionParser::MissingArgument => exception
|
128
|
+
puts exception.to_s
|
129
|
+
puts 'For a list of options, run `twig --help`.'
|
130
|
+
exit
|
131
|
+
end
|
132
|
+
|
133
|
+
def read_cli_args!(args)
|
134
|
+
if args.any?
|
135
|
+
# Run subcommand binary, if any, and exit here
|
136
|
+
possible_subcommand_name = args[0]
|
137
|
+
command_path = Twig.run("which twig-#{possible_subcommand_name}")
|
138
|
+
unless command_path.empty?
|
139
|
+
command = ([command_path] + args[1..-1]).join(' ')
|
140
|
+
exec(command)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
read_cli_options!(args)
|
145
|
+
branch_name = options[:branch] || current_branch_name
|
146
|
+
property_to_unset = options.delete(:unset_property)
|
147
|
+
|
148
|
+
# Handle remaining arguments, if any
|
149
|
+
if args.any?
|
150
|
+
property_name, property_value = args[0], args[1]
|
151
|
+
|
152
|
+
read_cli_options!(args)
|
153
|
+
|
154
|
+
# Get/set branch property
|
155
|
+
if property_value
|
156
|
+
# `$ twig <key> <value>`
|
157
|
+
puts set_branch_property(branch_name, property_name, property_value)
|
158
|
+
else
|
159
|
+
# `$ twig <key>`
|
160
|
+
value = get_branch_property(branch_name, property_name)
|
161
|
+
if value && !value.empty?
|
162
|
+
puts value
|
163
|
+
else
|
164
|
+
puts %{The branch "#{branch_name}" does not have the property "#{property_name}".}
|
165
|
+
end
|
166
|
+
end
|
167
|
+
elsif property_to_unset
|
168
|
+
# `$ twig --unset <key>`
|
169
|
+
puts unset_branch_property(branch_name, property_to_unset)
|
170
|
+
else
|
171
|
+
# `$ twig`
|
172
|
+
puts list_branches
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
end
|
177
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
class Twig
|
2
|
+
class CommitTime
|
3
|
+
|
4
|
+
def initialize(time, time_ago)
|
5
|
+
@time = time
|
6
|
+
|
7
|
+
# Shorten relative time
|
8
|
+
@time_ago = time_ago.
|
9
|
+
sub(' years', 'y').
|
10
|
+
sub(' months', 'mo').
|
11
|
+
sub(' weeks', 'w').
|
12
|
+
sub(' days', 'd').
|
13
|
+
sub(' hours', 'h').
|
14
|
+
sub(' minutes', 'm').
|
15
|
+
sub(' seconds', 's')
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_i
|
19
|
+
@time.to_i
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_s
|
23
|
+
time_string = @time.strftime('%F %R %z')
|
24
|
+
"#{time_string} (#{@time_ago})"
|
25
|
+
end
|
26
|
+
|
27
|
+
def <=>(other)
|
28
|
+
to_i <=> other.to_i
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
data/lib/twig/display.rb
ADDED
@@ -0,0 +1,116 @@
|
|
1
|
+
class Twig
|
2
|
+
module Display
|
3
|
+
COLORS = {
|
4
|
+
:black => 30,
|
5
|
+
:red => 31,
|
6
|
+
:green => 32,
|
7
|
+
:yellow => 33,
|
8
|
+
:blue => 34,
|
9
|
+
:purple => 35,
|
10
|
+
:cyan => 36,
|
11
|
+
:white => 37
|
12
|
+
}
|
13
|
+
WEIGHTS = {
|
14
|
+
:normal => 0,
|
15
|
+
:bold => 1
|
16
|
+
}
|
17
|
+
CURRENT_BRANCH_INDICATOR = '* '
|
18
|
+
EMPTY_BRANCH_PROPERTY_INDICATOR = '-'
|
19
|
+
|
20
|
+
def column(string = ' ', num_columns = 1, column_options = {})
|
21
|
+
# Returns `string` with an exact fixed width. If `string` is too wide, it
|
22
|
+
# is truncated with an ellipsis and a trailing space to separate columns.
|
23
|
+
#
|
24
|
+
# `column_options`:
|
25
|
+
# - `:color`: `nil` by default. Accepts a key from `COLORS`.
|
26
|
+
# - `:weight`: `nil` by default. Accepts a key from `WEIGHTS`.
|
27
|
+
# - `:width`: 8 (characters) by default.
|
28
|
+
|
29
|
+
width_per_column = column_options[:width] || 8
|
30
|
+
total_width = num_columns * width_per_column
|
31
|
+
new_string = string[0, total_width]
|
32
|
+
omission = '... '
|
33
|
+
|
34
|
+
if string.size >= total_width
|
35
|
+
new_string[-omission.size, omission.size] = omission
|
36
|
+
else
|
37
|
+
new_string = ' ' * total_width
|
38
|
+
new_string[0, string.size] = string
|
39
|
+
end
|
40
|
+
|
41
|
+
new_string = format_string(
|
42
|
+
new_string,
|
43
|
+
column_options.reject { |k, v| ![:color, :weight].include?(k) }
|
44
|
+
)
|
45
|
+
|
46
|
+
new_string
|
47
|
+
end
|
48
|
+
|
49
|
+
def branch_list_headers(header_options = { :color => :blue })
|
50
|
+
columns_for_date_time = 5
|
51
|
+
columns_per_property = 2
|
52
|
+
branch_indicator_padding = ' ' * CURRENT_BRANCH_INDICATOR.size
|
53
|
+
|
54
|
+
out =
|
55
|
+
column(' ', columns_for_date_time) <<
|
56
|
+
Twig::Branch.all_properties.map do |property|
|
57
|
+
column(property, columns_per_property, header_options)
|
58
|
+
end.join <<
|
59
|
+
column(branch_indicator_padding + 'branch',
|
60
|
+
columns_per_property, header_options) <<
|
61
|
+
"\n"
|
62
|
+
out <<
|
63
|
+
column(' ', columns_for_date_time) <<
|
64
|
+
Twig::Branch.all_properties.map do |property|
|
65
|
+
column('-' * property.size, columns_per_property, header_options)
|
66
|
+
end.join <<
|
67
|
+
column(branch_indicator_padding + '------',
|
68
|
+
columns_per_property, header_options) <<
|
69
|
+
"\n"
|
70
|
+
|
71
|
+
out
|
72
|
+
end
|
73
|
+
|
74
|
+
def branch_list_line(branch)
|
75
|
+
is_current_branch = branch.name == current_branch_name
|
76
|
+
|
77
|
+
properties = Twig::Branch.all_properties.inject({}) do |result, property_name|
|
78
|
+
property = get_branch_property(branch.name, property_name).strip
|
79
|
+
property = column(EMPTY_BRANCH_PROPERTY_INDICATOR) if property.empty?
|
80
|
+
result.merge(property_name => property)
|
81
|
+
end
|
82
|
+
|
83
|
+
line = column(branch.last_commit_time.to_s, 5)
|
84
|
+
|
85
|
+
line <<
|
86
|
+
Twig::Branch.all_properties.map do |property_name|
|
87
|
+
property = properties[property_name] || ''
|
88
|
+
column(property, 2)
|
89
|
+
end.join
|
90
|
+
|
91
|
+
line <<
|
92
|
+
if is_current_branch
|
93
|
+
CURRENT_BRANCH_INDICATOR + branch.to_s
|
94
|
+
else
|
95
|
+
(' ' * CURRENT_BRANCH_INDICATOR.size) + branch.to_s
|
96
|
+
end
|
97
|
+
|
98
|
+
line = format_string(line, :weight => :bold) if is_current_branch
|
99
|
+
|
100
|
+
line
|
101
|
+
end
|
102
|
+
|
103
|
+
def format_string(string, options)
|
104
|
+
# Options:
|
105
|
+
# - `:color`: `nil` by default. Accepts a key from `COLORS`.
|
106
|
+
# - `:weight`: `nil` by default. Accepts a key from `WEIGHTS`.
|
107
|
+
|
108
|
+
string_options = []
|
109
|
+
string_options << COLORS[options[:color]] if options[:color]
|
110
|
+
string_options << WEIGHTS[options[:weight]] if options[:weight]
|
111
|
+
return string if string_options.empty?
|
112
|
+
|
113
|
+
"\033[#{string_options.join(';')}m#{string}\033[0m"
|
114
|
+
end
|
115
|
+
end # module Display
|
116
|
+
end
|
data/lib/twig/options.rb
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
class Twig
|
2
|
+
module Options
|
3
|
+
|
4
|
+
CONFIG_FILE = '~/.twigrc'
|
5
|
+
|
6
|
+
def read_config_file!
|
7
|
+
config_file_path = File.expand_path(Twig::CONFIG_FILE)
|
8
|
+
return unless File.readable?(config_file_path)
|
9
|
+
|
10
|
+
File.open(config_file_path) do |f|
|
11
|
+
opts = f.read.split("\n").inject({}) do |hsh, opt|
|
12
|
+
key, value = opt.split(':', 2)
|
13
|
+
hsh.merge(key.strip => value.strip)
|
14
|
+
end
|
15
|
+
|
16
|
+
opts.each do |key, value|
|
17
|
+
case key
|
18
|
+
when 'branch' then set_option(:branch, value)
|
19
|
+
when 'except-branch' then set_option(:branch_except, value)
|
20
|
+
when 'only-branch' then set_option(:branch_only, value)
|
21
|
+
when 'max-days-old' then set_option(:max_days_old, value)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def set_option(key, value)
|
28
|
+
case key
|
29
|
+
when :branch
|
30
|
+
if branch_names.include?(value)
|
31
|
+
options[:branch] = value
|
32
|
+
else
|
33
|
+
abort %{The branch "#{value}" could not be found.}
|
34
|
+
end
|
35
|
+
when :branch_except
|
36
|
+
options[:branch_except] = Regexp.new(value)
|
37
|
+
when :branch_only
|
38
|
+
options[:branch_only] = Regexp.new(value)
|
39
|
+
when :max_days_old
|
40
|
+
if Twig::Util.numeric?(value)
|
41
|
+
options[:max_days_old] = value.to_f
|
42
|
+
else
|
43
|
+
abort %{The value `--max-days-old=#{value}` is invalid.}
|
44
|
+
end
|
45
|
+
when :unset_property
|
46
|
+
options[:unset_property] = value
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def unset_option(key)
|
51
|
+
options.delete(key)
|
52
|
+
end
|
53
|
+
|
54
|
+
end # module Options
|
55
|
+
end
|
data/lib/twig/util.rb
ADDED
data/lib/twig/version.rb
ADDED
data/lib/twig.rb
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
Dir[File.join(File.dirname(__FILE__), 'twig', '*')].each { |file| require file }
|
2
|
+
require 'time'
|
3
|
+
|
4
|
+
class Twig
|
5
|
+
include Cli
|
6
|
+
include Display
|
7
|
+
include Options
|
8
|
+
|
9
|
+
attr_accessor :options
|
10
|
+
|
11
|
+
REF_FORMAT_SEPARATOR = ','
|
12
|
+
REF_FORMAT = %w[refname committerdate committerdate:relative].
|
13
|
+
map { |field| '%(' + field + ')' }.join(REF_FORMAT_SEPARATOR)
|
14
|
+
REF_PREFIX = 'refs/heads/'
|
15
|
+
|
16
|
+
def self.run(command)
|
17
|
+
`#{command}`.strip
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize(options = {})
|
21
|
+
# Options:
|
22
|
+
# - :branch_except (Regexp)
|
23
|
+
# - :branch_only (Regexp)
|
24
|
+
# - :max_days_old (integer)
|
25
|
+
|
26
|
+
self.options = options
|
27
|
+
end
|
28
|
+
|
29
|
+
def repo?
|
30
|
+
Twig.run('git rev-parse')
|
31
|
+
$?.success?
|
32
|
+
end
|
33
|
+
|
34
|
+
def current_branch_name
|
35
|
+
@_current_branch_name ||=
|
36
|
+
Twig.run('git symbolic-ref -q HEAD').sub(%r{^#{ REF_PREFIX }}, '')
|
37
|
+
end
|
38
|
+
|
39
|
+
def all_branches
|
40
|
+
@_all_branches ||= begin
|
41
|
+
branch_tuples = Twig.
|
42
|
+
run(%{git for-each-ref #{ REF_PREFIX } --format="#{ REF_FORMAT }"}).
|
43
|
+
split("\n")
|
44
|
+
|
45
|
+
branch_tuples.inject([]) do |result, branch_tuple|
|
46
|
+
ref, time_string, time_ago = branch_tuple.split(REF_FORMAT_SEPARATOR)
|
47
|
+
name = ref.sub(%r{^#{ REF_PREFIX }}, '')
|
48
|
+
time = Time.parse(time_string)
|
49
|
+
commit_time = Twig::CommitTime.new(time, time_ago)
|
50
|
+
branch = Branch.new(name, :last_commit_time => commit_time)
|
51
|
+
result << branch
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def branches
|
57
|
+
branches = all_branches
|
58
|
+
now = Time.now
|
59
|
+
max_seconds_old = options[:max_days_old] * 86400 if options[:max_days_old]
|
60
|
+
|
61
|
+
branches.select do |branch|
|
62
|
+
if max_seconds_old
|
63
|
+
seconds_old = now.to_i - branch.last_commit_time.to_i
|
64
|
+
next if seconds_old > max_seconds_old
|
65
|
+
end
|
66
|
+
|
67
|
+
next if options[:branch_except] && branch.name =~ options[:branch_except]
|
68
|
+
next if options[:branch_only] && branch.name !~ options[:branch_only]
|
69
|
+
|
70
|
+
true
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def branch_names
|
75
|
+
branches.map { |branch| branch.name }
|
76
|
+
end
|
77
|
+
|
78
|
+
|
79
|
+
|
80
|
+
### Actions ###
|
81
|
+
|
82
|
+
def list_branches
|
83
|
+
if branches.empty?
|
84
|
+
if all_branches.any?
|
85
|
+
return 'There are no branches matching your selected options.'
|
86
|
+
else
|
87
|
+
return 'This repository has no branches.'
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
out = "\n" << branch_list_headers
|
92
|
+
|
93
|
+
# List most recently modified branches first
|
94
|
+
listable_branches =
|
95
|
+
branches.sort_by { |branch| branch.last_commit_time }.reverse
|
96
|
+
|
97
|
+
branch_lines = listable_branches.inject([]) do |result, branch|
|
98
|
+
result << branch_list_line(branch)
|
99
|
+
end
|
100
|
+
|
101
|
+
out << branch_lines.join("\n")
|
102
|
+
end
|
103
|
+
|
104
|
+
def get_branch_property(branch_name, property_name)
|
105
|
+
branch = Branch.new(branch_name)
|
106
|
+
branch.get_property(property_name)
|
107
|
+
end
|
108
|
+
|
109
|
+
def set_branch_property(branch_name, property_name, value)
|
110
|
+
branch = Branch.new(branch_name)
|
111
|
+
branch.set_property(property_name, value)
|
112
|
+
end
|
113
|
+
|
114
|
+
def unset_branch_property(branch_name, property_name)
|
115
|
+
branch = Branch.new(branch_name)
|
116
|
+
branch.unset_property(property_name)
|
117
|
+
end
|
118
|
+
|
119
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'twig'
|