Over the last few days I’ve been trying out Redmine as a replacement for Basecamp, which I have been using for the past year or so. Redmine has a new (and more or less undocumented) REST API for working with issues, which opens up some interesting possibilities for connecting to other systems.

Something I’ve been wanting to try for a while is better handling of test related tasks and todo comments, which is where the code below comes in - everything ends up in the issue tracker and can be assigned/prioritised as for regular issues.

I have TeamCity set up to build the project and produce an xml test report whenever code changes in subversion. When the test report changes, the sync app gets all tasks of the appropriate type from Redmine (I set up two new trackers, “Test” and “Todo”), compares them to what is found in the code and test report, then updates Redmine as needed.

Note that this is proof of concept, works-on-my-machine code - It shouldn’t be too hard to make it do what you want, but there are no guarantees.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Text.RegularExpressions;
using System.Net;
using System.Xml;

namespace OrangeGuava.TaskSync
{
enum TaskType
{
    Test,
    Todo
}

class Task
{
    public int? Id { get; set; }
    public TaskType Type { get; set; }
    public string Filename { get; set; }
    public string Result { get; set; }
    public int Line { get; set; }
    public string Text { get; set; }
    public int Status { get; set; }
}

class Program
{

    static int TRACKER_TODO = 5;
    static int TRACKER_TEST = 4;
    static string ServerUrl = "http://redmineURL";
    static int ProjectId = 1;
    static string basepath = //@"C:\code\twoneeds\";
    @"C:\TeamCity\buildAgent\work\597b948abdcc6ce0\";
    static string login = "TaskSync";
    static string password = "[password]";


    static void UpdateTask(Task t)
    {

        var request = (HttpWebRequest)HttpWebRequest.Create(
            string.Format(
            "{0}/issues/{1}.xml", ServerUrl, t.Id));

        string cre = String.Format("{0}:{1}", login, password);
        byte[] bytes = Encoding.ASCII.GetBytes(cre);
        string base64 = Convert.ToBase64String(bytes);
        request.Headers.Add("Authorization", "Basic " + base64);
        request.ContentType = "application/xml";

        request.Method = "PUT";

        var reqsw = new StreamWriter(request.GetRequestStream());

        WriteTask(reqsw, t);

        reqsw.Close();

        var response = request.GetResponse();
        response.Close();


    }

    static void WriteTask(StreamWriter reqsw, Task t)
    {
        XmlDocument doc = new XmlDocument();
        doc.AppendChild(doc.CreateElement("issue"));
        doc.DocumentElement.SetAttribute("project_id", ProjectId.ToString());
        doc.DocumentElement.SetAttribute("tracker_id", (t.Type == TaskType.Test ? TRACKER_TEST : TRACKER_TODO).ToString());
        doc.DocumentElement.SetAttribute("status_id", t.Status.ToString());

        if (t.Type == TaskType.Todo)
        {
            var fn = t.Filename;
            if (fn.Contains('/')) fn = fn.Substring(fn.LastIndexOf('/'));

            var subj = doc.CreateElement("subject");
            doc.DocumentElement.AppendChild(subj);
            subj.InnerText = fn + ":" + t.Line + " - " + t.Text;

            var desc = doc.CreateElement("description");
            doc.DocumentElement.AppendChild(desc);
            desc.InnerText = t.Filename + "\n" + t.Line + "\n" + t.Text;
        }
        else
        {

            var subj = doc.CreateElement("subject");
            doc.DocumentElement.AppendChild(subj);
            subj.InnerText = t.Result + ": " + t.Filename;

            var desc = doc.CreateElement("description");
            doc.DocumentElement.AppendChild(desc);
            desc.InnerText = t.Filename + "\n" + t.Result + "\n" + t.Text;
        }

        reqsw.Write(doc.DocumentElement.OuterXml);

    }

    static void CreateTask(Task t) {

        var request = (HttpWebRequest)HttpWebRequest.Create(
            string.Format(
            "{0}/issues.xml", ServerUrl));

        string cre = String.Format("{0}:{1}", login, password);
        byte[] bytes = Encoding.ASCII.GetBytes(cre);
        string base64 = Convert.ToBase64String(bytes);
        request.Headers.Add("Authorization", "Basic " + base64);
        request.ContentType= "application/xml";

        request.Method = "POST";

        var reqsw = new StreamWriter(request.GetRequestStream());

        WriteTask(reqsw, t);

        reqsw.Close();

        var response = request.GetResponse();
        response.Close();

    }

    static List GetTasksFromIssueTracker(TaskType type, int page)
    {
        var result = new List();

        var request = (HttpWebRequest)HttpWebRequest.Create(
            string.Format(
            "{0}/issues.xml?project_id={1}&tracker_id={2}&status_id=*&page={3}",
            ServerUrl,
            ProjectId,
            type == TaskType.Test ? TRACKER_TEST : TRACKER_TODO,
            page
            ));
        //request.Credentials = new NetworkCredential("tqc01", "asdf12");
        string cre = String.Format("{0}:{1}", login, password);
        byte[] bytes = Encoding.ASCII.GetBytes(cre);
        string base64 = Convert.ToBase64String(bytes);
        request.Headers.Add("Authorization", "Basic " + base64);

        var response = (HttpWebResponse)request.GetResponse();

        StreamReader reader = new StreamReader(response.GetResponseStream());
        string xml = reader.ReadToEnd();
        response.Close();

        XmlDocument doc = new XmlDocument();
        doc.LoadXml(xml);

        var total = int.Parse(doc.DocumentElement.GetAttribute("count"));
        var pages = (int)Math.Ceiling(total / 25.0);

        var issueNodes = doc.DocumentElement.SelectNodes("//issue");

        foreach (XmlElement issueNode in issueNodes)
        {
            var task = new Task();
            task.Type = type;
            task.Id = int.Parse(issueNode.SelectSingleNode("./id").InnerText);
            task.Status = int.Parse(((XmlElement)issueNode.SelectSingleNode("./status")).GetAttribute("id"));
            //task.Text = issueNode.SelectSingleNode("./subject").InnerText;
            var description = issueNode.SelectSingleNode("./description").InnerText;
            var ds = description.Split('\n');
            if (type == TaskType.Todo)
            {
                task.Filename = ds[0];
                task.Line = int.Parse(ds[1]);
                task.Text = (ds[2]);
            }

            if (type == TaskType.Test)
            {
                task.Filename = ds[0].Trim();
                task.Result = (ds[1]).Trim();
                task.Text = description.Substring(description.IndexOf('\n', description.IndexOf('\n')+1) + 1);
            }
            result.Add(task);
        }


        if (page < pages)
        {
            result.AddRange(GetTasksFromIssueTracker(type, page + 1));
        }
        return result;
    }

    static List GetTasksFromCode()
    {
        var result = new List();


        var files = Directory.GetFiles(basepath, "*.cs", SearchOption.AllDirectories);

        foreach (var fn in files)
        {
            //Console.WriteLine(fn);
            var lines = File.ReadAllLines(fn);
            for (int i = 0; i  DateTime.Now.AddSeconds(-30)) return;
        lastchange = DateTime.Now;

        Console.WriteLine("Changed");
        SyncTests(e.FullPath);
        SyncToDo();
        Console.WriteLine("Sync complete - waiting for change");
    }
    }
}