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