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 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