TicGit-ng 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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