jirasync 0.4.3 → 0.4.4
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.
- 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
|