-#!/bin/csh -f
+#!/usr/bin/env python
#
-# Sccs2rcs is a script to convert an existing SCCS
-# history into an RCS history without losing any of
-# the information contained therein.
+# sccs2rcs is a script to convert an existing SCCS history into an RCS
+# history without losing any of the information contained therein.
#
-# Things to note:
-# + The -b (batch) option can be used to tell this script to run
-# non-interactively. In this mode, it assumes that you do not
-# want to enter descriptions on initial checkins, nor change
-# the default keyword substitution
+# This tool has a complicated history for its size. It began life as a
+# script in C-shell written by a hacker named Ken Cox in 1991.
+#
+# In 1993 I applied some minor fixes for newer RCS versions and it
+# shipped as part of one of Rich Morin's Prime Time Freeware releases.
+#
+# At some undetermined time after 1993, Brian Berliner hacked Ken
+# Cox's original in a slightly different direction than mine, adding
+# more SCCS keyword translations and an improvement in dealing with
+# branches.
+#
+# In 2002 I fixed it not to require the Berkeley sccs(1) front end.
+# In 2003 I folded in a minor bug fix from Roger Pilkey.
+# In 2007 I merged in the missing features from Brian Berliner's version
+# and sent it to the GNU RCS project hoping they'd take it off my hands.
+#
+# Then it started to bother me that I was casting the thing loose in a
+# dying language, so I decided to port it to Python and clean it up.
+# I added error checking, added the comments-file processing, added
+# auto-checkout of RCS workfiles replacing SCCS ones, and removed
+# no-longer-needed options. Finally I made it able to parse tag data from
+# snapshots made by Emacs VC mode.
#
-# + The -u option will cause a copy of the file to be checked out
-# after the conversion, so if you started with a workfile in place,
-# you'll end up with one there.
+# Ken said:
+# "This file may be copied, processed, hacked, mutilated, and
+# even destroyed as long as you don't tell anyone you wrote it."
+# Those were more innocent days. This version is released under MIT terms.
#
+# Things to note:
# + It will NOT delete or alter your ./SCCS history under any circumstances.
#
# + Run in a directory where ./SCCS exists and where you can create ./RCS
# the date of a delta to be in sync, then run "sccs admin -z"
# on the thing.
#
-# + After everything finishes, ./SCCS will be moved to ./old-SCCS.
-#
-# This file may be copied, processed, hacked, mutilated, and
-# even destroyed as long as you don't tell anyone you wrote it.
-#
-# Ken Cox
-# Viewlogic Systems, Inc.
-# kenstir@viewlogic.com
-# ...!harvard!cg-atla!viewlog!kenstir
-#
# 1.1: I added some portability fixes and the -u option. More importantly,
# I removed the dependencies on the BSD/SV `sccs' utility; this script
# will now work on any system that has the base SCCS/RCS tools available.
# of this script, the one distributed with CVS. That and 4 SCCS
# keyword substitutions were we only thing it did that this didn't.
#
+# 1.4: Python rewrite describe aboved. Interface cleanup. Auto-checkout
+# of RCS workfiles corresponding to SCCS workfiles present when started.
+#
# Eric S. Raymond <esr@thyrsus.com>
#
# $Id$
+import os, sys, getopt, re, commands
-# Note: newer versions of RCS require century digits on the -d option
-# of ci. Losing SCCS doesn't supply them. Kluge around this.
-#
-set century = 19
-
-############################################################
-# Option handling
-#
-set nodesc = 0
-set nohack = 0
-set regen = 0
-while ($#argv)
+# Here are the translations for SCCS % keywords to RCS-style $ keywords.
+# The quotes surround the dollar signs are to fool RCS and later version-
+# control systems when the script is checked in. In the unlikely event
+# that you need more keyword translations, add them to the end of this list.
+# The ones this misses are %[ABCDFHLMPQRSTYZ]%; the script will issue
+# warnings if it sees them after translation.
+conversions = [
+ ('%W%[ \t]*%G%', "$""Id""$"),
+ ('%W%[ \t]*%E%', "$""Id""$"),
+ ('%W%', "$""Id""$"),
+ ('%Z%%M%[ \t]*%I%[ \t]*%G%', "$""SunId""$"),
+ ('%Z%%M%[ \t]*%I%[ \t]*%E%', "$""SunId""$"),
+ ('%M%[ \t]*%I%[ \t]*%G%', "$""Id""$"),
+ ('%M%[ \t]*%I%[ \t]*%E%', "$""Id""$"),
+ ('%M%', "$""RCSfile""$"),
+ ('%I%', "$""Revision""$"),
+ ('%G%', "$""Date""$"),
+ ('%E%', "$""Date""$"),
+ ('%U%', ""),
+ ]
+conversions = map(lambda (s, t): (re.compile(s), t), conversions)
- if ($argv[1] == "-b") then
- set nodesc = 1
- set nohack = 1
- endif
+months = ("Jan","Feb","Mar","Apr","May","Jun",
+ "Jul","Aug","Sep","Oct","Nov","Dec")
- if ($argv[1] == "-u") then
- set regen = 1
- endif
+def writeable(f):
+ return os.access(f, os.W_OK)
- shift
-end
+def sccsfile(f):
+ return os.path.join("SCCS", "s." + f)
-############################################################
-# Error checking
-#
-if (! -w .) then
- echo "Error: ./ not writeable by you."
- exit 1
-endif
-if (! -d SCCS) then
- echo "Error: ./SCCS directory not found."
- exit 1
-endif
-foreach f (*)
- if (-f SCCS/s.$f && -w $f) then
- echo "Error: $f is out for edit...clean up before converting."
- exit 1
- endif
-end
-if (-d RCS) then
- echo "Warning: RCS directory exists"
- if (`ls -a RCS | wc -l` > 2) then
- echo "Error: RCS directory not empty"
- exit 1
- endif
-else
- mkdir RCS
-endif
+def progress(msg):
+ global logtext
+ logtext += "sccs2rcs: " + msg + "\n"
+ print msg
-set months = (Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec)
+def get_output(command):
+ global logtext
+ logtext += "$ " + command
+ (st, out) = commands.getstatusoutput(command)
+ logtext += out + "\n"
+ return out + "\n"
-# Remove all files for which there exist SCCS masters
-foreach f (*)
- if (-f SCCS/s.$f) then
- rm $f
- endif
-end
+def die(msg):
+ print >>sys.stderr, "sccs2rcs: fatal error, ", msg
+ for filename in os.listdir("."):
+ if os.path.exists(sccsfile(filename)) and writeable(filename):
+ get_output("unget " + sccsfile(filename))
+ print >>sys.stderr, """\
+Danger! Danger!
+Incomplete history in ./RCS -- remove it.
+Original unchanged history in ./SCCS.
+"""
+ if logtext:
+ print "Log follows:"
+ print logtext
+ print "Log ends here."
+ sys.exit(1)
-set logfile = /tmp/sccs2rcs_$$_log
-rm -f $logfile
-set tmpfile = /tmp/sccs2rcs_$$_tmp
-rm -f $tmpfile
-set initialfile = /tmp/sccs2rcs_$$_init
-echo "Initial revision" > $initialfile
-set sedfile = /tmp/sccs2rcs_$$_sed
-rm -f $sedfile
+def warn(msg):
+ print >>sys.stderr, "sccs2rcs: warning,", msg
-# the quotes surround the dollar signs are to fool RCS when I check in this script
-set sccs_keywords = (\
- '%W%[ ]*%G%'\
- '%W%[ ]*%E%'\
- '%W%'\
- '%Z%%M%[ ]*%I%[ ]*%G%'\
- '%Z%%M%[ ]*%I%[ ]*%E%'\
- '%M%[ ]*%I%[ ]*%G%'\
- '%M%[ ]*%I%[ ]*%E%'\
- '%M%'\
- '%I%'\
- '%G%'\
- '%E%'\
- '%U%')
-set rcs_keywords = (\
- '$'Id'$'\
- '$'Id'$'\
- '$'Id'$'\
- '$'SunId'$'\
- '$'SunId'$'\
- '$'Id'$'\
- '$'Id'$'\
- '$'RCSfile'$'\
- '$'Revision'$'\
- '$'Date'$'\
- '$'Date'$'\
- '')
+def get_output_or_die(command):
+ global logtext
+ logtext += "$ " + command
+ (st, out) = commands.getstatusoutput(command)
+ if st != 0:
+ die("'%s' failed" % command)
+ logtext += out + "\n"
+ return out + "\n"
+if __name__ == '__main__':
+ comments = None
+ (options, arguments) = getopt.getopt(sys.argv[1:], "c:")
+ for (switch, val) in options:
+ if switch == '-c':
+ comments = val
-############################################################
-# Get some answers from user
-#
-if ($nodesc == 0) then
- echo ""
- echo "Do you want to be prompted for a description of each"
- echo "file as it is checked in to RCS initially?"
- echo -n "(y = prompt for description, n = null description) [y]? "
- set ans = $<
- if ((_$ans == _n) || (_$ans == _N)) then
- set nodesc = 1
- endif
-endif
+ logtext = ""
-if ($nohack == 0) then
- echo ""
- echo "The default keyword substitutions are as follows and are"
- echo "applied in the order specified:"
- set i = 1
- while ($i <= $#sccs_keywords)
-# echo ' '\"$sccs_keywords[$i]\"' ==> '\"$rcs_keywords[$i]\"
- echo " $sccs_keywords[$i] ==> $rcs_keywords[$i]"
- @ i = $i + 1
- end
- echo ""
- echo -n "Do you want to change them [n]? "
- set ans = $<
- if ((_$ans != _) && (_$ans != _n) && (_$ans != _N)) then
- echo "You can't always get what you want."
- echo "Edit this script file and change the variables:"
- echo ' $sccs_keywords'
- echo ' $rcs_keywords'
- else
- echo "Good idea."
- endif
-endif
+ # Sanity checks on the directory
+ if not writeable("."):
+ die("./ is not writeable by you.")
+ elif not os.path.isdir("SCCS"):
+ die("no SCCS directory to convert.")
+ for filename in os.listdir("SCCS"):
+ if filename.startswith("s.") and writeable(filename):
+ die(filename + " is locked for edit...unlock before converting.")
+ if os.path.isdir("RCS"):
+ warn("RCS directory already exists.")
+ #if len(os.listdir("RCS")) > 2: # Allow . and ..
+ # die("RCS directory is nonempty.")
+ else:
+ try:
+ os.mkdir("RCS")
+ except OSError:
+ die("failed while attempting to create RCS directory.")
-# create the sed script
-set i = 1
-while ($i <= $#sccs_keywords)
- echo "s,$sccs_keywords[$i],$rcs_keywords[$i],g" >> $sedfile
- @ i = $i + 1
-end
+ # User may have supplied a comments file
+ initstrings = {}
+ if comments:
+ for line in open(comments):
+ try:
+ line = line.strip()
+ if not line: # ignore blank lines
+ continue
+ (filename, initstring) = line.split(":")
+ except ValueError:
+ die("ill-formed line '%s' in comments file" % line)
+ if filename in initstrings:
+ die("duplicate filename in initstrings")
+ initstrings[filename] = initstrings
-mkdir old-SCCS
+ # If the CSSC package is installed, the SCCS commands might be here.
+ # This assumption works under Debian/Ubuntu and will be harmless elsewhere.
+ os.environ["PATH"] += ":/usr/lib/cssc"
-############################################################
-# Loop over every s-file in SCCS dir
-#
-foreach sfile (SCCS/s.*)
- # get rid of the "s." at the beginning of the name
- set file = `echo $sfile:t | sed -e "s/^..//"`
+ # Loop over every s-file in SCCS dir
+ for sfile in filter(lambda f: f.startswith("s."), os.listdir("SCCS")):
+ file = sfile[2:]
+ firsttime = True
- # work on each rev of that file in ascending order
- set firsttime = 1
- foreach rev (`prs SCCS/s.$file | grep "^D " | awk '{print $2}' | tac`)
- if ($status != 0) goto ERROR
+ # Extract a list of revisions from the master
+ revisions = []
+ try:
+ for line in os.popen("prs " + sccsfile(file), "r"):
+ if line.startswith("D "):
+ revisions.append(line.split()[1])
+ except OSError:
+ die("open of master file %s failed" % sfile)
+ except IndexError:
+ die("garbled revision line in master file " + sfile)
+ revisions.reverse()
- # get file into current dir and get stats
- set date = (`prs -d":Dd: :Dm: :Dy: :T: :P:" -r$rev SCCS/s.$file`)
- set day = $date[1]
- set month = $date[2]
- if ($date[3] < 10) then
- set next_century = (`expr $century + 1`)
- set year = ${next_century}${date[3]}
- else
- set year = ${century}${date[3]}
- endif
- set time = $date[4]
- set author = $date[5]
- set date = "$day $months[$month] $year $time"
+ # Now use prs to run through the revision list extracting data
+ progress("%s revisions: %s\n" % (file, ", ".join(revisions)))
+ for rev in revisions:
+ cmd = 'prs -d":Dd: :Dm: :Dy: :T: :P:" -r%s SCCS/s.%s' % (rev, file)
+ date = get_output_or_die(cmd)
+ try:
+ (day, month, year, tod, author) = date.split()
+ except ValueError:
+ die("prs output %s from '%s'was ill-formed" % (`date`, cmd))
- echo ""
- echo "==> file $file, rev=$rev, date=$date, author=$author"
- get -e -r$rev SCCS/s.$file >>& $logfile
- if ($status != 0) goto ERROR
- echo checked out of SCCS
+ # We assume here that no SCCS archives were made before 1970.
+ # If you're converting an SCCS archive made after 2070, you lose.
+ if int(year) < 70:
+ year = "20" + year
+ else:
+ year = "19" + year
- # add RCS keywords in place of SCCS keywords
- sed -f $sedfile $file > $tmpfile
- if ($status != 0) goto ERROR
- echo performed keyword substitutions
- cp $tmpfile $file
+ rcsdate = day + " " + months[int(month)] + " " + year + " " + tod
+ progress("==> file %s, rev=%s, date=%s, author=%s" \
+ % (file, rev, rcsdate, author))
+ get_output_or_die("get -e -r%s SCCS/s.%s" % (rev, file))
+ print "checked out of SCCS"
- # check file into RCS
- if ($firsttime) then
- set firsttime = 0
- if ($nodesc == 1) then
- ci -f -r$rev -d"$date" -w$author -t/dev/null $file <$initialfile >>& $logfile
- if ($status != 0) goto ERROR
- echo initial rev checked into RCS without description
- else
- echo ""
- echo Enter a brief description of the file $file \(end w/ Ctrl-D\):
- cat > $tmpfile
- ci -f -r$rev -d"$date" -w$author -t$tmpfile $file <$initialfile >>& $logfile
- if ($status != 0) goto ERROR
- echo initial rev checked into RCS
- endif
- else
- # get RCS lock
- set lckrev = `echo $rev | sed -e 's/\.[0-9]*$//'`
- if ("$lckrev" =~ [0-9]*.*) then
- # need to lock the brach -- it is OK if the lock fails
- rcs -l$lckrev $file >>& $logfile
- else
- # need to lock the trunk -- must succeed
- rcs -l $file >>& $logfile
- if ($status != 0) goto ERROR
- endif
- echo got lock
- prs -r$rev SCCS/s.$file | grep "." > $tmpfile
- # it's OK if grep fails here and gives status == 1
- # put the delta message in $tmpfile
- ed $tmpfile >>& $logfile <<EOF
-/COMMENTS
-1,.d
-w
-q
-EOF
- ci -f -r$rev -d"$date" -w$author $file < $tmpfile >>& $logfile
- if ($status != 0) goto ERROR
- echo checked into RCS
- endif
- unget SCCS/s.$file >>& $logfile
- if ($status != 0) goto ERROR
- end
- rm -f $file
- mv $sfile old-SCCS
- if ($regen == 1) then
- co $file
- endif
-end
+ # replace SCCS keywords with RCS keywords
+ try:
+ rfp = open(file, "r")
+ filetext = rfp.read()
+ rfp.close()
+ for (regexp, rcskey) in conversions:
+ filetext = regexp.sub(rcskey, filetext)
+ wfp = open(file, "w")
+ wfp.write(filetext)
+ wfp.close()
+ except OSError:
+ die("keyword substitution failed")
+ progress("performed keyword substitutions")
+ # check file into RCS
+ if firsttime:
+ firsttime = False
+ if file in initstrings:
+ cmd = 'ci -f -r%s -d"%s" -w%s -t-%s %s' \
+ % (rev, date, author, initstrings[file], file)
+ else:
+ if initstrings:
+ warn("no description for %s in comments file" % file)
+ cmd = 'ci -f -r%s -d"%s" -w%s -t/dev/null %s' \
+ % (rev, date, author, file)
+ cmd = "echo 'Initial revision.' | " + cmd
+ get_output_or_die(cmd)
+ else:
+ # get RCS lock
+ revparts = rev.split(".")
+ lckrev = ".".join(revparts[:-1])
+ if re.match(r"[0-9]+\."):
+ # need to lock the branch -- it is OK if the lock fails
+ get_output("rcs -l%s %s" % (lckrev, file))
+ progress("got branch lock")
+ else:
+ # need to lock the trunk -- must succeed
+ get_output_or_die("rcs -l %s" % (file,))
+ progress("got trunk lock")
+ # Extract the delta message for this revision
+ deltamsg = get_output_or_die("prs -r$rev SCCS/s.$file")
+ deltalines = deltamsg.split("\n")
+ while deltalines:
+ deltalines.pop(0)
+ if deltalines[0].startswith("COMMENTS"):
+ break
+ deltamsg = deltalines.join("\n")
+ cmd = 'ci -f -r%s -d"%s" -w%s %s' % (rev, date, author, file)
+ # FIXME: output of this command isn't logged
+ progress("$ " + cmd)
+ try:
+ cfp = os.popen(cmd, "w")
+ cfp.write(deltamsg)
+ cfp.close()
+ except OSError:
+ die("RCS checkin of delta failed")
+ # We're done
+ logtext += get_output_or_die("unget " + sccsfile(file))
-############################################################
-# Clean up
-#
-echo cleaning up...
-rm -f $tmpfile $initialfile $sedfile
-echo ===================================================
-echo "Conversion of `pwd` OK"
-echo ===================================================
-exit 0
+ # Replace all files for which there exist SCCS masters,
+ progress("replacing workfiles")
+ for filename in os.listdir("."):
+ if os.exists(sccsfile(filename)):
+ os.remove(filename)
+ os.system("co " + file)
+ print progress("done.")
-ERROR:
-foreach f (*)
- if (-f SCCS/s.$f && -w $f) then
- unget SCCS/s.$f
- endif
-end
-echo ""
-echo ""
-echo Danger\! Danger\!
-echo Some command exited with a non-zero exit status.
-echo Log file exists in $logfile.
-echo ""
-echo Incomplete history in ./RCS -- remove it
-echo Original unchanged history in ./SCCS
-exit 1
+# sccs2rcs ends here