Tuesday, June 14, 2016

Migrating away from YouTrack - Export all your Issues......

Are you a JetBrains YouTrack user?  It is probably one of the best issue tracking systems out there and I have tried a few.

The current one that seems to be in vogue at the moment is JIRA, and we are migrating across. On the whole YouTrack is easier to use, however there are probably some advantages (workflows, reporting) where JIRA may shine.

In any case if you want a text dump of all the information you have stored in YouTrack, there is a great article on this on Gerben's blog:


This gave me the head start I needed, and pasting that into VS 2015 had me most of the way there. Instead of displaying the info in a console window, I actually wanted to save this info into a discrete file per issue, in the format of:

XX-1 - Issue summary.txt

So the project short code, issue number, and summary in the file name, then the same details in the file with the description and date, a list of all the comments when they were added and by whom and then a list of the attachments.

It might have been nice to export all the attachments as well, but for our purposes we are good just to have the issue text available to search through. We aren't importing this anywhere, it's more a case of being able to see what we did in the past...

Two things you need to do first:

1) Enable REST API in your YouTrack settings page.
2) Add the "YouTrackSharp" NuGet package to your project.

Then paste in the following code and update the Username and Password to whatever you are using to log in. Use an admin login with access to all the projects if possible.

using System;
using System.IO;
using System.Collections.Generic;
using YouTrackSharp.Infrastructure;
using YouTrackSharp.Projects;
using YouTrackSharp.Issues;

namespace LesMills.YouTrackExporter
    class Program
        static void Main(string[] args)
            String Username = "username";
            String Password = "password";
            String Site = "your-company.myjetbrains.com";
            string saveDirectory = "youtrack-issues";
            string separatorLine = "------------------------------------------------------------------------------------------------------------------------------";
            DateTime start = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);

            YouTrackSharp.Infrastructure.Connection connection;
            YouTrackSharp.Projects.ProjectManagement ProjectManager;
            IEnumerable<YouTrackSharp.Projects.Project> Projects;
            YouTrackSharp.Issues.IssueManagement IssueManager;
            IEnumerable<YouTrackSharp.Issues.Issue> Issues;
            IEnumerable<YouTrackSharp.Issues.Comment> Comments;

            connection = new Connection(Site, 80, false, "youtrack");
            connection.Authenticate(Username, Password);

            // directory to save our you tracks

            ProjectManager = new ProjectManagement(connection);
            Projects = ProjectManager.GetProjects();
            foreach (Project project in Projects)
                Console.WriteLine(string.Format("Found project {0} - {1} ", project.ShortName, project.Name));
                IssueManager = new IssueManagement(connection);
                Issues = IssueManager.GetAllIssuesForProject(project.ShortName);

                //An issue in youtrack can have many fields that we dont know at compile time.
                //Therefore its a .Net DynamicObject
                foreach (dynamic Issue in Issues)
                        Console.WriteLine(String.Format("\tFound issue {0}", Issue.Id));

                        string fileName = Issue.Id + " " + Issue.Summary;
                        fileName = MakeValidFileName(Truncate(fileName,150));
                        fileName = saveDirectory + "\\" + fileName + ".txt";
                        // if file already exists skip ahead
                        if (File.Exists(fileName)) continue;

                        using (StreamWriter file = new StreamWriter(fileName))
                            DateTime issueDate = start.AddMilliseconds(Issue.Created).ToLocalTime();

                            file.WriteLine("ID      : " + Issue.Id.ToString());
                            file.WriteLine("TYPE    : " + Issue.Type.ToString());
                            file.WriteLine("CREATED : " + issueDate.ToLongDateString() + " " + issueDate.ToLongTimeString());

                            Comments = IssueManager.GetCommentsForIssue(Issue.Id);

                            foreach (Comment Comment in Comments)
                                DateTime commentDate = start.AddMilliseconds(Comment.Created).ToLocalTime();

                                string commentHeading = String.Format("COMMENT: {0} - {1}\n", Comment.Author, commentDate.ToLongDateString() + " " + commentDate.ToLongTimeString());

                            foreach (dynamic Attachment in Issue.Attachments)
                                String Name = Attachment.name;
                                String Url = Attachment.url;
                                String Author = Attachment.authorLogin;
                                String Id = Attachment.id;
                                String Group = Attachment.group;
                                long Created = Attachment.created;
                                DateTime attachmentDate = start.AddMilliseconds(Created).ToLocalTime();

                                string attachmentHeading = String.Format("ATTACHMENT: {0} - {1}\n", Author, attachmentDate.ToLongDateString() + " " + attachmentDate.ToLongTimeString());
                                file.WriteLine("NAME : " + Name.ToString());
                                file.WriteLine("URL  : " + Url.ToString());

                                // if we want to download attachment file - but need to authenticate first
                                //webClient.DownloadFile(Url, "c:\\temp\\" + Name);
                    catch(Exception ex)
                        Console.WriteLine("Exception : " + ex.Message);

        /// <summary>
        /// Clean up any special chars out of the potential filename
        /// </summary>
        /// <param name="name"></param>
        /// <returns></returns>
        private static string MakeValidFileName(string name)
            string invalidChars = System.Text.RegularExpressions.Regex.Escape(new string(Path.GetInvalidFileNameChars()));
            string invalidRegStr = string.Format(@"([{0}]*\.+$)|([{0}]+)", invalidChars);

            return System.Text.RegularExpressions.Regex.Replace(name, invalidRegStr, "_");

        /// <summary>
        /// Filename can't be more than 260 chars, we use this to limit name size
        /// </summary>
        /// <param name="value"></param>
        /// <param name="maxLength"></param>
        /// <returns></returns>
        public static string Truncate(string value, int maxLength)
            if (string.IsNullOrEmpty(value)) return value;
            return value.Length <= maxLength ? value : value.Substring(0, maxLength);

This will then create a directory called youtrack-issues.

Then run the program.

Watch the console output command window and your folder as the program iterates over all your projects and dumps each one out into its own file.

I had a lot of exceptions. It seems every so often WriteLine will get into a strange state? In any case if you run the program again it will then work ok and carry on until there is another exception later on. If anyone knows what is going on please add in the comments below. However after about 20 runs I had all the issues saved out...

No comments:

Post a Comment