gh-diff 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +24 -0
- data/.rspec +2 -0
- data/.travis.yml +3 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.ja.md +298 -0
- data/README.md +113 -0
- data/Rakefile +7 -0
- data/bin/gh-diff +5 -0
- data/gh-diff.gemspec +35 -0
- data/lib/gh-diff.rb +96 -0
- data/lib/gh-diff/auth.rb +25 -0
- data/lib/gh-diff/cli.rb +165 -0
- data/lib/gh-diff/option.rb +40 -0
- data/lib/gh-diff/version.rb +3 -0
- data/spec/cassettes/auth.yml +63 -0
- data/spec/cassettes/dir.yml +74 -0
- data/spec/cassettes/docs.yml +145 -0
- data/spec/cassettes/nonexist.yml +63 -0
- data/spec/cassettes/quickstart.yml +147 -0
- data/spec/cassettes/ref-tag.yml +76 -0
- data/spec/cassettes/ref.yml +76 -0
- data/spec/cassettes/save-diff.yml +147 -0
- data/spec/cassettes/save-diffs.yml +291 -0
- data/spec/cli_spec.rb +102 -0
- data/spec/fixtures/docs/migrations.md +13 -0
- data/spec/fixtures/docs/quickstart.md +26 -0
- data/spec/fixtures/ja-docs/quickstart.ja.md +50 -0
- data/spec/gh-diff_spec.rb +138 -0
- data/spec/option_spec.rb +57 -0
- data/spec/spec_helper.rb +33 -0
- metadata +246 -0
data/Rakefile
ADDED
data/bin/gh-diff
ADDED
data/gh-diff.gemspec
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'gh-diff/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "gh-diff"
|
8
|
+
spec.version = GhDiff::VERSION
|
9
|
+
spec.authors = ["kyoendo"]
|
10
|
+
spec.email = ["postagie@gmail.com"]
|
11
|
+
spec.summary = %q{Take diffs between local and a github repository files.}
|
12
|
+
spec.description = %q{Take diffs between local and a github repository files.}
|
13
|
+
spec.homepage = "https://github.com/melborne/gh-diff"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.required_ruby_version = ">= 2.0.0"
|
22
|
+
|
23
|
+
spec.add_dependency "togglate", ">= 0.1.2"
|
24
|
+
spec.add_dependency "octokit"
|
25
|
+
spec.add_dependency "dotenv"
|
26
|
+
spec.add_dependency "thor"
|
27
|
+
spec.add_dependency "diffy"
|
28
|
+
|
29
|
+
spec.add_development_dependency "bundler", "~> 1.6"
|
30
|
+
spec.add_development_dependency "rake"
|
31
|
+
spec.add_development_dependency "rspec"
|
32
|
+
spec.add_development_dependency "webmock"
|
33
|
+
spec.add_development_dependency "vcr"
|
34
|
+
spec.add_development_dependency "tildoc", ">= 0.0.2"
|
35
|
+
end
|
data/lib/gh-diff.rb
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
require "gh-diff/version"
|
2
|
+
require "gh-diff/cli"
|
3
|
+
require "gh-diff/option"
|
4
|
+
require "gh-diff/auth"
|
5
|
+
|
6
|
+
require "base64"
|
7
|
+
require "octokit"
|
8
|
+
require "diffy"
|
9
|
+
require "togglate"
|
10
|
+
|
11
|
+
module GhDiff
|
12
|
+
class Diff
|
13
|
+
attr_accessor :repo, :revision, :dir
|
14
|
+
def initialize(repo, revision:'master', dir:nil)
|
15
|
+
@repo = repo
|
16
|
+
@revision = revision
|
17
|
+
@dir = dir
|
18
|
+
end
|
19
|
+
|
20
|
+
def get(file, repo:@repo, revision:@revision, dir:@dir, **opts)
|
21
|
+
path = build_path(dir, file)
|
22
|
+
f = get_contents(repo, path, revision)
|
23
|
+
Base64.decode64(f.content)
|
24
|
+
end
|
25
|
+
|
26
|
+
def diff(file1, file2=file1, commentout:false,
|
27
|
+
comment_tag:'original', **opts)
|
28
|
+
opts = {context:3}.merge(opts)
|
29
|
+
is_dir = File.directory?(file1)
|
30
|
+
|
31
|
+
file_pairs = build_file_pairs(file1, file2, dir:is_dir)
|
32
|
+
diffs = parallel(file_pairs) { |file1, file2|
|
33
|
+
_diff(file1, file2, commentout, comment_tag, opts) }
|
34
|
+
diffs
|
35
|
+
end
|
36
|
+
|
37
|
+
def dir_diff(directory, repo:@repo, revision:@revision, dir:@dir)
|
38
|
+
local_files = Dir.glob("#{directory}/*").map { |f| File.basename f }
|
39
|
+
remote_path = build_path(dir, directory)
|
40
|
+
remote_files = get_contents(repo, remote_path, revision).map(&:name)
|
41
|
+
added = remote_files - local_files
|
42
|
+
removed = local_files - remote_files
|
43
|
+
[added, removed]
|
44
|
+
end
|
45
|
+
|
46
|
+
def ref(ref='master', repo:@repo)
|
47
|
+
type = ref.match(/^v\d/) ? :tags : :heads
|
48
|
+
get_ref(repo, "#{type}/#{ref}")
|
49
|
+
rescue Octokit::NotFound
|
50
|
+
{ref:'', object:{sha:ref}}
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
def build_path(dir, file)
|
55
|
+
(dir.nil? || dir.empty?) ? file : File.join(dir, file)
|
56
|
+
end
|
57
|
+
|
58
|
+
def _diff(file1, file2, commentout, comment_tag, opts)
|
59
|
+
local = File.read(file1)
|
60
|
+
local = Togglate.commentout(local, tag:comment_tag)[0] if commentout
|
61
|
+
remote = get(file2, opts)
|
62
|
+
Diffy::Diff.new(local, remote, opts)
|
63
|
+
rescue Errno::ENOENT
|
64
|
+
:LocalNotFound
|
65
|
+
rescue Octokit::NotFound
|
66
|
+
:RemoteNotFound
|
67
|
+
end
|
68
|
+
|
69
|
+
def get_contents(repo, path, ref)
|
70
|
+
Octokit.contents(repo, path:path, ref:ref)
|
71
|
+
end
|
72
|
+
|
73
|
+
def get_ref(repo, ref)
|
74
|
+
Octokit.ref(repo, ref)
|
75
|
+
end
|
76
|
+
|
77
|
+
def build_file_pairs(file1, file2, dir:false)
|
78
|
+
if dir
|
79
|
+
fs = Dir.glob("#{file1}/*").select { |f| File.file? f }
|
80
|
+
fs.zip(fs)
|
81
|
+
else
|
82
|
+
[[file1, file2]]
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def parallel(items)
|
87
|
+
result = {}
|
88
|
+
items.map do |item1, item2|
|
89
|
+
Thread.new(item1, item2) do |_item1, _item2|
|
90
|
+
result[[_item1, _item2]] = yield(_item1, _item2)
|
91
|
+
end
|
92
|
+
end.each(&:join)
|
93
|
+
result
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
data/lib/gh-diff/auth.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
module GhDiff
|
2
|
+
class Auth
|
3
|
+
def self.[](opts={})
|
4
|
+
new(username:opts[:username],
|
5
|
+
password:opts[:password],
|
6
|
+
token:opts[:token]).login
|
7
|
+
end
|
8
|
+
|
9
|
+
def initialize(username:nil, password:nil, token:nil)
|
10
|
+
@username = username
|
11
|
+
@password = password
|
12
|
+
@token = token
|
13
|
+
@@login = nil
|
14
|
+
end
|
15
|
+
|
16
|
+
def login
|
17
|
+
if @token
|
18
|
+
Octokit.configure { |c| c.access_token = @token }
|
19
|
+
else
|
20
|
+
Octokit.configure { |c| c.login = @username; c.password = @password }
|
21
|
+
end
|
22
|
+
@@login = Octokit.user
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/lib/gh-diff/cli.rb
ADDED
@@ -0,0 +1,165 @@
|
|
1
|
+
require "thor"
|
2
|
+
|
3
|
+
module GhDiff
|
4
|
+
class CLI < Thor
|
5
|
+
class_option :repo,
|
6
|
+
aliases:'-g',
|
7
|
+
desc:'target repository'
|
8
|
+
class_option :revision,
|
9
|
+
aliases:'-r',
|
10
|
+
default:'master',
|
11
|
+
desc:'target revision'
|
12
|
+
class_option :dir,
|
13
|
+
aliases:'-p',
|
14
|
+
desc:'target remote directory'
|
15
|
+
class_option :username,
|
16
|
+
desc:'github username'
|
17
|
+
class_option :password,
|
18
|
+
desc:'github password'
|
19
|
+
class_option :token,
|
20
|
+
desc:'github API access token'
|
21
|
+
|
22
|
+
desc "get FILE", "Get FILE content from github repository"
|
23
|
+
def get(file)
|
24
|
+
opts = Option.new(options).with_env
|
25
|
+
github_auth(opts[:username], opts[:password], opts[:token])
|
26
|
+
|
27
|
+
gh = Diff.new(opts[:repo], revision:opts[:revision], dir:opts[:dir])
|
28
|
+
print gh.get(file)
|
29
|
+
rescue ::Octokit::NotFound
|
30
|
+
path = (dir=opts[:dir]) ? "#{dir}/#{file}" : file
|
31
|
+
puts "File not found at remote: '#{path}'"
|
32
|
+
exit(1)
|
33
|
+
rescue => e
|
34
|
+
puts "something go wrong: #{e}"
|
35
|
+
exit(1)
|
36
|
+
end
|
37
|
+
|
38
|
+
desc "diff LOCAL_FILE [REMOTE_FILE]", "Compare FILE(s) between local and remote repository. LOCAL_FILE can be DIRECTORY."
|
39
|
+
option :commentout,
|
40
|
+
aliases:'-c',
|
41
|
+
default:false,
|
42
|
+
type: :boolean,
|
43
|
+
desc:"compare html-commented texts in local file(s) with the remote"
|
44
|
+
option :comment_tag,
|
45
|
+
aliases:'-t',
|
46
|
+
default:'original'
|
47
|
+
option :format,
|
48
|
+
aliases:'-f',
|
49
|
+
default:'color',
|
50
|
+
desc:"output format: any of text, color, html or html_simple"
|
51
|
+
option :save,
|
52
|
+
aliases:'-s',
|
53
|
+
default:false,
|
54
|
+
type: :boolean
|
55
|
+
option :save_dir,
|
56
|
+
default:'diff',
|
57
|
+
desc:'save directory'
|
58
|
+
option :name_only,
|
59
|
+
default:true,
|
60
|
+
type: :boolean
|
61
|
+
def diff(file1, file2=file1)
|
62
|
+
opts = Option.new(options).with_env
|
63
|
+
github_auth(opts[:username], opts[:password], opts[:token])
|
64
|
+
|
65
|
+
gh = Diff.new(opts[:repo], revision:opts[:revision], dir:opts[:dir])
|
66
|
+
diffs = gh.diff(file1, file2, commentout:opts[:commentout],
|
67
|
+
comment_tag:opts[:comment_tag])
|
68
|
+
|
69
|
+
ref = gh.ref(opts[:revision], repo:opts[:repo])
|
70
|
+
|
71
|
+
diffs.each do |(f1, f2), diff|
|
72
|
+
next if file_not_found?(f1, f2, diff)
|
73
|
+
header = <<-EOS
|
74
|
+
Base revision: #{ref[:object][:sha]}[#{ref[:ref]}]
|
75
|
+
--- #{f1}
|
76
|
+
+++ #{f2}
|
77
|
+
|
78
|
+
EOS
|
79
|
+
diff_form = "#{f1} <-> #{f2} [%s:%s]" %
|
80
|
+
[ref[:object][:sha][0,7], ref[:ref].match(/\w+$/).to_s]
|
81
|
+
|
82
|
+
if opts[:save]
|
83
|
+
format = opts[:format]=='color' ? :text : opts[:format]
|
84
|
+
content = diff.to_s(format)
|
85
|
+
unless content.empty?
|
86
|
+
save(header + content, opts[:save_dir], f1)
|
87
|
+
else
|
88
|
+
print "\e[32mno Diff on\e[0m #{diff_form}\n"
|
89
|
+
end
|
90
|
+
else
|
91
|
+
content = diff.to_s(:text)
|
92
|
+
unless content.empty?
|
93
|
+
if opts[:name_only]
|
94
|
+
printf "\e[31mDiff found on\e[0m #{diff_form}\n"
|
95
|
+
else
|
96
|
+
print header
|
97
|
+
print diff.to_s(opts[:format])
|
98
|
+
end
|
99
|
+
else
|
100
|
+
print "\e[32mno Diff on\e[0m #{diff_form}\n"
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
desc "dir_diff DIRECTORY", "Print added and removed files in remote repository"
|
107
|
+
def dir_diff(dir)
|
108
|
+
opts = Option.new(options).with_env
|
109
|
+
github_auth(opts[:username], opts[:password], opts[:token])
|
110
|
+
|
111
|
+
gh = Diff.new(opts[:repo], revision:opts[:revision], dir:opts[:dir])
|
112
|
+
added, removed = gh.dir_diff(dir)
|
113
|
+
if [added, removed].all?(&:empty?)
|
114
|
+
puts "\e[33mNothing changed\e[0m"
|
115
|
+
else
|
116
|
+
if added.any?
|
117
|
+
puts "\e[33mNew files:\e[0m"
|
118
|
+
puts added.map { |f| " \e[32m" + f + "\e[0m" }
|
119
|
+
end
|
120
|
+
if removed.any?
|
121
|
+
puts "\e[33mRemoved files:\e[0m"
|
122
|
+
puts removed.map { |f| " \e[31m" + f + "\e[0m" }
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
@@login = nil
|
128
|
+
no_tasks do
|
129
|
+
def github_auth(username, password, token)
|
130
|
+
return true if @@login
|
131
|
+
return false unless token || [username, password].all?
|
132
|
+
|
133
|
+
@@login = Auth[username:username, password:password, token:token]
|
134
|
+
rescue ::Octokit::Unauthorized
|
135
|
+
puts "Bad Credentials"
|
136
|
+
exit(1)
|
137
|
+
end
|
138
|
+
|
139
|
+
def mkdir(dir)
|
140
|
+
FileUtils.mkdir_p(dir) unless Dir.exist?(dir)
|
141
|
+
end
|
142
|
+
|
143
|
+
def save(content, save_dir, file)
|
144
|
+
file = File.join(File.dirname(file), (File.basename(file, '.*') + '.diff'))
|
145
|
+
path = File.join(save_dir, file)
|
146
|
+
mkdir(File.dirname path)
|
147
|
+
File.write(path, content)
|
148
|
+
print "\e[32mDiff saved at '#{path}'\e[0m\n"
|
149
|
+
end
|
150
|
+
|
151
|
+
def file_not_found?(f1, f2, content)
|
152
|
+
case content
|
153
|
+
when :RemoteNotFound
|
154
|
+
print "\e[31m#{f2} not found on remote\e[0m\n"
|
155
|
+
true
|
156
|
+
when :LocalNotFound
|
157
|
+
print "\e[31m#{f1} not found on local\e[0m\n"
|
158
|
+
true
|
159
|
+
else
|
160
|
+
false
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require "dotenv"
|
2
|
+
|
3
|
+
module GhDiff
|
4
|
+
class Option
|
5
|
+
attr_reader :opts
|
6
|
+
def initialize(opts)
|
7
|
+
@opts = down_symbolize_key(opts)
|
8
|
+
end
|
9
|
+
|
10
|
+
def update(opts)
|
11
|
+
@opts.update(down_symbolize_key opts)
|
12
|
+
end
|
13
|
+
|
14
|
+
def dotenv
|
15
|
+
@dotenv ||= down_symbolize_key(Dotenv.load)
|
16
|
+
end
|
17
|
+
|
18
|
+
# returns: ENV variables prefixed with 'GH_'(default)
|
19
|
+
# and variables defined in dotenv file.
|
20
|
+
def env(prefix='GH_')
|
21
|
+
@envs ||= begin
|
22
|
+
envs = ENV.select { |env| env.start_with? prefix }
|
23
|
+
.map { |k, v| [k.sub(/^#{prefix}/, ''), v] }
|
24
|
+
down_symbolize_key(envs)
|
25
|
+
end
|
26
|
+
@envs.merge(dotenv)
|
27
|
+
end
|
28
|
+
|
29
|
+
def with_env(prefix='GH_')
|
30
|
+
env(prefix).merge(@opts)
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
def down_symbolize_key(opts)
|
35
|
+
opts.inject({}) do |h, (k, v)|
|
36
|
+
h[k.to_s.downcase.intern] = v; h
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
---
|
2
|
+
http_interactions:
|
3
|
+
- request:
|
4
|
+
method: get
|
5
|
+
uri: https://aaa:xxx@api.github.com/user
|
6
|
+
body:
|
7
|
+
encoding: US-ASCII
|
8
|
+
string: ''
|
9
|
+
headers:
|
10
|
+
Accept:
|
11
|
+
- application/vnd.github.v3+json
|
12
|
+
User-Agent:
|
13
|
+
- Octokit Ruby Gem 3.1.2
|
14
|
+
Accept-Encoding:
|
15
|
+
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
|
16
|
+
response:
|
17
|
+
status:
|
18
|
+
code: 401
|
19
|
+
message: Unauthorized
|
20
|
+
headers:
|
21
|
+
Server:
|
22
|
+
- GitHub.com
|
23
|
+
Date:
|
24
|
+
- Thu, 26 Jun 2014 14:59:19 GMT
|
25
|
+
Content-Type:
|
26
|
+
- application/json; charset=utf-8
|
27
|
+
Status:
|
28
|
+
- 401 Unauthorized
|
29
|
+
X-Github-Media-Type:
|
30
|
+
- github.v3; format=json
|
31
|
+
X-Ratelimit-Limit:
|
32
|
+
- '60'
|
33
|
+
X-Ratelimit-Remaining:
|
34
|
+
- '57'
|
35
|
+
X-Ratelimit-Reset:
|
36
|
+
- '1403798098'
|
37
|
+
X-Xss-Protection:
|
38
|
+
- 1; mode=block
|
39
|
+
X-Frame-Options:
|
40
|
+
- deny
|
41
|
+
Content-Security-Policy:
|
42
|
+
- default-src 'none'
|
43
|
+
Content-Length:
|
44
|
+
- '83'
|
45
|
+
Access-Control-Allow-Credentials:
|
46
|
+
- 'true'
|
47
|
+
Access-Control-Expose-Headers:
|
48
|
+
- ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset,
|
49
|
+
X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval
|
50
|
+
Access-Control-Allow-Origin:
|
51
|
+
- "*"
|
52
|
+
X-Github-Request-Id:
|
53
|
+
- 3C2F1730:6288:1EAD8FC4:53AC3546
|
54
|
+
Strict-Transport-Security:
|
55
|
+
- max-age=31536000
|
56
|
+
X-Content-Type-Options:
|
57
|
+
- nosniff
|
58
|
+
body:
|
59
|
+
encoding: UTF-8
|
60
|
+
string: '{"message":"Bad credentials","documentation_url":"https://developer.github.com/v3"}'
|
61
|
+
http_version:
|
62
|
+
recorded_at: Thu, 26 Jun 2014 14:59:19 GMT
|
63
|
+
recorded_with: VCR 2.9.2
|