package hudson.plugins.tfs; import static hudson.Util.fixEmpty; import java.io.File; import java.io.IOException; import java.text.ParseException; import java.util.Calendar; import java.util.List; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Pattern; import net.sf.json.JSONObject; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.StaplerRequest; import hudson.AbortException; import hudson.Extension; import hudson.FilePath; import hudson.Launcher; import hudson.Util; import hudson.model.AbstractBuild; import hudson.model.AbstractProject; import hudson.model.BuildListener; import hudson.model.Computer; import hudson.model.Node; import hudson.model.ParametersAction; import hudson.model.Run; import hudson.model.TaskListener; import hudson.plugins.tfs.actions.CheckoutAction; import hudson.plugins.tfs.actions.RemoveWorkspaceAction; import hudson.plugins.tfs.browsers.TeamFoundationServerRepositoryBrowser; import hudson.plugins.tfs.model.WorkspaceConfiguration; import hudson.plugins.tfs.model.Server; import hudson.plugins.tfs.model.ChangeSet; import hudson.plugins.tfs.util.BuildVariableResolver; import hudson.plugins.tfs.util.BuildWorkspaceConfigurationRetriever; import hudson.plugins.tfs.util.BuildWorkspaceConfigurationRetriever.BuildWorkspaceConfiguration; import hudson.scm.ChangeLogParser; import hudson.scm.RepositoryBrowsers; import hudson.scm.SCM; import hudson.scm.SCMDescriptor; import hudson.util.FormValidation; import hudson.util.LogTaskListener; import hudson.util.Scrambler; import org.kohsuke.stapler.QueryParameter; /** * SCM for Microsoft Team Foundation Server. * * @author Erik Ramfelt */ public class TeamFoundationServerScm extends SCM { public static final String WORKSPACE_ENV_STR = "TFS_WORKSPACE"; public static final String WORKFOLDER_ENV_STR = "TFS_WORKFOLDER"; public static final String PROJECTPATH_ENV_STR = "TFS_PROJECTPATH"; public static final String SERVERURL_ENV_STR = "TFS_SERVERURL"; public static final String USERNAME_ENV_STR = "TFS_USERNAME"; private final String serverUrl; private final String projectPath; private final String localPath; private final String workspaceName; private final String userPassword; private final String userName; private final boolean useUpdate; private TeamFoundationServerRepositoryBrowser repositoryBrowser; private transient String normalizedWorkspaceName; private static final Logger logger = Logger.getLogger(TeamFoundationServerScm.class.getName()); @DataBoundConstructor public TeamFoundationServerScm(String serverUrl, String projectPath, String localPath, boolean useUpdate, String workspaceName, String userName, String userPassword) { this.serverUrl = serverUrl; this.projectPath = projectPath; this.useUpdate = useUpdate; this.localPath = (Util.fixEmptyAndTrim(localPath) == null ? "." : localPath); this.workspaceName = (Util.fixEmptyAndTrim(workspaceName) == null ? "Hudson-${JOB_NAME}-${NODE_NAME}" : workspaceName); this.userName = userName; this.userPassword = Scrambler.scramble(userPassword); } // Bean properties need for job configuration public String getServerUrl() { return serverUrl; } public String getWorkspaceName() { return workspaceName; } public String getProjectPath() { return projectPath; } public String getLocalPath() { return localPath; } public boolean isUseUpdate() { return useUpdate; } public String getUserPassword() { return Scrambler.descramble(userPassword); } public String getUserName() { return userName; } // Bean properties END String getWorkspaceName(AbstractBuild<?,?> build, Computer computer) { normalizedWorkspaceName = workspaceName; if (build != null) { normalizedWorkspaceName = substituteBuildParameter(build, normalizedWorkspaceName); normalizedWorkspaceName = Util.replaceMacro(normalizedWorkspaceName, new BuildVariableResolver(build.getProject(), computer)); } normalizedWorkspaceName = normalizedWorkspaceName.replaceAll("[\"/:<>\\|\\*\\?]+", "_"); normalizedWorkspaceName = normalizedWorkspaceName.replaceAll("[\\.\\s]+$", "_"); return normalizedWorkspaceName; } public String getServerUrl(Run<?,?> run) { return substituteBuildParameter(run, serverUrl); } String getProjectPath(Run<?,?> run) { return Util.replaceMacro(substituteBuildParameter(run, projectPath), new BuildVariableResolver(run.getParent())); } private String substituteBuildParameter(Run<?,?> run, String text) { if (run instanceof AbstractBuild<?, ?>){ AbstractBuild<?,?> build = (AbstractBuild<?, ?>) run; if (build.getAction(ParametersAction.class) != null) { return build.getAction(ParametersAction.class).substitute(build, text); } } return text; } @Override public boolean checkout(AbstractBuild build, Launcher launcher, FilePath workspaceFilePath, BuildListener listener, File changelogFile) throws IOException, InterruptedException { Server server = createServer(new TfTool(getDescriptor().getTfExecutable(), launcher, listener, workspaceFilePath), build); WorkspaceConfiguration workspaceConfiguration = new WorkspaceConfiguration(server.getUrl(), getWorkspaceName(build, Computer.currentComputer()), getProjectPath(build), getLocalPath()); // Check if the configuration has changed if (build.getPreviousBuild() != null) { BuildWorkspaceConfiguration nodeConfiguration = new BuildWorkspaceConfigurationRetriever().getLatestForNode(build.getBuiltOn(), build.getPreviousBuild()); if ((nodeConfiguration != null) && nodeConfiguration.workspaceExists() && (! workspaceConfiguration.equals(nodeConfiguration))) { listener.getLogger().println("Deleting workspace as the configuration has changed since a build was performed on this computer."); new RemoveWorkspaceAction(workspaceConfiguration.getWorkspaceName()).remove(server); nodeConfiguration.setWorkspaceWasRemoved(); nodeConfiguration.save(); } } build.addAction(workspaceConfiguration); CheckoutAction action = new CheckoutAction(workspaceConfiguration.getWorkspaceName(), workspaceConfiguration.getProjectPath(), workspaceConfiguration.getWorkfolder(), isUseUpdate()); try { List<ChangeSet> list = action.checkout(server, workspaceFilePath, (build.getPreviousBuild() != null ? build.getPreviousBuild().getTimestamp() : null)); ChangeSetWriter writer = new ChangeSetWriter(); writer.write(list, changelogFile); } catch (ParseException pe) { listener.fatalError(pe.getMessage()); throw new AbortException(); } return true; } @Override public boolean pollChanges(AbstractProject hudsonProject, Launcher launcher, FilePath workspace, TaskListener listener) throws IOException, InterruptedException { Run<?,?> lastRun = hudsonProject.getLastBuild(); if (lastRun == null) { return true; } else { Server server = createServer(new TfTool(getDescriptor().getTfExecutable(), launcher, listener, workspace), lastRun); try { return (server.getProject(getProjectPath(lastRun)).getDetailedHistory( lastRun.getTimestamp(), Calendar.getInstance() ).size() > 0); } catch (ParseException pe) { listener.fatalError(pe.getMessage()); throw new AbortException(); } } } @Override public boolean processWorkspaceBeforeDeletion(AbstractProject<?, ?> project, FilePath workspace, Node node) throws IOException, InterruptedException { Run<?,?> lastRun = project.getLastBuild(); if ((lastRun == null) || !(lastRun instanceof AbstractBuild<?, ?>)) { return true; } // Due to an error in Hudson core (pre 1.321), null was sent in for all invocations of this method // Therefore we try to work around the problem, and see if its only built on one node or not. if (node == null) { while (lastRun != null) { AbstractBuild<?,?> build = (AbstractBuild<?, ?>) lastRun; Node buildNode = build.getBuiltOn(); if (node == null) { node = buildNode; } else { if (!buildNode.getNodeName().equals(node.getNodeName())) { logger.warning("Could not wipe out workspace as there is no way of telling what Node the request is for. Please upgrade Hudson to a newer version."); return false; } } lastRun = lastRun.getPreviousBuild(); } if (node == null) { return true; } lastRun = project.getLastBuild(); } BuildWorkspaceConfiguration configuration = new BuildWorkspaceConfigurationRetriever().getLatestForNode(node, lastRun); if ((configuration != null) && configuration.workspaceExists()) { LogTaskListener listener = new LogTaskListener(logger, Level.INFO); Launcher launcher = node.createLauncher(listener); Server server = createServer(new TfTool(getDescriptor().getTfExecutable(), launcher, listener, workspace), lastRun); if (new RemoveWorkspaceAction(configuration.getWorkspaceName()).remove(server)) { configuration.setWorkspaceWasRemoved(); configuration.save(); } } return true; } protected Server createServer(TfTool tool, Run<?,?> run) { return new Server(tool, getServerUrl(run), getUserName(), getUserPassword()); } @Override public boolean requiresWorkspaceForPolling() { return true; } @Override public boolean supportsPolling() { return true; } @Override public ChangeLogParser createChangeLogParser() { return new ChangeSetReader(); } @Override public FilePath getModuleRoot(FilePath workspace) { return workspace.child(getLocalPath()); } @Override public TeamFoundationServerRepositoryBrowser getBrowser() { return repositoryBrowser; } @Override public void buildEnvVars(AbstractBuild build, Map<String, String> env) { super.buildEnvVars(build, env); if (normalizedWorkspaceName != null) { env.put(WORKSPACE_ENV_STR, normalizedWorkspaceName); } if (env.containsKey("WORKSPACE")) { env.put(WORKFOLDER_ENV_STR, env.get("WORKSPACE") + File.separator + getLocalPath()); } if (projectPath != null) { env.put(PROJECTPATH_ENV_STR, projectPath); } if (serverUrl != null) { env.put(SERVERURL_ENV_STR, serverUrl); } if (userName != null) { env.put(USERNAME_ENV_STR, userName); } } @Override public DescriptorImpl getDescriptor() { return (DescriptorImpl)super.getDescriptor(); } @Extension public static class DescriptorImpl extends SCMDescriptor<TeamFoundationServerScm> { public static final Pattern WORKSPACE_NAME_REGEX = Pattern.compile("[^\"/:<>\\|\\*\\?]+[^\\s\\.\"/:<>\\|\\*\\?]$", Pattern.CASE_INSENSITIVE); public static final Pattern USER_AT_DOMAIN_REGEX = Pattern.compile("^([^\\/\\\\\"\\[\\]:|<>+=;,\\*@]+)@([a-z][a-z0-9.-]+)$", Pattern.CASE_INSENSITIVE); public static final Pattern DOMAIN_SLASH_USER_REGEX = Pattern.compile("^([a-z][a-z0-9.-]+)\\\\([^\\/\\\\\"\\[\\]:|<>+=;,\\*@]+)$", Pattern.CASE_INSENSITIVE); public static final Pattern PROJECT_PATH_REGEX = Pattern.compile("^\\$\\/.*", Pattern.CASE_INSENSITIVE); private String tfExecutable; public DescriptorImpl() { super(TeamFoundationServerScm.class, TeamFoundationServerRepositoryBrowser.class); load(); } public String getTfExecutable() { if (tfExecutable == null) { return "tf"; } else { return tfExecutable; } } @Override public SCM newInstance(StaplerRequest req, JSONObject formData) throws FormException { TeamFoundationServerScm scm = (TeamFoundationServerScm) super.newInstance(req, formData); scm.repositoryBrowser = RepositoryBrowsers.createInstance(TeamFoundationServerRepositoryBrowser.class,req,formData,"browser"); return scm; } public FormValidation doExecutableCheck(@QueryParameter final String value) { return FormValidation.validateExecutable(value); } private FormValidation doRegexCheck(final Pattern[] regexArray, final String noMatchText, final String nullText, String value) { value = fixEmpty(value); if (value == null) { if (nullText == null) { return FormValidation.ok(); } else { return FormValidation.error(nullText); } } for (Pattern regex : regexArray) { if (regex.matcher(value).matches()) { return FormValidation.ok(); } } return FormValidation.error(noMatchText); } public FormValidation doUsernameCheck(@QueryParameter final String value) { return doRegexCheck(new Pattern[]{DOMAIN_SLASH_USER_REGEX, USER_AT_DOMAIN_REGEX}, "Login name must contain the name of the domain and user", null, value ); } public FormValidation doProjectPathCheck(@QueryParameter final String value) { return doRegexCheck(new Pattern[]{PROJECT_PATH_REGEX}, "Project path must begin with '$/'.", "Project path is mandatory.", value ); } public FormValidation doWorkspaceNameCheck(@QueryParameter final String value) { return doRegexCheck(new Pattern[]{WORKSPACE_NAME_REGEX}, "Workspace name cannot end with a space or period, and cannot contain any of the following characters: \"/:<>|*?", "Workspace name is mandatory", value); } @Override public boolean configure(StaplerRequest req, JSONObject formData) throws FormException { tfExecutable = Util.fixEmpty(req.getParameter("tfs.tfExecutable").trim()); save(); return true; } @Override public String getDisplayName() { return "Team Foundation Server"; } } }