gh-diff 0.0.1
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 +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
|