vcs-ann 0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +27 -0
- data/README.md +29 -0
- data/bin/vcs-ann +5 -0
- data/lib/vcs-ann.rb +13 -0
- data/lib/vcs-ann/git.rb +162 -0
- data/lib/vcs-ann/main.rb +195 -0
- data/lib/vcs-ann/svn.rb +230 -0
- metadata +54 -0
checksums.yaml
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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
|
data/bin/vcs-ann
ADDED
data/lib/vcs-ann.rb
ADDED
data/lib/vcs-ann/git.rb
ADDED
@@ -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
|
data/lib/vcs-ann/main.rb
ADDED
@@ -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
|
data/lib/vcs-ann/svn.rb
ADDED
@@ -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: []
|