/* Copyright 2004-2014 Jim Voris * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.qumasoft.server; import com.qumasoft.qvcslib.ArchiveAttributes; import com.qumasoft.qvcslib.CompareFilesEditHeader; import com.qumasoft.qvcslib.CompareFilesEditInformation; import com.qumasoft.qvcslib.CompareFilesWithApacheDiff; import com.qumasoft.qvcslib.Compressor; import com.qumasoft.qvcslib.LabelInfo; import com.qumasoft.qvcslib.LogFileHeaderInfo; import com.qumasoft.qvcslib.QVCSException; import com.qumasoft.qvcslib.QumaAssert; import com.qumasoft.qvcslib.RevisionDescriptor; import com.qumasoft.qvcslib.RevisionHeader; import com.qumasoft.qvcslib.Utility; import com.qumasoft.qvcslib.ZlibCompressor; import com.qumasoft.qvcslib.commandargs.CheckInCommandArgs; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.RandomAccessFile; import java.util.Date; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import java.util.logging.Level; import java.util.logging.Logger; /** * Checkin file revision. * @author Jim Voris */ class LogFileOperationCheckIn extends AbstractLogFileOperation { // Create our logger object private static final Logger LOGGER = Logger.getLogger("com.qumasoft.server"); private final CompareFilesWithApacheDiff compareFilesOperator; private final CheckInCommandArgs commandLineArgs; private final boolean lockFlag; private final boolean labelFlag; private final boolean reuseLabelFlag; private final boolean floatLabelFlag; private final boolean forceBranchFlag; private final boolean ignoreLocksToEnableBranchCheckInFlag; private final String userName; private String lockedRevisionString; private final String filename; private final String checkInComment; private final String labelString; private RandomAccessFile newArchiveStream; private RandomAccessFile oldArchiveStream; private int creatorIndex = -1; private Date checkInDate; /** * Creates a new instance of LogFileOperationCheckIn. * @param a arguments for the operation. a[0] is the logfileImpl; a[1] is the fetch-to filename; a[2] is the command arguments object. * @param flag ignore locks to enable branch check in flag. */ public LogFileOperationCheckIn(Object[] a, boolean flag) { super(a, (LogFileImpl) a[0]); this.ignoreLocksToEnableBranchCheckInFlag = flag; this.filename = (String) a[1]; this.commandLineArgs = (CheckInCommandArgs) a[2]; this.userName = commandLineArgs.getUserName(); this.lockedRevisionString = commandLineArgs.getLockedRevisionString(); this.checkInComment = commandLineArgs.getCheckInComment(); this.lockFlag = commandLineArgs.getLockFlag(); this.labelFlag = commandLineArgs.getApplyLabelFlag(); this.reuseLabelFlag = commandLineArgs.getReuseLabelFlag(); this.floatLabelFlag = commandLineArgs.getFloatLabelFlag(); this.forceBranchFlag = commandLineArgs.getForceBranchFlag(); this.labelString = commandLineArgs.getLabel(); this.compareFilesOperator = new CompareFilesWithApacheDiff(); } @Override public boolean execute() throws QVCSException { boolean retVal = false; boolean unlockRequiredFlag = false; // Figure out the checkin date if (commandLineArgs.getCheckInTimestamp() == null) { // The checkin date is now. checkInDate = new Date(); } else { checkInDate = commandLineArgs.getCheckInTimestamp(); } // Make sure user is on the access list. getLogFileImpl().makeSureIsOnAccessList(userName); // Figure out who is doing this operation. creatorIndex = getLogFileImpl().getModifierList().userToIndex(userName); RevisionHeader revInfo; // If we check locks, then check that this user has the file locked... if (getLogFileImpl().getLogFileHeaderInfo().getLogFileHeader().attributes().getIsCheckLock() && !ignoreLocksToEnableBranchCheckInFlag) { AtomicInteger revisionIndex = new AtomicInteger(); AtomicReference<String> revisionString = new AtomicReference<>(lockedRevisionString); // Make sure the revision is locked. if (!getLogFileImpl().isRevisionLocked(revisionString, revisionIndex)) { throw new QVCSException("Revision " + revisionString.get() + " is not locked for " + userName); } // Make sure the user has locked this if (!getLogFileImpl().isLockedByUser(userName)) { throw new QVCSException("User '" + userName + "' does not hold a lock on " + getLogFileImpl().getShortWorkfileName()); } revInfo = getLogFileImpl().getRevisionHeader(revisionIndex.get()); // If working on the default revision, this gets filled in for us. lockedRevisionString = revisionString.get(); } else { if (lockedRevisionString == null) { // TODO -- This will change if/when I support concurrent development. // Always check in to the tip revision of the default branch. revInfo = getLogFileImpl().getDefaultRevisionHeader(); lockedRevisionString = revInfo.getRevisionString(); } else { // (For rename and move operations that need to be able to checkin // new tip revisions on all branches). revInfo = getLogFileImpl().getRevisionHeader(getLogFileImpl().getRevisionInformation().getRevisionIndex(lockedRevisionString)); } } /* * 1. Get the locked revision into a temp file. */ String tempFileNameForExistingRevision = getRevisionToTempfile(userName, lockedRevisionString); File tempFileNameForExistingRevisionFile = new File(tempFileNameForExistingRevision); // Make sure the temp file is gone. getLogFileImpl().getTempFile().delete(); File tempFileForCompareResults = null; try { newArchiveStream = new java.io.RandomAccessFile(getLogFileImpl().getTempFile(), "rw"); oldArchiveStream = new java.io.RandomAccessFile(getLogFileImpl().getFile(), "r"); String tempFileNameForCompareResults; // If we're checking in a new tip revision on the TRUNK. if ((revInfo.getDepth() == 0) && revInfo.isTip() && !forceBranchFlag) { assert (revInfo.getDepth() == 0); LOGGER.log(Level.FINE, "Adding new TRUNK tip revision"); /* * 2. Compare that temp file with the filename passed in, writing the delta to a temp file. */ tempFileNameForCompareResults = compareFileToTempfile(tempFileNameForExistingRevision, true); tempFileForCompareResults = new File(tempFileNameForCompareResults); // Store a new revision. if (!compareFilesOperator.isEqual() || commandLineArgs.getCreateNewRevisionIfEqual()) { writeNewLogfileCreatingNewTrunkTip(revInfo, tempFileForCompareResults); updateDigestManager(); } else { // Nothing changed. Just unlock the revision. if (labelFlag) { // The user is requesting that we apply a label. writeNewLogfileThatOnlyChangesTheHeader(revInfo); } else { unlockRequiredFlag = true; } } } else if ((revInfo.getDepth() == 0) && revInfo.isTip() && forceBranchFlag) { // If we are checking in a new branch right off the tip of the TRUNK. QumaAssert.isTrue(revInfo.getDepth() == 0); LOGGER.log(Level.FINE, "Forcing a new branch off of the tip of the TRUNK."); // Compare that temp file with the filename passed in, writing the delta to a temp file. tempFileNameForCompareResults = compareFileToTempfile(tempFileNameForExistingRevision, false); tempFileForCompareResults = new File(tempFileNameForCompareResults); unlockRequiredFlag = createBranch(revInfo, tempFileForCompareResults); } else if (revInfo.isTip() && !forceBranchFlag) { // If we're checking in a new tip revision on a branch. QumaAssert.isTrue(revInfo.getDepth() > 0); LOGGER.log(Level.FINE, "Adding new tip revision to branch " + revInfo.getRevisionString()); /* * 2. Compare that temp file with the filename passed in, writing the delta to a temp file. */ tempFileNameForCompareResults = compareFileToTempfile(tempFileNameForExistingRevision, false); tempFileForCompareResults = new File(tempFileNameForCompareResults); // Store a new revision. if (!compareFilesOperator.isEqual() || commandLineArgs.getCreateNewRevisionIfEqual()) { writeNewLogfileCreatingNewBranchTip(revInfo, tempFileForCompareResults); updateDigestManager(); } else { // Nothing changed. Just unlock the revision. if (labelFlag) { // The user is requesting that we apply a label. writeNewLogfileThatOnlyChangesTheHeader(revInfo); } else { unlockRequiredFlag = true; } } } else if (revInfo.isTip() && forceBranchFlag) { // If we're forcing a new branch off the tip revision of an existing branch. QumaAssert.isTrue(revInfo.getDepth() > 0); QumaAssert.isTrue(revInfo.isTip()); LOGGER.log(Level.FINE, "Forcing a new branch off of the tip of branch " + revInfo.getRevisionString()); // Compare that temp file with the filename passed in, writing the delta to a temp file. tempFileNameForCompareResults = compareFileToTempfile(tempFileNameForExistingRevision, false); tempFileForCompareResults = new File(tempFileNameForCompareResults); unlockRequiredFlag = createBranch(revInfo, tempFileForCompareResults); } else { // TODO -- auto merge? // If we're checking in a new revision somewhere in the middle. // (we'll have to make a new branch automatically). I guess this would // be where we'd automatically merge if auto-merge was implemented. QumaAssert.isTrue(!revInfo.isTip()); LOGGER.log(Level.FINE, "Creating new branch at revision " + revInfo.getRevisionString()); // Compare that temp file with the filename passed in, writing the delta to a temp file. tempFileNameForCompareResults = compareFileToTempfile(tempFileNameForExistingRevision, false); tempFileForCompareResults = new File(tempFileNameForCompareResults); unlockRequiredFlag = createBranch(revInfo, tempFileForCompareResults); } retVal = true; } catch (QVCSException | IOException e) { LOGGER.log(Level.WARNING, "LogFileOperationCheckIn exception: " + e.toString() + ": " + e.getMessage()); LOGGER.log(Level.WARNING, Utility.expandStackTraceToString(e)); retVal = false; } finally { try { if (oldArchiveStream != null) { oldArchiveStream.close(); oldArchiveStream = null; } if (newArchiveStream != null) { newArchiveStream.close(); newArchiveStream = null; } if (tempFileForCompareResults != null) { tempFileForCompareResults.delete(); } if (tempFileNameForExistingRevisionFile.exists()) { tempFileNameForExistingRevisionFile.delete(); } } catch (IOException e) { LOGGER.log(Level.WARNING, Utility.expandStackTraceToString(e)); retVal = false; newArchiveStream = null; oldArchiveStream = null; } } // If we just need to unlock the archive, that's what we'll do. if (unlockRequiredFlag) { if (getLogFileImpl().getLogFileHeaderInfo().getLogFileHeader().attributes().getIsCheckLock()) { if (!lockFlag) { AtomicReference<String> revisionString = new AtomicReference<>(lockedRevisionString); retVal = getLogFileImpl().unlockRevision(userName, revisionString); commandLineArgs.setNewRevisionString(revisionString.get()); } else { retVal = true; commandLineArgs.setNewRevisionString(revInfo.getRevisionString()); } } else { // There is no lock checking for this archive, and it did not // change, so we're done. retVal = true; commandLineArgs.setNewRevisionString(revInfo.getRevisionString()); } } else { // Replace existing archive with new one. if (retVal) { getLogFileImpl().replaceExistingArchiveWithNewTempArchive(); } else { getLogFileImpl().getTempFile().delete(); } } return retVal; } // Return the state of the unlockRequiredFlag flag. private boolean createBranch(RevisionHeader revInfo, File tempFileForCompareResults) throws QVCSException, IOException { boolean retVal = false; // Store a new revision. if (!compareFilesOperator.isEqual() || commandLineArgs.getCreateNewRevisionIfEqual()) { writeNewLogfileCreatingNewBranch(revInfo, tempFileForCompareResults); updateDigestManager(); } else { // Nothing changed. Just unlock the revision. if (labelFlag) { // The user is requesting that we apply a label. writeNewLogfileThatOnlyChangesTheHeader(revInfo); } else { retVal = true; } } return retVal; } private void updateDigestManager() { FileInputStream inStream = null; try { File workfile = new File(filename); inStream = new FileInputStream(workfile); byte[] buffer = new byte[(int) workfile.length()]; Utility.readDataFromStream(buffer, inStream); ArchiveDigestManager.getInstance().addRevision(getLogFileImpl(), commandLineArgs.getNewRevisionString(), buffer); } catch (IOException e) { LOGGER.log(Level.WARNING, e.getClass().toString() + " " + e.getLocalizedMessage()); } finally { if (inStream != null) { try { inStream.close(); } catch (IOException e) { LOGGER.log(Level.WARNING, Utility.expandStackTraceToString(e)); } } } } /** * Compare the filename to a tempfile that contains a fetched revision from the archive. We return the name of the file that * contains the result of the compare. This routine uses the member m_CompareFilesOperator to perform the comparison. */ String compareFileToTempfile(String tempfileName, boolean isReverseDelta) throws QVCSException { File tempFileForCompareResults = null; try { tempFileForCompareResults = File.createTempFile("QVCS", ".tmp"); } catch (java.io.IOException e) { LOGGER.log(Level.WARNING, Utility.expandStackTraceToString(e)); throw new QVCSException("Failed to create QVCS temp file: " + e.getMessage()); } String tempFileNameForCompareResults = tempFileForCompareResults.getAbsolutePath(); ArchiveAttributes attributes = getLogFileImpl().getLogFileHeaderInfo().getLogFileHeader().attributes(); if (attributes.getIsBinaryfile() || !attributes.getIsComputeDelta()) { // The file is binary or we are not supposed to compute a delta for it. // So, create an edit script that just replaces the current revision // with the new revision. createNoComputeDeltaEditScript(tempfileName, tempFileNameForCompareResults, isReverseDelta); } else { // <editor-fold> String[] localArgs = new String[3]; // </editor-fold> if (isReverseDelta) { localArgs[0] = filename; localArgs[1] = tempfileName; } else { localArgs[0] = tempfileName; localArgs[1] = filename; } localArgs[2] = tempFileNameForCompareResults; LOGGER.log(Level.FINEST, "Comparing " + filename + " to " + tempfileName); if (!compareFilesOperator.execute(localArgs)) { throw new QVCSException("Failed to compare " + filename + " to file revision " + lockedRevisionString); } } return tempFileNameForCompareResults; } void writeNewLogfileCreatingNewTrunkTip(RevisionHeader parentRevInfo, File tempFileForCompareResults) throws QVCSException, IOException { // The file that we are checking in (note that it is already keyword compressed). File workfile = new File(filename); // Create the revision header for this new revision. RevisionHeader newRevisionHeader = new RevisionHeader(getLogFileImpl().getAccessList(), getLogFileImpl().getModifierList()); newRevisionHeader.setCreator(userName); newRevisionHeader.setCheckInDate(checkInDate); newRevisionHeader.setEditDate(commandLineArgs.getInputfileTimeStamp()); newRevisionHeader.setRevisionDescription(checkInComment); newRevisionHeader.setIsTip(true); if (lockFlag) { newRevisionHeader.setIsLocked(true); newRevisionHeader.setLocker(userName); } newRevisionHeader.setMajorNumber(parentRevInfo.getMajorNumber()); newRevisionHeader.setMinorNumber(1 + parentRevInfo.getMinorNumber()); Compressor compressor = new ZlibCompressor(); newRevisionHeader.setRevisionSize(getCompressedRevisionSizeForWorkfile(workfile, compressor)); newRevisionHeader.setIsCompressed(compressor.getBufferIsCompressedFlag()); newRevisionHeader.setRevisionDescriptor(new RevisionDescriptor(newRevisionHeader, parentRevInfo)); commandLineArgs.setNewRevisionString(newRevisionHeader.getRevisionString()); // Increment the number of revisions. getLogFileImpl().getLogFileHeaderInfo().getLogFileHeader().incrementRevisionCount(); if (!lockFlag) { getLogFileImpl().getLogFileHeaderInfo().getLogFileHeader().decrementLockCount(); } // TODO -- is there anything we need to do here if the default branch is not the TRUNK? // e.g. is there anything else in the archive header that might need to get updated? getLogFileImpl().getLogFileHeaderInfo().getLogFileHeader().incrementMinorNumber(); // Set the stuff that goes in the supplemental info. getLogFileImpl().getLogFileHeaderInfo().setWorkfileName(commandLineArgs.getFullWorkfileName()); getLogFileImpl().getLogFileHeaderInfo().getSupplementalHeaderInfo().setLastWorkfileSize(workfile.length()); getLogFileImpl().getLogFileHeaderInfo().getSupplementalHeaderInfo().setLastArchiveUpdateDate(checkInDate); getLogFileImpl().getLogFileHeaderInfo().getSupplementalHeaderInfo().setLastModifierIndex(getLogFileImpl().getModifierList().userToIndex(userName)); // Update the labels if we need to. if (labelFlag) { addLabelToHeader(newRevisionHeader); } // Write the header. getLogFileImpl().getLogFileHeaderInfo().write(newArchiveStream); // Write the new tip revision header newRevisionHeader.write(newArchiveStream); // Copy the (potentially compressed) workfile as the first revision. copyFromOneOpenFileToAnotherOpenFile(compressor.getCompressedStream(), newArchiveStream, newRevisionHeader.getRevisionSize()); // Figure out the number of bytes we'll need to copy from the original archive // (before we overwrite the data that allows us to figure that out). long oldArchiveSeekPosition = parentRevInfo.getRevisionDataStartPosition() + parentRevInfo.getRevisionSize(); long numberOfBytesToCopyFromSource = getLogFileImpl().getFile().length() - oldArchiveSeekPosition; // Update the 'parent' revision parentRevInfo.setIsTip(false); parentRevInfo.setIsLocked(false); Compressor deltaCompressor = new ZlibCompressor(); parentRevInfo.setRevisionSize(getCompressedRevisionSizeForDelta(tempFileForCompareResults, deltaCompressor)); parentRevInfo.setIsCompressed(deltaCompressor.getBufferIsCompressedFlag()); parentRevInfo.write(newArchiveStream); commandLineArgs.setParentRevisionString(parentRevInfo.getRevisionString()); // Write the delta to the archive copyFromOneOpenFileToAnotherOpenFile(deltaCompressor.getCompressedStream(), newArchiveStream, parentRevInfo.getRevisionSize()); // Copy the rest of the archive file... // Copy the rest of the original archive to the new archive. oldArchiveStream.seek(oldArchiveSeekPosition); copyFromOneOpenFileToAnotherOpenFile(oldArchiveStream, newArchiveStream, numberOfBytesToCopyFromSource); } void writeNewLogfileCreatingNewBranchTip(RevisionHeader parentRevInfo, File tempFileForCompareResults) throws QVCSException, IOException { // This revision will be a forward delta. // Create the revision header for this new revision. RevisionHeader newRevisionHeader = new RevisionHeader(getLogFileImpl().getAccessList(), getLogFileImpl().getModifierList()); newRevisionHeader.setCreator(userName); newRevisionHeader.setCheckInDate(checkInDate); newRevisionHeader.setEditDate(commandLineArgs.getInputfileTimeStamp()); newRevisionHeader.setRevisionDescription(checkInComment); newRevisionHeader.setParentRevisionHeader(parentRevInfo); newRevisionHeader.setIsTip(true); if (lockFlag) { newRevisionHeader.setIsLocked(true); newRevisionHeader.setLocker(userName); } newRevisionHeader.setDepth(parentRevInfo.getDepth()); Compressor deltaCompressor = new ZlibCompressor(); newRevisionHeader.setRevisionSize(getCompressedRevisionSizeForDelta(tempFileForCompareResults, deltaCompressor)); newRevisionHeader.setIsCompressed(deltaCompressor.getBufferIsCompressedFlag()); newRevisionHeader.setMajorNumber(parentRevInfo.getMajorNumber()); newRevisionHeader.setMinorNumber(1 + parentRevInfo.getMinorNumber()); newRevisionHeader.setRevisionDescriptor(new RevisionDescriptor(newRevisionHeader, parentRevInfo)); commandLineArgs.setNewRevisionString(newRevisionHeader.getRevisionString()); // Write the header, incrementing the number of revisions. // Increment the number of revisions. getLogFileImpl().getLogFileHeaderInfo().getLogFileHeader().incrementRevisionCount(); if (!lockFlag) { getLogFileImpl().getLogFileHeaderInfo().getLogFileHeader().decrementLockCount(); } // Add the label to the header if we need to if (labelFlag) { addLabelToHeader(newRevisionHeader); } // Write the header. getLogFileImpl().getLogFileHeaderInfo().write(newArchiveStream); // Write the logfile, up to the parent revision. RevisionHeader firstRevInfo = getLogFileImpl().getRevisionHeader(0); long startingSourceSeekPosition = firstRevInfo.getRevisionStartPosition(); long endingSourceSeekPosition = parentRevInfo.getRevisionStartPosition(); long numberOfBytesToCopyFromSource = endingSourceSeekPosition - startingSourceSeekPosition; oldArchiveStream.seek(startingSourceSeekPosition); copyFromOneOpenFileToAnotherOpenFile(oldArchiveStream, newArchiveStream, numberOfBytesToCopyFromSource); // Update the parent revision parentRevInfo.setIsLocked(false); parentRevInfo.setIsTip(false); // Write the parent revision header parentRevInfo.write(newArchiveStream); commandLineArgs.setParentRevisionString(parentRevInfo.getRevisionString()); // Write the parent revision data startingSourceSeekPosition = parentRevInfo.getRevisionDataStartPosition(); oldArchiveStream.seek(startingSourceSeekPosition); copyFromOneOpenFileToAnotherOpenFile(oldArchiveStream, newArchiveStream, parentRevInfo.getRevisionSize()); // Copy any deeper branch revisions to the new archive so that any child branch revisions // immediately follow their parent. This fixes the bug that was posted on the forums where things // got hosed up on creation of a 2nd revision on a branch that already had children branches. writeAnyBranchRevisionsThatAreChildrenOfParent(parentRevInfo); // Write the new revision. Recall that this new revision is a forward delta, and therefore // follows the parent revision in the logfile. newRevisionHeader.write(newArchiveStream); copyFromOneOpenFileToAnotherOpenFile(deltaCompressor.getCompressedStream(), newArchiveStream, newRevisionHeader.getRevisionSize()); // Copy the rest of the original archive to the new archive. numberOfBytesToCopyFromSource = getLogFileImpl().getFile().length() - oldArchiveStream.getFilePointer(); copyFromOneOpenFileToAnotherOpenFile(oldArchiveStream, newArchiveStream, numberOfBytesToCopyFromSource); } void writeNewLogfileCreatingNewBranch(RevisionHeader parentRevInfo, File tempFileForCompareResults) throws QVCSException, IOException { // This revision will be a forward delta. // Update the parent revision parentRevInfo.incrementChildCount(); parentRevInfo.setIsLocked(false); // Create the revision header for this new revision. We need to capture // the new revision header info before we write the logfile header // because we need the new revision header info in the case that we // are applying a label. The label info gets written out to the logfile // header. RevisionHeader newRevisionHeader = new RevisionHeader(getLogFileImpl().getAccessList(), getLogFileImpl().getModifierList()); newRevisionHeader.setCreator(userName); newRevisionHeader.setCheckInDate(checkInDate); newRevisionHeader.setEditDate(commandLineArgs.getInputfileTimeStamp()); newRevisionHeader.setRevisionDescription(checkInComment); newRevisionHeader.setParentRevisionHeader(parentRevInfo); newRevisionHeader.setIsTip(true); if (lockFlag) { newRevisionHeader.setIsLocked(true); newRevisionHeader.setLocker(userName); } Compressor compressor = new ZlibCompressor(); newRevisionHeader.setDepth(1 + parentRevInfo.getDepth()); newRevisionHeader.setRevisionSize(getCompressedRevisionSizeForDelta(tempFileForCompareResults, compressor)); newRevisionHeader.setIsCompressed(compressor.getBufferIsCompressedFlag()); newRevisionHeader.setMajorNumber(parentRevInfo.getChildCount()); newRevisionHeader.setMinorNumber(1); newRevisionHeader.setRevisionDescriptor(new RevisionDescriptor(newRevisionHeader, parentRevInfo)); // Update the command line with the new revision's revision string so we // can return that to the client. commandLineArgs.setNewRevisionString(newRevisionHeader.getRevisionString()); // TODO -- is there anything we need to do here if the default branch is not the TRUNK? // e.g. is there anything else in the archive header that might need to get updated? // Might have to update the supplemental info -- I'm not sure if the supplemental info // ever contains non-TRUNK info, e.g. can it contain the default branch info? // Increment the number of revisions. getLogFileImpl().getLogFileHeaderInfo().getLogFileHeader().incrementRevisionCount(); if (!lockFlag) { getLogFileImpl().getLogFileHeaderInfo().getLogFileHeader().decrementLockCount(); } if (labelFlag) { addLabelToHeader(newRevisionHeader); } // Write the header. getLogFileImpl().getLogFileHeaderInfo().write(newArchiveStream); // Write the logfile, up to the parent revision. RevisionHeader firstRevInfo = getLogFileImpl().getRevisionHeader(0); long startingSourceSeekPosition = firstRevInfo.getRevisionStartPosition(); long endingSourceSeekPosition = parentRevInfo.getRevisionStartPosition(); long numberOfBytesToCopyFromSource = endingSourceSeekPosition - startingSourceSeekPosition; oldArchiveStream.seek(startingSourceSeekPosition); copyFromOneOpenFileToAnotherOpenFile(oldArchiveStream, newArchiveStream, numberOfBytesToCopyFromSource); // Write the parent revision header parentRevInfo.write(newArchiveStream); commandLineArgs.setParentRevisionString(parentRevInfo.getRevisionString()); // Write the parent revision data startingSourceSeekPosition = parentRevInfo.getRevisionDataStartPosition(); oldArchiveStream.seek(startingSourceSeekPosition); copyFromOneOpenFileToAnotherOpenFile(oldArchiveStream, newArchiveStream, parentRevInfo.getRevisionSize()); newRevisionHeader.write(newArchiveStream); copyFromOneOpenFileToAnotherOpenFile(compressor.getCompressedStream(), newArchiveStream, newRevisionHeader.getRevisionSize()); // Copy the rest of the original archive to the new archive. numberOfBytesToCopyFromSource = oldArchiveStream.length() - oldArchiveStream.getFilePointer(); copyFromOneOpenFileToAnotherOpenFile(oldArchiveStream, newArchiveStream, numberOfBytesToCopyFromSource); } /** * This method is used in the case where there are no differences between the existing TRUNK tip revision and the new revision, * AND the user wants to apply a label at checkin time. This is a feature that I never got implemented in the C++ product in the * 'right' way. */ void writeNewLogfileThatOnlyChangesTheHeader(RevisionHeader revInfo) throws QVCSException, IOException { assert (labelFlag); if (!lockFlag) { getLogFileImpl().getLogFileHeaderInfo().getLogFileHeader().decrementLockCount(); } // Update the command line with the revision's revision string so we // can return that to the client. commandLineArgs.setNewRevisionString(revInfo.getRevisionString()); // Add the label to the header. addLabelToHeader(revInfo); // Write the header information to the stream. getLogFileImpl().getLogFileHeaderInfo().write(newArchiveStream); // Position the old archive to the end of the header area. LogFileHeaderInfo oldHeaderInfo = new LogFileHeaderInfo(); oldHeaderInfo.read(oldArchiveStream); if (!lockFlag) { // We need to release the lock on the revision. // At this point, both the new and old archive streams are positioned // immediately after the archive header. // Copy up to the revision that needs to change. long bytesToBeginningOfRevision = revInfo.getRevisionStartPosition() - oldArchiveStream.getFilePointer(); copyFromOneOpenFileToAnotherOpenFile(oldArchiveStream, newArchiveStream, bytesToBeginningOfRevision); // Now write the revision header, unlocked. revInfo.setIsLocked(false); revInfo.write(newArchiveStream); // Position the old stream to the beginning of the revision data. oldArchiveStream.seek(revInfo.getRevisionDataStartPosition()); long remainingBytesToCopy = oldArchiveStream.length() - oldArchiveStream.getFilePointer(); // Copy the rest of the original archive to the new archive. copyFromOneOpenFileToAnotherOpenFile(oldArchiveStream, newArchiveStream, remainingBytesToCopy); } else { // The user is keeping the archive locked. This means we don't // need to specifically update the revision header, since it will // remain locked. // Figure out how many bytes remain in the original that need to // be copied to the new archive. long numberOfBytesToCopyFromSource = oldArchiveStream.length() - oldArchiveStream.getFilePointer(); // Copy the rest of the original archive to the new archive. copyFromOneOpenFileToAnotherOpenFile(oldArchiveStream, newArchiveStream, numberOfBytesToCopyFromSource); } } private void addLabelToHeader(RevisionHeader revInfo) throws QVCSException { // Check to see that the label is not already in use.... If it is, get // rid of it IF they have specified the reuse label flag. If it is // already in use, and they did not specify the reuse label flag, then // we have a problem... so we bail with an exception. LabelInfo[] existingLabels = getLogFileImpl().getLogFileHeaderInfo().getLabelInfo(); if (existingLabels != null) { for (int i = 0; i < existingLabels.length; i++) { String existingLabel = existingLabels[i].getLabelString(); if (existingLabel.equals(labelString)) { if (reuseLabelFlag) { // Remove the old label from the array of labels. LabelInfo[] retainedLabels = new LabelInfo[existingLabels.length - 1]; int k = 0; for (int j = 0; j < existingLabels.length; j++) { if (j == i) { // skip the matching label. continue; } else { retainedLabels[k++] = existingLabels[j]; } } getLogFileImpl().getLogFileHeaderInfo().setLabelInfo(retainedLabels); } else { // The user is trying to use a label that is already in use, // and they did not specify the reuse label flag. Don't // allow this! String errorMessage = "Checkin failed. Label request ignored for " + getLogFileImpl().getShortWorkfileName() + ". Label (" + labelString + ") is already in use. Try using the 'reuse label' flag."; commandLineArgs.setFailureReason(errorMessage); throw new QVCSException(errorMessage); } break; } } } // Add the label to the header. // Create a new LabelInfo array that's one bigger than before LabelInfo[] oldLabelInfo = getLogFileImpl().getLogFileHeaderInfo().getLabelInfo(); int newLabelInfoSize = 1; if (oldLabelInfo != null) { newLabelInfoSize = oldLabelInfo.length + 1; } LabelInfo[] newLabelInfo = new LabelInfo[newLabelInfoSize]; // Copy the existing labels to the new label info array, leaving // room at the front for the new label if (oldLabelInfo != null) { System.arraycopy(oldLabelInfo, 0, newLabelInfo, 1, newLabelInfo.length - 1); } // Create the LabelInfo object for this label. LabelInfo labelInfo = new LabelInfo(labelString, revInfo.getRevisionString(), floatLabelFlag, creatorIndex); // Add it to the array at the beginning. newLabelInfo[0] = labelInfo; // Set the new label info array on the archive header. getLogFileImpl().getLogFileHeaderInfo().setLabelInfo(newLabelInfo); } /** * Create an edit script that just replaces the existing revision with the new file. */ private void createNoComputeDeltaEditScript(String existingRevisionFilename, String editScriptOutputFilename, boolean isReverseDelta) { DataOutputStream outStream = null; DataInputStream inStream = null; long bytesToCopy; try { File newRevisionFile = new File(filename); File existingRevisionFile = new File(existingRevisionFilename); File editScriptFile = new File(editScriptOutputFilename); CompareFilesEditInformation editInfo = new CompareFilesEditInformation(); editInfo.setEditType(CompareFilesEditInformation.QVCS_EDIT_REPLACE); editInfo.setSeekPosition(0); outStream = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(editScriptFile))); if (isReverseDelta) { editInfo.setDeletedBytesCount((int) newRevisionFile.length()); editInfo.setInsertedBytesCount((int) existingRevisionFile.length()); inStream = new DataInputStream(new BufferedInputStream(new FileInputStream(existingRevisionFile))); bytesToCopy = existingRevisionFile.length(); } else { editInfo.setDeletedBytesCount((int) existingRevisionFile.length()); editInfo.setInsertedBytesCount((int) newRevisionFile.length()); inStream = new DataInputStream(new BufferedInputStream(new FileInputStream(newRevisionFile))); bytesToCopy = newRevisionFile.length(); } // Pad the beginning of the file with a bogus header. A read compare // puts something useful in this header info, but we just need to // put those bytes in for padding... byte[] padBytes = new byte[CompareFilesEditHeader.getEditHeaderSize()]; outStream.write(padBytes); editInfo.write(outStream); AbstractLogFileOperation.copyFromOneOpenFileToAnotherOpenFile(inStream, outStream, bytesToCopy); // Fake things out so we'll think a comparison was actually attempted. // This is a bit of a kludge, in that it creates coupling between this // code and the CompareFiles class. The correct way to handle this would // be to create a separate implementation of a compare algorithm that // had the same interface as CompareFiles, but basically performed the // same code that we have above. That implementation would set the // compare attempted flag to true, and it would be good. As it is, // we'll survive with the kludge. compareFilesOperator.setCompareAttempted(true); } catch (IOException e) { LOGGER.log(Level.WARNING, "Caught exception: " + e.getClass().toString() + ": " + e.getLocalizedMessage()); } finally { try { if (outStream != null) { outStream.close(); } if (inStream != null) { inStream.close(); } } catch (IOException e) { LOGGER.log(Level.WARNING, "Caught exception: " + e.getClass().toString() + ": " + e.getLocalizedMessage()); } } } /** * This method handles the case where a user is checking in a new branch revision and that new branch revision's parent revision * has some child branches. The logfile layout requires that the child branch revisions (that have a greater depth) get written * to the logfile <i>before</i> the new revision. * * @param parentRevInfo the parent revision header. */ private void writeAnyBranchRevisionsThatAreChildrenOfParent(RevisionHeader parentRevHeader) throws IOException, QVCSException { int parentDepth = parentRevHeader.getDepth(); int revisionToLookAtIndex = parentRevHeader.getRevisionIndex() + 1; // We have to subtract 2 to get the maximum index, since we have already written the header, and incremented the revision count in the header. int maximumRevisionIndex = getLogFileImpl().getRevisionCount() - 2; while (revisionToLookAtIndex <= maximumRevisionIndex) { RevisionHeader revisionToLookAt = getLogFileImpl().getRevisionHeader(revisionToLookAtIndex); if (revisionToLookAt.getDepth() > parentDepth) { // Write the revision header revisionToLookAt.write(newArchiveStream); // Write the revision data. long startingSourceSeekPosition = revisionToLookAt.getRevisionDataStartPosition(); if (revisionToLookAt.getRevisionSize() > 0L) { oldArchiveStream.seek(startingSourceSeekPosition); copyFromOneOpenFileToAnotherOpenFile(oldArchiveStream, newArchiveStream, revisionToLookAt.getRevisionSize()); } } else { break; } revisionToLookAtIndex++; } } }