/* * The MIT License * * Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Erik Ramfelt * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package hudson.scm; import hudson.Util; import hudson.model.AbstractBuild; import hudson.model.Job; import hudson.model.Run; import hudson.model.User; import hudson.scm.SubversionChangeLogSet.LogEntry; import hudson.scm.SubversionSCM.ModuleLocation; import java.io.IOException; import java.io.Serializable; import java.util.AbstractList; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import jenkins.triggers.SCMTriggerItem; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; import org.kohsuke.stapler.export.Exported; import org.kohsuke.stapler.export.ExportedBean; import org.tmatesoft.svn.core.internal.util.SVNDate; /** * {@link ChangeLogSet} for Subversion. * * @author Kohsuke Kawaguchi */ public final class SubversionChangeLogSet extends ChangeLogSet<LogEntry> { private final List<LogEntry> logs; /** * @GuardedBy this */ private Map<String,Long> revisionMap; private boolean ignoreDirPropChanges; @Deprecated /*package*/ SubversionChangeLogSet(AbstractBuild<?,?> build, List<LogEntry> logs) { this(build, build.getProject().getScm().getEffectiveBrowser(), logs, false); } /*package*/ SubversionChangeLogSet(Run<?,?> build, RepositoryBrowser<?> browser, List<LogEntry> logs, boolean ignoreDirPropChanges) { super(build, browser); this.ignoreDirPropChanges = ignoreDirPropChanges; this.logs = prepareChangeLogEntries(logs); } public boolean isEmptySet() { return logs.isEmpty(); } public List<LogEntry> getLogs() { return logs; } public Iterator<LogEntry> iterator() { return logs.iterator(); } @Override public String getKind() { return "svn"; } public synchronized Map<String,Long> getRevisionMap() throws IOException { if(revisionMap==null) revisionMap = SubversionSCM.parseRevisionFile(getRun()); return revisionMap; } private List<LogEntry> prepareChangeLogEntries(List<LogEntry> items) { items = removeDuplicatedEntries(items); if (ignoreDirPropChanges) items = removePropertyOnlyChanges(items); // we want recent changes first Collections.sort(items, new ReverseByRevisionComparator()); for (LogEntry log : items) { log.setParent(this); } return Collections.unmodifiableList(items); } static List<LogEntry> removePropertyOnlyChanges(List<LogEntry> items) { for (LogEntry entry : items) { entry.removePropertyOnlyPaths(); } return items; } /** * Removes duplicate entries, e.g. those coming form svn:externals. * * @param items list of items * @return filtered list without duplicated entries */ static List<LogEntry> removeDuplicatedEntries(List<LogEntry> items) { Set<LogEntry> entries = new HashSet<LogEntry>(items); for (LogEntry sourceEntry : items) { // LogEntry equality does not consider paths, but some might have localPath attributes // that would get lost by HashSet duplicate removal for (LogEntry destinationEntry : entries) { if (sourceEntry.equals(destinationEntry)) { // get all local paths and set in destination for (Path sourcePath : sourceEntry.getPaths()) { if (sourcePath.localPath != null) { for (Path destinationPath : destinationEntry.getPaths()) { if (sourcePath.value.equals(destinationPath.value)) { destinationPath.localPath = sourcePath.localPath; } } } } } } } return new ArrayList<LogEntry>(entries); } @Exported public List<RevisionInfo> getRevisions() throws IOException { List<RevisionInfo> r = new ArrayList<RevisionInfo>(); for (Map.Entry<String, Long> e : getRevisionMap().entrySet()) r.add(new RevisionInfo(e.getKey(),e.getValue())); return r; } @ExportedBean(defaultVisibility=999) public static final class RevisionInfo { @Exported public final String module; @Exported public final long revision; public RevisionInfo(String module, long revision) { this.module = module; this.revision = revision; } } /** * One commit. * <p> * Setter methods are public only so that the objects can be constructed from Digester. * So please consider this object read-only. */ public static class LogEntry extends ChangeLogSet.Entry { private int revision; private User author; private String date; private String msg; private List<Path> paths = new ArrayList<Path>(); /** * Gets the {@link SubversionChangeLogSet} to which this change set belongs. */ public SubversionChangeLogSet getParent() { return (SubversionChangeLogSet)super.getParent(); } protected void removePropertyOnlyPaths() { for (Iterator<Path> it = paths.iterator(); it.hasNext();) { Path path = it.next(); if (path.isPropOnlyChange()) it.remove(); } } // because of the classloader difference, we need to extend this method to make it accessible // to the rest of SubversionSCM @Override @SuppressWarnings("rawtypes") protected void setParent(ChangeLogSet changeLogSet) { super.setParent(changeLogSet); } /** * Gets the revision of the commit. * * <p> * If the commit made the repository revision 1532, this * method returns 1532. */ @Exported public int getRevision() { return revision; } public void setRevision(int revision) { this.revision = revision; } @Override public String getCommitId() { return String.valueOf(revision); } @Override public long getTimestamp() { return date!=null ? SVNDate.parseDate(date).getTime() : -1; } @Override public User getAuthor() { if(author==null) return User.getUnknown(); return author; } @Override public Collection<String> getAffectedPaths() { return new AbstractList<String>() { public String get(int index) { return preparePath(paths.get(index).value); } public int size() { return paths.size(); } }; } private String preparePath(String path) { Job job = getParent().getRun().getParent(); SCMTriggerItem s = SCMTriggerItem.SCMTriggerItems.asSCMTriggerItem(job); if (s == null) { return path; } for (SCM scm : s.getSCMs()) { if (!(scm instanceof SubversionSCM)) { continue; } ModuleLocation[] locations = ((SubversionSCM)scm).getLocations(); for (int i = 0; i < locations.length; i++) { String commonPart = findCommonPart(locations[i].remote, path); if (commonPart != null) { if (path.startsWith("/")) { path = path.substring(1); } String newPath = path.substring(commonPart.length()); if (newPath.startsWith("/")) { newPath = newPath.substring(1); } return newPath; } } } return path; } private String findCommonPart(String folder, String filePath) { if (folder == null || filePath == null) { return null; } if (filePath.startsWith("/")) { filePath = filePath.substring(1); } for (int i = 0; i < folder.length(); i++) { String part = folder.substring(i); if (filePath.startsWith(part)) { return part; } } return null; } public void setUser(String author) { this.author = User.get(author); } @Exported public String getUser() {// digester wants read/write property, even though it never reads. Duh. return author!=null ? author.getDisplayName() : "unknown"; } @Exported public String getDate() { return date; } public void setDate(String date) { this.date = date; } @Override @Exported public String getMsg() { return msg; } public void setMsg(String msg) { this.msg = msg; } public void addPath( Path p ) { p.entry = this; paths.add(p); } /** * Gets the files that are changed in this commit. * @return * can be empty but never null. */ @Exported public List<Path> getPaths() { return paths; } @Override public Collection<Path> getAffectedFiles() { Collection<Path> affectedFiles = new ArrayList<Path>(); for (Path p : paths) { if (p.hasLocalPath()) { affectedFiles.add(p); } } // FIXME backwards compatibility? return affectedFiles; } void finish() { Collections.sort(paths, new Comparator<Path>() { @Override public int compare(Path o1, Path o2) { String path1 = Util.fixNull(o1.getValue()); String path2 = Util.fixNull(o2.getValue()); return path1.compareTo(path2); } }); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } LogEntry that = (LogEntry) o; if (revision != that.revision) { return false; } if (author != null ? !author.equals(that.author) : that.author != null) { return false; } if (date != null ? !date.equals(that.date) : that.date != null) { return false; } if (msg != null ? !msg.equals(that.msg) : that.msg != null) { return false; } return true; } @Override public int hashCode() { int result = revision; result = 31 * result + (author != null ? author.hashCode() : 0); result = 31 * result + (date != null ? date.hashCode() : 0); result = 31 * result + (msg != null ? msg.hashCode() : 0); return result; } } /** * A file in a commit. * <p> * Setter methods are public only so that the objects can be constructed from Digester. * So please consider this object read-only. */ @ExportedBean(defaultVisibility=999) public static class Path implements AffectedFile { private LogEntry entry; private char action; /** * full path to file within SVN repository, e.g. /trunk/project/foo/bar.txt */ private String value; /** * Path to file within workspace, e.g. stuff/foo/bar.txt */ private String localPath; private String kind; /** * Gets the {@link LogEntry} of which this path is a member. */ public LogEntry getLogEntry() { return entry; } /** * Sets the {@link LogEntry} of which this path is a member. */ public void setLogEntry(LogEntry entry) { this.entry = entry; } public void setAction(String action) { this.action = action.charAt(0); } /** * Path in the repository. Such as <tt>/test/trunk/foo.c</tt> */ @Exported(name="file") public String getValue() { return value; } /** * Inherited from AffectedFile * * Since 2.TODO this no longer returns the path relative to repository root, * but the path relative to the workspace root. Use getValue() instead. */ public String getPath() { if (localPath == null) { // compatibility to older versions that did not store this path return value; } return localPath; } @Restricted(NoExternalUse.class) public void setLocalPath(String path) { this.localPath = path; } public boolean hasLocalPath() { return localPath != null; } public void setValue(String value) { this.value = value; } public boolean isPropOnlyChange() { return action == 'M' && "dir".equals(kind); } public String getKind() { return kind; } public void setKind(String kind) { this.kind = kind; } @Exported public EditType getEditType() { if( action=='A' ) return EditType.ADD; if( action=='D' ) return EditType.DELETE; return EditType.EDIT; } } private static final class ReverseByRevisionComparator implements Comparator<LogEntry>, Serializable { private static final long serialVersionUID = 1L; public int compare(LogEntry a, LogEntry b) { return b.getRevision() - a.getRevision(); } } }