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 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 a the project `MYPROJ` from the server at
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/json \
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/json \
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
- ~~~ {.bash}
68
+ ~~~ {.sh}
65
69
 
66
- jira-format-issues \
67
- --source issues/MYPROJECT/json \
68
- --target issues/MYPROJECT/markdown
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.co/browse/MYPROJ-1): Build a working System
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 paragraph
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 make information available offline.
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
@@ -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['content']
94
- "* [#{filename}](#{content_url})"
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
 
@@ -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
@@ -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
@@ -1,3 +1,3 @@
1
1
  module JiraSync
2
- VERSION = "0.4.3"
2
+ VERSION = "0.4.4"
3
3
  end
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.3
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-02 00:00:00.000000000 Z
12
+ date: 2015-04-21 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: trollop