Tuesday, February 9, 2010

Tortoise SVN Plugin for issue tracking system

In the field of software development it is enssential that source code is managed through a version control system. Without that, you loose an easy way to track changes and versions of your project.
(But discuss that is not the intention of this blog post, so skip comments about that.)

On the other hand, a version control system is not the right tool to manage project dependent things, feature requests and to track bugs. So professional teams use also a project management tools in combinition with bug tracking systems.
(To discuss the pro an cons of different systems is also not the intention of this post, so ...)

And now, one question:  
What it will be, if you combine these applications and let them communicate in two ways?

Right! A powerful combination of tools that makes the live much easier for software developer and project managers 8) so it's obvious to combine them.

How to realize that depends on the interfaces that both systems publish to manage data.

From my point of view, I will present a way to do this with Subversion esp. TortoiseSVN and Redmine. Tortoise offers an easy COM-Interface to integrate plugins for bug tracking system interaction.

Let's start with the interface.

To integrate your plugin you have to implement the IBugTraqProvider and the IBugTraqProvider2 interface supplied by TortoiseSVN and available also as source and binary within the Subversion repository of Tortoise: TortoiseSVN-Repository (login data available at project page of Tortoise at Source code section).

Lets take a look at the IBugTraqProvider.cs: (condensed and reduced to needed)
[codeformatting with manoli.net]

   1:  public interface IBugTraqProvider
   2:  {
   3:     // validates parameters
   4:     bool ValidateParameters(IntPtr hParentWnd, string parameters);
   5:     // gets the button text
   6:     string GetLinkText(IntPtr hParentWnd, string parameters);
   7:     // get the commit message
   8:     string GetCommitMessage(IntPtr hParentWnd, string parameters, string commonRoot, string[] pathList, string originalMessage);
   9:  }
  10:  public interface IBugTraqProvider2 : IBugTraqProvider
  11:  {
  12:     // validates parameters
  13:     new bool ValidateParameters(IntPtr hParentWnd, string parameters);
  14:     // gets the button text
  15:     new string GetLinkText(IntPtr hParentWnd, string parameters);
  16:     // get the commit message
  17:     new string GetCommitMessage(IntPtr hParentWnd, string parameters, string commonRoot, string[] pathList, string originalMessage);
  18:     // get the commit message
  19:     string GetCommitMessage2(IntPtr hParentWnd, string parameters, string commonURL, string commonRoot, string[] pathList, string originalMessage, string bugID, out string bugIDOut, out string[] revPropNames, out string[] revPropValues);
  20:     // checks the commit conditions
  21:     string CheckCommit(IntPtr hParentWnd, string parameters, string commonURL, string commonRoot, string[] pathList, string commitMessage);
  22:     // called after commit
  23:     string OnCommitFinished(IntPtr hParentWnd, string commonRoot, string[] pathList, string logMessage, int revision);
  24:     // indicates that plugin has options
  25:     bool HasOptions();
  26:     // opens the options dialog
  27:     string ShowOptionsDialog(IntPtr hParentWnd, string parameters);
  28:  }

Information about every single parameters are provided at the documentation. Here (other languages here)

So first, just let us provide the button text.  
Attention: without that or error occuring within this method the button will not be shown! 

   1:  [ComVisible(true), Guid(/* GUID-GOES-HERE */), ClassInterface(ClassInterfaceType.None)]
   2:  public class RedmineTraqProvider : Interop.BugTraqProvider.IBugTraqProvider, Interop.BugTraqProvider.IBugTraqProvider2
   3:  {
   4:     //[...]
   5:     string GetLinkText(IntPtr hParentWnd, string parameters) { return "Get Entry"; }
   6:     //[...]
   7:  }

From my choice, I set the project URL to Redmine at the options dialog. My Parameters are encapsulated within a specialiced class that handles serialization and deserialization of the parameter string.

Now it's time to take a look at the main function, GetCommitMessage2. Here lays the main functionality of the PlugIn.

As the first step, you have to fetch the data from Redmine. I've choosen the CVS way, because of it provides more information than the atom way.

So I designed a small and handy data object:

   1:  public class TicketCollection : List { }
   2:   
   3:  public class Ticket
   4:  {
   5:    public string Id { get; private set; }
   6:    public string Title { get; private set; }
   7:    public string Details { get; set; }
   8:    public Ticket(string id, string title)
   9:    {
  10:       this.Id = id;
  11:       this.Title = title;
  12:    }
  13:  }

At GetCommitMessage2 I push this into my main form so the user could select the issue for this commit.

   1:  using (MainForm form = new MainForm())
   2:  {
   3:     form.Tickets.AddRange(dataProvider.Tickets);
   4:     if (DialogResult.OK == form.ShowDialog())
   5:     {
   6:        // some nasty things
   7:     }
   8:  }

But how to get the data?

As said, I have choosen the CVS way, so let's get this via WebClient.

   1:  TicketCollection tickets = new TicketCollection();
   2:   
   3:  using (WebClient client = new WebClient())
   4:  {
   5:     string data = client.DownloadString(uri);
   6:     using (StringReader reader = new StringReader(data))
   7:     {
   8:        // skip columndefinition
   9:        string line = reader.ReadLine();
  10:        while (null != (line = reader.ReadLine()))
  11:        {
  12:           string[] columnsData = line.Split(';');
  13:           tickets.Add(new Ticket(
  14:              columnsData[0], // ID
  15:              columnsData[5]) // title
  16:              {
  17:                 Details = columnsData[columnsData.Length - 1] // description
  18:              };
  19:        }
  20:     }
  21:  }

Now you got the data. The only thing left is to return the commit message, but that's trivial.
Now merge this together. Here is the whole GetCommitMessage2:

   1:  public string GetCommitMessage2(IntPtr hParentWnd, string parameters, 
   2:     string commonURL, string commonRoot, string[] pathList, 
   3:     string originalMessage, string bugID, out string bugIDOut, 
   4:     out string[] revPropNames, out string[] revPropValues)
   5:  {
   6:     TicketCollection tickets = new TicketCollection();
   7:   
   8:     using (WebClient client = new WebClient())
   9:     {
  10:        string data = client.DownloadString(uri);
  11:        using (StringReader reader = new StringReader(data))
  12:        {
  13:           // skip columndefinition
  14:           string line = reader.ReadLine();
  15:           while (null != (line = reader.ReadLine()))
  16:           {
  17:              string[] columnsData = line.Split(';');
  18:              tickets.Add(new Ticket(
  19:                 columnsData[0], // ID
  20:                 columnsData[5]) // title
  21:                 {
  22:                    Details = columnsData[columnsData.Length - 1] // description
  23:                 };
  24:           }
  25:        }
  26:     }
  27:   
  28:     using (MainForm form = new MainForm())
  29:     {
  30:        form.Tickets.AddRange(tickets);
  31:   
  32:        if (DialogResult.OK == form.ShowDialog())
  33:        {
  34:           string[] ids = form.SelectedTickets.Select(p => p.Id).ToArray();
  35:   
  37:           return String.Concat(
  38:              originalMessage,
  39:              Environment.NewLine,
  40:              "IssueId ",
  41:              String.Join(", ", form.SelectedTickets.Select(ticket => String.Format("#{0}", ticket.Id)).ToArray()));
  42:           );
  43:        }
  44:     }
  45:     return originalMessage;
  46:  }

Update: Project is now hosted on

1 comment: