Copyright 1996-2003 John D. Polstra.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. All advertising materials mentioning features or use of this software
* must display the following acknowledgment:
* This product includes software developed by John D. Polstra.
* 4. The name of the author may not be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
* IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
MODULE RCSDelta EXPORTS RCSDelta, RCSDeltaClass;
IMPORT
Fmt, RCSDeltaList, RCSEdit, RCSError, RCSFile, RCSPhrases,
RCSRevNum, RCSString, SortedRCSEditTbl, Text, TokScan;
***************************************************************************
Accessor methods.
***************************************************************************
REVEAL
Accessor = AccessorPublic BRANDED OBJECT
delta: T;
METHODS
init(delta: T): Accessor := AccessorInit;
END;
PROCEDURE AccessorInit(self: Accessor; delta: T): Accessor =
BEGIN
self.delta := delta;
RETURN self;
END AccessorInit;
***************************************************************************
HeadAccessor methods.
***************************************************************************
TYPE
HeadAccessor = Accessor OBJECT
str: RCSString.T;
nLines: CARDINAL;
curLine: CARDINAL;
iter: RCSString.Iterator;
METHODS
init(delta: T): HeadAccessor := HeadInit;
OVERRIDES
getLine := HeadGetLine;
numLines := HeadNumLines;
END;
PROCEDURE HeadInit(ha: HeadAccessor; delta: T): HeadAccessor =
BEGIN
EVAL Accessor.init(ha, delta);
ha.str := delta.text;
ha.nLines := delta.text.numLines();
ha.curLine := LAST(CARDINAL);
ha.iter := NIL;
RETURN ha;
END HeadInit;
PROCEDURE HeadGetLine(ha: HeadAccessor; i: CARDINAL): RCSString.T =
VAR
line: RCSString.T;
ok: BOOLEAN;
BEGIN
<* ASSERT i >= 1 *>
<* ASSERT i <= ha.nLines *>
IF ha.curLine > i THEN (* Seeking backwards, must start from front. *)
ha.iter := ha.str.iterate();
ha.curLine := 1;
END;
WHILE ha.curLine <= i DO (* Skip over unwanted lines, keep one we want. *)
ok := ha.iter.next(line);
<* ASSERT ok *>
INC(ha.curLine);
END;
RETURN line;
END HeadGetLine;
PROCEDURE HeadNumLines(ha: HeadAccessor): CARDINAL =
BEGIN
RETURN ha.nLines;
END HeadNumLines;
***************************************************************************
DiffAccessor methods.
***************************************************************************
TYPE
DiffAccessor = Accessor OBJECT
tbl: SortedRCSEditTbl.T;
nLines: CARDINAL;
iter: SortedRCSEditTbl.Iterator;
curStart: CARDINAL;
curKey: INTEGER;
curEdit: RCSEdit.T;
delKey: INTEGER; (* Pending delete key *)
delEdit: RCSEdit.T; (* Pending delete record, or NIL *)
METHODS
init(delta: T): DiffAccessor RAISES {RCSError.E} := DiffInit;
OVERRIDES
getLine := DiffGetLine;
numLines := DiffNumLines;
END;
PROCEDURE DiffInit(da: DiffAccessor; delta: T): DiffAccessor
RAISES {RCSError.E} =
VAR
iter := delta.text.iterate();
ts: TokScan.T;
line: RCSString.T;
type: CHAR;
where: CARDINAL;
numLines: CARDINAL;
baseLines := delta.diffBase.accessor.numLines();
lastALine := -1;
lastDLine := -1;
key: CARDINAL; (* Last output line affected by edit *)
text: REF ARRAY OF RCSString.T;
offset := 0; (* Output line # + offset = input line # *)
ok: BOOLEAN;
BEGIN
EVAL Accessor.init(da, delta);
da.tbl := NEW(SortedRCSEditTbl.Default).init();
da.delEdit := NIL;
WHILE iter.next(line) DO
ts := TokScan.New(line.toText());
TRY
WITH field = ts.getToken("edit command") DO
type := Text.GetChar(field, 0);
where := TokScan.AtoI(Text.Sub(field, 1), "edit line number");
END;
numLines := ts.getInt("edit line count");
EXCEPT TokScan.Error(msg) =>
RAISE RCSError.E(msg);
END;
CASE type OF
| 'a' =>
IF where > baseLines THEN
RAISE RCSError.E(
"\"a\" edit command specifies line past end of base delta");
END;
IF where <= lastALine OR where < lastDLine THEN
RAISE RCSError.E(
"\"a\" edit command applies to line preceding earlier edit");
END;
lastALine := where;
key := where + numLines - offset;
text := NEW(REF ARRAY OF RCSString.T, numLines);
FOR i := 0 TO numLines-1 DO
IF NOT iter.next(text[i]) THEN
RAISE RCSError.E("Too few edit lines to add");
END;
END;
IF da.delEdit # NIL AND da.delKey = key - numLines THEN
(* We can merge this add into the previous delete. *)
da.delKey := key;
da.delEdit.text := text;
DiffPutPendingDelete(da);
ELSE
DiffPutPendingDelete(da);
DiffAddEdit(da, key, NEW(RCSEdit.T, offset := offset, text := text));
END;
DEC(offset, numLines);
| 'd' =>
IF where + numLines - 1 > baseLines THEN
RAISE RCSError.E(
"\"d\" edit command specifies line past end of base delta");
END;
IF where <= lastALine OR where <= lastDLine THEN
RAISE RCSError.E(
"\"d\" edit command applies to line preceding earlier edit");
END;
lastDLine := where + numLines - 1;
DiffPutPendingDelete(da);
da.delKey := where - 1 - offset;
da.delEdit := NEW(RCSEdit.T, offset := offset, text := NIL);
INC(offset, numLines);
ELSE
RAISE RCSError.E(
"Invalid edit command \"" & Text.FromChar(type) & "\"");
END;
END;
DiffPutPendingDelete(da);
da.nLines := baseLines - offset;
DiffAddEdit(da, da.nLines+1, NEW(RCSEdit.T, offset := offset,
text := NIL));
da.iter := da.tbl.iterate();
da.curStart := 1;
ok := da.iter.next(da.curKey, da.curEdit);
<* ASSERT ok *>
RETURN da;
END DiffInit;
PROCEDURE DiffGetLine(da: DiffAccessor; i: CARDINAL): RCSString.T =
VAR
daCur := da;
iCur := i;
BEGIN
(* This used to be a nice clean recursive function. But it caused
overflow of the limited thread stacks in some pathological cases.
Luckily, the recursion was only tail recursion, so it was fairly
easy to transform it into a loop. *)
LOOP
<* ASSERT iCur >= 1 *>
<* ASSERT iCur <= daCur.nLines *>
IF iCur < daCur.curStart OR iCur > daCur.curKey THEN (* Need new edit *)
IF iCur # daCur.curKey + 1 THEN (* Random access, we have to seek *)
daCur.iter.seek(iCur);
END;
WITH ok = daCur.iter.next(daCur.curKey, daCur.curEdit) DO
<* ASSERT ok *>
END;
<* ASSERT iCur <= daCur.curKey *>
daCur.curStart := iCur;
END;
IF daCur.curEdit.text # NIL THEN
WITH firstAdded = daCur.curKey - NUMBER(daCur.curEdit.text^) + 1 DO
IF firstAdded <= iCur THEN
RETURN daCur.curEdit.text[iCur-firstAdded];
END;
END;
END;
INC(iCur, daCur.curEdit.offset);
WITH newAcc = daCur.delta.diffBase.accessor DO
IF newAcc.delta.diffBase = NIL THEN (* Found the head. *)
RETURN newAcc.getLine(iCur);
END;
daCur := newAcc;
END;
END;
END DiffGetLine;
PROCEDURE DiffNumLines(da: DiffAccessor): CARDINAL =
BEGIN
RETURN da.nLines;
END DiffNumLines;
PROCEDURE DiffAddEdit(da: DiffAccessor; key: CARDINAL; edit: RCSEdit.T)
RAISES {RCSError.E} =
BEGIN
IF da.tbl.put(key, edit) THEN
RAISE RCSError.E("Duplicate edit ending with output line "
& Fmt.Int(key));
END;
END DiffAddEdit;
PROCEDURE DiffPutPendingDelete(da: DiffAccessor)
RAISES {RCSError.E} =
BEGIN
IF da.delEdit # NIL THEN
DiffAddEdit(da, da.delKey, da.delEdit);
da.delEdit := NIL;
END;
END DiffPutPendingDelete;
***************************************************************************
Support for getting reverse diffs.
***************************************************************************
TYPE
DiffIterator = RCSString.Iterator OBJECT
delta: T;
state: DIState;
iter: SortedRCSEditTbl.Iterator;
curKey, nextKey: INTEGER;
curEdit, nextEdit: RCSEdit.T;
nextAddedLine, lastAddedLine: CARDINAL;
numAdded, numDeleted: CARDINAL;
END;
DIState = { GetNextEdit, PutDelCmd, PutAddCmd, PutAddedLine };
PROCEDURE GetReverseDiff(delta: T): RCSString.Iterator
RAISES {RCSError.E} =
VAR
di: DiffIterator;
ok: BOOLEAN;
BEGIN
<* ASSERT delta.diffBase # NIL *>
MakeAccessors(delta);
di := NEW(DiffIterator, next := ReverseDiffNext);
di.delta := delta;
di.iter := NARROW(delta.accessor, DiffAccessor).tbl.iterate();
di.state := DIState.GetNextEdit;
ok := di.iter.next(di.nextKey, di.nextEdit);
<* ASSERT ok *>
RETURN di;
END GetReverseDiff;
PROCEDURE ReverseDiffNext(di: DiffIterator; VAR line: RCSString.T): BOOLEAN =
VAR
BEGIN
LOOP
CASE di.state OF
| DIState.GetNextEdit =>
di.curKey := di.nextKey;
di.curEdit := di.nextEdit;
IF NOT di.iter.next(di.nextKey, di.nextEdit) THEN
RETURN FALSE;
END;
IF di.curEdit.text # NIL THEN
di.numDeleted := NUMBER(di.curEdit.text^);
ELSE
di.numDeleted := 0;
END;
di.numAdded := di.numDeleted + di.nextEdit.offset - di.curEdit.offset;
IF di.numDeleted > 0 THEN
di.state := DIState.PutDelCmd;
ELSIF di.numAdded > 0 THEN
di.state := DIState.PutAddCmd;
END;
| DIState.PutDelCmd =>
<* ASSERT di.numDeleted > 0 *>
WITH firstOutputLineDeleted = di.curKey - di.numDeleted + 1 DO
line := RCSString.FromText(
"d" & Fmt.Int(firstOutputLineDeleted) &
" " & Fmt.Int(di.numDeleted) & "\n");
END;
IF di.numAdded > 0 THEN
di.state := DIState.PutAddCmd;
ELSE
di.state := DIState.GetNextEdit;
END;
RETURN TRUE;
| DIState.PutAddCmd =>
<* ASSERT di.numAdded > 0 *>
WITH lastOutputLineDeleted = di.curKey DO
line := RCSString.FromText(
"a" & Fmt.Int(lastOutputLineDeleted) &
" " & Fmt.Int(di.numAdded) & "\n");
END;
di.nextAddedLine := di.curKey - di.numDeleted + 1 + di.curEdit.offset;
di.lastAddedLine := di.nextAddedLine + di.numAdded - 1;
di.state := DIState.PutAddedLine;
RETURN TRUE;
| DIState.PutAddedLine =>
<* ASSERT di.nextAddedLine <= di.lastAddedLine *>
line := di.delta.diffBase.accessor.getLine(di.nextAddedLine);
INC(di.nextAddedLine);
IF di.nextAddedLine > di.lastAddedLine THEN
di.state := DIState.GetNextEdit;
END;
RETURN TRUE;
END;
END;
END ReverseDiffNext;
The following procedure is unused at present. But it is correct. It
is rather complicated, and potentially useful, so I am leaving it in
the code for the time being.
<*UNUSED*>
PROCEDURE ForwardDiffNext(di: DiffIterator; VAR line: RCSString.T): BOOLEAN =
VAR
BEGIN
LOOP
CASE di.state OF
| DIState.GetNextEdit =>
di.curKey := di.nextKey;
di.curEdit := di.nextEdit;
IF NOT di.iter.next(di.nextKey, di.nextEdit) THEN
RETURN FALSE;
END;
IF di.curEdit.text # NIL THEN
di.numAdded := NUMBER(di.curEdit.text^);
ELSE
di.numAdded := 0;
END;
di.numDeleted := di.numAdded + di.nextEdit.offset - di.curEdit.offset;
IF di.numDeleted > 0 THEN
di.state := DIState.PutDelCmd;
ELSIF di.numAdded > 0 THEN
di.state := DIState.PutAddCmd;
END;
| DIState.PutDelCmd =>
<* ASSERT di.numDeleted > 0 *>
WITH lastInputLineNotDeleted = di.curKey - di.numAdded +
di.curEdit.offset,
firstInputLineDeleted = lastInputLineNotDeleted + 1 DO
line := RCSString.FromText(
"d" & Fmt.Int(firstInputLineDeleted) &
" " & Fmt.Int(di.numDeleted) & "\n");
END;
IF di.numAdded > 0 THEN
di.state := DIState.PutAddCmd;
ELSE
di.state := DIState.GetNextEdit;
END;
RETURN TRUE;
| DIState.PutAddCmd =>
<* ASSERT di.numAdded > 0 *>
WITH lastInputLineNotDeleted = di.curKey - di.numAdded +
di.curEdit.offset,
lastInputLineDeleted = lastInputLineNotDeleted + di.numDeleted DO
line := RCSString.FromText(
"a" & Fmt.Int(lastInputLineDeleted) &
" " & Fmt.Int(di.numAdded) & "\n");
END;
di.lastAddedLine := di.curKey;
di.nextAddedLine := di.curKey - di.numAdded + 1;
di.state := DIState.PutAddedLine;
RETURN TRUE;
| DIState.PutAddedLine =>
<* ASSERT di.nextAddedLine <= di.lastAddedLine *>
line := di.delta.accessor.getLine(di.nextAddedLine);
INC(di.nextAddedLine);
IF di.nextAddedLine > di.lastAddedLine THEN
di.state := DIState.GetNextEdit;
END;
RETURN TRUE;
END;
END;
END ForwardDiffNext;
***************************************************************************
TYPE
TextIterator = RCSString.Iterator OBJECT
delta: T;
curLine: CARDINAL;
numLines: CARDINAL;
OVERRIDES
next := TextNext;
END;
PROCEDURE GetText(delta: T;
diffBase: T := NIL): RCSString.Iterator
RAISES {RCSError.E} =
VAR
iter: RCSString.Iterator;
BEGIN
IF delta.diffBase = diffBase THEN
(* What we want is in the delta's text. *)
RCSFile.ParseDelta(delta.rcsFile, delta);
iter := delta.text.iterate();
ELSIF diffBase = NIL THEN
(* Get the full text. *)
MakeAccessors(delta);
iter := NEW(TextIterator, delta := delta, curLine := 1,
numLines := delta.accessor.numLines());
ELSIF diffBase.diffBase = delta THEN
(* We can do it with a reverse diff. *)
iter := GetReverseDiff(diffBase);
ELSE
RAISE RCSError.E("Cannot compute diffs from " & diffBase.revision
& " to " & delta.revision);
END;
RETURN iter;
END GetText;
PROCEDURE TextNext(ti: TextIterator; VAR line: RCSString.T): BOOLEAN =
BEGIN
IF ti.curLine <= ti.numLines THEN
line := ti.delta.accessor.getLine(ti.curLine);
INC(ti.curLine);
RETURN TRUE;
END;
RETURN FALSE;
END TextNext;
PROCEDURE MakeAccessors(delta: T) RAISES {RCSError.E} =
VAR
stack: RCSDeltaList.T := NIL;
BEGIN
(* We use an explicit stack rather than recursion, to avoid
overflowing the possibly-limited thread stack space. *)
WHILE delta.accessor = NIL AND delta.diffBase # NIL DO
stack := RCSDeltaList.Cons(delta, stack);
delta := delta.diffBase;
END;
IF delta.accessor = NIL THEN (* This delta has full text. *)
RCSFile.ParseDelta(delta.rcsFile, delta);
delta.accessor := NEW(HeadAccessor).init(delta);
END;
WHILE stack # NIL DO
delta := stack.head;
RCSFile.ParseDelta(delta.rcsFile, delta);
delta.accessor := NEW(DiffAccessor).init(delta);
stack := stack.tail;
END;
END MakeAccessors;
***************************************************************************
PROCEDURE AddBranch(delta: T;
branch: T;
diffBase: T)
RAISES {RCSError.E} =
VAR
bl := delta.branches;
last: RCSDeltaList.T := NIL;
BEGIN
IF diffBase = NIL THEN diffBase := delta END;
WHILE bl # NIL AND Compare(bl.head, branch) < 0 DO
last := bl;
bl := bl.tail;
END;
IF bl # NIL AND Equal(bl.head, branch) THEN
RAISE RCSError.E("Attempt to add existing branch " & branch.revision);
END;
WITH newElem = RCSDeltaList.Cons(branch, bl) DO
IF last = NIL THEN (* Adding at front of list *)
delta.branches := newElem;
ELSE
last.tail := newElem;
END;
END;
branch.prev := delta;
branch.diffBase := diffBase;
END AddBranch;
PROCEDURE ChangeBranch(delta, oldBranch, newBranch: T)
RAISES {RCSError.E} =
VAR
bl := delta.branches;
BEGIN
WHILE bl # NIL AND bl.head # oldBranch DO
bl := bl.tail;
END;
IF bl = NIL THEN
RAISE RCSError.E("Attempt to change nonexistent branch "
& oldBranch.revision);
END;
oldBranch.prev := NIL;
bl.head := newBranch;
newBranch.prev := delta;
END ChangeBranch;
PROCEDURE Compare(d1, d2: T): [-1..1] =
BEGIN
RETURN RCSRevNum.Compare(d1.revision, d2.revision);
END Compare;
PROCEDURE Dead(delta: T; inAttic: BOOLEAN): BOOLEAN =
VAR
rf: RCSFile.T;
BEGIN
(* If its state is "dead", then it is dead. *)
IF delta.state # NIL AND Text.Equal(delta.state, "dead") THEN
RETURN TRUE;
END;
(* If it is in the Attic and it is the head of the default branch,
then it is dead. This is intended to approximate CVS's broken
backward compatibility with early-version repositories. *)
IF inAttic THEN
rf := delta.rcsFile;
IF rf.branch = NIL OR RCSRevNum.NumParts(rf.branch) = 1 THEN
(* Default branch is main branch. *)
RETURN RCSRevNum.IsTrunk(delta.revision) AND delta.prev = NIL;
ELSE
RETURN RCSRevNum.Equal(RCSRevNum.Prefix(delta.revision), rf.branch)
AND delta.next = NIL;
END;
END;
(* It is alive. *)
RETURN FALSE;
END Dead;
PROCEDURE DeleteBranch(delta: T; branch: T)
RAISES {RCSError.E} =
VAR
bl := delta.branches;
last: RCSDeltaList.T := NIL;
BEGIN
WHILE bl # NIL AND bl.head # branch DO
last := bl;
bl := bl.tail;
END;
IF bl = NIL THEN
RAISE RCSError.E("Attempt to delete nonexistent branch "
& branch.revision);
END;
IF last = NIL THEN
delta.branches := bl.tail;
ELSE
last.tail := bl.tail;
END;
END DeleteBranch;
PROCEDURE Equal(d1, d2: T): BOOLEAN =
BEGIN
RETURN RCSRevNum.Equal(d1.revision, d2.revision);
END Equal;
PROCEDURE GetBranch(delta: T; revNum: RCSRevNum.T): T
RAISES {RCSError.E} =
VAR
bl := delta.branches;
BEGIN
WITH n = RCSRevNum.NumParts(revNum) DO
IF n < 3 OR n MOD 2 = 0 THEN
RAISE RCSError.E("Attempt to get invalid branch number " & revNum);
END;
END;
WHILE bl # NIL
AND NOT RCSRevNum.Equal(RCSRevNum.Prefix(bl.head.revision), revNum) DO
bl := bl.tail;
END;
IF bl = NIL THEN
RAISE RCSError.E("Attempt to get nonexistent branch " & revNum);
END;
RETURN bl.head;
END GetBranch;
PROCEDURE GetLog(delta: T): RCSString.T
RAISES {RCSError.E} =
BEGIN
RCSFile.ParseDelta(delta.rcsFile, delta);
RETURN delta.log;
END GetLog;
PROCEDURE GetPrev(delta: T): T =
BEGIN
RETURN delta.prev;
END GetPrev;
PROCEDURE Predecessor(delta: T): T =
BEGIN
IF RCSRevNum.IsTrunk(delta.revision) THEN
RETURN delta.next;
ELSE
RETURN delta.prev;
END;
END Predecessor;
PROCEDURE ToText(delta : T) : TEXT =
VAR res : TEXT;
BEGIN
IF delta.revision = NIL THEN
res := "(no revision) ";
ELSE
res := delta.revision & " ";
END;
IF delta.date = NIL THEN
res := res & "(no date) ";
ELSE
res := res & delta.date & " ";
END;
IF delta.author = NIL THEN
res := res & "no author ";
ELSE
res := res & delta.author & " ";
END;
IF delta.state = NIL THEN
res := res & "no state ";
ELSE
res := res & delta.state;
END;
RETURN res;
END ToText;
***************************************************************************
TYPE
BranchIterator = Iterator OBJECT
cur: RCSDeltaList.T;
OVERRIDES
next := NextBranch;
END;
PROCEDURE IterateBranches(delta: T): Iterator =
BEGIN
RETURN NEW(BranchIterator, cur := delta.branches);
END IterateBranches;
PROCEDURE IterateBranchesReversed(delta: T): Iterator =
BEGIN
RETURN NEW(BranchIterator, cur := RCSDeltaList.Reverse(delta.branches));
END IterateBranchesReversed;
PROCEDURE NextBranch(bi: BranchIterator; VAR delta: T): BOOLEAN =
BEGIN
IF bi.cur = NIL THEN RETURN FALSE END;
delta := bi.cur.head;
bi.cur := bi.cur.tail;
RETURN TRUE;
END NextBranch;
***************************************************************************
PROCEDURE IterateTextPhrases(delta: T): RCSPhrases.Iterator =
BEGIN
RETURN RCSPhrases.Iterate(delta.textPhrases);
END IterateTextPhrases;
PROCEDURE IterateTreePhrases(delta: T): RCSPhrases.Iterator =
BEGIN
RETURN RCSPhrases.Iterate(delta.treePhrases);
END IterateTreePhrases;
BEGIN
END RCSDelta.