monotes 0.0.1 → 0.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.
- checksums.yaml +4 -4
- data/.rspec +3 -0
- data/.travis.yml +9 -0
- data/README.md +31 -2
- data/Rakefile +3 -0
- data/bin/monotes +4 -0
- data/lib/monotes.rb +1 -1
- data/lib/monotes/IO/fs_delegate.rb +28 -0
- data/lib/monotes/app_directory.rb +7 -0
- data/lib/monotes/authenticator.rb +23 -0
- data/lib/monotes/body_text.rb +36 -0
- data/lib/monotes/cli/application.rb +122 -0
- data/lib/monotes/issue_download.rb +17 -0
- data/lib/monotes/issue_repository.rb +37 -0
- data/lib/monotes/models/issue.rb +21 -0
- data/lib/monotes/sync_list.rb +21 -0
- data/lib/monotes/version.rb +1 -1
- data/monotes.gemspec +6 -0
- data/spec/factories.rb +6 -0
- data/spec/spec_helper.rb +29 -0
- data/spec/unit/authenticator_spec.rb +48 -0
- data/spec/unit/issue_download_spec.rb +22 -0
- data/spec/unit/issue_repository_spec.rb +75 -0
- data/spec/unit/sync_list_spec.rb +84 -0
- metadata +113 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1230cb7946ec444383f76e8d171a2788fc1f081e
|
4
|
+
data.tar.gz: 0a44dc0b60cb8cb6e6b834cd771d26e0ff22e8dd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b22e8efb7f377518417941e0972cf0528b3d651a17096dfd3c79ee5b38b405c444c600f1e3d68c5d604d810ae93dfabc63c4b654a1208c8c067f4f30b864f389
|
7
|
+
data.tar.gz: 184e1e2ab900e0c9bd95204cbfea993330044971069292b75765c2f54ce100a4960b887ad152044b5697b6e3d4cd864a320e5fa73cd5715830191ebd19af1d5e
|
data/.rspec
ADDED
data/.travis.yml
ADDED
data/README.md
CHANGED
@@ -1,6 +1,8 @@
|
|
1
|
+
[](https://travis-ci.org/schultyy/monotes)
|
1
3
|
# Monotes
|
2
4
|
|
3
|
-
|
5
|
+
Monotes is a GitHub Issues commandline client.
|
4
6
|
|
5
7
|
## Installation
|
6
8
|
|
@@ -18,7 +20,34 @@ Or install it yourself as:
|
|
18
20
|
|
19
21
|
## Usage
|
20
22
|
|
21
|
-
|
23
|
+
### Login to GitHub
|
24
|
+
|
25
|
+
```bash
|
26
|
+
$ monotes login
|
27
|
+
Username: <your usename>
|
28
|
+
Password: <Password>
|
29
|
+
```
|
30
|
+
|
31
|
+
### Login with Two-Factor Authentication
|
32
|
+
|
33
|
+
```bash
|
34
|
+
$ monotes login
|
35
|
+
Username: <your usename>
|
36
|
+
Password: <Password>
|
37
|
+
Your 2FA token: <Token>
|
38
|
+
```
|
39
|
+
|
40
|
+
### Download issues for repository
|
41
|
+
|
42
|
+
```bash
|
43
|
+
$ monotes download 'schultyy/monotes'
|
44
|
+
```
|
45
|
+
|
46
|
+
### Browse downloaded issues
|
47
|
+
|
48
|
+
```bash
|
49
|
+
$ monotes show 'schultyy/monotes'
|
50
|
+
```
|
22
51
|
|
23
52
|
## Contributing
|
24
53
|
|
data/Rakefile
CHANGED
data/bin/monotes
ADDED
data/lib/monotes.rb
CHANGED
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'monotes/app_directory'
|
2
|
+
|
3
|
+
module Monotes
|
4
|
+
module IO
|
5
|
+
class FSDelegate
|
6
|
+
include Monotes::AppDirectory
|
7
|
+
|
8
|
+
#
|
9
|
+
# issues: Issues represented as Hash
|
10
|
+
#
|
11
|
+
def save(username, repository, issues)
|
12
|
+
if !File.directory?(app_path)
|
13
|
+
Dir.mkdir(app_path)
|
14
|
+
end
|
15
|
+
user_folder = File.join(app_path, username)
|
16
|
+
Dir.mkdir(user_folder) if !File.directory?(user_folder)
|
17
|
+
File.open(File.join(user_folder, "#{repository}.yaml"), "w") do |handle|
|
18
|
+
handle.write(issues.to_yaml)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def load(username, repository)
|
23
|
+
abs_path = File.join(app_path, username, "#{repository}.yaml")
|
24
|
+
YAML.load_file(abs_path)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Monotes
|
2
|
+
class Authenticator
|
3
|
+
ACCESS_NOTE = "Monotes access token"
|
4
|
+
def initialize(api_client_klass)
|
5
|
+
@api_client_klass = api_client_klass
|
6
|
+
end
|
7
|
+
|
8
|
+
def get_oauth_token(username, password, &acquire_two_fa)
|
9
|
+
api_client = @api_client_klass.new(:login => username, :password => password)
|
10
|
+
begin
|
11
|
+
api_client.create_authorization(:scopes => scopes, :note => ACCESS_NOTE)
|
12
|
+
rescue Octokit::OneTimePasswordRequired
|
13
|
+
two_fa_token = yield acquire_two_fa
|
14
|
+
api_client.create_authorization(:scopes => scopes, :note => ACCESS_NOTE,
|
15
|
+
:headers => { "X-GitHub-OTP" => two_fa_token })
|
16
|
+
end
|
17
|
+
end
|
18
|
+
private
|
19
|
+
def scopes
|
20
|
+
["user", "repo"]
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'monotes/app_directory'
|
2
|
+
|
3
|
+
module Monotes
|
4
|
+
class BodyText
|
5
|
+
include Monotes::AppDirectory
|
6
|
+
FILENAME = "ISSUE_BODY_TEXT"
|
7
|
+
|
8
|
+
def initialize(title)
|
9
|
+
@title = title
|
10
|
+
end
|
11
|
+
|
12
|
+
def read
|
13
|
+
File.read(path)
|
14
|
+
end
|
15
|
+
|
16
|
+
def flush
|
17
|
+
File.delete(path)
|
18
|
+
end
|
19
|
+
|
20
|
+
def create_issue
|
21
|
+
edit_success = system "vim #{path}"
|
22
|
+
if edit_success
|
23
|
+
body_text = read
|
24
|
+
flush
|
25
|
+
Monotes::Models::Issue.new(:title => @title, :body => body_text)
|
26
|
+
else
|
27
|
+
nil
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
def path
|
33
|
+
File.join(app_path, FILENAME)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
require 'thor'
|
2
|
+
require 'yaml'
|
3
|
+
require 'netrc'
|
4
|
+
require 'octokit'
|
5
|
+
require 'monotes/authenticator'
|
6
|
+
require 'monotes/issue_download'
|
7
|
+
require 'monotes/issue_repository'
|
8
|
+
require 'monotes/models/issue'
|
9
|
+
require 'monotes/app_directory'
|
10
|
+
require 'monotes/body_text'
|
11
|
+
require 'monotes/sync_list'
|
12
|
+
|
13
|
+
module Monotes
|
14
|
+
module CLI
|
15
|
+
class Application < Thor
|
16
|
+
include Monotes::AppDirectory
|
17
|
+
|
18
|
+
desc "login", "Login into GitHub"
|
19
|
+
def login
|
20
|
+
print "Username > "
|
21
|
+
username = STDIN.gets.chomp
|
22
|
+
validate!("username", username)
|
23
|
+
print "Password > "
|
24
|
+
password = STDIN.noecho(&:gets).chomp
|
25
|
+
validate!("password", password)
|
26
|
+
STDOUT.puts "\n"
|
27
|
+
authenticator = Monotes::Authenticator.new(Octokit::Client)
|
28
|
+
begin
|
29
|
+
oauth_token = authenticator.get_oauth_token(username, password) do
|
30
|
+
print "Two-Factor token > "
|
31
|
+
token = STDIN.gets.chomp
|
32
|
+
validate!("Two-Factor token", token)
|
33
|
+
token
|
34
|
+
end
|
35
|
+
rescue Octokit::Unauthorized => unauthorized
|
36
|
+
STDERR.puts "Unauthorized: #{unauthorized.message}"
|
37
|
+
exit 77
|
38
|
+
rescue Exception => e
|
39
|
+
fatal!(e)
|
40
|
+
|
41
|
+
else
|
42
|
+
write_to_netrc(username, oauth_token.token)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
desc "download REPOSITORY", "Download issues for a repository"
|
47
|
+
def download(repository)
|
48
|
+
STDOUT.puts "Downloading issues for #{repository}..."
|
49
|
+
downloader = Monotes::IssueDownload.new(Octokit)
|
50
|
+
begin
|
51
|
+
issues = downloader.download(repository)
|
52
|
+
rescue Exception => exc
|
53
|
+
fatal!(exc)
|
54
|
+
end
|
55
|
+
repository = Monotes::IssueRepository.build(repository: repository)
|
56
|
+
repository.save(issues)
|
57
|
+
end
|
58
|
+
|
59
|
+
desc "show REPOSITORY", "Show downloaded issues"
|
60
|
+
def show(repository_name)
|
61
|
+
repository = Monotes::IssueRepository.build(repository: repository_name)
|
62
|
+
issues = repository.load
|
63
|
+
issues.map do |issue|
|
64
|
+
if issue.unsynced?
|
65
|
+
STDOUT.puts "(new) - #{issue.title}"
|
66
|
+
else
|
67
|
+
STDOUT.puts "#{issue.number} - #{issue.title}"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
desc "create REPOSITORY TITLE", "Creates a new local issue"
|
73
|
+
def create(repository_name, title)
|
74
|
+
text = Monotes::BodyText.new(title)
|
75
|
+
issue = text.create_issue
|
76
|
+
repository = Monotes::IssueRepository.build(repository: repository_name)
|
77
|
+
repository.append(issue)
|
78
|
+
end
|
79
|
+
|
80
|
+
desc "sync REPOSITORY", "Synchronizes local issues with GitHub"
|
81
|
+
def sync(repository_name)
|
82
|
+
repository = Monotes::IssueRepository.build(repository: repository_name)
|
83
|
+
issues = repository.load
|
84
|
+
already_synced = issues.reject { |i| i.unsynced? }
|
85
|
+
adapter = Octokit::Client.new(netrc: true)
|
86
|
+
begin
|
87
|
+
sync_list = Monotes::SyncList.new(list: issues, repo: repository_name, adapter: adapter)
|
88
|
+
synced = sync_list.sync do |issue|
|
89
|
+
STDOUT.puts "Synced issue #{issue.title}"
|
90
|
+
end
|
91
|
+
rescue Exception => exc
|
92
|
+
fatal!(exc)
|
93
|
+
end
|
94
|
+
repository.save(already_synced.concat(synced))
|
95
|
+
end
|
96
|
+
|
97
|
+
private
|
98
|
+
|
99
|
+
def fatal!(exc)
|
100
|
+
STDERR.puts "FATAL: #{exc.message}"
|
101
|
+
exit 74
|
102
|
+
end
|
103
|
+
|
104
|
+
def validate!(name, param)
|
105
|
+
if param.nil? || param.empty?
|
106
|
+
STDERR.puts "Fatal: #{name} cannot be empty"
|
107
|
+
exit 74
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def split_repository_identifier(repo)
|
112
|
+
repo.split('/')
|
113
|
+
end
|
114
|
+
|
115
|
+
def write_to_netrc(username, token)
|
116
|
+
netrc_handle = Netrc.read
|
117
|
+
netrc_handle["api.github.com"] = username, token
|
118
|
+
netrc_handle.save
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'monotes/models/issue'
|
2
|
+
|
3
|
+
module Monotes
|
4
|
+
class IssueDownload
|
5
|
+
def initialize(api_client)
|
6
|
+
@api_client = api_client
|
7
|
+
end
|
8
|
+
|
9
|
+
def download(repository)
|
10
|
+
raise ArgumentError, 'repository must not be nil' if repository.nil?
|
11
|
+
|
12
|
+
@api_client.list_issues(repository).map do |issue|
|
13
|
+
Monotes::Models::Issue.new(issue)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'monotes/IO/fs_delegate'
|
3
|
+
|
4
|
+
module Monotes
|
5
|
+
class IssueRepository
|
6
|
+
|
7
|
+
def initialize(args)
|
8
|
+
@context = args.fetch(:context)
|
9
|
+
@repository = args.fetch(:repository)
|
10
|
+
end
|
11
|
+
|
12
|
+
def save(args)
|
13
|
+
issues = Array(args).map do |issue|
|
14
|
+
issue.to_hash
|
15
|
+
end
|
16
|
+
@context.save(*@repository.split('/'), issues)
|
17
|
+
end
|
18
|
+
|
19
|
+
def append(new_issue)
|
20
|
+
raise ArgumentError, 'issue must not be nil' if new_issue.nil?
|
21
|
+
issues = load
|
22
|
+
issues << new_issue
|
23
|
+
save(issues)
|
24
|
+
end
|
25
|
+
|
26
|
+
def load
|
27
|
+
@context.load(*@repository.split('/')).map do |issue_hash|
|
28
|
+
Monotes::Models::Issue.new(issue_hash)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.build(args)
|
33
|
+
context = Monotes::IO::FSDelegate.new
|
34
|
+
Monotes::IssueRepository.new(args.merge(:context => context))
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'virtus'
|
2
|
+
|
3
|
+
module Monotes
|
4
|
+
module Models
|
5
|
+
class Issue
|
6
|
+
include Virtus.model
|
7
|
+
attribute :url, String
|
8
|
+
attribute :id, Fixnum, :default => 0
|
9
|
+
attribute :number, Fixnum, :default => 0
|
10
|
+
attribute :title, String
|
11
|
+
attribute :state, String
|
12
|
+
attribute :created_at, DateTime
|
13
|
+
attribute :updated_at, DateTime
|
14
|
+
attribute :body, String
|
15
|
+
|
16
|
+
def unsynced?
|
17
|
+
number == 0
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'monotes/models/issue'
|
2
|
+
|
3
|
+
module Monotes
|
4
|
+
class SyncList
|
5
|
+
|
6
|
+
def initialize(args)
|
7
|
+
@list = args.fetch(:list)
|
8
|
+
@adapter = args.fetch(:adapter)
|
9
|
+
@repository = args.fetch(:repo)
|
10
|
+
end
|
11
|
+
|
12
|
+
def sync
|
13
|
+
unsynced = @list.find_all {|issue| issue.unsynced? }
|
14
|
+
unsynced.map do |issue|
|
15
|
+
result = @adapter.create_issue(@repository, issue.title, issue.body)
|
16
|
+
yield(result) if block_given?
|
17
|
+
Monotes::Models::Issue.new(result.to_hash)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/lib/monotes/version.rb
CHANGED
data/monotes.gemspec
CHANGED
@@ -18,6 +18,12 @@ Gem::Specification.new do |spec|
|
|
18
18
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
19
|
spec.require_paths = ["lib"]
|
20
20
|
|
21
|
+
spec.add_dependency "thor"
|
22
|
+
spec.add_dependency "octokit", "~> 3.0"
|
23
|
+
spec.add_dependency "netrc"
|
24
|
+
spec.add_dependency "virtus"
|
21
25
|
spec.add_development_dependency "bundler", "~> 1.6"
|
22
26
|
spec.add_development_dependency "rake"
|
27
|
+
spec.add_development_dependency "rspec"
|
28
|
+
spec.add_development_dependency "factory_girl", "~> 4.4.0"
|
23
29
|
end
|
data/spec/factories.rb
ADDED
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'factory_girl'
|
2
|
+
|
3
|
+
RSpec.configure do |config|
|
4
|
+
config.filter_run :focus
|
5
|
+
config.run_all_when_everything_filtered = true
|
6
|
+
|
7
|
+
if config.files_to_run.one?
|
8
|
+
config.default_formatter = 'doc'
|
9
|
+
end
|
10
|
+
|
11
|
+
config.profile_examples = 10
|
12
|
+
|
13
|
+
config.order = :random
|
14
|
+
|
15
|
+
Kernel.srand config.seed
|
16
|
+
|
17
|
+
config.include FactoryGirl::Syntax::Methods
|
18
|
+
FactoryGirl.definition_file_paths = [File.expand_path('../factories', __FILE__)]
|
19
|
+
FactoryGirl.find_definitions
|
20
|
+
|
21
|
+
config.expect_with :rspec do |expectations|
|
22
|
+
expectations.syntax = :expect
|
23
|
+
end
|
24
|
+
|
25
|
+
config.mock_with :rspec do |mocks|
|
26
|
+
mocks.syntax = :expect
|
27
|
+
mocks.verify_partial_doubles = true
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'monotes/authenticator'
|
3
|
+
require 'octokit'
|
4
|
+
|
5
|
+
describe Monotes::Authenticator do
|
6
|
+
let(:api_client_mock_class) { double('Octokit::Client') }
|
7
|
+
let(:api_client_mock) { double('Octokit::Client instance') }
|
8
|
+
let(:username) { 'Jim' }
|
9
|
+
let(:password) { 'passw' }
|
10
|
+
let(:expected_oauth_token) { 'expected_oauth_token' }
|
11
|
+
|
12
|
+
context 'without 2FA' do
|
13
|
+
subject(:authenticator) { Monotes::Authenticator.new(api_client_mock_class) }
|
14
|
+
|
15
|
+
before do
|
16
|
+
allow(api_client_mock_class).to receive(:new).with(any_args).and_return(api_client_mock)
|
17
|
+
allow(api_client_mock).to receive(:create_authorization).with(any_args).and_return(expected_oauth_token)
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'authenticates without asking for 2-FA token' do
|
21
|
+
actual_token = authenticator.get_oauth_token(username, password) { raise "2-FA Block was called" }
|
22
|
+
expect(actual_token).to eq expected_oauth_token
|
23
|
+
end
|
24
|
+
end
|
25
|
+
context 'with 2FA' do
|
26
|
+
let(:two_fa_token) { '2-factor token' }
|
27
|
+
subject(:authenticator) { Monotes::Authenticator.new(api_client_mock_class) }
|
28
|
+
|
29
|
+
before do
|
30
|
+
params = { :scopes => ["user", "repo"], :note => Monotes::Authenticator::ACCESS_NOTE }
|
31
|
+
params_with_2fa = params.merge(:headers => { "X-GitHub-OTP" => two_fa_token })
|
32
|
+
allow(api_client_mock_class).to receive(:new).with(any_args).and_return(api_client_mock)
|
33
|
+
allow(api_client_mock).to receive(:create_authorization).with(params).and_raise(Octokit::OneTimePasswordRequired)
|
34
|
+
allow(api_client_mock).to receive(:create_authorization).with(params_with_2fa).and_return(expected_oauth_token)
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'authenticates and asks for 2-FA token' do
|
38
|
+
block_called = false
|
39
|
+
authenticator.get_oauth_token(username, password) { block_called = true; two_fa_token }
|
40
|
+
expect(block_called).to be true
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'authenticates with 2-FA token' do
|
44
|
+
actual_token = authenticator.get_oauth_token(username, password) { two_fa_token }
|
45
|
+
expect(actual_token).to eq expected_oauth_token
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'monotes/issue_download'
|
3
|
+
|
4
|
+
describe Monotes::IssueDownload do
|
5
|
+
let(:octo_mock) { double('Octokit') }
|
6
|
+
let(:issue_list) { [attributes_for(:issue)] }
|
7
|
+
let(:repository) { 'franz/franz-repo' }
|
8
|
+
subject(:downloader) { Monotes::IssueDownload.new(octo_mock) }
|
9
|
+
|
10
|
+
context '#download' do
|
11
|
+
before do
|
12
|
+
allow(octo_mock).to receive(:list_issues).and_return(issue_list)
|
13
|
+
end
|
14
|
+
it 'returns a list of issues' do
|
15
|
+
expect(downloader.download(repository).length).to be > 0
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'raises error when repository is nil' do
|
19
|
+
expect { downloader.download(nil) }.to raise_error(ArgumentError)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'yaml'
|
3
|
+
require 'monotes/issue_repository'
|
4
|
+
|
5
|
+
describe Monotes::IssueRepository do
|
6
|
+
let(:issues) { build_list(:issue, 2) }
|
7
|
+
let(:context) { double('fs context') }
|
8
|
+
let(:repository_name) { 'franz/franz-seins' }
|
9
|
+
let(:issue) { issues.first }
|
10
|
+
subject(:repository) { Monotes::IssueRepository.new(repository: repository_name, context: context) }
|
11
|
+
|
12
|
+
context '#initialize' do
|
13
|
+
it 'accepts context and repository' do
|
14
|
+
expect { Monotes::IssueRepository.new(context: context, repository: repository_name) }.to_not raise_error
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'raises error when context not passed' do
|
18
|
+
expect { Monotes::IssueRepository.new(repository: repository_name) }.to raise_error
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'raises error when repository not passed' do
|
22
|
+
expect { Monotes::IssueRepository.new(context: context) }.to raise_error
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
context '#save' do
|
27
|
+
before do
|
28
|
+
allow(context).to receive(:save)
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'saves a single issue' do
|
32
|
+
repository.save(issue)
|
33
|
+
expect(context).to have_received(:save).with('franz', 'franz-seins', [issue.to_hash])
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
context '#load' do
|
38
|
+
before do
|
39
|
+
allow(context).to receive(:load).and_return(attributes_for_list(:issue, 2))
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'returns a list of issues' do
|
43
|
+
expect(repository.load.length).to be > 0
|
44
|
+
end
|
45
|
+
|
46
|
+
context 'result set' do
|
47
|
+
context 'element' do
|
48
|
+
it 'is of type Issue' do
|
49
|
+
issue = repository.load.first
|
50
|
+
expect(issue.class).to eq Monotes::Models::Issue
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
context '#append' do
|
57
|
+
before do
|
58
|
+
allow(context).to receive(:save)
|
59
|
+
allow(context).to receive(:load).and_return(attributes_for_list(:issue, 1))
|
60
|
+
repository.append(build(:issue))
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'loads issues' do
|
64
|
+
expect(context).to have_received(:load)
|
65
|
+
end
|
66
|
+
|
67
|
+
it 'saves with appended issue' do
|
68
|
+
expect(context).to have_received(:save)
|
69
|
+
end
|
70
|
+
|
71
|
+
it 'raises error when issue is nil' do
|
72
|
+
expect { repository.append(nil) }.to raise_error(ArgumentError)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'monotes/models/issue'
|
3
|
+
require 'monotes/sync_list'
|
4
|
+
|
5
|
+
describe Monotes::SyncList do
|
6
|
+
let(:issue) { Monotes::Models::Issue.new }
|
7
|
+
let(:adapter) { double('OctoKit') }
|
8
|
+
let(:repo_name) { 'alice/example' }
|
9
|
+
|
10
|
+
context '#initialize' do
|
11
|
+
it 'accepts a list of issues' do
|
12
|
+
expect do
|
13
|
+
Monotes::SyncList.new(list: build_list(:issue, 1),
|
14
|
+
adapter: adapter,
|
15
|
+
repo: repo_name)
|
16
|
+
end.to_not raise_error
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'raises error if no list was passed' do
|
20
|
+
expect { Monotes::SyncList.new(adapter: adapter, repo: repo_name) }.to raise_error
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'raises error if no adapter was passed' do
|
24
|
+
expect { Monotes::SyncList.new(list: [issue], repo: repo_name) }.to raise_error
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'raises error if no repository name was passed' do
|
28
|
+
expect { Monotes::SyncList.new(list: [issue], adapter: adapter) }.to raise_error
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
context '#sync' do
|
33
|
+
let(:unsynced_issues) { build_list(:issue, 1, number: 0, title: 'foo', body:'bar') }
|
34
|
+
let(:synced_issues) { build_list(:issue, 1, number: 45, title: 'baz', body:'yadda yadda') }
|
35
|
+
let(:issue_result) { attributes_for(:issue, number: 1, id: 1) }
|
36
|
+
|
37
|
+
before(:each) do
|
38
|
+
allow(adapter).to receive(:create_issue) { issue_result }
|
39
|
+
end
|
40
|
+
|
41
|
+
context 'with unsynced issues' do
|
42
|
+
subject(:sync_list) { Monotes::SyncList.new(list: unsynced_issues, adapter: adapter, repo: repo_name) }
|
43
|
+
|
44
|
+
it 'calls adapter to create issue' do
|
45
|
+
sync_list.sync
|
46
|
+
expect(adapter).to have_received(:create_issue).with(repo_name, 'foo', 'bar')
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'calls block for each issue' do
|
50
|
+
block_called = false
|
51
|
+
sync_list.sync { |issue| block_called = true }
|
52
|
+
expect(block_called).to be true
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'calls block with result from adapter call' do
|
56
|
+
block_result = nil
|
57
|
+
sync_list.sync { |issue| block_result = issue }
|
58
|
+
expect(block_result).to eq issue_result
|
59
|
+
end
|
60
|
+
|
61
|
+
context 'after sync' do
|
62
|
+
context 'issue' do
|
63
|
+
it 'has number' do
|
64
|
+
result = sync_list.sync.first
|
65
|
+
expect(result.unsynced?).to be false
|
66
|
+
end
|
67
|
+
it 'has id' do
|
68
|
+
result = sync_list.sync.first
|
69
|
+
expect(result.id).to_not eq 0
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
context 'with synced and unsynced issues' do
|
76
|
+
subject(:sync_list) { Monotes::SyncList.new(list: unsynced_issues.concat(synced_issues), adapter: adapter, repo: repo_name) }
|
77
|
+
|
78
|
+
it 'calls adapter only for unsynced issue' do
|
79
|
+
sync_list.sync
|
80
|
+
expect(adapter).to have_received(:create_issue).with(repo_name, 'foo', 'bar').once
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
metadata
CHANGED
@@ -1,15 +1,71 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: monotes
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jan Schulte
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-08-
|
11
|
+
date: 2014-08-23 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: thor
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: octokit
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '3.0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '3.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: netrc
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: virtus
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
13
69
|
- !ruby/object:Gem::Dependency
|
14
70
|
name: bundler
|
15
71
|
requirement: !ruby/object:Gem::Requirement
|
@@ -38,22 +94,69 @@ dependencies:
|
|
38
94
|
- - ">="
|
39
95
|
- !ruby/object:Gem::Version
|
40
96
|
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rspec
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: factory_girl
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: 4.4.0
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: 4.4.0
|
41
125
|
description: GitHub Issues commandline client
|
42
126
|
email:
|
43
127
|
- schulte@unexpected-co.de
|
44
|
-
executables:
|
128
|
+
executables:
|
129
|
+
- monotes
|
45
130
|
extensions: []
|
46
131
|
extra_rdoc_files: []
|
47
132
|
files:
|
48
133
|
- ".gitignore"
|
134
|
+
- ".rspec"
|
135
|
+
- ".travis.yml"
|
49
136
|
- Gemfile
|
50
137
|
- LICENSE
|
51
138
|
- LICENSE.txt
|
52
139
|
- README.md
|
53
140
|
- Rakefile
|
141
|
+
- bin/monotes
|
54
142
|
- lib/monotes.rb
|
143
|
+
- lib/monotes/IO/fs_delegate.rb
|
144
|
+
- lib/monotes/app_directory.rb
|
145
|
+
- lib/monotes/authenticator.rb
|
146
|
+
- lib/monotes/body_text.rb
|
147
|
+
- lib/monotes/cli/application.rb
|
148
|
+
- lib/monotes/issue_download.rb
|
149
|
+
- lib/monotes/issue_repository.rb
|
150
|
+
- lib/monotes/models/issue.rb
|
151
|
+
- lib/monotes/sync_list.rb
|
55
152
|
- lib/monotes/version.rb
|
56
153
|
- monotes.gemspec
|
154
|
+
- spec/factories.rb
|
155
|
+
- spec/spec_helper.rb
|
156
|
+
- spec/unit/authenticator_spec.rb
|
157
|
+
- spec/unit/issue_download_spec.rb
|
158
|
+
- spec/unit/issue_repository_spec.rb
|
159
|
+
- spec/unit/sync_list_spec.rb
|
57
160
|
homepage: https://github.com/schultyy/monotes
|
58
161
|
licenses:
|
59
162
|
- MIT
|
@@ -78,5 +181,11 @@ rubygems_version: 2.1.11
|
|
78
181
|
signing_key:
|
79
182
|
specification_version: 4
|
80
183
|
summary: GitHub Issues commandline client
|
81
|
-
test_files:
|
184
|
+
test_files:
|
185
|
+
- spec/factories.rb
|
186
|
+
- spec/spec_helper.rb
|
187
|
+
- spec/unit/authenticator_spec.rb
|
188
|
+
- spec/unit/issue_download_spec.rb
|
189
|
+
- spec/unit/issue_repository_spec.rb
|
190
|
+
- spec/unit/sync_list_spec.rb
|
82
191
|
has_rdoc:
|