Skip to content

Commit 68850ef

Browse files
Merge pull request #1135 from solidify/feature/jira-api-v3
Feature/jira api v3
2 parents a88584d + 219d278 commit 68850ef

File tree

12 files changed

+189
-32
lines changed

12 files changed

+189
-32
lines changed

docs/Samples/config-agile.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"target-project": "TargetProject",
44
"query": "project=ProjectName ORDER BY Rank ASC",
55
"using-jira-cloud": true,
6+
"jira-api-version": 3,
67
"workspace": "C:\\Temp\\JiraExport\\",
78
"epic-link-field": "Epic Link",
89
"sprint-field": "Sprint",

docs/Samples/config-basic.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"target-project": "TargetProject",
44
"query": "project=ProjectName ORDER BY Prio ASC",
55
"using-jira-cloud": true,
6+
"jira-api-version": 3,
67
"workspace": "C:\\Temp\\JiraExport\\",
78
"epic-link-field": "Epic Link",
89
"sprint-field": "Sprint",

docs/Samples/config-cmmi.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"target-project": "TargetProject",
44
"query": "project=SourceProject ORDER BY Rank ASC",
55
"using-jira-cloud": false,
6+
"jira-api-version": 3,
67
"workspace": "C:\\Temp\\JiraExport\\",
78
"epic-link-field": "Epic Link",
89
"sprint-field": "Sprint",

docs/Samples/config-scrum.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"target-project": "TargetProject",
44
"query": "project=ProjectName ORDER BY Rank ASC",
55
"using-jira-cloud": true,
6+
"jira-api-version": 3,
67
"workspace": "C:\\Temp\\JiraExport\\",
78
"epic-link-field": "Epic Link",
89
"sprint-field": "Sprint",

docs/config.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ The migration configuration file is defined in a json file with the properties d
2121
|**target-project**|True|string|Name of the project to migrate to.|
2222
|**query**|True|string|Name of the JQL query to use for identifying work items to migrate.|
2323
|**using-jira-cloud**|False|boolean|Set to False if connected to Jira Server instance, by default it is True|
24+
|**jira-api-version**|False|integer|Version of the Jira API to use. Must be either **2** or **3**. Default: 3.|
2425
|**workspace**|True|string|Location where logs and export data are saved on disk.|
2526
|**epic-link-field**|False|string|Jira name of epic link field. Default = "Epic Link". **Note:** requires customization per account and sometimes project|
2627
|**sprint-field**|False|string|Jira name of sprint field. Default = "Sprint". **Note:** requires customization per account and sometimes project|

src/WorkItemMigrator/JiraExport/JiraCommandLine.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,8 @@ private bool ExecuteMigration(CommandOption user, CommandOption password, Comman
9292
JQL = config.Query,
9393
UsingJiraCloud = config.UsingJiraCloud,
9494
IncludeDevelopmentLinks = config.IncludeDevelopmentLinks,
95-
RepositoryMap = config.RepositoryMap
95+
RepositoryMap = config.RepositoryMap,
96+
JiraApiVersion = config.JiraApiVersion
9697
};
9798

9899
var jiraServiceWrapper = new JiraServiceWrapper(jiraSettings);

src/WorkItemMigrator/JiraExport/JiraItem.cs

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -220,16 +220,14 @@ private static void HandleAttachmentChange(JiraChangeItem item, List<RevisionAct
220220

221221
private static List<JiraRevision> BuildCommentRevisions(JiraItem jiraItem, IJiraProvider jiraProvider)
222222
{
223-
var renderedFields = jiraItem.RemoteIssue.SelectToken("$.renderedFields.comment.comments");
224223
var comments = jiraProvider.GetCommentsByItemKey(jiraItem.Key);
225224
return comments.Select((c, i) =>
226225
{
227-
var rc = renderedFields.SelectToken($"$.[{i}].body");
228-
return BuildCommentRevision(c, rc, jiraItem);
226+
return BuildCommentRevision(c, c.RenderedBody, jiraItem);
229227
}).ToList();
230228
}
231229

232-
private static JiraRevision BuildCommentRevision(Comment c, JToken rc, JiraItem jiraItem)
230+
private static JiraRevision BuildCommentRevision(Comment c, string rc, JiraItem jiraItem)
233231
{
234232
var author = "NoAuthorDefined";
235233
if (c.AuthorUser is null)
@@ -252,7 +250,7 @@ private static JiraRevision BuildCommentRevision(Comment c, JToken rc, JiraItem
252250
{
253251
Author = author,
254252
Time = c.CreatedDate.Value,
255-
Fields = new Dictionary<string, object>() { { "comment", c.Body }, { "comment$Rendered", rc.Value<string>() } },
253+
Fields = new Dictionary<string, object>() { { "comment", c.Body }, { "comment$Rendered", rc } },
256254
AttachmentActions = new List<RevisionAction<JiraAttachment>>(),
257255
LinkActions = new List<RevisionAction<JiraLink>>()
258256
};
@@ -527,17 +525,17 @@ private static Dictionary<string, object> ExtractFields(string key, JObject remo
527525
if (value != null)
528526
{
529527
fields[name] = value;
528+
}
530529

531-
if (renderedFields.TryGetValue(name, out JToken rendered))
530+
if (renderedFields.TryGetValue(name, out JToken rendered))
531+
{
532+
if (rendered.Type == JTokenType.String)
532533
{
533-
if (rendered.Type == JTokenType.String)
534-
{
535-
fields[name + "$Rendered"] = rendered.Value<string>();
536-
}
537-
else
538-
{
539-
Logger.Log(LogLevel.Debug, $"Rendered field {name} contains unparsable type {rendered.Type.ToString()}, using text");
540-
}
534+
fields[name + "$Rendered"] = rendered.Value<string>();
535+
}
536+
else
537+
{
538+
Logger.Log(LogLevel.Debug, $"Rendered field {name} contains unparsable type {rendered.Type.ToString()}, using text");
541539
}
542540
}
543541
}

src/WorkItemMigrator/JiraExport/JiraProvider.cs

Lines changed: 141 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
using Atlassian.Jira;
2-
using Atlassian.Jira.Remote;
32
using Migration.Common;
43
using Migration.Common.Log;
54
using Newtonsoft.Json.Linq;
@@ -9,7 +8,6 @@
98
using System.IO;
109
using System.Linq;
1110
using System.Threading.Tasks;
12-
using System.Web;
1311

1412
namespace JiraExport
1513
{
@@ -24,7 +22,7 @@ public enum DownloadOptions
2422
IncludeSubItems = 4
2523
}
2624

27-
private readonly string JiraApiV2 = "rest/api/2";
25+
private readonly string JiraApi = "rest/api";
2826

2927
private ILookup<string, string> JiraNameFieldCache = null;
3028

@@ -49,6 +47,11 @@ public void Initialize(JiraSettings settings, ExportIssuesSummary exportIssuesSu
4947
{
5048
Settings = settings;
5149

50+
if (Settings.JiraApiVersion != 2 && Settings.JiraApiVersion != 3)
51+
{
52+
Logger.Log(LogLevel.Error, $"Invalid Jira API version: {Settings.JiraApiVersion}. Must be either 2 or 3.");
53+
}
54+
5255
Logger.Log(LogLevel.Info, "Retrieving Jira fields...");
5356
try
5457
{
@@ -108,7 +111,9 @@ public IssueLinkType GetLinkType(string linkTypeString, string targetItemKey, ou
108111

109112
public IEnumerable<Comment> GetCommentsByItemKey(string itemKey)
110113
{
111-
return _jiraServiceWrapper.Issues.GetCommentsAsync(itemKey).Result;
114+
var options = new CommentQueryOptions();
115+
options.Expand.Add("renderedBody");
116+
return _jiraServiceWrapper.Issues.GetCommentsAsync(itemKey, options).Result;
112117
}
113118

114119
public CustomField GetCustomField(string fieldName)
@@ -140,7 +145,7 @@ private async Task<JiraAttachment> GetAttachmentInfo(string id)
140145

141146
try
142147
{
143-
var response = await _jiraServiceWrapper.RestClient.ExecuteRequestAsync(Method.GET, $"{JiraApiV2}/attachment/{id}");
148+
var response = await _jiraServiceWrapper.RestClient.ExecuteRequestAsync(Method.GET, $"{JiraApi}/{Settings.JiraApiVersion}/attachment/{id}");
144149
var attObj = (JObject)response;
145150

146151
return new JiraAttachment
@@ -217,6 +222,24 @@ private void EnsurePath(string path)
217222
}
218223

219224
public IEnumerable<JiraItem> EnumerateIssues(string jql, HashSet<string> skipList, DownloadOptions downloadOptions)
225+
{
226+
if (Settings.JiraApiVersion == 2)
227+
{
228+
return EnumerateIssuesV2(jql, skipList, downloadOptions);
229+
}
230+
else if (Settings.JiraApiVersion == 3)
231+
{
232+
return EnumerateIssuesV3(jql, skipList, downloadOptions);
233+
}
234+
else
235+
{
236+
// Invalid API Version already checked in Initialize()
237+
Logger.Log(LogLevel.Error, $"Invalid Jira API version: {Settings.JiraApiVersion}. Must be either 2 or 3.");
238+
return null;
239+
}
240+
}
241+
242+
public IEnumerable<JiraItem> EnumerateIssuesV2(string jql, HashSet<string> skipList, DownloadOptions downloadOptions)
220243
{
221244
var currentStart = 0;
222245
IEnumerable<string> remoteIssueBatch = null;
@@ -229,7 +252,7 @@ public IEnumerable<JiraItem> EnumerateIssues(string jql, HashSet<string> skipLis
229252
JToken response = null;
230253
try
231254
{
232-
response = _jiraServiceWrapper.RestClient.ExecuteRequestAsync(Method.GET, $"{JiraApiV2}/search?jql={jql}&startAt={currentStart}&maxResults={Settings.BatchSize}&fields=key").Result;
255+
response = _jiraServiceWrapper.RestClient.ExecuteRequestAsync(Method.GET, $"{JiraApi}/{Settings.JiraApiVersion}/search?jql={jql}&startAt={currentStart}&maxResults={Settings.BatchSize}&fields=key").Result;
233256
}
234257
catch (Exception e)
235258
{
@@ -304,6 +327,93 @@ public IEnumerable<JiraItem> EnumerateIssues(string jql, HashSet<string> skipLis
304327
while (remoteIssueBatch != null && remoteIssueBatch.Any());
305328
}
306329

330+
public IEnumerable<JiraItem> EnumerateIssuesV3(string jql, HashSet<string> skipList, DownloadOptions downloadOptions)
331+
{
332+
var nextPageToken = string.Empty;
333+
IEnumerable<string> remoteIssueBatch = null;
334+
var index = 0;
335+
336+
Logger.Log(LogLevel.Debug, "Enumerate remote issues");
337+
338+
var totalItems = GetItemCount(jql);
339+
340+
do
341+
{
342+
JToken response = null;
343+
try
344+
{
345+
response = _jiraServiceWrapper.RestClient.ExecuteRequestAsync(Method.GET, $"{JiraApi}/{Settings.JiraApiVersion}/search/jql?jql={jql}&nextPageToken={nextPageToken}&maxResults={Settings.BatchSize}&fields=key").Result;
346+
nextPageToken = (string)response.SelectToken("$.nextPageToken");
347+
}
348+
catch (Exception e)
349+
{
350+
Logger.Log(e, "Failed to retrieve issues");
351+
break;
352+
}
353+
if (response != null)
354+
{
355+
remoteIssueBatch = response.SelectTokens("$.issues[*]").OfType<JObject>()
356+
.Select(i => i.SelectToken("$.key").Value<string>());
357+
358+
if (remoteIssueBatch == null || !remoteIssueBatch.Any())
359+
{
360+
if (index == 0)
361+
{
362+
Logger.Log(LogLevel.Warning, $"No issuse were found using jql: {jql}");
363+
}
364+
break;
365+
}
366+
367+
foreach (var issueKey in remoteIssueBatch)
368+
{
369+
if (skipList.Contains(issueKey))
370+
{
371+
Logger.Log(LogLevel.Info, $"Skipped Jira '{issueKey}' - already downloaded.");
372+
index++;
373+
continue;
374+
}
375+
376+
Logger.Log(LogLevel.Info, $"Processing {index + 1}/{totalItems} - '{issueKey}'.");
377+
var issue = ProcessItem(issueKey, skipList);
378+
379+
if (issue == null)
380+
continue;
381+
382+
yield return issue;
383+
index++;
384+
385+
if (downloadOptions.HasFlag(DownloadOptions.IncludeParentEpics) && (issue.EpicParent != null) && !skipList.Contains(issue.EpicParent))
386+
{
387+
Logger.Log(LogLevel.Info, $"Processing epic parent '{issue.EpicParent}'.");
388+
var parentEpic = ProcessItem(issue.EpicParent, skipList);
389+
yield return parentEpic;
390+
}
391+
392+
if (downloadOptions.HasFlag(DownloadOptions.IncludeParents) && (issue.Parent != null) && !skipList.Contains(issue.Parent))
393+
{
394+
Logger.Log(LogLevel.Info, $"Processing parent issue '{issue.Parent}'.");
395+
var parent = ProcessItem(issue.Parent, skipList);
396+
yield return parent;
397+
}
398+
399+
if (downloadOptions.HasFlag(DownloadOptions.IncludeSubItems) && (issue.SubItems != null) && issue.SubItems.Any())
400+
{
401+
foreach (var subitemKey in issue.SubItems)
402+
{
403+
if (!skipList.Contains(subitemKey))
404+
{
405+
Logger.Log(LogLevel.Info, $"Processing sub-item '{subitemKey}'.");
406+
var subItem = ProcessItem(subitemKey, skipList);
407+
yield return subItem;
408+
}
409+
}
410+
}
411+
}
412+
}
413+
}
414+
while (nextPageToken != null);
415+
}
416+
307417
public struct JiraVersion
308418
{
309419
public string Version { get; set; }
@@ -321,9 +431,27 @@ public int GetItemCount(string jql)
321431
Logger.Log(LogLevel.Debug, $"Get item count using query: '{jql}'");
322432
try
323433
{
324-
var response = _jiraServiceWrapper.RestClient.ExecuteRequestAsync(Method.GET, $"{JiraApiV2}/search?jql={jql}&maxResults=0").Result;
434+
if (Settings.JiraApiVersion == 2)
435+
{
436+
var response = _jiraServiceWrapper.RestClient.ExecuteRequestAsync(Method.GET, $"{JiraApi}/{Settings.JiraApiVersion}/search?jql={jql}&maxResults=0").Result;
437+
return (int)response.SelectToken("$.total");
438+
}
439+
else if (Settings.JiraApiVersion == 3)
440+
{
441+
var requestBody = new
442+
{
443+
jql = jql
444+
};
445+
var response = _jiraServiceWrapper.RestClient.ExecuteRequestAsync(Method.POST, $"{JiraApi}/{Settings.JiraApiVersion}/search/approximate-count", requestBody).Result;
325446

326-
return (int)response.SelectToken("$.total");
447+
return (int)response.SelectToken("$.count");
448+
}
449+
else
450+
{
451+
// Invalid API Version already checked in Initialize()
452+
Logger.Log(LogLevel.Error, $"Invalid Jira API version: {Settings.JiraApiVersion}. Must be either 2 or 3.");
453+
return -1;
454+
}
327455
}
328456
catch (Exception e)
329457
{
@@ -335,13 +463,13 @@ public int GetItemCount(string jql)
335463

336464
public JiraVersion GetJiraVersion()
337465
{
338-
var response = (JObject)_jiraServiceWrapper.RestClient.ExecuteRequestAsync(Method.GET, $"{JiraApiV2}/serverInfo").Result;
466+
var response = (JObject)_jiraServiceWrapper.RestClient.ExecuteRequestAsync(Method.GET, $"{JiraApi}/{Settings.JiraApiVersion}/serverInfo").Result;
339467
return new JiraVersion((string)response.SelectToken("$.version"), (string)response.SelectToken("$.deploymentType"));
340468
}
341469

342470
public IEnumerable<JObject> DownloadChangelog(string issueKey)
343471
{
344-
var response = (JObject)_jiraServiceWrapper.RestClient.ExecuteRequestAsync(Method.GET, $"{JiraApiV2}/issue/{issueKey}?expand=changelog,renderedFields&fields=created").Result;
472+
var response = (JObject)_jiraServiceWrapper.RestClient.ExecuteRequestAsync(Method.GET, $"{JiraApi}/{Settings.JiraApiVersion}/issue/{issueKey}?expand=changelog,renderedFields&fields=created").Result;
345473
return response.SelectTokens("$.changelog.histories[*]").Cast<JObject>();
346474
}
347475

@@ -350,7 +478,7 @@ public JObject DownloadIssue(string key)
350478
try
351479
{
352480
var response =
353-
_jiraServiceWrapper.RestClient.ExecuteRequestAsync(Method.GET, $"{JiraApiV2}/issue/{key}?expand=renderedFields").Result;
481+
_jiraServiceWrapper.RestClient.ExecuteRequestAsync(Method.GET, $"{JiraApi}/{Settings.JiraApiVersion}/issue/{key}?expand=renderedFields").Result;
354482

355483
var remoteItem = (JObject)response;
356484
return remoteItem;
@@ -360,7 +488,6 @@ public JObject DownloadIssue(string key)
360488
Logger.Log(e, $"Failed to download issue with key: {key}");
361489
return default(JObject);
362490
}
363-
364491
}
365492

366493
public async Task<List<RevisionAction<JiraAttachment>>> DownloadAttachments(JiraRevision rev)
@@ -440,7 +567,7 @@ public string GetCustomId(string propertyName)
440567

441568
if (JiraNameFieldCache == null)
442569
{
443-
response = (JArray)_jiraServiceWrapper.RestClient.ExecuteRequestAsync(Method.GET, $"{JiraApiV2}/field").Result;
570+
response = (JArray)_jiraServiceWrapper.RestClient.ExecuteRequestAsync(Method.GET, $"{JiraApi}/{Settings.JiraApiVersion}/field").Result;
444571
JiraNameFieldCache = CreateFieldCacheLookup(response, "name", "id");
445572
}
446573

@@ -450,7 +577,7 @@ public string GetCustomId(string propertyName)
450577
{
451578
if (JiraKeyFieldCache == null)
452579
{
453-
response = response ?? (JArray)_jiraServiceWrapper.RestClient.ExecuteRequestAsync(Method.GET, $"{JiraApiV2}/field").Result;
580+
response = response ?? (JArray)_jiraServiceWrapper.RestClient.ExecuteRequestAsync(Method.GET, $"{JiraApi}/{Settings.JiraApiVersion}/field").Result;
454581
JiraKeyFieldCache = CreateFieldCacheLookup(response, "key", "id");
455582
}
456583
customId = GetItemFromFieldCache(propertyName, JiraKeyFieldCache);

src/WorkItemMigrator/JiraExport/JiraSettings.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ public class JiraSettings
1919
public string JQL { get; set; }
2020
public bool UsingJiraCloud { get; set; }
2121
public bool IncludeDevelopmentLinks { get; set; }
22+
public int JiraApiVersion { get; set; }
2223
public RepositoryMap RepositoryMap { get; set; }
2324

2425
public JiraSettings(string userID, string pass, string token, string url, string project)

src/WorkItemMigrator/Migration.Common/Config/ConfigJson.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,9 @@ public class ConfigJson
8989

9090
[JsonProperty(PropertyName = "suppress-notifications")]
9191
public bool SuppressNotifications { get; set; } = false;
92-
92+
93+
[JsonProperty(PropertyName = "jira-api-version")]
94+
public int JiraApiVersion { get; set; } = 3;
95+
9396
}
9497
}

0 commit comments

Comments
 (0)