#!/bin/sh
exec ruby -w -x $0 ${1+"$@"} # -*- ruby -*-
#!ruby -w
# $Id: p4deltarun.rb,v 1.2 2003/06/18 17:59:17 jeugenepace Exp $
# p4delta: summarizes Perforce changes and executes the appropriate commands
# Returns the home directory, for both Unix and Windows.
def home_directory
if hm = ENV["HOME"]
return hm
else
hd = ENV["HOMEDRIVE"]
hp = ENV["HOMEPATH"]
if hd || hp
return (hd || "") + (hp || "\\")
else
return nil
end
end
end
# Very minimal logging output. If $verbose is set, this displays the method and
# line number whence called.
module Log
@@verbose = false
@@width = 0
@@output = $stdout
@@fmt = "[%s:%04d] {%s} %s\n"
@@align = false
def Log.verbose=(v)
@@verbose = v
end
def Log.output=(fname)
@@output = File.new(fname, "w")
end
def Log.set_widths(file_width, line_width, func_width)
@@fmt = "[%#{file_width}s:%#{line_width}d] {%#{func_width}s} %s\n"
end
def Log.align
@@align = true
end
def Log.log(msg)
if @@verbose
c = caller(1)[0]
c.index(/(.*):(\d+)(?::in \`(.*)\')?/)
file, line, func = $1, $2, $3
file.sub!(/.*\//, "")
if @@align
@@width = [ @@width, func.length ].max
@@output.printf "[%s:%04d] {%-*s} %s\n", file, line, @@width, func, msg.chomp
else
@@output.printf @@fmt, file, line, func, msg.chomp
end
end
end
def log(msg)
if @@verbose
c = caller(1)[0]
c.index(/(.*):(\d+)(?::in \`(.*)\')?/)
file, line, func = $1, $2, $3
func = self.class.to_s + "#" + func
file.sub!(/.*\//, "")
if @@align
@@width = [ @@width, func.length ].max
@@output.printf "[%s:%04d] {%-*s} %s\n", file, line, @@width, func, msg.chomp
else
@@output.printf @@fmt, file, line, func, msg.chomp
end
end
end
protected
end
class P4DeltaOptions
include Log
attr_accessor :confirm
attr_accessor :execute
attr_accessor :force_diff
attr_accessor :package
attr_accessor :quiet
attr_accessor :verbose
attr_accessor :version
def initialize(package = "undef", version = "1.2.3.4")
@package = package
@version = version
@verbose = false # whether to spew debugging output
@quiet = false # whether to suppress warnings
@confirm = false # whether to confirm remove commands
@execute = false # whether to execute
@force_diff = false # whether to do a forced-diff
end
def run
read_rcfile
read_environment_variable
read_options
end
def read_environment_variable
# process the environment variable
if ENV["P4DELTAOPTS"]
log "reading environment variable"
options = ENV["P4DELTAOPTS"].split(/\s+/)
while options.length > 0
opt = options.shift
Log.log "processing opt " + opt
arg = options.shift
process_option(opt, options)
end
end
end
def read_options
log "reading options"
while ARGV.length > 0
arg = ARGV.shift
log "processing arg #{arg}"
if process_option(arg, ARGV)
ARGV.unshift(arg)
break
end
end
log "done reading options"
end
# Returns whether the value matches a true value, such as "yes", "true", or
# "on".
def to_boolean(value)
[ "yes", "true", "on" ].include?(value.to_s.downcase)
end
def process_option(opt, args = nil)
opt.gsub!(/^\-+/, "")
case opt
when "e", "execute"
@execute = true
when "h", "help"
show_help
exit
when "f", "force"
@force_diff = true
when "q", "quiet"
@quiet = true
when "V", "verbose"
@verbose = true
when "v", "version"
print @package, ", version ", @version, "\n"
print "Written by Jeff Pace (jpace@incava.org).\n"
print "Released under the Lesser GNU Public License.\n"
exit 1
else
return true
end
return false
end
def read_rc_file(rc)
log "reading rc file #{rc}"
IO.readlines(rc).each do |line|
line.sub!(/\s*#.*/, "")
line.chomp!
name, value = line.split(/\s*[=:]\s*/)
next unless name && value
case name
when "execute"
@execute = to_boolean(value)
when "quiet"
@quiet = to_boolean(value)
when "verbose"
@verbose = to_boolean(value)
when /^force/
@force_diff = to_boolean(value)
end
end
end
def read_rcfile
# process the rc file
if hd = home_directory
rc = hd + "/.p4deltarc"
Log.log "reading RC file: #{rc}"
if File.exists?(rc)
read_rc_file(rc)
else
log "no such file: #{rc}"
end
end
end
def show_help
puts "USAGE"
puts " p4delta [options] directory..."
puts ""
puts "OPTIONS"
puts " -c, --confirm"
puts " Confirm deletions from Perforce. Valid only with the execute option."
puts ""
puts " -e, --execute"
puts " Run the add and remove commands for the appropriate files."
puts ""
puts " -f, --force"
puts " Compare all local files against those in Perforce."
puts ""
puts " -h, --help"
puts " Display this help message."
puts ""
puts " -q, --quiet"
puts " Run with minimum output."
puts ""
puts " -v, --version"
puts " Display the version and exit."
puts ""
puts " -V, --verbose"
puts " Run with maximum output."
puts ""
puts "See the man page for more information."
puts ""
end
end
# $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"
# A primitive "progress meter", for showing when something time-consuming is
# being done. If the global variable $verbose is set, then the tick() method
# displays the argument passed. If not set, the tick() method displays the
# spinning character with each tick.
class ProgressMeter
def initialize(verbose)
@progress = %w{ | \\ - / }
@count = 0
@verbose = verbose
end
def tick(what = "...")
if @verbose
# what.chomp!
# Log.log "processing #{what}"
else
print "\r"
@count = (@count + 1) % 4
print @progress[@count]
end
end
end
# Extended so that we can convert "Unix" (shell, actually) regular expressions
# ("*.java") to Ruby regular expressions ("/\.java$/").
class Regexp
# shell expressions to Ruby regular expressions
@@sh2re = Hash[
'*' => '.*',
'?' => '.',
'[' => '[',
']' => ']',
'.' => '\.',
'$' => '\$',
'/' => '\/'
]
# Returns a regular expression for the given Unix file system expression.
def Regexp.unixre_to_string(pat)
str = pat.gsub(/(\\.)|(.)/) do
if $1
$1
else
if @@sh2re.has_key?($2) then
@@sh2re[$2]
else
$2
end
end
end
str
end
end
module FileTester
# the percentage of characters that we allow to be odd in a text file
ODD_FACTOR = 0.3
# how many bytes (characters) of a file we test
TEST_LENGTH = 1024
# extensions associated with files that are always text:
KNOWN_TEXT = %w{ txt c cpp mk h hpp html java }
# extensions associated with files that are never text:
KNOWN_NONTEXT = %w{ a o obj class elc gif gz jar jpg jpeg png pdf tar Z }
# returns if the given file is nothing but text (ASCII).
def FileTester.text?(file)
# Don't waste our time if it doesn't even exist:
return false unless File.exists?(file)
if file.index(/\.(\w+)\s*$/)
suffix = $1
return true if KNOWN_TEXT.include?(suffix)
return false if KNOWN_NONTEXT.include?(suffix)
end
ntested = 0
nodd = 0
f = File.new(file)
f.each do |line|
# split returns strings, whereas we want characters (bytes)
chars = line.split(//, TEST_LENGTH).collect { |w| w[0] }
# using the limit parameter to split results in the last character being
# "0" (nil), so remove it
if chars.size > 1 and chars[-1].to_i == 0
chars = chars[0 .. -2]
end
chars.each do |ch|
ntested += 1
# never allow null in a text file
return false if ch.to_i == 0
nodd += 1 unless FileTester.ascii?(ch)
return FileTester.summary(nodd, ntested) if ntested >= TEST_LENGTH
end
end
return FileTester.summary(nodd, ntested)
end
def FileTester.summary(nodd, ntested)
return nodd < ntested * ODD_FACTOR
end
# returns if the given character is ASCII.
def FileTester.ascii?(c)
# from ctype.h
return (c.to_i & ~0x7f) == 0
end
end
# Additions to the File built-in class.
class File
include Log
# Returns a File::Stat object, or null if there were errors (such as the file
# not existing, access denied, etc.).
def File.status(fd)
begin
return File.stat(fd)
rescue
# ignore files that could not be read, etc.
return nil
end
end
# Returns whether the given object is a file. Ignores errors.
def File.is_file?(fd)
fs = File.status(fd)
return fs && fs.file?
end
# Returns whether the given object is a directory. Ignores errors.
def File.is_directory?(fd)
fs = File.status(fd)
return fs && fs.directory?
end
# Returns an array containing each of the names for which the associated block
# returned true.
def File.find_where(dir)
names = Array.new
Find.find(dir) do |f|
names.push(f) if yield(f)
end
names
end
# Returns an array of all files under the given directory.
def File.find_files(dir)
File.find_where(dir) { |f| is_file?(f) }
end
# Returns an array of all directory under the given directory.
def File.find_directories(dir)
File.find_where(dir) { |f| is_directory?(f) }
end
# Returns an array of all files within the given directory.
def File.local_files(dir)
Dir.new(dir).find_all { |f| is_file?(f) }
end
# Strips the PWD and the leading ./
def File.clean_name(fname)
Log.log "fname: #{fname}"
file = fname.dup
file.gsub!(Dir.pwd, "")
file.gsub!(/^\//, "")
file.sub!(/^\.\//, "")
file
end
def File.is_text?(fname)
FileTester.text?(fname)
end
end
# A hash that ensures that we use file name of the form: "foo/Bar", not
# "./foo/Bar".
class FileHash < Hash
def []=(f, value)
fname = File.clean_name(f)
super(fname, value)
end
def [](f)
fname = File.clean_name(f)
super(fname)
end
end
# An array that ensures that we use file name of the form: "foo/Bar", not
# "./foo/Bar".
class FileArray < Array
def []=(index, name)
file = File.clean_name(f)
super(index, name)
end
def push(name)
fname = File.clean_name(name)
super(fname)
end
end
# Directories listed so that the parents are first in the list.
class OrderedDirectoryList < Array
def initialize(files)
files.each { |f| add(File.dirname(f)) }
end
# add a directory
def add(dir)
if dir && !File.exists?(dir + "/CVS/Entries")
# TODO: remove the CVS-icity of this:
# attempt to add the parent, unless this is "."
# note: this won't work if dir == "."
if dir == "."
puts "ERROR: Cannot process files from within a directory"
puts "not in CVS. Please move up to the parent directory"
puts "and retry."
exit
end
add(File.dirname(dir))
pos = index(dir)
if pos
# nothing to do; dir is already in the list
else
pdpos = index(File.dirname(dir))
if pdpos
# parent already in the list, so insert this dir immediately afterward
self[pdpos + 1, 0] = dir
else
# prepending
unshift(dir)
end
end
end
end
end
# Represents .p4ignore files, which are modeled on .cvsignore files.
class IgnoredPatterns < Hash
include Log
def initialize(ignorename)
@ignorename = ignorename
@dirsread = Array.new
end
def read(dir)
# from the CVS default settings -- ignoring overrides
log "reading ignored patterns for " + dir
return if @dirsread.include?(dir)
@dirsread.push(dir)
pats = %w{
CVS
*~
.p4ignore
.p4config
*.o
*$
*.BAK
*.Z
*.a
*.bak
*.elc
*.exe
*.ln
*.obj
*.olb
*.old
*.orig
*.rej
*.so
.
..
.del-*
.make.state
.nse_depinfo
CVS.adm
RCS
RCSLOG
SCCS
TAGS
_$*
core
cvslog.*
tags
}
# can't put these guys in the qw() list:
['.#*', '#*', ',*'].each { |p| pats.push(p) }
# read ~/<ignore>
homedir = ENV["HOME"] # unix
unless homedir # windows
homedir = ENV["HOMEDRIVE"]
homepath = ENV["HOMEPATH"]
if homepath then
if homedir then
homedir += homepath
else
homedir = homepath
end
end
end
global = read_ignore_file(homedir)
pats.push(*global) unless global.length == 0
# read <ignore> in the current directory
local = read_ignore_file(dir)
pats.push(*local) unless local.length == 0
# prepend the current directory to the patterns, contending with the fact
# that the directory might actually be a valid regular expression.
# wildcard if the pattern is a directory
pats = pats.collect do |p|
p += "/*" if File.directory?(dir + "/" + p)
p
end
qdir = Regexp.quote(dir)
pats = pats.collect do |p|
p = Regexp.unixre_to_string(p)
qdir + "/" + p
end
# make a regular expression for each one, to be the entire string (^...$)
self[dir] = Array.new
pats.each do |p|
re = Regexp.new("^" + p + "$")
# log "IgnoredPatterns: storing re " + re.source + " for dir " + dir
self[dir].push(re)
end
end
def read_ignore_file(dir)
pats = Array.new
if dir then
cifile = dir + "/" + @ignorename
if File.exists?(cifile)
IO.foreach(cifile) do |line|
line.chomp!
line.gsub!(/\+/, '\\+')
pats.push(*line.split)
end
else
log "no ignore file in " + dir
end
end
pats
end
# Returns if the file is ignored. Checks the name as both "./name" and "name".
def is_ignored?(name)
log "is_ignored?(" + name + ")"
if name.index("./") == 0
withpref, nopref = name, name.sub!("./", "")
else
withpref, nopref = "./" + name, name
end
[ withpref, nopref ].each do |name|
dir = name
log "dirs = " + keys.join(", ")
while dir = File.dirname(dir)
if include?(dir)
regexps = self[dir]
regexps.each do |re|
# log "matching " + name + " against " + re.source
# stop as soon as we find out it is ignored
return true if re.match(name)
end
else
log "dir " + dir + " is not included"
end
break if dir == "." # else we'll cycle continuously
end
end
return false # it's not ignored
end
end
## -------------------------------------------------------
## 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
$stdout.sync = true # unbuffer
$stderr.sync = true # unbuffer
$stdin.sync = true # unbuffer
$PACKAGE = "p4delta"
$VERSION = "1.3.2"
begin
trap("INT") do
# This bypasses the stack trace on exit.
abort
end
Log.log "creating options"
Log.log "ARGV: #{ARGV}"
options = P4DeltaOptions.new($PACKAGE, $VERSION)
Log.log "running options"
options.run
Log.log "done running options"
Log.verbose = options.verbose
# Log.output = "/tmp/p4delta.log." + Process.pid.to_s
Log.set_widths(-12, 5, -35)
Log.log "creating p4delta"
Log.log "ARGV: #{ARGV}"
delta = P4Delta.new(options, ARGV)
Log.log "running p4delta"
delta.run
Log.log "printing change summary"
delta.print_change_summary
if options.execute
delta.execute
end
rescue => e
# show only the message, not the stack trace:
$stderr.puts "error: #{e}"
end
syntax highlighted by Code2HTML, v. 0.9.1