redmine-api 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +4 -0
- data/.ruby-version +1 -0
- data/.travis.yml +2 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +22 -0
- data/LICENSE.txt +21 -0
- data/README.md +59 -0
- data/Rakefile +24 -0
- data/bin/console +11 -0
- data/bin/rake +16 -0
- data/bin/setup +6 -0
- data/exe/redmine +4 -0
- data/lib/redmine.rb +42 -0
- data/lib/redmine/accept_json.rb +41 -0
- data/lib/redmine/cli.rb +67 -0
- data/lib/redmine/client.rb +54 -0
- data/lib/redmine/command.rb +81 -0
- data/lib/redmine/commands.rb +10 -0
- data/lib/redmine/commands/issue.rb +22 -0
- data/lib/redmine/commands/issue/activity.rb +32 -0
- data/lib/redmine/commands/issue/show.rb +28 -0
- data/lib/redmine/commands/issues.rb +44 -0
- data/lib/redmine/commands/lead_times.rb +62 -0
- data/lib/redmine/commands/projects.rb +20 -0
- data/lib/redmine/configuration.rb +27 -0
- data/lib/redmine/http_caching.rb +63 -0
- data/lib/redmine/issue.rb +42 -0
- data/lib/redmine/issue_change.rb +40 -0
- data/lib/redmine/issue_event.rb +23 -0
- data/lib/redmine/project.rb +16 -0
- data/lib/redmine/rest_client.rb +33 -0
- data/lib/redmine/value.rb +88 -0
- data/lib/redmine/version.rb +3 -0
- data/redmine_api.gemspec +26 -0
- metadata +123 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: c822179cddb921fe6c543449ca43b7291b41df6d
|
4
|
+
data.tar.gz: 42c89075ee550c5512ab6095bf3aa3f29796c894
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 68c19e628b931f63bd86cf420761a624fda5455078b9ad5088aad97cd9a9a47490590a55e6b98d6be831d0a76882040239ead45a0371c56a174eebdb5ad879d6
|
7
|
+
data.tar.gz: 787a81963065b3b7f01d57cc847da8595cd9aecbf38c12d2d6bcc7931e42fdb8a46a357de87d3b6ef8dd023ed5c0fab61776cf39b475590f96b0fb9beb2f5c15
|
data/.gitignore
ADDED
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.3.0
|
data/.travis.yml
ADDED
data/CODE_OF_CONDUCT.md
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
# Contributor Code of Conduct
|
2
|
+
|
3
|
+
As contributors and maintainers of this project, and in the interest of
|
4
|
+
fostering an open and welcoming community, we pledge to respect all people who
|
5
|
+
contribute through reporting issues, posting feature requests, updating
|
6
|
+
documentation, submitting pull requests or patches, and other activities.
|
7
|
+
|
8
|
+
We are committed to making participation in this project a harassment-free
|
9
|
+
experience for everyone, regardless of level of experience, gender, gender
|
10
|
+
identity and expression, sexual orientation, disability, personal appearance,
|
11
|
+
body size, race, ethnicity, age, religion, or nationality.
|
12
|
+
|
13
|
+
Examples of unacceptable behavior by participants include:
|
14
|
+
|
15
|
+
* The use of sexualized language or imagery
|
16
|
+
* Personal attacks
|
17
|
+
* Trolling or insulting/derogatory comments
|
18
|
+
* Public or private harassment
|
19
|
+
* Publishing other's private information, such as physical or electronic
|
20
|
+
addresses, without explicit permission
|
21
|
+
* Other unethical or unprofessional conduct
|
22
|
+
|
23
|
+
Project maintainers have the right and responsibility to remove, edit, or
|
24
|
+
reject comments, commits, code, wiki edits, issues, and other contributions
|
25
|
+
that are not aligned to this Code of Conduct, or to ban temporarily or
|
26
|
+
permanently any contributor for other behaviors that they deem inappropriate,
|
27
|
+
threatening, offensive, or harmful.
|
28
|
+
|
29
|
+
By adopting this Code of Conduct, project maintainers commit themselves to
|
30
|
+
fairly and consistently applying these principles to every aspect of managing
|
31
|
+
this project. Project maintainers who do not follow or enforce the Code of
|
32
|
+
Conduct may be permanently removed from the project team.
|
33
|
+
|
34
|
+
This code of conduct applies both within project spaces and in public spaces
|
35
|
+
when an individual is representing the project or its community.
|
36
|
+
|
37
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
38
|
+
reported by contacting a project maintainer at arjan@arjanvandergaag.nl. All
|
39
|
+
complaints will be reviewed and investigated and will result in a response that
|
40
|
+
is deemed necessary and appropriate to the circumstances. Maintainers are
|
41
|
+
obligated to maintain confidentiality with regard to the reporter of an
|
42
|
+
incident.
|
43
|
+
|
44
|
+
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
45
|
+
version 1.3.0, available at
|
46
|
+
[http://contributor-covenant.org/version/1/3/0/][version]
|
47
|
+
|
48
|
+
[homepage]: http://contributor-covenant.org
|
49
|
+
[version]: http://contributor-covenant.org/version/1/3/0/
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
redmine-api (0.1.0)
|
5
|
+
|
6
|
+
GEM
|
7
|
+
remote: https://rubygems.org/
|
8
|
+
specs:
|
9
|
+
minitest (5.8.4)
|
10
|
+
rake (10.5.0)
|
11
|
+
|
12
|
+
PLATFORMS
|
13
|
+
ruby
|
14
|
+
|
15
|
+
DEPENDENCIES
|
16
|
+
bundler (~> 1.11)
|
17
|
+
minitest (~> 5.0)
|
18
|
+
rake (~> 10.0)
|
19
|
+
redmine-api!
|
20
|
+
|
21
|
+
BUNDLED WITH
|
22
|
+
1.11.2
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2016 Arjan van der Gaag
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
# Redmine API [![Build Status](https://travis-ci.org/avdgaag/redmine-api.svg?branch=master)](https://travis-ci.org/avdgaag/redmine-api)
|
2
|
+
|
3
|
+
This is a command-line interface to the popular Redmine issue tracking system, based on its REST API. Use it to quickly get the information you need without taking your hands off the keyboard to open a browser.
|
4
|
+
|
5
|
+
This is a simple pure-Ruby gem with no additional runtime dependencies. It is as much an exercise in using the Ruby standard library as it is meant to be useful.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
You can install this gem using Rubygems:
|
10
|
+
|
11
|
+
$ gem install redmine-api
|
12
|
+
|
13
|
+
## Usage
|
14
|
+
|
15
|
+
To use this gem, invoke its executable from the command line:
|
16
|
+
|
17
|
+
% redmine --version
|
18
|
+
0.1.0
|
19
|
+
|
20
|
+
This program consists of several subcommands, which you can see listed when you print the help information:
|
21
|
+
|
22
|
+
% redmine --help
|
23
|
+
|
24
|
+
For example, to list all available projects in your Redmine installation, use the `projects` subcommand:
|
25
|
+
|
26
|
+
% redmine projects
|
27
|
+
|
28
|
+
For more information, read the docs or the inline help.
|
29
|
+
|
30
|
+
## Authentication
|
31
|
+
|
32
|
+
In order to access your Redmine data, you need to configure the gem to point to the right Redmine installation and provide proper credentials. You can do so by creating a configuration file in your project directory containing that information. Such a configuration file would be named `.redmine.yml` and might look like this:
|
33
|
+
|
34
|
+
---
|
35
|
+
api_token: '14acda31941e8c5dc3be12d1a5d108311b7da3eb'
|
36
|
+
base_uri: 'http://redmine.mydomain.tld'
|
37
|
+
http_cache: 'redmine.cache'
|
38
|
+
|
39
|
+
Using the API token is currently the only supported way to authenticate against Redmine. Since your API token is private, make sure you make your configuration file only accessible to yourself. This program will look for a `.redmine.yml` file in the current directory its ancestor directories, merging all of them together into one final configuration (with the first file to be encountered taking precedence over later files).
|
40
|
+
|
41
|
+
## Development
|
42
|
+
|
43
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
44
|
+
|
45
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
46
|
+
|
47
|
+
## Contributing
|
48
|
+
|
49
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/avdgaag/redmine-api. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
50
|
+
|
51
|
+
## Credits
|
52
|
+
|
53
|
+
* **Author**: Arjan van der Gaag <arjan@arjanvandergaag.nl>
|
54
|
+
* **URL**: arjanvandergaag.nl
|
55
|
+
* **Homepage**: https://github.com/avdgaag/redmine-api
|
56
|
+
|
57
|
+
## License
|
58
|
+
|
59
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'bundler/gem_tasks'
|
4
|
+
require 'rake/testtask'
|
5
|
+
require 'rdoc/task'
|
6
|
+
|
7
|
+
RDoc::Task.new do |t|
|
8
|
+
t.main = 'README.md'
|
9
|
+
t.rdoc_files.include(
|
10
|
+
'README.md',
|
11
|
+
'CODE_OF_CONDUCT.md',
|
12
|
+
'LICENSE.txt',
|
13
|
+
'lib/**/*.rb'
|
14
|
+
)
|
15
|
+
t.rdoc_dir = 'doc'
|
16
|
+
end
|
17
|
+
|
18
|
+
Rake::TestTask.new(:test) do |t|
|
19
|
+
t.libs << 'test'
|
20
|
+
t.libs << 'lib'
|
21
|
+
t.test_files = FileList['test/**/*_test.rb']
|
22
|
+
end
|
23
|
+
|
24
|
+
task default: :test
|
data/bin/console
ADDED
data/bin/rake
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# This file was generated by Bundler.
|
4
|
+
#
|
5
|
+
# The application 'rake' is installed as part of a gem, and
|
6
|
+
# this file is here to facilitate running it.
|
7
|
+
#
|
8
|
+
|
9
|
+
require "pathname"
|
10
|
+
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
|
11
|
+
Pathname.new(__FILE__).realpath)
|
12
|
+
|
13
|
+
require "rubygems"
|
14
|
+
require "bundler/setup"
|
15
|
+
|
16
|
+
load Gem.bin_path("rake", "rake")
|
data/bin/setup
ADDED
data/exe/redmine
ADDED
data/lib/redmine.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'pstore'
|
2
|
+
require 'uri'
|
3
|
+
|
4
|
+
require 'redmine/configuration'
|
5
|
+
require 'redmine/cli'
|
6
|
+
require 'redmine/client'
|
7
|
+
require 'redmine/accept_json'
|
8
|
+
require 'redmine/http_caching'
|
9
|
+
require 'redmine/rest_client'
|
10
|
+
|
11
|
+
# = Redmine command line API
|
12
|
+
#
|
13
|
+
# This gem provides a command-line API to the popular Redmine issue tracking
|
14
|
+
# system, using its REST API.
|
15
|
+
module Redmine
|
16
|
+
module_function
|
17
|
+
|
18
|
+
def configuration
|
19
|
+
@configuration ||= Configuration.autoload
|
20
|
+
end
|
21
|
+
|
22
|
+
def configure
|
23
|
+
@configuration = Configuration.autoload
|
24
|
+
yield @configuration
|
25
|
+
@configuration.freeze
|
26
|
+
end
|
27
|
+
|
28
|
+
def cli(args)
|
29
|
+
cache = PStore.new(configuration.http_cache)
|
30
|
+
base_uri = URI.parse(configuration.base_uri)
|
31
|
+
rest_client = RestClient.new(
|
32
|
+
base_uri: base_uri,
|
33
|
+
default_headers: {
|
34
|
+
'X-Redmine-Api-Key' => configuration.api_token
|
35
|
+
}
|
36
|
+
)
|
37
|
+
rest_client = AcceptJson.new(HttpCaching.new(rest_client, cache))
|
38
|
+
Cli.new(
|
39
|
+
redmine_client: Client.new(rest_client: rest_client)
|
40
|
+
).call(args)
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'delegate'
|
3
|
+
|
4
|
+
module Redmine
|
5
|
+
# A decorator object for RestClient that provides immediate JSON-parsing
|
6
|
+
# capabilities. Rather than dealing with raw response objects, this decorator
|
7
|
+
# helps us get at parsed JSON data immediately.
|
8
|
+
#
|
9
|
+
# This decorator works by intercepting outgoing requests and adding an
|
10
|
+
# +Accept+ header to indicate we would like to receive JSON data
|
11
|
+
# back. Responses will then be parsed, given they actually are JSON, and both
|
12
|
+
# the parsed response body and the original response are returned.
|
13
|
+
class AcceptJson < SimpleDelegator
|
14
|
+
# Value of the outgoing +Accept+ header.
|
15
|
+
ACCEPT = 'application/json'.freeze
|
16
|
+
|
17
|
+
# Matcher for recognizing response content types.
|
18
|
+
CONTENT_TYPE = %r{application/json}
|
19
|
+
|
20
|
+
# Wrap requests to add an `Accept` header to ask for JSON, and parse
|
21
|
+
# response bodies as JSON data.
|
22
|
+
def get(path, headers = {})
|
23
|
+
response = super(path, { 'Accept' => ACCEPT }.merge(headers.to_h))
|
24
|
+
case response.content_type
|
25
|
+
when CONTENT_TYPE then [parse_response(response), response]
|
26
|
+
else raise "Unknown content type #{response.content_type.inspect}"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def parse_response(response)
|
33
|
+
JSON.parse(response.body)
|
34
|
+
rescue JSON::ParserError => e
|
35
|
+
# TODO: log output here
|
36
|
+
p response
|
37
|
+
p response.to_hash
|
38
|
+
raise e
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
data/lib/redmine/cli.rb
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'redmine/issue'
|
2
|
+
require 'redmine/commands'
|
3
|
+
|
4
|
+
module Redmine
|
5
|
+
# Command line interface dispatcher: invoke subcommands based on incoming
|
6
|
+
# command line switches.
|
7
|
+
class Cli
|
8
|
+
def initialize(redmine_client:)
|
9
|
+
@redmine = redmine_client
|
10
|
+
end
|
11
|
+
|
12
|
+
def call(arguments)
|
13
|
+
subcommand, *other_args = arguments
|
14
|
+
if subcommand =~ /^\d+$/
|
15
|
+
call_issue_command(subcommand, other_args)
|
16
|
+
elsif subcommand =~ /^\w/
|
17
|
+
call_subcommand(subcommand, other_args)
|
18
|
+
else
|
19
|
+
option_parser.parse(arguments)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def call_subcommand(cmd, args)
|
26
|
+
command_name_to_class(cmd).new(redmine: @redmine).call(args)
|
27
|
+
end
|
28
|
+
|
29
|
+
def command_name_to_class(cmd)
|
30
|
+
Commands.const_get(cmd.split('_').map(&:capitalize).join)
|
31
|
+
end
|
32
|
+
|
33
|
+
def call_issue_command(cmd, args)
|
34
|
+
Commands::Issue.new(issue_id: cmd, redmine: @redmine).call(args)
|
35
|
+
end
|
36
|
+
|
37
|
+
def option_parser # rubocop:disable Metrics/MethodLength
|
38
|
+
OptionParser.new do |o|
|
39
|
+
o.banner = <<~EOS
|
40
|
+
redmine [SUBCOMMAND] [OPTIONS]
|
41
|
+
|
42
|
+
Perform common operations in a Redmine issue tracker from the command
|
43
|
+
line.
|
44
|
+
|
45
|
+
Available subcommands:
|
46
|
+
|
47
|
+
projects
|
48
|
+
issues
|
49
|
+
show
|
50
|
+
activity
|
51
|
+
lead_times
|
52
|
+
|
53
|
+
Generic options:
|
54
|
+
EOS
|
55
|
+
o.separator ''
|
56
|
+
o.on_tail '-h', '--help', 'Show this message' do
|
57
|
+
puts o
|
58
|
+
exit
|
59
|
+
end
|
60
|
+
o.on_tail '-v', '--version', 'Display version number' do
|
61
|
+
puts Redmine::VERSION
|
62
|
+
exit
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'redmine/project'
|
2
|
+
require 'redmine/issue'
|
3
|
+
|
4
|
+
module Redmine
|
5
|
+
# The client is a Redmine-aware REST API client that maps remote resources
|
6
|
+
# into local methods and data. It uses the RestClient under the hood and
|
7
|
+
# outputs our own domain objects.
|
8
|
+
class Client
|
9
|
+
def initialize(rest_client:)
|
10
|
+
@rest_client = rest_client
|
11
|
+
end
|
12
|
+
|
13
|
+
def issue(issue_id)
|
14
|
+
data = @rest_client.get("/issues/#{issue_id}.json?include=journals").first
|
15
|
+
Issue.new(data.fetch('issue'))
|
16
|
+
end
|
17
|
+
|
18
|
+
def projects
|
19
|
+
@rest_client
|
20
|
+
.get('/projects.json')
|
21
|
+
.first
|
22
|
+
.fetch('projects')
|
23
|
+
.map { |p| Project.new(p) }
|
24
|
+
end
|
25
|
+
|
26
|
+
def project(id)
|
27
|
+
@rest_client
|
28
|
+
.get("/projects/#{id}.json?include=trackers")
|
29
|
+
.fetch('project')
|
30
|
+
end
|
31
|
+
|
32
|
+
def issue_statuses
|
33
|
+
@rest_client
|
34
|
+
.get('/issue_statuses.json')
|
35
|
+
.first
|
36
|
+
.fetch('issue_statuses')
|
37
|
+
end
|
38
|
+
|
39
|
+
def issues(options = {}) # rubocop:disable Metrics/AbcSize
|
40
|
+
options = { limit: 10, offset: 0, sort: :asc }.merge(options.to_h)
|
41
|
+
Enumerator.new do |yielder|
|
42
|
+
loop do
|
43
|
+
result, _response = @rest_client.get(
|
44
|
+
'/issues.json?' + URI.encode_www_form(options)
|
45
|
+
)
|
46
|
+
result.fetch('issues').each { |issue| yielder << Issue.new(issue) }
|
47
|
+
position = result.fetch('limit') + result.fetch('offset')
|
48
|
+
raise StopIteration unless result.fetch('total_count').to_i > position
|
49
|
+
options.merge!(offset: options.fetch(:limit) + options.fetch(:offset))
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
|
3
|
+
module Redmine
|
4
|
+
# The Command mixin helps define command objects used in the command line
|
5
|
+
# interface. Classes can mix in this behaviour to make it easier to take
|
6
|
+
# command line arguments, parse them and use the resulting options in the
|
7
|
+
# command execution.
|
8
|
+
#
|
9
|
+
# Usage example:
|
10
|
+
#
|
11
|
+
# class MyCommand
|
12
|
+
# extend Command
|
13
|
+
#
|
14
|
+
# usage 'greet [OPTIONS]' do |o|
|
15
|
+
# o.on '-n', '--name', 'Provide the name to be used' do |name|
|
16
|
+
# options[:name] = name
|
17
|
+
# end
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# def call(arguments)
|
21
|
+
# puts "Hello, #{options[:name]}!"
|
22
|
+
# end
|
23
|
+
# end
|
24
|
+
module Command
|
25
|
+
def self.extended(base) # :nodoc:
|
26
|
+
base.send(:prepend, InstanceMethods)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Define the command line options available to this command. This is a
|
30
|
+
# wrapper around Ruby's own +OptionParser+. Options defined here will be
|
31
|
+
# parsed out of incoming arguments, and what remains will be passed to
|
32
|
+
# #call. Within this block, you have access to an +options+ hash that
|
33
|
+
# is also available in the #call method.
|
34
|
+
def usage(description, &block)
|
35
|
+
@usage_description = description
|
36
|
+
@usage_options = block
|
37
|
+
end
|
38
|
+
|
39
|
+
# Get the usage description set with #usage.
|
40
|
+
def usage_description
|
41
|
+
@usage_description ||= ''
|
42
|
+
end
|
43
|
+
|
44
|
+
# Get the +OptionParser+ definition block set with #usage.
|
45
|
+
def usage_options
|
46
|
+
@usage_options ||= ->(opts) {}
|
47
|
+
end
|
48
|
+
|
49
|
+
# Special instance methods for Command objects that define a common
|
50
|
+
# interface:
|
51
|
+
#
|
52
|
+
# * #options can hold options parsed from command-line switches
|
53
|
+
# * #call can be invoked to run the command.
|
54
|
+
module InstanceMethods
|
55
|
+
# An generic +options+ hash that can be used to store preferences in from
|
56
|
+
# command line options, available in both the #usage block and the #call
|
57
|
+
# method.
|
58
|
+
def options
|
59
|
+
@otions ||= {}
|
60
|
+
end
|
61
|
+
|
62
|
+
# Override #call to provide your custom logic for a command object. The
|
63
|
+
# #call in this module will be prepended to #call in your own objects,
|
64
|
+
# ensuring that when invoked, all options will first be parsed. All
|
65
|
+
# non-recognized options will be passed as-is to the original #call
|
66
|
+
# method.
|
67
|
+
def call(arguments)
|
68
|
+
OptionParser.new do |o|
|
69
|
+
o.banner = 'Usage: ' + self.class.usage_description
|
70
|
+
o.separator ''
|
71
|
+
instance_exec o, &self.class.usage_options
|
72
|
+
o.on_tail '-h', '--help', 'Show this message' do
|
73
|
+
puts o
|
74
|
+
exit
|
75
|
+
end
|
76
|
+
end.parse!(arguments)
|
77
|
+
super(arguments)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'redmine/commands/issue/show'
|
2
|
+
require 'redmine/commands/issue/activity'
|
3
|
+
|
4
|
+
module Redmine
|
5
|
+
module Commands
|
6
|
+
# Dispatcher for issue-related subcommands.
|
7
|
+
class Issue
|
8
|
+
def initialize(issue_id:, redmine:)
|
9
|
+
@issue_id = issue_id
|
10
|
+
@redmine = redmine
|
11
|
+
end
|
12
|
+
|
13
|
+
def call(arguments)
|
14
|
+
subcommand, *other_args = arguments
|
15
|
+
command = self.class.const_get(
|
16
|
+
subcommand.split('_').map(&:capitalize).join
|
17
|
+
)
|
18
|
+
command.new(issue_id: @issue_id, redmine: @redmine).call(other_args)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'redmine/command'
|
2
|
+
|
3
|
+
module Redmine
|
4
|
+
module Commands
|
5
|
+
class Issue
|
6
|
+
# Sub-subcommand to show activity (comments, changes) on a given issue.
|
7
|
+
class Activity
|
8
|
+
extend Command
|
9
|
+
|
10
|
+
usage 'redmine [ISSUE] activity'
|
11
|
+
|
12
|
+
def initialize(issue_id:, redmine:)
|
13
|
+
@issue_id = issue_id
|
14
|
+
@redmine = redmine
|
15
|
+
end
|
16
|
+
|
17
|
+
def call(_args)
|
18
|
+
issue_statuses = @redmine.issue_statuses
|
19
|
+
issue = @redmine.issue(@issue_id)
|
20
|
+
issue.activity.each do |event|
|
21
|
+
puts "* #{event.user} on #{event.created_on}"
|
22
|
+
event.issue_changes.each do |change|
|
23
|
+
puts " #{change.with_statuses(issue_statuses)}"
|
24
|
+
end
|
25
|
+
puts " #{event.notes}"
|
26
|
+
puts
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'redmine/command'
|
2
|
+
|
3
|
+
module Redmine
|
4
|
+
module Commands
|
5
|
+
class Issue
|
6
|
+
# Sub-subcommand to show general information about a given issue.
|
7
|
+
class Show
|
8
|
+
extend Command
|
9
|
+
|
10
|
+
usage 'redmine [ISSUE] show'
|
11
|
+
|
12
|
+
def initialize(issue_id:, redmine:)
|
13
|
+
@issue_id = issue_id
|
14
|
+
@redmine = redmine
|
15
|
+
end
|
16
|
+
|
17
|
+
def call(_args)
|
18
|
+
issue = @redmine.issue(@issue_id)
|
19
|
+
puts "#{issue.tracker} #{issue.id} " \
|
20
|
+
"(#{issue.status}): #{issue.subject}"
|
21
|
+
puts "Author: #{issue.author}"
|
22
|
+
puts "Assigned to: #{issue.assigned_to}"
|
23
|
+
puts "\n#{issue.description}"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'redmine/command'
|
2
|
+
|
3
|
+
module Redmine
|
4
|
+
module Commands
|
5
|
+
# Command to list issues from Redmine.
|
6
|
+
class Issues
|
7
|
+
extend Command
|
8
|
+
|
9
|
+
# rubocop:disable Metrics/LineLength
|
10
|
+
usage 'redmine issues [options]' do |op|
|
11
|
+
op.on '--mine', 'Limit results to issues assigned to me' do
|
12
|
+
options[:assigned_to_id] = 'me'
|
13
|
+
end
|
14
|
+
|
15
|
+
op.on '-p', '--project ID', Integer, 'Limit results to the given project' do |id|
|
16
|
+
options[:project_id] = id
|
17
|
+
end
|
18
|
+
|
19
|
+
op.on '-s', '--status STATUS', %i(open closed), 'Show only open or closed issues', '(open, closed)' do |status|
|
20
|
+
options[:status_id] = status
|
21
|
+
end
|
22
|
+
|
23
|
+
op.on '-o', '--offset N', Integer, 'Skip the first N issues' do |n|
|
24
|
+
options[:offset] = n
|
25
|
+
end
|
26
|
+
|
27
|
+
op.on '-l', '--limit N', Integer, 'Limit the total number of issues' do |_n|
|
28
|
+
options[:total]
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def initialize(redmine:)
|
33
|
+
@redmine = redmine
|
34
|
+
end
|
35
|
+
|
36
|
+
def call(_args)
|
37
|
+
total = options.delete(:total) || 10
|
38
|
+
@redmine.issues(options).first(total).each do |issue|
|
39
|
+
puts "#{issue.id} #{issue.subject}"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'redmine/command'
|
2
|
+
require 'json'
|
3
|
+
require 'csv'
|
4
|
+
|
5
|
+
module Redmine
|
6
|
+
module Commands
|
7
|
+
# Command to calculate lead times for one or more tickets in Redmine.
|
8
|
+
class LeadTimes
|
9
|
+
extend Command
|
10
|
+
|
11
|
+
def initialize(redmine:)
|
12
|
+
@redmine = redmine
|
13
|
+
end
|
14
|
+
|
15
|
+
# rubocop:disable Metrics/LineLength
|
16
|
+
usage 'redmine lead_times [OPTIONS] [ISSUE_ID...]' do |o|
|
17
|
+
o.on '-f', '--format FORMAT', %w(text csv json), 'Output format to use', '(text, csv, json)' do |format|
|
18
|
+
options[:format] = format
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def call(arguments)
|
23
|
+
case options[:format]
|
24
|
+
when 'json' then generate_json(arguments)
|
25
|
+
when 'csv' then generate_csv(arguments)
|
26
|
+
when 'text' then generate_text(arguments)
|
27
|
+
else raise ArgumentError, "Unknown format #{options[:format].inspect}"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def generate_json(arguments)
|
34
|
+
lead_times = to_enum(:each_issue_lead_time, arguments).inject({}) do |output, (issue_id, lead_time)|
|
35
|
+
output.merge issue_id => lead_time
|
36
|
+
end
|
37
|
+
puts JSON.dump(lead_times)
|
38
|
+
end
|
39
|
+
|
40
|
+
def generate_csv(arguments)
|
41
|
+
csv_output = CSV.generate do |csv|
|
42
|
+
each_issue_lead_time(arguments) do |issue_id, lead_time|
|
43
|
+
csv << [issue_id, lead_time]
|
44
|
+
end
|
45
|
+
end
|
46
|
+
puts csv_output
|
47
|
+
end
|
48
|
+
|
49
|
+
def generate_text
|
50
|
+
each_issue_lead_time(arguments) do |issue_id, lead_time|
|
51
|
+
puts "#{issue_id}\t#{lead_time}"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def each_issue_lead_time(ids)
|
56
|
+
ids.each do |id|
|
57
|
+
yield id, @redmine.issue(id).lead_time
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'redmine/command'
|
2
|
+
|
3
|
+
module Redmine
|
4
|
+
module Commands
|
5
|
+
# Command to list projects in Redmine.
|
6
|
+
class Projects
|
7
|
+
extend Command
|
8
|
+
|
9
|
+
def initialize(redmine:)
|
10
|
+
@redmine = redmine
|
11
|
+
end
|
12
|
+
|
13
|
+
def call(_arguments)
|
14
|
+
@redmine.projects.each do |project|
|
15
|
+
puts "#{project.id} #{project.name}"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
require 'pathname'
|
3
|
+
require 'yaml'
|
4
|
+
|
5
|
+
module Redmine
|
6
|
+
# Container for library-level configuration, acting like an OpenStruct.
|
7
|
+
class Configuration < OpenStruct
|
8
|
+
# Filename of configuration files to look for.
|
9
|
+
CONFIG_FILENAME = '.redmine.yml'.freeze
|
10
|
+
|
11
|
+
# Load configuration from special YAML config files on disk, looking into
|
12
|
+
# the current directory and upwards, merging all together.
|
13
|
+
def self.autoload
|
14
|
+
new(Pathname
|
15
|
+
.pwd
|
16
|
+
.ascend
|
17
|
+
.lazy
|
18
|
+
.map { |p| p.join(CONFIG_FILENAME) }
|
19
|
+
.select(&:exist?)
|
20
|
+
.map { |p| YAML.load_file(p) }
|
21
|
+
.to_a
|
22
|
+
.reverse
|
23
|
+
.inject(:merge)
|
24
|
+
)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'delegate'
|
2
|
+
|
3
|
+
module Redmine
|
4
|
+
# A decorator object for RestClient that adds caching capabilities to regular
|
5
|
+
# RestClient objects. It wraps normal request methods and uses the +Etag+ and
|
6
|
+
# +If-None-Match+ headers to locally store responses. On subsequent requests
|
7
|
+
# for the same resource, we can skip downloading the entire body when the
|
8
|
+
# server indicates nothing has changed (Not Modified).
|
9
|
+
#
|
10
|
+
# A cache is used to keep track of responses. This is assumed to support the
|
11
|
+
# +Pstore+ interface, responding to +#transaction+, +#[]+ and +#[]=+ methods.
|
12
|
+
#
|
13
|
+
# Usage example:
|
14
|
+
#
|
15
|
+
# require 'pstore'
|
16
|
+
# cache = PStore.new('cache.pstore')
|
17
|
+
# rest_client = RestClient.new
|
18
|
+
# caching_rest_client = HttpCaching.new(rest_client, cache)
|
19
|
+
# caching_rest_client.get('/path')
|
20
|
+
class HttpCaching < SimpleDelegator
|
21
|
+
def initialize(http, cache)
|
22
|
+
@cache = cache
|
23
|
+
super(http)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Wrap RestClient#get to provide HTTP caching.
|
27
|
+
#
|
28
|
+
# New requests are stored in a cache if they have an +Etag+ header. On
|
29
|
+
# subsequent requests to the same resource, a +If-None-Match+ header is sent
|
30
|
+
# along, using the original Etag value. The server can then indicate that
|
31
|
+
# nothing has changed, which will trigger this decorator to return the
|
32
|
+
# cached response -- rather than downloading a whole new copy of the same
|
33
|
+
# data.
|
34
|
+
def get(path, headers = {})
|
35
|
+
cached_response = fetch_cached_response(path)
|
36
|
+
if cached_response
|
37
|
+
headers = headers.merge 'If-None-Match' => cached_response['Etag']
|
38
|
+
end
|
39
|
+
response = super(path, headers)
|
40
|
+
case response
|
41
|
+
when Net::HTTPNotModified then cached_response
|
42
|
+
else
|
43
|
+
cache_response(path, response)
|
44
|
+
response
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def fetch_cached_response(path)
|
51
|
+
@cache.transaction do
|
52
|
+
@cache[URI(path).path]
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def cache_response(path, response)
|
57
|
+
return unless response['Etag']
|
58
|
+
@cache.transaction do
|
59
|
+
@cache[path] = response
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'time'
|
2
|
+
require 'redmine/value'
|
3
|
+
require 'redmine/issue_event'
|
4
|
+
|
5
|
+
module Redmine
|
6
|
+
# Represents a single issue (ticket) in the Redmine system.
|
7
|
+
class Issue < Value
|
8
|
+
attribute :id, type: Integer
|
9
|
+
attribute :subject
|
10
|
+
attribute :description
|
11
|
+
attribute :done_ratio, type: Integer
|
12
|
+
attribute :start_date, type: DateTime
|
13
|
+
attribute :created_on, type: DateTime
|
14
|
+
attribute :updated_on, type: DateTime
|
15
|
+
attribute :closed_on, type: DateTime
|
16
|
+
attribute :project, path: 'name'
|
17
|
+
attribute :tracker, path: 'name'
|
18
|
+
attribute :status, path: 'name'
|
19
|
+
attribute :priority, path: 'name'
|
20
|
+
attribute :author, path: 'name'
|
21
|
+
attribute :assigned_to, path: 'name'
|
22
|
+
attribute :fixed_version, path: 'name'
|
23
|
+
attribute :journals, type: Array
|
24
|
+
attribute :spent_hours, type: Integer
|
25
|
+
|
26
|
+
# List event history for this Issue as IssueEvent objects.
|
27
|
+
def activity
|
28
|
+
journals.map do |event|
|
29
|
+
IssueEvent.new(event)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Calculate the lead time for this ticket.
|
34
|
+
#
|
35
|
+
# This returns the difference in days between the closed date and the start
|
36
|
+
# date.
|
37
|
+
def lead_time
|
38
|
+
return Float::INFINITY unless closed_on
|
39
|
+
(closed_on - [created_on, start_date].max).to_i
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'redmine/value'
|
2
|
+
|
3
|
+
module Redmine
|
4
|
+
# An IssueChange is a change in the history of an Issue, such as assigning it
|
5
|
+
# to a different user, or changing its state.
|
6
|
+
class IssueChange < Value
|
7
|
+
attribute :id, type: Integer
|
8
|
+
attribute :name
|
9
|
+
attribute :property
|
10
|
+
attribute :old_value
|
11
|
+
attribute :new_value
|
12
|
+
|
13
|
+
# Provide a human-readable description of the change that this object
|
14
|
+
# represents.
|
15
|
+
def to_s
|
16
|
+
format '%s: %s => %s', name, old_value, new_value
|
17
|
+
end
|
18
|
+
|
19
|
+
# Like #to_s, but use a given map of IDs to human-readable statuses to
|
20
|
+
# provide more meaningful information.
|
21
|
+
def with_statuses(issue_statuses)
|
22
|
+
if name == 'status_id'
|
23
|
+
format 'Status: %s => %s',
|
24
|
+
find_issue_status(issue_statuses, old_value),
|
25
|
+
find_issue_status(issue_statuses, new_value)
|
26
|
+
else
|
27
|
+
to_s
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def find_issue_status(issue_statuses, value)
|
34
|
+
issue_status = issue_statuses.find do |is|
|
35
|
+
is.fetch('id').to_i == value.to_i
|
36
|
+
end
|
37
|
+
issue_status.fetch('name')
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'time'
|
2
|
+
require 'redmine/value'
|
3
|
+
require 'redmine/issue_change'
|
4
|
+
|
5
|
+
module Redmine
|
6
|
+
# An IssueEvent is a set of changes by a user to an Issue in the system,
|
7
|
+
# optionally with some notes. A user might change several properties of a
|
8
|
+
# ticket at once, as represented by IssueChange.
|
9
|
+
class IssueEvent < Value
|
10
|
+
attribute :id, type: Integer
|
11
|
+
attribute :user, path: 'name'
|
12
|
+
attribute :notes
|
13
|
+
attribute :created_on, type: DateTime
|
14
|
+
attribute :details, type: Array
|
15
|
+
|
16
|
+
# List of all changes introduced by this event as IssueChange objects.
|
17
|
+
def issue_changes
|
18
|
+
@details.map do |change|
|
19
|
+
IssueChange.new(change)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'time'
|
2
|
+
require 'redmine/value'
|
3
|
+
|
4
|
+
module Redmine
|
5
|
+
# A project is a container of issues.
|
6
|
+
class Project < Value
|
7
|
+
attribute :id, type: Integer
|
8
|
+
attribute :name
|
9
|
+
attribute :identifier
|
10
|
+
attribute :description
|
11
|
+
attribute :status
|
12
|
+
attribute :parent
|
13
|
+
attribute :created_on, type: DateTime
|
14
|
+
attribute :updated_on, type: DateTime
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'redmine/accept_json'
|
2
|
+
require 'redmine/http_caching'
|
3
|
+
require 'net/http'
|
4
|
+
|
5
|
+
module Redmine
|
6
|
+
# Simple REST-aware HTTP client that uses +Net::HTTP+ under the hood to make
|
7
|
+
# external requests. Most of its functionality comes from decorators, such as
|
8
|
+
# AcceptJson and HttpCaching.
|
9
|
+
class RestClient
|
10
|
+
# Initialize a new RestClient using default headers to be included in all
|
11
|
+
# requests, and optionally a customized HTTP client. If not providing a
|
12
|
+
# custom +http+ option, you can provide a +base_uri+ that will be used to
|
13
|
+
# create a new Net::HTTP instance.
|
14
|
+
def initialize(
|
15
|
+
default_headers: {},
|
16
|
+
base_uri: nil,
|
17
|
+
http: Net::HTTP.new(base_uri.host, base_uri.port)
|
18
|
+
)
|
19
|
+
@http = http
|
20
|
+
@default_headers = default_headers
|
21
|
+
end
|
22
|
+
|
23
|
+
# Perform a GET requests to a given path, optionally with additional
|
24
|
+
# headers. Returns raw Net::HTTPResponse objects.
|
25
|
+
def get(path, headers = {})
|
26
|
+
request = Net::HTTP::Get.new(path)
|
27
|
+
@default_headers.merge(headers).each do |key, value|
|
28
|
+
request[key] = value
|
29
|
+
end
|
30
|
+
@http.request(request)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
module Redmine
|
2
|
+
# Base class for immutable value objects.
|
3
|
+
#
|
4
|
+
# Value objects with the same attributes are considered equal. Value objects
|
5
|
+
# cannot be modified, but new values can be constructed by applying new
|
6
|
+
# attribute values to existing values.
|
7
|
+
class Value
|
8
|
+
attr_reader :hash
|
9
|
+
|
10
|
+
# List of all known attributes supported by this Value.
|
11
|
+
def self.attributes
|
12
|
+
@attributes ||= []
|
13
|
+
end
|
14
|
+
|
15
|
+
# @param [Object] value raw data to be tranformed into `type`.
|
16
|
+
# @param [Object, #parse] type
|
17
|
+
# @return [Object] value parsed to `type`
|
18
|
+
def self.parse_value(value, type = nil)
|
19
|
+
case type
|
20
|
+
when ->(t) { t.nil? } then value
|
21
|
+
when ->(t) { t === value } then value
|
22
|
+
when ->(t) { t.respond_to?(:parse) } then type.parse(value)
|
23
|
+
when ->(t) { Kernel.respond_to?(t.to_s) }
|
24
|
+
Kernel.send(type.to_s, value)
|
25
|
+
else raise ArgumentError, "Invalid type #{type.inspect}"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Define a new attribute for this value. Attributes can be casted
|
30
|
+
# into a specific type, and can optionally be read from a path
|
31
|
+
# inside a nested structure (see Rubys `dig` method).
|
32
|
+
#
|
33
|
+
# @param [Symbol, #to_sym] name
|
34
|
+
# @param [Class, #parse] type conversion method or class that can be
|
35
|
+
# used to parse the incoming values.
|
36
|
+
# @param [Array] path for `dig` to retrieve nested values
|
37
|
+
# @return [nil]
|
38
|
+
def self.attribute(name, type: nil, path: nil)
|
39
|
+
attributes << name.to_sym
|
40
|
+
attr_reader name
|
41
|
+
|
42
|
+
define_method :"#{name}=" do |value|
|
43
|
+
value = value.dig(*Array(path)) if path && value.respond_to?(:dig)
|
44
|
+
parsed_value = self.class.parse_value(value, type)
|
45
|
+
instance_variable_set(:"@#{name}", parsed_value)
|
46
|
+
end
|
47
|
+
|
48
|
+
private :"#{name}="
|
49
|
+
nil
|
50
|
+
end
|
51
|
+
|
52
|
+
# @param [Hash] attrs
|
53
|
+
def initialize(attrs = {})
|
54
|
+
attrs.to_h.each do |name, value|
|
55
|
+
send(:"#{name}=", value)
|
56
|
+
end
|
57
|
+
@hash = self.class.hash ^ to_a.hash
|
58
|
+
freeze
|
59
|
+
end
|
60
|
+
|
61
|
+
# Create a new value based on the current value, but with the incoming
|
62
|
+
# attributes assigned. This "changes" the value, but leaves the original
|
63
|
+
# value intact -- as you'll get a new instance instead.
|
64
|
+
#
|
65
|
+
# @param [Hash] attrs new attributes to be merged with this value's
|
66
|
+
# attributes.
|
67
|
+
# @return [Value]
|
68
|
+
def with(attrs = {})
|
69
|
+
self.class.new(to_h.merge(attrs))
|
70
|
+
end
|
71
|
+
|
72
|
+
# @return [Bool]
|
73
|
+
def eql?(other)
|
74
|
+
hash == other.hash
|
75
|
+
end
|
76
|
+
alias == eql?
|
77
|
+
|
78
|
+
# @return [Hash]
|
79
|
+
def to_h
|
80
|
+
Hash[to_a]
|
81
|
+
end
|
82
|
+
|
83
|
+
# @return [Array]
|
84
|
+
def to_a
|
85
|
+
self.class.attributes.map { |attr| [attr, send(attr)] }
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
data/redmine_api.gemspec
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'redmine/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'redmine-api'
|
8
|
+
spec.version = Redmine::VERSION
|
9
|
+
spec.authors = ['Arjan van der Gaag']
|
10
|
+
spec.email = ['arjan@arjanvandergaag.nl']
|
11
|
+
|
12
|
+
spec.summary = 'Work with Redmine from the command line.'
|
13
|
+
spec.homepage = 'https://avdgaag.github.io/redmine-api'
|
14
|
+
spec.license = 'MIT'
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f|
|
17
|
+
f.match(%r{^(test|spec|features)/})
|
18
|
+
}
|
19
|
+
spec.bindir = 'exe'
|
20
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
21
|
+
spec.require_paths = ['lib']
|
22
|
+
|
23
|
+
spec.add_development_dependency 'bundler', '~> 1.11'
|
24
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
25
|
+
spec.add_development_dependency 'minitest', '~> 5.0'
|
26
|
+
end
|
metadata
ADDED
@@ -0,0 +1,123 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: redmine-api
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Arjan van der Gaag
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-03-11 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.11'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.11'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: minitest
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '5.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '5.0'
|
55
|
+
description:
|
56
|
+
email:
|
57
|
+
- arjan@arjanvandergaag.nl
|
58
|
+
executables:
|
59
|
+
- redmine
|
60
|
+
extensions: []
|
61
|
+
extra_rdoc_files: []
|
62
|
+
files:
|
63
|
+
- ".gitignore"
|
64
|
+
- ".ruby-version"
|
65
|
+
- ".travis.yml"
|
66
|
+
- CODE_OF_CONDUCT.md
|
67
|
+
- Gemfile
|
68
|
+
- Gemfile.lock
|
69
|
+
- LICENSE.txt
|
70
|
+
- README.md
|
71
|
+
- Rakefile
|
72
|
+
- bin/console
|
73
|
+
- bin/rake
|
74
|
+
- bin/setup
|
75
|
+
- exe/redmine
|
76
|
+
- lib/redmine.rb
|
77
|
+
- lib/redmine/accept_json.rb
|
78
|
+
- lib/redmine/cli.rb
|
79
|
+
- lib/redmine/client.rb
|
80
|
+
- lib/redmine/command.rb
|
81
|
+
- lib/redmine/commands.rb
|
82
|
+
- lib/redmine/commands/issue.rb
|
83
|
+
- lib/redmine/commands/issue/activity.rb
|
84
|
+
- lib/redmine/commands/issue/show.rb
|
85
|
+
- lib/redmine/commands/issues.rb
|
86
|
+
- lib/redmine/commands/lead_times.rb
|
87
|
+
- lib/redmine/commands/projects.rb
|
88
|
+
- lib/redmine/configuration.rb
|
89
|
+
- lib/redmine/http_caching.rb
|
90
|
+
- lib/redmine/issue.rb
|
91
|
+
- lib/redmine/issue_change.rb
|
92
|
+
- lib/redmine/issue_event.rb
|
93
|
+
- lib/redmine/project.rb
|
94
|
+
- lib/redmine/rest_client.rb
|
95
|
+
- lib/redmine/value.rb
|
96
|
+
- lib/redmine/version.rb
|
97
|
+
- redmine_api.gemspec
|
98
|
+
homepage: https://avdgaag.github.io/redmine-api
|
99
|
+
licenses:
|
100
|
+
- MIT
|
101
|
+
metadata: {}
|
102
|
+
post_install_message:
|
103
|
+
rdoc_options: []
|
104
|
+
require_paths:
|
105
|
+
- lib
|
106
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
112
|
+
requirements:
|
113
|
+
- - ">="
|
114
|
+
- !ruby/object:Gem::Version
|
115
|
+
version: '0'
|
116
|
+
requirements: []
|
117
|
+
rubyforge_project:
|
118
|
+
rubygems_version: 2.5.1
|
119
|
+
signing_key:
|
120
|
+
specification_version: 4
|
121
|
+
summary: Work with Redmine from the command line.
|
122
|
+
test_files: []
|
123
|
+
has_rdoc:
|