diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..d1fa1b6 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[*] +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.rb] +indent_style = space +indent_size = 2 diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..2d4a1b4 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,4 @@ +language: ruby +rvm: +- 2.2 +script: rake test:one_by_one diff --git a/README.md b/README.md index de088bb..b9cbe10 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # ghi +[![Build Status](https://travis-ci.org/shubhamshuklaer/ghi.svg?branch=travis-ci)](https://travis-ci.org/shubhamshuklaer/ghi) + GitHub Issues on the command line. Use your `$EDITOR`, not your browser. `ghi` was originally created by [Stephen Celis](https://github.com/stephencelis), and is now maintained by [Alex Chesters](https://github.com/alexchesters). @@ -69,3 +71,47 @@ FAQs can be found in the [wiki](https://github.com/stephencelis/ghi/wiki/FAQ) ## Screenshot ![Example](images/example.png) + +## Testing Guidlines +* You are encouraged to add tests if you are adding new feature or solving some +problem which do not have a test. +* A test file should be named as `something_test.rb` and should be kept in the +`test` folder. A test class should be named `Test_something` and a test +function `test_something`. Helper functions must not start with `test`. +* Before running tests `GITHUB_USER` and `GITHUB_PASSWORD` environment +variables must be exported. It is best to use a fake account as a bug can mess +up your original account. You can either export these 2 environment variables +through `~/.bashrc`(As ghi only uses these while generating its token, so after +you generate the token for your original account(for regular use), fake account +details can be exported) or you can pass it on command line, eg. `rake +test:one_by_one GITHUB_USER='abc' GITHUB_PASSWORD='xyz'`. +* Run `rake test:one_by_one` to run all the tests +* Check [Single Test](https://github.com/grosser/single_test) for better +control over which test to run. Eg. `rake test:assign:un_assign` will run a +test function matching `/un_assign/` in file `assign_test.rb`. One more eg. +`rake test:edit test:assign` will run tests `edit_test.rb` and +`assign_test.rb`. Or you can also use `ruby -I"lib:test" test/file_name.rb -n +method_name` +* By default, the repo and token created while testing will be deleted. But if +you want to see the state of repo and tokens after the test has run, then add +`NO_DELETE_REPO=1` and `NO_DELETE_TOKEN=1` to the command. For eg. `rake +test:assign NO_DELETE_REPO=1 NO_DELETE_TOKEN=1`. +* If you don't wanna run the tests locally use travis-ci. See section below. + +## Enable Travis CI in fork + +* Open a Travis CI account and activate travis-ci for the fork +* Create a fake github account for testing. The username, password and token +will be available to the tests and if by mistake the test prints it, it will be +available in public log. So its best to create a fake account and use a +password you are not using for anything else. Apart from security reasons, +bugs in tests or software can also mess up your original account, so to be +on safe side use a fake account. +* At Travis-CI, on the settings page for the fork, add environment variables +`GITHUB_USER` and `GITHUB_PASSWORD`. Ensure that the "Display value in build +log" is set to false. It is possible to add these in ".travis.yml", but don't +as all forks as well as original repo will be using different accounts(We cannot +provide the details of a common account for testing because of security reasons) for +testing, so it will cause problems during merge. +* Note that the build status badge in the README points to the travis-ci page +for this repo, not the fork. diff --git a/Rakefile b/Rakefile index 45efe62..c0ab85d 100644 --- a/Rakefile +++ b/Rakefile @@ -1,3 +1,5 @@ +require 'single_test/tasks' + desc 'Build the standalone script' task :build do manifest = %w( diff --git a/ghi b/ghi index c41c2c7..ecb55a8 100755 --- a/ghi +++ b/ghi @@ -1085,7 +1085,7 @@ module GHI @token = GHI.config 'ghi.token' end - def authorize! user = username, pass = password, local = true + def authorize! user = username, pass = password, local = true, just_print_token = false return false unless user && pass code ||= nil # 2fa args = code ? [] : [54, "✔\r"] @@ -1106,11 +1106,15 @@ module GHI } @token = res.body['token'] - unless username - system "git config#{' --global' unless local} github.user #{user}" - end + if just_print_token + puts token + else + unless username + system "git config#{' --global' unless local} github.user #{user}" + end - store_token! user, token, local + store_token! user, token, local + end rescue Client::Error => e if e.response['X-GitHub-OTP'] =~ /required/ puts "Bad code." if code @@ -1882,6 +1886,9 @@ EOF opts.on '--local', 'set for local repo only' do assigns[:local] = true end + opts.on '--just_print_token', "Just print the token don't add it to ~/.gitconfig(Useful while testing)" do + assigns[:just_print_token] = true + end opts.on '--auth []' do |username| self.action = 'auth' assigns[:username] = username || Authorization.username @@ -1899,7 +1906,7 @@ EOF if action == 'auth' assigns[:password] = Authorization.password || get_password Authorization.authorize!( - assigns[:username], assigns[:password], assigns[:local] + assigns[:username], assigns[:password], assigns[:local], assigns[:just_print_token] ) end end @@ -1997,6 +2004,9 @@ EOF case action when 'edit' begin + unless args.empty? + assigns[:title], assigns[:body] = args.join(' '), assigns[:title] + end if editor || assigns.empty? i = throb { api.get "/repos/#{repo}/issues/#{issue}" }.body e = Editor.new "GHI_ISSUE_#{issue}.md" @@ -2743,6 +2753,9 @@ EOF if web Web.new(repo).open 'issues/milestones/new' else + unless args.empty? + assigns[:title], assigns[:description] = args.join(' '), assigns[:title] + end if assigns[:title].nil? e = Editor.new 'GHI_MILESTONE.md' message = e.gets format_milestone_editor diff --git a/ghi.gemspec b/ghi.gemspec index df62be9..83eec9f 100644 --- a/ghi.gemspec +++ b/ghi.gemspec @@ -23,4 +23,6 @@ EOF s.add_development_dependency 'rake' s.add_development_dependency 'ronn' + s.add_development_dependency 'typhoeus' + s.add_development_dependency 'single_test' end diff --git a/lib/ghi/authorization.rb b/lib/ghi/authorization.rb index ae6a65c..31595de 100644 --- a/lib/ghi/authorization.rb +++ b/lib/ghi/authorization.rb @@ -15,7 +15,7 @@ def token @token = GHI.config 'ghi.token' end - def authorize! user = username, pass = password, local = true + def authorize! user = username, pass = password, local = true, just_print_token = false return false unless user && pass code ||= nil # 2fa args = code ? [] : [54, "✔\r"] @@ -36,11 +36,15 @@ def authorize! user = username, pass = password, local = true } @token = res.body['token'] - unless username - system "git config#{' --global' unless local} github.user #{user}" - end + if just_print_token + puts token + else + unless username + system "git config#{' --global' unless local} github.user #{user}" + end - store_token! user, token, local + store_token! user, token, local + end rescue Client::Error => e if e.response['X-GitHub-OTP'] =~ /required/ puts "Bad code." if code diff --git a/lib/ghi/commands/config.rb b/lib/ghi/commands/config.rb index 5864ce2..b559313 100644 --- a/lib/ghi/commands/config.rb +++ b/lib/ghi/commands/config.rb @@ -10,6 +10,9 @@ def options opts.on '--local', 'set for local repo only' do assigns[:local] = true end + opts.on '--just_print_token', "Just print the token don't add it to ~/.gitconfig(Useful while testing)" do + assigns[:just_print_token] = true + end opts.on '--auth []' do |username| self.action = 'auth' assigns[:username] = username || Authorization.username @@ -27,7 +30,7 @@ def execute if action == 'auth' assigns[:password] = Authorization.password || get_password Authorization.authorize!( - assigns[:username], assigns[:password], assigns[:local] + assigns[:username], assigns[:password], assigns[:local], assigns[:just_print_token] ) end end diff --git a/lib/ghi/commands/edit.rb b/lib/ghi/commands/edit.rb index 4f46569..fa88e04 100644 --- a/lib/ghi/commands/edit.rb +++ b/lib/ghi/commands/edit.rb @@ -68,6 +68,9 @@ def execute case action when 'edit' begin + unless args.empty? + assigns[:title], assigns[:body] = args.join(' '), assigns[:title] + end if editor || assigns.empty? i = throb { api.get "/repos/#{repo}/issues/#{issue}" }.body e = Editor.new "GHI_ISSUE_#{issue}.md" diff --git a/lib/ghi/commands/milestone.rb b/lib/ghi/commands/milestone.rb index 65956ae..88a9366 100644 --- a/lib/ghi/commands/milestone.rb +++ b/lib/ghi/commands/milestone.rb @@ -139,6 +139,9 @@ def execute if web Web.new(repo).open 'issues/milestones/new' else + unless args.empty? + assigns[:title], assigns[:description] = args.join(' '), assigns[:title] + end if assigns[:title].nil? e = Editor.new 'GHI_MILESTONE.md' message = e.gets format_milestone_editor diff --git a/test/assign_test.rb b/test/assign_test.rb new file mode 100644 index 0000000..cd71330 --- /dev/null +++ b/test/assign_test.rb @@ -0,0 +1,42 @@ +require "test/unit" +require "helper" +require "pp" + +class Test_assign < Test::Unit::TestCase + def setup + gen_token + @repo_name=create_repo + end + + def un_assign issue_no=1 + `#{ghi_exec} assign -d #{issue_no} -- #{@repo_name}` + + response_issue = get_body("repos/#{@repo_name}/issues/#{issue_no}","Issue does not exist") + + assert_equal(nil,response_issue["assignee"],"User not unassigned") + end + + def test_un_assign + open_issue @repo_name + + un_assign + end + + def test_assign + open_issue @repo_name + + un_assign + + `#{ghi_exec} assign -u "#{ENV['GITHUB_USER']}" 1 -- #{@repo_name}` + + response_issue=get_body("repos/#{@repo_name}/issues/1","Issue does not exist") + + assert_not_equal(nil,response_issue["assignee"],"No user assigned") + assert_equal(ENV['GITHUB_USER'],response_issue["assignee"]["login"],"Not assigned to proper user") + end + + def teardown + delete_repo(@repo_name) + delete_token + end +end diff --git a/test/close_test.rb b/test/close_test.rb new file mode 100644 index 0000000..4441b6e --- /dev/null +++ b/test/close_test.rb @@ -0,0 +1,30 @@ +require "test/unit" +require "helper" +require "pp" + +class Test_close < Test::Unit::TestCase + def setup + gen_token + @repo_name=create_repo + end + + def test_close_issue + open_issue @repo_name + comment=get_comment + + `#{ghi_exec} close -m "#{comment}" 1 -- #{@repo_name}` + + response_issue=get_body("repos/#{@repo_name}/issues/1","Issue does not exist") + + assert_equal("closed",response_issue["state"],"Issue not closed") + + response_body=get_body("repos/#{@repo_name}/issues/1/comments","Issue does not exist") + + assert_equal(comment,response_body[-1]["body"],"Close comment text not proper") + end + + def teardown + delete_repo(@repo_name) + delete_token + end +end diff --git a/test/comment_test.rb b/test/comment_test.rb new file mode 100644 index 0000000..29e2c82 --- /dev/null +++ b/test/comment_test.rb @@ -0,0 +1,45 @@ +require "test/unit" +require "helper" +require "pp" + +class Test_comment < Test::Unit::TestCase + def setup + gen_token + @repo_name=create_repo + end + + def test_comment + open_issue @repo_name + create_comment @repo_name + end + + def test_comment_amend + open_issue @repo_name + create_comment @repo_name + + comment=get_comment 1 + + `#{ghi_exec} comment --amend "#{comment}" 1 -- #{@repo_name}` + + response_body=get_body("repos/#{@repo_name}/issues/1/comments","Issue does not exist") + + assert_equal(1,response_body.length,"Comment does not exist") + assert_equal(comment,response_body[-1]["body"],"Comment text not proper") + end + + def test_comment_delete + open_issue @repo_name + create_comment @repo_name + + `#{ghi_exec} comment -D 1 -- #{@repo_name}` + + response_body=get_body("repos/#{@repo_name}/issues/1/comments","Issue does not exist") + + assert_equal(0,response_body.length,"Comment not deleted") + end + + def teardown + delete_repo(@repo_name) + delete_token + end +end diff --git a/test/delete_all_repos.rb b/test/delete_all_repos.rb new file mode 100644 index 0000000..30a5056 --- /dev/null +++ b/test/delete_all_repos.rb @@ -0,0 +1,30 @@ +require "helper" +require "json" +require "pp" + +# This is helpful if your account(fake) gets littered with repos. +# You need to run ruby -I. delete_all_repos.rb. If you are executing from a +# different directory then you need to change the parameter of -I +# appropriately. + +puts "Warning this will delete ALL repositories from the account pointed by GITHUB_USER environment variable" +puts "The account name(login) is #{ENV["GITHUB_USER"]}" +puts "Do you want to continue [N/y]" +option = gets +if option.chop == "y" + puts "Deleting" + while true + response=request("users/#{ENV["GITHUB_USER"]}/repos",:get,{},true) + repos=JSON.load(response.body) + if repos.length == 0 + puts "Exiting" + break + end + repos.each do |repo| + puts "Deleting #{repo["full_name"]}" + delete_repo(repo["full_name"]) + end + end +else + puts 'Not deleting' +end diff --git a/test/edit_test.rb b/test/edit_test.rb new file mode 100644 index 0000000..706c41b --- /dev/null +++ b/test/edit_test.rb @@ -0,0 +1,35 @@ +require "test/unit" +require "helper" +require "pp" + +class Test_edit < Test::Unit::TestCase + def setup + gen_token + @repo_name=create_repo + end + + def test_edit_issue + open_issue @repo_name + + issue=get_issue 1 + + create_milestone @repo_name, 1 + + `#{ghi_exec} edit 1 "#{issue[:title]}" -m "#{issue[:des]}" -L "#{issue[:labels].join(",")}" -M 2 -s open -u "#{ENV['GITHUB_USER']}" -- #{@repo_name}` + + response_issue=get_body("repos/#{@repo_name}/issues/1","Issue does not exist") + + assert_equal(issue[:title],response_issue["title"],"Title not proper") + assert_equal(issue[:des],response_issue["body"],"Descreption not proper") + assert_equal(issue[:labels].uniq.sort,extract_labels(response_issue),"Labels do not match") + assert_equal("open",response_issue["state"],"Issue state not changed") + assert_equal(2,response_issue["milestone"]["number"],"Milestone not proper") + assert_not_equal(nil,response_issue["assignee"],"No user assigned") + assert_equal(ENV['GITHUB_USER'],response_issue["assignee"]["login"],"Not assigned to proper user") + end + + def teardown + delete_repo(@repo_name) + delete_token + end +end diff --git a/test/helper.rb b/test/helper.rb new file mode 100644 index 0000000..e8e664c --- /dev/null +++ b/test/helper.rb @@ -0,0 +1,187 @@ +require "typhoeus" +require "json" +require "shellwords" +require "pp" +require "securerandom" +require "mock_data" +require "test/unit" +require "date" + +def append_token headers + headers.merge(:Authorization=>"token #{ENV["GHI_TOKEN"]}") +end + +def get_url path + "https://api.github.com/#{path}" +end + +def request path, method, options={}, use_basic_auth=true + if options[:params].nil? + options.merge!(:params=>{}) + end + if options[:headers].nil? + options.merge!(:headers=>{}) + end + if options[:body].nil? + options.merge!(:body=>{}) + end + + Typhoeus::Request.new(get_url(path), + method: method, + body: JSON.dump(options[:body]), + params: options[:params], + username: (if use_basic_auth then ENV["GITHUB_USER"] else nil end), + password: (if use_basic_auth then ENV["GITHUB_PASSWORD"] else nil end), + headers: (if use_basic_auth then options[:headers] else append_token(options[:headers]) end) + ).run +end + +def head path, options={} + request(path,:head,options) +end + +def get path, options ={} + request(path,:get,options) +end + +def post path, options ={} + request(path,:post,options) +end + +def delete path, options ={} + request(path,:delete,options) +end + +def delete_repo repo_name + unless ENV["NO_DELETE_REPO"]=="1" + delete("repos/#{repo_name}") + end +end + +def ghi_exec + File.expand_path('../ghi', File.dirname(__FILE__)) +end + +def get_attr index, attr + Shellwords.escape(issues[index][attr]) +end + +def gen_token + ENV["GHI_TOKEN"]=`#{ghi_exec} config --auth --just_print_token`.chop + response=request("users/#{ENV['GITHUB_USER']}",:head,{},false) + + assert_equal('public_repo, repo',response.headers["X-OAuth-Scopes"]) +end + +def delete_token + unless ENV["NO_DELETE_TOKEN"]=="1" + token_info=get_body("authorizations","Impossible api error").detect {|token| token["token_last_eight"] == ENV["GHI_TOKEN"][-8..-1]} + assert_not_equal(nil,token_info,"Token with hash: #{ENV["GHI_TOKEN"]} does not exist") + delete("authorizations/#{token_info["id"]}") + end +end + +def create_repo + repo_name=SecureRandom.uuid + response=post("user/repos",{body:{'name':repo_name}}) + response_body=JSON.load(response.response_body) + assert_not_equal(nil,response_body["name"],"Could not create repo #{repo_name}") + response_body["full_name"] +end + +def get_issue index=0 + if index == -1 + tmp_issues=issues + else + tmp_issues=[issues[index]] + end + for i in 0..(tmp_issues.length-1) + tmp_issues[i][:des].gsub!(/\n/,"
") + # http://stackoverflow.com/questions/12700218/how-do-i-escape-a-single-quote-in-ruby + tmp_issues[i][:des].gsub!(/'/){"\\'"} + end + return (index != -1)?tmp_issues[0]:tmp_issues +end + +def get_comment index=0 + comments[index] +end + +def get_milestone index=0 + milestones[index] +end + +def extract_labels response_issue + tmp_labels=[] + response_issue["labels"].each do |label| + tmp_labels<