jirasync 0.4.3 → 0.4.4
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +21 -16
- data/bin/jira-format-issues +33 -6
- data/bin/jira-sync +5 -1
- data/lib/jirasync/jira_client.rb +16 -0
- data/lib/jirasync/local_repository.rb +7 -1
- data/lib/jirasync/syncer.rb +8 -1
- data/lib/jirasync/version.rb +1 -1
- metadata +2 -2
data/README.md
CHANGED
@@ -11,21 +11,19 @@ A suite of utilities to synchronise JIRA projects to the local file system
|
|
11
11
|
|
12
12
|
### Initial Fetch
|
13
13
|
|
14
|
-
The following command will start synchronising
|
14
|
+
The following command will start synchronising the project `MYPROJ` from the server at
|
15
15
|
`https://jira.myorganisation.com`. The issues from that project will be written to the
|
16
16
|
`issues/MYPROJ` folder:
|
17
17
|
|
18
|
-
|
19
18
|
jira-sync \
|
20
19
|
--baseurl https://jira.myorganisation.com \
|
21
20
|
--project MYPROJ \
|
22
21
|
--user jira_user \
|
23
22
|
--password jira_password \
|
24
|
-
--target issues/MYPROJ/
|
23
|
+
--target issues/MYPROJ/raw \
|
25
24
|
fetch
|
26
25
|
|
27
|
-
|
28
|
-
When this passes successfully the `issues/MYPROJ/json` directory will contain the following structure:
|
26
|
+
When this passes successfully the `issues/MYPROJ/raw` directory will contain the following structure:
|
29
27
|
|
30
28
|
MYPROJ-1.json
|
31
29
|
MYPROJ-2.json
|
@@ -51,39 +49,43 @@ jira-sync \
|
|
51
49
|
--project MYPROJ \
|
52
50
|
--user jira_user \
|
53
51
|
--password jira_password \
|
54
|
-
--target issues/MYPROJ/
|
52
|
+
--target issues/MYPROJ/raw \
|
55
53
|
update
|
56
54
|
|
57
55
|
~~~
|
58
56
|
|
57
|
+
### Fetching and Storing Attachments
|
58
|
+
|
59
|
+
|
60
|
+
Passing in the `--store-attachments` option leads to attachments being fetched and stored in the local filesystem.
|
61
|
+
They will we be stored in the `attachments/` sub-directory of the target directory.
|
62
|
+
|
59
63
|
### Formatting Issues
|
60
64
|
|
61
65
|
While JSON files are very handy to use in code, they are not very readable. The `jira-format-issues` command
|
62
66
|
formats JSON issues to markdown. It is invoked as follows:
|
63
67
|
|
64
|
-
~~~ {.
|
68
|
+
~~~ {.sh}
|
65
69
|
|
66
|
-
|
67
|
-
|
68
|
-
|
70
|
+
jira-format-issues \
|
71
|
+
--source issues/MYPROJ/raw\
|
72
|
+
--target issues/MYPROJ/markdown
|
69
73
|
|
70
74
|
~~~
|
71
75
|
|
72
76
|
This will create the following structure in the `issues/MYPROJ/markdown`:
|
73
77
|
|
74
|
-
|
75
78
|
MYPROJ-1.md
|
76
79
|
MYPROJ-2.md
|
77
80
|
MYPROJ-3.md
|
78
81
|
MYPROJ-4.md
|
79
82
|
…
|
80
83
|
|
81
|
-
|
82
84
|
The individual files look like this:
|
83
85
|
|
84
86
|
~~~ {.md}
|
85
|
-
[MYPROJ-1](https://jira.myorganisation.
|
86
|
-
|
87
|
+
[MYPROJ-1](https://jira.myorganisation.com/browse/MYPROJ-1): Build a working System
|
88
|
+
===================================================================================
|
87
89
|
|
88
90
|
Type
|
89
91
|
: Story
|
@@ -123,7 +125,7 @@ These files can be easily searched by ensuring they get indexed by a desktop sea
|
|
123
125
|
[spotlight](https://gist.github.com/gereon/3150445) on the Mac.
|
124
126
|
|
125
127
|
There is also the possibility to render custom jira fields by supplying a *custom data* file, which declares *simple
|
126
|
-
data* fields which are rendered as definitions at the top of the ticket and *sections* that are rendered as
|
128
|
+
data* fields which are rendered as definitions at the top of the ticket and *sections* that are rendered as one or more paragraphs
|
127
129
|
with a heading. Here is an example file, `custom-data.json`:
|
128
130
|
|
129
131
|
~~~ {.json}
|
@@ -152,12 +154,15 @@ jira-format-issues \
|
|
152
154
|
|
153
155
|
## Motivation
|
154
156
|
|
155
|
-
Having a local, unix-friendly copy to avoid JIRA performance issues and
|
157
|
+
Having a local, unix-friendly copy of all tickets to avoid JIRA performance issues and
|
158
|
+
make information available offline.
|
156
159
|
|
157
160
|
## Potential Future Work
|
158
161
|
|
159
162
|
- [X] Make progress bar work
|
160
163
|
- [X] Make output less noisy
|
164
|
+
- [X] Download attachments
|
165
|
+
- [ ] Produce tabular output
|
161
166
|
- [ ] Deal with authentication problems explicitly
|
162
167
|
- [ ] Remove tickets that have been moved to a different project
|
163
168
|
- [ ] Use OAuth authentication
|
data/bin/jira-format-issues
CHANGED
@@ -9,8 +9,10 @@ require 'uri'
|
|
9
9
|
|
10
10
|
class IssueRenderer
|
11
11
|
|
12
|
-
def initialize(custom_data)
|
12
|
+
def initialize(custom_data, source_attachments)
|
13
13
|
@custom_data = custom_data
|
14
|
+
@source_attachments = source_attachments
|
15
|
+
@found_attachments = []
|
14
16
|
end
|
15
17
|
|
16
18
|
def h1(s)
|
@@ -84,17 +86,36 @@ class IssueRenderer
|
|
84
86
|
}.join("\n" * 2)
|
85
87
|
end
|
86
88
|
|
87
|
-
def render_attachments(attachments)
|
89
|
+
def render_attachments(attachments, issue)
|
88
90
|
return "" if attachments.length == 0
|
89
91
|
|
90
92
|
h2("Attachments") + "\n" * 2 +
|
91
93
|
attachments.map{|attachment|
|
92
94
|
filename = attachment['filename']
|
93
|
-
content_url = attachment
|
94
|
-
|
95
|
+
content_url = find_attachment(attachment, issue)
|
96
|
+
extension = File.extname(filename)
|
97
|
+
STDERR.puts("extension: " + extension)
|
98
|
+
image_marker = [".png", ".jpg"].include?(extension) ? "!": ""
|
99
|
+
|
100
|
+
STDERR.puts("marker: " + image_marker)
|
101
|
+
"* #{image_marker}[#{filename}](#{content_url})"
|
95
102
|
}.join("\n" * 2)
|
96
103
|
end
|
97
104
|
|
105
|
+
def find_attachment(attachment, issue)
|
106
|
+
filename = "#{issue['key']}-#{attachment['id']}-#{attachment['filename'].gsub(" ", "_")}"
|
107
|
+
if File.file?("#{@source_attachments}/#{filename}")
|
108
|
+
@found_attachments.push(filename)
|
109
|
+
"attachments/#{filename}"
|
110
|
+
else
|
111
|
+
attachment['content']
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def found_attachments
|
116
|
+
@found_attachments
|
117
|
+
end
|
118
|
+
|
98
119
|
def render(issue)
|
99
120
|
|
100
121
|
|
@@ -140,7 +161,7 @@ class IssueRenderer
|
|
140
161
|
sections.select{|kv| kv[1]}.map{|kv| optional_section(kv[0], kv[1])}.join("\n\n") + "\n\n" +
|
141
162
|
render_links(links, project_key) + "\n\n" +
|
142
163
|
render_comments(comments) + "\n\n" +
|
143
|
-
render_attachments(attachments)
|
164
|
+
render_attachments(attachments, issue)
|
144
165
|
end
|
145
166
|
end
|
146
167
|
|
@@ -164,6 +185,7 @@ Trollop::die :target, "must be speficied" if !opts[:target]
|
|
164
185
|
source = opts[:source]
|
165
186
|
target = opts[:target]
|
166
187
|
|
188
|
+
|
167
189
|
FileUtils::mkdir_p target
|
168
190
|
|
169
191
|
|
@@ -176,7 +198,7 @@ else
|
|
176
198
|
custom_data = {'simple_fields' => [], 'sections' => []}
|
177
199
|
end
|
178
200
|
|
179
|
-
renderer = IssueRenderer.new(custom_data)
|
201
|
+
renderer = IssueRenderer.new(custom_data, "#{source}/attachments")
|
180
202
|
|
181
203
|
input_files.each do |file_path|
|
182
204
|
begin
|
@@ -193,3 +215,8 @@ input_files.each do |file_path|
|
|
193
215
|
STDERR.puts(e.to_s)
|
194
216
|
end
|
195
217
|
end
|
218
|
+
|
219
|
+
FileUtils::mkdir_p "#{target}/attachments" unless renderer.found_attachments.empty?
|
220
|
+
renderer.found_attachments.each do |filename|
|
221
|
+
FileUtils.cp("#{source}/attachments/#{filename}", "#{target}/attachments/#{filename}")
|
222
|
+
end
|
data/bin/jira-sync
CHANGED
@@ -32,6 +32,7 @@ EOS
|
|
32
32
|
opt :user, "User, defaults to the current system user", :type => :string
|
33
33
|
opt :password, "Password, if not specified there, will be an interactive prompt", :type => :string
|
34
34
|
opt :target, "Target directory, defaults to the project key", :type => :string
|
35
|
+
opt :store_attachments, "Fetch and store attachments"
|
35
36
|
end
|
36
37
|
|
37
38
|
Trollop::die :baseurl, "must be speficied" if !opts[:baseurl]
|
@@ -42,6 +43,7 @@ user = opts[:user] || ENV['USER']
|
|
42
43
|
pw = opts[:password] || prompt_for_password
|
43
44
|
target = opts[:target] || opts[:project].chomp
|
44
45
|
project = opts[:project]
|
46
|
+
store_attachments = opts[:store_attachments]
|
45
47
|
|
46
48
|
command = ARGV[0]
|
47
49
|
|
@@ -52,7 +54,7 @@ end
|
|
52
54
|
|
53
55
|
client = JiraSync::JiraClient.new(opts[:baseurl], user, pw)
|
54
56
|
repo = JiraSync::LocalIssueRepository.new(target)
|
55
|
-
syncer = JiraSync::Syncer.new(client, repo, project)
|
57
|
+
syncer = JiraSync::Syncer.new(client, repo, project, store_attachments)
|
56
58
|
|
57
59
|
if command == "fetch"
|
58
60
|
if (repo.state_exists?)
|
@@ -62,6 +64,7 @@ jira-sync \
|
|
62
64
|
--user #{user} \\
|
63
65
|
--project #{project} \\
|
64
66
|
--target #{target} \\
|
67
|
+
--store-attachments \\
|
65
68
|
update
|
66
69
|
END
|
67
70
|
|
@@ -83,6 +86,7 @@ jira-sync \
|
|
83
86
|
--user #{user} \\
|
84
87
|
--project #{project} \\
|
85
88
|
--target #{target} \\
|
89
|
+
--store-attachments \\
|
86
90
|
fetch
|
87
91
|
END
|
88
92
|
|
data/lib/jirasync/jira_client.rb
CHANGED
@@ -3,6 +3,7 @@ module JiraSync
|
|
3
3
|
require 'httparty'
|
4
4
|
require 'uri'
|
5
5
|
require 'json'
|
6
|
+
require 'parallel'
|
6
7
|
|
7
8
|
|
8
9
|
class FetchError < StandardError
|
@@ -42,6 +43,21 @@ module JiraSync
|
|
42
43
|
end
|
43
44
|
end
|
44
45
|
|
46
|
+
def attachments_for_issue(issue)
|
47
|
+
attachments = []
|
48
|
+
Parallel.map(issue['fields']['attachment'], :in_threads => 64) do |attachment|
|
49
|
+
auth = {:username => @username, :password => @password}
|
50
|
+
response = HTTParty.get attachment['content'], {:basic_auth => auth, :timeout => @timeout}
|
51
|
+
if response.code == 200
|
52
|
+
attachments.push({:data => response.body, :attachment => attachment, :issue => issue})
|
53
|
+
else
|
54
|
+
raise FetchError.new(response.code, url)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
attachments
|
59
|
+
end
|
60
|
+
|
45
61
|
def latest_issue_for_project(project_id)
|
46
62
|
url = "#{@baseurl}/rest/api/2/search?"
|
47
63
|
auth = {:username => @username, :password => @password}
|
@@ -40,5 +40,11 @@ module JiraSync
|
|
40
40
|
s = IO.read(file_path)
|
41
41
|
JSON.parse(s)
|
42
42
|
end
|
43
|
+
|
44
|
+
def save_attachment(issue, attachment, data)
|
45
|
+
FileUtils::mkdir_p("#{@path}/attachments")
|
46
|
+
file_path = "#{@path}/attachments/#{issue['key']}-#{attachment['id']}-#{attachment['filename'].gsub(" ", "_")}"
|
47
|
+
File.write(file_path, data, {:mode => 'wb'})
|
48
|
+
end
|
43
49
|
end
|
44
|
-
end
|
50
|
+
end
|
data/lib/jirasync/syncer.rb
CHANGED
@@ -5,12 +5,13 @@ module JiraSync
|
|
5
5
|
|
6
6
|
class Syncer
|
7
7
|
|
8
|
-
def initialize(client, repo, project_key)
|
8
|
+
def initialize(client, repo, project_key, store_attachments)
|
9
9
|
@client = client
|
10
10
|
@project_key = project_key
|
11
11
|
latest_issue = @client.latest_issue_for_project(@project_key)['issues'][0]
|
12
12
|
@latest_issue_key = latest_issue['key'].split("-")[1].to_i
|
13
13
|
@repo = repo
|
14
|
+
@store_attachments = store_attachments
|
14
15
|
end
|
15
16
|
|
16
17
|
# Fetches a number of tickets in parallel
|
@@ -26,6 +27,12 @@ module JiraSync
|
|
26
27
|
issue_project_key = issue['fields']['project']['key']
|
27
28
|
if (issue_project_key == @project_key)
|
28
29
|
@repo.save(issue)
|
30
|
+
if @store_attachments
|
31
|
+
attachments = @client.attachments_for_issue(issue)
|
32
|
+
attachments.each do |attachment|
|
33
|
+
@repo.save_attachment(attachment[:issue], attachment[:attachment], attachment[:data])
|
34
|
+
end
|
35
|
+
end
|
29
36
|
else
|
30
37
|
tickets_moved.push(issue_project_key)
|
31
38
|
end
|
data/lib/jirasync/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: jirasync
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.4.
|
4
|
+
version: 0.4.4
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2015-04-
|
12
|
+
date: 2015-04-21 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: trollop
|