From df4be45933baf61350d4f702fc8e914a7d6bb254 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 23 Apr 2009 17:12:00 +0000 Subject: [PATCH] - merged Changed files: cvsspam-svnspam-branch.diff -> 1.7 --- cvsspam-svnspam-branch.diff | 727 ------------------------------------ 1 file changed, 727 deletions(-) delete mode 100644 cvsspam-svnspam-branch.diff diff --git a/cvsspam-svnspam-branch.diff b/cvsspam-svnspam-branch.diff deleted file mode 100644 index 737ac0b..0000000 --- a/cvsspam-svnspam-branch.diff +++ /dev/null @@ -1,727 +0,0 @@ ---- svn_post_commit_hook.rb (.../trunk) (revision 0) -+++ svn_post_commit_hook.rb (.../branches/svn_support) (revision 269) -@@ -0,0 +1,412 @@ -+#!/usr/bin/ruby -w -+ -+$svnlook_exe = "svnlook" # default assumes the program is in $PATH -+ -+def usage(msg) -+ $stderr.puts(msg) -+ exit(1) -+end -+ -+def blah(msg) -+ if $debug -+ $stderr.puts "svn_post_commit_hook.rb: #{msg}" -+ end -+end -+ -+$debug = false -+$tmpdir = ENV["TMPDIR"] || "/tmp" -+$dirtemplate = "#svnspam.#{Process.getpgrp}.#{Process.uid}" -+# arguments to pass though to 'cvsspam.rb' -+$passthrough_args = [] -+ -+def make_data_dir -+ dir = "#{$tmpdir}/#{$dirtemplate}-#{rand(99999999)}" -+ Dir.mkdir(dir, 0700) -+ dir -+end -+ -+def init -+ $datadir = make_data_dir -+ -+ # set PWD so that svnlook can create its .svnlook directory -+ Dir.chdir($datadir) -+end -+ -+def cleanup -+ unless $debug -+ File.unlink("#{$datadir}/logfile") -+ Dir.rmdir($datadir) -+ end -+end -+ -+def send_email -+ cmd = File.dirname($0) + "/cvsspam.rb" -+ unless system(cmd,"--svn","#{$datadir}/logfile", *$passthrough_args) -+ fail "problem running '#{cmd}'" -+ end -+end -+ -+# Like IO.popen, but accepts multiple arguments like Kernel.exec -+# (So no need to escape shell metacharacters) -+def safer_popen(*args) -+ IO.popen("-") do |pipe| -+ if pipe==nil -+ exec(*args) -+ else -+ yield pipe -+ end -+ end -+end -+ -+ -+# Process the command-line arguments in the given list -+def process_args -+ require 'getoptlong' -+ -+ opts = GetoptLong.new( -+ [ "--to", "-t", GetoptLong::REQUIRED_ARGUMENT ], -+ [ "--config", "-c", GetoptLong::REQUIRED_ARGUMENT ], -+ [ "--debug", "-d", GetoptLong::NO_ARGUMENT ], -+ [ "--from", "-u", GetoptLong::REQUIRED_ARGUMENT ], -+ [ "--charset", GetoptLong::REQUIRED_ARGUMENT ] -+ ) -+ -+ opts.each do |opt, arg| -+ if ["--to", "--config", "--from", "--charset"].include?(opt) -+ $passthrough_args << opt << arg -+ end -+ if ["--debug"].include?(opt) -+ $passthrough_args << opt -+ end -+ $config = arg if opt=="--config" -+ $debug = true if opt == "--debug" -+ end -+ -+ $repository = ARGV[0] -+ $revision = ARGV[1] -+ -+ unless $revision =~ /^\d+$/ -+ usage("revision must be an integer: #{revision.inspect}") -+ end -+ $revision = $revision.to_i -+ -+ unless FileTest.directory?($repository) -+ usage("no such directory: #{$repository.inspect}") -+ end -+ $repository =~ /([^\/]+$)/ -+ $shortrepo = $1 -+end -+ -+# runs the given svnlook subcommand -+def svnlook(cmd, revision, *args) -+ rev = revision.to_s -+ safer_popen($svnlook_exe, cmd, $repository, "-r", rev, *args) do |io| -+ yield io -+ end -+end -+ -+class Change -+ def initialize(filechange, propchange, path) -+ @filechange = filechange -+ @propchange = propchange -+ @path = path -+ end -+ -+ attr_accessor :filechange, :propchange, :path -+ -+ def property_change? -+ @propchange != " " -+ end -+ -+ def file_change? -+ @filechange != "_" -+ end -+ -+ def addition? -+ @filechange == "A" -+ end -+ -+ def deletion? -+ @filechange == "D" -+ end -+end -+ -+ -+ -+# Line-oriented access to an underlying IO object. Remembers 'current' line -+# for lookahead during parsing. -+class LineReader -+ def initialize(io) -+ @io = io -+ end -+ -+ def current -+ @line -+ end -+ -+ def next_line -+ (@line = @io.gets) != nil -+ end -+ -+ def assert_current(re) -+ raise "unexpected #{current.inspect}" unless @line =~ re -+ $~ -+ end -+ -+ def assert_next(re=nil) -+ raise "unexpected end of text" unless next_line -+ unless re.nil? -+ raise "unexpected #{current.inspect}" unless @line =~ re -+ end -+ $~ -+ end -+end -+ -+ -+def read_modified_diff(out, lines, path) -+ lines.assert_next(/^=+$/) -+ lines.assert_next -+ if lines.current =~ /\(Binary files differ\)/ -+ process_modified_binary_diff(out, lines, path) -+ else -+ process_modified_text_diff(out, lines, path) -+ end -+end -+ -+ -+def process_modified_binary_diff(out, lines, path) -+ prev_rev= $revision-1 -+ next_rev= $revision -+ out.puts "#V #{prev_rev},#{next_rev}" -+ out.puts "#M #{$shortrepo}/#{path}" -+ out.puts "#U diff x x" -+ out.puts "#U Binary files x and y differ" -+end -+ -+ -+def process_modified_text_diff(out, lines, path) -+ m = lines.assert_current(/^---.*\(rev (\d+)\)$/) -+ prev_rev = m[1].to_i -+ diff1 = lines.current -+ m = lines.assert_next(/^\+\+\+.*\(rev (\d+)\)$/) -+ next_rev = m[1].to_i -+ diff2 = lines.current -+ out.puts "#V #{prev_rev},#{next_rev}" -+ out.puts "#M #{$shortrepo}/#{path}" -+ out.puts "#U #{diff1}" -+ out.puts "#U #{diff2}" -+ while lines.next_line && lines.current =~ /^[-\+ @\\]/ -+ out.puts "#U #{lines.current}" -+ end -+end -+ -+def read_added_diff(out, lines, path) -+ lines.assert_next(/^=+$/) -+ lines.assert_next -+ if lines.current =~ /\(Binary files differ\)/ -+ process_added_binary_diff(out, lines, path) -+ else -+ process_added_text_diff(out, lines, path) -+ end -+end -+ -+def process_added_binary_diff(out, lines, path) -+ next_rev= $revision -+ out.puts "#V NONE,#{next_rev}" -+ out.puts "#A #{$shortrepo}/#{path}" -+ out.puts "#U diff x x" -+ out.puts "#U Binary file x added" -+end -+ -+def process_added_text_diff(out, lines, path) -+ m = lines.assert_current(/^---.*\(rev (\d+)\)$/) -+ prev_rev = m[1].to_i -+ diff1 = lines.current -+ m = lines.assert_next(/^\+\+\+.*\(rev (\d+)\)$/) -+ next_rev = m[1].to_i -+ diff2 = lines.current -+ out.puts "#V NONE,#{next_rev}" -+ out.puts "#A #{$shortrepo}/#{path}" -+ out.puts "#U #{diff1}" -+ out.puts "#U #{diff2}" -+ while lines.next_line && lines.current =~ /^[-\+ @\\]/ -+ out.puts "#U #{lines.current}" -+ end -+end -+ -+def read_deleted_diff(out, lines, path) -+ lines.assert_next(/^=+$/) -+ m = lines.assert_next(/^---.*\(rev (\d+)\)$/) -+ prev_rev = m[1].to_i -+ diff1 = lines.current -+ m = lines.assert_next(/^\+\+\+.*\(rev (\d+)\)$/) -+ next_rev = m[1].to_i -+ diff2 = lines.current -+ out.puts "#V #{prev_rev},NONE" -+ out.puts "#R #{$shortrepo}/#{path}" -+ out.puts "#U #{diff1}" -+ out.puts "#U #{diff2}" -+ while lines.next_line && lines.current =~ /^[-\+ @\\]/ -+ out.puts "#U #{lines.current}" -+ end -+end -+ -+def read_property_lines(path, prop_name, revision) -+ lines = [] -+ svnlook("propget", revision, prop_name, path) do |io| -+ io.each_line do |line| -+ lines << line.chomp -+ end -+ end -+ lines -+end -+ -+def assert_prop_match(a, b) -+ if !b.nil? && a != b -+ raise "property mismatch: #{a.inspect}!=#{b.inspect}" -+ end -+end -+ -+# We need to read the property change from the output of svnlook, but have -+# a difficulty in that there's no unambiguous delimiter marking the end of -+# a potentially multi-line property value. Therefore, we do a seperate -+# svn propget on the given file to get the value of the property on its own, -+# and then use that value as a guide as to how much data to read from the -+# svnlook output. -+def munch_prop_text(path, prop_name, revision, lines, line0) -+ prop = read_property_lines(path, prop_name, revision) -+ if prop.empty? -+ assert_prop_match(line0, "") -+ return -+ end -+ assert_prop_match(line0, prop.shift) -+ prop.each do |prop_line| -+ lines.assert_next -+ assert_prop_match(lines.current.chomp, prop_line) -+ end -+end -+ -+def read_properties_changed(out, lines, path) -+ prev_rev= $revision-1 -+ next_rev= $revision -+ lines.assert_next(/^_+$/) -+ return unless lines.next_line -+ out.puts "#V #{prev_rev},#{next_rev}" -+ out.puts "#P #{$shortrepo}/#{path}" -+# The first three get consumed and not highlighted -+ out.puts "#U " -+ out.puts "#U Property changes:" -+ out.puts "#U " -+ -+ while true -+ break unless lines.current =~ /^(?:Name|Added|Deleted): (.+)$/ -+ -+ prop_name = $1 -+ m = lines.assert_next(/^ ([-+]) (.*)/) -+ op = m[1] -+ line0 = m[2] -+ if op == "-" -+ munch_prop_text(path, prop_name, $revision-1, lines, line0) -+ if lines.next_line && lines.current =~ /^ \+ (.*)/ -+ munch_prop_text(path, prop_name, $revision, lines, $1) -+ lines.next_line -+ end -+ else # op == "+" -+ munch_prop_text(path, prop_name, $revision, lines, line0) -+ lines.next_line -+ end -+ out.puts "#U #{m[1]} #{prop_name}:#{m[2]}" -+ end -+ out.puts "#U " -+end -+ -+def handle_copy(out, lines, path, from_ref, from_file) -+ prev_rev= $revision-1 -+ next_rev= $revision -+ out.puts "#V #{$shortrepo}/#{from_file}:#{prev_rev},#{next_rev}" -+ out.puts "#C #{$shortrepo}/#{path}" -+ if lines.next_line && lines.current =~ /^=+$/ -+ m = lines.assert_next(/^---.*\(rev (\d+)\)$/) -+ prev_rev = m[1].to_i -+ diff1 = lines.current -+ m = lines.assert_next(/^\+\+\+.*\(rev (\d+)\)$/) -+ next_rev = m[1].to_i -+ diff2 = lines.current -+ out.puts "#U #{diff1}" -+ out.puts "#U #{diff2}" -+ while lines.next_line && lines.current =~ /^[-\+ @\\]/ -+ out.puts "#U #{lines.current}" -+ end -+ else -+ out.puts "#U " -+ out.puts "#U Copied from #{$shortrepo}/#{from_file}:#{from_ref}" -+ out.puts "#U " -+ end -+end -+ -+def svnlook_author -+ svnlook("author", $revision) do |io| -+ return io.readline.chomp -+ end -+ nil -+end -+ -+def find_author -+ return if $passthrough_args.include?("--from") -+ author = svnlook_author -+ if author -+ blah("Author from svnlook: '#{author}'") -+ $passthrough_args << "--from" << author -+ end -+end -+ -+def process_svnlook_log(file) -+ svnlook("log", $revision) do |io| -+ io.each_line do |line| -+ file.puts("#> #{line}") -+ end -+ end -+end -+ -+def process_svnlook_diff(file) -+ svnlook("diff", $revision) do |io| -+ lines = LineReader.new(io) -+ while lines.next_line -+ if lines.current =~ /^Modified:\s+(.*)/ -+ read_modified_diff(file, lines, $1) -+ elsif lines.current =~ /^Added:\s+(.*)/ -+ read_added_diff(file, lines, $1) -+ elsif lines.current =~ /^Copied:\s+(.*) \(from rev (\d+), (.*)\)$/ -+ handle_copy(file, lines, $1, $2, $3) -+ elsif lines.current =~ /^Deleted:\s+(.*)/ -+ read_deleted_diff(file, lines, $1) -+ elsif lines.current =~ /^Property changes on:\s+(.*)/ -+ read_properties_changed(file, lines, $1) -+ elsif lines.current == "\n" -+ # ignore -+ else -+ raise "unable to parse line '#{lines.current.inspect}'" -+ end -+ end -+ end -+end -+ -+def process_commit() -+ File.open("#{$datadir}/logfile", File::WRONLY|File::CREAT) do |file| -+ process_svnlook_log(file) -+ process_svnlook_diff(file) -+ end -+end -+ -+ -+def main -+ init() -+ process_args() -+ find_author() -+ process_commit() -+ send_email() -+ cleanup() -+end -+ -+ -+main ---- cvsspam.rb (.../trunk) (revision 269) -+++ cvsspam.rb (.../branches/svn_support) (revision 269) -@@ -398,7 +398,7 @@ - - # the full path and filename within the repository - attr_accessor :path -- # the type of change committed 'M'=modified, 'A'=added, 'R'=removed -+ # the type of change committed 'M'=modified, 'A'=added, 'R'=removed, 'P'=properties, 'C'=copied - attr_accessor :type - # records number of 'addition' lines in diff output, once counted - attr_accessor :lineAdditions -@@ -453,17 +453,28 @@ - def removal? - @type == "R" - end -- -+ - # was this file added during the commit? - def addition? - @type == "A" - end - -+ # was this file copied during the commit? -+ def copied? -+ @type == "C" -+ end -+ - # was this file simply modified during the commit? - def modification? - @type == "M" - end -+ -+ # was this file simply modified during the commit? -+ def modifiedprops? -+ @type == "P" -+ end - -+ - # passing true, this object remembers that a diff will appear in the email, - # passing false, this object remembers that no diff will appear in the email. - # Once the value is set, it will not be changed -@@ -889,6 +900,15 @@ - end - end - -+# Note when LogReader finds record of a file that was copied in this commit -+class CopiedFileHandler < FileHandler -+ def handleFile(file) -+ file.type="C" -+ file.fromVer=$fromVer -+ file.toVer=$toVer -+ end -+end -+ - # Note when LogReader finds record of a file that was modified in this commit - class ModifiedFileHandler < FileHandler - def handleFile(file) -@@ -898,7 +918,16 @@ - end - end - -+# Note when LogReader finds record of a file whose properties were modified in this commit -+class ModifiedPropsFileHandler < FileHandler -+ def handleFile(file) -+ file.type="P" -+ file.fromVer=$fromVer -+ file.toVer=$toVer -+ end -+end - -+ - # Used by UnifiedDiffHandler to record the number of added and removed lines - # appearing in a unidiff. - class UnifiedDiffStats -@@ -1064,11 +1093,21 @@ - print($frontend.path($file.basedir, $file.tag)) - println("
") - println("
#{htmlEncode($file.file)} removed after #{$frontend.version($file.path,$file.fromVer)}
") -+ when "C" -+ print("") -+ print($frontend.path($file.basedir, $file.tag)) -+ println("
") -+ println("
#{htmlEncode($file.file)} copied from #{$frontend.version($file.path,$file.fromVer)}
") - when "M" - print("") - print($frontend.path($file.basedir, $file.tag)) - println("
") - println("
#{htmlEncode($file.file)} #{$frontend.version($file.path,$file.fromVer)} #{$frontend.diff($file)} #{$frontend.version($file.path,$file.toVer)}
") -+ when "P" -+ print("") -+ print($frontend.path($file.basedir, $file.tag)) -+ println("
") -+ println("
#{htmlEncode($file.file)} #{$frontend.version($file.path,$file.fromVer)} #{$frontend.diff($file)} #{$frontend.version($file.path,$file.toVer)}
") - end - print("
")
-     lines.each do |line|
-@@ -1329,6 +1368,7 @@
- $users_file_charset = nil
- 
- $debug = false
-+$svn = false
- $recipients = Array.new
- $sendmail_prog = "/usr/sbin/sendmail"
- $hostname = ENV['HOSTNAME'] || 'localhost'
-@@ -1366,6 +1406,7 @@
-   [ "--to",     "-t", GetoptLong::REQUIRED_ARGUMENT ],
-   [ "--config", "-c", GetoptLong::REQUIRED_ARGUMENT ],
-   [ "--debug",  "-d", GetoptLong::NO_ARGUMENT ],
-+  [ "--svn",    "-s", GetoptLong::NO_ARGUMENT ],
-   [ "--from",   "-u", GetoptLong::REQUIRED_ARGUMENT ],
-   [ "--charset",      GetoptLong::REQUIRED_ARGUMENT ]
- )
-@@ -1374,6 +1415,7 @@
-   $recipients << EmailAddress.new(arg) if opt=="--to"
-   $config = arg if opt=="--config"
-   $debug = true if opt=="--debug"
-+  $svn = true if opt=="--svn"
-   $from_address = EmailAddress.new(arg) if opt=="--from"
-   # must use different variable as the config is readed later.
-   $arg_charset = arg if opt == "--charset"
-@@ -1386,7 +1428,7 @@
-   else
-     $stderr.puts "missing required file argument"
-   end
--  puts "Usage: cvsspam.rb [ --to  ] [ --config  ] "
-+  puts "Usage: cvsspam.rb [ --svn ] [ --to  ] [ --config  ] "
-   exit(-1)
- end
- 
-@@ -1495,12 +1537,16 @@
- 		 "T" => tagHandler,
- 		 "A" => AddedFileHandler.new,
- 		 "R" => RemovedFileHandler.new,
-+		 "C" => CopiedFileHandler.new,
- 		 "M" => ModifiedFileHandler.new,
-+		 "P" => ModifiedPropsFileHandler.new,
- 		 "V" => VersionHandler.new]
- 
- $handlers["A"].setTagHandler(tagHandler)
- $handlers["R"].setTagHandler(tagHandler)
-+$handlers["C"].setTagHandler(tagHandler)
- $handlers["M"].setTagHandler(tagHandler)
-+$handlers["P"].setTagHandler(tagHandler)
- 
- $fileEntries = Array.new
- $task_list = Array.new
-@@ -1525,7 +1571,11 @@
- end
- 
- if $subjectPrefix == nil
--  $subjectPrefix = "[CVS #{Repository.array.join(',')}]"
-+  if $svn
-+    $subjectPrefix = "[SVN #{Repository.array.join(',')}]"
-+  else
-+    $subjectPrefix = "[CVS #{Repository.array.join(',')}]"
-+  end
- end
- 
- if $files_in_subject
-@@ -1572,6 +1622,8 @@
-   #removed {background-color:#ffdddd;}
-   #removedchars {background-color:#ff9999;font-weight:bolder;}
-   tr.alt #removed {background-color:#f7cccc;}
-+  #copied {background-color:#ccccff;}
-+  tr.alt #copied {background-color:#bbbbf7;}
-   #info {color:#888888;}
-   #context {background-color:#eeeeee;}
-   td {padding-left:.3em;padding-right:.3em;}
-@@ -1604,7 +1656,9 @@
- 
-   filesAdded = 0
-   filesRemoved = 0
-+  filesCopied = 0
-   filesModified  = 0
-+  filesModifiedProps  = 0
-   totalLinesAdded = 0
-   totalLinesRemoved = 0
-   file_count = 0
-@@ -1613,24 +1667,26 @@
-   $fileEntries.each do |file|
-     unless file.repository == last_repository
-       last_repository = file.repository
--      mail.print("")
-+      mail.print("")
-       if last_repository.has_multiple_tags
-         mail.print("Mixed-tag commit")
-       else
-         mail.print("Commit")
-       end
-       mail.print(" in #{htmlEncode(last_repository.common_prefix)}")
--      if last_repository.trunk_only?
--        mail.print(" on MAIN")
--      else
--        mail.print(" on ")
--        tagCount = 0
--        last_repository.each_tag do |tag|
--          tagCount += 1
--          if tagCount > 1
--            mail.print tagCount on MAIN")
-+        else
-+          mail.print(" on ")
-+          tagCount = 0
-+          last_repository.each_tag do |tag|
-+            tagCount += 1
-+            if tagCount > 1
-+              mail.print tagCountMAIN"
-           end
--          mail.print tag ? htmlEncode(tag) : "MAIN"
-         end
-       end
-       mail.puts("")
-@@ -1645,8 +1701,12 @@
-       filesAdded += 1
-     elsif file.removal?
-       filesRemoved += 1
-+    elsif file.copied?
-+      filesCopied += 1
-     elsif file.modification?
-       filesModified += 1
-+    elsif file.modifiedprops?
-+      filesModifiedProps += 1
-     end
-     name = htmlEncode(file.name_after_common_prefix)
-     slashPos = name.rindex("/")
-@@ -1666,6 +1726,8 @@
-       name = "#{name}"
-     elsif file.removal?
-       name = "#{name}"
-+    elsif file.copied?
-+      name = "#{name}"
-     end
-     mail.print("")
-     if file.has_diff?
-@@ -1675,11 +1737,18 @@
-     end
-     mail.print(" #{$frontend.log(file)}")
-     mail.print("")
--    if file.isEmpty
--      mail.print("[empty]")
-+    if file.copied?
-+      mail.print("[copied]")
-+    elsif file.isEmpty
-+      mail.print("[empty]")
-     elsif file.isBinary
--      mail.print("[binary]")
-+      mail.print("[binary]")
-     else
-+      if file.modifiedprops?
-+        mail.print("[props]")
-+      else
-+        mail.print("")
-+      end
-       if file.lineAdditions>0
-         totalLinesAdded += file.lineAdditions
-         mail.print("+#{file.lineAdditions}")
-@@ -1706,15 +1775,19 @@
-       mail.print("added #{$frontend.version(file.path,file.toVer)}")
-     elsif file.removal?
-       mail.print("#{$frontend.version(file.path,file.fromVer)} removed")
-+    elsif file.copied?
-+      mail.print("#{$frontend.version(file.path,file.fromVer)} #{$frontend.diff(file)} #{$frontend.version(file.path,file.toVer)}")
-     elsif file.modification?
-       mail.print("#{$frontend.version(file.path,file.fromVer)} #{$frontend.diff(file)} #{$frontend.version(file.path,file.toVer)}")
-+    elsif file.modifiedprops?
-+      mail.print("#{$frontend.version(file.path,file.fromVer)} #{$frontend.diff(file)} #{$frontend.version(file.path,file.toVer)}")
-     end
- 
-     mail.puts("")
-   end
-   if $fileEntries.size>1 && (totalLinesAdded+totalLinesRemoved)>0
-     # give total number of lines added/removed accross all files
--    mail.print("")
-+    mail.print("")
-     if totalLinesAdded>0
-       mail.print("+#{totalLinesAdded}")
-     else
-@@ -1731,7 +1804,7 @@
-   
-   mail.puts("")
- 
--  totalFilesChanged = filesAdded+filesRemoved+filesModified
-+  totalFilesChanged = filesAdded+filesRemoved+filesCopied+filesModified+filesModifiedProps
-   if totalFilesChanged > 1
-     mail.print("")
-     changeKind = 0
-@@ -1744,11 +1817,21 @@
-       mail.print("#{filesRemoved} removed")
-       changeKind += 1
-     end
-+    if filesCopied>0
-+      mail.print(" + ") if changeKind>0
-+      mail.print("#{filesCopied} copied")
-+      changeKind += 1
-+    end
-     if filesModified>0
-       mail.print(" + ") if changeKind>0
-       mail.print("#{filesModified} modified")
-       changeKind += 1
-     end
-+    if filesModifiedProps>0
-+      mail.print(" + ") if changeKind>0
-+      mail.print("#{filesModifiedProps} modified properties")
-+      changeKind += 1
-+    end
-     mail.print(", total #{totalFilesChanged}") if changeKind > 1
-     mail.puts(" files
") - end -- 2.43.0