TicGit-ng 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.
@@ -0,0 +1,240 @@
1
+ require 'ticgit-ng'
2
+ require 'ticgit-ng/command'
3
+
4
+ # used Cap as a model for this - thanks Jamis
5
+
6
+ module TicGitNG
7
+ class CLI
8
+ def self.execute
9
+ parse(ARGV).execute!
10
+ end
11
+
12
+ def self.parse(args)
13
+ #new() calls initialize(...) below
14
+ cli = new(args)
15
+ cli.parse_options!
16
+ cli
17
+ end
18
+
19
+ attr_reader :action, :options, :args, :tic
20
+ attr_accessor :out
21
+
22
+ def initialize(args, path = '.', out = $stdout)
23
+ @args = args.dup
24
+ @tic = TicGitNG.open(path, :keep_state => true)
25
+ @options = OpenStruct.new
26
+ @out = out
27
+
28
+ @out.sync = true # so that Net::SSH prompts show up
29
+ rescue NoRepoFound
30
+ puts "No repo found"
31
+ exit
32
+ end
33
+
34
+ def execute!
35
+ if mod = Command.get(action)
36
+ extend(mod)
37
+
38
+ if respond_to?(:parser)
39
+ option_parser = Command.parser(action, &method(:parser))
40
+ else
41
+ option_parser = Command.parser(action)
42
+ end
43
+
44
+ option_parser.parse!(args)
45
+
46
+ execute if respond_to?(:execute)
47
+ else
48
+ puts usage
49
+
50
+ if args.empty? and !action
51
+ exit
52
+ else
53
+ puts('%p is not a command' % action)
54
+ exit 1
55
+ end
56
+ end
57
+ end
58
+
59
+ def parse_options! #:nodoc:
60
+ if args.empty?
61
+ puts "Please specify at least one action to execute."
62
+ puts
63
+ puts usage(args)
64
+ exit 1
65
+ end
66
+
67
+ @action = args.shift
68
+ end
69
+
70
+ def usage(args = nil)
71
+ old_args = args || [action, *self.args].compact
72
+
73
+ if respond_to?(:parser)
74
+ Command.parser('COMMAND', &method(:parser))
75
+ # option_parser.parse!(args)
76
+ else
77
+ Command.usage(old_args.first, old_args)
78
+ end
79
+ end
80
+
81
+ def get_editor_message(message_file = nil)
82
+ message_file = Tempfile.new('ticgitng_message').path if !message_file
83
+
84
+ editor = ENV["EDITOR"] || 'vim'
85
+ system("#{editor} #{message_file}");
86
+ message = File.readlines(message_file)
87
+ message = message.select { |line| line[0, 1] != '#' } # removing comments
88
+ if message.empty?
89
+ return false
90
+ else
91
+ return message
92
+ end
93
+ end
94
+
95
+ def ticket_show(t, more=nil)
96
+ days_ago = ((Time.now - t.opened) / (60 * 60 * 24)).round
97
+
98
+ data = [
99
+ ['Title', t.title],
100
+ ['TicId', t.ticket_id],
101
+ '',
102
+ ['Assigned', t.assigned],
103
+ ['Opened', "#{t.opened} (#{days_ago} days)"],
104
+ ['State', t.state.upcase],
105
+ ['Points', t.points || 'no estimate'],
106
+ ['Tags', t.tags.join(', ')],
107
+ ''
108
+ ]
109
+
110
+ data.each do |(key, value)|
111
+ puts(value ? "#{key}: #{value}" : key)
112
+ end
113
+
114
+ unless t.comments.empty?
115
+ puts "Comments (#{t.comments.size}):"
116
+ t.comments.reverse_each do |c|
117
+ puts " * Added #{c.added.strftime('%m/%d %H:%M')} by #{c.user}"
118
+
119
+ wrapped = c.comment.split("\n").map{|line|
120
+ line.length > 80 ? line.gsub(/(.{1,80})(\s+|$)/, "\\1\n").strip : line
121
+ }.join("\n")
122
+
123
+ wrapped = wrapped.split("\n").map{|line| "\t#{line}" }
124
+
125
+ if wrapped.size > 6 and more.nil?
126
+ puts wrapped[0, 6].join("\n")
127
+ puts "\t** more... **"
128
+ else
129
+ puts wrapped.join("\n")
130
+ end
131
+ puts
132
+ end
133
+ end
134
+ end
135
+
136
+ class << self
137
+ attr_accessor :window_lines, :window_cols
138
+
139
+ TIOCGWINSZ_INTEL = 0x5413 # For an Intel processor
140
+ TIOCGWINSZ_PPC = 0x40087468 # For a PowerPC processor
141
+ STDOUT_HANDLE = 0xFFFFFFF5 # For windows
142
+
143
+ def reset_window_width
144
+ try_using(TIOCGWINSZ_PPC) ||
145
+ try_using(TIOCGWINSZ_INTEL) ||
146
+ try_windows ||
147
+ use_fallback
148
+ end
149
+
150
+ # Set terminal dimensions using ioctl syscall on *nix platform
151
+ # TODO: find out what is raised here on windows.
152
+ def try_using(mask)
153
+ buf = [0,0,0,0].pack("S*")
154
+
155
+ if $stdout.ioctl(mask, buf) >= 0
156
+ self.window_lines, self.window_cols = buf.unpack("S2")
157
+ true
158
+ end
159
+ rescue Errno::EINVAL
160
+ end
161
+
162
+ def try_windows
163
+ lines, cols = windows_terminal_size
164
+ self.window_lines, self.window_cols = lines, cols if lines and cols
165
+ end
166
+
167
+ # Determine terminal dimensions on windows platform
168
+ def windows_terminal_size
169
+ m_GetStdHandle = Win32API.new(
170
+ 'kernel32', 'GetStdHandle', ['L'], 'L')
171
+ m_GetConsoleScreenBufferInfo = Win32API.new(
172
+ 'kernel32', 'GetConsoleScreenBufferInfo', ['L', 'P'], 'L' )
173
+ format = 'SSSSSssssSS'
174
+ buf = ([0] * format.size).pack(format)
175
+ stdout_handle = m_GetStdHandle.call(STDOUT_HANDLE)
176
+
177
+ m_GetConsoleScreenBufferInfo.call(stdout_handle, buf)
178
+ (bufx, bufy, curx, cury, wattr,
179
+ left, top, right, bottom, maxx, maxy) = buf.unpack(format)
180
+ return bottom - top + 1, right - left + 1
181
+ rescue NameError
182
+ end
183
+
184
+ def use_fallback
185
+ self.window_lines, self.window_cols = 25, 80
186
+ end
187
+ end
188
+
189
+ def window_lines
190
+ TicGitNG::CLI.window_lines
191
+ end
192
+
193
+ def window_cols
194
+ TicGitNG::CLI.window_cols
195
+ end
196
+
197
+ if ''.respond_to?(:chars)
198
+ # assume 1.9
199
+ def just(value, size = 10, side = :left)
200
+ value = value.to_s
201
+
202
+ if value.bytesize > size
203
+ sub_value = "#{value[0, size - 1]}\xe2\x80\xa6"
204
+ else
205
+ sub_value = value[0, size]
206
+ end
207
+
208
+ just_common(sub_value, size, side)
209
+ end
210
+ else
211
+ def just(value, size = 10, side = :left)
212
+ chars = value.to_s.scan(/./um)
213
+
214
+ if chars.size > size
215
+ sub_value = "#{chars[0, size-1]}\xe2\x80\xa6"
216
+ else
217
+ sub_value = chars.join
218
+ end
219
+
220
+ just_common(sub_value, size, side)
221
+ end
222
+ end
223
+
224
+ def just_common(value, size, side)
225
+ case side
226
+ when :r, :right
227
+ value.rjust(size)
228
+ when :l, :left
229
+ value.ljust(size)
230
+ end
231
+ end
232
+
233
+ def puts(*strings)
234
+ @out.puts(*strings)
235
+ end
236
+ end
237
+ end
238
+
239
+ TicGitNG::CLI.reset_window_width
240
+ Signal.trap("SIGWINCH") { TicGitNG::CLI.reset_window_width }
@@ -0,0 +1,43 @@
1
+ module TicGitNG
2
+ module Command
3
+ # Assigns a ticket to someone
4
+ #
5
+ # Usage:
6
+ # ti assign (assign checked out ticket to current user)
7
+ # ti assign {1} (assign ticket to current user)
8
+ # ti assign -c {1} (assign ticket to current user and checkout the ticket)
9
+ # ti assign -u {name} (assign ticket to specified user)
10
+ # ti assign -u {name} {1} (assign specified ticket to specified user)
11
+ module Assign
12
+ def parser(opts)
13
+ opts.banner = "Usage: ti assign [options] [ticket_id]"
14
+ opts.on_head(
15
+ "-u USER", "--user USER", "Assign the ticket to this user"){|v|
16
+ options.user = v
17
+ }
18
+ opts.on_head(
19
+ "-c TICKET", "--checkout TICKET", "Checkout this ticket"){|v|
20
+ options.checkout = v
21
+ }
22
+ end
23
+ def execute
24
+ handle_ticket_assign
25
+ end
26
+ def handle_ticket_assign
27
+ tic.ticket_checkout(options.checkout) if options.checkout
28
+ if ARGV.length == 1 #ti assign
29
+ tic_id=nil
30
+ elsif ARGV.length == 2 #ti assign {ticid}
31
+ tic_id=ARGV[2]
32
+ elsif ARGV.length == 3 #ti assign -u/-c {user/ticid}
33
+ if options.user
34
+ tic_id=nil
35
+ else
36
+ tic_id=options.checkout
37
+ end
38
+ end
39
+ tic.ticket_assign((options.user rescue nil), tic_id)
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,14 @@
1
+ module TicGitNG
2
+ module Command
3
+ module Checkout
4
+ def parser(opts)
5
+ opts.banner = "ti checkout [ticid]"
6
+ end
7
+
8
+ def execute
9
+ tid = args[0]
10
+ tic.ticket_checkout(tid)
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,36 @@
1
+ module TicGitNG
2
+ module Command
3
+ module Comment
4
+ def parser(opts)
5
+ opts.banner = "Usage: ti comment [tic_id] [options]"
6
+ opts.on_head(
7
+ "-m MESSAGE", "--message MESSAGE",
8
+ "Message you would like to add as a comment"){|v|
9
+ options.message = v
10
+ }
11
+ opts.on_head(
12
+ "-f FILE", "--file FILE",
13
+ "A file that contains the comment you would like to add"){|v|
14
+ raise ArgumentError, "Only 1 of -f/--file and -m/--message can be specified" if options.message
15
+ raise ArgumentError, "File #{v} doesn't exist" unless File.file?(v)
16
+ raise ArgumentError, "File #{v} must be <= 2048 bytes" unless File.size(v) <= 2048
17
+ options.file = v
18
+ }
19
+ end
20
+
21
+ def execute
22
+ tid = args[0].strip if args[0]
23
+ message, file = options.message, options.file
24
+
25
+ if message
26
+ tic.ticket_comment(message, tid)
27
+ elsif file
28
+ tic.ticket_comment(File.read(file), tid)
29
+ else
30
+ return unless message = get_editor_message
31
+ tic.ticket_comment(message.join(''), tid)
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,80 @@
1
+ module TicGitNG
2
+ module Command
3
+ # List tickets
4
+ module List
5
+ def parser(o)
6
+ o.banner = "Usage: ti list [options]"
7
+ o.on_head(
8
+ "-o ORDER", "--order ORDER",
9
+ "Field to order by - one of : assigned,state,date,title"){|v|
10
+ options.order = v
11
+ }
12
+
13
+ o.on_head(
14
+ "-t TAG[,TAG]", "--tags TAG[,TAG]", Array,
15
+ "List only tickets with specific tag(s)",
16
+ "Prefix the tag with '-' to negate"){|v|
17
+ options.tags ||= Set.new
18
+ options.tags.merge v
19
+ }
20
+
21
+ o.on_head(
22
+ "-s STATE[,STATE]", "--states STATE[,STATE]", Array,
23
+ "List only tickets in a specific state(s)",
24
+ "Prefix the state with '-' to negate"){|v|
25
+ options.states ||= Set.new
26
+ options.states.merge v
27
+ }
28
+
29
+ o.on_head(
30
+ "-a ASSIGNED", "--assigned ASSIGNED",
31
+ "List only tickets assigned to someone"){|v|
32
+ options.assigned = v
33
+ }
34
+
35
+ o.on_head("-S SAVENAME", "--saveas SAVENAME",
36
+ "Save this list as a saved name"){|v|
37
+ options.save = v
38
+ }
39
+
40
+ o.on_head("-l", "--list", "Show the saved queries"){|v|
41
+ options.list = true
42
+ }
43
+ end
44
+
45
+ def execute
46
+ options.saved = args[0] if args[0]
47
+
48
+ if tickets = tic.ticket_list(options.to_hash)
49
+ counter = 0
50
+ cols = [80, window_cols].max
51
+
52
+ puts
53
+ puts [' ', just('#', 4, 'r'),
54
+ just('TicId', 6),
55
+ just('Title', cols - 56),
56
+ just('State', 5),
57
+ just('Date', 5),
58
+ just('Assgn', 8),
59
+ just('Tags', 20) ].join(" ")
60
+
61
+ puts "-" * cols
62
+
63
+ tickets.each do |t|
64
+ counter += 1
65
+ tic.current_ticket == t.ticket_name ? add = '*' : add = ' '
66
+ puts [add, just(counter, 4, 'r'),
67
+ t.ticket_id[0,6],
68
+ just(t.title, cols - 56),
69
+ just(t.state, 5),
70
+ t.opened.strftime("%m/%d"),
71
+ just(t.assigned_name, 8),
72
+ just(t.tags.join(','), 20) ].join(" ")
73
+ end
74
+ puts
75
+ end
76
+
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,26 @@
1
+ module TicGitNG
2
+ module Command
3
+ # tic milestone
4
+ # tic milestone migration1 (list tickets)
5
+ # tic milestone -n migration1 3/4/08 (new milestone)
6
+ # tic milestone -a {1} (add ticket to milestone)
7
+ # tic milestone -d migration1 (delete)
8
+ module Milestone
9
+ def parser(opts)
10
+ opts.banner = "Usage: ti milestone [milestone_name] [options] [date]"
11
+
12
+ opts.on_head(
13
+ "-n MILESTONE", "--new MILESTONE",
14
+ "Add a new milestone to this project"){|v| options.new = v }
15
+
16
+ opts.on_head(
17
+ "-a TICKET", "--new TICKET",
18
+ "Add a ticket to this milestone"){|v| options.add = v }
19
+
20
+ opts.on_head(
21
+ "-d MILESTONE", "--delete MILESTONE",
22
+ "Remove a milestone"){|v| options.remove = v }
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,46 @@
1
+ module TicGitNG
2
+ module Command
3
+ module New
4
+ def parser(opts)
5
+ opts.banner = "Usage: ti new [options]"
6
+ opts.on_head(
7
+ "-t TITLE", "--title TITLE",
8
+ "Title to use for the name of the new ticket"){|v| options.title = v }
9
+ end
10
+
11
+ def execute
12
+ if title = options.title
13
+ ticket_show(tic.ticket_new(title, options.to_hash))
14
+ else
15
+ # interactive
16
+ message_file = Tempfile.new('ticgit_message').path
17
+ File.open(message_file, 'w') do |f|
18
+ f.puts "\n# ---"
19
+ f.puts "tags:"
20
+ f.puts "# first line will be the title of the tic, the rest will be the first comment"
21
+ f.puts "# if you would like to add initial tags, put them on the 'tags:' line, comma delim"
22
+ end
23
+ if message = get_editor_message(message_file)
24
+ title = message.shift
25
+ if title && title.chomp.length > 0
26
+ title = title.chomp
27
+ if message.last[0, 5] == 'tags:'
28
+ tags = message.pop
29
+ tags = tags.gsub('tags:', '')
30
+ tags = tags.split(',').map { |t| t.strip }
31
+ end
32
+ if message.size > 0
33
+ comment = message.join("")
34
+ end
35
+ ticket_show(tic.ticket_new(title, :comment => comment, :tags => tags))
36
+ else
37
+ puts "You need to at least enter a title"
38
+ end
39
+ else
40
+ puts "It seems you wrote nothing"
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,28 @@
1
+ module TicGitNG
2
+ module Command
3
+ # Assigns points to a ticket
4
+ #
5
+ # Usage:
6
+ # ti points {1} {points} (assigns points to a specified ticket)
7
+ module Points
8
+ def parser(opts)
9
+ opts.banner = "ti points [ticket_id] points"
10
+ end
11
+
12
+ def execute
13
+ case args.size
14
+ when 1
15
+ new_points = args[0].to_i
16
+ when 2
17
+ tid = args[0]
18
+ new_points = args[1].to_i
19
+ else
20
+ puts usage
21
+ exit 1
22
+ end
23
+
24
+ tic.ticket_points(new_points, tid)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,29 @@
1
+ module TicGitNG
2
+ module Command
3
+ module Recent
4
+ def parser(opts)
5
+ opts.banner = 'Usage: ti recent'
6
+ end
7
+
8
+ def execute
9
+ # "args[0]" seems to be superfluous. It's usage
10
+ # is undocumented, and supplying an argument
11
+ # doesn't seem to do anything.
12
+ #
13
+ # Im guessing the purpose of args[0] was to provide a
14
+ # specific ticket_id whos history would be looked up
15
+ # intead of looking up the history for all tickets.
16
+ #
17
+ # #FIXME Reimplement that functionality and updte
18
+ # docs to match
19
+ tic.ticket_recent(args[0]).each do |commit|
20
+ sha = commit.sha[0, 7]
21
+ date = commit.date.strftime("%m/%d %H:%M")
22
+ message = commit.message
23
+
24
+ puts "#{sha} #{date}\t#{message}"
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,18 @@
1
+ module TicGitNG
2
+ module Command
3
+ module Show
4
+ def parser(opts)
5
+ opts.banner = "Usage: ti show [--full] [ticid]"
6
+ opts.on_head(
7
+ "-f", "--full", "Show long comments in full, don't truncate after the 5th line"){|v|
8
+ options.full= v
9
+ }
10
+ end
11
+
12
+ def execute
13
+ t = tic.ticket_show(args[0])
14
+ ticket_show(t, options.full ) if t
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,45 @@
1
+ module TicGitNG
2
+ module Command
3
+ module State
4
+ def parser(opts)
5
+ opts.banner = "Usage: ti state [ticid] state"
6
+ end
7
+
8
+ def execute
9
+ if args.size > 1
10
+ tid, new_state = args[0].strip, args[1].strip
11
+
12
+ if valid_state?(new_state)
13
+ tic.ticket_change(new_state, tid)
14
+ else
15
+ puts "Invalid State - please choose from: #{joined_states}"
16
+ end
17
+ elsif args.size > 0
18
+ # new state
19
+ new_state = args[0].chomp
20
+
21
+ if valid_state?(new_state)
22
+ tic.ticket_change(new_state)
23
+ else
24
+ puts "Invalid State - please choose from: #{joined_states}"
25
+ end
26
+ else
27
+ puts 'You need to at least specify a new state for the current ticket'
28
+ puts "please choose from: #{joined_states}"
29
+ end
30
+ end
31
+
32
+ def valid_state?(state)
33
+ available_states.include?(state)
34
+ end
35
+
36
+ def available_states
37
+ tic.tic_states.sort
38
+ end
39
+
40
+ def joined_states
41
+ available_states.join(', ')
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,29 @@
1
+ module TicGitNG
2
+ module Command
3
+ module Sync
4
+ def parser(opts)
5
+ opts.banner = "Usage: ti sync [options]"
6
+ opts.on_head(
7
+ "-r REPO", "--repo REPO", "Sync ticgit-ng branch with REPO"){|v|
8
+ options.repo = v
9
+ }
10
+ opts.on_head(
11
+ "-n", "--no-push", "Do not push to the remote repo"){|v|
12
+ options.no_push = true
13
+ }
14
+ end
15
+
16
+ def execute
17
+ if options.repo and options.no_push
18
+ tic.sync_tickets(options.repo, false)
19
+ elsif options.repo
20
+ tic.sync_tickets(options.repo)
21
+ elsif options.no_push
22
+ tic.sync_tickets('origin', false)
23
+ else
24
+ tic.sync_tickets()
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,31 @@
1
+ module TicGitNG
2
+ module Command
3
+ module Tag
4
+ def parser(opts)
5
+ opts.banner = "Usage: ti tag [tic_id] [options] [tag_name] "
6
+ opts.on_head(
7
+ "-d", "--delete",
8
+ "Remove this tag from the ticket"){|v| options.remove = v }
9
+ end
10
+
11
+ def execute
12
+ if options.remove
13
+ puts 'remove'
14
+ end
15
+
16
+ if ARGV.size > 3
17
+ tid = ARGV[1].chomp
18
+ tic.ticket_tag(ARGV[3].chomp, tid, options)
19
+ elsif ARGV.size > 2 #tag
20
+ tic.ticket_tag(ARGV[2], nil, options)
21
+ elsif ARGV.size == 2 #tag add 'tag_foobar'
22
+ tic.ticket_tag(ARGV[1], nil, options)
23
+ else
24
+ puts 'You need to at least specify one tag to add'
25
+ puts
26
+ puts parser
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end