twig 1.0.1 → 1.1

Sign up to get free protection for your applications and to get access to all the features.
data/CONTRIBUTING.md CHANGED
@@ -8,18 +8,24 @@ Found a bug or have a suggestion? [Please open an issue][issues] or ping
8
8
 
9
9
  If you want to hack on some code, even better! Here are the basics:
10
10
 
11
- 1. Fork the Twig repo.
12
- 2. Check out the [**`development`** branch][dev branch]; the `master` branch is
11
+ 1. If you plan to work on a large feature or bug fix, first
12
+ [open an issue][issues] first to discuss whether you're on the right track.
13
+ If you're working on something small, go right ahead.
14
+ 2. Fork the Twig repo.
15
+ 3. Check out the [**`development`** branch][dev branch]; the `master` branch is
13
16
  for stable builds only.
14
- 3. Run the tests to make sure that they pass on your machine: `bundle && rake`
15
- 4. Add one or more failing tests for your feature or bug fix.
16
- 5. Write your feature or bug fix to make the test(s) pass. Tests should pass in
17
- the latest **Ruby 1.8.7** and **Ruby 1.9.3**, which you can do with
18
- [rvm][rvm] or [rbenv][rbenv].
19
- 6. Test the change manually:
17
+ 4. Run the tests to make sure that they pass on your machine: `bundle && rake`
18
+ 5. Add one or more failing tests for your feature or bug fix.
19
+ 6. Write your feature or bug fix to make the test(s) pass.
20
+ * Tests should pass in the latest **Ruby 1.8.7** and **Ruby 1.9.3**, which
21
+ you can do with [rvm][rvm] or [rbenv][rbenv].
22
+ * Keep the branch focused on a single topic, rather than covering multiple
23
+ features or bug fixes in a single branch. This makes branches quicker to
24
+ review and merge.
25
+ 7. Test the change manually:
20
26
  1. `gem build twig.gemspec`
21
27
  2. `gem install twig-x.y.z.gem` (fill in the current version number)
22
- 7. Push to your fork and submit a pull request.
28
+ 8. Push to your fork and submit a pull request.
23
29
 
24
30
  Thanks for contributing!
25
31
 
data/HISTORY.md CHANGED
@@ -1,6 +1,22 @@
1
1
  Twig
2
2
  ====
3
3
 
4
+ 1.1 (2013-03-06)
5
+ ----------------
6
+ * ENHANCEMENT: Add branch name tab completion for `-b` and `--branch` options.
7
+ (GH-12)
8
+ * ENHANCEMENT: Add `--header-style` option for changing the column headers'
9
+ colors and weights. (GH-11. Thanks [tsujigiri](https://github.com/tsujigiri)!)
10
+ * ENHANCEMENT: Add `twig gh-open-issue` for opening a branch's GitHub issue, if
11
+ any, in a browser.
12
+ * FIX: Make `branch` a reserved property name, along with `merge`, `rebase`, and
13
+ `remote`.
14
+ * FIX: Handle line breaks gracefully in `~/.twigrc` config file.
15
+ * FIX: Exit with a non-zero status when trying to get or unset a branch property
16
+ that isn't set, or trying to set a branch property to an invalid value.
17
+ * FIX : Don't allow getting/setting/unsetting a branch property whose name is an
18
+ empty string.
19
+
4
20
  1.0.1 (2013-02-13)
5
21
  ------------------
6
22
  * ENHANCEMENT: Add Travis CI integration for running tests in multiple versions
data/README.md CHANGED
@@ -43,6 +43,8 @@ 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
+ * `twig init-completion`: Set up tab completion for `-b` and `--branch`
46
48
  * `twig --help`: More info
47
49
 
48
50
 
@@ -66,6 +68,7 @@ automatically included when you run `twig`. Example:
66
68
 
67
69
  # ~/.twigrc:
68
70
  except-branch: staging
71
+ header-style: green bold
69
72
  max-days-old: 30
70
73
 
71
74
 
@@ -137,8 +140,8 @@ You can set just about any custom property you need to remember for each branch.
137
140
  Subcommands
138
141
  ===========
139
142
 
140
- Twig comes with two subcommands, `gh-open` and `gh-update`, which are handy for
141
- use with GitHub repositories.
143
+ Twig comes with a few subcommands that are handy for use with GitHub
144
+ repositories: `gh-open`, `gh-open-issue`, and `gh-update`.
142
145
 
143
146
  While inside a Git repo, run `twig gh-open` to see the repo's GitHub URL, and open
144
147
  a browser window if possible:
@@ -149,8 +152,8 @@ a browser window if possible:
149
152
  GitHub URL: https://github.com/myname/myproject
150
153
  # Also opens a browser window (OS X only).
151
154
 
152
- If you're working on an issue for a GitHub repository, you can also use the
153
- `gh-update` subcommand:
155
+ If you're working on an issue for a GitHub repository, the `gh-update`
156
+ subcommand syncs issue statuses with GitHub:
154
157
 
155
158
  $ git checkout add-feature
156
159
  Switched to branch 'add-feature'.
@@ -180,6 +183,18 @@ If you're working on an issue for a GitHub repository, you can also use the
180
183
 
181
184
  Run `twig gh-update` periodically to keep up with GitHub issues locally.
182
185
 
186
+ For any branch that has an `issue` property, you can use the `gh-open-issue`
187
+ subcommand to view that issue on GitHub:
188
+
189
+ # Current branch:
190
+ $ twig gh-open-issue
191
+ GitHub issue URL: https://github.com/myname/myproject/issues/111
192
+ # Also opens a browser window (OS X only).
193
+
194
+ # Any branch:
195
+ $ twig gh-open-issue -b <branch name>
196
+ GitHub issue URL: https://github.com/myname/myproject/issues/222
197
+
183
198
  You can write any Twig subcommand that fits your own Git workflow. To write a
184
199
  Twig subcommand:
185
200
 
@@ -204,6 +219,7 @@ Some ideas for subcommands:
204
219
  they've shipped.
205
220
  * Generate a formatted list of your branches from the past week. Useful for
206
221
  emailing your team about what you're up to.
222
+ * Create a gem that contains your team's favorite custom Twig subcommands.
207
223
 
208
224
  If you write a subcommand that others can appreciate, send a pull request or add
209
225
  it to the [Twig wiki][wiki]!
data/bin/twig CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require File.join(File.dirname(__FILE__), '..', 'lib', 'twig')
3
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib', 'twig'))
4
4
 
5
5
  twig = Twig.new
6
6
  abort unless twig.repo?
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Usage:
4
+ # - `twig gh-open-issue`:
5
+ # Opens a browser window for the GitHub issue, if any, for the current branch.
6
+ # - `twig gh-open-issue -b <branch>`:
7
+ # Opens the GitHub issue, if any, for the specified branch.
8
+ #
9
+ # Author: Ron DeVera <http://rondevera.com>
10
+
11
+ require 'rubygems'
12
+ require 'twig'
13
+
14
+ class TwigGithubRepo
15
+ def initialize
16
+ if origin_url.empty? || username.empty? || repository.empty?
17
+ abort_for_non_github_repo
18
+ end
19
+
20
+ yield(self)
21
+ end
22
+
23
+ def origin_url
24
+ @origin_url ||= `git config remote.origin.url`.strip
25
+ end
26
+
27
+ def origin_url_parts
28
+ @origin_url_parts ||= origin_url.split(/[\/:]/)
29
+ end
30
+
31
+ def username
32
+ @username ||= origin_url_parts[-2] || ''
33
+ end
34
+
35
+ def repository
36
+ @repo ||= origin_url_parts[-1].sub(/\.git$/, '') || ''
37
+ end
38
+
39
+ def abort_for_non_github_repo
40
+ abort 'This does not appear to be a GitHub repository.'
41
+ end
42
+ end
43
+
44
+ TwigGithubRepo.new do |gh_repo|
45
+ twig = Twig.new
46
+ twig.read_cli_options!(ARGV)
47
+ branch_name = twig.options[:branch] || twig.current_branch_name
48
+ issue_id = twig.get_branch_property(branch_name, 'issue')
49
+
50
+ unless issue_id
51
+ abort %{The branch "#{branch_name}" doesn't have an "issue" property.}
52
+ end
53
+
54
+ url = "https://github.com/#{gh_repo.username}/#{gh_repo.repository}"
55
+ url << "/issues/#{issue_id}"
56
+
57
+ puts "GitHub issue URL: #{url}"
58
+ `which open && open #{url}`
59
+ end
data/bin/twig-help CHANGED
@@ -1,4 +1,7 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  # Your friendly neighborhood `twig --help` alias.
4
+ #
5
+ # Author: Ron DeVera <http://rondevera.com>
6
+
4
7
  puts `twig --help`
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Initializes tab completion for Twig. To use this, run
4
+ # `twig init-completion` and follow the instructions.
5
+ #
6
+ # Author: Ron DeVera <http://rondevera.com>
7
+
8
+ debug = ARGV.include?('--debug')
9
+ bash_version = `bash -c 'echo $BASH_VERSION'`.strip
10
+
11
+ if debug
12
+ puts "- Ruby version: #{`ruby --version`.strip}"
13
+ puts "- bash_version: #{bash_version.inspect}"
14
+ end
15
+
16
+ exec('twig-init-completion-bash') if bash_version != ''
17
+ abort 'Could not initialize Twig tab completion for this shell.'
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Initializes bash tab completion for Twig. To use this, run
4
+ # `twig init-completion` and follow the instructions.
5
+ #
6
+ # Author: Ron DeVera <http://rondevera.com>
7
+
8
+ require 'fileutils'
9
+
10
+ version = `twig --version`.strip
11
+ script = <<-SCRIPT
12
+
13
+ #!/usr/bin/env bash
14
+
15
+ # AUTO-GENERATED with Twig v#{version}. Regenerate with `twig init-completion`.
16
+ #
17
+ # Initializes bash tab completion for Twig. To use this, run
18
+ # `twig init-completion` and follow the instructions.
19
+ #
20
+ # Author: Ron DeVera <http://rondevera.com>
21
+
22
+ __twig_branches() {
23
+ local current words
24
+ current="${COMP_WORDS[COMP_CWORD]}"
25
+ words="$(git for-each-ref refs/heads/ --format="%(refname:short)" | tr '\n' ' ')"
26
+ COMPREPLY=($(compgen -W "$words" -- "$current"))
27
+ return 0
28
+ }
29
+
30
+ __twig() {
31
+ if [ -z "$(git rev-parse HEAD 2>/dev/null)" ]; then
32
+ return 0;
33
+ fi
34
+
35
+ local previous=${COMP_WORDS[COMP_CWORD-1]}
36
+
37
+ case "${previous}" in
38
+ -b|--branch) __twig_branches ;;
39
+ esac
40
+
41
+ return 0
42
+ }
43
+
44
+ complete -F __twig twig
45
+
46
+ SCRIPT
47
+ script = script.strip + "\n"
48
+
49
+
50
+
51
+ twig_dir = '~/.twig'
52
+ FileUtils.mkdir_p(File.expand_path(twig_dir))
53
+
54
+ script_path = File.join(twig_dir, '/twig-completion.bash')
55
+ full_script_path = File.expand_path(script_path)
56
+ unless File.exists?(full_script_path)
57
+ File.open(full_script_path, 'w') do |file|
58
+ file.write script
59
+ end
60
+ puts "Created #{script_path}."
61
+ end
62
+
63
+ puts
64
+ puts 'To enable tab completion for Twig, add the following to your `~/.bashrc`'
65
+ puts 'or equivalent:'
66
+ puts
67
+ puts " [[ -s #{script_path} ]] && source #{script_path}"
68
+ puts
69
+ puts 'To finish setup, open a new command-line window or run `source ~/.bashrc`.'
data/lib/twig.rb CHANGED
@@ -1,4 +1,4 @@
1
- Dir[File.join(File.dirname(__FILE__), 'twig', '*')].each { |file| require file }
1
+ Dir[File.join(File.dirname(__FILE__), 'twig', '*.rb')].each { |file| require file }
2
2
  require 'time'
3
3
 
4
4
  class Twig
@@ -9,21 +9,20 @@ class Twig
9
9
  attr_accessor :options
10
10
 
11
11
  REF_FORMAT_SEPARATOR = ','
12
- REF_FORMAT = %w[refname committerdate committerdate:relative].
12
+ REF_FORMAT = %w[refname:short committerdate committerdate:relative].
13
13
  map { |field| '%(' + field + ')' }.join(REF_FORMAT_SEPARATOR)
14
14
  REF_PREFIX = 'refs/heads/'
15
+ DEFAULT_HEADER_COLOR = :blue
15
16
 
16
17
  def self.run(command)
17
18
  `#{command}`.strip
18
19
  end
19
20
 
20
- def initialize(options = {})
21
- # Options:
22
- # - :branch_except (Regexp)
23
- # - :branch_only (Regexp)
24
- # - :max_days_old (integer)
21
+ def initialize
22
+ self.options = {}
25
23
 
26
- self.options = options
24
+ # Set defaults
25
+ set_option(:header_style, DEFAULT_HEADER_COLOR.to_s)
27
26
  end
28
27
 
29
28
  def repo?
@@ -44,10 +43,9 @@ class Twig
44
43
 
45
44
  branch_tuples.inject([]) do |result, branch_tuple|
46
45
  ref, time_string, time_ago = branch_tuple.split(REF_FORMAT_SEPARATOR)
47
- name = ref.sub(%r{^#{ REF_PREFIX }}, '')
48
46
  time = Time.parse(time_string)
49
47
  commit_time = Twig::CommitTime.new(time, time_ago)
50
- branch = Branch.new(name, :last_commit_time => commit_time)
48
+ branch = Branch.new(ref, :last_commit_time => commit_time)
51
49
  result << branch
52
50
  end
53
51
  end
@@ -88,7 +86,7 @@ class Twig
88
86
  end
89
87
  end
90
88
 
91
- out = "\n" << branch_list_headers
89
+ out = "\n" << branch_list_headers(options)
92
90
 
93
91
  # List most recently modified branches first
94
92
  listable_branches =
data/lib/twig/branch.rb CHANGED
@@ -1,8 +1,17 @@
1
1
  class Twig
2
2
  class Branch
3
3
 
4
- RESERVED_BRANCH_PROPERTIES = %w[merge rebase remote]
4
+ EMPTY_PROPERTY_NAME_ERROR = 'Branch property names cannot be empty strings.'
5
5
  PROPERTY_NAME_FROM_GIT_CONFIG = /^branch\.[^.]+\.([^=]+)=.*$/
6
+ RESERVED_BRANCH_PROPERTIES = %w[branch merge rebase remote]
7
+
8
+ class EmptyPropertyNameError < ArgumentError
9
+ def initialize(message = nil)
10
+ message ||= EMPTY_PROPERTY_NAME_ERROR
11
+ super
12
+ end
13
+ end
14
+ class MissingPropertyError < StandardError; end
6
15
 
7
16
  attr_accessor :name, :last_commit_time
8
17
 
@@ -38,35 +47,48 @@ class Twig
38
47
  end
39
48
 
40
49
  def get_property(property_name)
41
- Twig.run("git config branch.#{name}.#{property_name}")
50
+ property_name = sanitize_property(property_name)
51
+ raise EmptyPropertyNameError if property_name.empty?
52
+
53
+ value = Twig.run("git config branch.#{name}.#{property_name}")
54
+ value == '' ? nil : value
42
55
  end
43
56
 
44
57
  def set_property(property_name, value)
45
58
  property_name = sanitize_property(property_name)
46
59
  value = value.to_s.strip
47
60
 
48
- if RESERVED_BRANCH_PROPERTIES.include?(property_name)
49
- %{Can't modify the reserved property "#{property_name}".}
61
+ if property_name.empty?
62
+ raise EmptyPropertyNameError
63
+ elsif RESERVED_BRANCH_PROPERTIES.include?(property_name)
64
+ raise ArgumentError,
65
+ %{Can't modify the reserved property "#{property_name}".}
50
66
  elsif value.empty?
51
- %{Can't set a branch property to an empty string.}
67
+ raise ArgumentError,
68
+ %{Can't set a branch property to an empty string.}
52
69
  else
53
70
  Twig.run(%{git config branch.#{name}.#{property_name} "#{value}"})
54
71
  result_body = %{property "#{property_name}" as "#{value}" for branch "#{name}".}
55
72
  if $?.success?
56
73
  "Saved #{result_body}"
57
74
  else
58
- "Could not save #{result_body}"
75
+ raise RuntimeError, "Could not save #{result_body}"
59
76
  end
60
77
  end
61
78
  end
62
79
 
63
80
  def unset_property(property_name)
81
+ property_name = sanitize_property(property_name)
82
+ raise EmptyPropertyNameError if property_name.empty?
83
+
64
84
  value = get_property(property_name)
65
- if value && !value.empty?
85
+
86
+ if value
66
87
  Twig.run(%{git config --unset branch.#{name}.#{property_name}})
67
88
  %{Removed property "#{property_name}" for branch "#{name}".}
68
89
  else
69
- %{The branch "#{name}" does not have the property "#{property_name}".}
90
+ raise MissingPropertyError,
91
+ %{The branch "#{name}" does not have the property "#{property_name}".}
70
92
  end
71
93
  end
72
94
 
data/lib/twig/cli.rb CHANGED
@@ -6,45 +6,58 @@ class Twig
6
6
  def help_intro
7
7
  version_string = "Twig v#{Twig::VERSION}"
8
8
 
9
+ intro = help_paragraph(%{
10
+ Twig is your personal Git branch assistant. It shows you your most
11
+ recent branches, and tracks issue tracker ids, tasks, and other metadata
12
+ for your Git branches.
13
+ })
14
+
9
15
  <<-BANNER.gsub(/^[ ]+/, '')
10
16
 
11
17
  #{version_string}
12
18
  #{'-' * version_string.size}
13
19
 
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.
20
+ #{intro}
17
21
 
18
22
  https://rondevera.github.com/twig
19
23
 
20
24
  BANNER
21
25
  end
22
26
 
23
- def help_separator(option_parser, text)
24
- option_parser.separator "\n#{text}\n\n"
27
+ def help_separator(option_parser, text, options={})
28
+ options[:trailing] ||= "\n\n"
29
+ option_parser.separator "\n#{text}#{options[:trailing]}"
25
30
  end
26
31
 
27
32
  def help_description(text, options={})
28
33
  width = options[:width] || 40
29
- text = text.dup
30
-
31
- # Split into lines
34
+ words = text.gsub(/\n?\s+/, ' ').strip.split(' ')
32
35
  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!
36
+
37
+ # Split words into lines
38
+ while words.any?
39
+ current_word = words.shift
40
+ current_word_size = formatted_string_display_size(current_word)
41
+ last_line_size = lines.last && formatted_string_display_size(lines.last)
42
+
43
+ if last_line_size && (last_line_size + current_word_size + 1 <= width)
44
+ lines.last << ' ' << current_word
45
+ elsif current_word_size >= width
46
+ lines << current_word[0...width]
47
+ words.unshift(current_word[width..-1])
38
48
  else
39
- lines << text.slice!(0..-1)
49
+ lines << current_word
40
50
  end
41
51
  end
42
52
 
43
53
  lines << ' ' if options[:add_separator]
44
-
45
54
  lines
46
55
  end
47
56
 
57
+ def help_paragraph(text)
58
+ help_description(text, :width => 80).join("\n")
59
+ end
60
+
48
61
  def read_cli_options!(args)
49
62
  option_parser = OptionParser.new do |opts|
50
63
  opts.banner = help_intro
@@ -114,13 +127,46 @@ class Twig
114
127
  unset_option(:branch_only)
115
128
  end
116
129
 
130
+
131
+
132
+ help_separator(opts, 'Listing branches:')
133
+
134
+ colors = Twig::Display::COLORS.keys.map do |value|
135
+ format_string(value, { :color => value })
136
+ end.join(', ')
137
+ weights = Twig::Display::WEIGHTS.keys.map do |value|
138
+ format_string(value, { :weight => value })
139
+ end.join(' and ')
140
+ default_color = format_string(
141
+ Twig::DEFAULT_HEADER_COLOR.to_s,
142
+ :color => Twig::DEFAULT_HEADER_COLOR
143
+ )
144
+ desc = <<-DESC
145
+ STYLE is a color, weight, or a space-separated pair of one of each.
146
+ Valid colors are #{colors}. Valid weights are #{weights}.
147
+ The default is "#{default_color}".
148
+ DESC
149
+ opts.on('--header-style "STYLE"', *help_description(desc)) do |style|
150
+ set_option(:header_style, style)
151
+ end
152
+
153
+
154
+
155
+ help_separator(opts, help_paragraph(%{
156
+ You can put your most frequently used options for filtering and
157
+ listing branches into #{Twig::Options::CONFIG_FILE}. For example:
158
+ }), :trailing => '')
159
+
117
160
  help_separator(opts, [
118
- 'You can put your most frequently used branch filtering options in',
119
- "#{Twig::Options::CONFIG_FILE}. For example:",
120
- '',
121
161
  ' except-branch: staging',
162
+ ' header-style: green bold',
122
163
  ' max-days-old: 30'
123
- ].join("\n"))
164
+ ].join("\n"), :trailing => '')
165
+
166
+ help_separator(opts, help_paragraph(%{
167
+ To enable tab completion for Twig, run `twig init-completion` and
168
+ follow the instructions.
169
+ }))
124
170
  end
125
171
 
126
172
  option_parser.parse!(args)
@@ -154,19 +200,31 @@ class Twig
154
200
  # Get/set branch property
155
201
  if property_value
156
202
  # `$ twig <key> <value>`
157
- puts set_branch_property(branch_name, property_name, property_value)
203
+ begin
204
+ puts set_branch_property(branch_name, property_name, property_value)
205
+ rescue ArgumentError, RuntimeError => exception
206
+ abort exception.message
207
+ end
158
208
  else
159
209
  # `$ twig <key>`
160
- value = get_branch_property(branch_name, property_name)
161
- if value && !value.empty?
162
- puts value
163
- else
164
- abort %{The branch "#{branch_name}" does not have the property "#{property_name}".}
210
+ begin
211
+ value = get_branch_property(branch_name, property_name)
212
+ if value
213
+ puts value
214
+ else
215
+ abort %{The branch "#{branch_name}" does not have the property "#{property_name}".}
216
+ end
217
+ rescue ArgumentError => exception
218
+ abort exception.message
165
219
  end
166
220
  end
167
221
  elsif property_to_unset
168
222
  # `$ twig --unset <key>`
169
- puts unset_branch_property(branch_name, property_to_unset)
223
+ begin
224
+ puts unset_branch_property(branch_name, property_to_unset)
225
+ rescue ArgumentError, Twig::Branch::MissingPropertyError => exception
226
+ abort exception.message
227
+ end
170
228
  else
171
229
  # `$ twig`
172
230
  puts list_branches