sync_issues 0.5.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: e6ec2a1e421c8518f79623a00467957940783a15
4
- data.tar.gz: 36447ea778835cca858463eeeec3e633b03c1ed0
3
+ metadata.gz: 44337f4aa3175ff1ba7e15f4583309ce034a251a
4
+ data.tar.gz: 29027fe887bb51d2412cf2fc9c4bba32d482b7db
5
5
  SHA512:
6
- metadata.gz: fd6ca399baa4a34133cca84e434b129b07b9c9b9f478ae5d6f9b3d8c9a929a2267864a51625375471369c67dbdb0145d299f5f6a7f22d98ab041153894e2c562
7
- data.tar.gz: 905384ce86b6e966b01b474750705d130d9379be3f01ef910e3567f964e390defeba6897f89e69225b0466fa37645e1200a7f75bcbc51106e892c9f1ec2db50a
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
- * __label__: (optional) Set the labels of the issue to this comma-separated
69
- string of issues. Existing labels will not be cleared on sync when the field
70
- is not provided.
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
+ ```
@@ -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, sync_assignee)
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
- @sync_assignee = sync_assignee
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 @sync_assignee && issue.assignee != @assignee
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
@@ -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, title, content, assignee)
33
- @client.update_issue(repository.full_name, issue_number, title, content,
34
- assignee: assignee)
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
@@ -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
- @title = verify_string 'title', title, allow_nil: false
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 string"
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, sync_assignees: true,
10
- update_only: false)
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, @sync_assignees)
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.title,
98
- comparison.content, comparison.assignee)
105
+ @github.update_issue(repository, github_issue.number, comparison)
99
106
  end
100
107
  end
101
108
  end
@@ -1,4 +1,4 @@
1
1
  # SyncIssues
2
2
  module SyncIssues
3
- VERSION = '0.5.0'.freeze
3
+ VERSION = '0.6.0'.freeze
4
4
  end
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.5.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-10 00:00:00.000000000 Z
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