vcs-ann 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 85fa80b26e688b8e09a0c28fb0bfea9bd72ffc6b
4
+ data.tar.gz: 3d8d857d12ad8c3b055dc916083f6ec215ddc6d9
5
+ SHA512:
6
+ metadata.gz: a685fb6517a6bc731764d9e6734ef53b65f860f97faa7472cd4c66b96952debaed6c54b05a4b65077913dacc4279bc5364e6111938384987d5b7f8b22b535276
7
+ data.tar.gz: fa01791a9d1c7337215c687494b215629a043b61e8e61527124a08e4aa148c7428d83a5349b173a32e10c85fb6719ae4662393285c81a893601f57f62252e829
data/LICENSE ADDED
@@ -0,0 +1,27 @@
1
+ Copyright (C) 2014 Tanaka Akira <akr@fsij.org>
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without modification,
5
+ are permitted provided that the following conditions are met:
6
+
7
+ * Redistributions of source code must retain the above copyright notice, this
8
+ list of conditions and the following disclaimer.
9
+
10
+ * Redistributions in binary form must reproduce the above copyright notice, this
11
+ list of conditions and the following disclaimer in the documentation and/or
12
+ other materials provided with the distribution.
13
+
14
+ * Neither the name of the {organization} nor the names of its
15
+ contributors may be used to endorse or promote products derived from
16
+ this software without specific prior written permission.
17
+
18
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
19
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
22
+ ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
25
+ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,29 @@
1
+ vcs-ann
2
+ =======
3
+
4
+ vcs-ann is an interactive wrapper for "annotate" and "diff" of svn and git.
5
+
6
+ ## Usage:
7
+
8
+ vcs-ann svn-or-git-managed-file
9
+
10
+ ## Requirement
11
+
12
+ * ruby 2.1
13
+ * w3m
14
+ * svn
15
+ * git
16
+
17
+ ## Install
18
+
19
+ gem install vcs-ann
20
+
21
+ ## Run without gem
22
+
23
+ git clone https://github.com/akr/vcs-ann.git
24
+ ruby -Ivcs-ann/lib vcs-ann/bin/vcs-ann svn-or-git-managed-file
25
+
26
+ ## Author
27
+
28
+ Tanaka Akira
29
+ akr@fsij.org
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'vcs-ann'
4
+
5
+ main(ARGV)
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'webrick'
4
+ require 'pathname'
5
+ require 'cgi'
6
+ require 'tempfile'
7
+ require 'erb'
8
+ require 'pp'
9
+ require 'open3'
10
+
11
+ require 'vcs-ann/svn'
12
+ require 'vcs-ann/git'
13
+ require 'vcs-ann/main'
@@ -0,0 +1,162 @@
1
+ class GITRepo
2
+ def initialize(topdir)
3
+ @topdir = topdir
4
+ end
5
+
6
+ def git_blame_each(topdir, relpath, rev)
7
+ out, status = Open3.capture2('git', '-C', topdir, 'blame', '--porcelain', rev, '--', relpath)
8
+ out.force_encoding('locale').scrub!
9
+ if !status.success?
10
+ raise "git blame failed"
11
+ end
12
+
13
+ header_hash = {}
14
+ prev_header = {}
15
+ block = []
16
+ out.each_line {|line|
17
+ line.force_encoding('locale').scrub!
18
+ if /\A\t/ !~ line
19
+ block << line
20
+ else
21
+ content_line = line.sub(/\A\t/, '')
22
+ rev, original_file_line_number, final_file_line_number, numlines = block.shift.split(/\s+/)
23
+ if !block.empty?
24
+ header = {}
25
+ block.each {|header_line|
26
+ if / / =~ header_line.chomp
27
+ header[$`] = $'
28
+ end
29
+ }
30
+ header_hash[rev] = header
31
+ end
32
+ header = header_hash[rev]
33
+ yield rev, original_file_line_number, final_file_line_number, numlines, header, content_line
34
+ block = []
35
+ end
36
+ }
37
+ end
38
+
39
+ def format_file(list)
40
+ rev = list[0]
41
+ relpath = list[1..-1].join('/')
42
+
43
+ result = '<pre>'
44
+
45
+ data = []
46
+ author_name_width = 0
47
+ git_blame_each(@topdir.to_s, relpath, rev) {|rev, original_file_line_number, final_file_line_number, numlines, header, content_line|
48
+ author_time = Time.at(header['author-time'].to_i).strftime("%Y-%m-%d")
49
+ author_name = header['author']
50
+ content_line = content_line.chomp.expand_tab
51
+ author_name_width = author_name.length if author_name_width < author_name.length
52
+ data << [rev, author_time, author_name, content_line, header['filename'], original_file_line_number]
53
+ }
54
+
55
+ prev_rev = nil
56
+ ln = 1
57
+ data.each {|rev, author_time, author_name, content_line, filename, original_file_line_number|
58
+ formatted_author_time = prev_rev == rev ? ' ' * 10 : author_time
59
+ formatted_author_name = "%-#{author_name_width}s" % author_name
60
+ commit_url = "/commit/#{rev}\##{u(rev+"/"+filename.to_s+":"+original_file_line_number.to_s)}"
61
+ result << %{<a name="#{h ln.to_s}"></a>}
62
+ result << %{<a href="#{h commit_url}">#{h formatted_author_time}</a> }
63
+ result << %{#{h formatted_author_name} }
64
+ result << %{#{h content_line}\n}
65
+ prev_rev = rev
66
+ ln += 1
67
+ }
68
+
69
+ result << '</pre>'
70
+
71
+ result
72
+ end
73
+
74
+ def format_commit(list)
75
+ rev = list[0]
76
+ rev
77
+ log_out, log_status = Open3.capture2({'LC_ALL'=>'C'},
78
+ 'git', '-C', @topdir.to_s, 'log', '-1', '--parents', rev)
79
+ log_out.force_encoding('locale').scrub!
80
+ if !log_status.success?
81
+ raise "git log failed."
82
+ end
83
+
84
+ if /^commit ([0-9a-f]+)(.*)\n/ !~ log_out
85
+ raise "git log doesn't produce 'commit' line."
86
+ end
87
+ commit_rev = $1
88
+ parent_revs = $2.strip.split(/\s+/)
89
+
90
+ result = '<pre>'
91
+ log_out.each_line {|line|
92
+ result << "#{h line.chomp}\n"
93
+ }
94
+ result << '</pre>'
95
+
96
+ parent_revs.each {|parent_rev|
97
+ diff_out, diff_status = Open3.capture2({'LC_ALL'=>'C'},
98
+ 'git', '-C', @topdir.to_s, 'diff', parent_rev, commit_rev)
99
+ diff_out.force_encoding('locale').scrub!
100
+ if !diff_status.success?
101
+ raise "git diff failed."
102
+ end
103
+
104
+ rev1 = parent_rev
105
+ rev2 = commit_rev
106
+ filename1 = filename2 = '?'
107
+ result << '<pre>'
108
+ scan_udiff(diff_out) {|tag, *rest|
109
+ case tag
110
+ when :filename1
111
+ line, filename1 = rest
112
+ filename1.sub!(%r{\Aa/}, '')
113
+ result << " "
114
+ result << (h line.chomp.expand_tab) << "\n"
115
+ when :filename2
116
+ line, filename2 = rest
117
+ filename2.sub!(%r{\Ab/}, '')
118
+ result << " "
119
+ result << (h line.chomp.expand_tab) << "\n"
120
+ when :hunk_header
121
+ line, ln_cur1, ln_num1, ln_cur2, ln_num2 = rest
122
+ result << " "
123
+ result << (h line.chomp.expand_tab) << "\n"
124
+ when :del
125
+ line, content_line, ln_cur1 = rest
126
+ content_line = content_line.chomp.expand_tab
127
+ rev1_url = "/file/#{rev1}/#{filename1}\##{ln_cur1}"
128
+ result << %{<a name="#{h(u(rev1.to_s+"/"+filename1+":"+ln_cur1.to_s))}"></a>}
129
+ result << %{<a href="#{h rev1_url}"> -</a>}
130
+ result << (h content_line) << "\n"
131
+ when :add
132
+ line, content_line, ln_cur2 = rest
133
+ content_line = content_line.chomp.expand_tab
134
+ rev2_url = "/file/#{rev2}/#{filename2}\##{ln_cur2}"
135
+ result << %{<a name="#{h(u(rev2.to_s+"/"+filename2+":"+ln_cur2.to_s))}"></a>}
136
+ result << %{<a href="#{h rev2_url}"> +</a>}
137
+ result << (h content_line) << "\n"
138
+ when :com
139
+ line, content_line, ln_cur1, ln_cur2 = rest
140
+ content_line = content_line.chomp.expand_tab
141
+ rev1_url = "/file/#{rev1}/#{filename1}\##{ln_cur1}"
142
+ rev2_url = "/file/#{rev2}/#{filename2}\##{ln_cur2}"
143
+ result << %{<a name="#{h(u(rev1.to_s+"/"+filename1+":"+ln_cur1.to_s))}"></a>}
144
+ result << %{<a name="#{h(u(rev2.to_s+"/"+filename2+":"+ln_cur2.to_s))}"></a>}
145
+ result << %{<a href="#{h rev1_url}"> </a>}
146
+ result << %{<a href="#{h rev2_url}"> </a>}
147
+ result << (h content_line) << "\n"
148
+ when :other
149
+ line, = rest
150
+ result << " "
151
+ result << (h line.chomp.expand_tab) << "\n"
152
+ else
153
+ raise "unexpected udiff line tag"
154
+ end
155
+ }
156
+ result << '</pre>'
157
+
158
+ }
159
+
160
+ result
161
+ end
162
+ end
@@ -0,0 +1,195 @@
1
+ include ERB::Util
2
+
3
+ class String
4
+ # expand TABs destructively.
5
+ # TAB width is assumed as 8.
6
+ def expand_tab!
7
+ self.sub!(/\A\t+/) { ' ' * ($&.length * 8) }
8
+ nil
9
+ end
10
+
11
+ # returns a string which TABs are expanded.
12
+ # TAB width is assumed as 8.
13
+ def expand_tab
14
+ result = dup
15
+ result.expand_tab!
16
+ result
17
+ end
18
+ end
19
+
20
+ def scan_udiff(string)
21
+ ln_cur1 = 0
22
+ ln_cur2 = 0
23
+ ln_num1 = 0
24
+ ln_num2 = 0
25
+ string.each_line {|line|
26
+ line.force_encoding('locale').scrub!
27
+ case line
28
+ when /\A---\s+(\S+)/
29
+ yield :filename1, line, $1
30
+ when /\A\+\+\+\s+(\S+)/
31
+ yield :filename2, line, $1
32
+ when /\A@@ -(\d+),(\d+) \+(\d+),(\d+) @@/
33
+ ln_cur1 = $1.to_i
34
+ ln_num1 = $2.to_i
35
+ ln_cur2 = $3.to_i
36
+ ln_num2 = $4.to_i
37
+ yield :hunk_header, line, ln_cur1, ln_num1, ln_cur2, ln_num2
38
+ else
39
+ if /\A-/ =~ line && 0 < ln_num1
40
+ content_line = $'
41
+ yield :del, line, content_line, ln_cur1
42
+ ln_cur1 += 1
43
+ ln_num1 -= 1
44
+ elsif /\A\+/ =~ line && 0 < ln_num2
45
+ content_line = $'
46
+ yield :add, line, content_line, ln_cur2
47
+ ln_cur2 += 1
48
+ ln_num2 -= 1
49
+ elsif /\A / =~ line && 0 < ln_num1 && 0 < ln_num2
50
+ content_line = $'
51
+ yield :com, line, content_line, ln_cur1, ln_cur2
52
+ ln_cur1 += 1
53
+ ln_cur2 += 1
54
+ ln_num1 -= 1
55
+ ln_num2 -= 1
56
+ else
57
+ yield :other, line
58
+ end
59
+ end
60
+ }
61
+ end
62
+
63
+ NullLogSink = Object.new
64
+ def NullLogSink.<<(s)
65
+ end
66
+ NullLog = WEBrick::BasicLog.new(NullLogSink)
67
+
68
+ class Server
69
+ def initialize(repo)
70
+ @repo = repo
71
+ @httpd = WEBrick::HTTPServer.new(
72
+ :BindAddress => '127.0.0.1',
73
+ :Port => 0,
74
+ :AccessLog => NullLog,
75
+ :Logger => NullLog)
76
+ @httpd.mount_proc("/") {|req, res|
77
+ handle_request0(repo, req, res)
78
+ }
79
+ trap(:INT){ @httpd.shutdown }
80
+ addr = @httpd.listeners[0].connect_address
81
+ @http_root = "http://#{addr.ip_address}:#{addr.ip_port}"
82
+ @th = Thread.new { @httpd.start }
83
+ end
84
+
85
+ def stop
86
+ @httpd.shutdown
87
+ @th.join
88
+ end
89
+
90
+ def annotate_url(relpath, rev)
91
+ reluri = relpath.gsub(%r{[^/]+}) { CGI.escape($&) }
92
+ reluri = '/' + reluri if %r{\A/} !~ reluri
93
+ "#{@http_root}/file/#{rev}#{reluri}"
94
+ end
95
+
96
+ def handle_request0(repo, req, res)
97
+ begin
98
+ handle_request(repo, req, res)
99
+ rescue Exception
100
+ res.content_type = 'text/html'
101
+ result = '<pre>'
102
+ result << "#{h $!.message} (#{h $!.class})\n"
103
+ $!.backtrace.each {|b|
104
+ result << "#{h b}\n"
105
+ }
106
+ result << "</pre>"
107
+ res.body = result
108
+ end
109
+ end
110
+
111
+ def handle_request(repo, req, res)
112
+ res.content_type = 'text/html'
113
+ list = req.request_uri.path.scan(%r{[^/]+}).map {|s| CGI.unescape(s) }
114
+ case list[0]
115
+ when 'file'
116
+ res.body = repo.format_file list[1..-1]
117
+ when 'commit'
118
+ res.body = repo.format_commit list[1..-1]
119
+ else
120
+ raise "unexpected command"
121
+ end
122
+ end
123
+ end
124
+
125
+ def find_svn_repository(arg)
126
+ svn_info_xml = IO.popen(['svn', 'info', '--xml', arg]) {|io| io.read }
127
+
128
+ # <url>http://svn.ruby-lang.org/repos/ruby/trunk/ChangeLog</url>
129
+ # <root>http://svn.ruby-lang.org/repos/ruby</root>
130
+ # <commit
131
+ # revision="44930">
132
+
133
+ if %r{<url>(.*?)</url>} !~ svn_info_xml
134
+ raise "unexpected 'svn info' result: no url element"
135
+ end
136
+ url = CGI.unescapeHTML($1)
137
+ if %r{<root>(.*?)</root>} !~ svn_info_xml
138
+ raise "unexpected 'svn info' result: no root element"
139
+ end
140
+ root = CGI.unescapeHTML($1)
141
+ if %r{#{Regexp.escape root}} !~ url
142
+ raise "unexpected 'svn info' result: url is not a prefix of root"
143
+ end
144
+ relpath = $'
145
+ if !relpath.empty? && %r{\A/} !~ relpath
146
+ raise "unexpected 'svn info' result: relpath doesn't start with a slash"
147
+ end
148
+
149
+ if %r{<commit\s+revision="(\d+)">} !~ svn_info_xml
150
+ raise "unexpected 'svn info' result: no revision"
151
+ end
152
+ rev = $1.to_i
153
+
154
+ return SVNRepo.new(root), relpath, rev
155
+ end
156
+
157
+ def find_git_repository(realpath, d)
158
+ relpath = realpath.relative_path_from(d).to_s
159
+ rev, status = Open3.capture2('git', '-C', d.to_s, 'log', '--pretty=format:%H', '-1', relpath.to_s)
160
+ if !status.success?
161
+ raise "git log failed"
162
+ end
163
+ return GITRepo.new(d), relpath, rev
164
+ end
165
+
166
+ def parse_arguments(argv)
167
+ # process options
168
+ filename = argv[0]
169
+ filename
170
+ end
171
+
172
+ def setup_repository(filename)
173
+ realpath = Pathname(filename).realpath
174
+ realpath.dirname.ascend {|d|
175
+ if (d+".svn").exist?
176
+ return find_svn_repository(filename)
177
+ end
178
+ if (d+".git").exist?
179
+ return find_git_repository(realpath, d)
180
+ end
181
+ }
182
+ raise "cannot find a repository"
183
+ end
184
+
185
+ def run_browser(url)
186
+ system "w3m", url
187
+ end
188
+
189
+ def main(argv)
190
+ filename = parse_arguments(argv)
191
+ repo, relpath, rev = setup_repository filename
192
+ server = Server.new(repo)
193
+ run_browser server.annotate_url(relpath, rev)
194
+ server.stop
195
+ end
@@ -0,0 +1,230 @@
1
+ class SVNRepo
2
+ def initialize(root)
3
+ @root = root
4
+ @type = {}
5
+ @cat = {}
6
+ @ann = {}
7
+ end
8
+
9
+ def run_info(relpath, rev)
10
+ key = [relpath, rev]
11
+ if !@type.has_key?(key)
12
+ out, err, status = Open3.capture3({"LC_ALL"=>"C"}, "svn", "info", "-r#{rev}", "#{@root}#{relpath}")
13
+ out.force_encoding('locale').scrub!
14
+ err.force_encoding('locale').scrub!
15
+ if !status.success?
16
+ case err
17
+ when /Unable to find repository location/
18
+ @type[key] = :not_exist
19
+ else
20
+ raise "unexpected failing svn info result: #{err}"
21
+ end
22
+ else
23
+ case out
24
+ when /^Node Kind: file$/
25
+ @type[key] = :file
26
+ when /^Node Kind: directory$/
27
+ @type[key] = :dir
28
+ else
29
+ raise "unexpected succeseed svn info result"
30
+ end
31
+ end
32
+ end
33
+ @type[key]
34
+ end
35
+
36
+ def run_cat(relpath, rev)
37
+ key = [relpath, rev]
38
+ if !@cat.has_key?(key)
39
+ if run_info(relpath, rev) != :file
40
+ raise "file expected"
41
+ end
42
+ tmpbase = File.basename(relpath) + "-r#{rev}"
43
+ cat = Tempfile.new([tmpbase, ".txt"])
44
+ cat.close
45
+ out, err, status = Open3.capture3({"LC_ALL"=>"C"}, "svn", "cat", "-r#{rev}", "#{@root}#{relpath}")
46
+ out.force_encoding('locale').scrub!
47
+ err.force_encoding('locale').scrub!
48
+ if !status.success?
49
+ raise "svn cat failed"
50
+ end
51
+ cat.open
52
+ cat << out
53
+ cat.close
54
+ @cat[key] = cat
55
+ end
56
+ @cat[key]
57
+ end
58
+
59
+ def run_ann(relpath, rev)
60
+ key = [relpath, rev]
61
+ if !@ann.has_key?(key)
62
+ if run_info(relpath, rev) != :file
63
+ raise "file expected"
64
+ end
65
+ tmpbase = File.basename(relpath) + "-r#{rev}"
66
+ ann = Tempfile.new([tmpbase, ".xml"])
67
+ ann.close
68
+ system "svn", "ann", "--xml", "-r#{rev}", "#{@root}#{relpath}", :out => ann.path
69
+ if !$?.success?
70
+ raise "svn ann failed"
71
+ end
72
+ @ann[key] = ann
73
+ end
74
+ @ann[key]
75
+ end
76
+
77
+ def format_file(list)
78
+ rev = list[0]
79
+ relpath = list[1..-1].map {|s| "/" + s }.join
80
+
81
+ case type = run_info(relpath, rev)
82
+ when :file
83
+ else
84
+ raise "unexpected type #{type}: #{relpath}@#{rev}"
85
+ end
86
+
87
+ cat = run_cat(relpath, rev)
88
+ if !cat
89
+ raise "not a plain file: #{relpath}@#{rev}"
90
+ end
91
+
92
+ ann = run_ann(relpath, rev)
93
+ ann.open
94
+ lines = []
95
+ ann.each("</entry>\n") {|entry|
96
+ next if /<entry\n/ !~ entry
97
+ entry = $'
98
+ next if /line-number="(\d+)"/ !~ entry
99
+ line_number = $1
100
+ next if /revision="(\d+)"/ !~ entry
101
+ line_rev = $1
102
+ next if %r{<author>(.*)</author>} !~ entry
103
+ line_author = CGI.unescapeHTML($1)
104
+ next if %r{<date>(.*)</date>} !~ entry
105
+ line_date = $1
106
+ lines << [line_number, line_rev, line_author, line_date]
107
+ }
108
+ ann.close
109
+
110
+ width_list = lines.map {|line_number, line_rev, line_author, line_date|
111
+ [line_number.length, line_rev.length, line_author.length]
112
+ }
113
+ line_number_width = width_list.map {|ln_width, rev_width, authoer_width| ln_width }.max
114
+ line_rev_width = width_list.map {|ln_width, rev_width, authoer_width| rev_width }.max
115
+ line_author_width = width_list.map {|ln_width, rev_width, author_width| author_width }.max
116
+
117
+ result = "<pre>"
118
+ prev_rev = nil
119
+ cat.open
120
+ cat.each.with_index {|line, i|
121
+ line.expand_tab!
122
+ ln = i+1
123
+ line_number, line_rev, line_author, line_date = lines[i]
124
+ commit_url = "/commit/#{line_rev}\##{u line_rev+':'+line.chomp}"
125
+ authorsp = line_author.ljust(line_author_width)
126
+ line_number_anchor = ln.to_s
127
+ line_contents_anchor = u line.chomp
128
+ if prev_rev != line_rev
129
+ revsp = line_rev.rjust(line_rev_width)
130
+ prev_rev = line_rev
131
+ else
132
+ revsp = ' ' * line_rev_width
133
+ end
134
+ result << %{<a name="#{h line_number_anchor}"></a>}
135
+ result << %{<a name="#{h line_contents_anchor}"></a>}
136
+ result << %{<a href="#{h commit_url}" title="#{h line_date}">#{h revsp}</a> }
137
+ result << "#{h authorsp} #{h line.chomp}\n"
138
+ }
139
+ cat.close
140
+ result << "</pre>"
141
+ result
142
+ end
143
+
144
+ def format_commit(list)
145
+ rev = list[0]
146
+ log_out, log_err, log_status = Open3.capture3({"LC_ALL"=>"C"}, "svn", "log", "-r#{rev}", "#{@root}")
147
+ diff_out, diff_err, diff_status = Open3.capture3({"LC_ALL"=>"C"}, "svn", "diff", "-c#{rev}", "#{@root}")
148
+ log_out.force_encoding('locale').scrub!
149
+ log_err.force_encoding('locale').scrub!
150
+ diff_out.force_encoding('locale').scrub!
151
+ diff_err.force_encoding('locale').scrub!
152
+
153
+ rev_hash = {}
154
+ diff_out.each_line {|line|
155
+ next if /\A(?:---|\+\+\+)\s+(\S+)\s+\(revision (\d+)\)/ !~ line
156
+ filename = $1
157
+ rev = $2
158
+ rev_hash[rev] = true
159
+ }
160
+ rev_maxwidth = rev_hash.keys.map {|rev| rev.length }.max
161
+ rev_space = " " * rev_maxwidth
162
+ rev_fmt = "%.#{rev_maxwidth}s"
163
+
164
+ result = ''
165
+
166
+ result << '<pre>'
167
+ log_out.each_line {|line|
168
+ result << (h line.chomp) << "\n"
169
+ }
170
+ result << '</pre>'
171
+
172
+ rev1 = (rev.to_i-1).to_s
173
+ rev2 = rev
174
+ filename1 = filename2 = '?'
175
+ result << '<pre>'
176
+ scan_udiff(diff_out) {|tag, *rest|
177
+ case tag
178
+ when :filename1
179
+ line, filename1 = rest
180
+ result << " "
181
+ result << (h line.chomp) << "\n"
182
+ when :filename2
183
+ line, filename2 = rest
184
+ result << " "
185
+ result << (h line.chomp) << "\n"
186
+ when :hunk_header
187
+ line, ln_cur1, ln_num1, ln_cur2, ln_num2 = rest
188
+ result << " "
189
+ result << (h line.chomp) << "\n"
190
+ when :del
191
+ line, content_line, ln_cur1 = rest
192
+ content_line = content_line.chomp.expand_tab
193
+ rev1_url = "/file/#{rev1}/#{filename1}\##{ln_cur1}"
194
+ result << %{<a name="#{h(u(rev1.to_s+"/"+filename1+":"+ln_cur1.to_s))}"></a>}
195
+ result << %{<a name="#{h(u(rev1.to_s+":"+content_line.chomp))}"></a>}
196
+ result << %{<a href="#{h rev1_url}"> -</a>}
197
+ result << (h content_line) << "\n"
198
+ when :add
199
+ line, content_line, ln_cur2 = rest
200
+ content_line = content_line.chomp.expand_tab
201
+ rev2_url = "/file/#{rev2}/#{filename2}\##{ln_cur2}"
202
+ result << %{<a name="#{h(u(rev2.to_s+"/"+filename2+":"+ln_cur2.to_s))}"></a>}
203
+ result << %{<a name="#{h(u(rev2.to_s+":"+content_line.chomp))}"></a>}
204
+ result << %{<a href="#{h rev2_url}"> +</a>}
205
+ result << (h content_line) << "\n"
206
+ when :com
207
+ line, content_line, ln_cur1, ln_cur2 = rest
208
+ content_line = content_line.chomp.expand_tab
209
+ rev1_url = "/file/#{rev1}/#{filename1}\##{ln_cur1}"
210
+ rev2_url = "/file/#{rev2}/#{filename2}\##{ln_cur2}"
211
+ result << %{<a name="#{h(u(rev1.to_s+"/"+filename1+":"+ln_cur1.to_s))}"></a>}
212
+ result << %{<a name="#{h(u(rev2.to_s+"/"+filename2+":"+ln_cur2.to_s))}"></a>}
213
+ result << %{<a name="#{h(u(rev1.to_s+":"+content_line.chomp))}"></a>}
214
+ result << %{<a name="#{h(u(rev2.to_s+":"+content_line.chomp))}"></a>}
215
+ result << %{<a href="#{h rev1_url}"> </a>}
216
+ result << %{<a href="#{h rev2_url}"> </a>}
217
+ result << (h content_line) << "\n"
218
+ when :other
219
+ line, = rest
220
+ result << " "
221
+ result << (h line.chomp) << "\n"
222
+ else
223
+ raise "unexpected udiff line tag"
224
+ end
225
+ }
226
+ result << '</pre>'
227
+
228
+ result
229
+ end
230
+ end
metadata ADDED
@@ -0,0 +1,54 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: vcs-ann
3
+ version: !ruby/object:Gem::Version
4
+ version: '0.1'
5
+ platform: ruby
6
+ authors:
7
+ - Tanaka Akira
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-04-29 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: |
14
+ vcs-ann is an interactive wrapper for "annotate" and "diff" of svn and git.
15
+
16
+ vcs-ann enables you to browse annotated sources and diffs interactively.
17
+ email: akr@fsij.org
18
+ executables:
19
+ - vcs-ann
20
+ extensions: []
21
+ extra_rdoc_files: []
22
+ files:
23
+ - LICENSE
24
+ - README.md
25
+ - bin/vcs-ann
26
+ - lib/vcs-ann.rb
27
+ - lib/vcs-ann/git.rb
28
+ - lib/vcs-ann/main.rb
29
+ - lib/vcs-ann/svn.rb
30
+ homepage: https://github.com/akr/vcs-ann
31
+ licenses:
32
+ - BSD-3-Clause
33
+ metadata: {}
34
+ post_install_message:
35
+ rdoc_options: []
36
+ require_paths:
37
+ - lib
38
+ required_ruby_version: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: '2.1'
43
+ required_rubygems_version: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ requirements: []
49
+ rubyforge_project:
50
+ rubygems_version: 2.2.2
51
+ signing_key:
52
+ specification_version: 4
53
+ summary: an interactive wrapper for "annotate" and "diff" of svn and git
54
+ test_files: []