├── .red ├── README.md └── red.rb /.red: -------------------------------------------------------------------------------- 1 | # Example configuration file for redcmd http://github.com/textgoeshere/redcmd 2 | username: dave 3 | password: d4ve 4 | url: http://www.myredmineinstance.com 5 | project: redcmd 6 | tracker: bug 7 | priorty: normal 8 | 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Red creates [Redmine](http://www.redmine.org/) issues from the command line. 2 | 3 | NB: This library is no longer maintained. 4 | ----------------------------------------- 5 | 6 | 0.3 (c) 2009 Dave Nolan 7 | 8 | [http://textgoeshere.org.uk](http://textgoeshere.org.uk) 9 | 10 | [http://github.com/textgoeshere/redcmd](http://github.com/textgoeshere/redcmd) 11 | 12 | Released under MIT license 13 | ========================== 14 | 15 | Permission is hereby granted, free of charge, to any person obtaining 16 | a copy of this software and associated documentation files (the 17 | "Software"), to deal in the Software without restriction, including 18 | without limitation the rights to use, copy, modify, merge, publish, 19 | distribute, sublicense, and/or sell copies of the Software, and to 20 | permit persons to whom the Software is furnished to do so, subject to 21 | the following conditions: 22 | 23 | The above copyright notice and this permission notice shall be 24 | included in all copies or substantial portions of the Software. 25 | 26 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 27 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 28 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 29 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 30 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 31 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 32 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 33 | 34 | Example 35 | ======= 36 | 37 | Add an issue: 38 | 39 | red add -s "New feature" -d "Some longer description text" -t feature -p cashprinter -r high -a "Dave" -f /path/to/attachment 40 | # => 41 | "Created Feature #999 New feature" 42 | 43 | List some issues (you can reference a Redmine custom query here): 44 | 45 | red list 3 46 | # => 47 | Fix widget 48 | Design thingy 49 | Document Windows 95 compatibility issues 50 | 51 | 52 | Command line arguments override settings in the configuration file, which override Redmine form defaults. 53 | 54 | An example configuration file 55 | ============================= 56 | 57 | username: dave 58 | password: d4ve 59 | url: http://www.myredmineinstance.com 60 | project: redcmd 61 | tracker: bug 62 | priorty: normal 63 | 64 | I recommend creating a configuration file per-project, and sticking it in your path. 65 | 66 | TODO 67 | ==== 68 | 69 | * due, start, done, est. hours 70 | * custom fields 71 | * subcommands (list, update, etc.) 72 | -------------------------------------------------------------------------------- /red.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | VER = "0.3 (c) 2009 Dave Nolan textgoeshere.org.uk, github.com/textgoeshere/redcmd" 3 | BANNER =<<-EOS 4 | Red creates Redmine (http://www.redmine.org/) issues from the command line. 5 | 6 | ==Example== 7 | Add an issue: 8 | 9 | red add -s "New feature" -d "Some longer description text" -t feature -p cashprinter -r high -a "Dave" -f /path/to/attachment 10 | # => 11 | "Created Feature #999 New feature" 12 | 13 | List some issues (you can reference a Redmine custom query here): 14 | 15 | red list 3 16 | # => 17 | Fix widget 18 | Design thingy 19 | Document Windows 95 compatibility issues 20 | 21 | Command line arguments override settings in the configuration file, which override Redmine form defaults. 22 | 23 | ==An example configuration file== 24 | username: dave 25 | password: d4ve 26 | url: http://www.myredmineinstance.com 27 | project: redcmd 28 | tracker: bug 29 | priorty: normal 30 | 31 | I recommend creating a configuration file per-project, and sticking it in your path. 32 | 33 | # TODO: due, start, done, est. hours 34 | # TODO: custom fields 35 | # TODO: subcommands (list, update, etc.) 36 | 37 | #{VER} 38 | 39 | ==Options== 40 | 41 | EOS 42 | 43 | 44 | begin 45 | require 'trollop' 46 | require 'mechanize' 47 | require 'yaml' 48 | rescue LoadError 49 | require 'rubygems' 50 | require 'trollop' 51 | require 'mechanize' 52 | require 'yaml' 53 | end 54 | 55 | module Textgoeshere 56 | class RedmineError < StandardError; end 57 | 58 | class Red 59 | SELECTS = %w{priority tracker category assigned_to status} 60 | 61 | def initialize(command, opts) 62 | @opts = opts 63 | @mech = Mechanize.new 64 | login 65 | send(command) 66 | end 67 | 68 | private 69 | 70 | def login 71 | @mech.get login_url 72 | @mech.page.form_with(:action => login_action) do |f| 73 | f.field_with(:name => 'username').value = @opts[:username] 74 | f.field_with(:name => 'password').value = @opts[:password] 75 | f.click_button 76 | end 77 | catch_redmine_errors 78 | end 79 | 80 | def add 81 | @mech.get new_issue_url 82 | @mech.page.form_with(:action => create_issue_action) do |f| 83 | SELECTS.each do |name| 84 | value = @opts[name.to_sym] 85 | unless value.nil? 86 | field = f.field_with(:name => "issue[#{name}_id]") 87 | field.value = field.options.detect { |o| o.text.downcase =~ Regexp.new(value) } 88 | raise RedmineError.new("Cannot find #{name} #{value}") if field.value.nil? || field.value.empty? 89 | end 90 | end 91 | f.field_with(:name => 'issue[subject]').value = @opts[:subject] 92 | f.field_with(:name => 'issue[description]').value = @opts[:description] || @opts[:subject] 93 | @opts[:file].each_with_index do |file, i| 94 | f.file_uploads_with(:name => "attachments[#{i.to_s()}][file]").first.file_name = file 95 | end 96 | f.click_button 97 | catch_redmine_errors 98 | puts "Created #{@mech.page.search('h2').text}: #{@opts[:subject]}" 99 | end 100 | end 101 | 102 | def list 103 | @mech.get(list_issues_url) 104 | issues = @mech.page.parser.xpath('//table[@class="list issues"]/tbody//tr') 105 | if issues.empty? 106 | puts "No issues found at #{list_issues_url}" 107 | else 108 | @opts[:number].times do |i| 109 | issue = issues[i] 110 | break unless issue 111 | subject = issue.xpath('td[@class="subject"]/a').inner_html 112 | puts subject 113 | end 114 | end 115 | end 116 | 117 | def login_action; '/login'; end 118 | def login_url; "#{@opts[:url]}#{login_action}"; end 119 | 120 | def create_issue_action; "/projects/#{@opts[:project]}/issues/new"; end 121 | def new_issue_url; "#{@opts[:url]}#{create_issue_action}"; end 122 | def list_issues_url 123 | params = @opts[:query_id] ? "?query_id=#{@opts[:query_id]}" : "" 124 | "#{@opts[:url]}/projects/#{@opts[:project]}/issues#{params}" 125 | end 126 | 127 | def catch_redmine_errors 128 | error_flash = @mech.page.search('.flash.error')[0] 129 | raise RedmineError.new(error_flash.text) if error_flash 130 | end 131 | end 132 | end 133 | 134 | # NOTE: Trollop's default default for boolean values is false, not nil, so if extending to include boolean options ensure you explicity set :default => nil 135 | 136 | COMMANDS = %w(add list) 137 | 138 | global_options = Trollop::options do 139 | banner BANNER 140 | opt :username, "Username", :type => String, :short => 'u' 141 | opt :password, "Password", :type => String, :short => 'p' 142 | opt :url, "Url to redmine", :type => String 143 | opt :project, "Project identifier", :type => String 144 | opt :filename, "Configuration file, YAML format, specifying default options.", 145 | :type => String, :default => ".red" 146 | version VER 147 | stop_on COMMANDS 148 | end 149 | 150 | command = ARGV.shift 151 | command_options = case command 152 | when "add" 153 | Trollop::options do 154 | opt :subject, "Issue subject (title). This must be wrapped in inverted commas like this: \"My new feature\".", 155 | :type => String, :required => true 156 | opt :description, "Description", :type => String 157 | opt :tracker, "Tracker (bug, feature etc.)", :type => String 158 | opt :assigned_to, "Assigned to", :type => String 159 | opt :priority, "Priority", :type => String 160 | opt :status, "Status", :type => String, :short => 'x' 161 | opt :category, "Category", :type => String 162 | opt :file, "File", :type => String, :multi => true 163 | end 164 | when "list" 165 | Trollop::options do 166 | opt :number, "Number of issues to display", :type => Integer, :default => 5 167 | opt :query_id, "Optional custom query id", :type => Integer 168 | end 169 | else 170 | Trollop::die "Uknown command #{command}" 171 | end 172 | 173 | opts = global_options.merge(command_options) 174 | YAML::load_file(opts[:filename]).each_pair { |name, default| opts[name.to_sym] ||= default } if File.exist?(opts[:filename]) 175 | Textgoeshere::Red.new(command, opts) 176 | --------------------------------------------------------------------------------