Question

I am running a subversion server, a TeamCity server and a Youtrack server. At the moment these are all connected, which means that whenever I commit something, I add a youtrack issue number on the commit message and that changeset appears under the issue in Youtrack, with the help of TeamCity that acts as the middle man. What I am now trying to achieve is to block all commits that are not linked against an open issue in youtrack.

I've seen similar functionality existing in other version control systems and ticket management systems. I noticed that Youtrack has a rest api interface, so that may be (part of?) the solution.

Was it helpful?

Solution

I ended up implementing a solution myself.

Basically, a pre-commit hook is a tiny program that either exits with 0 (and by doing so the commit gets accepted) or it exits with another value (and by doing so the commit gets rejected.

I wrote a batch script that calls a runnable jar that logs in to Youtrack rest api and gets all the details of the ticket mentioned in the commit message. If that ticket exists, and it has the state that we want it to have, then the java application exits with the value 0, causing the batch script to exit with the same value and accept the commit. If on the other hand something isn't right, the java application exits with another value causing the commit to get rejected.

The batch file for my visual SVN repository is the following:

@set echo off
setlocal enabledelayedexpansion

rem Subversion sends through the path to the repository and transaction id  
set REPOS=%1  
set TXN=%2           
rem get the commit message from svn server
for /f "delims= " %%a in ('"C:\Program Files\VisualSVN Server\bin\svnlook" log %REPOS% -t %TXN%') do (
@set COMMIT_MSG=%%a
rem call the java jar that performs the api call to match the commit message against an open ticket
java -jar C:\Users\Administrator\Desktop\Repositories\MyProject\hooks\preCommitHook-with-dependencies.jar !COMMIT_MSG!
rem if java returns System.exit(0) then we accept the commit. Otherwise print out a failure message and decline it.
echo !COMMIT_MSG! 1>&2
echo !errorlevel! 1>&2
if !errorlevel! gtr 0 (goto err) else exit 0  
)

:err
echo ===================================================================== 1>&2
echo Your commit has been rejected. This is because the issue you assigned 1>&2
echo on it does not exist or is not "In Progress" state. Please try again. 1>&2  
echo ===================================================================== 1>&2
exit 1

As for Java, there's 2 interesting things: one is the class itself that does all the rest calls, and the other is the pom.xml where we make the JAR to include all the dependencies.

package com.myproject;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Component;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

@Component
public class JarStart {

    private static final String REST_API_LOGIN_URL = "http://yourServerIP:yourServerPort/rest/user/login";
    private static final String REST_API_ISSUE_URL = "http:/yourServerIP:yourServerPort/rest/issue/";
    private static final String IN_PROGRESS = "In Progress";
    private RestTemplate restTemplate;

    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("spring-context.xml");
        JarStart jarStart = context.getBean(JarStart.class);
        try {
            jarStart.init(args[0]);
        } catch (Throwable e) {
            //if anything goes wrong the commit gets declined with an error code of 12
            System.exit(12);
        }
    }

    private void init(String issueId) {
        restTemplate = new RestTemplate();
        String cookies = login();
        String responseWithIssueDetails = getIssueDetails(issueId, cookies);
        String issueState = getIssueState(responseWithIssueDetails);
        decideHowToExit(issueState);
    }

    private String login() {
        MultiValueMap<String, String> map = new LinkedMultiValueMap<String, String>();
        map.add("login", "YourUsername");
        map.add("password", "YourPassword");
        HttpEntity<MultiValueMap<String, String>> entity = new HttpEntity<MultiValueMap<String, String>>(map, null);
        HttpEntity<String> loginResponse = restTemplate.exchange(REST_API_LOGIN_URL, HttpMethod.POST, entity, String.class);
        return loginResponse.getHeaders().get("Set-Cookie").toString();
    }

    private String getIssueDetails(String issueId, String cookies) {
        HttpHeaders headers = createHeadersWithAuthentication(cookies);
        HttpEntity newEntity = new HttpEntity(headers);
        return restTemplate.exchange(REST_API_ISSUE_URL + issueId, HttpMethod.GET, newEntity, String.class).getBody();
    }

    private HttpHeaders createHeadersWithAuthentication(String cookies) {
        HttpHeaders headers = new HttpHeaders();
        headers.add("Cookie", cookies);
        headers.add("Accept", "application/json");
        headers.add("Cache-Control", "no-cache");
        return headers;
    }

    private String getIssueState(String responseWithIssueDetails) {
        Pattern pattern = Pattern.compile(".*State\",\"value\":\\[\"([a-zA-Z ]*)");
        Matcher matcher = pattern.matcher(responseWithIssueDetails);
        matcher.find();
        return matcher.group(1);
    }

    private void decideHowToExit(String issueState) {
        if (IN_PROGRESS.equals(issueState)) {
            System.exit(0);
        } else {
            System.exit(1);
        }
    }
}

pom.xml:

http://maven.apache.org/maven-v4_0_0.xsd">

<modelVersion>4.0.0</modelVersion>

<artifactId>preCommitHook</artifactId>
<packaging>jar</packaging>
<version>1.0-SNAPSHOT</version>

<properties>
    <spring.version>3.2.5.RELEASE</spring.version>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-web</artifactId>
        <version>${spring.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-core</artifactId>
        <version>${spring.version}</version>
    </dependency>
</dependencies>
<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-shade-plugin</artifactId>
            <version>2.2</version>
            <executions>
                <execution>
                    <phase>package</phase>
                    <goals>
                        <goal>shade</goal>
                    </goals>
                    <configuration>
                        <finalName>preCommitHook-with-dependencies</finalName>
                        <transformers>
                            <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                <mainClass>com.yourproject.JarStart</mainClass>
                            </transformer>
                            <transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
                                <resource>META-INF/spring.handlers</resource>
                            </transformer>
                            <transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
                                <resource>META-INF/spring.schemas</resource>
                            </transformer>
                        </transformers>
                    </configuration>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top