twig 1.0.1 → 1.1

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/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