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/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
@@ -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
@@ -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
@@ -0,0 +1,9 @@
1
+ class Twig
2
+ module Util
3
+
4
+ def self.numeric?(value)
5
+ !!Float(value) rescue false
6
+ end
7
+
8
+ end
9
+ end
@@ -0,0 +1,3 @@
1
+ class Twig
2
+ VERSION = '1.0.0'
3
+ end
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
@@ -0,0 +1 @@
1
+ require 'twig'