All Downloads are FREE. Search and download functionalities are using the official Maven repository.

hudson.scm.SubversionSCM Maven / Gradle / Ivy

package hudson.scm;

import hudson.FilePath;
import hudson.Launcher;
import hudson.Proc;
import hudson.Util;
import hudson.model.Build;
import hudson.model.BuildListener;
import hudson.model.Descriptor;
import hudson.model.Project;
import hudson.model.TaskListener;
import hudson.util.ArgumentListBuilder;
import hudson.util.FormFieldValidator;
import org.apache.commons.digester.Digester;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.xml.sax.SAXException;

import javax.servlet.ServletException;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.StringTokenizer;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Subversion.
 *
 * Check http://svn.collab.net/repos/svn/trunk/subversion/svn/schema/ for
 * various output formats.
 *
 * @author Kohsuke Kawaguchi
 */
public class SubversionSCM extends AbstractCVSFamilySCM {
    private final String modules;
    private boolean useUpdate;
    private String username;
    private String otherOptions;

    SubversionSCM( String modules, boolean useUpdate, String username, String otherOptions ) {
        StringBuilder normalizedModules = new StringBuilder();
        StringTokenizer tokens = new StringTokenizer(modules);
        while(tokens.hasMoreTokens()) {
            if(normalizedModules.length()>0)    normalizedModules.append(' ');
            String m = tokens.nextToken();
            if(m.endsWith("/"))
                // the normalized name is always without the trailing '/'
                m = m.substring(0,m.length()-1);
            normalizedModules.append(m);
       }

        this.modules = normalizedModules.toString();
        this.useUpdate = useUpdate;
        this.username = nullify(username);
        this.otherOptions = nullify(otherOptions);
    }

    /**
     * Whitespace-separated list of SVN URLs that represent
     * modules to be checked out.
     */
    public String getModules() {
        return modules;
    }

    public boolean isUseUpdate() {
        return useUpdate;
    }

    public String getUsername() {
        return username;
    }

    public String getOtherOptions() {
        return otherOptions;
    }

    private Collection getModuleDirNames() {
        List dirs = new ArrayList();
        StringTokenizer tokens = new StringTokenizer(modules);
        while(tokens.hasMoreTokens()) {
            dirs.add(getLastPathComponent(tokens.nextToken()));
        }
        return dirs;
    }

    private boolean calcChangeLog(Build build, File changelogFile, Launcher launcher, BuildListener listener) throws IOException {
        if(build.getPreviousBuild()==null) {
            // nothing to compare against
            return createEmptyChangeLog(changelogFile, listener, "log");
        }

        PrintStream logger = listener.getLogger();

        Map previousRevisions = parseRevisionFile(build.getPreviousBuild());
        Map thisRevisions     = parseRevisionFile(build);

        Map env = createEnvVarMap(true);

        boolean changelogFileCreated = false;

        for( String module : getModuleDirNames() ) {
            Integer prevRev = previousRevisions.get(module);
            if(prevRev==null) {
                logger.println("no revision recorded for "+module+" in the previous build");
                continue;
            }
            Integer thisRev = thisRevisions.get(module);
            if(thisRev!=null && thisRev.equals(prevRev)) {
                logger.println("no change for "+module+" since the previous build");
                continue;
            }

            // TODO: this seems to clobber previously recorded changes when there are multiple modules
            String cmd = DESCRIPTOR.getSvnExe()+" log -v --xml --non-interactive -r "+(prevRev+1)+":BASE "+module;
            OutputStream os = new BufferedOutputStream(new FileOutputStream(changelogFile));
            changelogFileCreated = true;
            try {
                int r = launcher.launch(cmd,env,os,build.getProject().getWorkspace()).join();
                if(r!=0) {
                    listener.fatalError("revision check failed");
                    // report the output
                    FileInputStream log = new FileInputStream(changelogFile);
                    try {
                        Util.copyStream(log,listener.getLogger());
                    } finally {
                        log.close();
                    }
                    return false;
                }
            } finally {
                os.close();
            }
        }

        if(!changelogFileCreated)
            createEmptyChangeLog(changelogFile, listener, "log");

        return true;
    }

    /*package*/ static Map parseRevisionFile(Build build) throws IOException {
        Map revisions = new HashMap(); // module -> revision
        {// read the revision file of the last build
            File file = getRevisionFile(build);
            if(!file.exists())
                // nothing to compare against
                return revisions;

            BufferedReader br = new BufferedReader(new FileReader(file));
            String line;
            while((line=br.readLine())!=null) {
                int index = line.indexOf('/');
                if(index<0) {
                    continue;   // invalid line?
                }
                try {
                    revisions.put(line.substring(0,index), Integer.parseInt(line.substring(index+1)));
                } catch (NumberFormatException e) {
                    // perhaps a corrupted line. ignore
                }
            }
        }

        return revisions;
    }

    public boolean checkout(Build build, Launcher launcher, FilePath workspace, BuildListener listener, File changelogFile) throws IOException {
        boolean result;

        if(useUpdate && isUpdatable(workspace,listener)) {
            result = update(launcher,workspace,listener);
            if(!result)
                return false;
        } else {
            workspace.deleteContents();
            StringTokenizer tokens = new StringTokenizer(modules);
            while(tokens.hasMoreTokens()) {
                ArgumentListBuilder cmd = new ArgumentListBuilder();
                cmd.add(DESCRIPTOR.getSvnExe(),"co",/*"-q",*/"--non-interactive");
                if(username!=null)
                    cmd.add("--username",username);
                if(otherOptions!=null)
                    cmd.add(Util.tokenize(otherOptions));
                cmd.add(tokens.nextToken());

                result = run(launcher,cmd,listener,workspace);
                if(!result)
                    return false;
            }
        }

        // write out the revision file
        PrintWriter w = new PrintWriter(new FileOutputStream(getRevisionFile(build)));
        try {
            Map revMap = buildRevisionMap(workspace,listener);
            for (Entry e : revMap.entrySet()) {
                w.println( e.getKey() +'/'+ e.getValue().revision );
            }
        } finally {
            w.close();
        }

        return calcChangeLog(build, changelogFile, launcher, listener);
    }

    /**
     * Output from "svn info" command.
     */
    public static class SvnInfo {
        /** The remote URL of this directory */
        String url;
        /** Current workspace revision. */
        int revision = -1;

        private SvnInfo() {}

        /**
         * Returns true if this object is fully populated.
         */
        public boolean isComplete() {
            return url!=null && revision!=-1;
        }

        public void setUrl(String url) {
            this.url = url;
        }

        public void setRevision(int revision) {
            this.revision = revision;
        }

        /**
         * Executes "svn info" command and returns the parsed output
         *
         * @param subject
         *      The target to run "svn info". Either local path or remote URL.
         */
        public static SvnInfo parse(String subject, Map env, FilePath workspace, TaskListener listener) throws IOException {
            String cmd = DESCRIPTOR.getSvnExe()+" info --xml "+subject;
            listener.getLogger().println("$ "+cmd);

            ByteArrayOutputStream baos = new ByteArrayOutputStream();

            int r = new Proc(cmd,env,baos,workspace.getLocal()).join();
            if(r!=0) {
                // failed. to allow user to diagnose the problem, send output to log
                listener.getLogger().write(baos.toByteArray());
                throw new IOException("svn info failed");
            }

            SvnInfo info = new SvnInfo();

            Digester digester = new Digester();
            digester.push(info);

            digester.addBeanPropertySetter("info/entry/url");
            digester.addSetProperties("info/entry/commit","revision","revision");  // set attributes. in particular @revision

            try {
                digester.parse(new ByteArrayInputStream(baos.toByteArray()));
            } catch (SAXException e) {
                // failed. to allow user to diagnose the problem, send output to log
                listener.getLogger().write(baos.toByteArray());
                e.printStackTrace(listener.fatalError("Failed to parse Subversion output"));
                throw new IOException("Unabled to parse svn info output");
            }

            if(!info.isComplete())
                throw new IOException("No revision in the svn info output");

            return info;
        }

    }

    /**
     * Checks .svn files in the workspace and finds out revisions of the modules
     * that the workspace has.
     *
     * @return
     *      null if the parsing somehow fails. Otherwise a map from module names to revisions.
     */
    private Map buildRevisionMap(FilePath workspace, TaskListener listener) throws IOException {
        PrintStream logger = listener.getLogger();

        Map revisions = new HashMap();

        Map env = createEnvVarMap(false);

        // invoke the "svn info"
        for( String module : getModuleDirNames() ) {
            // parse the output
            SvnInfo info = SvnInfo.parse(module,env,workspace,listener);
            revisions.put(module,info);
            logger.println("Revision:"+info.revision);
        }

        return revisions;
    }

    /**
     * Gets the file that stores the revision.
     */
    private static File getRevisionFile(Build build) {
        return new File(build.getRootDir(),"revision.txt");
    }

    public boolean update(Launcher launcher, FilePath remoteDir, BuildListener listener) throws IOException {
        ArgumentListBuilder cmd = new ArgumentListBuilder();
        cmd.add(DESCRIPTOR.getSvnExe(), "update", /*"-q",*/ "--non-interactive");

        if(username!=null)
            cmd.add(" --username ",username);
        if(otherOptions!=null)
            cmd.add(Util.tokenize(otherOptions));

        StringTokenizer tokens = new StringTokenizer(modules);
        while(tokens.hasMoreTokens()) {
            if(!run(launcher,cmd,listener,new FilePath(remoteDir,getLastPathComponent(tokens.nextToken()))))
                return false;
        }
        return true;
    }

    /**
     * Returns true if we can use "svn update" instead of "svn checkout"
     */
    private boolean isUpdatable(FilePath workspace,BuildListener listener) {
        StringTokenizer tokens = new StringTokenizer(modules);
        while(tokens.hasMoreTokens()) {
            String url = tokens.nextToken();
            String moduleName = getLastPathComponent(url);
            File module = workspace.child(moduleName).getLocal();

            try {
                SvnInfo svnInfo = SvnInfo.parse(moduleName, createEnvVarMap(false), workspace, listener);
                if(!svnInfo.url.equals(url)) {
                    listener.getLogger().println("Checking out a fresh workspace because the workspace is not "+url);
                    return false;
                }
            } catch (IOException e) {
                listener.getLogger().println("Checking out a fresh workspace because Hudson failed to detect the current workspace "+module);
                e.printStackTrace(listener.error(e.getMessage()));
                return false;
            }
        }
        return true;
    }

    public boolean pollChanges(Project project, Launcher launcher, FilePath workspace, TaskListener listener) throws IOException {
        // current workspace revision
        Map wsRev = buildRevisionMap(workspace,listener);

        Map env = createEnvVarMap(false);

        // check the corresponding remote revision
        for (SvnInfo localInfo : wsRev.values()) {
            SvnInfo remoteInfo = SvnInfo.parse(localInfo.url,env,workspace,listener);
            listener.getLogger().println("Revision:"+remoteInfo.revision);
            if(remoteInfo.revision > localInfo.revision)
                return true;    // change found
        }

        return false; // no change
    }

    public ChangeLogParser createChangeLogParser() {
        return new SubversionChangeLogParser();
    }


    public DescriptorImpl getDescriptor() {
        return DESCRIPTOR;
    }

    public void buildEnvVars(Map env) {
        // no environment variable
    }

    public FilePath getModuleRoot(FilePath workspace) {
        String s;

        // if multiple URLs are specified, pick the first one
        int idx = modules.indexOf(' ');
        if(idx>=0)  s = modules.substring(0,idx);
        else        s = modules;

        return workspace.child(getLastPathComponent(s));
    }

    private String getLastPathComponent(String s) {
        String[] tokens = s.split("/");
        return tokens[tokens.length-1]; // return the last token
    }

    static final DescriptorImpl DESCRIPTOR = new DescriptorImpl();

    public static final class DescriptorImpl extends Descriptor {
        /**
         * Path to svn.exe. Null to default.
         */
        private String svnExe;

        DescriptorImpl() {
            super(SubversionSCM.class);
            load();
        }

        protected void convert(Map oldPropertyBag) {
            svnExe = (String)oldPropertyBag.get("svn_exe");
        }

        public String getDisplayName() {
            return "Subversion";
        }

        public SCM newInstance(StaplerRequest req) {
            return new SubversionSCM(
                req.getParameter("svn_modules"),
                req.getParameter("svn_use_update")!=null,
                req.getParameter("svn_username"),
                req.getParameter("svn_other_options")
            );
        }

        public String getSvnExe() {
            String value = svnExe;
            if(value==null)
                value = "svn";
            return value;
        }

        public void setSvnExe(String value) {
            svnExe = value;
            save();
        }

        public boolean configure( StaplerRequest req ) {
            svnExe = req.getParameter("svn_exe");
            return true;
        }

        /**
         * Returns the Subversion version information.
         *
         * @return
         *      null if failed to obtain.
         */
        public Version version(Launcher l, String svnExe) {
            try {
                if(svnExe==null || svnExe.equals(""))    svnExe="svn";

                ByteArrayOutputStream out = new ByteArrayOutputStream();
                l.launch(new String[]{svnExe,"--version"},new String[0],out,FilePath.RANDOM).join();

                // parse the first line for version
                BufferedReader r = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(out.toByteArray())));
                String line;
                while((line = r.readLine())!=null) {
                    Matcher m = SVN_VERSION.matcher(line);
                    if(m.matches())
                        return new Version(Integer.parseInt(m.group(2)), m.group(1));
                }

                // ancient version of subversions didn't have the fixed version number line.
                // or maybe something else is going wrong.
                LOGGER.log(Level.WARNING, "Failed to parse the first line from svn output: "+line);
                return new Version(0,"(unknown)");
            } catch (IOException e) {
                // Stack trace likely to be overkill for a problem that isn't necessarily a problem at all:
                LOGGER.log(Level.WARNING, "Failed to check svn version: {0}", e.toString());
                return null; // failed to obtain
            }
        }

        // web methods

        public void doVersionCheck(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
            // this method runs a new process, so it needs to be protected
            new FormFieldValidator(req,rsp,true) {
                protected void check() throws IOException, ServletException {
                    String svnExe = request.getParameter("exe");

                    Version v = version(new Launcher(TaskListener.NULL),svnExe);
                    if(v==null) {
                        error("Failed to check subversion version info. Is this a valid path?");
                        return;
                    }
                    if(v.isOK()) {
                        ok();
                    } else {
                        error("Version "+v.versionId+" found, but 1.3.0 is required");
                    }
                }
            }.process();
        }
    }

    public static final class Version {
        private final int revision;
        private String versionId;

        public Version(int revision, String versionId) {
            this.revision = revision;
            this.versionId = versionId;
        }

        /**
         * Repository revision ID of this build.
         */
        public int getRevision() {
            return revision;
        }

        /**
         * Human-readable version string.
         */
        public String getVersionId() {
            return versionId;
        }

        /**
         * We use "svn info --xml", which is new in 1.3.0
         */
        public boolean isOK() {
            return revision>=17949;
        }
    }

    private static final Pattern SVN_VERSION = Pattern.compile("svn, .+ ([0-9.]+) \\(r([0-9]+)\\)");

    private static final Logger LOGGER = Logger.getLogger(SubversionSCM.class.getName());
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy