watson-ruby 1.5.1 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/README.md +37 -8
- data/lib/watson.rb +23 -15
- data/lib/watson/asana.rb +619 -0
- data/lib/watson/bitbucket.rb +0 -3
- data/lib/watson/command.rb +29 -6
- data/lib/watson/config.rb +79 -25
- data/lib/watson/formatters/base_formatter.rb +0 -1
- data/lib/watson/formatters/default_formatter.rb +9 -0
- data/lib/watson/fs.rb +0 -3
- data/lib/watson/github.rb +5 -4
- data/lib/watson/gitlab.rb +0 -3
- data/lib/watson/parser.rb +7 -5
- data/lib/watson/printer.rb +0 -3
- data/lib/watson/remote.rb +2 -4
- data/lib/watson/version.rb +1 -1
- data/spec/config_spec.rb +6 -0
- data/spec/parser_spec.rb +1 -0
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 91ffcd1dd3152a9e63f14d03f55f7c7ea50e7e95
|
4
|
+
data.tar.gz: f799be3066226f1f7ccae68dc1a2502325817a6c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8c8be18c25738d0c92401834d3adb7259356c98f4c6422e67a06ce4a15638865b1aada5e8be03c38110957d986c3e6ba31d3acb617892fe6dcb44d00c818781e
|
7
|
+
data.tar.gz: 0270accff9246e278d2e89ea71a5823a21e6771dc14e4d523d96b9bd6e6d2d5b645816b73303c649a46282f4c2f098924af2a4a215da1f6ae8c3cde353a49212
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -1,4 +1,5 @@
|
|
1
|
-
# watson-ruby [](https://travis-ci.org/nhmood/watson-ruby)
|
1
|
+
# watson-ruby [](http://badge.fury.io/rb/watson-ruby) [](https://travis-ci.org/nhmood/watson-ruby)
|
2
|
+
|
2
3
|
### an inline issue manager
|
3
4
|
[watson](http://goosecode.com/watson) ([mirror](http://nhmood.github.io/watson-ruby)) is a tool for creating and tracking bug reports, issues, and internal notes in code.
|
4
5
|
It is avaliable in two flavors, [watson-ruby](http://github.com/nhmood/watson-ruby) and [watson-perl](http://github.com/nhmood/watson-perl)
|
@@ -27,6 +28,7 @@ bundle exec rake
|
|
27
28
|
|
28
29
|
|
29
30
|
## Recent Updates
|
31
|
+
- watson now supports [Asana](http://asana.com)
|
30
32
|
- watson now supports [GitLab](http://gitlab.com)
|
31
33
|
- [vim-unite-watson.vim](https://github.com/alpaca-tc/vim-unite-watson.vim) integrates watson right into vim!
|
32
34
|
- watson now supports multiple export modes
|
@@ -63,6 +65,10 @@ submit a pull request (comment parsing happens in **lib/watson/paser.rb**)
|
|
63
65
|
- **Python**
|
64
66
|
- **Coffeescript**
|
65
67
|
- **Clojure**
|
68
|
+
- **VimL**
|
69
|
+
- **Markdown**
|
70
|
+
- **HTML**
|
71
|
+
- **Emacslisp**
|
66
72
|
|
67
73
|
|
68
74
|
## Command line arguments
|
@@ -80,7 +86,7 @@ If no RC file exists, default RC file will be created
|
|
80
86
|
-i, --ignore list of files, directories, or types to ignore
|
81
87
|
-p, --parse-depth depth to recursively parse directories
|
82
88
|
-r, --remote list / create tokens for remote posting
|
83
|
-
[github, bitbucket, gitlab]
|
89
|
+
[github, bitbucket, gitlab, asana]
|
84
90
|
-s, --show filter results (files listed) based on issue status
|
85
91
|
[all, clean, dirty]
|
86
92
|
-t, --tags list of tags to search for
|
@@ -111,6 +117,13 @@ It should be followed by a space separated list of directories that should be pa
|
|
111
117
|
If watson is run without this parameter, the current directory is parsed.
|
112
118
|
|
113
119
|
|
120
|
+
### --debug [CLASS]
|
121
|
+
This parameter enables debug prints for the specified class.
|
122
|
+
It should be followed by a space separated list of classes that should print debug messages.
|
123
|
+
The list of classes are found in `lib/watson`.
|
124
|
+
If passed without arguments, debug prints in **ALL** classes of watson will be enabled.
|
125
|
+
|
126
|
+
|
114
127
|
### -f, --files [FILES]
|
115
128
|
This parameter specifies which files watson should parse through.
|
116
129
|
It should be followed by a space separated list of files that should be parsed.
|
@@ -149,10 +162,10 @@ If individual directories are passed with the -d (--dirs) flag, each will be par
|
|
149
162
|
If watson is run without this parameter, the parsing depth is unlimited and will search through all subdirectories found.
|
150
163
|
|
151
164
|
|
152
|
-
### -r, --remote [GITHUB, BITBUCKET, GITLAB]
|
165
|
+
### -r, --remote [GITHUB, BITBUCKET, GITLAB, ASANA]
|
153
166
|
This parameter is used to both list currently established remotes as well as setup new ones.
|
154
167
|
If passed without any options, the currently established remotes will be listed.
|
155
|
-
If passed with a github, bitbucket, gitlab argument, watson will proceed to ask some questions to set up the corresponding remote.
|
168
|
+
If passed with a github, bitbucket, gitlab, asana argument, watson will proceed to ask some questions to set up the corresponding remote.
|
156
169
|
|
157
170
|
|
158
171
|
### -s, --show [ALL, CLEAN, DIRTY]
|
@@ -180,9 +193,13 @@ This parameter displays the current version of watson you are running.
|
|
180
193
|
## .watsonrc
|
181
194
|
watson supports an RC file that allows for reusing commong settings without repeating command line arguments every time.
|
182
195
|
|
183
|
-
The .watsonrc is placed in every directory that watson is run from as opposed to a unified file (in ~/.watsonrc for example). The
|
196
|
+
The .watsonrc is placed in every directory that watson is run from as opposed to a unified file (in ~/.watsonrc for example). The thought process behind this is that each project may have a different set of folders to ignore, directories to search through, and tags to look for.
|
184
197
|
For example, a C/C++ project might want to look in src/ and ignore obj/ whereas a Ruby project might want to look in lib/ and ignore assets/.
|
185
198
|
|
199
|
+
A base `.watsonrc` is created in the users `$HOME` directory and used as the template for creating all subsequent config files.
|
200
|
+
Any changes made in the `$HOME/.watsonrc` will carry on towards new configs, but will **not** update previously created configs.
|
201
|
+
The `$HOME/.watsonrc` is directly copied, not merged, as to avoid confusion as to the source of config parameters.
|
202
|
+
|
186
203
|
The .watsonrc file is fairly straightforward...
|
187
204
|
**[dirs]** - This is a newline separated list of directories to look in while parsing.
|
188
205
|
|
@@ -191,15 +208,27 @@ The .watsonrc file is fairly straightforward...
|
|
191
208
|
**[ignore]** - This is a newline separated list of files / folders to ignore while parsing.
|
192
209
|
This supports wildcard type selecting by providing .filename (no * required)
|
193
210
|
|
194
|
-
**[context_depth]** - This value determines how many lines of context should be grabbed for each issue when posting to a remote.
|
211
|
+
**[context_depth]** - This value determines how many lines of context should be grabbed for each issue when posting to a remote.
|
195
212
|
|
196
|
-
**[
|
213
|
+
**[type]** - This field allows for custom filetype/comment associations without having to edit the gem itself.
|
214
|
+
The format of filetype entries should be
|
215
|
+
```
|
216
|
+
".type" => ["comment1", "comment2"]
|
217
|
+
".nhmood" => ["@@", "***"]
|
218
|
+
".cc" => ["//"]
|
219
|
+
```
|
220
|
+
|
221
|
+
**[(github/bitbucket/gitlab/asana)_api]** - If a remote is established, the API key for the corresponding remote is stored here.
|
197
222
|
Currently, OAuth has yet to be implemented for Bitbucket so the Bitbucket username is stored here.
|
198
223
|
|
199
224
|
**[(github/bitbucket/gitlab)_repo]** - The repo name / path is stored here.
|
200
225
|
|
201
226
|
**[(github/gitlab)_endpoint]** - The endpoint in case of GitHub Enterprise of GitLab configuration.
|
202
227
|
|
228
|
+
**[asana_project]** - The Asana project in case of Asana integration is stored here.
|
229
|
+
|
230
|
+
**[asana_workspace]** - The Asana workspace in case of Asana integration is stored here.
|
231
|
+
|
203
232
|
The remote related .watsonrc options shouldn't need to be edited manually, as they are automatically populated when the -r, --remote setup is called.
|
204
233
|
|
205
234
|
## unite.vim
|
@@ -222,7 +251,7 @@ Special thanks to [@eugenekolo](http://twitter.com/eugenekolo) [[email](eugenek@
|
|
222
251
|
Special thanks to [@crowell](http://github.com/crowell) for testing out watson-ruby!
|
223
252
|
|
224
253
|
## FAQ
|
225
|
-
- **
|
254
|
+
- **Will inline issues get deleted if I close them on GitHub/Bitbucket/GitLab?**
|
226
255
|
No, watson won't touch your source code, it will only inform you that issues have been closed remotely.
|
227
256
|
|
228
257
|
- **Why Ruby?**
|
data/lib/watson.rb
CHANGED
@@ -8,6 +8,7 @@ require_relative 'watson/remote'
|
|
8
8
|
require_relative 'watson/github'
|
9
9
|
require_relative 'watson/bitbucket'
|
10
10
|
require_relative 'watson/gitlab'
|
11
|
+
require_relative 'watson/asana'
|
11
12
|
require_relative 'watson/version'
|
12
13
|
|
13
14
|
module Watson
|
@@ -28,13 +29,14 @@ module Watson
|
|
28
29
|
# [todo] - Replace Identify line in each method with method_added call
|
29
30
|
# http://ruby-doc.org/core-2.0.0/Module.html#method-i-method_added
|
30
31
|
|
31
|
-
# Separate ON and OFF so we can force state and still let
|
32
|
-
# individual classes have some control over their prints
|
33
32
|
|
34
|
-
#
|
35
|
-
|
36
|
-
|
37
|
-
|
33
|
+
# Module container for debug mode (which classes to debug print)
|
34
|
+
# [review] - This doesn't seem like the right place to put this
|
35
|
+
class << self
|
36
|
+
attr_accessor :debug_mode
|
37
|
+
@@debug_mode = Array.new()
|
38
|
+
end
|
39
|
+
|
38
40
|
|
39
41
|
# [review] - Not sure if module_function is proper way to scope
|
40
42
|
# I want to be able to call debug_print without having to use the scope
|
@@ -46,18 +48,24 @@ module Watson
|
|
46
48
|
###########################################################
|
47
49
|
# Global debug print that prints based on local file DEBUG flag as well as GLOBAL debug flag
|
48
50
|
def debug_print(msg)
|
49
|
-
# [todo] - If input msg is a Hash, use pp to dump it
|
50
51
|
|
51
|
-
#
|
52
|
-
|
53
|
-
|
52
|
+
# If nothing set from CLI, debug_mode will be nil
|
53
|
+
return if Watson.debug_mode.nil?
|
54
|
+
|
55
|
+
# If empty, just --debug passed, print ALL, else selective print
|
56
|
+
_enabled = false
|
57
|
+
if !Watson.debug_mode.empty?
|
58
|
+
_debug = (self.is_a? Class) ? self.name.downcase : self.class.name.downcase
|
59
|
+
Watson.debug_mode.each do |dbg|
|
60
|
+
_enabled = true if _debug.include?(dbg)
|
61
|
+
end
|
62
|
+
else
|
63
|
+
_enabled = true
|
64
|
+
end
|
54
65
|
|
55
|
-
|
56
|
-
|
57
|
-
# from a class vs object, so lets take care of that
|
58
|
-
_DEBUG = (self.is_a? Class) ? self::DEBUG : self.class::DEBUG
|
66
|
+
return if !_enabled
|
67
|
+
(msg.is_a? Hash) ? (pp msg) : (print "=> #{msg}")
|
59
68
|
|
60
|
-
print "=> #{msg}" if ( (_DEBUG == true || GLOBAL_DEBUG_ON == true) && (GLOBAL_DEBUG_OFF == false))
|
61
69
|
end
|
62
70
|
|
63
71
|
|
data/lib/watson/asana.rb
ADDED
@@ -0,0 +1,619 @@
|
|
1
|
+
module Watson
|
2
|
+
|
3
|
+
class Remote
|
4
|
+
|
5
|
+
class Asana
|
6
|
+
|
7
|
+
@end_point = "https://app.asana.com/api/1.0"
|
8
|
+
@formatter = nil
|
9
|
+
|
10
|
+
# Debug printing for this class
|
11
|
+
DEBUG = true
|
12
|
+
|
13
|
+
class << self
|
14
|
+
|
15
|
+
# [todo] - post issue to diff. project in same workspace depending on config?
|
16
|
+
|
17
|
+
# Include for debug_print
|
18
|
+
include Watson
|
19
|
+
|
20
|
+
#############################################################################
|
21
|
+
# Setup remote access to Asana
|
22
|
+
# Get API Key, Workspace, Project
|
23
|
+
def setup(config)
|
24
|
+
|
25
|
+
@formatter = Printer.new(config).build_formatter
|
26
|
+
|
27
|
+
# Identify method entry
|
28
|
+
debug_print "#{ self.class } : #{ __method__ }\n"
|
29
|
+
|
30
|
+
@formatter.print_status "+", GREEN
|
31
|
+
print BOLD + "Setting up Asana...\n" + RESET
|
32
|
+
|
33
|
+
config_exists = config.asana_api.empty? && config.asana_workspace.empty? && config.asana_project.empty?
|
34
|
+
unless config_exists
|
35
|
+
@formatter.print_status "!", RED
|
36
|
+
print BOLD + "Previous Asana API is in RC, are you sure you want to overwrite?\n" + RESET
|
37
|
+
print " (Y)es/(N)o: "
|
38
|
+
|
39
|
+
# Get user input
|
40
|
+
_overwrite = $stdin.gets.chomp
|
41
|
+
if ["no", "n"].include?(_overwrite.downcase)
|
42
|
+
print "\n\n"
|
43
|
+
@formatter.print_status "x", RED
|
44
|
+
print BOLD + "Not overwriting current Asana config\n" + RESET
|
45
|
+
return false
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
@formatter.print_status "!", YELLOW
|
50
|
+
print BOLD + "Asana API key required to make/update issues\n" + RESET
|
51
|
+
print " See help or README for more details on Asana access\n\n"
|
52
|
+
|
53
|
+
print "\n"
|
54
|
+
|
55
|
+
print BOLD + "API Key: " + RESET
|
56
|
+
_api_key = $stdin.gets.chomp
|
57
|
+
if _api_key.empty?
|
58
|
+
@formatter.print_status "x", RED
|
59
|
+
print BOLD + "Input blank. Please enter your api key!\n\n" + RESET
|
60
|
+
return false
|
61
|
+
end
|
62
|
+
|
63
|
+
print BOLD + "Workspace: " + RESET
|
64
|
+
_workspace = $stdin.gets.chomp
|
65
|
+
if _workspace.empty?
|
66
|
+
@formatter.print_status "x", RED
|
67
|
+
print BOLD + "Input blank. Please enter your workspace!\n\n" + RESET
|
68
|
+
return false
|
69
|
+
end
|
70
|
+
|
71
|
+
print BOLD + "Project: " + RESET
|
72
|
+
_project = $stdin.gets.chomp
|
73
|
+
if _project.empty?
|
74
|
+
@formatter.print_status "x", RED
|
75
|
+
print BOLD + "Input blank. Please enter your project!\n\n" + RESET
|
76
|
+
return false
|
77
|
+
end
|
78
|
+
|
79
|
+
workspace_dict = get_workspaces(_api_key)
|
80
|
+
|
81
|
+
unless workspace_dict
|
82
|
+
@formatter.print_status "x", RED
|
83
|
+
print BOLD + "Asana setup failed\n" + RESET
|
84
|
+
return false
|
85
|
+
end
|
86
|
+
|
87
|
+
unless workspace_dict.include?(_workspace)
|
88
|
+
print "\n"
|
89
|
+
@formatter.print_status "x", RED
|
90
|
+
print BOLD + "Workspace doesn't exist\n" + RESET
|
91
|
+
print " Check that the workspace name is correct\n"
|
92
|
+
print " Possible workspaces: '#{ workspace_dict.keys.join('\', \'') }'\n\n"
|
93
|
+
return false
|
94
|
+
end
|
95
|
+
|
96
|
+
workspace_id = workspace_dict[_workspace]
|
97
|
+
project_dict = get_projects(_api_key, workspace_id)
|
98
|
+
|
99
|
+
unless project_dict
|
100
|
+
@formatter.print_status "x", RED
|
101
|
+
print BOLD + "Asana API Request failed\n" + RESET
|
102
|
+
return false
|
103
|
+
end
|
104
|
+
|
105
|
+
unless project_dict.include?(_project)
|
106
|
+
print "\n"
|
107
|
+
@formatter.print_status "x", RED
|
108
|
+
print BOLD + "Project doesn't exist within workspace '#{ _workspace }'\n" + RESET
|
109
|
+
print " Check that the project name is correct\n"
|
110
|
+
print " Possible projects: '#{ project_dict.keys.join('\', \'') }'\n\n"
|
111
|
+
return false
|
112
|
+
end
|
113
|
+
|
114
|
+
config.asana_api = _api_key
|
115
|
+
debug_print "Asana API Key updated to: #{ config.asana_api }\n"
|
116
|
+
config.asana_workspace = _workspace
|
117
|
+
debug_print "Asana Workspace updated to: #{ config.asana_workspace }\n"
|
118
|
+
config.asana_project = _project
|
119
|
+
debug_print "Asana Project updated to: #{ config.asana_project }\n"
|
120
|
+
|
121
|
+
# All setup has been completed, need to update RC
|
122
|
+
# Call config updater/writer from @config to write config
|
123
|
+
debug_print "Updating config with new Asana info\n"
|
124
|
+
config.update_conf("asana_api", "asana_workspace", "asana_project")
|
125
|
+
|
126
|
+
# Give user some info
|
127
|
+
print "\n"
|
128
|
+
@formatter.print_status "o", GREEN
|
129
|
+
print BOLD + "Asana successfully setup\n" + RESET
|
130
|
+
print " Issues will now automatically be retrieved from Asana by default\n"
|
131
|
+
print " Use -u, --update to post issues to Asana\n"
|
132
|
+
print " See help or README for more details on GitHub/Bitbucket/Asana access\n\n"
|
133
|
+
|
134
|
+
true
|
135
|
+
|
136
|
+
end
|
137
|
+
|
138
|
+
###########################################################
|
139
|
+
# Post given issue to Asana project
|
140
|
+
def post_issue(issue, config)
|
141
|
+
|
142
|
+
# Identify method entry
|
143
|
+
debug_print "#{ self.class } : #{ __method__ }\n"
|
144
|
+
|
145
|
+
# Set up formatter for printing errors
|
146
|
+
# config.output_format should be set based on less status by now
|
147
|
+
@formatter = Printer.new(config).build_formatter
|
148
|
+
|
149
|
+
# Only attempt to get issues if API is specified
|
150
|
+
if config.asana_api.empty?
|
151
|
+
debug_print "No asana API found, this shouldn't be called...\n"
|
152
|
+
return false
|
153
|
+
end
|
154
|
+
|
155
|
+
return false if config.asana_issues.key?(issue[:md5])
|
156
|
+
debug_print "#{issue[:md5]} not found in remote issues, posting\n"
|
157
|
+
|
158
|
+
_api_key = config.asana_api
|
159
|
+
_workspace = config.asana_workspace
|
160
|
+
_project = config.asana_project
|
161
|
+
|
162
|
+
workspace_id = get_workspace_id(_api_key, _workspace)
|
163
|
+
|
164
|
+
unless workspace_id
|
165
|
+
@formatter.print_status "x", RED
|
166
|
+
print BOLD + "Unable to get workspace info from Asana API\n" + RESET
|
167
|
+
return false
|
168
|
+
end
|
169
|
+
|
170
|
+
tags = init_tags(config, _api_key, workspace_id)
|
171
|
+
|
172
|
+
unless tags
|
173
|
+
@formatter.print_status "x", RED
|
174
|
+
print BOLD + "Unable to initialise tags\n" + RESET
|
175
|
+
return false
|
176
|
+
end
|
177
|
+
|
178
|
+
project_id = get_project_identifier(_api_key, _project, workspace_id)
|
179
|
+
|
180
|
+
unless project_id
|
181
|
+
@formatter.print_status "x", RED
|
182
|
+
print BOLD + "Unable to get project info from Asana API\n" + RESET
|
183
|
+
return false
|
184
|
+
end
|
185
|
+
|
186
|
+
tasks_url = "#{ @end_point }/workspaces/#{ workspace_id }/tasks"
|
187
|
+
|
188
|
+
# Create the body text for the issue here, too long to fit nicely into opts hash
|
189
|
+
_body =
|
190
|
+
"__filename__ : #{ issue[:path] }\n" +
|
191
|
+
"__line #__ : #{ issue[:line_number] }\n" +
|
192
|
+
"__tag__ : #{ issue[:tag] }\n" +
|
193
|
+
"__md5__ : #{ issue[:md5] }\n\n" +
|
194
|
+
"#{ issue[:context].join }\n"
|
195
|
+
|
196
|
+
opts = {
|
197
|
+
:url => tasks_url,
|
198
|
+
:ssl => true,
|
199
|
+
:method => "POST",
|
200
|
+
:basic_auth => [_api_key, ""],
|
201
|
+
:data => [{"name" => issue[:title],
|
202
|
+
"notes" => _body,
|
203
|
+
"projects" => project_id}],
|
204
|
+
:verbose => false
|
205
|
+
}
|
206
|
+
|
207
|
+
_json, _resp = Watson::Remote.http_call(opts)
|
208
|
+
|
209
|
+
unless _resp.code == "201"
|
210
|
+
@formatter.print_status "x", RED
|
211
|
+
print BOLD + "Unable to access Asana API, key may be invalid\n" + RESET
|
212
|
+
print " Consider running --remote (-r) option to regenerate key\n\n"
|
213
|
+
print " Status: #{ _resp.code } - #{ _resp.message }\n"
|
214
|
+
return false
|
215
|
+
end
|
216
|
+
|
217
|
+
_data = _json["data"]
|
218
|
+
new_task_id = _data["id"]
|
219
|
+
|
220
|
+
debug_print "creating file tag"
|
221
|
+
file_tag = create_or_get_tag(_api_key,workspace_id,issue[:path])
|
222
|
+
debug_print "tagging with file tag <#{ file_tag }>"
|
223
|
+
|
224
|
+
if file_tag
|
225
|
+
file_tag_id = file_tag['id']
|
226
|
+
unless tag_task(_api_key, new_task_id, file_tag_id)
|
227
|
+
@formatter.print_status "!", RED
|
228
|
+
print BOLD + "Unable to tag with '#{ issue[:path] }'\n" + RESET
|
229
|
+
end
|
230
|
+
else
|
231
|
+
@formatter.print_status "!", RED
|
232
|
+
print BOLD + "Unable create tag '#{ issue[:path] }'\n" + RESET
|
233
|
+
end
|
234
|
+
|
235
|
+
debug_print "tagging with issue tag <#{ issue[:tag] }>"
|
236
|
+
issue_tag_id = tags[issue[:tag]]['id']
|
237
|
+
unless tag_task(_api_key, new_task_id, issue_tag_id)
|
238
|
+
@formatter.print_status "!", RED
|
239
|
+
print BOLD + "Unable to tag with '#{ issue[:tag] }'\n" + RESET
|
240
|
+
end
|
241
|
+
|
242
|
+
debug_print "tagging with watson tag"
|
243
|
+
watson_tag_id = tags['watson']['id']
|
244
|
+
unless tag_task(_api_key, new_task_id, watson_tag_id)
|
245
|
+
@formatter.print_status "!", RED
|
246
|
+
print BOLD + "Unable to tag with 'watson'\n" + RESET
|
247
|
+
end
|
248
|
+
|
249
|
+
# Parse response and append issue hash so we are up to date
|
250
|
+
config.asana_issues[issue[:md5]] = {
|
251
|
+
:title => _data["name"],
|
252
|
+
:id => _data["id"],
|
253
|
+
:state => _data["completed"]
|
254
|
+
}
|
255
|
+
|
256
|
+
true
|
257
|
+
|
258
|
+
end
|
259
|
+
|
260
|
+
###########################################################
|
261
|
+
# Get all remote Asana issues and store into Config container class
|
262
|
+
def get_issues(config)
|
263
|
+
|
264
|
+
# Identify method entry
|
265
|
+
debug_print "#{ self.class } : #{ __method__ }\n"
|
266
|
+
|
267
|
+
# Set up formatter for printing errors
|
268
|
+
# config.output_format should be set based on less status by now
|
269
|
+
@formatter = Printer.new(config).build_formatter
|
270
|
+
|
271
|
+
# Only attempt to get issues if API is specified
|
272
|
+
if config.asana_api.empty?
|
273
|
+
debug_print "No asana API found, this shouldn't be called...\n"
|
274
|
+
return false
|
275
|
+
end
|
276
|
+
|
277
|
+
_api_key = config.asana_api
|
278
|
+
_workspace = config.asana_workspace
|
279
|
+
_project = config.asana_project
|
280
|
+
|
281
|
+
task_records = get_tasks(_api_key, _project, _workspace)
|
282
|
+
|
283
|
+
unless task_records
|
284
|
+
config.asana_valid = false
|
285
|
+
return false
|
286
|
+
end
|
287
|
+
|
288
|
+
task_records.each do |issue|
|
289
|
+
# Skip this issue if it doesn't have watson md5 tag
|
290
|
+
_md5 = issue["notes"].match(/.*__md5__ : (\w+)\s.*/)
|
291
|
+
next if (_md5).nil?
|
292
|
+
|
293
|
+
# If it does, use md5 as hash key and populate values with our info
|
294
|
+
config.asana_issues[_md5[1]] = {
|
295
|
+
:title => issue["name"],
|
296
|
+
:id => issue["id"],
|
297
|
+
:state => issue["completed"] # TODO: How to check status?
|
298
|
+
}
|
299
|
+
end
|
300
|
+
|
301
|
+
config.asana_valid = true
|
302
|
+
|
303
|
+
end
|
304
|
+
|
305
|
+
private
|
306
|
+
|
307
|
+
###########################################################
|
308
|
+
# Return all projects under the given api_key/workspace
|
309
|
+
def get_projects(_api_key, workspace_id)
|
310
|
+
|
311
|
+
debug_print "#{ self.class } : #{ __method__ }\n"
|
312
|
+
|
313
|
+
projects_url = "#{ @end_point }/workspaces/#{ workspace_id}/projects"
|
314
|
+
|
315
|
+
opts = {
|
316
|
+
:url => projects_url,
|
317
|
+
:ssl => true,
|
318
|
+
:method => "GET",
|
319
|
+
:basic_auth => [_api_key, ""],
|
320
|
+
:verbose => false
|
321
|
+
}
|
322
|
+
|
323
|
+
_json, _resp = Watson::Remote.http_call(opts)
|
324
|
+
|
325
|
+
if _resp.code != "200"
|
326
|
+
print "\n"
|
327
|
+
@formatter.print_status "x", RED
|
328
|
+
print BOLD + "Unable to access project list\n" + RESET
|
329
|
+
print " Status: #{ _resp.code } - #{ _resp.message }\n\n"
|
330
|
+
return false
|
331
|
+
end
|
332
|
+
|
333
|
+
project_list = _json["data"]
|
334
|
+
project_dict = {}
|
335
|
+
project_list.each { |x| project_dict[x["name"]] = x["id"] }
|
336
|
+
project_dict
|
337
|
+
end
|
338
|
+
|
339
|
+
###########################################################
|
340
|
+
# Return all workspaces under the given API key
|
341
|
+
def get_workspaces(_api_key)
|
342
|
+
|
343
|
+
debug_print "#{ self.class } : #{ __method__ }\n"
|
344
|
+
|
345
|
+
workspaces_url = "#{ @end_point }/workspaces"
|
346
|
+
|
347
|
+
opts = {
|
348
|
+
:url => workspaces_url,
|
349
|
+
:ssl => true,
|
350
|
+
:method => "GET",
|
351
|
+
:basic_auth => [_api_key, ""],
|
352
|
+
:verbose => false
|
353
|
+
}
|
354
|
+
|
355
|
+
_json, _resp = Watson::Remote.http_call(opts)
|
356
|
+
|
357
|
+
if _resp.code != "200"
|
358
|
+
print "\n"
|
359
|
+
@formatter.print_status "x", RED
|
360
|
+
print BOLD + "Unable to access workspace list with given credentials\n" + RESET
|
361
|
+
print " Check that API key is correct\n"
|
362
|
+
print " Status: #{ _resp.code } - #{ _resp.message }\n\n"
|
363
|
+
return false
|
364
|
+
end
|
365
|
+
|
366
|
+
workspace_list = _json["data"]
|
367
|
+
workspace_dict = {}
|
368
|
+
workspace_list.each { |x| workspace_dict[x["name"]] = x["id"] }
|
369
|
+
workspace_dict
|
370
|
+
end
|
371
|
+
|
372
|
+
###########################################################
|
373
|
+
# Return all tasks in given project/workspace
|
374
|
+
def get_tasks(_api_key, _project, _workspace)
|
375
|
+
|
376
|
+
debug_print "#{ self.class } : #{ __method__ }\n"
|
377
|
+
|
378
|
+
workspace_id = get_workspace_id(_api_key, _workspace)
|
379
|
+
|
380
|
+
unless workspace_id
|
381
|
+
@formatter.print_status "x", RED
|
382
|
+
print BOLD + "Unable to get workspace info from Asana API\n" + RESET
|
383
|
+
return false end
|
384
|
+
|
385
|
+
project_id = get_project_identifier(_api_key, _project, workspace_id)
|
386
|
+
|
387
|
+
unless project_id
|
388
|
+
@formatter.print_status "x", RED
|
389
|
+
print BOLD + "Unable to get project info from Asana API\n" + RESET
|
390
|
+
return false
|
391
|
+
end
|
392
|
+
|
393
|
+
tasks_url = "#{ @end_point }/projects/#{ project_id }/tasks?opt_fields=name,notes,completed&include_archived=true"
|
394
|
+
|
395
|
+
opts = {
|
396
|
+
:url => tasks_url,
|
397
|
+
:ssl => true,
|
398
|
+
:method => "GET",
|
399
|
+
:basic_auth => [_api_key, ""],
|
400
|
+
:verbose => false
|
401
|
+
}
|
402
|
+
|
403
|
+
_json, _resp = Watson::Remote.http_call(opts)
|
404
|
+
|
405
|
+
# Check response to validate repo access
|
406
|
+
if _resp.code != "200"
|
407
|
+
@formatter.print_status "x", RED
|
408
|
+
print BOLD + "Unable to access Asana API, key may be invalid\n" + RESET
|
409
|
+
print " Consider running --remote (-r) option to regenerate key\n\n"
|
410
|
+
print " Status: #{ _resp.code } - #{ _resp.message }\n"
|
411
|
+
|
412
|
+
debug_print "Asana invalid, setting config var\n"
|
413
|
+
#return false
|
414
|
+
end
|
415
|
+
|
416
|
+
_json["data"]
|
417
|
+
|
418
|
+
end
|
419
|
+
|
420
|
+
###########################################################
|
421
|
+
# Return full record for a particular task
|
422
|
+
def get_task_record(_api_key, task_id)
|
423
|
+
|
424
|
+
debug_print "#{ self.class } : #{ __method__ }\n"
|
425
|
+
|
426
|
+
task_url = "#{ @end_point }/tasks/#{ task_id }"
|
427
|
+
|
428
|
+
opts = {
|
429
|
+
:url => task_url,
|
430
|
+
:ssl => true,
|
431
|
+
:method => "GET",
|
432
|
+
:basic_auth => [_api_key, ""],
|
433
|
+
:verbose => false
|
434
|
+
}
|
435
|
+
|
436
|
+
_json, _resp = Watson::Remote.http_call(opts)
|
437
|
+
|
438
|
+
if _resp.code != "200"
|
439
|
+
@formatter.print_status "x", RED
|
440
|
+
print BOLD + "Unable to get task, API key may be invalid\n" + RESET
|
441
|
+
print " Consider running --remote (-r) option to regenerate key\n\n"
|
442
|
+
print " Status: #{ _resp.code } - #{ _resp.message }\n"
|
443
|
+
return false
|
444
|
+
end
|
445
|
+
|
446
|
+
_json["data"]
|
447
|
+
end
|
448
|
+
|
449
|
+
###########################################################
|
450
|
+
# Get project id given project and work space name
|
451
|
+
def get_project_identifier(_api_key, _project, workspace_id)
|
452
|
+
debug_print "#{ self.class } : #{ __method__ }\n"
|
453
|
+
projects_dict = get_projects(_api_key, workspace_id)
|
454
|
+
unless projects_dict
|
455
|
+
return false
|
456
|
+
end
|
457
|
+
projects_dict[_project]
|
458
|
+
end
|
459
|
+
|
460
|
+
###########################################################
|
461
|
+
# Get workspace id given workspace name
|
462
|
+
def get_workspace_id(_api_key, _workspace)
|
463
|
+
debug_print "#{ self.class } : #{ __method__ }\n"
|
464
|
+
workspace_dict = get_workspaces(_api_key)
|
465
|
+
unless workspace_dict
|
466
|
+
return false
|
467
|
+
end
|
468
|
+
workspace_dict[_workspace]
|
469
|
+
end
|
470
|
+
|
471
|
+
###########################################################
|
472
|
+
# Tags task with task_id with the tag specified by tag_id
|
473
|
+
def tag_task(_api_key, task_id, tag_id)
|
474
|
+
debug_print "#{ self.class } : #{ __method__ }\n"
|
475
|
+
|
476
|
+
tags_url = "#{ @end_point }/tasks/#{ task_id }/addTag"
|
477
|
+
|
478
|
+
opts = {
|
479
|
+
:url => tags_url,
|
480
|
+
:ssl => true,
|
481
|
+
:method => "POST",
|
482
|
+
:basic_auth => [_api_key, ""],
|
483
|
+
:data => [{"tag" => tag_id}],
|
484
|
+
:verbose => false
|
485
|
+
}
|
486
|
+
|
487
|
+
_json, _resp = Watson::Remote.http_call(opts)
|
488
|
+
|
489
|
+
unless _resp.code == "200"
|
490
|
+
@formatter.print_status "x", RED
|
491
|
+
print BOLD + "Unable to access Asana API, key may be invalid\n" + RESET
|
492
|
+
print " Consider running --remote (-r) option to regenerate key\n\n"
|
493
|
+
print " Status: #{ _resp.code } - #{ _resp.message }\n"
|
494
|
+
return false
|
495
|
+
end
|
496
|
+
|
497
|
+
_json['data']
|
498
|
+
|
499
|
+
end
|
500
|
+
|
501
|
+
###########################################################
|
502
|
+
# Returns hash, tag name => tag
|
503
|
+
def get_tags(_api_key, _workspace_id)
|
504
|
+
debug_print "#{ self.class } : #{ __method__ }\n"
|
505
|
+
|
506
|
+
tags_url = "#{ @end_point }/workspaces/#{ _workspace_id }/tags"
|
507
|
+
|
508
|
+
opts = {
|
509
|
+
:url => tags_url,
|
510
|
+
:ssl => true,
|
511
|
+
:method => "GET",
|
512
|
+
:basic_auth => [_api_key, ""],
|
513
|
+
:verbose => false
|
514
|
+
}
|
515
|
+
|
516
|
+
_json, _resp = Watson::Remote.http_call(opts)
|
517
|
+
|
518
|
+
unless _resp.code == "200"
|
519
|
+
@formatter.print_status "x", RED
|
520
|
+
print BOLD + "Unable to access Asana API, key may be invalid\n" + RESET
|
521
|
+
print " Consider running --remote (-r) option to regenerate key\n\n"
|
522
|
+
print " Status: #{ _resp.code } - #{ _resp.message }\n"
|
523
|
+
return false
|
524
|
+
end
|
525
|
+
|
526
|
+
# [review] - fancy ruby way of generating this hash?
|
527
|
+
|
528
|
+
_tags_dict = {}
|
529
|
+
_json['data'].each { |x| _tags_dict[x['name']] = x }
|
530
|
+
_tags_dict
|
531
|
+
|
532
|
+
end
|
533
|
+
|
534
|
+
###########################################################
|
535
|
+
# Make sure base tags exist (tag_list + the watson tag), add them if not
|
536
|
+
def init_tags(config, _api_key, _workspace_id)
|
537
|
+
debug_print "#{ self.class } : #{ __method__ }\n"
|
538
|
+
tags_to_check = config.tag_list.dup << 'watson'
|
539
|
+
create_and_get_tags(_api_key, _workspace_id, tags_to_check)
|
540
|
+
end
|
541
|
+
|
542
|
+
###########################################################
|
543
|
+
# Adds 'tags_to_add' and returns full list of tags.
|
544
|
+
# The reason that this is combined into one method is that
|
545
|
+
# Asana do not actually 'create' the tag properly until
|
546
|
+
# it has been used to tag a task
|
547
|
+
def create_and_get_tags(_api_key, _workspace_id, tags_to_add)
|
548
|
+
debug_print "#{ self.class } : #{ __method__ }\n"
|
549
|
+
|
550
|
+
_tags = get_tags(_api_key, _workspace_id)
|
551
|
+
|
552
|
+
unless _tags
|
553
|
+
return false
|
554
|
+
end
|
555
|
+
|
556
|
+
debug_print "Checking that tags #{ tags_to_add } exist\n"
|
557
|
+
tags_to_create = tags_to_add - _tags.keys
|
558
|
+
if tags_to_create
|
559
|
+
debug_print "Need to create tags #{ tags_to_create }\n"
|
560
|
+
end
|
561
|
+
tags_to_create.each do |tag_name|
|
562
|
+
_tag = create_tag(_api_key, _workspace_id, tag_name)
|
563
|
+
unless _tag
|
564
|
+
return false
|
565
|
+
end
|
566
|
+
_tags[_tag['name']] = _tag
|
567
|
+
end
|
568
|
+
|
569
|
+
_tags
|
570
|
+
end
|
571
|
+
|
572
|
+
###########################################################
|
573
|
+
# Creates a tag in the given workspace
|
574
|
+
def create_tag(_api_key, _workspace_id, tag)
|
575
|
+
debug_print "#{ self.class } : #{ __method__ }\n"
|
576
|
+
|
577
|
+
opts = {
|
578
|
+
:url => "#{ @end_point }/workspaces/#{ _workspace_id }/tags",
|
579
|
+
:ssl => true,
|
580
|
+
:method => "POST",
|
581
|
+
:basic_auth => [_api_key, ""],
|
582
|
+
:data => [{'name'=>tag}],
|
583
|
+
:verbose => false
|
584
|
+
}
|
585
|
+
|
586
|
+
_json, _resp = Watson::Remote.http_call(opts)
|
587
|
+
|
588
|
+
unless _resp.code == "201"
|
589
|
+
@formatter.print_status "x", RED
|
590
|
+
print BOLD + "Unable to access Asana API, key may be invalid\n" + RESET
|
591
|
+
print " Consider running --remote (-r) option to regenerate key\n\n"
|
592
|
+
print " Status: #{ _resp.code } - #{ _resp.message }\n"
|
593
|
+
return false
|
594
|
+
end
|
595
|
+
|
596
|
+
_data = _json['data']
|
597
|
+
|
598
|
+
debug_print "Created tag '#{ tag }' with id #{ _data['id'] } \n"
|
599
|
+
|
600
|
+
_data
|
601
|
+
|
602
|
+
end
|
603
|
+
|
604
|
+
###########################################################
|
605
|
+
# Creates a tag in the given workspace or else if already
|
606
|
+
# exists, returns that tag
|
607
|
+
def create_or_get_tag(_api_key, _workspace_id, tag_name)
|
608
|
+
debug_print "#{ self.class } : #{ __method__ }\n"
|
609
|
+
_tags = create_and_get_tags(_api_key, _workspace_id, [tag_name])
|
610
|
+
_tags ? _tags[tag_name] : false
|
611
|
+
end
|
612
|
+
|
613
|
+
end
|
614
|
+
|
615
|
+
end
|
616
|
+
|
617
|
+
end
|
618
|
+
|
619
|
+
end
|