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.
data/Rakefile ADDED
@@ -0,0 +1,7 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
7
+
data/bin/gh-diff ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'gh-diff'
4
+
5
+ GhDiff::CLI.start(ARGV)
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
@@ -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
@@ -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,3 @@
1
+ module GhDiff
2
+ VERSION = "0.0.1"
3
+ 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