* subversion/bindings/swig/python/libsvn_swig_py/swigutil_py.c
[svn.git] / subversion / bindings / swig / ruby / svn / info.rb
blob8d1fc0739f8037f11a2b363d244ceb9015a7601a
1 require "English"
2 require "time"
3 require "digest/sha2"
4 require "tempfile"
6 require "nkf"
7 begin
8   require "uconv"
9 rescue LoadError
10   module Uconv
11     class Error < StandardError
12     end
13     def self.u8toeuc(str)
14       raise Error
15     end
16   end
17 end
19 require "svn/core"
20 require "svn/repos"
21 require "svn/fs"
22 require "svn/delta"
24 module Svn
25   class Info
27     attr_reader :author, :log, :date, :changed_dirs
28     attr_reader :added_files, :deleted_files, :updated_files, :copied_files
29     attr_reader :added_dirs, :deleted_dirs, :updated_dirs, :copied_dirs
30     attr_reader :path, :revision, :diffs
31     attr_reader :sha256, :entire_sha256
33     def initialize(path, rev)
34       setup(path, rev)
35       get_info
36       get_dirs_changed
37       get_changed
38       get_diff
39       get_sha256
40       teardown
41     end
43     def paths
44       files + directories
45     end
47     def files
48       @added_files + @deleted_files + @updated_files + @copied_files
49     end
51     def directories
52       @added_dirs + @deleted_dirs + @updated_dirs + @copied_dirs
53     end
55     def sub_paths(prefix)
56       prefixes = prefix.split(/\/+/)
57       results = []
58       paths.each do |path,|
59         paths = path.split(/\/+/)
60         if prefixes.size < paths.size and prefixes == paths[0, prefixes.size]
61           results << paths[prefixes.size]
62         end
63       end
64       results
65     end
67     private
68     def setup(path, rev)
69       @path = path
70       @revision = Integer(rev)
71       @prev_rev = @revision - 1
72       @repos = Repos.open(@path)
73       @fs = @repos.fs
74       @root = @fs.root(@revision)
75     end
77     def teardown
78       @repos = @root = @fs = nil
79     end
81     def get_info
82       @author = force_to_utf8(prop(Core::PROP_REVISION_AUTHOR))
83       @date = prop(Core::PROP_REVISION_DATE)
84       @log = force_to_utf8(prop(Core::PROP_REVISION_LOG))
85     end
87     def get_dirs_changed
88       editor = traverse(Delta::ChangedDirsEditor)
89       @changed_dirs = editor.changed_dirs
90     end
92     def get_changed
93       editor = traverse(Delta::ChangedEditor, true)
94       @copied_files = editor.copied_files
95       @copied_dirs = editor.copied_dirs
96       @added_files = editor.added_files
97       @added_dirs = editor.added_dirs
98       @deleted_files = editor.deleted_files
99       @deleted_dirs = editor.deleted_dirs
100       @updated_files = editor.updated_files
101       @updated_dirs = editor.updated_dirs
102     end
104     def get_diff
105       tree = @repos.delta_tree(@root, @prev_rev)
106       @diffs = {}
107       get_diff_recurse(tree, @fs.root(@prev_rev), "", "")
108       @diffs.each do |key, values|
109         values.each do |type, diff|
110           diff.body = force_to_utf8(diff.body)
111         end
112       end
113     end
115     TYPE_TABLE = [
116       [:copy?, :copied],
117       [:add?, :added],
118       [:delete?, :deleted],
119       [:replace?, :modified],
120     ]
122     def get_type(node)
123       TYPE_TABLE.each do |meth, type|
124         return type if node.__send__(meth)
125       end
126       nil
127     end
129     def get_diff_recurse(node, base_root, path, base_path)
130       if node.copy?
131         base_path = node.copyfrom_path.sub(/\A\//, '')
132         base_root = base_root.fs.root(node.copyfrom_rev)
133       end
135       if node.file?
136         try_diff(node, base_root, path, base_path)
137       end
139       if node.prop_mod? and !node.delete?
140         get_prop_diff(node, base_root, path, base_path)
141       end
143       node = node.child
144       if node
145         get_diff_recurse(node, base_root,
146                          "#{path}/#{node.name}",
147                          "#{base_path}/#{node.name}")
148         while node.sibling
149           node = node.sibling
150           get_diff_recurse(node, base_root,
151                            "#{path}/#{node.name}",
152                            "#{base_path}/#{node.name}")
153         end
154       end
155     end
157     def get_prop_diff(node, base_root, path, base_path)
158       local_props = @root.node_prop_list(path)
159       if node.add?
160         base_props = {}
161       else
162         base_props = base_root.node_prop_list(base_path)
163       end
164       prop_changes = Core::Property.diffs2(local_props, base_props)
165       prop_changes.each do |name, value|
166         entry = diff_entry(path, :property_changed)
167         entry.body << "Name: #{force_to_utf8(name)}\n"
168         orig_value = base_props[name]
169         entry.body << "   - #{force_to_utf8(orig_value)}\n" if orig_value
170         entry.body << "   + #{force_to_utf8(value)}\n" if value
171       end
172     end
174     def try_diff(node, base_root, path, base_path)
175       if node.replace? and node.text_mod?
176         differ = Fs::FileDiff.new(base_root, base_path, @root, path)
177         do_diff(node, base_root, path, base_path, differ)
178       elsif node.add? and node.text_mod?
179         differ = Fs::FileDiff.new(nil, base_path, @root, path)
180         do_diff(node, base_root, path, base_path, differ)
181       elsif node.delete?
182         differ = Fs::FileDiff.new(base_root, base_path, nil, path)
183         do_diff(node, base_root, path, base_path, differ)
184       elsif node.copy?
185         diff_entry(path, get_type(node))
186       end
187     end
189     def do_diff(node, base_root, path, base_path, differ)
190       entry = diff_entry(path, get_type(node))
192       if differ.binary?
193         entry.body = "(Binary files differ)\n"
194       else
195         base_rev = base_root.revision
196         rev = @root.revision
197         base_date = base_root.fs.prop(Core::PROP_REVISION_DATE, base_rev)
198         date = @root.fs.prop(Core::PROP_REVISION_DATE, rev)
199         base_date = format_date(base_date)
200         date = format_date(date)
201         stripped_base_path = base_path.sub(/\A\//, '')
202         stripped_path = path.sub(/\A\//, '')
203         base_label = "#{stripped_base_path}\t#{base_date} (rev #{base_rev})"
204         label = "#{stripped_path}\t#{date} (rev #{rev})"
205         entry.body = differ.unified(base_label, label)
206         parse_diff_unified(entry)
207       end
208     end
210     def parse_diff_unified(entry)
211       in_content = false
212       entry.body.each do |line|
213         case line
214         when /^@@/
215           in_content = true
216         else
217           if in_content
218             case line
219             when /^\+/
220               entry.count_up_added_line!
221             when /^-/
222               entry.count_up_deleted_line!
223             end
224           end
225         end
226       end
227     end
229     def dump_contents(root, path)
230       root.file_contents(path) do |f|
231         if block_given?
232           yield f
233         else
234           f.read
235         end
236       end
237     end
239     def diff_entry(target, type)
240       target = target.sub(/\A\//, '')
241       @diffs[target] ||= {}
242       @diffs[target][type] ||= DiffEntry.new(type)
243       @diffs[target][type]
244     end
246     def get_sha256
247       sha = Digest::SHA256.new
248       @sha256 = {}
249       [
250         @added_files,
251 #        @deleted_files,
252         @updated_files,
253       ].each do |files|
254         files.each do |file|
255           content = dump_contents(@root, file)
256           sha << content
257           @sha256[file] = {
258             :file => file,
259             :revision => @revision,
260             :sha256 => Digest::SHA256.hexdigest(content),
261           }
262         end
263       end
264       @entire_sha256 = sha.hexdigest
265     end
267     def prop(name)
268       @fs.prop(name, @revision)
269     end
271     def force_to_utf8(str)
272       str = str.to_s
273       begin
274         # check UTF-8 or not
275         Uconv.u8toeuc(str)
276         str
277       rescue Uconv::Error
278         NKF.nkf("-w", str)
279       end
280     end
282     def traverse(editor_class, pass_root=false)
283       base_rev = @prev_rev
284       base_root = @fs.root(base_rev)
285       if pass_root
286         editor = editor_class.new(@root, base_root)
287       else
288         editor = editor_class.new
289       end
290       base_root.dir_delta("", "", @root, "", editor)
291       editor
292     end
294     def format_date(date)
295       formatted = date.strftime("%Y-%m-%d %H:%M:%S")
296       formatted << (" %+03d:00" % (date.hour - date.getutc.hour))
297       formatted
298     end
300     class DiffEntry
301       attr_reader :type, :added_line, :deleted_line
302       attr_accessor :body
304       def initialize(type)
305         @type = type
306         @added_line = 0
307         @deleted_line = 0
308         @body = ""
309       end
311       def count_up_added_line!
312         @added_line += 1
313       end
315       def count_up_deleted_line!
316         @deleted_line += 1
317       end
318     end
319   end