stephencelis-ghi 0.0.3 → 0.0.4

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.rdoc CHANGED
@@ -1,3 +1,15 @@
1
+ === 0.0.4 / 2009-04-24
2
+
3
+ * 1 major enhancement
4
+
5
+ * Cache messages created in $EDITOR.
6
+
7
+ * 2 minor enhancements
8
+
9
+ * Refactoring and cleanup.
10
+ * Change editing messages.
11
+
12
+
1
13
  === 0.0.3 / 2009-04-23
2
14
 
3
15
  * 2 minor enhancements
data/bin/ghi CHANGED
@@ -1,4 +1,4 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  require "ghi/cli"
4
- GHI::CLI.new
4
+ GHI::CLI::Executable.new
data/lib/ghi/api.rb CHANGED
@@ -2,6 +2,9 @@ require "net/http"
2
2
  require "yaml"
3
3
 
4
4
  class GHI::API
5
+ class InvalidRequest < StandardError
6
+ end
7
+
5
8
  class InvalidConnection < StandardError
6
9
  end
7
10
 
data/lib/ghi/cli.rb CHANGED
@@ -4,234 +4,272 @@ require "ghi"
4
4
  require "ghi/api"
5
5
  require "ghi/issue"
6
6
 
7
- class GHI::CLI
8
- attr_reader :user, :repo, :api, :action, :state, :number, :title,
9
- :search_term
10
-
11
- def initialize
12
- option_parser.parse!(ARGV)
13
-
14
- `git config --get remote.origin.url`.match %r{([^:/]+)/([^/]+).git$}
15
- @user ||= $1
16
- @repo ||= $2
17
- @api = GHI::API.new user, repo
18
-
19
- case action
20
- when :search then search search_term, state
21
- when :list then list state
22
- when :show then show number
23
- when :open then open title
24
- when :edit then edit number
25
- when :close then close number
26
- when :reopen then reopen number
27
- else puts option_parser
28
- end
29
- rescue GHI::API::InvalidConnection
30
- warn "#{File.basename $0}: not a GitHub repo"
31
- rescue GHI::API::ResponseError => e
32
- warn "#{File.basename $0}: #{e.message} (#{user}/#{repo})"
33
- rescue OptionParser::InvalidOption, OptionParser::MissingArgument => e
34
- warn "#{File.basename $0}: #{e.message}"
35
- end
7
+ module GHI::CLI #:nodoc:
8
+ module FileHelper
9
+ def launch_editor(file)
10
+ system "#{editor} #{file.path}"
11
+ end
36
12
 
37
- private
38
-
39
- def option_parser
40
- @option_parser ||= OptionParser.new { |opts|
41
- opts.banner = "Usage: #{File.basename $0} [options]"
42
-
43
- opts.on("-l", "--list", "--search", "--show [state|term|number]") do |v|
44
- @action = :list
45
- case v
46
- when nil, /^o$/
47
- @state = :open
48
- when /^\d+$/
49
- @action = :show
50
- @number = v.to_i
51
- when /^c$/
52
- @state = :closed
53
- else
54
- @action = :search
55
- @state ||= :open
56
- @search_term = v
57
- end
13
+ def gets_from_editor(issue)
14
+ if gitdir
15
+ File.open message_path, "a+", &file_proc(issue)
16
+ else
17
+ Tempfile.new message_filename, &file_proc(issue)
58
18
  end
19
+ return @message.shift.strip, @message.join.sub(/\b\n\b/, " ").strip
20
+ end
59
21
 
60
- opts.on("-o", "--open", "--reopen [number]") do |v|
61
- @action = :open
62
- case v
63
- when /^\d+$/
64
- @action = :reopen
65
- @number = v.to_i
66
- when /^l$/
67
- @action = :list
68
- @state = :open
69
- else
70
- @title = v
71
- end
72
- end
22
+ def delete_message
23
+ File.delete message_path
24
+ rescue TypeError
25
+ nil
26
+ end
73
27
 
74
- opts.on("-c", "--closed", "--close [number]") do |v|
75
- case v
76
- when /^\d+$/
77
- @action = :close
78
- @number = v.to_i
79
- when /^l/, nil
80
- @action = :list
81
- @state = :closed
82
- else
83
- raise OptionParser::InvalidOption
84
- end
28
+ def message_path
29
+ File.join gitdir, message_filename
30
+ end
31
+
32
+ private
33
+
34
+ def editor
35
+ ENV["VISUAL"] || ENV["EDITOR"] || "vi"
36
+ end
37
+
38
+ def gitdir
39
+ @gitdir ||= begin
40
+ dirs = []
41
+ Dir.pwd.count("/").times { |n| dirs << ([".."] * n << ".git") * "/" }
42
+ Dir[*dirs].first
85
43
  end
44
+ end
45
+
46
+ def message_filename
47
+ @message_filename ||= "GHI_#{action.to_s.upcase}#{number}_MESSAGE"
48
+ end
49
+
50
+ def file_proc(issue)
51
+ lambda do |file|
52
+ file << edit_format(issue).join("\n") if File.zero? message_path
53
+ file.rewind
54
+ launch_editor file
55
+ @message = File.readlines(file.path).find_all { |l| !l.match(/^#/) }
86
56
 
87
- opts.on("-e", "--edit [number]") do |v|
88
- case v
89
- when /^\d+$/
90
- @action = :edit
91
- @state = :closed
92
- @number = v.to_i
93
- else
94
- raise OptionParser::MissingArgument
57
+ if message.to_s =~ /\A\s*\Z/
58
+ raise GHI::API::InvalidRequest, "can't file empty issue"
95
59
  end
60
+ raise GHI::API::InvalidRequest, "no change" if issue == message
96
61
  end
62
+ end
63
+ end
97
64
 
98
- opts.on("-r", "--repo", "--repository [name]") do |v|
99
- repo = v.split "/"
100
- repo.unshift GHI.login if repo.length == 1
101
- @user, @repo = repo
65
+ module FormattingHelper
66
+ def list_format(issues, term = nil)
67
+ l = if term
68
+ ["# #{state.to_s.capitalize} #{term.inspect} issues on #{user}/#{repo}"]
69
+ else
70
+ ["# #{state.to_s.capitalize} issues on #{user}/#{repo}"]
102
71
  end
103
72
 
104
- opts.on("-V", "--version") do
105
- puts "#{File.basename($0)}: v#{GHI::VERSION}"
106
- exit
73
+ l += unless issues.empty?
74
+ issues.map { |i| " #{i.number.to_s.rjust 3}: #{truncate i.title, 72}" }
75
+ else
76
+ ["none"]
107
77
  end
78
+ end
108
79
 
109
- opts.on("-h", "--help") do
110
- puts opts
111
- exit
112
- end
113
- }
114
- end
80
+ def edit_format(issue)
81
+ l = []
82
+ l << issue.title if issue.title
83
+ l << ""
84
+ l << issue.body if issue.body
85
+ l << "# Please explain the issue. The first line will become the title."
86
+ l << "# Lines beginning '#' will be ignored. Empty issues won't be filed."
87
+ l << "# All line breaks will be honored in accordance with GFM:"
88
+ l << "#"
89
+ l << "# http://github.github.com/github-flavored-markdown"
90
+ l << "#"
91
+ l << "# On #{user}/#{repo}:"
92
+ l << "#"
93
+ l += show_format(issue, false).map { |line| "# #{line}" }
94
+ end
115
95
 
116
- def search(term, state)
117
- issues = api.search term, state
118
- puts "# #{state.to_s.capitalize} #{term.inspect} issues on #{user}/#{repo}"
119
- if issues.empty?
120
- puts "none"
121
- else
122
- puts issues.map { |i| " #{i.number.to_s.rjust(3)}: #{i.title[0,72]}" }
96
+ def show_format(issue, verbose = true)
97
+ l = []
98
+ l << " number: #{issue.number}" if issue.number
99
+ l << " state: #{issue.state}" if issue.state
100
+ l << " title: #{issue.title}" if issue.title
101
+ l << " user: #{issue.user || GHI.login}"
102
+ l << " votes: #{issue.votes}" if issue.votes
103
+ l << " created at: #{issue.created_at}" if issue.created_at
104
+ l << " updated at: #{issue.updated_at}" if issue.updated_at
105
+ return l unless verbose
106
+ l << ""
107
+ l += issue.body.scan(/.{0,75}(?:\s|$)/).map { |line| " #{line}" }
123
108
  end
124
- end
125
109
 
126
- def list(state)
127
- issues = api.list state
128
- puts "# #{state.to_s.capitalize} issues on #{user}/#{repo}"
129
- if issues.empty?
130
- puts "none"
131
- else
132
- puts issues.map { |i| " #{i.number.to_s.rjust(3)}: #{i.title[0,72]}" }
110
+ def action_format(issue)
111
+ key = "#{action.to_s.capitalize.sub(/e?$/, "ed")} issue #{issue.number}"
112
+ "#{key}: #{truncate issue.title, 78 - key.length}"
113
+ end
114
+
115
+ def truncate(string, length)
116
+ result = string.scan(/.{0,#{length}}(?:\s|$)/).first.strip
117
+ result << "..." if result != string
118
+ result
133
119
  end
134
120
  end
135
121
 
136
- def show(number)
137
- issue = api.show number
138
- puts <<-BODY
139
- #{issue.number}: #{issue.title} [#{issue.state}]
122
+ class Executable
123
+ include FileHelper, FormattingHelper
140
124
 
141
- votes: #{issue.votes}
142
- created_at: #{issue.created_at}
143
- updated_at: #{issue.updated_at}
125
+ attr_reader :message, :user, :repo, :api, :action, :state, :number, :title,
126
+ :search_term
144
127
 
145
- #{issue.body}
128
+ def initialize
129
+ option_parser.parse!(ARGV)
146
130
 
147
- -- #{issue.user}
148
- BODY
149
- end
131
+ `git config --get remote.origin.url`.match %r{([^:/]+)/([^/]+).git$}
132
+ @user ||= $1
133
+ @repo ||= $2
134
+ @api = GHI::API.new user, repo
150
135
 
151
- def open(title)
152
- edit = ENV["VISUAL"] || ENV["EDITOR"] || "vi"
153
- temp = Tempfile.open("open-issue-")
154
- temp.write <<-BODY
155
- #{"#{title}\n" unless title.nil?}
156
- # Please explain the issue. The first line will be used as the title.
157
- # Lines with "#" will be ignored, and empty issues will not be filed.
158
- # All line breaks will be honored in accordance with GFM:
159
- #
160
- # http://github.github.com/github-flavored-markdown
161
- #
162
- # On #{user}/#{repo}:
163
- #
164
- # user: #{GHI.login}
165
- BODY
166
- temp.rewind
167
- system "#{edit} #{temp.path}"
168
- lines = File.readlines(temp.path).find_all { |l| !l.match(/^#/) }
169
- temp.close!
170
- if lines.to_s =~ /\A\s*\Z/
171
- warn "can't file empty issue"
136
+ case action
137
+ when :search then search search_term, state
138
+ when :list then list state
139
+ when :show then show number
140
+ when :open then open title
141
+ when :edit then edit number
142
+ when :close then close number
143
+ when :reopen then reopen number
144
+ else puts option_parser
145
+ end
146
+ rescue GHI::API::InvalidConnection
147
+ warn "#{File.basename $0}: not a GitHub repo"
148
+ exit 1
149
+ rescue GHI::API::InvalidRequest, GHI::API::ResponseError => e
150
+ warn "#{File.basename $0}: #{e.message} (#{user}/#{repo})"
151
+ exit 1
152
+ rescue OptionParser::InvalidOption, OptionParser::MissingArgument => e
153
+ warn "#{File.basename $0}: #{e.message}"
172
154
  exit 1
173
- else
174
- title = lines.shift.strip
175
- body = lines.join.sub(/\b\n\b/, " ").strip
176
- issue = api.open title, body
177
- puts " Opened issue #{issue.number}: #{issue.title[0,58]}"
178
155
  end
179
- end
180
156
 
181
- def edit(number)
182
- edit = ENV["VISUAL"] || ENV["EDITOR"] || "vi"
183
- begin
184
- temp = Tempfile.open("open-issue-")
185
- issue = api.show number
186
- temp.write <<-BODY
187
- #{issue.title}#{"\n\n" + issue.body unless issue.body.to_s.strip == ""}
188
- # Please explain the issue. The first line will be used as the title.
189
- # Lines with "#" will be ignored, and empty issues will not be filed.
190
- # All line breaks will be honored in accordance with GFM:
191
- #
192
- # http://github.github.com/github-flavored-markdown
193
- #
194
- # On #@user/#@repo:
195
- #
196
- # number: #{issue.number}
197
- # user: #{issue.user}
198
- # votes: #{issue.votes}
199
- # state: #{issue.state}
200
- # created at: #{issue.created_at}
201
- BODY
202
- if issue.updated_at > issue.created_at
203
- temp.write "# updated at: #{issue.updated_at}"
204
- end
205
- temp.rewind
206
- system "#{edit} #{temp.path}"
207
- lines = File.readlines(temp.path)
208
- if temp.readlines == lines
209
- warn "no change"
210
- exit 1
211
- else
212
- lines.reject! { |l| l.match(/^#/) }
213
- if lines.to_s =~ /\A\s*\Z/
214
- warn "can't file empty issue"
215
- exit 1
216
- else
217
- title = lines.shift.strip
218
- body = lines.join.sub(/\b\n\b/, " ").strip
219
- issue = api.edit number, title, body
220
- puts " Updated issue #{issue.number}: #{issue.title[0,58]}"
157
+ private
158
+
159
+ def option_parser
160
+ @option_parser ||= OptionParser.new { |opts|
161
+ opts.banner = "Usage: #{File.basename $0} [options]"
162
+
163
+ opts.on("-l", "--list", "--search", "--show [state|term|number]") do |v|
164
+ @action = :list
165
+ case v
166
+ when nil, /^o$/
167
+ @state = :open
168
+ when /^\d+$/
169
+ @action = :show
170
+ @number = v.to_i
171
+ when /^c$/
172
+ @state = :closed
173
+ else
174
+ @action = :search
175
+ @state ||= :open
176
+ @search_term = v
177
+ end
221
178
  end
222
- end
223
- ensure
224
- temp.close!
179
+
180
+ opts.on("-o", "--open", "--reopen [number]") do |v|
181
+ @action = :open
182
+ case v
183
+ when /^\d+$/
184
+ @action = :reopen
185
+ @number = v.to_i
186
+ when /^l$/
187
+ @action = :list
188
+ @state = :open
189
+ else
190
+ @title = v
191
+ end
192
+ end
193
+
194
+ opts.on("-c", "--closed", "--close [number]") do |v|
195
+ case v
196
+ when /^\d+$/
197
+ @action = :close
198
+ @number = v.to_i
199
+ when /^l/, nil
200
+ @action = :list
201
+ @state = :closed
202
+ else
203
+ raise OptionParser::InvalidOption
204
+ end
205
+ end
206
+
207
+ opts.on("-e", "--edit [number]") do |v|
208
+ case v
209
+ when /^\d+$/
210
+ @action = :edit
211
+ @state = :closed
212
+ @number = v.to_i
213
+ else
214
+ raise OptionParser::MissingArgument
215
+ end
216
+ end
217
+
218
+ opts.on("-r", "--repo", "--repository [name]") do |v|
219
+ repo = v.split "/"
220
+ repo.unshift GHI.login if repo.length == 1
221
+ @user, @repo = repo
222
+ end
223
+
224
+ opts.on("-V", "--version") do
225
+ puts "#{File.basename($0)}: v#{GHI::VERSION}"
226
+ exit
227
+ end
228
+
229
+ opts.on("-h", "--help") do
230
+ puts opts
231
+ exit
232
+ end
233
+ }
225
234
  end
226
- end
227
235
 
228
- def close(number)
229
- issue = api.close number
230
- puts " Closed issue #{issue.number}: #{issue.title[0,58]}"
231
- end
236
+ def search(term, state)
237
+ issues = api.search term, state
238
+ puts list_format(issues, term)
239
+ end
240
+
241
+ def list(state)
242
+ issues = api.list state
243
+ puts list_format(issues)
244
+ end
245
+
246
+ def show(number)
247
+ issue = api.show number
248
+ puts show_format(issue)
249
+ end
232
250
 
233
- def reopen(number)
234
- issue = api.reopen number
235
- puts " Reopened issue #{issue.number}: #{issue.title[0,56]}"
251
+ def open(title)
252
+ title, body = gets_from_editor GHI::Issue.new(:title => title)
253
+ issue = api.open title, body
254
+ delete_message
255
+ puts action_format(issue)
256
+ end
257
+
258
+ def edit(number)
259
+ title, body = gets_from_editor api.show(number)
260
+ issue = api.edit number, title, body
261
+ delete_message
262
+ puts action_format(issue)
263
+ end
264
+
265
+ def close(number)
266
+ issue = api.close number
267
+ puts action_format(issue)
268
+ end
269
+
270
+ def reopen(number)
271
+ issue = api.reopen number
272
+ puts action_format(issue)
273
+ end
236
274
  end
237
275
  end
data/lib/ghi/issue.rb CHANGED
@@ -12,4 +12,18 @@ class GHI::Issue
12
12
  @created_at = options["created_at"]
13
13
  @updated_at = options["updated_at"]
14
14
  end
15
+
16
+ #-
17
+ # REFACTOR: This code is duplicated from cli.rb:gets_from_editor.
18
+ #+
19
+ def ==(other_issue)
20
+ case other_issue
21
+ when Array
22
+ other_title = other_issue.first.strip
23
+ other_body = other_issue[1..-1].join.sub(/\b\n\b/, " ").strip
24
+ title == other_title && body == other_body
25
+ else
26
+ super other_issue
27
+ end
28
+ end
15
29
  end
data/lib/ghi.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  module GHI
2
- VERSION = "0.0.3"
2
+ VERSION = "0.0.4"
3
3
 
4
4
  def self.login
5
5
  `git config --get github.user`.chomp
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: stephencelis-ghi
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stephen Celis