#!/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
syntax highlighted by Code2HTML, v. 0.9.1