sync_issues 0.5.0 → 0.6.0
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.
- checksums.yaml +4 -4
- data/README.md +33 -3
- data/lib/sync_issues/command.rb +13 -0
- data/lib/sync_issues/comparison.rb +21 -6
- data/lib/sync_issues/github.rb +13 -4
- data/lib/sync_issues/issue.rb +18 -4
- data/lib/sync_issues/label_sync.rb +93 -0
- data/lib/sync_issues/synchronizer.rb +14 -7
- data/lib/sync_issues/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 44337f4aa3175ff1ba7e15f4583309ce034a251a
|
4
|
+
data.tar.gz: 29027fe887bb51d2412cf2fc9c4bba32d482b7db
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9049e78d7fc5c02cfe7783d2f7676d3049218d627adf02dea2696da59b93f332277936d879b5b84cd78b614c54485a28ed02060ebdca11e33dd0923a104396d7
|
7
|
+
data.tar.gz: 120b8502dcb8ec42c1679ad735e0f80f2b2964ed4a491120ed7fba6cd3474e89b7fe006e618e228d28cb22496b5ea148a9472c3e5a725bfdbd21d4f23c3ccb71
|
data/README.md
CHANGED
@@ -65,6 +65,36 @@ The frontmatter of an issue file can contain the following attributes:
|
|
65
65
|
* __assignee__: (optional) Assign or reassign the issue to the github username
|
66
66
|
specified. Existing assignee will not be removed on sync if the field is not
|
67
67
|
provided.
|
68
|
-
*
|
69
|
-
|
70
|
-
|
68
|
+
* __labels__: (optional) When provided and the issue does not have any labels
|
69
|
+
this list of labels will be added to the issue. Labels will be dynamically
|
70
|
+
created with the default grey color if they don't already exist on the
|
71
|
+
repository. If you desire to reset the labels for each issue, run
|
72
|
+
``sync_issues`` with the ``--reset-labels`` flag.
|
73
|
+
|
74
|
+
## Synchronizing Labels
|
75
|
+
|
76
|
+
A path to a yaml file can be provided to synchronize labels on a
|
77
|
+
repository. This path is provided like so:
|
78
|
+
|
79
|
+
sync_issues --labels yaml_file.yml task_directory org/repo
|
80
|
+
|
81
|
+
The yaml file can contain two attributes:
|
82
|
+
|
83
|
+
* __keep_existing__: (optional) When ``false`` (default ``true``) all labels
|
84
|
+
that aren't in ``labels`` will be deleted. Set to ``false`` and provide no
|
85
|
+
labels to clear all labels.
|
86
|
+
* __labels__: (optional) A mapping of label names to their 6-character
|
87
|
+
hexidecimal color code without a `#` prefix. Color codes that contain only
|
88
|
+
numeric digits, e.g., `000000`, must be contained within quotes.
|
89
|
+
|
90
|
+
### Example labels yaml file
|
91
|
+
|
92
|
+
```yaml
|
93
|
+
keep_existing: false
|
94
|
+
labels:
|
95
|
+
in progress: bfe5bf
|
96
|
+
merged: "009800"
|
97
|
+
unstarted: fef2c0
|
98
|
+
waiting on developer: f7c6c7
|
99
|
+
waiting on reviewer: fad8c7
|
100
|
+
```
|
data/lib/sync_issues/command.rb
CHANGED
@@ -18,7 +18,10 @@ module SyncIssues
|
|
18
18
|
Options:
|
19
19
|
-h --help Output this help information.
|
20
20
|
-u --update Only update existing issues.
|
21
|
+
--labels FILE A yaml file listing labels that are to be available.
|
21
22
|
--no-assignees Do not synchronize assignees.
|
23
|
+
--no-labels Do not synchronize labels.
|
24
|
+
--reset-labels Reset labels on a per-issue basis.
|
22
25
|
--version Output the sync_issues version (#{VERSION}).
|
23
26
|
DOC
|
24
27
|
|
@@ -46,9 +49,19 @@ module SyncIssues
|
|
46
49
|
|
47
50
|
def handle_args(options)
|
48
51
|
SyncIssues.synchronizer(options['DIRECTORY'], options['REPOSITORY'],
|
52
|
+
label_yaml: read_file(options['--labels']),
|
53
|
+
reset_labels: options['--reset-labels'],
|
49
54
|
sync_assignees: !options['--no-assignees'],
|
55
|
+
sync_labels: !options['--no-labels'],
|
50
56
|
update_only: options['--update']).run
|
51
57
|
@exit_status
|
52
58
|
end
|
59
|
+
|
60
|
+
def read_file(filename)
|
61
|
+
return nil if filename.nil?
|
62
|
+
File.read(filename)
|
63
|
+
rescue Errno::ENOENT
|
64
|
+
raise Error, "not found: #{filename}"
|
65
|
+
end
|
53
66
|
end
|
54
67
|
end
|
@@ -3,15 +3,16 @@ require_relative 'error'
|
|
3
3
|
module SyncIssues
|
4
4
|
# Comparison represents differences between Issues (local and GitHub)
|
5
5
|
class Comparison
|
6
|
-
attr_reader :assignee, :changed, :content, :title
|
6
|
+
attr_reader :assignee, :changed, :content, :labels, :title
|
7
7
|
|
8
|
-
def initialize(issue, github_issue,
|
8
|
+
def initialize(issue, github_issue, reset_labels: false,
|
9
|
+
sync_assignee: true, sync_labels: true)
|
9
10
|
@changed = []
|
10
11
|
@assignee = github_issue.assignee && github_issue.assignee.login
|
11
12
|
@content = github_issue.body
|
12
|
-
@
|
13
|
+
@labels = github_issue.labels.map { |label| label[:name] }
|
13
14
|
@title = github_issue.title
|
14
|
-
compare(issue)
|
15
|
+
compare(issue, reset_labels, sync_assignee, sync_labels)
|
15
16
|
end
|
16
17
|
|
17
18
|
def changed?
|
@@ -20,11 +21,15 @@ module SyncIssues
|
|
20
21
|
|
21
22
|
private
|
22
23
|
|
23
|
-
def compare(issue)
|
24
|
-
if
|
24
|
+
def compare(issue, reset_labels, sync_assignee, sync_labels)
|
25
|
+
if sync_assignee && issue.assignee != @assignee
|
25
26
|
@changed << 'assignee'
|
26
27
|
@assignee = issue.assignee
|
27
28
|
end
|
29
|
+
if sync_labels && update_label?(issue, reset_labels)
|
30
|
+
@changed << 'labels'
|
31
|
+
@labels = issue.labels
|
32
|
+
end
|
28
33
|
unless issue.new_title.nil?
|
29
34
|
@changed << 'title'
|
30
35
|
@title = issue.new_title
|
@@ -38,5 +43,15 @@ module SyncIssues
|
|
38
43
|
second.delete!("\r")
|
39
44
|
first.gsub(/\[x\]/, '[ ]') == second.gsub(/\[x\]/, '[ ]')
|
40
45
|
end
|
46
|
+
|
47
|
+
def labels_match?(new_labels)
|
48
|
+
# Label uniqueness is not case-sensitive.
|
49
|
+
new_labels.map(&:downcase) == @labels.map(&:downcase)
|
50
|
+
end
|
51
|
+
|
52
|
+
def update_label?(issue, reset_labels)
|
53
|
+
!issue.labels.nil? && (reset_labels && !labels_match?(issue.labels) ||
|
54
|
+
@labels.size == 0)
|
55
|
+
end
|
41
56
|
end
|
42
57
|
end
|
data/lib/sync_issues/github.rb
CHANGED
@@ -5,14 +5,17 @@ require 'safe_yaml/load'
|
|
5
5
|
module SyncIssues
|
6
6
|
# GitHub is responsible access to GitHub's API
|
7
7
|
class GitHub
|
8
|
+
attr_reader :client
|
9
|
+
|
8
10
|
def initialize
|
9
11
|
@client = Octokit::Client.new access_token: token
|
10
12
|
@client.auto_paginate = true
|
11
13
|
end
|
12
14
|
|
13
|
-
def create_issue(repository, issue, add_assignee)
|
15
|
+
def create_issue(repository, issue, add_assignee, add_labels)
|
14
16
|
kwargs = {}
|
15
17
|
kwargs[:assignee] = issue.assignee if add_assignee
|
18
|
+
kwargs[:labels] = issue.labels if add_labels
|
16
19
|
@client.create_issue(repository.full_name, issue.title, issue.content,
|
17
20
|
**kwargs)
|
18
21
|
end
|
@@ -21,6 +24,10 @@ module SyncIssues
|
|
21
24
|
@client.issues(repository.full_name, state: :all)
|
22
25
|
end
|
23
26
|
|
27
|
+
def labels(repository)
|
28
|
+
@client.labels(repository.full_name)
|
29
|
+
end
|
30
|
+
|
24
31
|
def repository(repository_name)
|
25
32
|
@client.repository(repository_name)
|
26
33
|
rescue Octokit::InvalidRepository => exc
|
@@ -29,9 +36,11 @@ module SyncIssues
|
|
29
36
|
raise Error, 'repository not found'
|
30
37
|
end
|
31
38
|
|
32
|
-
def update_issue(repository, issue_number,
|
33
|
-
@client.update_issue(repository.full_name, issue_number,
|
34
|
-
|
39
|
+
def update_issue(repository, issue_number, comparison)
|
40
|
+
@client.update_issue(repository.full_name, issue_number,
|
41
|
+
comparison.title, comparison.content,
|
42
|
+
assignee: comparison.assignee,
|
43
|
+
labels: comparison.labels)
|
35
44
|
end
|
36
45
|
|
37
46
|
private
|
data/lib/sync_issues/issue.rb
CHANGED
@@ -6,22 +6,36 @@ module SyncIssues
|
|
6
6
|
# new_title is only used when an issue should be renamed. Issues with
|
7
7
|
# new_title set will never be created
|
8
8
|
class Issue
|
9
|
-
attr_reader :assignee, :content, :new_title, :title
|
9
|
+
attr_reader :assignee, :content, :labels, :new_title, :title
|
10
10
|
|
11
|
-
def initialize(content, title:, assignee: nil, new_title: nil)
|
11
|
+
def initialize(content, title:, assignee: nil, labels: nil, new_title: nil)
|
12
12
|
@assignee = verify_string 'assignee', assignee
|
13
13
|
@content = content
|
14
|
-
@
|
14
|
+
@labels = verify_labels(labels)
|
15
15
|
@new_title = verify_string 'new_title', new_title, allow_nil: true
|
16
|
+
@title = verify_string 'title', title, allow_nil: false
|
16
17
|
end
|
17
18
|
|
18
19
|
private
|
19
20
|
|
21
|
+
def verify_labels(labels)
|
22
|
+
return nil if labels.nil?
|
23
|
+
if labels.is_a?(String)
|
24
|
+
[verify_string('labels', labels, allow_nil: false)]
|
25
|
+
elsif !labels.is_a?(Array)
|
26
|
+
raise IssueError, "'labels' must be an Array or a String"
|
27
|
+
else
|
28
|
+
labels.each_with_index.map do |label, i|
|
29
|
+
verify_string("labels[#{i}]", label, allow_nil: false)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
20
34
|
def verify_string(field, value, allow_nil: true)
|
21
35
|
if value.nil?
|
22
36
|
raise IssueError, "'#{field}' must be provided" unless allow_nil
|
23
37
|
elsif !value.is_a?(String)
|
24
|
-
raise IssueError, "'#{field}' must be a
|
38
|
+
raise IssueError, "'#{field}' must be a String"
|
25
39
|
else
|
26
40
|
value.strip!
|
27
41
|
raise IssueError, "'#{field}' must not be blank" if value == ''
|
@@ -0,0 +1,93 @@
|
|
1
|
+
require_relative 'error'
|
2
|
+
require 'safe_yaml/load'
|
3
|
+
|
4
|
+
module SyncIssues
|
5
|
+
# Synchronizer is responsible for the actual synchronization.
|
6
|
+
class LabelSync
|
7
|
+
attr_reader :do_work, :keep_existing, :labels
|
8
|
+
|
9
|
+
def initialize(github, file_yaml)
|
10
|
+
@github = github
|
11
|
+
@labels = @keep_existing = nil
|
12
|
+
@do_work = file_yaml.nil? ? false : parse_yaml(file_yaml)
|
13
|
+
end
|
14
|
+
|
15
|
+
def synchronize(repository)
|
16
|
+
return unless @do_work
|
17
|
+
existing = existing_labels(repository)
|
18
|
+
make_changes(existing, repository)
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def add_labels(labels, repository)
|
24
|
+
labels.each do |label, color|
|
25
|
+
puts "\tadd label: #{label}"
|
26
|
+
@github.client.add_label(repository.full_name, label, color)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def delete_labels(labels, repository)
|
31
|
+
labels.each do |label, _|
|
32
|
+
if @keep_existing
|
33
|
+
puts "\tkeeping label: #{label}"
|
34
|
+
else
|
35
|
+
puts "\tdelete label: #{label}"
|
36
|
+
@github.client.delete_label!(repository.full_name, label)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def make_changes(existing, repository)
|
42
|
+
changes = { add: [], update: [] }
|
43
|
+
@labels.each do |label, color|
|
44
|
+
if existing.include?(label)
|
45
|
+
changes[:update] << [label, color] unless existing[label] == color
|
46
|
+
existing.delete(label)
|
47
|
+
else
|
48
|
+
changes[:add] << [label, color]
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
add_labels(changes[:add], repository)
|
53
|
+
update_labels(changes[:update], repository)
|
54
|
+
delete_labels(existing, repository)
|
55
|
+
rescue Octokit::UnprocessableEntity => exc
|
56
|
+
raise unless exc.errors.count == 1 && exc.errors[0][:resource] == 'Label'
|
57
|
+
error = exc.errors[0]
|
58
|
+
raise Error, "Label error: #{error[:code]} #{error[:field]}"
|
59
|
+
end
|
60
|
+
|
61
|
+
def existing_labels(repository)
|
62
|
+
Hash[@github.labels(repository).map do |label|
|
63
|
+
[label[:name], label[:color]]
|
64
|
+
end]
|
65
|
+
end
|
66
|
+
|
67
|
+
def parse_yaml(yaml)
|
68
|
+
data = SafeYAML.load(yaml)
|
69
|
+
return nil unless data
|
70
|
+
if data.include?('labels') && data['labels'].is_a?(Hash)
|
71
|
+
@labels = Hash[data['labels'].map do |label, color|
|
72
|
+
if color.is_a?(Integer)
|
73
|
+
raise Error, 'Label error: add quotes around numeric color values'
|
74
|
+
end
|
75
|
+
[label, color.to_s.downcase]
|
76
|
+
end]
|
77
|
+
else
|
78
|
+
@labels = {}
|
79
|
+
end
|
80
|
+
@keep_existing = data['keep_existing'] != false
|
81
|
+
@labels.size > 0 || !@keep_existing
|
82
|
+
rescue Psych::SyntaxError
|
83
|
+
raise ParseError, 'invalid label yaml file'
|
84
|
+
end
|
85
|
+
|
86
|
+
def update_labels(labels, repository)
|
87
|
+
labels.each do |label, color|
|
88
|
+
puts "\tupdate label: #{label}"
|
89
|
+
@github.client.update_label(repository.full_name, label, color: color)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -1,23 +1,27 @@
|
|
1
1
|
require_relative 'comparison'
|
2
2
|
require_relative 'error'
|
3
|
+
require_relative 'label_sync'
|
3
4
|
require_relative 'parser'
|
4
5
|
require 'English'
|
5
6
|
|
6
7
|
module SyncIssues
|
7
8
|
# Synchronizer is responsible for the actual synchronization.
|
8
9
|
class Synchronizer
|
9
|
-
def initialize(directory, repository_names,
|
10
|
-
|
10
|
+
def initialize(directory, repository_names, label_yaml: nil,
|
11
|
+
reset_labels: false, sync_assignees: true,
|
12
|
+
sync_labels: true, update_only: false)
|
11
13
|
@github = SyncIssues.github
|
12
14
|
@issues = issues(directory)
|
15
|
+
@label_sync = LabelSync.new(@github, label_yaml)
|
13
16
|
@repositories = repositories(repository_names)
|
17
|
+
@reset_labels = reset_labels
|
14
18
|
@sync_assignees = sync_assignees
|
19
|
+
@sync_labels = sync_labels
|
15
20
|
@update_only = update_only
|
16
21
|
end
|
17
22
|
|
18
23
|
def run
|
19
24
|
puts "Synchronize #{@issues.count} issue#{@issues.count == 1 ? '' : 's'}"
|
20
|
-
@issues.each { |issue| puts " * #{issue.title}" }
|
21
25
|
@repositories.each { |repository| synchronize(repository) }
|
22
26
|
end
|
23
27
|
|
@@ -62,6 +66,7 @@ module SyncIssues
|
|
62
66
|
|
63
67
|
def synchronize(repository)
|
64
68
|
puts "Repository: #{repository.full_name}"
|
69
|
+
@label_sync.synchronize(repository) if @sync_labels
|
65
70
|
|
66
71
|
existing_by_title = {}
|
67
72
|
@github.issues(repository).each do |issue|
|
@@ -84,18 +89,20 @@ module SyncIssues
|
|
84
89
|
puts "Skipping create issue: #{issue.title}"
|
85
90
|
else
|
86
91
|
puts "Adding issue: #{issue.title}"
|
87
|
-
@github.create_issue(repository, issue, @sync_assignees)
|
92
|
+
@github.create_issue(repository, issue, @sync_assignees, @sync_labels)
|
88
93
|
end
|
89
94
|
end
|
90
95
|
|
91
96
|
def update_issue(repository, issue, github_issue)
|
92
|
-
comparison = Comparison.new(issue, github_issue,
|
97
|
+
comparison = Comparison.new(issue, github_issue,
|
98
|
+
reset_labels: @reset_labels,
|
99
|
+
sync_assignee: @sync_assignees,
|
100
|
+
sync_labels: @sync_labels)
|
93
101
|
return unless comparison.changed?
|
94
102
|
|
95
103
|
changed = comparison.changed.join(', ')
|
96
104
|
puts "Updating #{changed} on ##{github_issue.number}"
|
97
|
-
@github.update_issue(repository, github_issue.number, comparison
|
98
|
-
comparison.content, comparison.assignee)
|
105
|
+
@github.update_issue(repository, github_issue.number, comparison)
|
99
106
|
end
|
100
107
|
end
|
101
108
|
end
|
data/lib/sync_issues/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sync_issues
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.6.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Bryce Boe
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-03-
|
11
|
+
date: 2016-03-16 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: docopt
|
@@ -70,6 +70,7 @@ files:
|
|
70
70
|
- lib/sync_issues/error.rb
|
71
71
|
- lib/sync_issues/github.rb
|
72
72
|
- lib/sync_issues/issue.rb
|
73
|
+
- lib/sync_issues/label_sync.rb
|
73
74
|
- lib/sync_issues/parser.rb
|
74
75
|
- lib/sync_issues/sync_issues_module.rb
|
75
76
|
- lib/sync_issues/synchronizer.rb
|