ctodo 0.0.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/README.md +24 -0
- data/bin/todo +6 -0
- data/lib/ctodo/color.rb +112 -0
- data/lib/ctodo/github.rb +93 -0
- data/lib/ctodo/localfs.rb +103 -0
- data/lib/ctodo/redmine.rb +60 -0
- data/lib/ctodo.rb +161 -0
- metadata +112 -0
data/README.md
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
todo
|
2
|
+
====
|
3
|
+
|
4
|
+
todo provides combined listing of "issues" or "todos" from different Issue
|
5
|
+
Providers with color support.
|
6
|
+
|
7
|
+
Provider
|
8
|
+
--------
|
9
|
+
|
10
|
+
- LocalFS -- grep-like local file system provider
|
11
|
+
- Github -- issues from identically named github.com/user repo
|
12
|
+
- Redmine -- issues from identically named your.redmine.com server
|
13
|
+
|
14
|
+
Setup
|
15
|
+
-----
|
16
|
+
|
17
|
+
gem install ctodo
|
18
|
+
|
19
|
+
Links
|
20
|
+
-----
|
21
|
+
|
22
|
+
- [Github API](http://developer.github.com/v3/)
|
23
|
+
- [Redmine API](http://www.redmine.org/projects/redmine/wiki/Rest_api)
|
24
|
+
- [HSV color space](https://secure.wikimedia.org/wikipedia/de/wiki/HSV-Farbraum)
|
data/bin/todo
ADDED
data/lib/ctodo/color.rb
ADDED
@@ -0,0 +1,112 @@
|
|
1
|
+
|
2
|
+
module CTodo
|
3
|
+
|
4
|
+
COLOR_SETS = ['Fg', 'Bg', 'None']
|
5
|
+
|
6
|
+
class ColorSet
|
7
|
+
def rst; "\e[0m" end
|
8
|
+
|
9
|
+
def demo
|
10
|
+
[:white, :cyan, :magenta, :blue, :yellow, :green, :red, :black].each do |color|
|
11
|
+
puts [self.send(color), color.to_s[0,1].upcase*3, rst].join
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class FgColorSet < ColorSet
|
17
|
+
# only use bold colors (1;) for those on black on white screens
|
18
|
+
def white; "\e[1;37m" end
|
19
|
+
def cyan; "\e[1;36m" end
|
20
|
+
def magenta;"\e[1;35m" end
|
21
|
+
def blue; "\e[1;34m" end
|
22
|
+
def yellow; "\e[1;33m" end
|
23
|
+
def green; "\e[1;32m" end
|
24
|
+
def red; "\e[1;31m" end
|
25
|
+
def black; "\e[1;30m" end
|
26
|
+
end
|
27
|
+
|
28
|
+
class BgColorSet < ColorSet
|
29
|
+
def white; "\e[47m\e[1;37m" end
|
30
|
+
def cyan; "\e[46m\e[1;37m" end
|
31
|
+
def magenta;"\e[45m\e[1;37m" end
|
32
|
+
def blue; "\e[44m\e[1;37m" end
|
33
|
+
def yellow; "\e[43m\e[1;37m" end
|
34
|
+
def green; "\e[42m\e[1;37m" end
|
35
|
+
def red; "\e[41m\e[1;37m" end
|
36
|
+
def black; "\e[40m\e[1;37m" end
|
37
|
+
end
|
38
|
+
|
39
|
+
class NoneColorSet < ColorSet
|
40
|
+
def white; "" end
|
41
|
+
def cyan; "" end
|
42
|
+
def magenta; "" end
|
43
|
+
def blue; "" end
|
44
|
+
def yellow; "" end
|
45
|
+
def green; "" end
|
46
|
+
def red; "" end
|
47
|
+
def black; "" end
|
48
|
+
end
|
49
|
+
|
50
|
+
class ColorUtils
|
51
|
+
|
52
|
+
def self.func4rgb(rgb, cs)
|
53
|
+
h,s,v = rgb_to_hsv(rgb)
|
54
|
+
|
55
|
+
if h.nan?
|
56
|
+
return proc { cs.white } if v > 0.5
|
57
|
+
return proc { cs.black }
|
58
|
+
end
|
59
|
+
|
60
|
+
assoc = [
|
61
|
+
[proc { cs.red }, 0.0],
|
62
|
+
[proc { cs.yellow }, 60.0],
|
63
|
+
[proc { cs.green }, 120.0],
|
64
|
+
[proc { cs.cyan }, 180.0],
|
65
|
+
[proc { cs.blue }, 240.0],
|
66
|
+
[proc { cs.magenta }, 300.0],
|
67
|
+
[proc { cs.red }, 360.0]
|
68
|
+
]
|
69
|
+
|
70
|
+
dist = 360.0; callee = assoc[0][0]
|
71
|
+
assoc.each do |pair|
|
72
|
+
d = (pair[1]-h).abs
|
73
|
+
if d < dist
|
74
|
+
dist = d
|
75
|
+
callee = pair[0]
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
callee
|
80
|
+
end
|
81
|
+
|
82
|
+
def self.rgb_to_hsv(rgb)
|
83
|
+
r = (rgb & 0xff0000) >> 16
|
84
|
+
g = (rgb & 0xff00) >> 8
|
85
|
+
b = rgb & 0xff
|
86
|
+
r /= 255.0; g /= 255.0; b /= 255.0
|
87
|
+
max = [r,g,b].max; min = [r,g,b].min
|
88
|
+
|
89
|
+
h = 0 if max == min
|
90
|
+
h = 60*(0+(g-b)/(max-min)) if max == r
|
91
|
+
h = 60*(2+(b-r)/(max-min)) if max == g
|
92
|
+
h = 60*(4+(r-g)/(max-min)) if max == b
|
93
|
+
h += 360 if h < 0
|
94
|
+
|
95
|
+
s = 0 if max == 0
|
96
|
+
s = (max-min)/max if max != 0
|
97
|
+
|
98
|
+
[h,s,max]
|
99
|
+
end
|
100
|
+
|
101
|
+
# cyan, magenta, blue, yellow, green, red
|
102
|
+
RAND_COLORS = ['ffffff', '00ffff', 'ff00ff', '0000ff', 'ffff00', '00ff00']
|
103
|
+
|
104
|
+
def self.rgb4string(string)
|
105
|
+
sum = 0
|
106
|
+
string.each_char do |c|
|
107
|
+
sum += c[0] % RAND_COLORS.length
|
108
|
+
end
|
109
|
+
RAND_COLORS[sum % RAND_COLORS.length]
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
data/lib/ctodo/github.rb
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
|
2
|
+
module CTodo
|
3
|
+
class Github
|
4
|
+
include HTTParty
|
5
|
+
base_uri "https://api.github.com"
|
6
|
+
|
7
|
+
def initialize(conf)
|
8
|
+
@enabled = (!conf[:gh_user].nil? and !conf[:gh_pass].nil? and !conf[:git_repo_dir].nil?)
|
9
|
+
if @enabled
|
10
|
+
self.class.basic_auth(conf[:gh_user], conf[:gh_pass])
|
11
|
+
@repo_dir = Pathname.new(conf[:git_repo_dir])
|
12
|
+
@repo_name = File.basename(conf[:git_repo_dir])
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def get_issues(issues)
|
17
|
+
return if not @enabled
|
18
|
+
|
19
|
+
status_msgs = []
|
20
|
+
|
21
|
+
# try current user first
|
22
|
+
r = self.class.get "/user"
|
23
|
+
login = r['login']
|
24
|
+
|
25
|
+
gh_issues = get_issues_for(login, @repo_name)
|
26
|
+
if !gh_issues.nil?
|
27
|
+
#status_msgs << "User: #{login}"
|
28
|
+
parse_issues(gh_issues, issues)
|
29
|
+
else
|
30
|
+
# find alternative login from "remote origin" config entry
|
31
|
+
alt_login = remote_login("origin")
|
32
|
+
if !alt_login.nil? and alt_login != login
|
33
|
+
alt_gh_issues = get_issues_for(alt_login, @repo_name)
|
34
|
+
if !alt_gh_issues.nil?
|
35
|
+
status_msgs << "User: #{alt_login}"
|
36
|
+
parse_issues(alt_gh_issues, issues)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# optional "upstream"
|
42
|
+
up_login = remote_login("upstream")
|
43
|
+
if !up_login.nil?
|
44
|
+
up_gh_issues = get_issues_for(up_login, @repo_name)
|
45
|
+
if !up_gh_issues.nil?
|
46
|
+
status_msgs << "Up: #{up_login}"
|
47
|
+
parse_issues(up_gh_issues, issues)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
puts status_msgs.join(', ') if not status_msgs.empty?
|
52
|
+
end
|
53
|
+
|
54
|
+
def get_issues_for(login, repo_name)
|
55
|
+
r = self.class.get "/repos/#{login}/#{repo_name}/issues"
|
56
|
+
return r if r.code == 200
|
57
|
+
nil
|
58
|
+
end
|
59
|
+
|
60
|
+
def parse_issues(gh_issues, issues)
|
61
|
+
gh_issues.each do |i|
|
62
|
+
loc = i['html_url']
|
63
|
+
assig = (i['assignee'].nil? ? i['user']['login'] : i['assignee']['login'])
|
64
|
+
tags = i['labels'].collect { |lab| Tag.new(lab['name'], lab['color']) }
|
65
|
+
tags << Tag.new(i['milestone']['title'], "000000") if not i['milestone'].nil?
|
66
|
+
issues << Issue.new(i['title'], loc, assig, tags)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# .git/config parsing
|
71
|
+
|
72
|
+
def remote_login(name)
|
73
|
+
git_dir = Pathname.new(!ENV['GIT_DIR'].nil? ? ENV['GIT_DIR'] : '.git')
|
74
|
+
# parse ini file
|
75
|
+
git_config = IniFile.new(@repo_dir + git_dir + 'config')
|
76
|
+
origin = find_remote(git_config.sections, name)
|
77
|
+
return nil if origin.nil?
|
78
|
+
origin_url = git_config[origin]['url']
|
79
|
+
return nil if origin_url.nil?
|
80
|
+
m = origin_url.match(/github.com\/([a-zA-Z0-9_-]+)\//)
|
81
|
+
return nil if m.nil?
|
82
|
+
# should be user login
|
83
|
+
m[1]
|
84
|
+
end
|
85
|
+
|
86
|
+
def find_remote(ini_sections, name)
|
87
|
+
ini_sections.each do |s|
|
88
|
+
return s if s.start_with?("remote") and s.include?(name)
|
89
|
+
end
|
90
|
+
nil
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
|
2
|
+
module CTodo
|
3
|
+
|
4
|
+
ALL_LABELS = ['BUG', 'FIXME', 'HACK', 'TODO', 'XXX']
|
5
|
+
IMP_LABELS = ['FIXME', 'TODO']
|
6
|
+
|
7
|
+
TRAVERSE_EXCLUDE = ['.', '..', '.git', '.svn', '.hg']
|
8
|
+
GREP_EXT_EXCLUDE = ['.tar']
|
9
|
+
|
10
|
+
class LocalFS
|
11
|
+
def initialize(conf)
|
12
|
+
@enabled = true
|
13
|
+
if @enabled
|
14
|
+
@parent_dir = conf[:git_repo_dir].nil? ? conf[:cur_dir] : conf[:git_repo_dir]
|
15
|
+
@todo_labels = (conf[:all] ? ALL_LABELS : IMP_LABELS).join('|')
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def get_issues(issues)
|
20
|
+
return if not @enabled
|
21
|
+
traverse(@parent_dir, issues)
|
22
|
+
end
|
23
|
+
|
24
|
+
def traverse(dir, issues)
|
25
|
+
Dir.entries(dir).each do |e|
|
26
|
+
next if TRAVERSE_EXCLUDE.include?(e)
|
27
|
+
path = File.join(dir, e)
|
28
|
+
# grep symlinked files but don't follow symlinked
|
29
|
+
# directories
|
30
|
+
if File.directory?(path) and not File.symlink?(path)
|
31
|
+
traverse(path, issues)
|
32
|
+
elsif File.file?(path)
|
33
|
+
grep_for_todo(path, issues)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def grep_for_todo(path, issues)
|
39
|
+
GREP_EXT_EXCLUDE.each do |e|
|
40
|
+
return if path.end_with?(e)
|
41
|
+
end
|
42
|
+
cf = CommentFilter.new(path)
|
43
|
+
spath = remove_common_path(path)
|
44
|
+
File.open(path, 'r') do |f|
|
45
|
+
linenr = 1
|
46
|
+
f.readlines.map {|line| cf.filter(line)}.each do |line|
|
47
|
+
m = line.match("[\\W](#{@todo_labels})\\(([\\w]+)\\):[\\W]+(.*)$")
|
48
|
+
if not m.nil?
|
49
|
+
loc = "#{spath}:#{linenr}"
|
50
|
+
tags = [Tag.new(m[1], tag_color(m[1])), Tag.new(m[2], rgb4string(m[2]))]
|
51
|
+
issues << Issue.new(m[3], loc, nil, tags)
|
52
|
+
next
|
53
|
+
end
|
54
|
+
m = line.match("[\\W](#{@todo_labels}):[\\W]+(.*)$")
|
55
|
+
if not m.nil?
|
56
|
+
loc = "#{spath}:#{linenr}"
|
57
|
+
issues << Issue.new(m[2], loc, nil, Tag.new(m[1], tag_color(m[1])))
|
58
|
+
next
|
59
|
+
end
|
60
|
+
m = line.match("[\\W](#{@todo_labels})[\\W]+(.*)$")
|
61
|
+
if not m.nil?
|
62
|
+
loc = "#{spath}:#{linenr}"
|
63
|
+
issues << Issue.new(m[2], loc, nil, Tag.new(m[1], tag_color(m[1])))
|
64
|
+
end
|
65
|
+
linenr += 1
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def remove_common_path(file)
|
71
|
+
begin
|
72
|
+
return Pathname.new(file).relative_path_from(Pathname.new(Dir.getwd))
|
73
|
+
rescue ArgumentError => e
|
74
|
+
return file
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def tag_color(title)
|
79
|
+
case title
|
80
|
+
when 'BUG'
|
81
|
+
color = "ff0000"
|
82
|
+
else
|
83
|
+
color = "ffffff"
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
class CommentFilter
|
89
|
+
def initialize(path)
|
90
|
+
@comment_char = '#' if path.end_with?('.rb')
|
91
|
+
end
|
92
|
+
|
93
|
+
# TODO: build comment filter
|
94
|
+
def filter(line)
|
95
|
+
# disable it for now
|
96
|
+
line
|
97
|
+
# return line if @comment_char.nil? or line.nil?
|
98
|
+
# i = line.index(@comment_char)
|
99
|
+
# return '' if i.nil?
|
100
|
+
# line[i, line.length-i]
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
|
2
|
+
module CTodo
|
3
|
+
# needs Redmine 1.1 at least
|
4
|
+
class Redmine
|
5
|
+
include HTTParty
|
6
|
+
|
7
|
+
def initialize(conf, prefix = 'red')
|
8
|
+
red_uri = conf[:"#{prefix}_uri"]
|
9
|
+
red_key = conf[:"#{prefix}_key"]
|
10
|
+
@enabled = (!red_uri.nil? and !red_key.nil? and !conf[:git_repo_dir].nil?)
|
11
|
+
if @enabled
|
12
|
+
self.class.base_uri(red_uri)
|
13
|
+
rand_pwd = (0...8).map {65.+(rand(25)).chr}.join
|
14
|
+
self.class.basic_auth(red_key, rand_pwd)
|
15
|
+
@repo = File.basename(conf[:git_repo_dir])
|
16
|
+
# HACK: 1000 should really be enough
|
17
|
+
@limit = (conf[:all] ? 1000 : 25)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def get_issues(issues)
|
22
|
+
return if not @enabled
|
23
|
+
|
24
|
+
# all projects
|
25
|
+
r = self.class.get "/projects.json"
|
26
|
+
return if r.code != 200
|
27
|
+
|
28
|
+
# get id for matching project identifier (not name!)
|
29
|
+
p_id = find_id_for_identifier(r['projects'], @repo)
|
30
|
+
return if p_id == nil
|
31
|
+
|
32
|
+
r = self.class.get "/issues.json?project_id=#{p_id}&offset=0&limit=#{@limit}"
|
33
|
+
|
34
|
+
r['issues'].each do |i|
|
35
|
+
loc = "#{self.class.base_uri}/issues/#{i['id']}"
|
36
|
+
if i['assigned_to'].nil?
|
37
|
+
assig = nil
|
38
|
+
else
|
39
|
+
assig = i['assigned_to']['name']
|
40
|
+
# BUG: missing login attribute in API, http://www.redmine.org/projects/redmine/wiki/Rest_Users
|
41
|
+
#u_id = i['assigned_to']['id']
|
42
|
+
#r2 = self.class.get "/users/#{u_id}.xml"
|
43
|
+
#assig = r2['user']['login']
|
44
|
+
end
|
45
|
+
tags = []
|
46
|
+
tags << Tag.new(i['tracker']['name'], "ffffff") if not i['tracker'].nil?
|
47
|
+
tags << Tag.new(i['category']['name'], ColorUtils.rgb4string(i['category']['name'])) if not i['category'].nil?
|
48
|
+
tags << Tag.new(i['fixed_version']['name'], "000000") if not i['fixed_version'].nil?
|
49
|
+
issues << Issue.new(i['subject'], loc, assig, tags)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def find_id_for_identifier(projects, ident)
|
54
|
+
projects.each do |p|
|
55
|
+
return p['id'] if ident == p['identifier']
|
56
|
+
end
|
57
|
+
nil
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
data/lib/ctodo.rb
ADDED
@@ -0,0 +1,161 @@
|
|
1
|
+
|
2
|
+
require 'optparse'
|
3
|
+
require 'pathname'
|
4
|
+
|
5
|
+
# ruby gem dependencies
|
6
|
+
require 'httparty'
|
7
|
+
require 'inifile'
|
8
|
+
require 'json'
|
9
|
+
|
10
|
+
# ctodo
|
11
|
+
require 'ctodo/color'
|
12
|
+
require 'ctodo/localfs'
|
13
|
+
require 'ctodo/github'
|
14
|
+
require 'ctodo/redmine'
|
15
|
+
|
16
|
+
class Integer
|
17
|
+
def clamp(min, max)
|
18
|
+
return min if self < min
|
19
|
+
return max if self > max
|
20
|
+
self
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
module Enumerable
|
25
|
+
def median
|
26
|
+
sorted = self.sort
|
27
|
+
sorted[self.length/2]
|
28
|
+
end
|
29
|
+
|
30
|
+
def sum
|
31
|
+
self.inject(0) { |sum,v| sum + v }
|
32
|
+
end
|
33
|
+
|
34
|
+
def avg
|
35
|
+
(length == 0) ? 0 : (sum * 1.0 / length)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
module CTodo
|
40
|
+
class Tag
|
41
|
+
attr_reader :title, :color
|
42
|
+
|
43
|
+
def initialize(title, color)
|
44
|
+
@title = title
|
45
|
+
@color = color.to_i(16)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
class Issue
|
50
|
+
attr_reader :title, :source, :assignee, :tags
|
51
|
+
|
52
|
+
def initialize(title, source, assignee, tags)
|
53
|
+
@title = title
|
54
|
+
@source = source
|
55
|
+
@assignee = assignee
|
56
|
+
@tags = tags.is_a?(Array) ? tags : [tags]
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
class Prog
|
61
|
+
TPAD_FAC = 7
|
62
|
+
MIN_TPAD = TPAD_FAC*3
|
63
|
+
MAX_TPAD = TPAD_FAC*8
|
64
|
+
|
65
|
+
def run!
|
66
|
+
@options = {
|
67
|
+
:cs => COLOR_SETS[0],
|
68
|
+
:limit_text => false,
|
69
|
+
:show_source => false,
|
70
|
+
:show_assignee => false
|
71
|
+
}
|
72
|
+
|
73
|
+
@opts = OptionParser.new do |opts|
|
74
|
+
opts.banner = "Usage: todo.rb [options]"
|
75
|
+
opts.banner += "\nOptions:\n"
|
76
|
+
|
77
|
+
opts.on('--all', 'Show all todos (some providers limit to a reasonable number)') do @options[:all] = true end
|
78
|
+
opts.on('-l', '--limit-text', 'Limit todo text to fixed width') do @options[:limit_text] = true end
|
79
|
+
opts.on('-s', '--source', 'Show source of todos') do @options[:show_source] = true end
|
80
|
+
opts.on('-a', '--assignee', 'Show assignee of todos (when present)') do @options[:show_assignee] = true end
|
81
|
+
opts.on('-u CREDS', '--gh-user CREDS', 'Specify github credentials as USER:PASS') do |u| @options[:gh_creds] = u end
|
82
|
+
opts.on('-c CS', '--color-set CS', COLOR_SETS, "There are: #{COLOR_SETS.join(', ')}") do |cs|
|
83
|
+
@options[:cs] = cs if ['Fg', 'Bg', 'None'].include?(cs)
|
84
|
+
end
|
85
|
+
opts.on('-h', '-?', '--help', 'Display this screen') do
|
86
|
+
puts opts
|
87
|
+
exit
|
88
|
+
end
|
89
|
+
end
|
90
|
+
@opts.parse!
|
91
|
+
|
92
|
+
conf = load_config(File.join(ENV['HOME'], '.todo'))
|
93
|
+
|
94
|
+
conf[:cur_dir] = Dir.getwd
|
95
|
+
conf[:git_repo_dir] = find_git_repo(Dir.getwd)
|
96
|
+
conf[:all] = @options[:all]
|
97
|
+
conf[:cs] = CTodo.const_get("#{@options[:cs]}ColorSet").new
|
98
|
+
|
99
|
+
ip = [LocalFS.new(conf), Github.new(conf), Redmine.new(conf), Redmine.new(conf, 'red2')]
|
100
|
+
|
101
|
+
issues = []
|
102
|
+
ip.each do |ip| ip.get_issues(issues) end
|
103
|
+
|
104
|
+
# calculate text spacing
|
105
|
+
if @options[:limit_text]
|
106
|
+
title_padding = MAX_TPAD
|
107
|
+
elsif issues.empty?
|
108
|
+
title_padding = MIN_TPAD
|
109
|
+
else
|
110
|
+
title_lens = issues.map { |i| i.title.length }
|
111
|
+
max_title_len = [title_lens.avg, title_lens.median].max
|
112
|
+
title_padding = (((max_title_len/TPAD_FAC.to_f).ceil)*TPAD_FAC).to_i.clamp(MIN_TPAD,MAX_TPAD)
|
113
|
+
end
|
114
|
+
|
115
|
+
issues.each do |i|
|
116
|
+
if @options[:limit_text] and i.title.length > title_padding
|
117
|
+
title = i.title[0,title_padding-3] + '...'
|
118
|
+
else
|
119
|
+
title = i.title
|
120
|
+
end
|
121
|
+
|
122
|
+
tag_list = i.tags.map { |t|
|
123
|
+
[ColorUtils.func4rgb(t.color, conf[:cs]).call, t.title, conf[:cs].rst].join
|
124
|
+
}
|
125
|
+
tag_list.push "@#{i.assignee}" if @options[:show_assignee] and not i.assignee.nil?
|
126
|
+
tag_list = (tag_list.empty? ? '' : format('(%s) ', tag_list.join(', ')))
|
127
|
+
|
128
|
+
if @options[:show_source]
|
129
|
+
puts format("- %-#{title_padding}s %sin %s",
|
130
|
+
title, tag_list, i.source)
|
131
|
+
else
|
132
|
+
puts format("- %-#{title_padding}s %s",
|
133
|
+
title, tag_list)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def load_config(config_file)
|
139
|
+
conf = {}
|
140
|
+
if File.exists?(config_file)
|
141
|
+
conf.merge!(YAML.load_file(config_file))
|
142
|
+
end
|
143
|
+
if @options.key?(:gh_creds)
|
144
|
+
u,p = @options[:gh_creds].split(':')
|
145
|
+
conf.merge!({:gh_user => u, :gh_pass => p})
|
146
|
+
end
|
147
|
+
conf
|
148
|
+
end
|
149
|
+
|
150
|
+
def find_git_repo(path)
|
151
|
+
git_dir = Pathname.new(!ENV['GIT_DIR'].nil? ? ENV['GIT_DIR'] : '.git')
|
152
|
+
p = Pathname.new(path)
|
153
|
+
until p.root?
|
154
|
+
git_dir = p + git_dir
|
155
|
+
return p.to_s if git_dir.exist? and git_dir.directory?
|
156
|
+
p = p.parent
|
157
|
+
end
|
158
|
+
nil
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
metadata
ADDED
@@ -0,0 +1,112 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ctodo
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 29
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
- 1
|
10
|
+
version: 0.0.1
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Christian Nicolai
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2011-10-02 00:00:00 Z
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: httparty
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ">="
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
hash: 3
|
29
|
+
segments:
|
30
|
+
- 0
|
31
|
+
version: "0"
|
32
|
+
type: :runtime
|
33
|
+
version_requirements: *id001
|
34
|
+
- !ruby/object:Gem::Dependency
|
35
|
+
name: inifile
|
36
|
+
prerelease: false
|
37
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
38
|
+
none: false
|
39
|
+
requirements:
|
40
|
+
- - ">="
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
hash: 3
|
43
|
+
segments:
|
44
|
+
- 0
|
45
|
+
version: "0"
|
46
|
+
type: :runtime
|
47
|
+
version_requirements: *id002
|
48
|
+
- !ruby/object:Gem::Dependency
|
49
|
+
name: json
|
50
|
+
prerelease: false
|
51
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
52
|
+
none: false
|
53
|
+
requirements:
|
54
|
+
- - ">="
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
hash: 3
|
57
|
+
segments:
|
58
|
+
- 0
|
59
|
+
version: "0"
|
60
|
+
type: :runtime
|
61
|
+
version_requirements: *id003
|
62
|
+
description: ""
|
63
|
+
email: chrnicolai@gmail.com
|
64
|
+
executables:
|
65
|
+
- todo
|
66
|
+
extensions: []
|
67
|
+
|
68
|
+
extra_rdoc_files: []
|
69
|
+
|
70
|
+
files:
|
71
|
+
- bin/todo
|
72
|
+
- lib/ctodo/color.rb
|
73
|
+
- lib/ctodo/github.rb
|
74
|
+
- lib/ctodo/localfs.rb
|
75
|
+
- lib/ctodo/redmine.rb
|
76
|
+
- lib/ctodo.rb
|
77
|
+
- README.md
|
78
|
+
homepage: https://github.com/cmur2/todo
|
79
|
+
licenses: []
|
80
|
+
|
81
|
+
post_install_message:
|
82
|
+
rdoc_options: []
|
83
|
+
|
84
|
+
require_paths:
|
85
|
+
- lib
|
86
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
87
|
+
none: false
|
88
|
+
requirements:
|
89
|
+
- - ">="
|
90
|
+
- !ruby/object:Gem::Version
|
91
|
+
hash: 3
|
92
|
+
segments:
|
93
|
+
- 0
|
94
|
+
version: "0"
|
95
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
96
|
+
none: false
|
97
|
+
requirements:
|
98
|
+
- - ">="
|
99
|
+
- !ruby/object:Gem::Version
|
100
|
+
hash: 3
|
101
|
+
segments:
|
102
|
+
- 0
|
103
|
+
version: "0"
|
104
|
+
requirements: []
|
105
|
+
|
106
|
+
rubyforge_project: ctodo
|
107
|
+
rubygems_version: 1.8.6
|
108
|
+
signing_key:
|
109
|
+
specification_version: 3
|
110
|
+
summary: Combined Todo
|
111
|
+
test_files: []
|
112
|
+
|