| /******************************************************************************* |
| * Copyright (c) 2000, 2011 IBM Corporation and others. |
| * |
| * This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License 2.0 |
| * which accompanies this distribution, and is available at |
| * https://www.eclipse.org/legal/epl-2.0/ |
| * |
| * SPDX-License-Identifier: EPL-2.0 |
| * |
| * Contributors: |
| * IBM Corporation - initial API and implementation |
| * Brock Janiczak <brockj@tpg.com.au> - Bug 179977 CVS log command doesn't scale well with lots of tags and versions |
| * Brock Janiczak <brockj@tpg.com.au> - Bug 194396 Reduce retained memory usage of LogEntry objects |
| * Olexiy Buyanskyy <olexiyb@gmail.com> - Bug 76386 - [History View] CVS Resource History shows revisions from all branches |
| *******************************************************************************/ |
| package org.eclipse.team.internal.ccvs.core.client.listeners; |
| |
| import java.text.*; |
| import java.util.*; |
| |
| import org.eclipse.core.runtime.IProgressMonitor; |
| import org.eclipse.core.runtime.IStatus; |
| import org.eclipse.osgi.util.NLS; |
| import org.eclipse.team.internal.ccvs.core.*; |
| import org.eclipse.team.internal.ccvs.core.client.CommandOutputListener; |
| import org.eclipse.team.internal.ccvs.core.resources.RemoteFile; |
| import org.eclipse.team.internal.ccvs.core.syncinfo.ResourceSyncInfo; |
| import org.eclipse.team.internal.ccvs.core.util.Util; |
| |
| /** |
| * Log listener that parses the log entries returned from the |
| * server but delegates the handling of the entries to a subclass. |
| */ |
| public class LogListener extends CommandOutputListener { |
| |
| /* |
| * A new format for log dates was introduced in 1.12.9 |
| */ |
| private static final String LOG_TIMESTAMP_FORMAT_OLD= "yyyy/MM/dd HH:mm:ss zzz";//$NON-NLS-1$ |
| private static final String LOG_TIMESTAMP_FORMAT= "yyyy-MM-dd HH:mm:ss zzz";//$NON-NLS-1$ |
| private static final Locale LOG_TIMESTAMP_LOCALE= Locale.US; |
| private final DateFormat LOG_DATE_FORMATTER_OLD = new SimpleDateFormat(LOG_TIMESTAMP_FORMAT_OLD, LOG_TIMESTAMP_LOCALE); |
| private final DateFormat LOG_DATE_FORMATTER = new SimpleDateFormat(LOG_TIMESTAMP_FORMAT, LOG_TIMESTAMP_LOCALE); |
| |
| // Server message prefix used for error detection |
| private static final String NOTHING_KNOWN_ABOUT = "nothing known about "; //$NON-NLS-1$ |
| |
| // States of log accumulation. |
| private final int DONE = 4; |
| private final int COMMENT = 3; |
| private final int REVISION = 2; |
| private final int SYMBOLIC_NAMES = 1; |
| private final int BEGIN = 0; |
| |
| //Tag used for accumulating all of a branch's revision info |
| public final static String BRANCH_REVISION = "branchRevision"; //$NON-NLS-1$ |
| |
| private static final CVSTag[] NO_TAGS = new CVSTag[0]; |
| private static final String[] NO_VERSIONS = new String[0]; |
| |
| // Instance variables for accumulating Log information |
| private RemoteFile currentFile; |
| private int state = BEGIN; |
| private StringBuffer comment; |
| private String fileState; |
| private String revision; |
| private String author; |
| private Date creationDate; |
| private List versions = new ArrayList(); |
| private Map internedStrings = new HashMap(); |
| private final ILogEntryListener listener; |
| |
| /** |
| * Create a log listener for receiving entries for one or more files. |
| */ |
| public LogListener(ILogEntryListener listener) { |
| this.listener = listener; |
| } |
| |
| public LogListener(RemoteFile file, ILogEntryListener listener) { |
| this(listener); |
| this.currentFile = file; |
| } |
| |
| private String getRelativeFilePath(ICVSRepositoryLocation location, String fileName) { |
| if (fileName.endsWith(",v")) { //$NON-NLS-1$ |
| fileName = fileName.substring(0, fileName.length() - 2); |
| } |
| fileName = Util.removeAtticSegment(fileName); |
| String rootDirectory = location.getRootDirectory(); |
| if (fileName.startsWith(rootDirectory)) { |
| try { |
| fileName = Util.getRelativePath(rootDirectory, fileName); |
| } catch (CVSException e) { |
| CVSProviderPlugin.log(e); |
| return null; |
| } |
| } |
| return fileName; |
| } |
| |
| public IStatus errorLine(String line, ICVSRepositoryLocation location, ICVSFolder commandRoot, IProgressMonitor monitor) { |
| String serverMessage = getServerMessage(line, location); |
| if (serverMessage != null) { |
| // look for the following condition |
| // E cvs server: nothing known about fileName |
| if (serverMessage.startsWith(NOTHING_KNOWN_ABOUT)) { |
| return new CVSStatus(IStatus.ERROR, CVSStatus.DOES_NOT_EXIST, line, commandRoot); |
| } |
| } |
| return OK; |
| } |
| |
| public IStatus messageLine(String line, ICVSRepositoryLocation location, ICVSFolder commandRoot, IProgressMonitor monitor) { |
| // Fields we will find in the log for a file |
| // keys = String (tag name), values = String (tag revision number) */ |
| switch (state) { |
| case BEGIN: |
| if (line.startsWith("RCS file: ")) { //$NON-NLS-1$ |
| // We are starting to recieve the log for a file |
| String fileName = getRelativeFilePath(location, line.substring(10).trim()); |
| if (fileName == null) { |
| currentFile = null; |
| handleInvalidFileName(location, fileName); |
| } else { |
| if (currentFile == null || !currentFile.getRepositoryRelativePath().equals(fileName)) { |
| // We are starting another file |
| beginFile(location, fileName); |
| } |
| } |
| } else if (line.startsWith("symbolic names:")) { //$NON-NLS-1$ |
| state = SYMBOLIC_NAMES; |
| } else if (line.startsWith("revision ")) { //$NON-NLS-1$ |
| // if the revision has been locked, remove the "locked by" suffix |
| revision = line.substring(9).replaceFirst(ResourceSyncInfo.LOCKEDBY_REGEX, ""); //$NON-NLS-1$ |
| revision = internAndCopyString(revision); |
| state = REVISION; |
| } else if (line.startsWith("total revisions:")){ //$NON-NLS-1$ |
| //if there are no current revision selected and this is a branch then we are in the |
| //case where there have been no changes made on the branch since the initial branching |
| //and we need to get the revision that the branch was made from |
| int indexOfSelectedRevisions = line.lastIndexOf("selected revisions: "); //$NON-NLS-1$ |
| //20 for length of "selected revisions: " |
| String selectedRevisions = line.substring(indexOfSelectedRevisions + 20).trim(); |
| if (selectedRevisions.equals("0")){ //$NON-NLS-1$ |
| //ok put into comment state to await ======= and add info to log |
| state = COMMENT; |
| revision = BRANCH_REVISION; |
| comment = new StringBuffer(); |
| } |
| } |
| break; |
| case SYMBOLIC_NAMES: |
| if (line.startsWith("keyword substitution:")) { //$NON-NLS-1$ |
| state = BEGIN; |
| } else { |
| int firstColon = line.indexOf(':'); |
| String tagName = internAndCopyString(line.substring(1, firstColon)); |
| String tagRevision = internAndCopyString(line.substring(firstColon + 2)); |
| versions.add(new VersionInfo(tagRevision, tagName)); |
| } |
| break; |
| case REVISION: |
| // date: 2000/06/19 04:56:21; author: somebody; state: Exp; lines: +114 -45 |
| // get the creation date |
| int endOfDateIndex = line.indexOf(';', 6); |
| creationDate = convertFromLogTime(line.substring(6, endOfDateIndex) + " GMT"); //$NON-NLS-1$ |
| |
| // get the author name |
| int endOfAuthorIndex = line.indexOf(';', endOfDateIndex + 1); |
| author = internAndCopyString(line.substring(endOfDateIndex + 11, endOfAuthorIndex)); |
| |
| // get the file state (because this revision might be "dead") |
| int endOfStateIndex = line.indexOf(';', endOfAuthorIndex + 1) < 0 ? line.length() : line.indexOf(';', endOfAuthorIndex + 1); |
| fileState = internAndCopyString(line.substring(endOfAuthorIndex + 10, endOfStateIndex)); |
| comment = new StringBuffer(); |
| state = COMMENT; |
| break; |
| case COMMENT: |
| // skip next line (info about branches) if it exists, if not then it is a comment line. |
| if (line.startsWith("branches:")) break; //$NON-NLS-1$ |
| if (line.equals("=============================================================================") //$NON-NLS-1$ |
| || line.equals("----------------------------")) { //$NON-NLS-1$ |
| state = DONE; |
| break; |
| } |
| //check for null if we are in the waiting to finish case (brought on by branches) |
| if (comment == null) |
| break; |
| |
| if (comment.length() != 0) comment.append('\n'); |
| comment.append(line); |
| break; |
| } |
| if (state == DONE) { |
| // we are only interested in tag names for this revision, remove all others. |
| List thisRevisionTags = versions.isEmpty() ? Collections.EMPTY_LIST : new ArrayList(3); |
| List thisRevisionBranches = new ArrayList(1); |
| //a parallel lists for revision tags (used only for branches with no commits on them) |
| List revisionVersions = versions.isEmpty() ? Collections.EMPTY_LIST : new ArrayList(3); |
| String branchRevision = this.getBranchRevision(revision); |
| for (Iterator i = versions.iterator(); i.hasNext();) { |
| VersionInfo version = (VersionInfo) i.next(); |
| String tagName = version.getTagName(); |
| String tagRevision = version.getTagRevision(); |
| String tagBranchRevision = version.getBranchRevision(); |
| int type = version.isBranch() ? CVSTag.BRANCH : CVSTag.VERSION; |
| if ( branchRevision.equals(tagBranchRevision) || |
| (version.isBranch() && revision.equals(tagRevision))) { |
| CVSTag cvsTag = new CVSTag(tagName, tagBranchRevision, type); |
| thisRevisionBranches.add(cvsTag); |
| } |
| |
| if (tagRevision.equals(revision) || |
| revision.equals(BRANCH_REVISION)) { |
| CVSTag cvsTag = new CVSTag(tagName, tagBranchRevision, type); |
| thisRevisionTags.add(cvsTag); |
| if (revision.equals(BRANCH_REVISION)){ |
| //also record the tag revision |
| revisionVersions.add(tagRevision); |
| } |
| } |
| } |
| |
| if (branchRevision.equals(CVSTag.HEAD_REVISION)) { |
| CVSTag tag = new CVSTag(CVSTag.HEAD_BRANCH, CVSTag.HEAD_REVISION, CVSTag.HEAD); |
| thisRevisionBranches.add(tag); |
| } else { |
| if ( thisRevisionBranches.size() == 0) { |
| CVSTag cvsTag = new CVSTag(CVSTag.UNKNOWN_BRANCH, branchRevision, CVSTag.BRANCH); |
| thisRevisionBranches.add(cvsTag); |
| } |
| } |
| if (currentFile != null) { |
| LogEntry entry = new LogEntry(currentFile, revision, author, creationDate, |
| internString(comment.toString()), fileState, |
| !thisRevisionTags.isEmpty() ? (CVSTag[]) thisRevisionTags.toArray(new CVSTag[thisRevisionTags.size()]) :NO_TAGS, |
| !thisRevisionBranches.isEmpty() ? (CVSTag[]) thisRevisionBranches.toArray(new CVSTag[thisRevisionBranches.size()]) :NO_TAGS, |
| !revisionVersions.isEmpty() ? (String[]) revisionVersions.toArray(new String[revisionVersions.size()]) : NO_VERSIONS); |
| addEntry(entry); |
| } |
| state = BEGIN; |
| } |
| return OK; |
| } |
| |
| /** |
| * Convert revision number to branch number. |
| * |
| * <table border="1"> |
| * <tr><th>revision</th><th>branch</th><th>comment</th></tr> |
| * <tr><td>1.1.2.1</td><td>1.1.0.2</td><td>regular branch</td></tr> |
| * <tr><td>1.1.4.1</td><td>1.1.0.4</td><td>regular branch</td></tr> |
| * <tr><td>1.1.1.2</td><td>1.1.1</td><td>vendor branch</td></tr> |
| * <tr><td>1.1.2.1.2.3</td><td>1.1.2.1.0.2</td><td>branch created from another branch</td></tr> |
| * </table> |
| * |
| * @param revision revision number |
| * @return branch number |
| * |
| */ |
| private String getBranchRevision(String revision) { |
| if (revision.length() == 0 || revision.lastIndexOf(".") == -1) //$NON-NLS-1$ |
| throw new IllegalArgumentException( |
| "Revision malformed: " + revision); //$NON-NLS-1$ |
| String branchNumber = revision.substring(0, revision.lastIndexOf(".")); //$NON-NLS-1$ |
| if (branchNumber.lastIndexOf(".") == -1 || branchNumber.equals(CVSTag.VENDOR_REVISION)) { //$NON-NLS-1$ |
| return branchNumber; |
| } |
| String branchPrefix = branchNumber.substring(0, |
| branchNumber.lastIndexOf(".")); //$NON-NLS-1$ |
| branchPrefix += ".0"; //$NON-NLS-1$ |
| branchPrefix += branchNumber.substring(branchNumber.lastIndexOf(".")); //$NON-NLS-1$ |
| return branchPrefix; |
| } |
| |
| protected void beginFile(ICVSRepositoryLocation location, String fileName) { |
| currentFile = RemoteFile.create(fileName, location); |
| versions.clear(); |
| } |
| |
| protected void addEntry(LogEntry entry) { |
| listener.handleLogEntryReceived(entry); |
| } |
| |
| protected void handleInvalidFileName(ICVSRepositoryLocation location, String badFilePath) { |
| CVSProviderPlugin.log(IStatus.WARNING, "Invalid file path '" + badFilePath + "' received from " + location.toString(), null); //$NON-NLS-1$ //$NON-NLS-2$ |
| } |
| |
| /** |
| * Converts a time stamp as sent from a cvs server for a "log" command into a |
| * <code>Date</code>. |
| */ |
| private Date convertFromLogTime(String modTime) { |
| DateFormat format = LOG_DATE_FORMATTER; |
| // Compatibility for older cvs version (pre 1.12.9) |
| if (modTime.length() > 4 && modTime.charAt(4) == '/') |
| format = LOG_DATE_FORMATTER_OLD; |
| |
| try { |
| return format.parse(modTime); |
| } catch (ParseException e) { |
| // fallback is to return null |
| return null; |
| } |
| } |
| |
| private String internAndCopyString(String string) { |
| String internedString = (String) internedStrings.get(string); |
| if (internedString == null) { |
| internedString = new String(string); |
| internedStrings.put(internedString, internedString); |
| } |
| return internedString; |
| } |
| |
| private String internString(String string) { |
| String internedString = (String) internedStrings.get(string); |
| if (internedString == null) { |
| internedString = string; |
| internedStrings.put(internedString, internedString); |
| } |
| return internedString; |
| } |
| |
| private static class VersionInfo { |
| private final boolean isBranch; |
| private String tagRevision; |
| private String branchRevision; |
| private final String tagName; |
| |
| public VersionInfo(String version, String tagName) { |
| this.tagName = tagName; |
| this.isBranch = isBranchTag(version); |
| tagRevision = version; |
| if (isBranch) { |
| int lastDot = version.lastIndexOf('.'); |
| if (lastDot == -1) { |
| CVSProviderPlugin.log(IStatus.ERROR, |
| NLS.bind(CVSMessages.LogListener_invalidRevisionFormat, new String[] { tagName, version }), null); |
| } else { |
| if (version.charAt(lastDot - 1) == '0' && version.charAt(lastDot - 2) == '.') { |
| lastDot = lastDot - 2; |
| } |
| this.branchRevision = version; |
| tagRevision = version.substring(0, lastDot); |
| } |
| } |
| } |
| |
| public String getTagName() { |
| return this.tagName; |
| } |
| |
| public String getTagRevision() { |
| return this.tagRevision; |
| } |
| |
| public boolean isBranch() { |
| return isBranch; |
| } |
| |
| /** branch tags have odd number of segments or have |
| * an even number with a zero as the second last segment |
| * e.g: 1.1.1, 1.26.0.2 are branch revision numbers */ |
| private boolean isBranchTag(String tagName) { |
| // First check if we have an odd number of segments (i.e. even number of dots) |
| int numberOfDots = 0; |
| int lastDot = 0; |
| for (int i = 0; i < tagName.length(); i++) { |
| if (tagName.charAt(i) == '.') { |
| numberOfDots++; |
| lastDot = i; |
| } |
| } |
| if ((numberOfDots % 2) == 0) return true; |
| if (numberOfDots == 1) return false; |
| |
| // If not, check if the second lat segment is a zero |
| if (tagName.charAt(lastDot - 1) == '0' && tagName.charAt(lastDot - 2) == '.') return true; |
| return false; |
| } |
| |
| public String getBranchRevision() { |
| return branchRevision; |
| } |
| } |
| } |