jirasync 0.2
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/.gitignore +18 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +2 -0
- data/Rakefile +5 -0
- data/bin/jira-sync +64 -0
- data/jira-sync.gemspec +19 -0
- data/jirasync.gemspec +27 -0
- data/lib/jirasync.rb +9 -0
- data/lib/jirasync/jira_client.rb +67 -0
- data/lib/jirasync/local_repository.rb +39 -0
- data/lib/jirasync/syncer.rb +70 -0
- data/lib/jirasync/version.rb +3 -0
- metadata +109 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2015 programmiersportgruppe
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
22
|
+
|
data/README.md
ADDED
data/Rakefile
ADDED
data/bin/jira-sync
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'io/console'
|
5
|
+
require 'trollop'
|
6
|
+
|
7
|
+
|
8
|
+
require 'jirasync'
|
9
|
+
|
10
|
+
|
11
|
+
def prompt_for_password
|
12
|
+
print("Please enter password: ")
|
13
|
+
pw = STDIN.noecho(&:gets).chomp
|
14
|
+
puts
|
15
|
+
pw
|
16
|
+
end
|
17
|
+
|
18
|
+
opts = Trollop::options do
|
19
|
+
banner <<-EOS
|
20
|
+
Utility to sync a jira project to the local file system.
|
21
|
+
|
22
|
+
Usage:
|
23
|
+
jira-sync [options] [command]
|
24
|
+
where command is one of the following
|
25
|
+
|
26
|
+
fetch-all: fetches all tickets from the given project
|
27
|
+
update: fetches all tickets that have been updated/
|
28
|
+
added since the last run
|
29
|
+
|
30
|
+
where [options] are:
|
31
|
+
EOS
|
32
|
+
opt :baseurl, "Jira base url", :type => :string
|
33
|
+
opt :project, "Project key", :type => :string
|
34
|
+
opt :user, "User, defaults to the current system user", :type => :string
|
35
|
+
opt :password, "Password, if not specified there, will be an interactive prompt", :type => :string
|
36
|
+
opt :target, "Target directory, defaults to the project key", :type => :string
|
37
|
+
end
|
38
|
+
|
39
|
+
Trollop::die :baseurl, "must be speficied" if !opts[:baseurl]
|
40
|
+
Trollop::die :project, "must be speficied" if !opts[:project]
|
41
|
+
|
42
|
+
|
43
|
+
user = opts[:user] || ENV['USER']
|
44
|
+
pw = opts[:password] || prompt_for_password
|
45
|
+
target = opts[:target] || opts[:project].chomp
|
46
|
+
|
47
|
+
command = ARGV[0]
|
48
|
+
|
49
|
+
if !["fetch", "update"].include?(command)
|
50
|
+
STDERR.puts("'#{command}' is not a valid command")
|
51
|
+
exit 1
|
52
|
+
end
|
53
|
+
|
54
|
+
client = JiraSync::JiraClient.new(opts[:baseurl], user, pw)
|
55
|
+
repo = JiraSync::LocalIssueRepository.new(target)
|
56
|
+
syncer = JiraSync::Syncer.new(client, repo, opts[:project].chomp)
|
57
|
+
|
58
|
+
if command == "fetch"
|
59
|
+
syncer.fetch_all
|
60
|
+
end
|
61
|
+
|
62
|
+
if command == "update"
|
63
|
+
syncer.update
|
64
|
+
end
|
data/jira-sync.gemspec
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'jira-sync/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = "jira-sync"
|
8
|
+
gem.version = Jira::Sync::VERSION
|
9
|
+
gem.authors = ["Felix Leipold"]
|
10
|
+
gem.email = ["felix.leipold@gmail.com"]
|
11
|
+
gem.description = %q{TODO: Write a gem description}
|
12
|
+
gem.summary = %q{TODO: Write a gem summary}
|
13
|
+
gem.homepage = ""
|
14
|
+
|
15
|
+
gem.files = `git ls-files`.split($/)
|
16
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
17
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
18
|
+
gem.require_paths = ["lib"]
|
19
|
+
end
|
data/jirasync.gemspec
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = 'jirasync'
|
3
|
+
s.summary = 'jirasync synchronises jira projects to the local file system'
|
4
|
+
s.description = 'jirasync synchronises tickets from a jira project to the local
|
5
|
+
file system. It supports a complete fetch operation as well as
|
6
|
+
an incremental update.
|
7
|
+
|
8
|
+
Each ticket is stored in a simple, pretty printed JSON file.'
|
9
|
+
s.version = '0.2'
|
10
|
+
s.platform = Gem::Platform::RUBY
|
11
|
+
|
12
|
+
s.files = ['bin/jira-sync']
|
13
|
+
|
14
|
+
s.bindir = 'bin'
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
18
|
+
|
19
|
+
s.author = 'Felix Leipold'
|
20
|
+
s.email = ''
|
21
|
+
s.homepage = 'https://github.com/programmiersportgruppe/jira-sync'
|
22
|
+
|
23
|
+
|
24
|
+
s.add_dependency('trollop')
|
25
|
+
s.add_dependency('httparty')
|
26
|
+
s.add_dependency('parallel')
|
27
|
+
end
|
data/lib/jirasync.rb
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
module JiraSync
|
2
|
+
|
3
|
+
require 'httparty'
|
4
|
+
require 'uri'
|
5
|
+
require 'json'
|
6
|
+
|
7
|
+
|
8
|
+
class JiraClient
|
9
|
+
|
10
|
+
def initialize(baseurl, username, password)
|
11
|
+
@username = username
|
12
|
+
@password = password
|
13
|
+
@baseurl = baseurl
|
14
|
+
end
|
15
|
+
|
16
|
+
def get(jira_id)
|
17
|
+
url = "#{@baseurl}/rest/api/latest/issue/#{jira_id}"
|
18
|
+
auth = {:username => @username, :password => @password}
|
19
|
+
response = HTTParty.get url, {:basic_auth => auth}
|
20
|
+
if response.code == 200
|
21
|
+
response.parsed_response
|
22
|
+
else
|
23
|
+
raise "no issue found for #{jira_id}. response code was #{response.code}, url was #{url}"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def latest_issue_for_project(project_id)
|
28
|
+
url = "#{@baseurl}/rest/api/2/search?"
|
29
|
+
auth = {:username => @username, :password => @password}
|
30
|
+
|
31
|
+
response = HTTParty.get url, {:basic_auth => auth, :query => {:jql => 'project="' + project_id + '" order by created', fields: 'summary,updated', maxResults: '1'}}
|
32
|
+
if response.code == 200
|
33
|
+
response.parsed_response
|
34
|
+
else
|
35
|
+
raise "no issue found for #{project_id}. response code was #{response.code}, url was #{url}"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def changed_since(project_id, date)
|
40
|
+
url = "#{@baseurl}/rest/api/2/search?"
|
41
|
+
auth = {:username => @username, :password => @password}
|
42
|
+
jql = 'project = "' + project_id + '" AND updated > ' + (date.to_time.to_i * 1000).to_s
|
43
|
+
# "' + date.to_s + '"'
|
44
|
+
response = HTTParty.get url, {:basic_auth => auth, :query => {:jql => jql, fields: 'summary,updated', maxResults: '1000'}}
|
45
|
+
if response.code == 200
|
46
|
+
response.parsed_response
|
47
|
+
else
|
48
|
+
raise "no issue found for #{project_id}. response code was #{response.code}, url was #{url}"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def project_info(project_id)
|
53
|
+
url = "#{@baseurl}/rest/api/2/project/#{project_id}"
|
54
|
+
auth = {:username => @username, :password => @password}
|
55
|
+
response = HTTParty.get url, {:basic_auth => auth, :query => {:jql => 'project="' + project_id + '"', fields: 'summary,updated', maxResults: '50'}}
|
56
|
+
if response.code == 200
|
57
|
+
response.parse_response
|
58
|
+
else
|
59
|
+
raise "no issue found for #{project_id}. response code was #{response.code}, url was #{url}"
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
|
67
|
+
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module JiraSync
|
2
|
+
require 'fileutils'
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
|
6
|
+
class LocalIssueRepository
|
7
|
+
|
8
|
+
def initialize(path)
|
9
|
+
@path = path
|
10
|
+
FileUtils::mkdir_p @path
|
11
|
+
end
|
12
|
+
|
13
|
+
def save(issue)
|
14
|
+
json = JSON.pretty_generate(issue)
|
15
|
+
file_path = "#{@path}/#{issue['key']}.json"
|
16
|
+
File.write(file_path, json)
|
17
|
+
|
18
|
+
updateTime = DateTime.parse(issue['fields']['updated'])
|
19
|
+
|
20
|
+
File.utime(DateTime.now.to_time, updateTime.to_time, file_path)
|
21
|
+
end
|
22
|
+
|
23
|
+
def save_state(state)
|
24
|
+
json = JSON.pretty_generate(state)
|
25
|
+
file_path = "#{@path}/sync_state.json"
|
26
|
+
File.write(file_path, json)
|
27
|
+
end
|
28
|
+
|
29
|
+
def load_state()
|
30
|
+
file_path = "#{@path}/sync_state.json"
|
31
|
+
if (!File.exists?(file_path))
|
32
|
+
raise ("File '#{file_path}' with previous sync state could not be found\n" +
|
33
|
+
"please run an initial fetch first")
|
34
|
+
end
|
35
|
+
s = IO.read(file_path)
|
36
|
+
JSON.parse(s)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module JiraSync
|
2
|
+
require 'parallel'
|
3
|
+
require 'json'
|
4
|
+
require 'date'
|
5
|
+
|
6
|
+
|
7
|
+
class Syncer
|
8
|
+
|
9
|
+
def initialize(client, repo, project_key)
|
10
|
+
@client = client
|
11
|
+
@project_key = project_key
|
12
|
+
latest_issue = @client.latest_issue_for_project(@project_key)['issues'][0]
|
13
|
+
@latest_issue_key = latest_issue['key'].split("-")[1].to_i
|
14
|
+
@repo = repo
|
15
|
+
end
|
16
|
+
|
17
|
+
|
18
|
+
# Fetches a number of tickets in parallel
|
19
|
+
# prints progress information to stderr
|
20
|
+
# and returns a list of tickets that
|
21
|
+
# couldn't be fetched.
|
22
|
+
def fetch(keys)
|
23
|
+
keys_with_errors = []
|
24
|
+
Parallel.each_with_index(keys, :in_threads => 64) do |key, index|
|
25
|
+
STDERR.puts(key) if ((index % 100) == 0)
|
26
|
+
begin
|
27
|
+
issue = @client.get(key)
|
28
|
+
issue_project_key = issue['fields']['project']['key']
|
29
|
+
if (issue_project_key == @project_key)
|
30
|
+
@repo.save(issue)
|
31
|
+
else
|
32
|
+
STDERR.puts("Skipping ticket #{key} which has moved to #{issue_project_key}.")
|
33
|
+
end
|
34
|
+
rescue => e
|
35
|
+
STDERR.puts(e.to_s)
|
36
|
+
keys_with_errors.push(key)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
keys_with_errors.sort
|
40
|
+
end
|
41
|
+
|
42
|
+
def fetch_all
|
43
|
+
start_time = DateTime.now
|
44
|
+
|
45
|
+
keys = (1..@latest_issue_key).map { |key_number| @project_key + "-" + key_number.to_s }
|
46
|
+
keys_with_errors = fetch(keys)
|
47
|
+
|
48
|
+
@repo.save_state({"time" => start_time, "errors" => keys_with_errors})
|
49
|
+
end
|
50
|
+
|
51
|
+
def update()
|
52
|
+
state = @repo.load_state()
|
53
|
+
start_time = DateTime.now
|
54
|
+
since = DateTime.parse(state['time']).new_offset(0)
|
55
|
+
STDERR.puts("Fetching issues that have changes since #{since.to_s}")
|
56
|
+
issues = @client.changed_since(@project_key, since)['issues'].map { |issue| issue['key'] }
|
57
|
+
STDERR.puts("Updated Issues")
|
58
|
+
STDERR.puts(issues.join(", "))
|
59
|
+
STDERR.puts("Issues with earlier errors")
|
60
|
+
STDERR.puts(state['errors'].join(", "))
|
61
|
+
keys_with_errors = fetch(issues + state['errors'])
|
62
|
+
@repo.save_state({"time" => start_time, "errors" => keys_with_errors})
|
63
|
+
end
|
64
|
+
|
65
|
+
def dump()
|
66
|
+
puts(@latest_issue_key)
|
67
|
+
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
metadata
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: jirasync
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: '0.2'
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Felix Leipold
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2015-03-19 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: trollop
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: httparty
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: parallel
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
description: ! "jirasync synchronises tickets from a jira project to the local\n file
|
63
|
+
system. It supports a complete fetch operation as well as\n an
|
64
|
+
incremental update.\n\n Each ticket is stored in a simple, pretty
|
65
|
+
printed JSON file."
|
66
|
+
email: ''
|
67
|
+
executables:
|
68
|
+
- jira-sync
|
69
|
+
extensions: []
|
70
|
+
extra_rdoc_files: []
|
71
|
+
files:
|
72
|
+
- .gitignore
|
73
|
+
- Gemfile
|
74
|
+
- LICENSE
|
75
|
+
- README.md
|
76
|
+
- Rakefile
|
77
|
+
- bin/jira-sync
|
78
|
+
- jira-sync.gemspec
|
79
|
+
- jirasync.gemspec
|
80
|
+
- lib/jirasync.rb
|
81
|
+
- lib/jirasync/jira_client.rb
|
82
|
+
- lib/jirasync/local_repository.rb
|
83
|
+
- lib/jirasync/syncer.rb
|
84
|
+
- lib/jirasync/version.rb
|
85
|
+
homepage: https://github.com/programmiersportgruppe/jira-sync
|
86
|
+
licenses: []
|
87
|
+
post_install_message:
|
88
|
+
rdoc_options: []
|
89
|
+
require_paths:
|
90
|
+
- lib
|
91
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
92
|
+
none: false
|
93
|
+
requirements:
|
94
|
+
- - ! '>='
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
98
|
+
none: false
|
99
|
+
requirements:
|
100
|
+
- - ! '>='
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '0'
|
103
|
+
requirements: []
|
104
|
+
rubyforge_project:
|
105
|
+
rubygems_version: 1.8.24
|
106
|
+
signing_key:
|
107
|
+
specification_version: 3
|
108
|
+
summary: jirasync synchronises jira projects to the local file system
|
109
|
+
test_files: []
|