001 package org.hackystat.sensor.ant.svn; 002 003 import java.util.ArrayList; 004 import java.util.Collection; 005 import java.util.Date; 006 import java.util.List; 007 import java.util.TreeMap; 008 import java.util.TreeSet; 009 010 import org.tmatesoft.svn.core.SVNLogEntry; 011 import org.tmatesoft.svn.core.SVNLogEntryPath; 012 import org.tmatesoft.svn.core.SVNNodeKind; 013 import org.tmatesoft.svn.core.io.SVNFileRevision; 014 import org.tmatesoft.svn.core.io.SVNRepository; 015 016 /** 017 * A commit record tracks all items that get modified during an SVN revision. 018 * Note that in SVN, versioning is performed on the entire repository instead of 019 * a single file. 020 * 021 * @author (Cedric) Qin ZHANG 022 */ 023 public class CommitRecord { 024 025 private SVNRepository svnRepository; 026 private SVNLogEntry svnLogEntry; 027 private List<CommitRecordEntry> commitRecordEntries = new ArrayList<CommitRecordEntry>(); 028 029 /** 030 * Constructs this instance. 031 * 032 * @param svnRepository The svn repository. 033 * @param svnLogEntry The svn log for a revision. 034 * 035 * @throws Exception If there is any error. 036 */ 037 CommitRecord(SVNRepository svnRepository, SVNLogEntry svnLogEntry) throws Exception { 038 this.svnRepository = svnRepository; 039 this.svnLogEntry = svnLogEntry; 040 this.processSvnLogEntry(); 041 } 042 043 /** 044 * Process SVN log to extract information about which file/directory gets 045 * modified in this revision. 046 * 047 * @throws Exception If there is any error. 048 */ 049 private void processSvnLogEntry() throws Exception { 050 long currentRevision = this.getRevision(); 051 052 // They are used to handle file or directory renaming. 053 TreeMap<String, SVNLogEntryPath> copyPaths = new TreeMap<String, SVNLogEntryPath>(); 054 TreeSet<String> deletePaths = new TreeSet<String>(); 055 056 for (Object entryValue : this.svnLogEntry.getChangedPaths().values()) { 057 SVNLogEntryPath changedPath = (SVNLogEntryPath) entryValue; 058 String path = changedPath.getPath(); 059 String copyPath = changedPath.getCopyPath(); // null if no copy path. 060 061 char changeType = changedPath.getType(); 062 if (changeType == 'A') { 063 // New file added, if copyPath == null. 064 // File rename, and possibly changed, then copyPath != null and there is 065 // a 'D' entry. 066 // (It seems there is always a 'D' entry') 067 // What about resurect of previously deleted file? 068 if (copyPath == null) { // newly created 069 this.commitRecordEntries.add(new CommitRecordEntry(this.svnRepository, null, -1, 070 path, currentRevision)); 071 } 072 else { // file rename, check for 'D' 073 copyPaths.put(copyPath, changedPath); 074 } 075 } 076 else if (changeType == 'D') { 077 // Delete an existing file, copyPath == null 078 // Note: Node kind is alway "none", we don't know it's a file or 079 // direcotry. 080 deletePaths.add(path); // process later, some are deletion, some are 081 // renaming. 082 } 083 else if (changeType == 'M') { 084 // Modify existing file, copyPath == null 085 // Note, if you rename a top directory, all files in the directory will 086 // be moified 087 // with copyPath == null, even if there is no change in those files. 088 // Find out the file name in the previous revision, which might have 089 // been changed. 090 091 if (SVNNodeKind.FILE == this.svnRepository.checkPath(path, currentRevision)) { 092 Collection<?> revisions = this.svnRepository.getFileRevisions(path, null, 1, 093 currentRevision); 094 // we should alway find at least one, since change type is 'M'. 095 if (revisions == null || revisions.isEmpty()) { 096 throw new RuntimeException("Inconsistent SVN record. Corrupted SVN repository?"); 097 } 098 long thePrevRevisionNumber = -1; 099 SVNFileRevision thePrevFileRevision = null; 100 for (Object revision : revisions) { 101 SVNFileRevision curRevision = (SVNFileRevision) revision; 102 long curRevisionNumber = curRevision.getRevision(); 103 if (thePrevRevisionNumber < curRevisionNumber 104 && curRevisionNumber < currentRevision) { 105 thePrevRevisionNumber = curRevisionNumber; 106 thePrevFileRevision = curRevision; 107 } 108 } 109 if (thePrevFileRevision == null) { 110 throw new RuntimeException("Assertion Failed."); 111 } 112 this.commitRecordEntries.add(new CommitRecordEntry(this.svnRepository, 113 thePrevFileRevision.getPath(), thePrevFileRevision.getRevision(), path, 114 currentRevision)); 115 } 116 } 117 else if (changeType == 'R') { 118 // Replace, the object is first deleted, and another with the same name 119 // added, 120 // all within single revision (Note: I CANNOT produce this in SVN). 121 // So, it's actually a deletion plus an addition. 122 this.commitRecordEntries.add(new CommitRecordEntry(this.svnRepository, path, 123 currentRevision - 1, null, currentRevision)); 124 this.commitRecordEntries.add(new CommitRecordEntry(this.svnRepository, null, -1, path, 125 currentRevision)); 126 } 127 else { 128 throw new RuntimeException("Unknown SVN change type."); 129 } 130 } 131 132 // handle delete and file rename 133 for (String deletePath : deletePaths) { 134 SVNLogEntryPath addLogEntryPath = copyPaths.get(deletePath); 135 if (addLogEntryPath == null) { // true delete 136 this.commitRecordEntries.add(new CommitRecordEntry(this.svnRepository, deletePath, 137 currentRevision - 1, null, currentRevision)); 138 } 139 else { // rename 140 this.commitRecordEntries.add(new CommitRecordEntry(this.svnRepository, addLogEntryPath 141 .getCopyPath(), addLogEntryPath.getCopyRevision(), addLogEntryPath.getPath(), 142 currentRevision)); 143 } 144 } 145 } 146 147 /** 148 * Gets this revision number. 149 * 150 * @return The revision number. 151 */ 152 public final long getRevision() { 153 return this.svnLogEntry.getRevision(); 154 } 155 156 /** 157 * Gets the author who made the commit. 158 * 159 * @return The authoer. 160 */ 161 public String getAuthor() { 162 return this.svnLogEntry.getAuthor(); 163 } 164 165 /** 166 * Gets the commit time. 167 * 168 * @return The commit time. 169 */ 170 public Date getCommitTime() { 171 return this.svnLogEntry.getDate(); 172 } 173 174 /** 175 * Gets the commit log message. 176 * 177 * @return The commit log message. 178 */ 179 public String getMessage() { 180 return this.svnLogEntry.getMessage(); 181 } 182 183 /** 184 * Returns the string representation of this record. 185 * @return the string representation. 186 */ 187 @Override 188 public String toString() { 189 return "Author=" + this.getAuthor() + ", Message=" + this.getMessage() + ", CommitTime=" 190 + this.getCommitTime(); 191 } 192 193 /** 194 * Gets all changed items in this revision. 195 * 196 * @return A collection of <code>CommitRecordEntry</code> instances. 197 */ 198 public Collection<CommitRecordEntry> getCommitRecordEntries() { 199 return this.commitRecordEntries; 200 } 201 }