sync_issues 0.0.1a1 → 0.0.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 0b6a0252d1b5582610e2b01359cf5841bd606d53
4
- data.tar.gz: e2231679897dca717069ffa4f99a4b89711e8b70
3
+ metadata.gz: 851be84063a71f5f66d9f4433ef9e412b81ed1a4
4
+ data.tar.gz: 525105d38ebbb876c30e49c37b5b0e3ebbde1470
5
5
  SHA512:
6
- metadata.gz: fb399b702215518077d1ef0cadc442785ae33e30d2b7658857019befa57a3a473ecf7216a5bdaceb3c2cb9cc69bb9c6f6b29a14882970c81f578da6b427ea21f
7
- data.tar.gz: 0264b355338743058a3b017da78ba9b8122f9371880ba14650ed7569bf28ff78f481ed800f2b25554915fccfffb21f42f812659128f9b60a9f29649af62155d2
6
+ metadata.gz: ba04b87e9d73cc9ffd051750c9738a9bc7ec8a7edea7b759274fd89aaf3d970fbeed37bbbb5dd74f31c73da855681a9f4d862229d3c4198fbfa3d22ef8bfcf27
7
+ data.tar.gz: 4606a46c96a4468bc805cb9f68f6c1dd4fd87e20834410c9166e576a28d57c6d8e565df336ecdc8fd2405d27a27561ac45835409900610557062fc750697fcf2
@@ -10,13 +10,14 @@ module SyncIssues
10
10
  sync_issues: A tool that synchronizes a local directory with GitHub issues.
11
11
 
12
12
  Usage:
13
- sync_issues DIRECTORY REPOSITORY...
13
+ sync_issues [options] DIRECTORY REPOSITORY...
14
14
  sync_issues -h | --help
15
15
  sync_issues --version
16
16
 
17
17
  Options:
18
- -h --help Output this help information.
19
- --version Output the sync_issues version (#{VERSION}).
18
+ -u --update Only update existing issues.
19
+ -h --help Output this help information.
20
+ --version Output the sync_issues version (#{VERSION}).
20
21
  DOC
21
22
 
22
23
  def initialize
@@ -39,8 +40,8 @@ module SyncIssues
39
40
  end
40
41
 
41
42
  def handle_args(options)
42
- SyncIssues.synchronizer(options['DIRECTORY'],
43
- options['REPOSITORY']).run
43
+ SyncIssues.synchronizer(options['DIRECTORY'], options['REPOSITORY'],
44
+ update_only: options['--update']).run
44
45
  @exit_status
45
46
  end
46
47
  end
@@ -1,4 +1,10 @@
1
1
  module SyncIssues
2
2
  class Error < StandardError
3
3
  end
4
+
5
+ class IssueError < Error
6
+ end
7
+
8
+ class ParseError < Error
9
+ end
4
10
  end
@@ -0,0 +1,40 @@
1
+ require_relative 'error'
2
+ require 'octokit'
3
+ require 'safe_yaml/load'
4
+
5
+ module SyncIssues
6
+ # GitHub is responsible access to GitHub's API
7
+ class GitHub
8
+ def initialize
9
+ @client = Octokit::Client.new access_token: token
10
+ end
11
+
12
+ def create_issue(repository, issue)
13
+ @client.create_issue(repository.full_name, issue.title, issue.content)
14
+ end
15
+
16
+ def issues(repository)
17
+ @client.issues(repository)
18
+ end
19
+
20
+ def repository(repository_name)
21
+ @client.repository(repository_name)
22
+ rescue Octokit::InvalidRepository => exc
23
+ raise Error, exc.message
24
+ rescue Octokit::NotFound
25
+ raise Error, 'repository not found'
26
+ end
27
+
28
+ def update_issue(repository, github_issue, issue)
29
+ @client.update_issue(repository.full_name, github_issue.number,
30
+ issue.new_title || issue.title, issue.content)
31
+ end
32
+
33
+ private
34
+
35
+ def token
36
+ path = File.expand_path('~/.config/sync_issues.yaml')
37
+ SafeYAML.load(File.read(path))['token']
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,32 @@
1
+ require_relative 'error'
2
+
3
+ module SyncIssues
4
+ # Issue represents an issue to be added or updated.
5
+ #
6
+ # new_title is only used when an issue should be renamed. Issues with
7
+ # new_title set will never be created
8
+ class Issue
9
+ attr_reader :assignee, :content, :new_title, :title
10
+
11
+ def initialize(content, title:, assignee: nil, new_title: nil)
12
+ @assignee = verify_string 'assignee', assignee
13
+ @content = content
14
+ @title = verify_string 'title', title, allow_nil: false
15
+ @new_title = verify_string 'new_title', new_title, allow_nil: true
16
+ end
17
+
18
+ private
19
+
20
+ def verify_string(field, value, allow_nil: true)
21
+ if value.nil?
22
+ raise IssueError, "'#{field}' must be provided" unless allow_nil
23
+ elsif !value.is_a?(String)
24
+ raise IssueError, "'#{field}' must be a string"
25
+ else
26
+ value.strip!
27
+ raise IssueError, "'#{field}' must not be blank" if value == ''
28
+ end
29
+ value
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,41 @@
1
+ require_relative 'error'
2
+ require_relative 'issue'
3
+ require 'safe_yaml/load'
4
+
5
+ module SyncIssues
6
+ # Synchronizer is responsible for the actual synchronization.
7
+ class Parser
8
+ attr_reader :issue
9
+
10
+ def initialize(data)
11
+ @issue = nil
12
+ parse(data)
13
+ end
14
+
15
+ def parse(data)
16
+ unless data =~ /\A(---\s*\n.*?\n?)^((---|\.\.\.)\s*$\n?)/m
17
+ raise ParseError, 'missing frontmatter'
18
+ end
19
+
20
+ if (content = $POSTMATCH).empty?
21
+ raise ParseError, 'empty markdown content'
22
+ elsif (metadata = SafeYAML.load(Regexp.last_match(1))).nil?
23
+ raise ParseError, 'empty frontmatter'
24
+ else
25
+ @issue = Issue.new content, **hash_keys_to_symbols(metadata)
26
+ end
27
+ rescue ArgumentError => exc
28
+ raise ParseError, exc.message
29
+ rescue Psych::SyntaxError
30
+ raise ParseError, 'invalid frontmatter'
31
+ end
32
+
33
+ private
34
+
35
+ def hash_keys_to_symbols(hash)
36
+ hash.each_with_object({}) do |(key, value), tmp_hash|
37
+ tmp_hash[key.to_sym] = value
38
+ end
39
+ end
40
+ end
41
+ end
@@ -1,15 +1,20 @@
1
1
  require_relative 'command'
2
+ require_relative 'github'
2
3
  require_relative 'synchronizer'
3
4
 
4
5
  # SyncIssues
5
6
  module SyncIssues
6
7
  class << self
7
8
  def command
8
- @command ||= SyncIssues::Command.new
9
+ @command ||= Command.new
9
10
  end
10
11
 
11
- def synchronizer(directory, repositories)
12
- SyncIssues::Synchronizer.new(directory, repositories)
12
+ def github
13
+ @github ||= GitHub.new
14
+ end
15
+
16
+ def synchronizer(*args)
17
+ Synchronizer.new(*args)
13
18
  end
14
19
  end
15
20
  end
@@ -1,23 +1,97 @@
1
+ require_relative 'error'
2
+ require_relative 'parser'
3
+ require 'English'
4
+
1
5
  module SyncIssues
2
6
  # Synchronizer is responsible for the actual synchronization.
3
7
  class Synchronizer
4
- def initialize(directory, repositories)
5
- @directory = check_directory(directory)
6
- @repositories = check_repositories(repositories)
8
+ def initialize(directory, repository_names, update_only: false)
9
+ @issues = issues(directory)
10
+ @repositories = repositories(repository_names)
11
+ @update_only = update_only
7
12
  end
8
13
 
9
14
  def run
10
- puts "Sync #{@directory} to #{@repositories.inspect}."
15
+ puts "Synchronize #{@issues.count} issue#{@issues.count == 1 ? '' : 's'}"
16
+ @issues.each { |issue| puts " * #{issue.title}" }
17
+ @repositories.each { |repository| synchronize(repository) }
11
18
  end
12
19
 
13
20
  private
14
21
 
15
- def check_directory(directory)
16
- directory
22
+ def issues(directory)
23
+ unless File.directory?(directory)
24
+ raise Error, "'#{directory}' is not a valid directory"
25
+ end
26
+
27
+ issues = Dir.glob(File.join(directory, '**/*')).map do |entry|
28
+ next unless entry.end_with?('.md') && File.file?(entry)
29
+ begin
30
+ Parser.new(File.read(entry)).issue
31
+ rescue ParseError => exc
32
+ puts "'#{entry}': #{exc}"
33
+ nil
34
+ end
35
+ end.compact
36
+
37
+ if issues.empty?
38
+ raise Error, "'#{directory}' does not contain any .md files"
39
+ end
40
+
41
+ issues
17
42
  end
18
43
 
19
- def check_repositories(repositories)
44
+ def repositories(repository_names)
45
+ repositories = repository_names.map do |repository_name|
46
+ begin
47
+ SyncIssues.github.repository(repository_name)
48
+ rescue Error => exc
49
+ puts "'#{repository_name}' #{exc}"
50
+ nil
51
+ end
52
+ end.compact
53
+
54
+ raise Error, 'No valid repositories specified' if repositories.empty?
55
+
20
56
  repositories
21
57
  end
58
+
59
+ def synchronize(repository)
60
+ puts "Repository: #{repository.full_name}"
61
+
62
+ existing_by_title = {}
63
+ SyncIssues.github.issues(repository.full_name).each do |issue|
64
+ existing_by_title[issue.title] = issue
65
+ end
66
+
67
+ @issues.each do |issue|
68
+ if existing_by_title.include?(issue.title)
69
+ update_issue(repository, issue, existing_by_title[issue.title])
70
+ else
71
+ create_issue(repository, issue)
72
+ end
73
+ end
74
+ end
75
+
76
+ private
77
+
78
+ def create_issue(repository, issue)
79
+ if @update_only || issue.new_title
80
+ puts "Skipping create issue: #{issue.title}"
81
+ else
82
+ puts "Adding issue: #{issue.title}"
83
+ SyncIssues.github.create_issue(repository, issue)
84
+ end
85
+ end
86
+
87
+ def update_issue(repository, issue, github_issue)
88
+ changed = []
89
+ changed << 'title' unless issue.new_title.nil?
90
+ changed << 'body' unless issue.content == github_issue.body
91
+ return if changed.empty?
92
+
93
+ puts "Updating #{changed.join(', ')} on ##{github_issue.number}"
94
+ SyncIssues.github.update_issue(repository, github_issue, issue)
95
+ end
22
96
  end
23
97
  end
@@ -1,4 +1,4 @@
1
1
  # SyncIssues
2
2
  module SyncIssues
3
- VERSION = '0.0.1a1'.freeze
3
+ VERSION = '0.0.1'.freeze
4
4
  end
data/lib/sync_issues.rb CHANGED
@@ -1,3 +1,2 @@
1
- require 'sync_issues/command'
2
1
  require 'sync_issues/sync_issues_module'
3
2
  require 'sync_issues/version'
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.0.1a1
4
+ version: 0.0.1
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-01-28 00:00:00.000000000 Z
11
+ date: 2016-02-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: docopt
@@ -24,6 +24,34 @@ dependencies:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0.5'
27
+ - !ruby/object:Gem::Dependency
28
+ name: octokit
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '4.2'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '4.2'
41
+ - !ruby/object:Gem::Dependency
42
+ name: safe_yaml
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.0'
27
55
  description: |2
28
56
  sync_issues is a ruby gem to that allows the easy creation and
29
57
  synchronization of issues on GitHub with structured local data.
@@ -39,6 +67,9 @@ files:
39
67
  - lib/sync_issues.rb
40
68
  - lib/sync_issues/command.rb
41
69
  - lib/sync_issues/error.rb
70
+ - lib/sync_issues/github.rb
71
+ - lib/sync_issues/issue.rb
72
+ - lib/sync_issues/parser.rb
42
73
  - lib/sync_issues/sync_issues_module.rb
43
74
  - lib/sync_issues/synchronizer.rb
44
75
  - lib/sync_issues/version.rb
@@ -57,9 +88,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
57
88
  version: '0'
58
89
  required_rubygems_version: !ruby/object:Gem::Requirement
59
90
  requirements:
60
- - - ">"
91
+ - - ">="
61
92
  - !ruby/object:Gem::Version
62
- version: 1.3.1
93
+ version: '0'
63
94
  requirements: []
64
95
  rubyforge_project:
65
96
  rubygems_version: 2.4.8