├── .gitignore ├── Rakefile ├── lib ├── vim_ext.rb ├── patch.rb ├── patch │ └── chunk.rb ├── github.rb └── code_review.rb ├── plugin └── codereview.vim ├── autoload └── codereview.vim ├── test ├── fixtures │ └── example.patch ├── patch_test.rb └── patch │ └── chunk_test.rb ├── doc └── codereview.txt └── README.markdown /.gitignore: -------------------------------------------------------------------------------- 1 | /doc/tags 2 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "rake/testtask" 2 | 3 | Rake::TestTask.new do |t| 4 | t.libs << "test" 5 | t.test_files = FileList['test/**/*_test.rb'] 6 | t.verbose = true 7 | end 8 | 9 | task :default => :test -------------------------------------------------------------------------------- /lib/vim_ext.rb: -------------------------------------------------------------------------------- 1 | module Vim 2 | class Buffer 3 | class << self 4 | include Enumerable 5 | 6 | def each 7 | count.times.each { |i| yield self[i] } 8 | end 9 | end 10 | 11 | def reload 12 | Buffer.detect { |buf| buf == self } 13 | end 14 | 15 | def ==(other) 16 | name == other.name && number == other.number 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /plugin/codereview.vim: -------------------------------------------------------------------------------- 1 | if !exists('g:CODEREVIEW_GITHUB_DOMAIN') 2 | let g:CODEREVIEW_GITHUB_DOMAIN = 'github.com' 3 | end 4 | if !exists('g:CODEREVIEW_INSTALL_PATH') 5 | let g:CODEREVIEW_INSTALL_PATH = fnamemodify(expand(""), ":p:h") 6 | end 7 | 8 | command! -nargs=1 CodeReview call codereview#Review() 9 | command! CodeReviewComment call codereview#NewComment() 10 | command! CodeReviewCommentChange call codereview#NewChangeComment() 11 | command! CodeReviewReloadComments call codereview#ReloadComments() -------------------------------------------------------------------------------- /autoload/codereview.vim: -------------------------------------------------------------------------------- 1 | if has('ruby') 2 | ruby $: << File.expand_path(File.join(Vim.evaluate('g:CODEREVIEW_INSTALL_PATH'), '..', 'lib')) 3 | ruby require 'code_review' 4 | 5 | fun! codereview#Review(url) 6 | ruby CodeReview.review Vim.evaluate("a:url") 7 | endfun 8 | 9 | fun! codereview#NewChangeComment() 10 | ruby CodeReview.current.new_change_comment 11 | endfun 12 | 13 | fun! codereview#NewComment() 14 | ruby CodeReview.current.new_comment 15 | endfun 16 | 17 | fun! codereview#ReloadComments() 18 | ruby CodeReview.current.reload_comments 19 | endfun 20 | else 21 | fun! codereview#Review() 22 | echo "Sorry, codereview.vim requires vim to be built with Ruby support." 23 | endfun 24 | 25 | fun! codereview#Comment() 26 | echo "Sorry, codereview.vim requires vim to be built with Ruby support." 27 | endfun 28 | endif 29 | -------------------------------------------------------------------------------- /lib/patch.rb: -------------------------------------------------------------------------------- 1 | require_relative 'patch/chunk' 2 | 3 | class Patch 4 | ProcessingError = Class.new(StandardError) 5 | Location = Struct.new(:commit_id, :path, :position) 6 | 7 | def initialize(patch) 8 | @patch = patch 9 | end 10 | 11 | def find_addition(filename, subjective_line, text) 12 | find_change(filename, subjective_line, :+, text) 13 | end 14 | 15 | def find_deletion(filename, subjective_line, text) 16 | find_change(filename, subjective_line, :-, text) 17 | end 18 | 19 | def find_change(filename, subjective_line, kind, text) 20 | chunks 21 | .select { |chunk| chunk.filename == filename } 22 | .reverse 23 | .each { |chunk| 24 | if location = chunk.find_change(subjective_line, kind, text) 25 | return location 26 | end 27 | } 28 | raise ProcessingError, "Couldn't find that line in the diff patch. Remember that you can only comment on additions or deletions." 29 | end 30 | 31 | private 32 | 33 | def chunks 34 | @chunks ||= Chunk.from_patch(@patch) 35 | end 36 | end -------------------------------------------------------------------------------- /test/fixtures/example.patch: -------------------------------------------------------------------------------- 1 | From 973a92b0ccf2291085d4b76ad619617288b42a73 Mon Sep 17 00:00:00 2001 2 | From: "Ed Balls" 3 | Date: Thu, 7 Nov 2013 14:41:12 +0100 4 | Subject: [PATCH 1/2] Fix the world 5 | 6 | --- 7 | app/controllers/foo_controller.rb | 4 +--- 8 | app/helpers/foo_helper.rb | 2 ++ 9 | 2 files changed, 3 insertion(+), 3 deletions(-) 10 | 11 | diff --git a/app/controllers/foo_controller.rb b/app/controllers/foo_controller.rb 12 | index 663e1e6..8c0ca2b 100644 13 | --- a/app/controllers/foo_controller.rb 14 | +++ b/app/controllers/foo_controller.rb 15 | @@ -1,6 +1,6 @@ 16 | # -*- encoding : utf-8 -*- 17 | class FooController 18 | def index 19 | @foo = Foo.new 20 | - render :bar 21 | + render :foo 22 | end 23 | @@ -8,4 +8,2 @@ 24 | def update 25 | - do_bar 26 | - do_baz 27 | end 28 | diff --git a/app/helpers/foo_helper.rb b/app/helpers/foo_helper.rb 29 | index c8eccd6..458c2f3 100644 30 | --- a/app/helpers/foo_helper.rb 31 | +++ b/app/helpers/foo_helper.rb 32 | @@ -1,3 +1,5 @@ 33 | # -*- encoding : utf-8 -*- 34 | module FooHelper 35 | + def render_foos 36 | + end 37 | end 38 | -- 39 | 1.8.5-rc3 40 | From 124a92b0ccf2291085d4b76ad619617288b42a73 Mon Sep 17 00:00:00 2001 41 | From: "Ed Balls" 42 | Date: Thu, 7 Nov 2013 14:52:23 +0100 43 | Subject: [PATCH 2/2] Fix the world again 44 | 45 | --- 46 | app/controllers/foo_controller.rb | 2 +- 47 | app/controllers/bogus_controller.rb | 2 -- 48 | 2 files changed, 1 insertion(+), 3 deletions(-) 49 | 50 | diff --git a/app/controllers/foo_controller.rb b/app/controllers/foo_controller.rb 51 | index 663e1e6..8c0ca2b 100644 52 | --- a/app/controllers/foo_controller.rb 53 | +++ b/app/controllers/foo_controller.rb 54 | @@ -1,6 +1,6 @@ 55 | # -*- encoding : utf-8 -*- 56 | class FooController 57 | def index 58 | - @foo = Foo.new 59 | + @foo = Foo.new(arguments) 60 | render :foo 61 | end 62 | -- 63 | 1.8.5-rc3 64 | -------------------------------------------------------------------------------- /test/patch_test.rb: -------------------------------------------------------------------------------- 1 | gem 'minitest' 2 | $: << 'lib' 3 | require 'patch' 4 | require 'minitest/autorun' 5 | 6 | describe Patch do 7 | def loc(commit, filename, line) 8 | Patch::Location.new(commit, filename, line) 9 | end 10 | 11 | let(:fixture) { File.expand_path('fixtures/example.patch', File.dirname(__FILE__)) } 12 | let(:first_commit) { "973a92b0ccf2291085d4b76ad619617288b42a73" } 13 | let(:last_commit) { "124a92b0ccf2291085d4b76ad619617288b42a73" } 14 | let(:patch) { Patch.new(File.read(fixture)) } 15 | 16 | it 'finds the line of an addition in a first hunk' do 17 | patch.find_addition( 18 | "app/controllers/foo_controller.rb", 5, 19 | " render :foo" 20 | ).must_equal loc(first_commit, 'app/controllers/foo_controller.rb', 6) 21 | end 22 | 23 | it 'finds the line of a deletion in a second hunk' do 24 | patch.find_deletion( 25 | "app/controllers/foo_controller.rb", 10, 26 | " do_baz" 27 | ).must_equal loc(first_commit, 'app/controllers/foo_controller.rb', 11) 28 | end 29 | 30 | it 'finds the line of an addition in another file' do 31 | patch.find_addition( 32 | "app/helpers/foo_helper.rb", 3, 33 | " def render_foos" 34 | ).must_equal loc(first_commit, 'app/helpers/foo_helper.rb', 3) 35 | end 36 | 37 | it 'finds the line of a deletion in another commit' do 38 | patch.find_deletion( 39 | "app/controllers/foo_controller.rb", 4, 40 | " @foo = Foo.new" 41 | ).must_equal loc(last_commit, 'app/controllers/foo_controller.rb', 4) 42 | end 43 | 44 | it 'rejects finding changes on context lines' do 45 | proc { 46 | patch.find_addition( 47 | "app/helpers/foo_helper", 2, "module FooHelper" 48 | ) 49 | }.must_raise Patch::ProcessingError 50 | end 51 | 52 | it 'rejects bogus patches' do 53 | bogus_patch = Patch.new("foo\nbar\nbaz") 54 | 55 | proc { 56 | bogus_patch.find_addition( 57 | "app/controllers/foo_controller.rb", 5, 58 | "foo" 59 | ) 60 | }.must_raise Patch::ProcessingError 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/patch/chunk.rb: -------------------------------------------------------------------------------- 1 | class Patch 2 | class Chunk 3 | class Part 4 | def initialize(offset, line, body) 5 | @offset = offset 6 | @line = line 7 | @lines = body.each_line.to_a 8 | end 9 | 10 | def find_change(subjective_line, kind, text) 11 | index = subjective_line - @line 12 | while @lines[index] && @lines[index].chomp != "#{kind}#{text}".chomp 13 | index += 1 14 | end 15 | return @offset + index if @lines[index] 16 | end 17 | end 18 | 19 | def self.from_patch(patch) 20 | commit = nil 21 | patch 22 | .split(/^From ([a-f0-9]{40})/).drop(1) 23 | .chunk { |line| !!(line =~ /^[a-f0-9]{40}$/) } 24 | .map { |(is_commit, elements)| 25 | if is_commit 26 | commit = elements.first 27 | nil 28 | else 29 | elements.join("\n").split(/^diff --git /).drop(1).map do |raw_chunk| 30 | filename = raw_chunk.scan(/^a\/(.*) b\/.*$/).first.first 31 | body = raw_chunk.split(/--- .*\n\+\+\+ .*\n/) 32 | .drop(1).first 33 | .gsub(/--\n\d\.\d.*/m, '').chomp 34 | new(commit, filename, body) 35 | end 36 | end 37 | }.compact.flatten 38 | end 39 | 40 | attr_reader :filename 41 | 42 | def initialize(commit, filename, body) 43 | @commit = commit 44 | @filename = filename 45 | @parts = body.split("@@ -").drop(1).map { |raw_part| 46 | header = raw_part.split("\n").first 47 | offset = body.split("\n").index("@@ -#{header}") 48 | line = raw_part.scan(/^(\d+),\d+ \+(\d+),/).first.map(&:to_i).max 49 | Part.new(offset, line, "@@ -" + raw_part) 50 | } 51 | end 52 | 53 | def find_change(subjective_line, kind, text) 54 | @parts.each do |part| 55 | if chunk_offset = part.find_change(subjective_line, kind, text) 56 | return Location.new(@commit, @filename, chunk_offset) 57 | end 58 | end 59 | nil 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /test/patch/chunk_test.rb: -------------------------------------------------------------------------------- 1 | gem 'minitest' 2 | $: << 'lib' 3 | require 'patch' 4 | require 'minitest/autorun' 5 | 6 | describe Patch::Chunk do 7 | def loc(commit, filename, offset) 8 | Patch::Location.new(commit, filename, offset) 9 | end 10 | 11 | let(:fixture) { File.expand_path('../fixtures/example.patch', File.dirname(__FILE__)) } 12 | let(:patch) { File.read(fixture) } 13 | 14 | describe '.from_patch' do 15 | it 'divides the patch in chunks' do 16 | Patch::Chunk.from_patch(patch).length.must_equal 3 17 | end 18 | 19 | it 'gets the filename from each chunk' do 20 | Patch::Chunk.from_patch(patch).map(&:filename).must_equal %w( 21 | app/controllers/foo_controller.rb 22 | app/helpers/foo_helper.rb 23 | app/controllers/foo_controller.rb 24 | ) 25 | end 26 | end 27 | 28 | describe '#find_change' do 29 | let(:first_chunk) { Patch::Chunk.from_patch(patch).first } 30 | let(:last_chunk) { Patch::Chunk.from_patch(patch).last } 31 | 32 | it 'finds an addition and returns the offset' do 33 | first_chunk.find_change(5, :+, " render :foo").must_equal( 34 | loc( 35 | '973a92b0ccf2291085d4b76ad619617288b42a73', 36 | 'app/controllers/foo_controller.rb', 6 37 | ) 38 | ) 39 | end 40 | 41 | it 'finds a deletion' do 42 | first_chunk.find_change(5, :-, " render :bar").must_equal( 43 | loc( 44 | '973a92b0ccf2291085d4b76ad619617288b42a73', 45 | 'app/controllers/foo_controller.rb', 5 46 | ) 47 | ) 48 | end 49 | 50 | it 'finds a deletion in later parts of the chunk' do 51 | first_chunk.find_change(10, :-, " do_baz").must_equal( 52 | loc( 53 | '973a92b0ccf2291085d4b76ad619617288b42a73', 54 | 'app/controllers/foo_controller.rb', 11 55 | ) 56 | ) 57 | end 58 | 59 | it 'finds an addition in a different commit' do 60 | last_chunk.find_change(5, :+, " @foo = Foo.new(arguments)").must_equal( 61 | loc( 62 | '124a92b0ccf2291085d4b76ad619617288b42a73', 63 | 'app/controllers/foo_controller.rb', 5 64 | a 65 | ) 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /doc/codereview.txt: -------------------------------------------------------------------------------- 1 | *codereview.txt* GitHub Pull Request-based Code Reviews 2 | 3 | Version: 0.1 4 | Author: Josep M. Bach 5 | Sponsor: Codegram Technologies 6 | License: MIT license {{{ 7 | Permission is hereby granted, free of charge, to any person obtaining 8 | a copy of this software and associated documentation files (the 9 | "Software"), to deal in the Software without restriction, including 10 | without limitation the rights to use, copy, modify, merge, publish, 11 | distribute, sublicense, and/or sell copies of the Software, and to 12 | permit persons to whom the Software is furnished to do so, subject to 13 | the following conditions: 14 | The above copyright notice and this permission notice shall be included 15 | in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 18 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 20 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 21 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 22 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 23 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | }}} 25 | 26 | INTRODUCTION *codereview* 27 | 28 | With codereview you can review Pull Requests on GitHub right from Vim, as 29 | well as comment on specific lines of the pull request or in the general PR 30 | comments. 31 | 32 | Since it builds upon the great |patchreview| and adds some GitHub-related 33 | convenience, it needs the patchreview Vim plug-in to be installed. 34 | 35 | INSTALL *codereview-install* 36 | 37 | Make sure you have compiled Vim with Ruby support and the Ruby you compiled it 38 | with is 1.9+ compatible. 39 | 40 | Also, you'll need `curl` installed. 41 | 42 | If you use Vundle put this in your vimrc: 43 | 44 | Bundle 'junkblocker/patchreview-vim' 45 | Bundle 'codegram/vim-codereview' 46 | 47 | If you use Pathogen, clone this repo in your `~/.vim/bundle` directory. 48 | 49 | USAGE *codereview-usage* 50 | 51 | Make sure you're in the correct folder for the Git repository you want to 52 | review the PR on. 53 | 54 | To start a code review for a specific pull request: 55 | > 56 | :CodeReview https://github.com/myorganization/myrepo/pulls/1328 57 | < 58 | 59 | codereview will now download the Pull Request patch, stash your 60 | current changes, and checkout the PR's base SHA. Then it'll open every 61 | changed file in a new tab. 62 | 63 | The first time, it'll ask you for a GitHub authorization token. You can 64 | generate those from your Applications settings in your GitHub account page. 65 | 66 | *g:CODEREVIEW_GITHUB_DOMAIN* 67 | g:CODEREVIEW_GITHUB_DOMAIN 68 | If using codereview against a GitHub Enterprise environment, set this 69 | to the domain that the instance is served on. 70 | 71 | vim:tw=78:ts=8:ft=help:norl: 72 | -------------------------------------------------------------------------------- /lib/github.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | require 'timeout' 3 | 4 | class Github 5 | TimeoutError = Class.new(Timeout::Error) 6 | TIMEOUT = 10 7 | 8 | TOKEN_PATH = File.expand_path("~/.codereview") 9 | 10 | class Comment 11 | attr_reader :author, :body, :created_at 12 | def initialize(hash) 13 | @author = hash["user"]["login"] 14 | @body = hash["body"] 15 | @created_at = hash["created_at"] 16 | end 17 | 18 | def to_s 19 | "@#{author}\n#{'-' * (author.length+1)}\n#{body}" 20 | end 21 | end 22 | 23 | def initialize(pull_request_url) 24 | @url = pull_request_url 25 | end 26 | 27 | def get_comments 28 | user, repo, pull = url_info 29 | @comments_path ||= Tempfile.new("review-#{user}-#{repo}-#{pull}-comments.json").path 30 | curl %Q{-H "Accept: application/json" #{base_api_url(true)}/comments -o #{@comments_path}} 31 | JSON.load(File.read(@comments_path)).map(&Comment.method(:new)) 32 | end 33 | 34 | def post_change_comment(contents, location) 35 | body = JSON.dump({ 36 | body: contents, 37 | commit_id: location.commit_id, 38 | path: location.path, 39 | position: location.position 40 | }) 41 | 42 | curl %Q{-X POST -H "Accept: application/json" -H "Content-type: application/json" #{base_api_url}/comments -d '#{body}'} 43 | :OK 44 | end 45 | 46 | def post_comment(contents) 47 | body = JSON.dump({ 48 | body: contents 49 | }) 50 | 51 | curl %Q{-X POST -H "Accept: application/json" -H "Content-type: application/json" #{base_api_url(true)}/comments -d '#{body}'} 52 | :OK 53 | end 54 | 55 | 56 | def patch_path 57 | @patch_path ||= download_pull_request("application/vnd.github.v3.patch") 58 | end 59 | 60 | def pull_request_data 61 | @pull_request_data ||= begin 62 | path = download_pull_request("application/json") 63 | data = JSON.parse(File.read(path)) 64 | { 65 | :head => data["head"]["sha"], 66 | :base => data["base"]["sha"], 67 | :merged => data["merged"] 68 | } 69 | end 70 | end 71 | 72 | private 73 | attr_reader :url 74 | 75 | def token 76 | @token ||= begin 77 | if File.exist?(TOKEN_PATH) 78 | File.read(TOKEN_PATH) 79 | else 80 | token = Vim.evaluate("input('Create a GitHub authorization token and paste it here: ')") 81 | File.open(TOKEN_PATH, "w") do |file| 82 | file.write token 83 | end 84 | token 85 | end 86 | end 87 | end 88 | 89 | def download_pull_request(content_type) 90 | user, repo, pull = url_info 91 | temp = Tempfile.new("review-#{user}-#{repo}-#{pull}.patch") 92 | puts "Downloading Pull Request #{user}/#{repo}##{pull}..." 93 | curl %Q{-H "Accept: #{content_type}" -L -o #{temp.path} #{base_api_url}} 94 | temp.path 95 | end 96 | 97 | def url_info 98 | url.scan(/#{Vim.evaluate('g:CODEREVIEW_GITHUB_DOMAIN')}\/(.*)\/(.*)\/pull\/(\d+)/).first 99 | end 100 | 101 | def base_api_url(issue=false) 102 | user, repo, pull = url_info 103 | api_endpoint = if Vim.evaluate('g:CODEREVIEW_GITHUB_DOMAIN') == 'github.com' 104 | 'api.github.com' 105 | else 106 | Vim.evaluate('g:CODEREVIEW_GITHUB_DOMAIN') + '/api/v3' 107 | end 108 | 109 | "https://#{api_endpoint}/repos/#{user}/#{repo}/#{issue ? 'issues' : 'pulls'}/#{pull}" 110 | end 111 | 112 | def curl(args) 113 | Timeout.timeout(TIMEOUT) { `curl --silent -H "Authorization: token #{token}" #{args}` } 114 | rescue Timeout::Error=>e 115 | raise TimeoutError, e.message 116 | end 117 | end 118 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | # vim-codereview 2 | 3 | ## GitHub Pull Request-based Code Reviews 4 | 5 | WARNING: A bit rough on the edges. I'm polishing it as I use it more. 6 | 7 | With *codereview* you can review Pull Requests on GitHub right from Vim, as 8 | well as comment on specific lines of the pull request or in the general PR 9 | comments. 10 | 11 | Since it builds upon the great *patchreview* and adds some GitHub-related 12 | convenience, it needs the *patchreview* Vim plug-in to be installed. 13 | 14 | ## Install 15 | 16 | Make sure you have compiled Vim with Ruby support and the Ruby you compiled it 17 | with is 1.9+ compatible. 18 | 19 | Also, you'll need `curl` installed. 20 | 21 | If you use Vundle put this in your vimrc: 22 | 23 | ``` 24 | Bundle 'junkblocker/patchreview-vim' 25 | Bundle 'codegram/vim-codereview' 26 | ``` 27 | 28 | If you use Pathogen, clone this repo in your `~/.vim/bundle` directory. 29 | 30 | ## Screencast 31 | 32 | For a quick live demo, check out this screencast: 33 | 34 | [![screencast](http://img.youtube.com/vi/1KaTY9AA48w/0.jpg)](http://youtu.be/1KaTY9AA48w) 35 | 36 | ## Usage 37 | 38 | Make sure you're in the correct folder for the Git repository you want to 39 | review the PR on. 40 | 41 | To start a code review for a specific pull request: 42 | 43 | ``` 44 | :CodeReview https://github.com/myorganization/myrepo/pulls/1328 45 | ``` 46 | 47 | *codereview* will now download the Pull Request patch, *stash your 48 | current changes* and *checkout the PR's base SHA*. Then it'll open every 49 | changed file in a new tab. 50 | 51 | The first time, it'll ask you for a GitHub authorization token. You can 52 | generate those from your Applications settings in your GitHub account page. 53 | More information available on [Github help](https://help.github.com/articles/creating-an-access-token-for-command-line-use/) 54 | 55 | If you need to change the token later on, you can find it under `~/.codereview`, 56 | or you can remove the file and be prompted for a token again. Please note 57 | this file is stored in plaintext. Contributions to store the key in the local 58 | keychain or encrypt it with GPG are very welcome. 59 | 60 | You'll be now on the Overview tab. Keep reading. 61 | 62 | ### The Overview tab 63 | 64 | Here you'll see a list of comments on the Pull Request itself (**not on the 65 | diff**). If you want to add a comment to this list, see "Commenting on the 66 | whole Pull Request" below. 67 | 68 | But for now you probably want to review some code. Switch through the different 69 | tabs to see all the changes. Once you see a specific change on the diff that 70 | you want to give your feedback on, you'll want to leave a constructive comment, 71 | right? Keep reading to learn how to do it. 72 | 73 | ### Commenting on a specific line 74 | 75 | When reviewing code in the diff tabs, you can go to any line and comment on any 76 | addition or deletion by issuing the `:CodeReviewCommentChange` command (you can 77 | map it to whatever you'd like). You can only comment on additions or deletions, 78 | not context lines. 79 | 80 | A new split will appear where you can write your comment, and when you're done, 81 | just press `c` to post your comment. 82 | 83 | ### Commenting on the whole Pull Request 84 | 85 | When you're done nitpicking on your colleague's diff, you can comment on the 86 | whole Pull Request to give them a +1 or a :ship: :it: or whatever by issuing 87 | `:CodeReviewComment` command (you can map it to whatever you'd like). 88 | 89 | A new split will appear where you can write your comment, and when you're done, 90 | just press `c` to post your comment. 91 | 92 | ### Reloading the comments 93 | 94 | If you want to fetch the newest comments for the PR you're reviewing, just 95 | issue `:CodeReviewReloadComments`! You'll be taken to the Overview tab with a, 96 | new, fresh list of comments. 97 | -------------------------------------------------------------------------------- /lib/code_review.rb: -------------------------------------------------------------------------------- 1 | require 'tempfile' 2 | require 'json' 3 | require_relative 'patch' 4 | require_relative 'github' 5 | require_relative 'vim_ext' 6 | 7 | class CodeReview 8 | def self.current 9 | @current 10 | end 11 | 12 | def self.review(url) 13 | @current = new(url) 14 | @current.review 15 | end 16 | 17 | def initialize(url) 18 | @github = Github.new(url) 19 | end 20 | 21 | def review 22 | `git stash --include-untracked; git checkout #{pull_request_data[:base]}` 23 | Vim.command "PatchReview #{patch_path}" 24 | Vim.command "file Overview" 25 | Vim.command "setlocal buftype=nofile" 26 | reload_comments 27 | end 28 | 29 | def new_change_comment 30 | contents = File.read(patch_path) 31 | 32 | win0 = VIM::Window[0] 33 | win1 = VIM::Window[1] 34 | win2 = VIM::Window[2] 35 | filename = (win0.buffer.name || win1.buffer.name || win2.buffer.name).gsub(Vim.evaluate('getcwd()') + '/', '') 36 | current_file = VIM::Buffer.current.name ? :original : :patched 37 | line_number = VIM::Buffer.current.line_number 38 | text = VIM::Buffer.current[line_number].chomp 39 | 40 | patch = Patch.new(contents) 41 | @location = if current_file == :original 42 | patch.find_deletion(filename, line_number, text) 43 | else 44 | patch.find_addition(filename, line_number, text) 45 | end 46 | 47 | Vim.command "vsplit New_Change_Comment" 48 | Vim.command "normal! ggdG" 49 | Vim.command "setlocal buftype=nofile" 50 | Vim.command "silent nnoremap c :ruby CodeReview.current.create_change_comment" 51 | Vim.command %Q{echo "Write your commit message in this window, then type c when you're done. And be constructive! :)"} 52 | end 53 | 54 | def create_change_comment 55 | if !@location 56 | raise ArgumentError, "Can't create a comment from a non-comment buffer. Call :CodeReviewComment first." 57 | end 58 | 59 | buf = VIM::Buffer.current 60 | contents = buf.count.times.map { |i| buf[i+1] }.join("\n") 61 | Vim.command %Q{echo "Posting comment to GitHub..."} 62 | github.post_change_comment(contents, @location) 63 | Vim.command "bd" 64 | Vim.command %Q{echo "Comment posted successfully."} 65 | end 66 | 67 | def new_comment 68 | Vim.command "vsplit New_Comment" 69 | Vim.command "normal! ggdG" 70 | Vim.command "setlocal buftype=nofile" 71 | Vim.command "silent nnoremap c :ruby CodeReview.current.create_comment" 72 | Vim.command %Q{echo "Write your commit message in this window, then type c when you're done. And be constructive! :)"} 73 | end 74 | 75 | def create_comment 76 | Vim.command %Q{echo "Posting comment to GitHub..."} 77 | buf = VIM::Buffer.current 78 | contents = buf.count.times.map { |i| buf[i+1] }.join("\n") 79 | github.post_comment(contents) 80 | Vim.command "bd" 81 | Vim.command %Q{echo "Comment posted successfully."} 82 | reload_comments 83 | end 84 | 85 | def reload_comments 86 | Vim.command "tabfirst" 87 | Vim.command "tabnext" until VIM::Buffer.current == overview_buffer 88 | Vim.command "%d" 89 | (["PULL REQUEST COMMENTS", "Type :CodeReviewComment to add yours", ""] + render_comments.split("\n")).each_with_index do |line, idx| 90 | overview_buffer.append(idx, line) 91 | end 92 | end 93 | 94 | private 95 | attr_reader :github 96 | 97 | def overview_buffer 98 | @overview_buffer ||= begin 99 | idx = VIM::Buffer.count.times.detect { |i| VIM::Buffer[i].name =~ /Overview$/ } 100 | idx ? VIM::Buffer[idx] : raise(RuntimeError, "Can't find Overview buffer -- did you close it?") 101 | end 102 | end 103 | 104 | def render_comments 105 | github.get_comments.map(&:to_s).join("\n\n") 106 | end 107 | 108 | def patch_path 109 | github.patch_path 110 | end 111 | 112 | def pull_request_data 113 | github.pull_request_data 114 | end 115 | end 116 | --------------------------------------------------------------------------------