Repository: gocd/gocd
Issue: handle special characters in material name in pipeline label template

In XSD [here](https://github.com/gocd/gocd/blob/master/config/config-server/resources/cruise-config.xsd#L715) the label template allows special characters like `-` in material name inside `${ }`. But [here](https://github.com/gocd/gocd/blob/master/config/config-api/src/com/thoughtworks/go/domain/label/PipelineLabel.java#L45) we don't match them in material name.

causes `${git-repo}` to not get resolved to revision.


Changed files:
config/config-api/src/com/thoughtworks/go/domain/label/PipelineLabel.java, common/test/unit/com/thoughtworks/go/domain/PipelineLabelTest.java

diff --git a/common/test/unit/com/thoughtworks/go/domain/PipelineLabelTest.java b/common/test/unit/com/thoughtworks/go/domain/PipelineLabelTest.java
index 36e2d48f34..7a834ee52a 100644
--- a/common/test/unit/com/thoughtworks/go/domain/PipelineLabelTest.java
+++ b/common/test/unit/com/thoughtworks/go/domain/PipelineLabelTest.java
@@ -19,6 +19,7 @@ package com.thoughtworks.go.domain;
 import com.thoughtworks.go.config.CaseInsensitiveString;
 import com.thoughtworks.go.config.materials.ScmMaterial;
 import com.thoughtworks.go.config.materials.mercurial.HgMaterial;
+import com.thoughtworks.go.config.materials.svn.SvnMaterial;
 import com.thoughtworks.go.domain.label.PipelineLabel;
 import com.thoughtworks.go.domain.materials.Modification;
 import com.thoughtworks.go.helper.MaterialsMother;
@@ -221,7 +222,7 @@ public class PipelineLabelTest {
     public void canMatchWithOneTruncationAsFirstRevision() throws Exception {
         final String[][] expectedGroups = { {"git", "4"}, { "svn" } };
         String res = assertLabelGroupsMatchingAndReplace("release-${git[:4]}-${svn}", expectedGroups);
-        assertThat(res, is("release-" + GIT_REVISION.substring(0, 4)  + "-" + SVN_REVISION));
+        assertThat(res, is("release-" + GIT_REVISION.substring(0, 4) + "-" + SVN_REVISION));
     }
 
     @Test
@@ -231,6 +232,36 @@ public class PipelineLabelTest {
         assertThat(res, is("release-" + GIT_REVISION.substring(0, 5) + "-" + SVN_REVISION.substring(0, 3)));
     }
 
+    @Test
+    public void canNotMatchWithTruncationWhenMaterialNameHasAColon() throws Exception {
+        final String[][] expectedGroups = { { "git:one", "7" } };
+        String res = assertLabelGroupsMatchingAndReplace("release-${git:one[:7]}", expectedGroups);
+        assertThat(res, is("release-${git:one[:7]}"));
+    }
+
+    @Test
+    public void shouldReplaceTheTemplateWithSpecialCharacters() throws Exception {
+        ensureLabelIsReplaced("SVNMaterial");
+        ensureLabelIsReplaced("SVN-Material");
+        ensureLabelIsReplaced("SVN_Material");
+        ensureLabelIsReplaced("SVN!Material");
+        ensureLabelIsReplaced("SVN__##Material_1023_WithNumbers");
+        ensureLabelIsReplaced("SVN_Material-_!!_");
+        ensureLabelIsReplaced("svn_Material'WithQuote");
+        ensureLabelIsReplaced("SVN_Material.With.Period");
+        ensureLabelIsReplaced("SVN_Material#With#Hash");
+        ensureLabelIsReplaced("SVN_Material:With:Colon");
+        ensureLabelIsReplaced("SVN_Material~With~Tilde");
+
+        ensureLabelIsNOTReplaced("SVN*MATERIAL");
+        ensureLabelIsNOTReplaced("SVN+Material");
+        ensureLabelIsNOTReplaced("SVN^Material");
+        ensureLabelIsNOTReplaced("SVN_Material(With)Parentheses");
+        ensureLabelIsNOTReplaced("SVN_Material{With}Braces");
+        ensureLabelIsNOTReplaced("SVN**Material");
+        ensureLabelIsNOTReplaced("SVN\\\\Material_With_Backslash");
+    }
+
     @BeforeClass
     public static void setup() {
         MATERIAL_REVISIONS.put(new CaseInsensitiveString("svnRepo.verynice"), SVN_REVISION);
@@ -285,4 +316,23 @@ public class PipelineLabelTest {
         }
         return builder.toString();
     }
+
+    private void ensureLabelIsReplaced(String name) {
+        PipelineLabel label = getReplacedLabelFor(name, String.format("release-${%s}", name));
+        assertThat(label.toString(), is("release-" + ModificationsMother.currentRevision()));
+    }
+
+    private void ensureLabelIsNOTReplaced(String name) {
+        String labelFormat = String.format("release-${%s}", name);
+        PipelineLabel label = getReplacedLabelFor(name, labelFormat);
+        assertThat(label.toString(), is(labelFormat));
+    }
+
+    private PipelineLabel getReplacedLabelFor(String name, String labelFormat) {
+        MaterialRevisions materialRevisions = ModificationsMother.oneUserOneFile();
+        PipelineLabel label = PipelineLabel.create(labelFormat);
+        ((SvnMaterial) materialRevisions.getRevisions().get(0).getMaterial()).setName(new CaseInsensitiveString(name));
+        label.updateLabel(materialRevisions.getNamedRevisions());
+        return label;
+    }
 }
diff --git a/config/config-api/src/com/thoughtworks/go/domain/label/PipelineLabel.java b/config/config-api/src/com/thoughtworks/go/domain/label/PipelineLabel.java
index 0b77ba92da..132a09b249 100644
--- a/config/config-api/src/com/thoughtworks/go/domain/label/PipelineLabel.java
+++ b/config/config-api/src/com/thoughtworks/go/domain/label/PipelineLabel.java
@@ -42,7 +42,7 @@ public class PipelineLabel implements Serializable {
         return label;
     }
 
-    public static final Pattern PATTERN = Pattern.compile("(?i)\\\\$\\\\{([\\\\w\\\\.]+)(\\\\[:(\\\\d+)\\\\])?\\\\}");
+    public static final Pattern PATTERN = Pattern.compile("(?i)\\\\$\\\\{([a-zA-Z0-9_\\\\-\\\\.!~'#:]+)(\\\\[:(\\\\d+)\\\\])?\\\\}");
 
     private String replaceRevisionsInLabel(Map<CaseInsensitiveString, String> materialRevisions) {
         final Matcher matcher = PATTERN.matcher(this.label);