#!/usr/bin/ruby -w # -*- ruby -*- # $Id: p4delta.rb,v 1.2 2003/06/18 17:59:17 jeugenepace Exp $ # p4delta: summarizes Perforce changes and executes the appropriate commands # This program is as identical as possible to cvsdelta. Changes should be kept # in sync require 'getoptlong' require 'find' $verbose = false # whether to spew debugging output $stdout.sync = true # unbuffer output $stderr.sync = true # unbuffer output $PACKAGE = "p4delta" $VERSION = "1.3.2" require './progmet' require './regexp' require './file' require './p4ignore' ## ------------------------------------------------------- ## A file that has changed with respect to the configuration management system. ## # This can be one that has been added (a new file), changed (a previously # ## existing file), or deleted (one that has been removed). ## ------------------------------------------------------- class DeltaFile attr_accessor :adds, :changes, :deletes, :name def initialize(name) # in Ruby, these are primitives, not Objects, so they are not # referencing the same primitive value (i.e., this is just like Java) @adds = @changes = @deletes = 0 @name = File.clean_name(name) end def total @adds + @changes + @deletes end end class ExistingFile < DeltaFile def symbol; "*"; end end class DeletedFile < DeltaFile def initialize(name) super # it would be nice to know how long the file was, i.e., many lines were # deleted end def symbol; "-"; end end class NewFile < DeltaFile def initialize(name) super @adds = IO.readlines(name).length end def symbol; "+"; end end ## ------------------------------------------------------- ## Processing of "diff" output, either contextual ("long form") or unified ## (traditional). ## ------------------------------------------------------- class DiffOutput def initialize(total, regexp) @total = total @regexp = regexp @md = nil end def match(line) @md = @regexp.match(line) end def update(record) nlines = number_of_lines update_record(record, nlines) update_record(@total, nlines) end def to_s self.class.to_s + " " + @regexp.source end end class NormalDiffOutput < DiffOutput def initialize(total, letter) fmt = '(\d+)(?:,(\d+))?' re = Regexp.new("^" + fmt + letter + fmt) super(total, re) end # Returns the amount of lines that changed, based on the MatchData object # which is from standard diff output def number_of_lines from = diff_difference(1, 2) to = diff_difference(3, 4) 1 + [from, to].max end # Returns the difference between the two match data objects, which represent # diff output (3,4c4). def diff_difference(from, to) if @md[to] then @md[to].to_i - @md[from].to_i else 0 end end end class NormalDiffOutputAdd < NormalDiffOutput def initialize(total) super(total, 'a') end def update_record(rec, nlines) rec.adds += nlines end end class NormalDiffOutputChange < NormalDiffOutput def initialize(total) super(total, 'c') end def update_record(rec, nlines) rec.changes += nlines end end class NormalDiffOutputDelete < NormalDiffOutput def initialize(total) super(total, 'd') end def update_record(rec, nlines) rec.deletes += nlines end end class NormalDiffProcessor def initialize(total) name = self.class @addre = NormalDiffOutputAdd.new(total) @delre = NormalDiffOutputDelete.new(total) @chgre = NormalDiffOutputChange.new(total) end def get_tests(line) [ @addre, @chgre, @delre ] end end ## ------------------------------------------------------- ## Perforce-specific code ## ------------------------------------------------------- # A difference within a configuration management system. class P4Delta include Log attr_reader :added, :changed, :deleted, :total def initialize(options, args) log "" @options = options log "set options." # sanity check. p4info = `p4 info 2>&1` log "p4info: #{p4info}" if p4info.index(/command\s+not\s+found/) $stderr.puts "ERROR: p4 executable not in path." exit(2) elsif p4info.index(/Perforce client error:/) if ENV['P4PORT'] $stderr.puts "ERROR: invalid P4PORT: #{ENV['P4PORT']}" else $stderr.puts "ERROR: P4PORT environment variable not set." end exit(2) end # for showing that we're actually doing something @progress = if @options.quiet then nil else ProgressMeter.new(options.verbose) end log "" @ignored_patterns = IgnoredPatterns.new(".p4ignore") log "" log "args: #{args}" @args = if args.length > 0 then args else [ "." ] end end def run @added = FileHash.new @changed = FileHash.new @deleted = FileHash.new @total = DeltaFile.new("total") @warned = Array.new @entries = Array.new @entfiles = Array.new log "" diffprocessor = NormalDiffProcessor.new(@total) curfile = nil # backticks seem to work more consistenty than IO.popen, which was losing # lines from the diff output. dirs = @args.collect do |a| if File.is_file?(a) then File.dirname(a) else a end end dirs.uniq.each { |dir| @ignored_patterns.read(dir) } # directories get appended with "...", Perforce-speak for "*.*" log "args: #{@args}" fileargs = @args.collect { |a| File.is_file?(a) ? a : a + "/..." } cmd = "p4 diff " + (if @options.force_diff then "-f " else "" end) + fileargs.join(" ") + " 2>&1" lines = `#{cmd}` pwd = Dir.pwd @cwdre = Regexp.new(pwd + "/?") lines.each do |line| @progress.tick(line) if @progress log "line: " + line if line.index(/^====.* \- (.*) ====\s*$/) curfile = $1 curfile.sub!(@cwdre, "") else tests = diffprocessor.get_tests(line) tests.each do |re| if re.match(line) log re.to_s + ": matches: " + line rec = get_record(curfile) re.update(rec) else log re.to_s + ": not a match line: " + line end end end end # determine new files cmd = "p4 where . 2>&1" dep2dir = `#{cmd}` mapping = dep2dir.split(/\s+/) @depot = mapping[0] @dir = mapping[-1] cmd = "p4 files " + fileargs.join(" ") + " 2>&1" lines = `#{cmd}` @p4files = Array.new lines.each do |line| line.chomp! next if line.index(/no such file/) fname, status = depot_to_local(line) log "fname = #{fname}" if status == "delete" log "skipping #{fname}" else log "p4file: #{fname}" @p4files.push(fname) end end @args.each do |arg| if File.directory?(arg) dirs = Array.new File.find_files(arg).each do |f| dir = File.dirname(f) unless dirs.include?(dir) @ignored_patterns.read(dir) dirs.push(dir) end add_file(f) end else add_file(arg) end end @p4files.each do |p4f| unless File.exists?(p4f) add_deleted_file(p4f) end end end # Converts the depot file name to a local one, removing the current working # directory from the front of the file name. Returns the name and the current # status. def depot_to_local(dname) @progress.tick(dname) if @progress log "depot_to_local(#{dname})" fname = dname.sub(/\#\d+ +\- +(add|edit|delete)?.*/, "") status = $1 log "fname = #{fname}" log "status = #{status}" cmd = "p4 where \"" + fname + "\"" dep2dir = `#{cmd}` log "dep2dir = #{dep2dir}" depot = nil dir = nil dep2dir.split(/\n/).each do |d2d| mapping = d2d.split(/\s+/) depot = mapping[0] dir = mapping[-1] break if depot && dir && !depot.index(/^\-/) end log "depot = #{depot}" log "dir = #{dir}" log "cwdre = #{@cwdre.source}" log "fname0 = #{fname}" fname.sub!(Regexp.escape(depot), dir) log "fname1 = #{fname}" fname.sub!(@cwdre, "") log "fname2 = #{fname}" [ fname, status ] end # Returns whether the given file is included in Perforce. def is_file_included?(file) @p4files.include?(file) end def add_file(file) fname = File.clean_name(file) if is_file_included?(fname) log "file already in entries: " + fname elsif @ignored_patterns.is_ignored?(fname) log "file is ignored: " + fname else log "adding file: " + fname if File.readable?(fname) unless @added.include?(fname) # don't add it twice @added[fname] = NewFile.new(fname) @total.adds += @added[fname].adds end else unless @warned.include?(fname) puts "not readable: " + fname @warned.push(fname) end end end end def add_deleted_file(file) @deleted[file] = DeletedFile.new(file) end def get_record(file) @changed[file] = ExistingFile.new(file) unless @changed.include?(file) @changed[file] end def execute print "\nEXECUTING COMMANDS\n" print "\n ADDs\n" if @added.length > 0 execute_command(@added.keys, "add") end print "\n EDITs\n" if @changed.length > 0 opened = Array.new `p4 opened`.each do |opline| opline.chomp! fname = depot_to_local(opline)[0] opened.push(fname) end toedit = @changed.keys.select { |c| !opened.include?(c) } execute_command(toedit, "edit") end if @options.confirm dels = @deleted.keys.reject { |name| print "delete " + name + "? " ans = $stdin.readline ans.upcase[0, 1] != 'Y' } else dels = @deleted.keys end print "\n DELETEs\n" execute_command(dels, "delete") end # TODO: fix for spaces in names def execute_command(names, command) if names.size > 0 cmd = "p4 " + command + ' "' + names.join('" "') + '"' print " ", cmd, "\n" system(cmd) else log "no files to " + command end end def print_change_summary puts printf "%-7s %-7s %-7s %-7s %s\n", "total", "added", "changed", "deleted", "file" printf "======= ======= ======= ======= ====================\n" files = Hash.new [ @added, @changed, @deleted ].each do |ary| ary.each do |file, record| files[file] = record end end files.sort.each do |file, record| print_record(record) end printf "------- ------- ------- ------- --------------------\n"; print_record(@total, "Total") end def print_record(rec, name = nil) name = rec.symbol + " " + rec.name unless name [rec.total, rec.adds, rec.changes, rec.deletes].each do |v| printf("%7d ", v) end print name, "\n" end end