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    }