vcs-ann 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.
@@ -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: []