diff --git a/CHANGES.txt b/CHANGES.txt index af7de9c..9be0fea 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1 +1,3 @@ +v0.5.1, 2014-11-18 -- Fix import problem + v0.5.0, 2014-11-18 -- Initial release diff --git a/editrcs/__init__.py b/editrcs/__init__.py old mode 100644 new mode 100755 index b7db254..77bb375 --- a/editrcs/__init__.py +++ b/editrcs/__init__.py @@ -1 +1,1093 @@ -# Empty +#!/usr/bin/python +############################################################################ +# EditRCS: Python library to read, manipulate and write RCS ,v files +# +# Copyright (C) 2014 Ben Cohen +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +############################################################################ + +"""EditRCS: Python library to read, manipulate and write RCS ,v files + +EditRCS is a library to with functions to read RCS files into Python +classes Rcs and RcsDelta, and to manipulate them and write them to a new +RCS file. + +It is intended to be used to manipulate RCS files in ways that the RCS tools +don't support, but it requires some knowledge of how RCS files work rather +than being a polished command-line tool. + +See the rcsfile(5) manpage for RCS file format definition. See also +https://www.gnu.org/software/rcs/manual/ and +http://www.gnu.org/software/rcs/tichy-paper.pdf + +Limitations: I'm going with the 5.9.2 version of the manpage, and ignoring +the newphrase in the earlier (e.g. 5.6) specifications for now. +It is written to be simple rather than optimised for speed or space.""" + +import re +import os +import sys +from threading import Thread + +############################################################################ +# Utility functions +############################################################################ + +def IsVisibleChar(c): + """Return whether the character c is a visible character, as defined in + rcsfile(5).""" + return 33 <= ord(c) <= 126 or 160 <= ord(c) <= 377 + + +def IsWhitespaceChar(c): + """Return whether the character c is a whitespace character, as defined in + rcsfile(5).""" + return 8 <= ord(c) <= 13 or ord(c) == 32 + + +def AddAtQuoting(s): + """Return the string s quoted for RCS, i.e. delimited by the at symbol '@' + with any at symbols doubled.""" + return '@' + s.replace('@', '@@') + '@' + + +def RemoveAtQuoting(s): + """Return the string s with RCS at quoting removed.""" + assert(s[0] == '@' and s[-1] == '@') + return s[1:-1].replace('@@', '@') + + +def NumToList(s): + """Return a list of the components of the dotted RCS number s.""" + return map(lambda v: int(v), s.split('.')) + + +def ListToNum(l): + """Return the dotted RCS number whose elements are those in the list l.""" + return ".".join(map(lambda v: str(v), l)) + + +def IncrementNum(num, delta): + """Given dotted RCS numbers num and delta, return the dotted RCS number + formed by adding components of delta to num, starting from the left. Delta + must not have more elements than num.""" + if num == None or num == "": + return num + num_v = NumToList(num) + delta_v = NumToList(delta) + assert len(delta_v) <= len(num_v) + for i in range(0, len(num_v)): + num_v[i] += delta_v[i] + return ListToNum(num_v) + + +def DecrementNum(num, delta): + """Given dotted RCS numbers num and delta, return the dotted RCS number + formed by subtracting components of delta from num, starting from the left. + Delta must not have more elements than num.""" + if num == None or num == "": + return num + num_v = NumToList(num) + delta_v = NumToList(delta) + assert len(delta_v) <= len(num_v) + for i in range(0, len(num_v)): + num_v[i] -= delta_v[i] + return ListToNum(num_v) + + +def StringToDate(s): + """For a dotted RCS date s, return the corresponding 6-tuple + (year, month, day, hour, minute, second).""" + m = re.search("^(\d\d|\d\d\d\d).(\d\d).(\d\d).(\d\d).(\d\d).(\d\d)$", s) + assert(m != None) + + (Y, MM, DD, hh, mm, ss) = m.groups() + Y = int(Y) + MM = int(MM) + DD = int(DD) + hh = int(hh) + mm = int(mm) + ss = int(ss) + + if not (0 <= Y < 100 or 2000 <= Y): + raise RcsError("Invalid year value in date %s"%s) + if not 01 <= MM <= 12: + raise RcsError("Invalid month value in date %s"%s) + if not 01 <= DD <= 31: + raise RcsError("Invalid day value in date %s"%s) + if not 00 <= hh <= 23: + raise RcsError("Invalid hour value in date %s"%s) + if not 00 <= mm <= 59: + raise RcsError("Invalid minute value in date %s"%s) + if not 00 <= ss <= 60: # ss can be 60 according to rcsfile5 + raise RcsError("Invalid second value in date %s"%s) + + if Y < 100: + Y += 1900 + return (Y, MM, DD, hh, mm, ss) + + +def DateToString(Y, MM, DD, hh, mm, ss): + """For a 6-tuple (year, month, day, hour, minute, second) date, return the + corresponding dotted RCS date.""" + if not Y >= 1900: + raise RcsError("Invalid year value in date %s"%[Y, MM, DD, hh, mm, ss]) + if not 01 <= MM <= 12: + raise RcsError("Invalid month value in date %s"%[Y, MM, DD, hh, mm, ss]) + if not 01 <= DD <= 31: + raise RcsError("Invalid day value in date %s"%[Y, MM, DD, hh, mm, ss]) + if not 00 <= hh <= 23: + raise RcsError("Invalid hour value in date %s"%[Y, MM, DD, hh, mm, ss]) + if not 00 <= mm <= 59: + raise RcsError("Invalid minute value in date %s"% + [Y, MM, DD, hh, mm, ss]) + if not 00 <= ss <= 60: # ss can be 60 according to rcsfile5 + raise RcsError("Invalid second value in date %s"% + [Y, MM, DD, hh, mm, ss]) + + if (Y < 2000): + Y = "%02d"%(Y - 1900) + else: + Y = "%04d"%Y + + return "%s.%02d.%02d.%02d.%02d.%02d"%(Y, MM, DD, hh, mm, ss) + + +def StringColonMapToMap(s): + """Turn a {sym:num}* or {id:num}* white-space separated list into a map + with the sym/id components as keys and the num components as values.""" + lex = Lexer(s) + ret = {} + while True: + # A sym is also an id so we only need to check for the latter + sym = lex.getId(False) + if sym == None: + break + lex.getColon() + num = lex.getNum() + ret[sym] = num + return ret + + +def MapToStringColonMap(m): + """Turn a map where the keys are RCS syms/ids and the values are RCS nums + into a {sym:num}* or {id:num}* white-space separated list.""" + ret = "" + for k in m.keys(): + v = m[k] + if ret == "": + ret = "%s:%s"%(k,v) + else: + ret += " %s:%s"%(k,v) + return ret + + +def StringNumsToList(s): + """Turn a string containing a whitespace separated list of RCS numbers into + a Python list.""" + lex = Lexer(s) + ret = [] + while True: + num = lex.getNum(False) + if num == None: + break + ret.append(num) + return ret + + +def ListToStringNums(m): + """Turn a Python list of RCS numbers into a string containing a whitespace + separated list.""" + ret = "" + for n in m: + if ret == "": + ret = "%s"%(n) + else: + ret += " %s"%(n) + return ret + + +def TextToDiff(source, dest): + """Given strings containing a source and destination revision return a + string containing an RCS-style diff.""" + # Use diff's RCS output option + (src_r, src_w) = os.pipe() + (dst_r, dst_w) = os.pipe() + (res_r, res_w) = os.pipe() + if os.fork() == 0: + # Child + os.close(src_w) + os.close(dst_w) + os.close(res_r) + os.dup2(res_w, sys.stdout.fileno()) + os.execlp("diff", + "diff", + "-n", + "/dev/fd/%d"%src_r, + "/dev/fd/%d"%dst_r) + + # Parent + os.close(src_r) + os.close(dst_r) + os.close(res_w) + src_w = os.fdopen(src_w, 'w') + dst_w = os.fdopen(dst_w, 'w') + res_r = os.fdopen(res_r, 'r') + + def FeederThread(f, file): + f.write(file) + f.close() + + src_t = Thread(target = FeederThread, args = (src_w, source)) + src_t.start() + dst_t = Thread(target = FeederThread, args = (dst_w, dest)) + dst_t.start() + result = res_r.read() + src_t.join() + dst_t.join() + res_r.close() + + return result + + +def TextFromDiff(source, diff): + """Given strings containing a source revision and an RCS-style diff, apply + the diff to the source revision and return the resulting string.""" + # Parse RCS-style diff and apply it + remdiff = diff + offset = -1 # "diff -n" is 1-based + source = source.split('\n') # need to keep any terminal '\n' + while True: + if len(remdiff) > 1: + (s, remdiff) = remdiff.split('\n', 1) + else: + s = remdiff + if re.search("^\s*$", s) != None: + break + res = re.search("^([ad])([0-9]+)\s+([0-9]+)\s*$", s) + if res == None: + raise RcsError("Invalid rcsdiff command '%s'"%s) + (c, start, numlines) = res.groups() + start = int(start) + numlines = int(numlines) + if c == 'd': + fromline = start + offset + toline = fromline + numlines + if fromline < 0 or fromline >= len(source): + raise RcsError("fromline has gone wrong") + if toline < 0 or toline >= len(source): + raise RcsError("toline has gone wrong") + source = source[0:fromline] + source[toline:] + offset -= numlines + elif c == 'a': + fromline = start + offset + 1 + split = remdiff.split('\n', numlines) + add = split[:-1] + remdiff = split[-1] + if fromline < 0 or fromline >= len(source): + raise RcsError("fromline has gone wrong") + source = source[0:fromline] + add + source[fromline:] + offset += numlines + else: + raise RcsError("This shouldn't be possible") + + return "\n".join(source) + + +def SymNumStringToList(s): + r = re.compile("^\s*(?:([0-9]*[a-z][0-9a-z]*):([0-9.]+))\s+(.*)$") + ret = [] + m = re.search(r, s) + while m != None: + ret += (m[0], m[1]) + s = m[2] + m = re.search(r, s) + return ret + + +def textOrNone(phrase, value, semicolon = True): + """If the value is None then return the empty string, otherwise return + a string containing phrase followed by value and (if semicolon is + True) a semicolon.""" + if value == None: + return "" + elif semicolon: + return "%s %s;\n"%(phrase, value) + else: + return "%s %s\n"%(phrase, value) + + +class RcsError(Exception): + """Class for errors produced by EditRCS""" + + def __init__(self, value): + """Return an RcsError object with the given error string set to + value.""" + self.value = value + + def __str__(self): + """Return this object's error string.""" + return repr(self.value) + + +############################################################################ +# Lexer class +############################################################################ +class Lexer: + """Lexer class used by ParseRcs() and other utility functions.""" + + # Lexer remembers the current position + def __init__(self, text): + """Return a lexer object initialised with the given string.""" + self.text = text + self.textlen = len(text) + self.offset = 0 + self.whitespace = " \b\t\n\v\f\r" + self.special = "$,.:;@" + idchars = "" + for i in range(0, 255): + if 0x21 <= i <= 0x7E or 0xA0 <= i <= 0xFF: + c = chr(i) + if c not in self.special: + idchars += chr(i) + self.idchar_re = "[%s]"%idchars + + def error(self, text): + """Raise an RcsError with text and the current offset as context""" + raise RcsError("Syntax error: %s at offset %d"%(text, self.offset)) + + def expectedError(self, text): + """Raise an RcsError saying text was expected and the current offset + as context.""" + self.error("expected %s"%text) + + def getRE(self, regexp, must_have, err_tok): + """Try to parse the regular expression regexp. If it can't and + must_have is True, then raise an error saying err_tok was expected. + Return the matching token as a string if it succeeded and None if + it failed and must_have is False.""" + #print "%s -> %s"%(regexp, self.text[self.offset:]) + skip_whitespace = "([%s]*)"%self.whitespace + res = re.search("^" + skip_whitespace + regexp, + self.text[self.offset:], + re.DOTALL) + if res == None: + if must_have: + self.expectedError("'%s'"%err_tok) + tok = None + else: + (ws, tok,) = res.groups() + self.offset += len(ws) + len(tok) + #print "%s -> %d"%(tok, self.offset) + return tok + + def getNum(self, must_have = True): + """Try to get an RCS num. Error if it can't and must_have is True. + Return the matching token as a string if it succeeded and None if + it failed and must_have is False.""" + return self.getRE('([0-9.]+)', must_have, "") + + def getSym(self, must_have = True): + """Try to get an RCS sym. Error if it can't and must_have is True. + Return the matching token as a string if it succeeded and None if + it failed and must_have is False.""" + # In the grammar, "sym ::= {digit}* idchar {idchar|digit}". I don't + # see the difference between that and "sym ::= {idchar}*" - perhaps I'm + # missing something but "rcs -a2 foo" works. + # ... In fact the 5.9.2 version of the manpage has exactly that! + return self.getRE('(%s+)'%self.idchar_re, must_have, "") + + def getId(self, must_have = True): + """Try to get an RCS id. Error if it can't and must_have is True. + Return the matching token as a string if it succeeded and None if + it failed and must_have is False.""" + # In the grammar, "id ::= {num} idchar {idchar|num}*". I don't see + # the difference between that and "id ::= {.|idchar}*" - perhaps I'm + # missing something but "rcs -n5:1.1 foo" works. + # ... In fact the 5.9.2 version of the manpage has exactly that! + return self.getRE('((?:\.|%s)+)'%self.idchar_re, must_have, "") + + def getKw(self, kw, must_have = True): + """Try to get the given RCS keyword. Error if it can't and must_have + is True. + Return the matching token as a string if it succeeded and None if + it failed and must_have is False.""" + return self.getRE('(%s)(?:[%s%s]|$)'%(kw, + self.whitespace, + self.special), + must_have, + "'%s'"%kw) + + def getString(self, must_have = True): + """Try to get an RCS at-quoted string. Error if it can't and must_have + is True. + Return the matching token as a string if it succeeded and None if + it failed and must_have is False.""" + return self.getRE('(@(?:[^@]|@@)*@)(?:[^@]|$)', must_have, "") + + def getSemicolon(self, must_have = True): + """Try to get a semi-colon. Error if it can't and must_have is + True. + Return the matching token as a string if it succeeded and None if + it failed and must_have is False.""" + return self.getRE('(;)', must_have, "';'") + + def getColon(self, must_have = True): + """Try to get a colon. Error if it can't and must_have is True. + Return the matching token as a string if it succeeded and None if + it failed and must_have is False.""" + return self.getRE('(:)', must_have, "':'") + + def checkNewlineTerm(self): + """Try to get a newline and end-of-string, as required by the RCS file + format. Error if it can't.""" + self.getRE('(\n)$', + True, + "file to end with a newline ('\\n') character"); + + +############################################################################ +# RcsDelta class +############################################################################ +class RcsDelta: + """Class representing the structure of a delta in an RCS file""" + + def __init__(self, revision): + """Return an uninitialised RcsDelta object. The caller is responsible + for filling in the fields.""" + self.__revision = revision + self.__commitid = None + self.__date = None + self.__author = None + self.__state = None + self.__branches = None + self.__next = None + self.__log = None + self.__text = None + self.__text_is_diff = None + + def setRevision(self, value): + """Set the revision number for this delta. This is an RCS dotted + number with an even number of components. + See the rcsfile(5) manpage for how revision numbers are used. EditRCS + doesn't (currently) enforce this but different numbering schemes will + probably break other tools.""" + self.__revision = value + + def getRevision(self): + """Get the revision number for this delta. This is an RCS dotted + number with an even number of components. + See the rcsfile(5) manpage for how revision numbers are used. EditRCS + doesn't (currently) enforce this but different numbering schemes will + probably break other tools.""" + return self.__revision + + def setCommitId(self, value): + """Get the commitid field, a value unique in the RCS file used to + identify a commit operation applied to a set of RCS files.""" + self.__commitid = value + + def getCommitId(self): + """Get the commitid field, a value unique in the RCS file used to + identify a commit operation applied to a set of RCS files.""" + return self.__commitid + + def setDate(self, value): + """Set the date field, giving the date and time at which this delta + was checked in.""" + self.__date = value + + def getDate(self): + """Get the date field, giving the date and time at which this delta + was checked in.""" + return self.__date + + def setAuthor(self, value): + """Set the author field, giving the identity of the user who checked + in this delta.""" + self.__author = value + + def getAuthor(self): + """Get the author field, giving the identity of the user who checked + in this delta.""" + return self.__author + + def setState(self, value): + """Set the state field. RCS defaults this to Exp ("experimental") but + it can be changed to a user-defined value such as "stable" or + "released". CVS sets it to "dead" for deleted files.""" + if value == None: + value = "" + self.__state = value + + def getState(self): + """Get the state field. RCS defaults this to Exp ("experimental") but + it can be changed to a user-defined value such as "stable" or + "released". CVS sets it to "dead" for deleted files.""" + return self.__state + + def setBranches(self, value, from_list = True): + """Set the branches field. This is a list of the first nodes of + all branches from this delta.""" + if value == None: + value = "" + elif from_list: + self.__branches = ListToStringNums(value) + else: + self.__branches = value + + def getBranches(self, to_list = True): + """Get the branches field. This is a list of the first nodes of + all branches from this delta.""" + if to_list: + return StringNumsToList(self.__branches) + else: + return self.__branches + + def setNext(self, value): + """Set the next field, pointing to the next revision. For the trunk + this is a *previous* revision but for branches it is a *subsequent* + revision. See the diagram in the rcsfile(5) manpage.""" + if value == None: + value = "" + self.__next = value + + def getNext(self): + """Get the next field, pointing to the next revision. For the trunk + this is a *previous* revision but for branches it is a *subsequent* + revision. See the diagram in the rcsfile(5) manpage.""" + return self.__next + + def setLog(self, value, handle_quoting = True): + """Set the log field given by the user for this revision. + If handle_quoting then RCS at quoting is to be added, otherwise value + is assumed to have been quoted already.""" + if handle_quoting: + self.__log = AddAtQuoting(value) + else: + self.__log = value + + def getLog(self, handle_quoting = True): + """Set the log field given by the user for this revision. If + handle_quoting then the RCS at quoting will be removed.""" + if handle_quoting: + return RemoveAtQuoting(self.__log) + else: + return self.__log + + def setText(self, value, text_is_diff, handle_quoting = True): + """Set the text field to value. If text_is_diff then it is to be + regarded as a diff, otherwise it is to be regarded as a revision. + (In an RCS file the head should be a revision and the other deltas + are diffs.) + If handle_quoting then RCS at quoting is to be added, otherwise value + is assumed to have been quoted already.""" + self.__text_is_diff = text_is_diff + if handle_quoting: + self.__text = AddAtQuoting(value) + else: + self.__text = value + + def getTextIsDiff(self): + """Return True if the text field is currently a diff or False + otherwise (a revision).""" + return self.__text_is_diff + + def getText(self, handle_quoting = True): + """Return the text field. If handle_quoting then the RCS at quoting + will be removed.""" + if handle_quoting: + return RemoveAtQuoting(self.__text) + else: + return self.__text + + def textToDiff(self, prev_rev): + """If the text field is currently a revision then, using prev_rev as + the previous revision, set the text field to the resulting diff. + Otherwise, raise a RcsError.""" + if self.__text_is_diff: + raise RcsError("revision %s is already a diff!"%self.__revision) + if prev_rev.getTextIsDiff(): + raise RcsError("previous revision %s is a diff!"%prev_rev.getRevision()) + self.setText(TextToDiff(prev_rev.getText(), + self.getText()), + True) + + def textFromDiff(self, prev_rev): + """If the text field is currently a diff then, using prev_rev as the + previous revision, set the text field to the resulting revision. + Otherwise, raise a RcsError.""" + if not self.__text_is_diff: + raise RcsError("revision %s is not a diff!"%self.__revision) + self.setText(TextFromDiff(prev_rev.getText(), + self.getText()), + False) + + def validate(self): + """Validate the RcsDelta object for missing phrases and other + problems. If a problem is found then raise an RcsError.""" + # This implicitly checks that there is a delta and a delta text for + # every revision. + if self.getDate() == None: + raise RcsError("date is not set (and is not optional)") + if self.getAuthor() == None: + raise RcsError("author is not set (and is not optional)") + if self.getState() == None: + raise RcsError("state is not set (and is not optional)") + if self.getBranches() == None: + raise RcsError("branches is not set (and is not optional)") + if self.getNext() == None: + raise RcsError("next is not set (and is not optional)") + if self.getLog() == None: + raise RcsError("log is not set (and is not optional)") + if self.getText() == None: + raise RcsError("text is not set (and is not optional)") + + def deltaToString(self): + """Return a string representation of the "delta" part of the RCS + grammar for this RcsDelta object. This assumes that validate() has + already been checked in the containing Rcs object.""" + s = ("%s\n"%self.__revision + + textOrNone("date", self.__date) + + textOrNone("author", self.__author) + + textOrNone("state", self.__state) + + textOrNone("branches", self.__branches) + + textOrNone("next", self.__next) + + textOrNone("commitid", self.__commitid) + + "\n") + return s + + def deltaTextToString(self): + """Return a string representation of the "deltatext" part of the RCS + grammar for this RcsDelta object. This assumes that validate() has + already been checked in the containing Rcs object.""" + # This assumes that Rcs.validate() has been checked + s = ("%s\n"%self.__revision + + textOrNone("log", self.__log, False) + + textOrNone("text", self.__text, False) + + "\n") + return s + + +############################################################################ +# Rcs class +############################################################################ +class Rcs: + """Class representing the structure of an RCS file""" + + def __init__(self): + """Return an uninitialised Rcs object. The caller is responsible for + filling in the fields.""" + # These are stored as strings as they appear in the rcs file. The + # getters and setters do conversion and validation. + self.__deltas = [] + self.__head = None + self.__branch = None + self.__access = None + self.__symbols = None + self.__locks = None + self.__strict = None + self.__integrity = None + self.__comment = None + self.__expand = None + self.__desc = None + + self.revisionsAreDiffs = True + + def setHead(self, revision): + """Set the head revision. This is the RCS dotted number of the latest + revision on the trunk.""" + if revision == None: + revision = "" + self.__head = revision + + def getHead(self): + """Get the head revision. This is the RCS dotted number of the latest + revision on the trunk.""" + return self.__head + + def setBranch(self, revision): + """Set the default branch (or revision) for RCS operations.""" + if revision == None: + revision = "" + self.__branch = revision + + def getBranch(self): + """Get the default branch (or revision) for RCS operations.""" + return self.__branch + + def setAccess(self, value): + """Set the access field. This is a whitespace-separated list of ids + for the users who are allowed to modify the RCS file. If it is empty + then any user can modify the file.""" + if value == None: + value = "" + self.__access = value + + def getAccess(self): + """Get the access field. This is a whitespace-separated list of ids + for the users who are allowed to modify the RCS file. If it is empty + then any user can modify the file.""" + return self.__access + + def setSymbols(self, value, from_map = True): + """Set the symbols field. This is a whitespace-separated list of + mappings from symbolic names to RCS dotted revision numbers (of the + form :). The symbolic names can be used as tags, possibly + across multiple RCS files.""" + if value == None: + value = "" + elif from_map: + self.__symbols = MapToStringColonMap(value) + else: + self.__symbols = value + + def getSymbols(self, to_map = True): + """Get the symbols field. This is a whitespace-separated list of + mappings from symbolic names to RCS dotted revision numbers (of the + form :). The symbolic names can be used as tags, possibly + across multiple RCS files.""" + if to_map: + return StringColonMapToMap(self.__symbols) + else: + return self.__symbols + + def setLocks(self, value, from_map = True): + """Set the locks field. This is a whitespace-separated list of + mappings from user ids to RCS dotted revision numbers (of the form + :). This is used to identify which revisions have been locked + by users.""" + if value == None: + value = "" + elif from_map: + self.__locks = MapToStringColonMap(value) + else: + self.__locks = value + + def getLocks(self, to_map = True): + """Get the locks field. This is a whitespace-separated list of + mappings from user ids to RCS dotted revision numbers (of the form + :). This is used to identify which revisions have been locked + by users.""" + if to_map: + return StringColonMapToMap(self.__locks) + else: + return self.__locks + + def setStrict(self, value, from_bool = True): + """Set the strict field. If set then RCS requires a user to hold + a lock on a revision before being allowed to check in the next + revision. + This function expects value to be a boolean (unless from_bool is + False).""" + if from_bool: + if value: + value = "" + else: + value = None + else: + if value not in [None, ""]: + raise RcsError("struct can only be None or \"\"") + self.__strict = value + + def getStrict(self, to_bool = True): + """Get the strict field. If set then RCS requires a user to hold + a lock on a revision before being allowed to check in the next + revision. + This function expects value to be a boolean (unless from_bool is + False).""" + if to_bool: + if value not in [None, ""]: + raise RcsError("struct can only be None or \"\"") + return self.__strict == "" + else: + return self.__strict + + def setIntegrity(self, value, handle_quoting = True): + """Set the integrity field. This was added in RCS 5.8 for RCS and + implementation-defined extensions. + If handle_quoting then RCS at quoting is to be added, otherwise value + is assumed to have been quoted already.""" + if handle_quoting: + self.__integrity = AddAtQuoting(value) + else: + self.__integrity = value + + def getIntegrity(self, handle_quoting = True): + """Get the integrity field. This was added in RCS 5.8 for RCS and + implementation-defined extensions. + If handle_quoting then the RCS at quoting will be removed.""" + if handle_quoting: + return RemoveAtQuoting(self.__integrity) + else: + return self.__integrity + + def setComment(self, value, handle_quoting = True): + """Set the comment field. This is an obsolete option used by old + RCS versions for $Log$. + If handle_quoting then RCS at quoting is to be added, otherwise value + is assumed to have been quoted already.""" + if handle_quoting: + self.__comment = AddAtQuoting(value) + else: + self.__comment = value + + def getComment(self, handle_quoting = True): + """Set the comment field. This is an obsolete option used by old + RCS versions for $Log$. + If handle_quoting then the RCS at quoting will be removed.""" + if handle_quoting: + return RemoveAtQuoting(self.__comment) + else: + return self.__comment + + def setExpand(self, value, handle_quoting = True): + """Set the expand field. + If handle_quoting then RCS at quoting is to be added, otherwise value + is assumed to have been quoted already.""" + if handle_quoting: + self.__expand = AddAtQuoting(value) + else: + self.__expand = value + + def getExpand(self, handle_quoting = True): + """Get the expand field. + If handle_quoting then the RCS at quoting will be removed.""" + if handle_quoting: + return RemoveAtQuoting(self.__expand) + else: + return self.__expand + + def addDelta(self, revision, delta): + """Add the given delta as the given revision number.""" + for d in self.__deltas: + if d.getRevision() == revision: + raise RcsError("Revision %s already in deltas"%(revision)) + self.__deltas.append(delta) + + def getDelta(self, revision): + """Return the delta for the given revision number.""" + for d in self.__deltas: + if d.getRevision() == revision: + return d + raise RcsError("Revision %s not found in deltas"%(revision)) + + def delDelta(self, revision): + """Delete the delta for the given revision number.""" + for d in self.__deltas: + if d.getRevision() == revision: + del self.__deltas[self.__deltas.index(d)] + raise RcsError("Revision %s not found in deltas"%(revision)) + + def mapDeltas(self, apply_fn): + """Apply the function apply_fn() to each delta in turn.""" + for d in self.__deltas: + apply_fn(d) + + def setDesc(self, value, handle_quoting = True): + """Set the description field given by the user for this RCS file. + If handle_quoting then RCS at quoting is to be added, otherwise value + is assumed to have been quoted already.""" + if handle_quoting: + self.__desc = AddAtQuoting(value) + else: + self.__desc = value + + def getDesc(self, handle_quoting = True): + """Get the description field given by the user for this RCS file. + If handle_quoting then the RCS at quoting will be removed.""" + if handle_quoting: + return RemoveAtQuoting(self.__desc) + else: + return self.__desc + + def validate(self): + """Validate the Rcs object for missing phrases and other problems. + If a problem is found then raise an RcsError.""" + for d in self.__deltas: + d.validate() + # TODO Check that d's next is in self.__deltas + # TODO Check that head, branch, etc. are in self.__deltas + + if self.getHead() == None: + raise RcsError("head is not set (and is not optional)") + if self.getAccess() == None: + raise RcsError("access is not set (and is not optional)") + if self.getSymbols(False) == None: + raise RcsError("symbols is not set (and is not optional)") + if self.getLocks(False) == None: + raise RcsError("locks is not set (and is not optional)") + if self.getDesc() == None: + raise RcsError("desc is not set (and is not optional)") + + def toString(self): + """Return a string representation of the RCS file for this object. + This performs validation using validate().""" + self.validate() + + s = (textOrNone("head", self.__head) + + textOrNone("branch", self.__branch) + + textOrNone("access", self.__access) + + textOrNone("symbols", self.__symbols) + + textOrNone("locks", self.__locks) + + textOrNone("strict", self.__strict) + + textOrNone("comment", self.__comment) + + textOrNone("expand", self.__expand) + + "\n" + + "".join(map(lambda d: d.deltaToString(), self.__deltas)) + + textOrNone("desc", self.__desc, False) + + "\n" + + "".join(map(lambda d: d.deltaTextToString(), self.__deltas))) + return s + + +############################################################################ +# ParseRcs function +############################################################################ +def ParseRcs(text): + """Parse a string in RCS format and return a corresponding Rcs object""" + + # RCS file format is a regular expression! + + rcs = Rcs() + lex = Lexer(text) + + lex.getKw("head") + rcs.setHead(lex.getNum()) + lex.getSemicolon() + + if lex.getKw("branch", False) != None: + rcs.setBranch(lex.getNum()) + lex.getSemicolon() + + t = lex.getKw("access") + access_list = [] + while True: + t = lex.getId(False) + if t == None: + break + access_list.append(t) + rcs.setAccess(" ".join(access_list)) + lex.getSemicolon() + + lex.getKw("symbols") + symbols_list = [] + while True: + t1 = lex.getSym(False) + if t1 == None: + break + lex.getColon() + t2 = lex.getNum() + symbols_list.append("%s:%s"%(t1, t2)) + rcs.setSymbols(" ".join(symbols_list), False) + lex.getSemicolon() + + lex.getKw("locks") + locks_list = [] + while True: + t1 = lex.getId(False) + if t1 == None: + break + lex.getColon() + t2 = lex.getNum() + locks_list.append("%s:%s"%(t1, t2)) + rcs.setLocks(" ".join(locks_list), False) + lex.getSemicolon() + + if lex.getKw("strict", False) != None: + rcs.setStrict("", False) + lex.getSemicolon() + + if lex.getKw("integrity", False) != None: + rcs.setIntegrity(lex.getString(False), False) + lex.getSemicolon() + + if lex.getKw("comment", False) != None: + rcs.setComment(lex.getString(False), False) + lex.getSemicolon() + + if lex.getKw("expand", False) != None: + rcs.setExpand(lex.getString(False), False) + lex.getSemicolon() + + # Haven't implemented from rcs 5.6, which would go here. + + # We might need a better parser (or at least backtracking) because + # is ';' terminated and we can't otherwise distinguish the + # keywords from in . + # But actually rcs 5.7 source code assumes that if a token contains an + # idchar that isn't a digit or a special then it's an ID; and it doesn't + # distinguish between SYM and ID. + + while True: + rev = lex.getNum(False) + if rev == None: + break + + delta = RcsDelta(rev) + rcs.addDelta(rev, delta) + + lex.getKw("date") + delta.setDate(lex.getNum()) + lex.getSemicolon() + + lex.getKw("author") + delta.setAuthor(lex.getId()) + lex.getSemicolon() + + lex.getKw("state") + delta.setState(lex.getId(False)) + lex.getSemicolon() + + lex.getKw("branches") + branches_list = [] + while True: + t = lex.getNum(False) + if t == None: + break + branches_list.append(t) + delta.setBranches(" ".join(branches_list)) + lex.getSemicolon() + + lex.getKw("next") + delta.setNext(lex.getNum(False)) + lex.getSemicolon() + + if lex.getKw("commitid", False) != None: + delta.setCommitId(lex.getId(True)) + lex.getSemicolon() + + # Haven't implemented from rcs 5.6, which would go here. + + lex.getKw("desc") + rcs.setDesc(lex.getString(), False) + + while True: + rev = lex.getNum(False) + if rev == None: + break + + delta = rcs.getDelta(rev) + + lex.getKw("log") + delta.setLog(lex.getString(), False) + + lex.getKw("text") + delta.setText(lex.getString(), (rcs.getHead() != rev), False) + + lex.checkNewlineTerm() + return rcs + +############################################################################ diff --git a/editrcs/editrcs.py b/editrcs/editrcs.py deleted file mode 100755 index 77bb375..0000000 --- a/editrcs/editrcs.py +++ /dev/null @@ -1,1093 +0,0 @@ -#!/usr/bin/python -############################################################################ -# EditRCS: Python library to read, manipulate and write RCS ,v files -# -# Copyright (C) 2014 Ben Cohen -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -############################################################################ - -"""EditRCS: Python library to read, manipulate and write RCS ,v files - -EditRCS is a library to with functions to read RCS files into Python -classes Rcs and RcsDelta, and to manipulate them and write them to a new -RCS file. - -It is intended to be used to manipulate RCS files in ways that the RCS tools -don't support, but it requires some knowledge of how RCS files work rather -than being a polished command-line tool. - -See the rcsfile(5) manpage for RCS file format definition. See also -https://www.gnu.org/software/rcs/manual/ and -http://www.gnu.org/software/rcs/tichy-paper.pdf - -Limitations: I'm going with the 5.9.2 version of the manpage, and ignoring -the newphrase in the earlier (e.g. 5.6) specifications for now. -It is written to be simple rather than optimised for speed or space.""" - -import re -import os -import sys -from threading import Thread - -############################################################################ -# Utility functions -############################################################################ - -def IsVisibleChar(c): - """Return whether the character c is a visible character, as defined in - rcsfile(5).""" - return 33 <= ord(c) <= 126 or 160 <= ord(c) <= 377 - - -def IsWhitespaceChar(c): - """Return whether the character c is a whitespace character, as defined in - rcsfile(5).""" - return 8 <= ord(c) <= 13 or ord(c) == 32 - - -def AddAtQuoting(s): - """Return the string s quoted for RCS, i.e. delimited by the at symbol '@' - with any at symbols doubled.""" - return '@' + s.replace('@', '@@') + '@' - - -def RemoveAtQuoting(s): - """Return the string s with RCS at quoting removed.""" - assert(s[0] == '@' and s[-1] == '@') - return s[1:-1].replace('@@', '@') - - -def NumToList(s): - """Return a list of the components of the dotted RCS number s.""" - return map(lambda v: int(v), s.split('.')) - - -def ListToNum(l): - """Return the dotted RCS number whose elements are those in the list l.""" - return ".".join(map(lambda v: str(v), l)) - - -def IncrementNum(num, delta): - """Given dotted RCS numbers num and delta, return the dotted RCS number - formed by adding components of delta to num, starting from the left. Delta - must not have more elements than num.""" - if num == None or num == "": - return num - num_v = NumToList(num) - delta_v = NumToList(delta) - assert len(delta_v) <= len(num_v) - for i in range(0, len(num_v)): - num_v[i] += delta_v[i] - return ListToNum(num_v) - - -def DecrementNum(num, delta): - """Given dotted RCS numbers num and delta, return the dotted RCS number - formed by subtracting components of delta from num, starting from the left. - Delta must not have more elements than num.""" - if num == None or num == "": - return num - num_v = NumToList(num) - delta_v = NumToList(delta) - assert len(delta_v) <= len(num_v) - for i in range(0, len(num_v)): - num_v[i] -= delta_v[i] - return ListToNum(num_v) - - -def StringToDate(s): - """For a dotted RCS date s, return the corresponding 6-tuple - (year, month, day, hour, minute, second).""" - m = re.search("^(\d\d|\d\d\d\d).(\d\d).(\d\d).(\d\d).(\d\d).(\d\d)$", s) - assert(m != None) - - (Y, MM, DD, hh, mm, ss) = m.groups() - Y = int(Y) - MM = int(MM) - DD = int(DD) - hh = int(hh) - mm = int(mm) - ss = int(ss) - - if not (0 <= Y < 100 or 2000 <= Y): - raise RcsError("Invalid year value in date %s"%s) - if not 01 <= MM <= 12: - raise RcsError("Invalid month value in date %s"%s) - if not 01 <= DD <= 31: - raise RcsError("Invalid day value in date %s"%s) - if not 00 <= hh <= 23: - raise RcsError("Invalid hour value in date %s"%s) - if not 00 <= mm <= 59: - raise RcsError("Invalid minute value in date %s"%s) - if not 00 <= ss <= 60: # ss can be 60 according to rcsfile5 - raise RcsError("Invalid second value in date %s"%s) - - if Y < 100: - Y += 1900 - return (Y, MM, DD, hh, mm, ss) - - -def DateToString(Y, MM, DD, hh, mm, ss): - """For a 6-tuple (year, month, day, hour, minute, second) date, return the - corresponding dotted RCS date.""" - if not Y >= 1900: - raise RcsError("Invalid year value in date %s"%[Y, MM, DD, hh, mm, ss]) - if not 01 <= MM <= 12: - raise RcsError("Invalid month value in date %s"%[Y, MM, DD, hh, mm, ss]) - if not 01 <= DD <= 31: - raise RcsError("Invalid day value in date %s"%[Y, MM, DD, hh, mm, ss]) - if not 00 <= hh <= 23: - raise RcsError("Invalid hour value in date %s"%[Y, MM, DD, hh, mm, ss]) - if not 00 <= mm <= 59: - raise RcsError("Invalid minute value in date %s"% - [Y, MM, DD, hh, mm, ss]) - if not 00 <= ss <= 60: # ss can be 60 according to rcsfile5 - raise RcsError("Invalid second value in date %s"% - [Y, MM, DD, hh, mm, ss]) - - if (Y < 2000): - Y = "%02d"%(Y - 1900) - else: - Y = "%04d"%Y - - return "%s.%02d.%02d.%02d.%02d.%02d"%(Y, MM, DD, hh, mm, ss) - - -def StringColonMapToMap(s): - """Turn a {sym:num}* or {id:num}* white-space separated list into a map - with the sym/id components as keys and the num components as values.""" - lex = Lexer(s) - ret = {} - while True: - # A sym is also an id so we only need to check for the latter - sym = lex.getId(False) - if sym == None: - break - lex.getColon() - num = lex.getNum() - ret[sym] = num - return ret - - -def MapToStringColonMap(m): - """Turn a map where the keys are RCS syms/ids and the values are RCS nums - into a {sym:num}* or {id:num}* white-space separated list.""" - ret = "" - for k in m.keys(): - v = m[k] - if ret == "": - ret = "%s:%s"%(k,v) - else: - ret += " %s:%s"%(k,v) - return ret - - -def StringNumsToList(s): - """Turn a string containing a whitespace separated list of RCS numbers into - a Python list.""" - lex = Lexer(s) - ret = [] - while True: - num = lex.getNum(False) - if num == None: - break - ret.append(num) - return ret - - -def ListToStringNums(m): - """Turn a Python list of RCS numbers into a string containing a whitespace - separated list.""" - ret = "" - for n in m: - if ret == "": - ret = "%s"%(n) - else: - ret += " %s"%(n) - return ret - - -def TextToDiff(source, dest): - """Given strings containing a source and destination revision return a - string containing an RCS-style diff.""" - # Use diff's RCS output option - (src_r, src_w) = os.pipe() - (dst_r, dst_w) = os.pipe() - (res_r, res_w) = os.pipe() - if os.fork() == 0: - # Child - os.close(src_w) - os.close(dst_w) - os.close(res_r) - os.dup2(res_w, sys.stdout.fileno()) - os.execlp("diff", - "diff", - "-n", - "/dev/fd/%d"%src_r, - "/dev/fd/%d"%dst_r) - - # Parent - os.close(src_r) - os.close(dst_r) - os.close(res_w) - src_w = os.fdopen(src_w, 'w') - dst_w = os.fdopen(dst_w, 'w') - res_r = os.fdopen(res_r, 'r') - - def FeederThread(f, file): - f.write(file) - f.close() - - src_t = Thread(target = FeederThread, args = (src_w, source)) - src_t.start() - dst_t = Thread(target = FeederThread, args = (dst_w, dest)) - dst_t.start() - result = res_r.read() - src_t.join() - dst_t.join() - res_r.close() - - return result - - -def TextFromDiff(source, diff): - """Given strings containing a source revision and an RCS-style diff, apply - the diff to the source revision and return the resulting string.""" - # Parse RCS-style diff and apply it - remdiff = diff - offset = -1 # "diff -n" is 1-based - source = source.split('\n') # need to keep any terminal '\n' - while True: - if len(remdiff) > 1: - (s, remdiff) = remdiff.split('\n', 1) - else: - s = remdiff - if re.search("^\s*$", s) != None: - break - res = re.search("^([ad])([0-9]+)\s+([0-9]+)\s*$", s) - if res == None: - raise RcsError("Invalid rcsdiff command '%s'"%s) - (c, start, numlines) = res.groups() - start = int(start) - numlines = int(numlines) - if c == 'd': - fromline = start + offset - toline = fromline + numlines - if fromline < 0 or fromline >= len(source): - raise RcsError("fromline has gone wrong") - if toline < 0 or toline >= len(source): - raise RcsError("toline has gone wrong") - source = source[0:fromline] + source[toline:] - offset -= numlines - elif c == 'a': - fromline = start + offset + 1 - split = remdiff.split('\n', numlines) - add = split[:-1] - remdiff = split[-1] - if fromline < 0 or fromline >= len(source): - raise RcsError("fromline has gone wrong") - source = source[0:fromline] + add + source[fromline:] - offset += numlines - else: - raise RcsError("This shouldn't be possible") - - return "\n".join(source) - - -def SymNumStringToList(s): - r = re.compile("^\s*(?:([0-9]*[a-z][0-9a-z]*):([0-9.]+))\s+(.*)$") - ret = [] - m = re.search(r, s) - while m != None: - ret += (m[0], m[1]) - s = m[2] - m = re.search(r, s) - return ret - - -def textOrNone(phrase, value, semicolon = True): - """If the value is None then return the empty string, otherwise return - a string containing phrase followed by value and (if semicolon is - True) a semicolon.""" - if value == None: - return "" - elif semicolon: - return "%s %s;\n"%(phrase, value) - else: - return "%s %s\n"%(phrase, value) - - -class RcsError(Exception): - """Class for errors produced by EditRCS""" - - def __init__(self, value): - """Return an RcsError object with the given error string set to - value.""" - self.value = value - - def __str__(self): - """Return this object's error string.""" - return repr(self.value) - - -############################################################################ -# Lexer class -############################################################################ -class Lexer: - """Lexer class used by ParseRcs() and other utility functions.""" - - # Lexer remembers the current position - def __init__(self, text): - """Return a lexer object initialised with the given string.""" - self.text = text - self.textlen = len(text) - self.offset = 0 - self.whitespace = " \b\t\n\v\f\r" - self.special = "$,.:;@" - idchars = "" - for i in range(0, 255): - if 0x21 <= i <= 0x7E or 0xA0 <= i <= 0xFF: - c = chr(i) - if c not in self.special: - idchars += chr(i) - self.idchar_re = "[%s]"%idchars - - def error(self, text): - """Raise an RcsError with text and the current offset as context""" - raise RcsError("Syntax error: %s at offset %d"%(text, self.offset)) - - def expectedError(self, text): - """Raise an RcsError saying text was expected and the current offset - as context.""" - self.error("expected %s"%text) - - def getRE(self, regexp, must_have, err_tok): - """Try to parse the regular expression regexp. If it can't and - must_have is True, then raise an error saying err_tok was expected. - Return the matching token as a string if it succeeded and None if - it failed and must_have is False.""" - #print "%s -> %s"%(regexp, self.text[self.offset:]) - skip_whitespace = "([%s]*)"%self.whitespace - res = re.search("^" + skip_whitespace + regexp, - self.text[self.offset:], - re.DOTALL) - if res == None: - if must_have: - self.expectedError("'%s'"%err_tok) - tok = None - else: - (ws, tok,) = res.groups() - self.offset += len(ws) + len(tok) - #print "%s -> %d"%(tok, self.offset) - return tok - - def getNum(self, must_have = True): - """Try to get an RCS num. Error if it can't and must_have is True. - Return the matching token as a string if it succeeded and None if - it failed and must_have is False.""" - return self.getRE('([0-9.]+)', must_have, "") - - def getSym(self, must_have = True): - """Try to get an RCS sym. Error if it can't and must_have is True. - Return the matching token as a string if it succeeded and None if - it failed and must_have is False.""" - # In the grammar, "sym ::= {digit}* idchar {idchar|digit}". I don't - # see the difference between that and "sym ::= {idchar}*" - perhaps I'm - # missing something but "rcs -a2 foo" works. - # ... In fact the 5.9.2 version of the manpage has exactly that! - return self.getRE('(%s+)'%self.idchar_re, must_have, "") - - def getId(self, must_have = True): - """Try to get an RCS id. Error if it can't and must_have is True. - Return the matching token as a string if it succeeded and None if - it failed and must_have is False.""" - # In the grammar, "id ::= {num} idchar {idchar|num}*". I don't see - # the difference between that and "id ::= {.|idchar}*" - perhaps I'm - # missing something but "rcs -n5:1.1 foo" works. - # ... In fact the 5.9.2 version of the manpage has exactly that! - return self.getRE('((?:\.|%s)+)'%self.idchar_re, must_have, "") - - def getKw(self, kw, must_have = True): - """Try to get the given RCS keyword. Error if it can't and must_have - is True. - Return the matching token as a string if it succeeded and None if - it failed and must_have is False.""" - return self.getRE('(%s)(?:[%s%s]|$)'%(kw, - self.whitespace, - self.special), - must_have, - "'%s'"%kw) - - def getString(self, must_have = True): - """Try to get an RCS at-quoted string. Error if it can't and must_have - is True. - Return the matching token as a string if it succeeded and None if - it failed and must_have is False.""" - return self.getRE('(@(?:[^@]|@@)*@)(?:[^@]|$)', must_have, "") - - def getSemicolon(self, must_have = True): - """Try to get a semi-colon. Error if it can't and must_have is - True. - Return the matching token as a string if it succeeded and None if - it failed and must_have is False.""" - return self.getRE('(;)', must_have, "';'") - - def getColon(self, must_have = True): - """Try to get a colon. Error if it can't and must_have is True. - Return the matching token as a string if it succeeded and None if - it failed and must_have is False.""" - return self.getRE('(:)', must_have, "':'") - - def checkNewlineTerm(self): - """Try to get a newline and end-of-string, as required by the RCS file - format. Error if it can't.""" - self.getRE('(\n)$', - True, - "file to end with a newline ('\\n') character"); - - -############################################################################ -# RcsDelta class -############################################################################ -class RcsDelta: - """Class representing the structure of a delta in an RCS file""" - - def __init__(self, revision): - """Return an uninitialised RcsDelta object. The caller is responsible - for filling in the fields.""" - self.__revision = revision - self.__commitid = None - self.__date = None - self.__author = None - self.__state = None - self.__branches = None - self.__next = None - self.__log = None - self.__text = None - self.__text_is_diff = None - - def setRevision(self, value): - """Set the revision number for this delta. This is an RCS dotted - number with an even number of components. - See the rcsfile(5) manpage for how revision numbers are used. EditRCS - doesn't (currently) enforce this but different numbering schemes will - probably break other tools.""" - self.__revision = value - - def getRevision(self): - """Get the revision number for this delta. This is an RCS dotted - number with an even number of components. - See the rcsfile(5) manpage for how revision numbers are used. EditRCS - doesn't (currently) enforce this but different numbering schemes will - probably break other tools.""" - return self.__revision - - def setCommitId(self, value): - """Get the commitid field, a value unique in the RCS file used to - identify a commit operation applied to a set of RCS files.""" - self.__commitid = value - - def getCommitId(self): - """Get the commitid field, a value unique in the RCS file used to - identify a commit operation applied to a set of RCS files.""" - return self.__commitid - - def setDate(self, value): - """Set the date field, giving the date and time at which this delta - was checked in.""" - self.__date = value - - def getDate(self): - """Get the date field, giving the date and time at which this delta - was checked in.""" - return self.__date - - def setAuthor(self, value): - """Set the author field, giving the identity of the user who checked - in this delta.""" - self.__author = value - - def getAuthor(self): - """Get the author field, giving the identity of the user who checked - in this delta.""" - return self.__author - - def setState(self, value): - """Set the state field. RCS defaults this to Exp ("experimental") but - it can be changed to a user-defined value such as "stable" or - "released". CVS sets it to "dead" for deleted files.""" - if value == None: - value = "" - self.__state = value - - def getState(self): - """Get the state field. RCS defaults this to Exp ("experimental") but - it can be changed to a user-defined value such as "stable" or - "released". CVS sets it to "dead" for deleted files.""" - return self.__state - - def setBranches(self, value, from_list = True): - """Set the branches field. This is a list of the first nodes of - all branches from this delta.""" - if value == None: - value = "" - elif from_list: - self.__branches = ListToStringNums(value) - else: - self.__branches = value - - def getBranches(self, to_list = True): - """Get the branches field. This is a list of the first nodes of - all branches from this delta.""" - if to_list: - return StringNumsToList(self.__branches) - else: - return self.__branches - - def setNext(self, value): - """Set the next field, pointing to the next revision. For the trunk - this is a *previous* revision but for branches it is a *subsequent* - revision. See the diagram in the rcsfile(5) manpage.""" - if value == None: - value = "" - self.__next = value - - def getNext(self): - """Get the next field, pointing to the next revision. For the trunk - this is a *previous* revision but for branches it is a *subsequent* - revision. See the diagram in the rcsfile(5) manpage.""" - return self.__next - - def setLog(self, value, handle_quoting = True): - """Set the log field given by the user for this revision. - If handle_quoting then RCS at quoting is to be added, otherwise value - is assumed to have been quoted already.""" - if handle_quoting: - self.__log = AddAtQuoting(value) - else: - self.__log = value - - def getLog(self, handle_quoting = True): - """Set the log field given by the user for this revision. If - handle_quoting then the RCS at quoting will be removed.""" - if handle_quoting: - return RemoveAtQuoting(self.__log) - else: - return self.__log - - def setText(self, value, text_is_diff, handle_quoting = True): - """Set the text field to value. If text_is_diff then it is to be - regarded as a diff, otherwise it is to be regarded as a revision. - (In an RCS file the head should be a revision and the other deltas - are diffs.) - If handle_quoting then RCS at quoting is to be added, otherwise value - is assumed to have been quoted already.""" - self.__text_is_diff = text_is_diff - if handle_quoting: - self.__text = AddAtQuoting(value) - else: - self.__text = value - - def getTextIsDiff(self): - """Return True if the text field is currently a diff or False - otherwise (a revision).""" - return self.__text_is_diff - - def getText(self, handle_quoting = True): - """Return the text field. If handle_quoting then the RCS at quoting - will be removed.""" - if handle_quoting: - return RemoveAtQuoting(self.__text) - else: - return self.__text - - def textToDiff(self, prev_rev): - """If the text field is currently a revision then, using prev_rev as - the previous revision, set the text field to the resulting diff. - Otherwise, raise a RcsError.""" - if self.__text_is_diff: - raise RcsError("revision %s is already a diff!"%self.__revision) - if prev_rev.getTextIsDiff(): - raise RcsError("previous revision %s is a diff!"%prev_rev.getRevision()) - self.setText(TextToDiff(prev_rev.getText(), - self.getText()), - True) - - def textFromDiff(self, prev_rev): - """If the text field is currently a diff then, using prev_rev as the - previous revision, set the text field to the resulting revision. - Otherwise, raise a RcsError.""" - if not self.__text_is_diff: - raise RcsError("revision %s is not a diff!"%self.__revision) - self.setText(TextFromDiff(prev_rev.getText(), - self.getText()), - False) - - def validate(self): - """Validate the RcsDelta object for missing phrases and other - problems. If a problem is found then raise an RcsError.""" - # This implicitly checks that there is a delta and a delta text for - # every revision. - if self.getDate() == None: - raise RcsError("date is not set (and is not optional)") - if self.getAuthor() == None: - raise RcsError("author is not set (and is not optional)") - if self.getState() == None: - raise RcsError("state is not set (and is not optional)") - if self.getBranches() == None: - raise RcsError("branches is not set (and is not optional)") - if self.getNext() == None: - raise RcsError("next is not set (and is not optional)") - if self.getLog() == None: - raise RcsError("log is not set (and is not optional)") - if self.getText() == None: - raise RcsError("text is not set (and is not optional)") - - def deltaToString(self): - """Return a string representation of the "delta" part of the RCS - grammar for this RcsDelta object. This assumes that validate() has - already been checked in the containing Rcs object.""" - s = ("%s\n"%self.__revision - + textOrNone("date", self.__date) - + textOrNone("author", self.__author) - + textOrNone("state", self.__state) - + textOrNone("branches", self.__branches) - + textOrNone("next", self.__next) - + textOrNone("commitid", self.__commitid) - + "\n") - return s - - def deltaTextToString(self): - """Return a string representation of the "deltatext" part of the RCS - grammar for this RcsDelta object. This assumes that validate() has - already been checked in the containing Rcs object.""" - # This assumes that Rcs.validate() has been checked - s = ("%s\n"%self.__revision - + textOrNone("log", self.__log, False) - + textOrNone("text", self.__text, False) - + "\n") - return s - - -############################################################################ -# Rcs class -############################################################################ -class Rcs: - """Class representing the structure of an RCS file""" - - def __init__(self): - """Return an uninitialised Rcs object. The caller is responsible for - filling in the fields.""" - # These are stored as strings as they appear in the rcs file. The - # getters and setters do conversion and validation. - self.__deltas = [] - self.__head = None - self.__branch = None - self.__access = None - self.__symbols = None - self.__locks = None - self.__strict = None - self.__integrity = None - self.__comment = None - self.__expand = None - self.__desc = None - - self.revisionsAreDiffs = True - - def setHead(self, revision): - """Set the head revision. This is the RCS dotted number of the latest - revision on the trunk.""" - if revision == None: - revision = "" - self.__head = revision - - def getHead(self): - """Get the head revision. This is the RCS dotted number of the latest - revision on the trunk.""" - return self.__head - - def setBranch(self, revision): - """Set the default branch (or revision) for RCS operations.""" - if revision == None: - revision = "" - self.__branch = revision - - def getBranch(self): - """Get the default branch (or revision) for RCS operations.""" - return self.__branch - - def setAccess(self, value): - """Set the access field. This is a whitespace-separated list of ids - for the users who are allowed to modify the RCS file. If it is empty - then any user can modify the file.""" - if value == None: - value = "" - self.__access = value - - def getAccess(self): - """Get the access field. This is a whitespace-separated list of ids - for the users who are allowed to modify the RCS file. If it is empty - then any user can modify the file.""" - return self.__access - - def setSymbols(self, value, from_map = True): - """Set the symbols field. This is a whitespace-separated list of - mappings from symbolic names to RCS dotted revision numbers (of the - form :). The symbolic names can be used as tags, possibly - across multiple RCS files.""" - if value == None: - value = "" - elif from_map: - self.__symbols = MapToStringColonMap(value) - else: - self.__symbols = value - - def getSymbols(self, to_map = True): - """Get the symbols field. This is a whitespace-separated list of - mappings from symbolic names to RCS dotted revision numbers (of the - form :). The symbolic names can be used as tags, possibly - across multiple RCS files.""" - if to_map: - return StringColonMapToMap(self.__symbols) - else: - return self.__symbols - - def setLocks(self, value, from_map = True): - """Set the locks field. This is a whitespace-separated list of - mappings from user ids to RCS dotted revision numbers (of the form - :). This is used to identify which revisions have been locked - by users.""" - if value == None: - value = "" - elif from_map: - self.__locks = MapToStringColonMap(value) - else: - self.__locks = value - - def getLocks(self, to_map = True): - """Get the locks field. This is a whitespace-separated list of - mappings from user ids to RCS dotted revision numbers (of the form - :). This is used to identify which revisions have been locked - by users.""" - if to_map: - return StringColonMapToMap(self.__locks) - else: - return self.__locks - - def setStrict(self, value, from_bool = True): - """Set the strict field. If set then RCS requires a user to hold - a lock on a revision before being allowed to check in the next - revision. - This function expects value to be a boolean (unless from_bool is - False).""" - if from_bool: - if value: - value = "" - else: - value = None - else: - if value not in [None, ""]: - raise RcsError("struct can only be None or \"\"") - self.__strict = value - - def getStrict(self, to_bool = True): - """Get the strict field. If set then RCS requires a user to hold - a lock on a revision before being allowed to check in the next - revision. - This function expects value to be a boolean (unless from_bool is - False).""" - if to_bool: - if value not in [None, ""]: - raise RcsError("struct can only be None or \"\"") - return self.__strict == "" - else: - return self.__strict - - def setIntegrity(self, value, handle_quoting = True): - """Set the integrity field. This was added in RCS 5.8 for RCS and - implementation-defined extensions. - If handle_quoting then RCS at quoting is to be added, otherwise value - is assumed to have been quoted already.""" - if handle_quoting: - self.__integrity = AddAtQuoting(value) - else: - self.__integrity = value - - def getIntegrity(self, handle_quoting = True): - """Get the integrity field. This was added in RCS 5.8 for RCS and - implementation-defined extensions. - If handle_quoting then the RCS at quoting will be removed.""" - if handle_quoting: - return RemoveAtQuoting(self.__integrity) - else: - return self.__integrity - - def setComment(self, value, handle_quoting = True): - """Set the comment field. This is an obsolete option used by old - RCS versions for $Log$. - If handle_quoting then RCS at quoting is to be added, otherwise value - is assumed to have been quoted already.""" - if handle_quoting: - self.__comment = AddAtQuoting(value) - else: - self.__comment = value - - def getComment(self, handle_quoting = True): - """Set the comment field. This is an obsolete option used by old - RCS versions for $Log$. - If handle_quoting then the RCS at quoting will be removed.""" - if handle_quoting: - return RemoveAtQuoting(self.__comment) - else: - return self.__comment - - def setExpand(self, value, handle_quoting = True): - """Set the expand field. - If handle_quoting then RCS at quoting is to be added, otherwise value - is assumed to have been quoted already.""" - if handle_quoting: - self.__expand = AddAtQuoting(value) - else: - self.__expand = value - - def getExpand(self, handle_quoting = True): - """Get the expand field. - If handle_quoting then the RCS at quoting will be removed.""" - if handle_quoting: - return RemoveAtQuoting(self.__expand) - else: - return self.__expand - - def addDelta(self, revision, delta): - """Add the given delta as the given revision number.""" - for d in self.__deltas: - if d.getRevision() == revision: - raise RcsError("Revision %s already in deltas"%(revision)) - self.__deltas.append(delta) - - def getDelta(self, revision): - """Return the delta for the given revision number.""" - for d in self.__deltas: - if d.getRevision() == revision: - return d - raise RcsError("Revision %s not found in deltas"%(revision)) - - def delDelta(self, revision): - """Delete the delta for the given revision number.""" - for d in self.__deltas: - if d.getRevision() == revision: - del self.__deltas[self.__deltas.index(d)] - raise RcsError("Revision %s not found in deltas"%(revision)) - - def mapDeltas(self, apply_fn): - """Apply the function apply_fn() to each delta in turn.""" - for d in self.__deltas: - apply_fn(d) - - def setDesc(self, value, handle_quoting = True): - """Set the description field given by the user for this RCS file. - If handle_quoting then RCS at quoting is to be added, otherwise value - is assumed to have been quoted already.""" - if handle_quoting: - self.__desc = AddAtQuoting(value) - else: - self.__desc = value - - def getDesc(self, handle_quoting = True): - """Get the description field given by the user for this RCS file. - If handle_quoting then the RCS at quoting will be removed.""" - if handle_quoting: - return RemoveAtQuoting(self.__desc) - else: - return self.__desc - - def validate(self): - """Validate the Rcs object for missing phrases and other problems. - If a problem is found then raise an RcsError.""" - for d in self.__deltas: - d.validate() - # TODO Check that d's next is in self.__deltas - # TODO Check that head, branch, etc. are in self.__deltas - - if self.getHead() == None: - raise RcsError("head is not set (and is not optional)") - if self.getAccess() == None: - raise RcsError("access is not set (and is not optional)") - if self.getSymbols(False) == None: - raise RcsError("symbols is not set (and is not optional)") - if self.getLocks(False) == None: - raise RcsError("locks is not set (and is not optional)") - if self.getDesc() == None: - raise RcsError("desc is not set (and is not optional)") - - def toString(self): - """Return a string representation of the RCS file for this object. - This performs validation using validate().""" - self.validate() - - s = (textOrNone("head", self.__head) - + textOrNone("branch", self.__branch) - + textOrNone("access", self.__access) - + textOrNone("symbols", self.__symbols) - + textOrNone("locks", self.__locks) - + textOrNone("strict", self.__strict) - + textOrNone("comment", self.__comment) - + textOrNone("expand", self.__expand) - + "\n" - + "".join(map(lambda d: d.deltaToString(), self.__deltas)) - + textOrNone("desc", self.__desc, False) - + "\n" - + "".join(map(lambda d: d.deltaTextToString(), self.__deltas))) - return s - - -############################################################################ -# ParseRcs function -############################################################################ -def ParseRcs(text): - """Parse a string in RCS format and return a corresponding Rcs object""" - - # RCS file format is a regular expression! - - rcs = Rcs() - lex = Lexer(text) - - lex.getKw("head") - rcs.setHead(lex.getNum()) - lex.getSemicolon() - - if lex.getKw("branch", False) != None: - rcs.setBranch(lex.getNum()) - lex.getSemicolon() - - t = lex.getKw("access") - access_list = [] - while True: - t = lex.getId(False) - if t == None: - break - access_list.append(t) - rcs.setAccess(" ".join(access_list)) - lex.getSemicolon() - - lex.getKw("symbols") - symbols_list = [] - while True: - t1 = lex.getSym(False) - if t1 == None: - break - lex.getColon() - t2 = lex.getNum() - symbols_list.append("%s:%s"%(t1, t2)) - rcs.setSymbols(" ".join(symbols_list), False) - lex.getSemicolon() - - lex.getKw("locks") - locks_list = [] - while True: - t1 = lex.getId(False) - if t1 == None: - break - lex.getColon() - t2 = lex.getNum() - locks_list.append("%s:%s"%(t1, t2)) - rcs.setLocks(" ".join(locks_list), False) - lex.getSemicolon() - - if lex.getKw("strict", False) != None: - rcs.setStrict("", False) - lex.getSemicolon() - - if lex.getKw("integrity", False) != None: - rcs.setIntegrity(lex.getString(False), False) - lex.getSemicolon() - - if lex.getKw("comment", False) != None: - rcs.setComment(lex.getString(False), False) - lex.getSemicolon() - - if lex.getKw("expand", False) != None: - rcs.setExpand(lex.getString(False), False) - lex.getSemicolon() - - # Haven't implemented from rcs 5.6, which would go here. - - # We might need a better parser (or at least backtracking) because - # is ';' terminated and we can't otherwise distinguish the - # keywords from in . - # But actually rcs 5.7 source code assumes that if a token contains an - # idchar that isn't a digit or a special then it's an ID; and it doesn't - # distinguish between SYM and ID. - - while True: - rev = lex.getNum(False) - if rev == None: - break - - delta = RcsDelta(rev) - rcs.addDelta(rev, delta) - - lex.getKw("date") - delta.setDate(lex.getNum()) - lex.getSemicolon() - - lex.getKw("author") - delta.setAuthor(lex.getId()) - lex.getSemicolon() - - lex.getKw("state") - delta.setState(lex.getId(False)) - lex.getSemicolon() - - lex.getKw("branches") - branches_list = [] - while True: - t = lex.getNum(False) - if t == None: - break - branches_list.append(t) - delta.setBranches(" ".join(branches_list)) - lex.getSemicolon() - - lex.getKw("next") - delta.setNext(lex.getNum(False)) - lex.getSemicolon() - - if lex.getKw("commitid", False) != None: - delta.setCommitId(lex.getId(True)) - lex.getSemicolon() - - # Haven't implemented from rcs 5.6, which would go here. - - lex.getKw("desc") - rcs.setDesc(lex.getString(), False) - - while True: - rev = lex.getNum(False) - if rev == None: - break - - delta = rcs.getDelta(rev) - - lex.getKw("log") - delta.setLog(lex.getString(), False) - - lex.getKw("text") - delta.setText(lex.getString(), (rcs.getHead() != rev), False) - - lex.checkNewlineTerm() - return rcs - -############################################################################ diff --git a/setup.py b/setup.py index 1d516fa..812cb6e 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ setup( name='editrcs', - version='0.5.0', + version='0.5.1', author='Ben Cohen', packages=['editrcs'], url='http://github.com/ben-cohen/editrcs',