/* * The MIT License * * Copyright (c) 2004-2010, Sun Microsystems, Inc., Kohsuke Kawaguchi, Jean-Baptiste Quenot, * Seiji Sogabe, Alan Harder, Vojtech Habarta, Yahoo! Inc. * * 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.Extension; import hudson.model.AbstractBuild; import hudson.model.Action; import hudson.model.Describable; import hudson.model.Descriptor; import hudson.model.Hudson; import hudson.model.TaskListener; import hudson.model.TaskThread; import hudson.scm.subversion.Messages; import hudson.scm.SubversionSCM.SvnInfo; import hudson.util.CopyOnWriteMap; import hudson.security.Permission; import hudson.util.MultipartFormDataParser; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; import org.kohsuke.stapler.export.Exported; import org.kohsuke.stapler.export.ExportedBean; import org.tmatesoft.svn.core.SVNException; import org.tmatesoft.svn.core.SVNURL; import org.tmatesoft.svn.core.wc.SVNClientManager; import org.tmatesoft.svn.core.wc.SVNCopyClient; import org.tmatesoft.svn.core.wc.SVNRevision; import org.tmatesoft.svn.core.wc.SVNCopySource; import javax.servlet.ServletException; import java.io.IOException; import java.io.PrintStream; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.tmatesoft.svn.core.ISVNLogEntryHandler; import org.tmatesoft.svn.core.SVNLogEntry; import org.tmatesoft.svn.core.wc.SVNLogClient; /** * {@link Action} that lets people create tag for the given build. * * @author Kohsuke Kawaguchi */ @ExportedBean public class SubversionTagAction extends AbstractScmTagAction implements Describable { /** * Map is from the repository URL to the URLs of tags. * If a module is not tagged, the value will be empty list. * Never an empty map. */ private final Map> tags = new CopyOnWriteMap.Tree>(); /*package*/ SubversionTagAction(AbstractBuild build,Collection svnInfos) { super(build); Map> m = new HashMap>(); for (SvnInfo si : svnInfos) m.put(si,new ArrayList()); tags.putAll(m); } /** * Was any tag created by the user already? */ public boolean hasTags() { return isTagged(); } public String getIconFileName() { if(!isTagged() && !getACL().hasPermission(getPermission())) return null; return "save.gif"; } public String getDisplayName() { int nonNullTag = 0; for (List v : tags.values()) { if(!v.isEmpty()) { nonNullTag++; if(nonNullTag>1) break; } } if(nonNullTag==0) return Messages.SubversionTagAction_DisplayName_HasNoTag(); if(nonNullTag==1) return Messages.SubversionTagAction_DisplayName_HasATag(); else return Messages.SubversionTagAction_DisplayName_HasTags(); } /** * @see #tags */ public Map> getTags() { return Collections.unmodifiableMap(tags); } @Exported(name="tags") public List getTagInfo() { List data = new ArrayList(); for (Entry> e : tags.entrySet()) { String module = e.getKey().toString(); for (String url : e.getValue()) data.add(new TagInfo(module, url)); } return data; } @ExportedBean public static class TagInfo { private String module, url; private TagInfo(String module, String url) { this.module = module; this.url = url; } @Exported public String getModule() { return module; } @Exported public String getUrl() { return url; } } /** * Returns true if this build has already been tagged at least once. */ @Override public boolean isTagged() { for (List t : tags.values()) { if(!t.isEmpty()) return true; } return false; } @Override public String getTooltip() { String tag = null; for (List v : tags.values()) { for (String s : v) { if (tag != null) return Messages.SubversionTagAction_Tooltip(); // Multiple tags tag = s; } } if(tag!=null) return Messages.SubversionTagAction_Tooltip_OneTag(tag); else return null; } private static final Pattern TRUNK_BRANCH_MARKER = Pattern.compile("/(trunk|branches)(/|$)"); /** * Creates a URL, to be used as the default value of the module tag URL. * * @return * null if failed to guess. */ public String makeTagURL(SvnInfo si) { // assume the standard trunk/branches/tags repository layout Matcher m = TRUNK_BRANCH_MARKER.matcher(si.url); if(!m.find()) return null; // doesn't have 'trunk' nor 'branches' return si.url.substring(0,m.start())+"/tags/"+getBuild().getProject().getName()+"-"+getBuild().getNumber(); } /** * Invoked to actually tag the workspace. */ public synchronized void doSubmit(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { getACL().checkPermission(getPermission()); MultipartFormDataParser parser = new MultipartFormDataParser(req); Map newTags = new HashMap(); int i=-1; for (SvnInfo e : tags.keySet()) { i++; if(tags.size()>1 && parser.get("tag"+i)==null) continue; // when tags.size()==1, UI won't show the checkbox. newTags.put(e,parser.get("name" + i)); } UserProvidedCredential upc=null; if (parser.get("credential")!=null) upc = UserProvidedCredential.fromForm(req,parser); new TagWorkerThread(newTags,upc,parser.get("comment")).start(); rsp.sendRedirect("."); } @Override public Permission getPermission() { return SubversionSCM.TAG; } /** * The thread that performs tagging operation asynchronously. */ public final class TagWorkerThread extends TaskThread { private final Map tagSet; /** * If the user provided a separate credential, this object represents that. */ private final UserProvidedCredential upc; private final String comment; public TagWorkerThread(Map tagSet, UserProvidedCredential upc, String comment) { super(SubversionTagAction.this,ListenerAndText.forMemory()); this.tagSet = tagSet; this.upc = upc; this.comment = comment; } @Override protected void perform(TaskListener listener) { try { final SVNClientManager cm = upc!=null ? SVNClientManager.newInstance(SubversionSCM.createDefaultSVNOptions(),upc.new AuthenticationManagerImpl(listener)) : SubversionSCM.createSvnClientManager(getBuild().getProject()); try { for (Entry e : tagSet.entrySet()) { PrintStream logger = listener.getLogger(); logger.println("Tagging "+e.getKey()+" to "+e.getValue()); try { SVNURL src = SVNURL.parseURIDecoded(e.getKey().url); SVNURL dst = SVNURL.parseURIDecoded(e.getValue()); SVNCopyClient svncc = cm.getCopyClient(); SVNRevision sourceRevision = SVNRevision.create(e.getKey().revision); SVNCopySource csrc = new SVNCopySource(sourceRevision, sourceRevision, src); svncc.doCopy( new SVNCopySource[]{csrc}, dst, false, true, false, comment, null); } catch (SVNException x) { if (x.getErrorMessage().getFullMessage().indexOf("does not exist in revision") > 0) { logger.println("Tagging failed using revision " + e.getKey().revision + ", I think because of a branch creation (known Hudson bug). I will try to locate the next revision and copy from this..."); try { SVNLogClient lc = cm.getLogClient(); MyLogHandler leh =new MyLogHandler(e.getKey().revision); lc.doLog(SVNURL.parseURIDecoded(e.getKey().url), null, SVNRevision.HEAD, SVNRevision.HEAD, SVNRevision.create(e.getKey().revision), false, false, -1, leh); SVNRevision nextRevision = SVNRevision.create(leh.nextRevision); logger.println("Hold your thumbs, I am trying with " + leh.nextRevision); SVNURL src = SVNURL.parseURIDecoded(e.getKey().url); SVNURL dst = SVNURL.parseURIDecoded(e.getValue()); SVNCopyClient svncc = cm.getCopyClient(); SVNCopySource csrc = new SVNCopySource(nextRevision, nextRevision, src); svncc.doCopy( new SVNCopySource[]{csrc}, dst, false, true, false, comment, null); } catch (SVNException xx) { xx.printStackTrace(listener.error("Failed to tag")); return; } } else { x.printStackTrace(listener.error("Failed to tag")); return; } } } // completed successfully for (Entry e : tagSet.entrySet()) SubversionTagAction.this.tags.get(e.getKey()).add(e.getValue()); getBuild().save(); workerThread = null; } finally { cm.dispose(); } } catch (Throwable e) { e.printStackTrace(listener.fatalError(e.getMessage())); } } } public Descriptor getDescriptor() { return Hudson.getInstance().getDescriptorOrDie(getClass()); } /** * Just for assisting form related stuff. */ @Extension public static class DescriptorImpl extends Descriptor { public String getDisplayName() { return null; } } public class MyLogHandler implements ISVNLogEntryHandler { private long failedRevision = 0; public long nextRevision = 0; public MyLogHandler(long fr) { this.failedRevision = fr; } public void handleLogEntry(SVNLogEntry logEntry) throws SVNException { if (failedRevision < logEntry.getRevision()) { nextRevision = logEntry.getRevision(); } } } }