octokom 0.0.2 → 0.0.3

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: 1e09a35e0cf96d8108161d2e98d3b0bb2eef732d
4
- data.tar.gz: 53ab6973726062831ce6fdea8e5abfe87eead951
3
+ metadata.gz: a827d2870894f3b5f4c778cbc97475081b536758
4
+ data.tar.gz: 925ab8ea02b3347905a44f4752f79348329b2801
5
5
  SHA512:
6
- metadata.gz: 748cbaac055db47202bfb129952bf8656b96054606599ef11790ec8beeeaf34a189266608cb0c1f72d937aafb978aa84335e7377c3573bc71b969beef4aea3c8
7
- data.tar.gz: 1ef8d30e2359cb1d5292f876acaeb306dccc11cb52e8a2edaab8ea5094fc7a793228eb3cf7f2e17dec0769caee7eed5cecce7751bad4fbee5c63d305f243383a
6
+ metadata.gz: 1cb21e97e894109e9b5e05a0a1c30df072d3de2f2a446a197f838ce6c6d07340d4896fcadca62b7007406990bc6cf3ea3d749d0a33d0f22009e05d1da0e7796a
7
+ data.tar.gz: cdde0f3ac7c090bb8ce170091a1ffcd2d5a3849d4d6ec93b5aee7cac933e48d091755d70596c8a2ccd4411797f3cef9f0e9dcc4025fd9fb11195f400a0b5387f
data/.gitignore CHANGED
@@ -15,3 +15,4 @@ spec/reports
15
15
  test/tmp
16
16
  test/version_tmp
17
17
  tmp
18
+ spec/support/.netrc
data/.rspec CHANGED
@@ -1,3 +1,2 @@
1
1
  --color
2
- --fail-fast
3
2
  --order random
data/.travis.yml CHANGED
@@ -1,15 +1,22 @@
1
1
  language: ruby
2
2
 
3
3
  rvm:
4
+ - rbx-19mode
5
+ - jruby-19mode
6
+ - 1.9.2
4
7
  - 1.9.3
5
8
  - 2.0.0
9
+ - 2.1.0
6
10
  - ruby-head
7
11
 
8
12
  matrix:
9
13
  allow_failures:
14
+ - rvm: rbx-19mode
15
+ - rvm: jruby-19mode
16
+ - rvm: 1.9.2
10
17
  - rvm: ruby-head
11
18
 
12
19
  notifications:
13
20
  email:
14
- on_success: change
21
+ on_success: never
15
22
  on_failure: always
data/LICENSE.txt CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2013 Martin Jagusch
1
+ Copyright (c) 2013-2014 Martin Jagusch
2
2
 
3
3
  MIT License
4
4
 
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Octokom
2
2
 
3
- Work in progress command line application for your every day GitHub workflow.
3
+ Create pull requests, GitHub issues etc. quickly from your command line in three words `octokom pull-request create`. Octokom finds automatically the name of your repository and Git branch.
4
4
 
5
5
  ## Installation
6
6
 
@@ -8,10 +8,18 @@ Work in progress command line application for your every day GitHub workflow.
8
8
  $ gem install octokom
9
9
  ```
10
10
 
11
- ## Examples
11
+ ## The Interface
12
12
 
13
- #### Create a new pull request
13
+ Run `octokom --help` to see a list of all supported commands and options.
14
+
15
+ #### Pull Request
14
16
 
15
17
  ```bash
16
18
  $ octokom pull-request create --title "New feature"
17
19
  ```
20
+
21
+ #### GitHub Issue
22
+
23
+ ```bash
24
+ $ octokom issue create --title "Nasty Bug"
25
+ ```
data/bin/octokom CHANGED
@@ -5,4 +5,4 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
5
 
6
6
  require 'octokom'
7
7
 
8
- Octokom.start(*ARGV)
8
+ Octokom::CLI.run
data/lib/octokom.rb CHANGED
@@ -1,15 +1,11 @@
1
- require 'clamp'
2
- require 'octokit'
3
- require 'security'
4
-
5
- require 'octokom/authentication'
6
- require 'octokom/command'
7
- require 'octokom/keychain'
8
- require 'octokom/repository'
9
- require 'octokom/version'
1
+ require 'octokom/core_ext/blank'
10
2
 
11
3
  module Octokom
12
- def self.start(*args)
13
- Octokom::Command.run(args)
14
- end
4
+ autoload :CLI, 'octokom/cli'
5
+ autoload :Client, 'octokom/client'
6
+ autoload :Command, 'octokom/command'
7
+ autoload :Editor, 'octokom/editor'
8
+ autoload :Repository, 'octokom/repository'
9
+ autoload :Shell, 'octokom/shell'
10
+ autoload :VERSION, 'octokom/version'
15
11
  end
@@ -0,0 +1,6 @@
1
+ module Octokom
2
+ class CLI < Command
3
+ sub_command 'issue', 'Create or update GitHub issues.', Issue
4
+ sub_command 'pull-request', 'Create, update or merge pull requests.', PullRequest
5
+ end
6
+ end
@@ -0,0 +1,99 @@
1
+ require 'io/console'
2
+ require 'netrc'
3
+
4
+ module Octokom
5
+ class Client
6
+ private_class_method :new
7
+
8
+ attr_reader :token
9
+
10
+ KEY = 'api.github.com'
11
+ URL = 'https://github.com/mjio/octokom'
12
+
13
+ def self.authenticate
14
+ new.authenticate
15
+ end
16
+
17
+ # Use the existing credentials form the ~/.netrc file to verify API
18
+ # requests to GitHub. When no credentials exist, ask for login and
19
+ # password to request a new token from GitHub.
20
+ def authenticate
21
+ _, @token = netrc[KEY]
22
+
23
+ unless token
24
+ login, password = ask_for_credentials
25
+ create_authorization(login, password)
26
+ end
27
+
28
+ self
29
+ end
30
+
31
+ def client
32
+ require 'octokit'
33
+ Octokit::Client.new(access_token: token)
34
+ end
35
+
36
+ private
37
+
38
+ def path
39
+ @path ||= Netrc.default_path
40
+ end
41
+
42
+ def file
43
+ @file ||= File.open(path, File::RDWR|File::CREAT, 0600)
44
+ end
45
+
46
+ def data
47
+ @data ||= Netrc.parse(Netrc.lex(file.each_line.to_a))
48
+ end
49
+
50
+ def netrc
51
+ @netrc ||= Netrc.new(path, data)
52
+ end
53
+
54
+ def ask_for_credentials
55
+ login = ask_for_login
56
+ password = ask_for_password
57
+ puts # newline
58
+ [login, password]
59
+ end
60
+
61
+ def ask_for_login
62
+ print 'GitHub Login: '
63
+ STDIN.gets.chomp
64
+ end
65
+
66
+ def ask_for_password
67
+ print 'Password: '
68
+ STDIN.noecho(&:gets).chomp
69
+ end
70
+
71
+ def create_authorization(login, password)
72
+ require 'octokit'
73
+
74
+ begin
75
+ client = Octokit::Client.new(login: login, password: password)
76
+ auth = client.create_authorization(scopes: ['repo'], note: api_note, note_url: URL)
77
+ rescue Octokit::Unauthorized
78
+ puts "Unable to authorise user @#{login}"
79
+ exit(1)
80
+ end
81
+
82
+ @token = auth.token
83
+ save_token_to_netrc(login)
84
+ end
85
+
86
+ def save_token_to_netrc(login)
87
+ netrc[KEY] = login, @token
88
+ netrc.save
89
+ end
90
+
91
+ def api_note
92
+ "Octokom #{hostname}@#{Time.now.usec}"
93
+ end
94
+
95
+ def hostname
96
+ `hostname`.chomp
97
+ end
98
+ end
99
+ end
@@ -1,7 +1,70 @@
1
+ require 'clamp'
2
+
3
+ autoload :Issue, 'octokom/command/issue'
4
+ autoload :PullRequest, 'octokom/command/pull_request'
5
+
1
6
  module Octokom
2
7
  class Command < Clamp::Command
3
- autoload :PullRequest, 'octokom/command/pull_request'
8
+ include Octokom::Shell
9
+
10
+ # Shortcut for `Clamp::Command.option` with `:flag` argument.
11
+ #
12
+ # For example:
13
+ # flag ['-a'], A option'
14
+ #
15
+ # Is the same as:
16
+ # option ['-a'], :flag, 'A option'
17
+ def self.flag(tags, desc, &block)
18
+ option_without_type(tags, :flag, desc, &block)
19
+ end
20
+
21
+ # Alias for the `Clamp::Command.option` method that uses the long option
22
+ # tag as type string.
23
+ #
24
+ # For example:
25
+ # Octokom::Command.option ['-a', '--abc'], 'ABC option'
26
+ #
27
+ # Is the same as:
28
+ # Clamp::Command.option ['-a', '--abc'], 'ABC', 'ABC option'
29
+ def self.option_with_type(tags, desc, &block)
30
+ type = tags.first[/^--(.*)$/, 1].upcase
31
+ option_without_type(tags, type, desc, &block)
32
+ end
33
+
34
+ # Override `Clamp::Command.execute` method so it can be used as a block.
35
+ def self.execute(&block)
36
+ define_method(:execute, block) do
37
+ yield if block_given?
38
+ end
39
+ end
40
+
41
+ # Create aliases for the Clamp::Command `option` and `subcommand` methods.
42
+ singleton_class.class_eval do
43
+ alias_method :option_without_type, :option
44
+ alias_method :option, :option_with_type
45
+ alias_method :sub_command, :subcommand
46
+ end
47
+
48
+ flag ['--dry-run', '-y'], 'Do nothing, only show what would happen.'
49
+ flag ['--verbose', '-v'], 'Show the debug output.'
50
+
51
+ flag ['--version'], 'Version number.' do
52
+ puts "Octokom #{Octokom::VERSION}"
53
+ end
54
+
55
+ # Override the Clamp help message option.
56
+ flag ['--help'], 'Help message.' do
57
+ request_help
58
+ end
59
+
60
+ # Initializes a new Repository instance that is available in all
61
+ # Octokom::Command instances.
62
+ def git
63
+ @git ||= Octokom::Repository.new(verbose?)
64
+ end
4
65
 
5
- subcommand 'pull-request', 'Create, update or merge pull requests.', PullRequest
66
+ def authenticate(&block)
67
+ yield Octokom::Client.authenticate
68
+ end
6
69
  end
7
70
  end
@@ -0,0 +1,26 @@
1
+ class Issue < Octokom::Command
2
+ sub_command 'create', 'Create a GitHub issue.' do
3
+ option ['--title', '-t'], 'Title for the issue.'
4
+ option ['--desc', '-d'], 'Description for the issue.'
5
+ option ['--repo', '-r'], 'The GitHub repository you want to update. (e.g. mjio/octokom)'
6
+ flag ['--editor', '-e'], 'Open the editor even when the issue title is set.'
7
+
8
+ execute do
9
+ @repo ||= git.repo_path
10
+
11
+ if @editor || @title.blank?
12
+ @title, @desc = Octokom::Editor.open(git, @title, @desc)
13
+ end
14
+
15
+ if @title.blank?
16
+ error('The title can not be empty')
17
+ end
18
+
19
+ authenticate do |auth|
20
+ task("Create issue #{@repo}") do
21
+ auth.client.create_issue(@repo, @title, @desc) unless dry_run?
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -1,29 +1,38 @@
1
- module Octokom
2
- class Command
3
- class PullRequest < Clamp::Command
4
- include Octokom::Authentication
1
+ class PullRequest < Octokom::Command
2
+ sub_command 'create', 'Create a pull request.' do
3
+ option ['--title', '-t'], 'Title for the pull request.'
4
+ option ['--desc', '-d'], 'Description for the pull request.'
5
+ option ['--base', '-b'], 'The branch you want your changes pulled into. (e.g. master)'
6
+ option ['--head', '-h'], 'The branch where your changes are implemented. (e.g. feature)'
7
+ option ['--repo', '-r'], 'The GitHub repository you want to update. (e.g. mjio/octokom)'
8
+ flag ['--editor', '-e'], 'Open the editor even when the pull request title is set.'
5
9
 
6
- subcommand 'create', 'Create a new pull request.' do
7
- option ['--repo', '-r'], '', 'The GitHub repository. Example: "mjio/octokom"'
8
- # TODO make ancestor default base
9
- option ['--base', '-b'], '', 'The branch you want your changes pulled into. Example: "master"', default: 'master'
10
- option ['--head', '-d'], '', 'The branch where your changes are implemented. Example: "new_feature"'
11
- # option ['--issue', '-i'], '', 'Create pull request from an existing issue. Example: "123"'
12
- option ['--title', '-t'], '', 'Title for the pull request.'
13
- option ['--body', '-b'], '', 'Body for the pull request.'
10
+ execute do
11
+ @base ||= git.base_branch
12
+ @head ||= git.head_branch
13
+ @repo ||= git.repo_path
14
14
 
15
- def execute
16
- authenticate do |client|
17
- repo ||= Repository.remote_path
18
- head ||= Repository.current_branch
15
+ if @base == @head
16
+ error("Base and head branch can not be the same (#{@base}:#{@head})")
17
+ end
18
+
19
+ if @editor || @title.blank?
20
+ @title, @desc = Octokom::Editor.open(git, @title, @desc)
21
+ end
19
22
 
20
- if Repository.unpushed_commits?
21
- exit unless system 'git push'
22
- end
23
+ if @title.blank?
24
+ error('The title can not be empty')
25
+ end
26
+
27
+ if git.pending_commits?
28
+ task('Push latests commits') do
29
+ git.push(@head) unless dry_run?
30
+ end
31
+ end
23
32
 
24
- puts "Create pull request (#{repo}) #{base}:#{head}"
25
- client.create_pull_request(repo, base, head, title, body)
26
- end
33
+ authenticate do |auth|
34
+ task("Create pull-request #{@repo} (#{@base}:#{@head})") do
35
+ auth.client.create_pull_request(@repo, @base, @head, @title, @desc) unless dry_run?
27
36
  end
28
37
  end
29
38
  end
@@ -0,0 +1,51 @@
1
+ # This file is from "activesupport/lib/active_support/core_ext/object/blank.rb"
2
+
3
+ class Object
4
+ def blank?
5
+ respond_to?(:empty?) ? !!empty? : !self
6
+ end
7
+
8
+ def present?
9
+ !blank?
10
+ end
11
+ end
12
+
13
+ class NilClass
14
+ def blank?
15
+ true
16
+ end
17
+ end
18
+
19
+ class FalseClass
20
+ def blank?
21
+ true
22
+ end
23
+ end
24
+
25
+ class TrueClass
26
+ def blank?
27
+ false
28
+ end
29
+ end
30
+
31
+ class Array
32
+ alias_method :blank?, :empty?
33
+ end
34
+
35
+ class Hash
36
+ alias_method :blank?, :empty?
37
+ end
38
+
39
+ class String
40
+ BLANK_RE = /\A[[:space:]]*\z/
41
+
42
+ def blank?
43
+ BLANK_RE === self
44
+ end
45
+ end
46
+
47
+ class Numeric
48
+ def blank?
49
+ false
50
+ end
51
+ end
@@ -0,0 +1,106 @@
1
+ module Octokom
2
+ # The `Octokom::Editor` class opens and parses files where you can insert
3
+ # a title and description for pull-requests or GitHub issues. This class is
4
+ # inspired by the `Pry::Editor` class (http://pry.github.io).
5
+ class Editor
6
+ def self.open(repo, title, desc)
7
+ editor = Editor.new(repo, title, desc)
8
+ editor.prepare_tempfile
9
+ editor.open_editor
10
+ editor.parse_user_input
11
+ end
12
+
13
+ def initialize(repo, title, desc)
14
+ @repo = repo
15
+ @title = title
16
+ @desc = desc
17
+ end
18
+
19
+ # Save a tempfile that includes the input from the command line or the
20
+ # last commit message.
21
+ # TODO Remove #Title and #Description section and use first line; following lines.
22
+ # TODO use last commit message for title
23
+ def prepare_tempfile
24
+ File.open(file_name, 'w') do |f|
25
+ f.puts '# Please enter a title and optional a description below'
26
+ f.puts '# the comments. To continue, save end close the file.'
27
+ f.puts "# Comment lines starting with '#' will be ignored."
28
+ f.puts
29
+ f.puts '# Title'
30
+ f.puts @title
31
+ f.puts
32
+ f.puts '# Description'
33
+ f.puts @desc
34
+ end
35
+ end
36
+
37
+ # Execute the command that is used to start the editor. This includes the
38
+ # waiting flag as well as the file and line number.
39
+ def open_editor
40
+ system("#{editor_name} #{waiting_flag} #{file_inc_line(file_name, 6)}")
41
+ end
42
+
43
+ # Opens the tempfile once again to parse the content and return the `title`
44
+ # and `description`.
45
+ def parse_user_input
46
+ input = {title: [], desc: []}
47
+ parsing = false
48
+
49
+ read_tempfile.each do |line|
50
+ match = /^\s*#\s*(title|desc)/i.match(line)
51
+ parsing = match[1].downcase.to_sym if match
52
+
53
+ unless line =~ /^\s*#/
54
+ input[parsing] << line if parsing
55
+ end
56
+ end
57
+
58
+ input.update(input) { |_, lines| lines.join("\n") }.values
59
+ end
60
+
61
+ private
62
+
63
+ def editor_name
64
+ @editor_name ||= ENV['GIT_EDITOR'] || ENV['VISUAL'] || ENV['EDITOR'] || 'vi'
65
+ end
66
+
67
+ # Make sure the editor waits for the file to be closed.
68
+ def waiting_flag
69
+ case editor_name
70
+ when /^[gm]?vi/
71
+ '--nofork'
72
+ when /^subl/, /^mate/
73
+ '--wait'
74
+ when /^jedit/
75
+ '-wait'
76
+ end
77
+ end
78
+
79
+ # Different editors have different ways how to open a file starting at
80
+ # a specific line number.
81
+ def file_inc_line(file_name, line)
82
+ case editor_name
83
+ when /^[gm]?vi/, /^emacs/, /^nano/, /^pico/, /^gedit/, /^kate/
84
+ "+#{line} #{file_name}"
85
+ when /^mate/, /^geany/
86
+ "--line #{line} #{file_name}"
87
+ when /^subl/
88
+ "#{file_name}:#{line}"
89
+ when /^uedit32/
90
+ "#{file_name}/#{line}"
91
+ when /^jedit/
92
+ "#{file_name} +line:#{line}"
93
+ else
94
+ file_name
95
+ end
96
+ end
97
+
98
+ def file_name
99
+ @file_name ||= "#{@repo.toplevel_path}/.git/OCTOKOM-#{Time.now.usec}"
100
+ end
101
+
102
+ def read_tempfile
103
+ File.open(file_name).map(&:chop).reject(&:empty?)
104
+ end
105
+ end
106
+ end