/*
* ====================================================================
*
* The Apache Software License, Version 1.1
*
* Copyright (c) 1999-2003 The Apache Software Foundation.
* 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. The end-user documentation included with the redistribution, if
* any, must include the following acknowledgement:
* "This product includes software developed by the
* Apache Software Foundation (http://www.apache.org/)."
* Alternately, this acknowledgement may appear in the software itself,
* if and wherever such third-party acknowledgements normally appear.
*
* 4. The names "The Jakarta Project", "Commons", and "Apache Software
* Foundation" must not be used to endorse or promote products derived
* from this software without prior written permission. For written
* permission, please contact apache@apache.org.
*
* 5. Products derived from this software may not be called "Apache"
* nor may "Apache" appear in their names without prior written
* permission of the Apache Software Foundation.
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED 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 APACHE SOFTWARE FOUNDATION OR
* ITS CONTRIBUTORS 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.
* ====================================================================
*
* This software consists of voluntary contributions made by many
* individuals on behalf of the Apache Software Foundation. For more
* information on the Apache Software Foundation, please see
* <http://www.apache.org/>.
*
*/
package org.apache.commons.jrcs.rcs;
import java.io.*;
import java.util.*;
import org.apache.commons.jrcs.diff.Diff;
import org.apache.commons.jrcs.diff.DiffException;
import org.apache.commons.jrcs.diff.PatchFailedException;
import org.apache.commons.jrcs.util.ToString;
/**
* Handling of RCS/CVS style version control archives.
*
*
* <p>JRCS is a library that knows how to manipulate the archive files produced
* by the RCS and CVS version control systems. JRCS is not intended to replace
* neither tool. JRCS was written to be able create archive analysis tools
* that can do things like identify hot spots in the source code,
* measure the contributions by each developer,
* or assess how bugs make it in.</p>
*
* <p>The reasons why JRCS has the ability do do check-ins and save archives
* is API symmetry, and to simplify the writing of unit tests.</p>
*
* <p><b>CAVEAT UTILITOR:</b> Do not make modifications to your archives with JRCS.
* There needs to be an important amount of additional testing
* before it's safe to do that.</p>
*
* <p>The {@link org.apache.commons.jrcs.rcs rcs} package implements the
* archive handling functionality. The entry point to the library is class
* {@link org.apache.commons.jrcs.rcs.Archive Archive}.</p>
*
*
* <p>The {@link org.apache.commons.jrcs.diff diff} package implements
* the differencing engine that JRCS uses. The engine has the power of Unix diff,
* is simple to understand, and can be used independently of the archive handling
* functionality. The entry point to the differencing engine is class
* {@link org.apache.commons.jrcs.diff.Diff Diff}.</p>
*
* <p>Within this library, the word <i>text</i> means a unit of information
* subject to version control. The word <i>revision</i> means a particular
* version of a text. Each <i>revision</i> has a <i>version number</i>
* associated to it. <i>Version numbers</i> are dot-separated lists of numbers.
* Version numbers with an odd number of dots indicate revisions, while those
* with an even number of dots (including zero dots) designate branches.</p>
*
* <p>Revisions of a text are represented as <code>Object[]</code> because
* the diff engine is capable of handling more than plain text. In fact,
* arrays of any type that implements
* {@link java.lang.Object#hashCode hashCode()} and
* {@link java.lang.Object#equals equals()}
* correctly can be subject to differencing and version control using this
* library.</p>
*
* <p>To create an empty archive use:
* <code><pre>
* Archive archive = new Archive();
* </pre></code>
* </p>
*
* <p>To read an archive from the file system, use:
* <code><pre>
* Archive archive = new Archive("/path/to/archive,v");
* </pre></code>
* </p>
*
* <p>You can also initialize archives from streams.</p>
*
* <p>To retreive a revision from an archive use:
* <code><pre>
* String versionNumber = "1.2";
* Object[] text = archive.getRevision(versionNumber);
* </pre></code>
* </p>
*
* <p>You can also retreive revisions in such a way that each item
* is annotated with the version number of the revision in which it was
* last changed or added. To retrieve annotated text use:
* <code><pre>
* String versionNumber = "1.2";
* {@link Line Line[]} text = archive.getRevision(versionNumber);
* for(int i = 0; i < text.length(); i++)
* System.out.println(text[i].revision.version);
* </pre></code>
* </p>
*
* <p>This class is NOT thread safe.</p>
* @see org.apache.commons.jrcs.diff
*
* @version $Id: Archive.java 84222 2010-11-03 13:15:52Z david.horwitz@uct.ac.za $
* @author <a href="mailto:juanco@suigeneris.org">Juanco Anez</a>
*/
public class Archive
extends ToString
{
public static final String RCS_NEWLINE = "\n";
protected TrunkNode head;
protected Version branch;
protected Map nodes = new TreeMap(); //!!! check Node.compareTo for correct RCS order
protected Set users = new TreeSet();
protected Set locked = new TreeSet();
protected Map symbols = new TreeMap();
protected Phrases phrases = new Phrases();
protected String desc = "";
protected boolean strictLocking = true;
protected String expand;
protected String comment = "# ";
protected String filename = "__unknown__,v";
// synchronize this if this has to be used in MT !
private static final KeywordsFormat FORMATTER = new KeywordsFormat();
/**
* Creates a new archive and sets the text of the initial revision.
* @param text The text of the initial revision.
* @param desc The archives description (not the log message).
*/
public Archive(Object[] text, String desc)
{
this(text, desc, new Version(1, 1));
}
/**
* Creates a new archive with the specified initial version number
* and sets the text of the initial revision.
* The initial revision must be of the form "n.m" (i.e. a trunk revision).
* @param text The text of the initial revision.
* @param desc The archives description (not the log message).
* @param vernum The initial revision number.
*/
public Archive(Object[] text, String desc, String vernum)
{
this(text, desc, new Version(vernum));
}
/**
* Creates a new archive with the specified initial version number
* and sets the text of the initial revision.
* The initial revision must be of the form "n.m" (i.e. a trunk revision).
* @param text The text of the initial revision.
* @param desc The archives description (not the log message).
* @param vernum The initial revision number.
*/
public Archive(Object[] text, String desc, Version vernum)
{
// can only add a trunk version
if (vernum.size() > 2)
{
throw new InvalidVersionNumberException(vernum + " must be a trunk version");
}
while (vernum.size() < 2)
{
vernum = vernum.newBranch(1);
}
// now add the _head node
this.head = (TrunkNode) newNode(vernum, null);
this.head.setText(text);
this.head.setLog(desc);
}
/**
* Load an archive from an input stream.
* Parses the archive given by the input stream, and gives it the provided name.
* @param fname The name to give to the archive.
* @param input Where to read the archive from
*/
public Archive(String fname, InputStream input) throws ParseException
{
this.filename = fname;
ArchiveParser.load(this, input);
}
/**
* Load an archive from an a file given by name.
* @param path The path to the file wher the archive resides.
*/
public Archive(String path) throws ParseException, FileNotFoundException
{
this.filename = new File(path).getPath();
ArchiveParser.load(this, this.filename);
}
/**
* Create an unitialized Archive.
* Used internally by the ArchiveParser.
* @see ArchiveParser
*/
Archive()
{
}
/**
* Set the name of the file for this archive
* @param path The full path name.
*/
public void setFileName(String path)
{
this.filename = path;
}
/**
* Save the archive to the provided stream.
* @param output The stream to save the archive to.
*/
public void save(OutputStream output)
throws IOException
{
output.write(toByteArray());
}
/**
* Save the archive to a file and the the Archives filename
* accordingly.
* @param path The file's path.
*/
public void save(String path)
throws IOException
{
OutputStream output = new FileOutputStream(path);
try
{
save(output);
this.filename = new File(path).getPath();
}
finally
{
output.close();
}
}
/**
* Add a head node with the given version number.
* @param vernum The version number to use.
*/
protected void setHead(Version vernum) throws InvalidVersionNumberException
{
if (head != null)
{
throw new HeadAlreadySetException(head.getVersion());
}
head = new TrunkNode(vernum, null);
nodes.put(vernum, head);
}
/**
* Set the active branch to the one identified by the given version number.
* Incomplete version numbers of the form "1" or "2.1.3" are accepted.
* @param v The version number.
*/
public void setBranch(String v) throws InvalidBranchVersionNumberException
{
setBranch(new Version(v));
}
/**
* Set the active branch to the one identified by the given version number.
* @param vernum The version number.
*/
public void setBranch(Version vernum) throws InvalidBranchVersionNumberException
{
if (!vernum.isBranch())
{
throw new InvalidBranchVersionNumberException(vernum);
}
if (head == null || vernum.getBase(2).isGreaterThan(head.getVersion()))
{
throw new InvalidBranchVersionNumberException(vernum + "is greater than _head version " + head.getVersion());
}
branch = vernum;
}
/** Add a user name to the list of archive users.
* @param name The user name.
*/
public void addUser(String name)
{
users.add(name);
}
/**
* Tag a given version with a symbol.
* @param sym The tag.
* @param vernum The version to tag.
*/
public void addSymbol(String sym, Version vernum) throws InvalidVersionNumberException
{
//@TODO: Verify if the symbol is valid
symbols.put(sym, vernum);
}
/**
* Returns a Map of the symbols (tags) associated with each revision.
* The symbols are the keys and the revision numbers are the values.
* @return A map of symbol/revision number pairs.
*/
public Map getSymbols()
{
return symbols;
}
/**
* Add a lock over a revison.
* @param user The user that locks the revision.
* @param vernum The version number of the revision to lock.
*/
public void addLock(String user, Version vernum)
throws InvalidVersionNumberException,
NodeNotFoundException
{
addUser(user);
Node node = newNode(vernum);
node.setLocker(user);
if (user == null)
{
locked.remove(node);
}
else
{
locked.add(node);
}
}
/**
* Set the strict locking flag for the archive.
* @param value Indicates if strict locking should be on or off.
*/
public void setStrictLocking(boolean value)
{
strictLocking = value;
}
/**
* Set the keyword expansion flag for the archive.
* @param value The keyword expansion value. It should be one of:
* <ul>
* <li> kv (Default) Substitue keyword and value.
* <li> kvl Substitute keyword, value, and locker (if any).
* <li> k Substitute keyword only.
* <li> o Preserve original string.
* <li> b Like o, but mark file as binary.
* <li> v Substitue value only.
* </ul>
*/
public void setExpand(String value)
{
expand = value;
}
/**
* Set the archive's comment.
* @param value The comment.
*/
public void setComment(String value)
{
comment = value;
}
/**
* Set the archives description.
* @param value The descriptions text.
*/
public void setDesc(String value)
{
desc = value;
}
/**
* Add a new phrase to the archive.
* Phrases are used to provide for extensions of the archive format.
* Each phrase has a key and a list of values associated with it.
* @param key The phrases key.
* @param values The values under the key.
*/
public void addPhrase(String key, Collection values)
{
phrases.put(key, values);
}
protected Node newNode(Version vernum)
{
return newNode(vernum, null);
}
protected Node newNode(Version vernum, Node prev)
throws InvalidVersionNumberException,
NodeNotFoundException
{
if (!vernum.isRevision())
{
throw new InvalidVersionNumberException(vernum);
}
Node node = (Node) nodes.get(vernum);
if (node == null)
{
node = Node.newNode(vernum, prev);
nodes.put(vernum, node);
}
return node;
}
protected TrunkNode newTrunkNode(Version vernum)
throws InvalidVersionNumberException,
NodeNotFoundException
{
if (!vernum.isTrunk())
{
throw new InvalidTrunkVersionNumberException(vernum);
}
return (TrunkNode) newNode(vernum);
}
protected BranchNode newBranchNode(Version vernum)
throws InvalidVersionNumberException,
NodeNotFoundException
{
if (!vernum.isBranch())
{
throw new InvalidBranchVersionNumberException(vernum);
}
return (BranchNode) newNode(vernum);
}
protected Node getNode(Version vernum)
throws InvalidVersionNumberException,
NodeNotFoundException
{
if (!vernum.isRevision())
{
throw new InvalidVersionNumberException(vernum);
}
Node node = (Node) nodes.get(vernum);
if (node == null)
{
throw new NodeNotFoundException(vernum);
}
return node;
}
/**
* Return the node with the version number that matches the one provided.
* The given version number may be partial.
* @param vernum the version number to match.
* @return the node, or null if no match found.
*/
public Node findNode(Version vernum)
{
Path path = getRevisionPath(vernum);
return (path == null ? null : path.last());
}
/**
* Place a string image of the archive in the given StringBuffer.
* @param s Where the image shoul go.
*/
public void toString(StringBuffer s)
{
toString(s, RCS_NEWLINE);
}
/**
* Return a text image of the archive.
* @param EOL The token to use as line separator.
* @return The text image of the archive.
*/
public String toString(String EOL)
{
StringBuffer s = new StringBuffer();
toString(s, EOL);
return s.toString();
}
/**
* Return a text image of the archive as a char array.
* This is useful for writing the archive to a file without
* having the characters be interpreted by the writer.
* @return The archive image.
*/
public char[] toCharArray()
{
return toString(Archive.RCS_NEWLINE).toCharArray();
}
/**
* Return a text image of the archive as a char array.
* This is useful for writing the archive to a file without
* having the characters be interpreted by the writer.
* @return The archive image.
*/
public byte[] toByteArray()
{
return toString(Archive.RCS_NEWLINE).getBytes();
}
/**
* Returns the path from the head node to the node identified
* by the given version number.
* @param vernum The version number that identifies the final node.
* Partial version numbers are OK.
* @return The path to the node, or null if not found.
*/
protected Path getRevisionPath(Version vernum)
{
if (head == null)
{
return null;
}
try
{
Path path = head.pathTo(vernum, true);
Node revisionFound = path.last();
if (revisionFound == null)
{
return null;
}
if (revisionFound.getVersion().isLessThan(vernum))
{
return null;
}
return path;
}
catch (NodeNotFoundException e)
{
return null;
}
}
/**
* Return the actual revision number of the node identified
* by the given version number.
* @param vernum The version number that identifies the node.
* Partial version numbers are OK.
* @return The actual version, or null if a node is not found.
*/
public Version getRevisionVersion(Version vernum)
{
Path path = getRevisionPath(vernum);
return (path == null ? null : path.last().getVersion());
}
/**
* Return the actual revision number of the node identified
* by the given version number.
* @param vernum The version number that identifies the node.
* Partial version numbers are OK.
* @return The actual version, or null if a node is not found.
*/
public Version getRevisionVersion(String vernum)
{
return getRevisionVersion(new Version(vernum));
}
/**
* Return the actual revision number of the active revision.
* The revision will be the tip of the branch identified as
* active, or the head revision of the trunk if no branch is set
* as active.
* @return The version number of the active revision, or null if
* there is none.
*/
public Version getRevisionVersion()
{
if (branch != null)
{
return getRevisionVersion(branch);
}
else if (head != null)
{
return head.getVersion();
}
else
{
return null;
}
}
/**
* Append a text image of the archive to the given buffer using
* the given token as line separator.
* @param s where to append the image.
* @param EOL the line separator.
*/
public void toString(StringBuffer s, String EOL)
{
String EOI = ";" + EOL;
String NLT = EOL + "\t";
s.append("head");
if (head != null)
{
s.append("\t");
head.getVersion().toString(s);
}
s.append(EOI);
if (branch != null)
{
s.append("branch\t");
s.append(branch.toString());
s.append(EOI);
}
s.append("access");
for (Iterator i = users.iterator(); i.hasNext();)
{
s.append(EOL);
s.append("\t");
s.append(i.next());
}
s.append(EOI);
s.append("symbols");
for (Iterator i = symbols.entrySet().iterator(); i.hasNext();)
{
Map.Entry e = (Map.Entry) i.next();
s.append(NLT);
s.append(e.getKey().toString());
s.append(":");
s.append(e.getValue().toString());
}
s.append(EOI);
s.append("locks");
for (Iterator i = locked.iterator(); i.hasNext();)
{
String locker = ((Node) i.next()).getLocker();
s.append(NLT);
s.append(locker);
}
if (strictLocking)
{
s.append("; strict");
}
s.append(EOI);
if (comment != null)
{
s.append("comment\t");
s.append(Archive.quoteString(comment));
s.append(EOI);
}
if (expand != null)
{
s.append("expand\t");
s.append(Archive.quoteString(expand));
s.append(EOI);
}
if (phrases != null)
{
phrases.toString(s, EOL);
}
s.append(EOL);
for (Iterator i = nodes.values().iterator(); i.hasNext();)
{
Node n = (Node) i.next();
if (!n.getVersion().isGhost() && n.getText() != null)
{
n.toString(s, EOL);
}
}
s.append(EOL + EOL);
s.append("desc");
s.append(EOL);
s.append(quoteString(desc));
s.append(EOL);
Node n = head;
while (n != null)
{
n.toText(s, EOL);
n = n.getRCSNext();
}
}
/**
* Quote a string.
* RCS strings are quoted using @. Any @ in the original
* string is doubled to @@.
* @param s the string to quote.
* @return The string quoted in RCS style.
*/
static public String quoteString(String s)
{
//!!! use org.apache.commons.jrcs.RegExp here !!!
StringBuffer result = new StringBuffer(s);
for (int i = 0; i < s.length(); i++)
{
if (result.charAt(i) == '@')
{
result.insert(i++, '@');
}
}
result.insert(0, '@');
result.append('@');
return new String(result);
}
/**
* Unquote a string quoted in RCS style.
* @param s the quoted string.
* @return s the string unquoted.
*/
static public String unquoteString(String s)
{
return unquoteString(s, true);
}
/**
* Unquote a string quoted in RCS style.
* @param s the quoted string.
* @param removeExtremes Determines if the enclosing @ quotes
* should be removed.
* @return s the string unquoted.
*/
static public String unquoteString(String s, boolean removeExtremes)
{
//!!! use org.apache.commons.jrcs.RegExp here !!!
//!!! always ignore extremes. Check they are @'s, though.
StringBuffer result = new StringBuffer();
int start = 0;
int end = s.length();
if (removeExtremes)
{
start += 1;
end -= 1;
}
for (int i = start; i < end; i++)
{
char c = s.charAt(i);
result.append(c);
if (c == '@')
{
i++;
}
}
return new String(result);
}
/**
* Get the text belonging to the head revision.
* @return The text of the head revision
* @throws NodeNotFoundException if the revision could not be found.
* @throws InvalidFileFormatException if any of the deltas cannot be parsed.
* @throws PatchFailedException if any of the deltas could not be applied
*/
public Object[] getRevision()
throws InvalidFileFormatException,
PatchFailedException,
NodeNotFoundException
{
return getRevision(false);
}
/**
* Get the text belonging to the head revision.
* Set annotate to true to have the lines be annotated with the
* number of the revision in which they were added or changed.
* @param annotate set to true to have the text be annotated
* @return The text of the head revision
* @throws NodeNotFoundException if the revision could not be found.
* @throws InvalidFileFormatException if any of the deltas cannot be parsed.
* @throws PatchFailedException if any of the deltas could not be applied
* to produce a new revision.
*/
public Object[] getRevision(boolean annotate)
throws InvalidFileFormatException,
PatchFailedException,
NodeNotFoundException
{
if (branch != null)
{
return getRevision(branch);
}
else if (head != null)
{
return getRevision(head.getVersion());
}
else
{
throw new IllegalStateException("no head node");
}
}
/**
* Get the text belonging to the revision identified by the
* given version number.
* Partial version numbers are OK.
* @param vernum the version number.
* @return The text of the revision if found.
* @throws InvalidVersionNumberException if the version number cannot be parsed.
* @throws NodeNotFoundException if the revision could not be found.
* @throws InvalidFileFormatException if any of the deltas cannot be parsed.
* @throws PatchFailedException if any of the deltas could not be applied
*/
public Object[] getRevision(String vernum)
throws InvalidFileFormatException,
PatchFailedException,
InvalidVersionNumberException,
NodeNotFoundException
{
return getRevision(vernum, false);
}
/**
* Get the text belonging to the revision identified by the
* given version number.
* Partial version numbers are OK.
* Set annotate to true to have the lines be annotated with the
* number of the revision in which they were added or changed.
* @param vernum the version number.
* @param annotate set to true to have the text be annotated
* @return The text of the revision if found.
* @throws InvalidVersionNumberException if the version number cannot be parsed.
* @throws NodeNotFoundException if the revision could not be found.
* @throws InvalidFileFormatException if any of the deltas cannot be parsed.
* @throws PatchFailedException if any of the deltas could not be applied
*/
public Object[] getRevision(String vernum, boolean annotate)
throws InvalidVersionNumberException,
NodeNotFoundException,
InvalidFileFormatException,
PatchFailedException
{
return getRevision(new Version(vernum), annotate);
}
/**
* Get the text belonging to the revision identified by the
* given version number.
* Partial version numbers are OK.
* @param vernum the version number.
* @return The text of the revision if found.
* @throws NodeNotFoundException if the revision could not be found.
* @throws InvalidFileFormatException if any of the deltas cannot be parsed.
* @throws PatchFailedException if any of the deltas could not be applied
*/
public Object[] getRevision(Version vernum)
throws InvalidFileFormatException,
PatchFailedException,
NodeNotFoundException
{
return getRevision(vernum, false);
}
/**
* Get the text belonging to the revision identified by the
* given version number.
* Partial version numbers are OK.
* Set annotate to true to have the lines be annotated with the
* number of the revision in which they were added or changed.
* @param vernum the version number.
* @param annotate set to true to have the text be annotated
* @return The text of the revision if found.
* @throws NodeNotFoundException if the revision could not be found.
* @throws InvalidFileFormatException if any of the deltas cannot be parsed.
* @throws PatchFailedException if any of the deltas could not be applied
*/
public Object[] getRevision(Version vernum, boolean annotate)
throws InvalidFileFormatException,
PatchFailedException,
NodeNotFoundException
{
Path path = getRevisionPath(vernum);
if (path == null)
{
throw new NodeNotFoundException(vernum);
}
Lines lines = new Lines();
Node revisionFound = path.last();
path.patch(lines, annotate);
return doKeywords(lines.toArray(), revisionFound);
}
/**
* Add the given revision to the active branch on the archive.
* @param text the text of the revision.
* @param log the log: a short note explaining what the revision is.
* @return The version number assigned to the revision.
*/
public Version addRevision(Object[] text, String log)
throws InvalidFileFormatException,
DiffException,
InvalidVersionNumberException,
NodeNotFoundException
{
if (branch != null)
{
return addRevision(text, branch, log);
}
else
{
return addRevision(text, head.getVersion().next(), log);
}
}
/**
* Add the given revision to the the archive using the given version
* number.
* The version number may be partial. If so, the rules used by RCS/CVS
* are used to decide which branch the revision should be added to. A
* new branch may be created if required.
* @param text the text of the revision.
* @param vernum is the version number wanted, or, if partial, identifies
* the target branch.
* @param log the log: a short note explaining what the revision is.
* @return The version number assigned to the revision.
*/
public Version addRevision(Object[] text, String vernum, String log)
throws InvalidFileFormatException,
DiffException,
InvalidVersionNumberException,
NodeNotFoundException
{
return addRevision(text, new Version(vernum), log);
}
/**
* Add the given revision to the the archive using the given version
* number.
* The version number may be partial. If so, the rules used by RCS/CVS
* are used to decide which branch the revision should be added to. A
* new branch may be created if required.
* @param text the text of the revision.
* @param vernum is the version number wanted, or, if partial, identifies
* the target branch.
* @param log the log: a short note explaining what the revision is.
* @return The version number assigned to the revision.
*/
public Version addRevision(Object[] text, Version vernum, String log)
throws InvalidFileFormatException,
DiffException,
NodeNotFoundException,
InvalidVersionNumberException
{
if (head == null)
{
throw new IllegalStateException("no head node");
}
Path path = head.pathTo(vernum, true);
Node target = path.last();
if (vernum.size() < target.getVersion().size())
{
vernum = target.nextVersion();
}
else if (!vernum.isGreaterThan(target.getVersion()))
{
throw new InvalidVersionNumberException(vernum + " revision must be higher than " + target.getVersion());
}
else if (vernum.odd())
{
if (vernum.last() == 0)
{
vernum = target.newBranchVersion();
}
else
{
vernum = vernum.newBranch(1);
}
}
else if (vernum.last() == 0)
{
vernum = vernum.next();
}
boolean headAdd = (target == head && !vernum.isBranch());
text = removeKeywords(text);
String deltaText;
if (headAdd)
{
deltaText = Diff.diff(text, head.getText()).toRCSString(RCS_NEWLINE);
}
else
{
Object[] oldText = path.patch().toArray();
deltaText = Diff.diff(oldText, text).toRCSString(RCS_NEWLINE);
}
if (deltaText.length() == 0)
{
return null;
} // no changes, no new version
Node newNode = null;
if (headAdd)
{
newNode = newNode(vernum, head);
newNode.setText(text);
head.setText(deltaText);
head = (TrunkNode) newNode;
}
else
{ // adding a branch node
newNode = newNode(vernum);
newNode.setText(deltaText);
if (vernum.size() > target.getVersion().size())
{
target.addBranch((BranchNode) newNode);
}
else
{
target.setRCSNext(newNode);
}
}
newNode.setLog(log);
return newNode.getVersion();
}
/**
* Returns the given text with values added to CVS-style keywords.
* @param text the text on which substitutions will be applied.
* @param rev a node that identifies the revision to which the
* given text belongs.
* @return the text with substitutions performed.
*/
public Object[] doKeywords(Object[] text, Node rev)
throws PatchFailedException
{
//!!! this is used specifically for the way
//!!! in which the keyword replacer works. Should be moved there.
//!!! Write a Format.format(Object[], Node rev) instead.
Object[] revisionInfo = new Object[]{
filename,
new File(filename).getName(),
rev.getVersion().toString(),
rev.getDate(),
rev.getAuthor(),
rev.getState(),
rev.getLocker()
};
Object[] result = new Object[text.length];
for (int i = 0; i < text.length; i++)
{
result[i] = FORMATTER.update(text[i].toString(), revisionInfo);
}
return result;
}
/**
* Returns the given text removing the values of any CVS-style
* keywords.
* @param text the text on which substitutions will be applied.
* @return the text with substitutions performed.
*/
protected static Object[] removeKeywords(Object[] text)
throws PatchFailedException
{
Object[] result = new Object[text.length];
for (int i = 0; i < text.length; i++)
{
result[i] = FORMATTER.reset(text[i].toString());
}
return result;
}
/**
* Return the list of nodes between the head revision and
* the root revision.
*/
public Node[] changeLog()
{
return changeLog(head.version);
}
/**
* Return the list of nodes between the the given revision
* and the root revision.
* @param latest the version of the last revision in the log.
*/
public Node[] changeLog(Version latest)
{
return changeLog(latest, head.root().version);
}
/**
* Return the list of nodes between the the given two revisions.
* @param latest the version of the last revision in the log.
* @param earliest the version of the first revision in the log.
*/
public Node[] changeLog(Version latest, Version earliest)
{
Node last = findNode(latest);
if (last == null)
{
throw new NodeNotFoundException(latest.toString());
}
Node first = findNode(earliest);
if (first == null)
{
throw new NodeNotFoundException(earliest.toString());
}
List result = new LinkedList();
Node node = last;
while (node != null)
{
result.add(0, node);
if (node == first)
{
break;
}
node = node.parent;
}
if (node == null)
{
throw new NodeNotFoundException(earliest.toString());
}
return (Node[]) result.toArray(new Node[result.size()]);
}
/**
* Returns the description associated with the archive.
* @return the description
*/
public String getDesc()
{
return desc;
}
/** Returns the log message associated with the given revision.
* @param version - the version to get the log message for
* @return the log message for the version.
* @exception - if the version does not exist for the archive.
*/
public String getLog(Version version)
throws NodeNotFoundException
{
Node node = this.findNode(version);
if (node == null)
{
throw new NodeNotFoundException("There's no version " + version);
}
return node.getLog();
}
/** Returns the log message associated with the given revision.
* @param version - the version to get the log message for
* @return the log message for the version.
* @exception - if the version does not exist for the archive.
*/
public String getLog(String vernum)
throws InvalidVersionNumberException, NodeNotFoundException
{
return getLog(new Version(vernum));
}
}