sync_issues 0.0.1a1 → 0.0.1

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: 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