gh-diff 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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