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