redmine-cli 0.1.1 → 0.1.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile.lock +23 -17
- data/LICENSE +20 -0
- data/README.md +110 -0
- data/lib/redmine-cli.rb +1 -1
- data/lib/redmine-cli/cli.rb +419 -25
- data/lib/redmine-cli/config.rb +48 -0
- data/lib/redmine-cli/field.rb +18 -0
- data/lib/redmine-cli/generators/.redmine +7 -0
- data/lib/redmine-cli/generators/install.rb +4 -3
- data/lib/redmine-cli/git.rb +1 -1
- data/lib/redmine-cli/resources.rb +64 -0
- data/lib/redmine-cli/version.rb +1 -1
- data/redmine-cli.gemspec +6 -1
- metadata +40 -77
- data/README.markdown +0 -44
- data/lib/redmine-cli/issue.rb +0 -22
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA512:
|
3
|
+
metadata.gz: cc478e4f17439fc1f2a7d167365e96b271c12c846182000c3c7dc5563b751a8c9e4f33add97d6df6195b60a7624db6a61eff7987bc8c6fad051c32688b71b1dc
|
4
|
+
data.tar.gz: c40552a5ff0f9b21b73190a151ae48a03fde7625828118ee5cbf64725ec66c7818a7448fc140a026e70f4d5be824520d8c8013ced4502714e1c94255a908080b
|
5
|
+
SHA1:
|
6
|
+
metadata.gz: 46a0d3ee489a5d807551fb3d1b38220e55ff0333
|
7
|
+
data.tar.gz: 8b27f9c02e87d3c635db32fea2a6a707cdae243e
|
data/Gemfile.lock
CHANGED
@@ -1,27 +1,28 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
redmine-cli (0.1.
|
4
|
+
redmine-cli (0.1.3)
|
5
5
|
activeresource (~> 3.0.0)
|
6
|
+
interactive_editor
|
6
7
|
thor
|
7
8
|
|
8
9
|
GEM
|
9
10
|
remote: http://rubygems.org/
|
10
11
|
specs:
|
11
|
-
activemodel (3.0.
|
12
|
-
activesupport (= 3.0.
|
12
|
+
activemodel (3.0.20)
|
13
|
+
activesupport (= 3.0.20)
|
13
14
|
builder (~> 2.1.2)
|
14
|
-
i18n (~> 0.
|
15
|
-
activeresource (3.0.
|
16
|
-
activemodel (= 3.0.
|
17
|
-
activesupport (= 3.0.
|
18
|
-
activesupport (3.0.
|
15
|
+
i18n (~> 0.5.0)
|
16
|
+
activeresource (3.0.20)
|
17
|
+
activemodel (= 3.0.20)
|
18
|
+
activesupport (= 3.0.20)
|
19
|
+
activesupport (3.0.20)
|
19
20
|
aruba (0.2.3)
|
20
21
|
background_process
|
21
22
|
cucumber (~> 0.9.0)
|
22
23
|
background_process (1.2)
|
23
24
|
builder (2.1.2)
|
24
|
-
columnize (0.3.
|
25
|
+
columnize (0.3.6)
|
25
26
|
cucumber (0.9.3)
|
26
27
|
builder (~> 2.1.2)
|
27
28
|
diff-lcs (~> 1.1.2)
|
@@ -29,12 +30,17 @@ GEM
|
|
29
30
|
json (~> 1.4.6)
|
30
31
|
term-ansicolor (~> 1.0.5)
|
31
32
|
diff-lcs (1.1.2)
|
33
|
+
ffi (1.9.3)
|
32
34
|
gherkin (2.2.9)
|
33
35
|
json (~> 1.4.6)
|
34
36
|
term-ansicolor (~> 1.0.5)
|
35
|
-
i18n (0.
|
37
|
+
i18n (0.5.3)
|
38
|
+
interactive_editor (0.0.10)
|
39
|
+
spoon (>= 0.0.1)
|
36
40
|
json (1.4.6)
|
37
|
-
linecache (0.
|
41
|
+
linecache (0.46)
|
42
|
+
rbx-require-relative (> 0.0.4)
|
43
|
+
rbx-require-relative (0.0.9)
|
38
44
|
rspec (2.0.1)
|
39
45
|
rspec-core (~> 2.0.1)
|
40
46
|
rspec-expectations (~> 2.0.1)
|
@@ -45,22 +51,22 @@ GEM
|
|
45
51
|
rspec-mocks (2.0.1)
|
46
52
|
rspec-core (~> 2.0.1)
|
47
53
|
rspec-expectations (~> 2.0.1)
|
48
|
-
ruby-debug (0.10.
|
54
|
+
ruby-debug (0.10.4)
|
49
55
|
columnize (>= 0.1)
|
50
|
-
ruby-debug-base (~> 0.10.
|
51
|
-
ruby-debug-base (0.10.
|
56
|
+
ruby-debug-base (~> 0.10.4.0)
|
57
|
+
ruby-debug-base (0.10.4)
|
52
58
|
linecache (>= 0.3)
|
59
|
+
spoon (0.0.4)
|
60
|
+
ffi
|
53
61
|
term-ansicolor (1.0.5)
|
54
|
-
thor (0.
|
62
|
+
thor (0.18.1)
|
55
63
|
|
56
64
|
PLATFORMS
|
57
65
|
ruby
|
58
66
|
|
59
67
|
DEPENDENCIES
|
60
|
-
activeresource (~> 3.0.0)
|
61
68
|
aruba
|
62
69
|
cucumber
|
63
70
|
redmine-cli!
|
64
71
|
rspec
|
65
72
|
ruby-debug
|
66
|
-
thor
|
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2012 Jorge Dias
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
# redmine-cli
|
2
|
+
### A command line interface for redmine
|
3
|
+
Because using the browser is overrated.
|
4
|
+
[![Gem Version](https://badge.fury.io/rb/redmine-cli.png)](http://badge.fury.io/rb/redmine-cli)
|
5
|
+
|
6
|
+
## Installation
|
7
|
+
You first need to have Ruby with RubyGems.
|
8
|
+
|
9
|
+
Then run:
|
10
|
+
|
11
|
+
gem install redmine-cli
|
12
|
+
redmine install
|
13
|
+
|
14
|
+
This will create a .redmine file in your home directory. The file is a yaml file which contains our necessary configuration
|
15
|
+
|
16
|
+
During install, you can select the fields that you wish to be displayed, or accept the default (url, status, subject). This list can contain custom fields.
|
17
|
+
|
18
|
+
Note that previous versions of redmine-cli installed a version of .redmine that do not take full advantage of new features. For compatiblity purposes, this version is compatible with older .redmine files. However, for best results, you should re-run "redmine install" every time you upgrade the gem.
|
19
|
+
|
20
|
+
## Usage
|
21
|
+
You can get help by simpling executing:
|
22
|
+
|
23
|
+
redmine
|
24
|
+
|
25
|
+
Listing tickets
|
26
|
+
|
27
|
+
redmine list
|
28
|
+
redmine list -a me -T bug
|
29
|
+
|
30
|
+
Display ticket
|
31
|
+
|
32
|
+
redmine show 524
|
33
|
+
|
34
|
+
Updating a ticket
|
35
|
+
|
36
|
+
redmine update 524 -description "New description"
|
37
|
+
redmine update 256 --assigned_to me
|
38
|
+
|
39
|
+
Updating multiple tickets
|
40
|
+
|
41
|
+
redmine update 2 3 4 --assigned_to johndoe
|
42
|
+
|
43
|
+
Updating all tickets for a list
|
44
|
+
|
45
|
+
redmine list --status new --std_output | xargs redmine update --asigned_to me --status 3 -l
|
46
|
+
|
47
|
+
(Note that the last argument of the update command must be -l)
|
48
|
+
|
49
|
+
Interactively editing a ticket's fields
|
50
|
+
|
51
|
+
redmine edit --description 2
|
52
|
+
|
53
|
+
Your editor will pop up, and you can modify the field. The ticket will be updated when you save the file and exit the editor.
|
54
|
+
|
55
|
+
## Configuration
|
56
|
+
Redmine-cli will install a default configuration file. However you can edit it to fit your redmine installation.
|
57
|
+
You can add mappings for users, statuses, custom queries, and trackers like:
|
58
|
+
|
59
|
+
user_mappings:
|
60
|
+
"me": 1
|
61
|
+
"johndoe": 24
|
62
|
+
status_mappings:
|
63
|
+
"new": 1
|
64
|
+
"closed": 4
|
65
|
+
tracker_mappings:
|
66
|
+
"bug" : 1
|
67
|
+
"feature" : 2
|
68
|
+
|
69
|
+
This will allow to use those names with the commands instead of the ids your redmine installation uses.
|
70
|
+
|
71
|
+
Additionally, you can choose which fields are displayed when you use "redmine list" by editing the list_fields section like:
|
72
|
+
list_fields:
|
73
|
+
- project
|
74
|
+
- id
|
75
|
+
- tracker
|
76
|
+
- status
|
77
|
+
- priority
|
78
|
+
- assigned_to
|
79
|
+
- subject
|
80
|
+
- updated_on
|
81
|
+
|
82
|
+
You can choose from any of these fields:
|
83
|
+
|
84
|
+
* url - A "clickable" link to the issue
|
85
|
+
* id - The ID number of the issue
|
86
|
+
* subject
|
87
|
+
* status - Open, Closed, Resolved, etc.
|
88
|
+
* start_date
|
89
|
+
* estimated_hours
|
90
|
+
* tracker - Bug, Feature, Improvement, etc.
|
91
|
+
* priority - Low, High, Immediate, etc. (Note: this field is colorized on terminals that support it)
|
92
|
+
* description
|
93
|
+
* assigned_to
|
94
|
+
* project
|
95
|
+
* author
|
96
|
+
* done_ratio
|
97
|
+
* due_date
|
98
|
+
* created_on
|
99
|
+
* updated_on
|
100
|
+
|
101
|
+
In addition to these fields, you can also specify any custom fields that you've configured in your Redmine site. If a field is not found on an issue, or the value is blank, then a blank value will be displayed in the list.
|
102
|
+
|
103
|
+
## Known Issues
|
104
|
+
|
105
|
+
If you use a non-administrative account, redmine-cli's mapping cache will not be able to retrieve the list of users (you must manually populate the user mappings in this case). Additionally, you'll receive an error like this whenever you try to update an issue:
|
106
|
+
|
107
|
+
Updating mapping cache...
|
108
|
+
Failed to fetch users: Failed. Response code = 403. Response message = Forbidden.
|
109
|
+
|
110
|
+
If this happens, you can disable the caching feature by setting "disable_caching": true in ~/.redmine
|
data/lib/redmine-cli.rb
CHANGED
data/lib/redmine-cli/cli.rb
CHANGED
@@ -1,39 +1,165 @@
|
|
1
1
|
require 'thor'
|
2
|
-
require 'redmine-cli/
|
2
|
+
require 'redmine-cli/field'
|
3
|
+
require 'redmine-cli/config'
|
4
|
+
require 'redmine-cli/resources'
|
3
5
|
require 'redmine-cli/generators/install'
|
6
|
+
require 'rubygems'
|
7
|
+
require 'interactive_editor'
|
8
|
+
require 'yaml'
|
9
|
+
require 'pp'
|
10
|
+
|
11
|
+
$KCODE='u'
|
4
12
|
|
5
13
|
module Redmine
|
6
14
|
module Cli
|
7
15
|
class CLI < Thor
|
16
|
+
|
8
17
|
desc "list", "List all issues for the user"
|
9
18
|
method_option :assigned_to, :aliases => "-a", :desc => "id or user name of person the ticket is assigned to"
|
10
19
|
method_option :status, :aliases => "-s", :desc => "id or name of status for ticket"
|
11
|
-
method_option :
|
12
|
-
|
20
|
+
method_option :project, :aliases => "-p", :desc => "project id"
|
21
|
+
method_option :version, :aliases => "-v", :desc => "the target version"
|
22
|
+
method_option :query, :aliases => "-q", :desc => "list issues according to a saved custom query"
|
23
|
+
method_option :tracker, :aliases => "-T", :desc => "id or name of tracker for ticket"
|
24
|
+
method_option :std_output, :aliases => "-o", :desc => "output a space-delimited list of ticket numbers that match the specified criteria (useful when piped to \"update\" command", :type => :boolean
|
25
|
+
method_option :sort, :aliases => "-S", :desc => "sort by the specified column"
|
26
|
+
|
13
27
|
def list
|
14
28
|
params = {}
|
15
29
|
|
16
30
|
params[:assigned_to_id] = map_user(options.assigned_to) if options.assigned_to
|
17
|
-
|
18
31
|
params[:status_id] = map_status(options.status) if options.status
|
32
|
+
params[:tracker_id] = map_tracker(options.tracker) if options.tracker
|
33
|
+
params[:project_id] = map_project(options.project) if options.project
|
34
|
+
params[:query_id] = map_query(options.query) if options.query
|
35
|
+
params[:sort] = options[:sort] if options[:sort]
|
36
|
+
|
37
|
+
collection = Issue.fetch_all(params)
|
19
38
|
|
20
|
-
|
39
|
+
selected_fields = Redmine::Cli::config.list_fields || ["url", "subject", "status"]
|
21
40
|
|
22
41
|
unless options.std_output
|
23
|
-
|
42
|
+
# collection.sort! {|i,j| i.status.id <=> j.status.id }
|
43
|
+
collection.sort! {|i,j| i.priority.id <=> j.priority.id }
|
44
|
+
|
45
|
+
# Retrieve the list of issue fields in selected_fields
|
46
|
+
issues = collection.collect { |issue| selected_fields.collect {| key |
|
47
|
+
|
48
|
+
assignee = ""
|
49
|
+
assignee = issue.assigned_to.name if issue.respond_to?(:assigned_to)
|
50
|
+
version = issue.fixed_version.name if issue.respond_to?(:fixed_version)
|
51
|
+
|
52
|
+
# Hack, because I don't feel like spending much time on this
|
53
|
+
next unless version == options.version
|
54
|
+
|
55
|
+
begin
|
56
|
+
# If this is a built-in field for which we have a title, ref, and display method, use that.
|
57
|
+
field = fields.fetch(key)
|
58
|
+
if field.display
|
59
|
+
value = issue.attributes.fetch(field.ref)
|
60
|
+
field.display.call(value)
|
61
|
+
else
|
62
|
+
f = fields.fetch(key).ref
|
63
|
+
issue.attributes.fetch(f)
|
64
|
+
end
|
65
|
+
rescue IndexError
|
66
|
+
# Otherwise, let's look for a custom field by that name.
|
67
|
+
if issue.attributes[:custom_fields].present?
|
68
|
+
issue.attributes[:custom_fields].collect { | field |
|
69
|
+
if field.attributes.fetch("name") == key
|
70
|
+
field.attributes.fetch("value")
|
71
|
+
end
|
72
|
+
}
|
73
|
+
end
|
74
|
+
""
|
75
|
+
#TODO: If the custom field doesn't exist, then we end up returning a blank value (not an error). I guess that's OK?
|
76
|
+
end
|
77
|
+
|
78
|
+
}}
|
24
79
|
|
25
80
|
if issues.any?
|
26
|
-
issues.insert(0,
|
81
|
+
issues.insert(0, selected_fields.collect {| key |
|
82
|
+
begin
|
83
|
+
fields.fetch(key).title
|
84
|
+
rescue IndexError
|
85
|
+
key
|
86
|
+
end
|
87
|
+
})
|
88
|
+
|
27
89
|
print_table(issues)
|
90
|
+
say "#{collection.count} issues - #{link_to_project(params[:project_id])}", :yellow
|
28
91
|
end
|
92
|
+
|
93
|
+
# Clean up after ourselves
|
94
|
+
issues.compact!
|
29
95
|
else
|
30
96
|
say collection.collect(&:id).join(" ")
|
31
97
|
end
|
32
98
|
end
|
33
99
|
|
100
|
+
desc "projects", "Lists all projects"
|
101
|
+
def projects
|
102
|
+
projects = Project.fetch_all.sort {|i,j| i.name <=> j.name}.collect { |project| [ project.id, project.identifier, project.name ] }
|
103
|
+
if projects.any?
|
104
|
+
projects.insert(0, ["Id", "Key", "Name"])
|
105
|
+
print_table(projects)
|
106
|
+
say "#{projects.count-1} projects - #{link_to_project}", :yellow
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
method_option :status, :type => :boolean, :aliases => "-s", :desc => "id or name of status for ticket"
|
111
|
+
method_option :priority, :type => :boolean, :aliases => "-p", :desc => "id or name of priority for ticket"
|
112
|
+
method_option :tracker, :type => :boolean, :aliases => "-T", :desc => "id or name of tracker for ticket"
|
113
|
+
method_option :subject, :type => :boolean, :aliases => "-t", :desc => "subject for ticket (title)"
|
114
|
+
method_option :description, :type => :boolean, :aliases => "-d", :desc => "description for ticket"
|
115
|
+
method_option :assigned_to, :type => :boolean, :aliases => "-a", :desc => "id or user name of person the ticket is assigned to"
|
116
|
+
method_option :notes, :type => :boolean, :aliases => "-n", :desc => "add a note to the Redmine ticket (interactively)"
|
117
|
+
desc "edit [OPTIONS] TICKET", "Interactively edit the fields of an issue"
|
118
|
+
def edit(ticket)
|
119
|
+
issue = Issue.find(ticket)
|
120
|
+
|
121
|
+
if options.empty?
|
122
|
+
say "You must specify at least one field to edit", :red
|
123
|
+
exit
|
124
|
+
end
|
125
|
+
|
126
|
+
data = {}
|
127
|
+
options.each { | key, val |
|
128
|
+
# Set the initial value to one of:
|
129
|
+
# The direct string representation of the issue's key
|
130
|
+
# The value of the issue's key's "name" member
|
131
|
+
# A multi-line string initialized to a message appropriate to the value (most useful for "notes" and "description")
|
132
|
+
initialval = issue.attributes.has_key?(key) && ! issue.attributes[key].nil? ? case issue.attributes[key]
|
133
|
+
when String then issue.attributes[key].to_s
|
134
|
+
else issue.attributes[key].name.to_s
|
135
|
+
end : "Enter your #{key} here.\n\n"
|
136
|
+
|
137
|
+
data[key] = initialval.clone.ed
|
138
|
+
|
139
|
+
if data[key] == initialval
|
140
|
+
# If the user didn't actually edit the field, then don't submit it for update.
|
141
|
+
say "You did not change the value, so the edit to #{key} was ignored.", :red
|
142
|
+
data.delete(key)
|
143
|
+
end
|
144
|
+
}
|
145
|
+
|
146
|
+
unless data.empty?
|
147
|
+
update_ticket(ticket, data.with_indifferent_access)
|
148
|
+
else
|
149
|
+
say "There was no new data to submit, so #{ticket} was not updated.", :red
|
150
|
+
end
|
151
|
+
|
152
|
+
|
153
|
+
rescue ActiveResource::ResourceNotFound
|
154
|
+
say "No ticket with number: #{ticket}", :red
|
155
|
+
end
|
156
|
+
|
34
157
|
desc "show TICKET", "Display information of a ticket"
|
35
158
|
def show(ticket)
|
36
|
-
|
159
|
+
params = {}
|
160
|
+
params[:params] = {:include => "journals,changesets"}
|
161
|
+
|
162
|
+
issue = Issue.find(ticket, params)
|
37
163
|
|
38
164
|
display_issue(issue)
|
39
165
|
rescue ActiveResource::ResourceNotFound
|
@@ -48,7 +174,7 @@ module Redmine
|
|
48
174
|
params =
|
49
175
|
Thor::CoreExt::HashWithIndifferentAccess.new(:subject => subject,
|
50
176
|
:description => description,
|
51
|
-
:project =>
|
177
|
+
:project => Redmine::Cli::config.default_project_id)
|
52
178
|
params.merge!(options)
|
53
179
|
|
54
180
|
unless params.project
|
@@ -66,9 +192,12 @@ module Redmine
|
|
66
192
|
|
67
193
|
method_option :tickets, :aliases => "-l", :desc => "list of tickets", :type => :array
|
68
194
|
method_option :status, :aliases => "-s", :desc => "id or name of status for ticket"
|
195
|
+
method_option :priority, :aliases => "-p", :desc => "id or name of priority for ticket"
|
196
|
+
method_option :tracker, :aliases => "-T", :desc => "id or name of tracker for ticket"
|
69
197
|
method_option :subject, :aliases => "-t", :desc => "subject for ticket (title)"
|
70
198
|
method_option :description, :aliases => "-d", :desc => "description for ticket"
|
71
199
|
method_option :assigned_to, :aliases => "-a", :desc => "id or user name of person the ticket is assigned to"
|
200
|
+
method_option :notes, :aliases => "-n", :desc => "an optional note about this update"
|
72
201
|
desc "update [TICKETS]", "Update tickets"
|
73
202
|
def update(*tickets)
|
74
203
|
tickets = options.tickets if tickets.blank? && options.tickets.present?
|
@@ -81,9 +210,9 @@ module Redmine
|
|
81
210
|
tickets.collect { |ticket| Thread.new { update_ticket(ticket, options) } }.each(&:join)
|
82
211
|
end
|
83
212
|
|
84
|
-
desc "install [URL][USERNAME]", "Generates a default configuration file"
|
213
|
+
desc "install [URL] [USERNAME] [FIELDS]", "Generates a default configuration file"
|
85
214
|
method_option :test, :type => :boolean
|
86
|
-
def install(url = "localhost:3000", username = "")
|
215
|
+
def install(url = "localhost:3000", username = "", fieldcsv="")
|
87
216
|
url = "http://#{url}" unless url =~ /\Ahttp/
|
88
217
|
|
89
218
|
if username.blank?
|
@@ -92,33 +221,154 @@ module Redmine
|
|
92
221
|
|
93
222
|
password = ask_password("Password?")
|
94
223
|
|
95
|
-
|
224
|
+
if fieldcsv.blank?
|
225
|
+
fieldcsv = ask("\nWhat fields should be displayed in \"redmine list\"?\n\nPossible values are: [" + fields.keys.join(", ") + "]\n\nEnter a list of comma-separated fields: ")
|
226
|
+
end
|
227
|
+
|
228
|
+
list_fields = fieldcsv.split(",")
|
229
|
+
|
230
|
+
arguments = [url, username, password, list_fields]
|
96
231
|
arguments.concat(["--test"]) if options.test
|
97
232
|
|
98
233
|
Redmine::Cli::Generators::Install.start(arguments)
|
99
234
|
end
|
100
235
|
|
101
236
|
no_tasks do
|
237
|
+
|
238
|
+
# Prints a table.
|
239
|
+
#
|
240
|
+
# ==== Parameters
|
241
|
+
# Array[Array[String, String, ...]]
|
242
|
+
#
|
243
|
+
# ==== Options
|
244
|
+
# indent<Integer>:: Indent the first column by indent value.
|
245
|
+
# colwidth<Integer>:: Force the first column to colwidth spaces wide.
|
246
|
+
#
|
247
|
+
def print_table(array, options = {}) # rubocop:disable MethodLength
|
248
|
+
return if array.empty?
|
249
|
+
|
250
|
+
formats, indent, colwidth = [], options[:indent].to_i, options[:colwidth]
|
251
|
+
options[:truncate] = terminal_width if options[:truncate] == true
|
252
|
+
|
253
|
+
formats << "%-#{colwidth + 2}s" if colwidth
|
254
|
+
start = colwidth ? 1 : 0
|
255
|
+
|
256
|
+
colcount = array.max { |a, b| a.size <=> b.size }.size
|
257
|
+
|
258
|
+
maximas = []
|
259
|
+
|
260
|
+
start.upto(colcount - 1) do |index|
|
261
|
+
maxima = array.map { |row|
|
262
|
+
skip = false
|
263
|
+
rowlen = row[index].to_s.chars.collect { | char |
|
264
|
+
# Deal with ASCII escape sequences (skip them), also acknowledge that
|
265
|
+
# a char might consist of more than one byte in the case of unicode characters.
|
266
|
+
bytes = char.bytes.to_a
|
267
|
+
if bytes[0] < 32
|
268
|
+
skip = true
|
269
|
+
end
|
270
|
+
if bytes[0] == 109 && skip == true
|
271
|
+
skip = false
|
272
|
+
0
|
273
|
+
next
|
274
|
+
end
|
275
|
+
#puts "#{bytes.to_s} #{char} skip=#{skip}"
|
276
|
+
# FIXME: Unicode characters in most terminal fonts take up somewhere
|
277
|
+
# between 1 and 2 columns. So this isn't exact. (But it's better than nothing)
|
278
|
+
skip == true ? 0 : char.bytes.to_a.length > 1 ? 2 : 1
|
279
|
+
}.compact.reduce(:+)
|
280
|
+
row[index].present? ? rowlen : 0
|
281
|
+
}.max
|
282
|
+
maximas << maxima
|
283
|
+
if index == colcount - 1
|
284
|
+
# Don't output 2 trailing spaces when printing the last column
|
285
|
+
formats << '%-s'
|
286
|
+
else
|
287
|
+
formats << "%-#{maxima + 2}s"
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
formats[0] = formats[0].insert(0, ' ' * indent)
|
292
|
+
formats << '%s'
|
293
|
+
|
294
|
+
array.each do |row|
|
295
|
+
sentence = ''
|
296
|
+
|
297
|
+
row.each_with_index do |column, index|
|
298
|
+
maxima = maximas[index]
|
299
|
+
|
300
|
+
if column.is_a?(Numeric)
|
301
|
+
if index == row.size - 1
|
302
|
+
# Don't output 2 trailing spaces when printing the last column
|
303
|
+
f = "%#{maxima}s"
|
304
|
+
else
|
305
|
+
f = "%#{maxima}s "
|
306
|
+
end
|
307
|
+
else
|
308
|
+
f = formats[index]
|
309
|
+
end
|
310
|
+
sentence << f % column.to_s
|
311
|
+
end
|
312
|
+
|
313
|
+
sentence = truncate(sentence, options[:truncate]) if options[:truncate]
|
314
|
+
puts sentence
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
102
318
|
def link_to_issue(id)
|
103
|
-
"#{
|
319
|
+
"#{Redmine::Cli::config.url}/issues/#{id}"
|
320
|
+
end
|
321
|
+
|
322
|
+
def link_to_project(name = nil)
|
323
|
+
if name
|
324
|
+
"#{Redmine::Cli::config.url}/projects/#{name}/issues"
|
325
|
+
else
|
326
|
+
"#{Redmine::Cli::config.url}"
|
327
|
+
end
|
328
|
+
end
|
329
|
+
|
330
|
+
def status_name(status)
|
331
|
+
status.name
|
104
332
|
end
|
105
333
|
|
106
334
|
def ticket_attributes(options)
|
107
335
|
attributes = {}
|
108
336
|
|
109
|
-
attributes[:subject] = options
|
110
|
-
attributes[:description] = options
|
111
|
-
attributes[:project_id] = options
|
112
|
-
attributes[:assigned_to_id] = map_user(options
|
113
|
-
attributes[:status_id] = options
|
337
|
+
attributes[:subject] = options["subject"] if options["subject"].present?
|
338
|
+
attributes[:description] = options["description"] if options["description"].present?
|
339
|
+
attributes[:project_id] = map_project(options["project"]) if options["project"].present?
|
340
|
+
attributes[:assigned_to_id] = map_user(options["assigned_to"]) if options["assigned_to"].present?
|
341
|
+
attributes[:status_id] = map_status(options["status"]) if options["status"].present?
|
342
|
+
attributes[:priority_id] = map_priority(options["priority"])if options["priority"].present?
|
343
|
+
attributes[:tracker_id] = map_tracker(options["tracker"]) if options["tracker"].present?
|
344
|
+
attributes[:notes] = options["notes"] if options["notes"].present?
|
114
345
|
|
115
346
|
attributes
|
116
347
|
end
|
117
348
|
|
118
349
|
def display_issue(issue)
|
119
|
-
shell.print_wrapped "#{link_to_issue(issue.id)} - #{issue.status.name}"
|
350
|
+
shell.print_wrapped "#{issue.project.name} ##{issue.id} - #{link_to_issue(issue.id)} - #{issue.status.name}"
|
351
|
+
shell.print_wrapped "Priority: #{issue.priority.name}"
|
352
|
+
if issue.attributes[:assigned_to].present?
|
353
|
+
shell.print_wrapped "Assigned To: #{issue.attributes[:assigned_to].name}"
|
354
|
+
end
|
120
355
|
shell.print_wrapped "Subject: #{issue.subject}"
|
121
|
-
shell.print_wrapped issue.
|
356
|
+
shell.print_wrapped "Tracker: #{issue.tracker.name}"
|
357
|
+
shell.print_wrapped issue.description || "", :indent => 2
|
358
|
+
if issue.journals.any?
|
359
|
+
issue.journals.each do |journal|
|
360
|
+
details = journal.details.collect do |detail|
|
361
|
+
unless detail.name == "description"
|
362
|
+
"#{detail.name}: #{detail.old_value} => #{detail.new_value}"
|
363
|
+
else
|
364
|
+
# Multi-line descriptions can make output unreadable.
|
365
|
+
"#{detail.name} updated"
|
366
|
+
end
|
367
|
+
end
|
368
|
+
shell.print_wrapped "Updated by #{journal.user.name} on #{journal.created_on} (#{details})"
|
369
|
+
shell.print_wrapped "#{journal.notes}\n\n###".bold, :indent => 2 if journal.notes.present?
|
370
|
+
end
|
371
|
+
end
|
122
372
|
end
|
123
373
|
|
124
374
|
def map_user(user_name)
|
@@ -129,22 +379,116 @@ module Redmine
|
|
129
379
|
get_mapping(:status_mappings, status_name)
|
130
380
|
end
|
131
381
|
|
132
|
-
def
|
133
|
-
|
382
|
+
def map_priority(priority_name)
|
383
|
+
result = get_mapping(:priority_mappings, priority_name)
|
384
|
+
if Redmine::Cli::config["colorize"]
|
385
|
+
case result
|
386
|
+
# This is sort of a hack... ANSI sequences make it hard to figure out column lengths.
|
387
|
+
# So we make all fields the same length and with the same number of unprintable ansi
|
388
|
+
# sequence characters.
|
389
|
+
|
390
|
+
when "Low" then "Low ".blue.blue.blue
|
391
|
+
when "Normal" then "Normal ".green.green.green
|
392
|
+
when "High" then "High ".red.red.red
|
393
|
+
when "Urgent" then "Urgent ".bold.red.red
|
394
|
+
when "Immediate" then "Immediate".blink.bold.red
|
395
|
+
else result[0...9]
|
396
|
+
end
|
397
|
+
else
|
398
|
+
result
|
399
|
+
end
|
400
|
+
end
|
401
|
+
|
402
|
+
def map_project(project_name)
|
403
|
+
get_mapping(:project_mappings, project_name)
|
404
|
+
end
|
405
|
+
|
406
|
+
def map_query(query_name)
|
407
|
+
get_mapping(:query_mappings, query_name)
|
408
|
+
end
|
134
409
|
|
135
|
-
|
136
|
-
|
137
|
-
|
410
|
+
def update_mapping_cache
|
411
|
+
unless Redmine::Cli::config["disable_caching"]
|
412
|
+
say 'Updating mapping cache...', :yellow
|
413
|
+
# TODO: Updating user mapping requries Redmine 1.1+
|
414
|
+
# TODO: Retrieving user mapping requires admin privileges in Redmine
|
415
|
+
users = []
|
416
|
+
begin
|
417
|
+
users = User.fetch_all.collect { |user| [ user.login, user.id ] }
|
418
|
+
rescue Exception => e
|
419
|
+
say "Failed to fetch users: #{e}", :red
|
420
|
+
end
|
421
|
+
projects = Project.fetch_all.collect { |project| [ project.identifier, project.id ] }
|
422
|
+
queries = Query.fetch_all.collect { |query|
|
423
|
+
[ query.name, query.id ]
|
424
|
+
}
|
425
|
+
|
426
|
+
priorities = {}
|
427
|
+
status = {}
|
428
|
+
Issue.fetch_all.each do |issue|
|
429
|
+
priorities[issue.priority.name] = issue.priority.id if issue.priority
|
430
|
+
status[issue.status.name] = issue.status.id if issue.status
|
431
|
+
end
|
432
|
+
|
433
|
+
# TODO: Need to determine where to place cache file based on
|
434
|
+
# config file location.
|
435
|
+
File.open(File.expand_path('~/.redmine_cache'), 'w') do |out|
|
436
|
+
YAML.dump({
|
437
|
+
:user_mappings => Hash[users],
|
438
|
+
:project_mappings => Hash[projects],
|
439
|
+
:priority_mappings => priorities,
|
440
|
+
:status_mappings => status,
|
441
|
+
:query_mappings => Hash[queries],
|
442
|
+
}, out)
|
443
|
+
end
|
444
|
+
end
|
445
|
+
end
|
446
|
+
|
447
|
+
def map_tracker(tracker_name)
|
448
|
+
get_mapping(:tracker_mappings, tracker_name)
|
449
|
+
end
|
450
|
+
|
451
|
+
def get_mapping(mapping, value)
|
452
|
+
begin
|
453
|
+
return value if value.to_i != 0
|
454
|
+
rescue NoMethodError
|
455
|
+
return value.attributes.fetch("name") if value.id.to_i != 0
|
456
|
+
end
|
457
|
+
|
458
|
+
if Redmine::Cli::config[mapping].nil? || (mapped = Redmine::Cli::config[mapping][value]).nil?
|
459
|
+
if !(mapped = get_mapping_from_cache(mapping, value))
|
460
|
+
update_mapping_cache
|
461
|
+
|
462
|
+
if !(mapped = get_mapping_from_cache(mapping, value))
|
463
|
+
say "No #{mapping} for #{value}", :red
|
464
|
+
exit 1
|
465
|
+
end
|
466
|
+
end
|
138
467
|
end
|
139
468
|
|
140
469
|
return mapped
|
141
470
|
end
|
142
471
|
|
472
|
+
def get_mapping_from_cache(mapping, value)
|
473
|
+
begin
|
474
|
+
if Redmine::Cli::cache[mapping].nil? || (mapped = Redmine::Cli::cache[mapping][value]).nil?
|
475
|
+
return false
|
476
|
+
end
|
477
|
+
return mapped
|
478
|
+
rescue
|
479
|
+
# We need to recover here from any error that could happen
|
480
|
+
# in case the cache is corrupted.
|
481
|
+
return false
|
482
|
+
end
|
483
|
+
end
|
484
|
+
|
143
485
|
def update_ticket(ticket, options)
|
144
486
|
issue = Issue.find(ticket)
|
145
487
|
params = ticket_attributes(options)
|
146
488
|
|
147
489
|
if issue.update_attributes(params)
|
490
|
+
params[:params] = {:include => "journals,changesets"}
|
491
|
+
issue = Issue.find(ticket, params)
|
148
492
|
say "Updated: #{ticket}. Options: #{params.inspect}", :green
|
149
493
|
display_issue(issue)
|
150
494
|
else
|
@@ -160,7 +504,57 @@ module Redmine
|
|
160
504
|
system "stty echo"
|
161
505
|
password
|
162
506
|
end
|
507
|
+
|
508
|
+
def fields()
|
509
|
+
# This is a collection of built-in Redmine fields, the key by which they can be accessed, and a wrapper
|
510
|
+
# method that is used to display their value. Pseudo-fields can be added that use different wrapper
|
511
|
+
# methods to give the user flexibility over their output (see url vs. id)
|
512
|
+
return {
|
513
|
+
"url" => Field.new("URL", "id", method(:link_to_issue)),
|
514
|
+
"id" => Field.new("ID#", "id"),
|
515
|
+
"subject" => Field.new("Subject", "subject"),
|
516
|
+
"status" => Field.new("Status", "status", method(:status_name)),
|
517
|
+
"start_date" => Field.new("Start", "start_date"),
|
518
|
+
"estimated_hours" => Field.new("Estd", "estimated_hours"),
|
519
|
+
"tracker" => Field.new("Type", "tracker", method(:map_user)),
|
520
|
+
"priority" => Field.new("Priority", "priority", method(:map_priority)),
|
521
|
+
"description" => Field.new("Description", "description"),
|
522
|
+
"assigned_to" => Field.new("Assigned To", "assigned_to", method(:map_user)),
|
523
|
+
"project" => Field.new("Project", "project", method(:map_user)),
|
524
|
+
"author" => Field.new("Author", "author", method(:map_user)),
|
525
|
+
"done_ratio" => Field.new("% Done", "done_ratio"),
|
526
|
+
"due_date" => Field.new("Due On", "due_date"),
|
527
|
+
"created_on" => Field.new("Created On", "created_on"),
|
528
|
+
"updated_on" => Field.new("Updated On", "updated_on"),
|
529
|
+
}
|
530
|
+
end
|
531
|
+
|
532
|
+
def default_parameters
|
533
|
+
{:limit => 100}
|
534
|
+
end
|
163
535
|
end
|
164
536
|
end
|
165
537
|
end
|
166
538
|
end
|
539
|
+
|
540
|
+
class String
|
541
|
+
def black; "\033[30m#{self}\033[0m" end
|
542
|
+
def red; "\033[31m#{self}\033[0m" end
|
543
|
+
def green; "\033[32m#{self}\033[0m" end
|
544
|
+
def brown; "\033[33m#{self}\033[0m" end
|
545
|
+
def blue; "\033[34m#{self}\033[0m" end
|
546
|
+
def magenta; "\033[35m#{self}\033[0m" end
|
547
|
+
def cyan; "\033[36m#{self}\033[0m" end
|
548
|
+
def gray; "\033[37m#{self}\033[0m" end
|
549
|
+
def bg_black; "\033[40m#{self}\0330m" end
|
550
|
+
def bg_red; "\033[41m#{self}\033[0m" end
|
551
|
+
def bg_green; "\033[42m#{self}\033[0m" end
|
552
|
+
def bg_brown; "\033[43m#{self}\033[0m" end
|
553
|
+
def bg_blue; "\033[44m#{self}\033[0m" end
|
554
|
+
def bg_magenta; "\033[45m#{self}\033[0m" end
|
555
|
+
def bg_cyan; "\033[46m#{self}\033[0m" end
|
556
|
+
def bg_gray; "\033[47m#{self}\033[0m" end
|
557
|
+
def bold; "\033[1m#{self}\033[22m" end
|
558
|
+
def blink; "\033[5m#{self}\033[22m" end
|
559
|
+
def reverse_color; "\033[7m#{self}\033[27m" end
|
560
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'thor'
|
2
|
+
require 'yaml'
|
3
|
+
|
4
|
+
module Redmine
|
5
|
+
module Cli
|
6
|
+
class << self
|
7
|
+
|
8
|
+
def config
|
9
|
+
begin
|
10
|
+
generic_conf '.redmine'
|
11
|
+
rescue Errno::ENOENT
|
12
|
+
puts "You need to create the file .redmine in your home with your username, password and url"
|
13
|
+
exit 1
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def cache
|
18
|
+
begin
|
19
|
+
generic_conf '.redmine_cache'
|
20
|
+
rescue Errno::ENOENT
|
21
|
+
@cache = Thor::CoreExt::HashWithIndifferentAccess.new
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def generic_conf(config_file)
|
28
|
+
# Using Ruby Magic(tm) to get the caller's function name to use for
|
29
|
+
# setting up instance variables/accessors for generic config files.
|
30
|
+
config_name = caller[0][/`.*'/][1..-2]
|
31
|
+
|
32
|
+
if !File.file? config_file
|
33
|
+
config_file = File.expand_path "~/#{config_file}"
|
34
|
+
end
|
35
|
+
|
36
|
+
contents = YAML.load_file config_file
|
37
|
+
if contents
|
38
|
+
config ||= Thor::CoreExt::HashWithIndifferentAccess.new(YAML.load_file(config_file))
|
39
|
+
else
|
40
|
+
config ||= Thor::CoreExt::HashWithIndifferentAccess.new
|
41
|
+
end
|
42
|
+
self.instance_variable_set("@#{config_name}", config)
|
43
|
+
end
|
44
|
+
|
45
|
+
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Redmine
|
2
|
+
class Field
|
3
|
+
def initialize(title, ref, dm=nil)
|
4
|
+
@title=title # The string displayed in the listing table for this field
|
5
|
+
@ref=ref # The attribute referenced from the issue record
|
6
|
+
@display_method=dm # A method used to process the value before displaying (optional)
|
7
|
+
end
|
8
|
+
def title
|
9
|
+
return @title
|
10
|
+
end
|
11
|
+
def ref
|
12
|
+
return @ref
|
13
|
+
end
|
14
|
+
def display
|
15
|
+
return @display_method
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -2,6 +2,8 @@ url: "<%= url %>"
|
|
2
2
|
username: "<%= username %>"
|
3
3
|
password: "<%= password %>"
|
4
4
|
default_project_id: 1
|
5
|
+
disable_caching: false
|
6
|
+
colorize: true
|
5
7
|
user_mappings:
|
6
8
|
"admin": 1
|
7
9
|
status_mappings:
|
@@ -11,3 +13,8 @@ status_mappings:
|
|
11
13
|
"feedback": 4
|
12
14
|
"closed": 5
|
13
15
|
"rejected": 6
|
16
|
+
list_fields:
|
17
|
+
- <%= list_fields.join("\n - ") %>
|
18
|
+
tracker_mappings:
|
19
|
+
"bug" : 1
|
20
|
+
"feature": 2
|
@@ -9,9 +9,10 @@ module Redmine::Cli::Generators
|
|
9
9
|
File.dirname(__FILE__)
|
10
10
|
end
|
11
11
|
|
12
|
-
argument :url,
|
13
|
-
argument :username,
|
14
|
-
argument :password,
|
12
|
+
argument :url, :type => :string
|
13
|
+
argument :username, :type => :string
|
14
|
+
argument :password, :type => :string
|
15
|
+
argument :list_fields, :type => :array
|
15
16
|
class_option :test, :type => :boolean
|
16
17
|
def copy_configuration_file
|
17
18
|
self.destination_root = File.expand_path("~") unless options.test
|
data/lib/redmine-cli/git.rb
CHANGED
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'thor'
|
2
|
+
require 'active_resource'
|
3
|
+
require 'active_support/core_ext/object/with_options'
|
4
|
+
require 'redmine-cli/config'
|
5
|
+
require 'pp'
|
6
|
+
|
7
|
+
module Redmine
|
8
|
+
module Cli
|
9
|
+
class BaseResource < ActiveResource::Base
|
10
|
+
self.site = Redmine::Cli::config.url
|
11
|
+
self.user = Redmine::Cli::config.username
|
12
|
+
self.password = Redmine::Cli::config.password
|
13
|
+
|
14
|
+
class << self
|
15
|
+
# HACK: Redmine API isn't ActiveResource-friendly out of the box, so
|
16
|
+
# we need to pass nometa=1 to all requests since we don't care about
|
17
|
+
# the metadata that gets passed back in the top level attributes.
|
18
|
+
def find(*arguments)
|
19
|
+
arguments[1] = arguments[1] || {}
|
20
|
+
arguments[1][:params] = arguments[1][:params] || {}
|
21
|
+
arguments[1][:params][:nometa] = 1
|
22
|
+
|
23
|
+
super
|
24
|
+
end
|
25
|
+
|
26
|
+
def fetch_all(params = {})
|
27
|
+
limit = 100
|
28
|
+
offset = 0
|
29
|
+
|
30
|
+
resources = []
|
31
|
+
|
32
|
+
while((fetched_resources = self.all(:params => params.merge({:limit => limit, :offset => offset}))).any?)
|
33
|
+
resources += fetched_resources
|
34
|
+
offset += limit
|
35
|
+
end
|
36
|
+
|
37
|
+
resources
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
class Issue < BaseResource; end
|
43
|
+
class User < BaseResource; end
|
44
|
+
class Project < BaseResource; end
|
45
|
+
class Query < BaseResource; end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
|
50
|
+
# HACK: Redmine API isn't ActiveResource-friendly out of the box, and
|
51
|
+
# also some versions of Redmine ignore the nometa=1 parameter. So we
|
52
|
+
# need to manually strip out metadata that confuses ActiveResource.
|
53
|
+
class Hash
|
54
|
+
class << self
|
55
|
+
alias_method :from_xml_original, :from_xml
|
56
|
+
def from_xml(xml)
|
57
|
+
scrubbed = scrub_attributes(xml)
|
58
|
+
from_xml_original(scrubbed)
|
59
|
+
end
|
60
|
+
def scrub_attributes(xml)
|
61
|
+
xml.gsub(/<issues .*?>/, "<issues type=\"array\">")
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
data/lib/redmine-cli/version.rb
CHANGED
data/redmine-cli.gemspec
CHANGED
@@ -21,8 +21,13 @@ Gem::Specification.new do |s|
|
|
21
21
|
|
22
22
|
s.add_dependency "activeresource", "~>3.0.0"
|
23
23
|
s.add_dependency "thor"
|
24
|
-
|
24
|
+
if RUBY_VERSION =~ /1.9/
|
25
|
+
s.add_development_dependency "ruby-debug19"
|
26
|
+
else
|
27
|
+
s.add_development_dependency "ruby-debug"
|
28
|
+
end
|
25
29
|
s.add_development_dependency "rspec"
|
26
30
|
s.add_development_dependency "cucumber"
|
27
31
|
s.add_development_dependency "aruba"
|
32
|
+
s.add_dependency "interactive_editor"
|
28
33
|
end
|
metadata
CHANGED
@@ -1,13 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: redmine-cli
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
|
5
|
-
prerelease: false
|
6
|
-
segments:
|
7
|
-
- 0
|
8
|
-
- 1
|
9
|
-
- 1
|
10
|
-
version: 0.1.1
|
4
|
+
version: 0.1.4
|
11
5
|
platform: ruby
|
12
6
|
authors:
|
13
7
|
- Jorge Dias
|
@@ -15,22 +9,15 @@ autorequire:
|
|
15
9
|
bindir: bin
|
16
10
|
cert_chain: []
|
17
11
|
|
18
|
-
date:
|
19
|
-
default_executable:
|
12
|
+
date: 2015-02-25 00:00:00 Z
|
20
13
|
dependencies:
|
21
14
|
- !ruby/object:Gem::Dependency
|
22
15
|
name: activeresource
|
23
16
|
prerelease: false
|
24
17
|
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
-
none: false
|
26
18
|
requirements:
|
27
19
|
- - ~>
|
28
20
|
- !ruby/object:Gem::Version
|
29
|
-
hash: 7
|
30
|
-
segments:
|
31
|
-
- 3
|
32
|
-
- 0
|
33
|
-
- 0
|
34
21
|
version: 3.0.0
|
35
22
|
type: :runtime
|
36
23
|
version_requirements: *id001
|
@@ -38,72 +25,53 @@ dependencies:
|
|
38
25
|
name: thor
|
39
26
|
prerelease: false
|
40
27
|
requirement: &id002 !ruby/object:Gem::Requirement
|
41
|
-
none: false
|
42
28
|
requirements:
|
43
|
-
-
|
29
|
+
- &id003
|
30
|
+
- ">="
|
44
31
|
- !ruby/object:Gem::Version
|
45
|
-
hash: 3
|
46
|
-
segments:
|
47
|
-
- 0
|
48
32
|
version: "0"
|
49
33
|
type: :runtime
|
50
34
|
version_requirements: *id002
|
51
35
|
- !ruby/object:Gem::Dependency
|
52
36
|
name: ruby-debug
|
53
37
|
prerelease: false
|
54
|
-
requirement: &
|
55
|
-
none: false
|
38
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
56
39
|
requirements:
|
57
|
-
-
|
58
|
-
- !ruby/object:Gem::Version
|
59
|
-
hash: 3
|
60
|
-
segments:
|
61
|
-
- 0
|
62
|
-
version: "0"
|
40
|
+
- *id003
|
63
41
|
type: :development
|
64
|
-
version_requirements: *
|
42
|
+
version_requirements: *id004
|
65
43
|
- !ruby/object:Gem::Dependency
|
66
44
|
name: rspec
|
67
45
|
prerelease: false
|
68
|
-
requirement: &
|
69
|
-
none: false
|
46
|
+
requirement: &id005 !ruby/object:Gem::Requirement
|
70
47
|
requirements:
|
71
|
-
-
|
72
|
-
- !ruby/object:Gem::Version
|
73
|
-
hash: 3
|
74
|
-
segments:
|
75
|
-
- 0
|
76
|
-
version: "0"
|
48
|
+
- *id003
|
77
49
|
type: :development
|
78
|
-
version_requirements: *
|
50
|
+
version_requirements: *id005
|
79
51
|
- !ruby/object:Gem::Dependency
|
80
52
|
name: cucumber
|
81
53
|
prerelease: false
|
82
|
-
requirement: &
|
83
|
-
none: false
|
54
|
+
requirement: &id006 !ruby/object:Gem::Requirement
|
84
55
|
requirements:
|
85
|
-
-
|
86
|
-
- !ruby/object:Gem::Version
|
87
|
-
hash: 3
|
88
|
-
segments:
|
89
|
-
- 0
|
90
|
-
version: "0"
|
56
|
+
- *id003
|
91
57
|
type: :development
|
92
|
-
version_requirements: *
|
58
|
+
version_requirements: *id006
|
93
59
|
- !ruby/object:Gem::Dependency
|
94
60
|
name: aruba
|
95
61
|
prerelease: false
|
96
|
-
requirement: &
|
97
|
-
none: false
|
62
|
+
requirement: &id007 !ruby/object:Gem::Requirement
|
98
63
|
requirements:
|
99
|
-
-
|
100
|
-
- !ruby/object:Gem::Version
|
101
|
-
hash: 3
|
102
|
-
segments:
|
103
|
-
- 0
|
104
|
-
version: "0"
|
64
|
+
- *id003
|
105
65
|
type: :development
|
106
|
-
version_requirements: *
|
66
|
+
version_requirements: *id007
|
67
|
+
- !ruby/object:Gem::Dependency
|
68
|
+
name: interactive_editor
|
69
|
+
prerelease: false
|
70
|
+
requirement: &id008 !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- *id003
|
73
|
+
type: :runtime
|
74
|
+
version_requirements: *id008
|
107
75
|
description: A simple command line interface for redmine for easy scripting
|
108
76
|
email:
|
109
77
|
- jorge@mrdias.com
|
@@ -118,7 +86,8 @@ files:
|
|
118
86
|
- .gitignore
|
119
87
|
- Gemfile
|
120
88
|
- Gemfile.lock
|
121
|
-
-
|
89
|
+
- LICENSE
|
90
|
+
- README.md
|
122
91
|
- Rakefile
|
123
92
|
- bin/git-redmine
|
124
93
|
- bin/redmine
|
@@ -127,45 +96,39 @@ files:
|
|
127
96
|
- features/support/setup.rb
|
128
97
|
- lib/redmine-cli.rb
|
129
98
|
- lib/redmine-cli/cli.rb
|
99
|
+
- lib/redmine-cli/config.rb
|
100
|
+
- lib/redmine-cli/field.rb
|
130
101
|
- lib/redmine-cli/generators/.redmine
|
131
102
|
- lib/redmine-cli/generators/install.rb
|
132
103
|
- lib/redmine-cli/git.rb
|
133
|
-
- lib/redmine-cli/
|
104
|
+
- lib/redmine-cli/resources.rb
|
134
105
|
- lib/redmine-cli/version.rb
|
135
106
|
- redmine-cli.gemspec
|
136
|
-
has_rdoc: true
|
137
107
|
homepage: http://rubygems.org/gems/redmine-cli
|
138
108
|
licenses: []
|
139
109
|
|
110
|
+
metadata: {}
|
111
|
+
|
140
112
|
post_install_message:
|
141
113
|
rdoc_options: []
|
142
114
|
|
143
115
|
require_paths:
|
144
116
|
- lib
|
145
117
|
required_ruby_version: !ruby/object:Gem::Requirement
|
146
|
-
none: false
|
147
118
|
requirements:
|
148
|
-
-
|
149
|
-
- !ruby/object:Gem::Version
|
150
|
-
hash: 3
|
151
|
-
segments:
|
152
|
-
- 0
|
153
|
-
version: "0"
|
119
|
+
- *id003
|
154
120
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
155
|
-
none: false
|
156
121
|
requirements:
|
157
|
-
-
|
158
|
-
- !ruby/object:Gem::Version
|
159
|
-
hash: 3
|
160
|
-
segments:
|
161
|
-
- 0
|
162
|
-
version: "0"
|
122
|
+
- *id003
|
163
123
|
requirements: []
|
164
124
|
|
165
125
|
rubyforge_project: redmine-cli
|
166
|
-
rubygems_version: 1.
|
126
|
+
rubygems_version: 2.1.11
|
167
127
|
signing_key:
|
168
|
-
specification_version:
|
128
|
+
specification_version: 4
|
169
129
|
summary: Command line interface for redmine
|
170
|
-
test_files:
|
171
|
-
|
130
|
+
test_files:
|
131
|
+
- features/install.feature
|
132
|
+
- features/step_definitions/install_steps.rb
|
133
|
+
- features/support/setup.rb
|
134
|
+
has_rdoc:
|
data/README.markdown
DELETED
@@ -1,44 +0,0 @@
|
|
1
|
-
# A command line interface for redmine
|
2
|
-
|
3
|
-
## Install
|
4
|
-
|
5
|
-
Execute
|
6
|
-
redmine install
|
7
|
-
|
8
|
-
This will create a .redmine file in your home directory. The file is a yaml file which contains our necessary configuration
|
9
|
-
|
10
|
-
Here you can add mappings for users and status like:
|
11
|
-
|
12
|
-
user_mappings:
|
13
|
-
"me": 1
|
14
|
-
"johndoe": 24
|
15
|
-
status_mappings:
|
16
|
-
"new": 1
|
17
|
-
"closed": 4
|
18
|
-
|
19
|
-
This will allow to use those names with the commands instead of the ids of users or status
|
20
|
-
|
21
|
-
## Use cases
|
22
|
-
|
23
|
-
- Listing tickets
|
24
|
-
|
25
|
-
redmine list
|
26
|
-
redmine list -a me
|
27
|
-
|
28
|
-
- Display ticket
|
29
|
-
|
30
|
-
redmine show 524
|
31
|
-
|
32
|
-
- Updating a ticket
|
33
|
-
|
34
|
-
redmine update 524 -description "New description"
|
35
|
-
redmine update 256 --assigned_to me
|
36
|
-
|
37
|
-
- Updating multiple tickets
|
38
|
-
|
39
|
-
redmine update 2 3 4 --assigned_to johndoe
|
40
|
-
|
41
|
-
- Updating all tickets for a list
|
42
|
-
|
43
|
-
redmine list --status new --std_output | xargs redmine update --asigned_to me --status 3 -l
|
44
|
-
\# Note that the last argument of the update command must be -l
|
data/lib/redmine-cli/issue.rb
DELETED
@@ -1,22 +0,0 @@
|
|
1
|
-
require 'thor'
|
2
|
-
require 'active_resource'
|
3
|
-
|
4
|
-
module Redmine
|
5
|
-
module Cli
|
6
|
-
class Issue < ActiveResource::Base
|
7
|
-
def self.config
|
8
|
-
@config ||=
|
9
|
-
begin
|
10
|
-
Thor::CoreExt::HashWithIndifferentAccess.new(YAML.load_file(File.expand_path("~/.redmine")))
|
11
|
-
rescue Errno::ENOENT
|
12
|
-
puts "You need to create the file .redmine in your home with your username, password and url"
|
13
|
-
Thor::CoreExt::HashWithIndifferentAccess.new
|
14
|
-
end
|
15
|
-
end
|
16
|
-
|
17
|
-
self.site = config.url
|
18
|
-
self.user = config.username
|
19
|
-
self.password = config.password
|
20
|
-
end
|
21
|
-
end
|
22
|
-
end
|