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 ADDED
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ *.iml
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in jira-sync.gemspec
4
+ gemspec
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
@@ -0,0 +1,2 @@
1
+ # jira-sync
2
+ A utility to synchronise jira projects to the local file system
data/Rakefile ADDED
@@ -0,0 +1,5 @@
1
+ task :default => :build
2
+
3
+ task :build do
4
+ system "gem build jirasync.gemspec"
5
+ end
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,9 @@
1
+ require 'jirasync/jira_client'
2
+ require 'jirasync/local_repository'
3
+ require 'jirasync/syncer'
4
+ require 'jirasync/version'
5
+
6
+ module JiraSync
7
+
8
+ end
9
+
@@ -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
@@ -0,0 +1,3 @@
1
+ module JiraSync
2
+ VERSION = "0.0.2"
3
+ 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: []