diff --git a/src/WorkItemMigrator/JiraExport/IJiraProvider.cs b/src/WorkItemMigrator/JiraExport/IJiraProvider.cs index bd6e41f2..01a5c2df 100644 --- a/src/WorkItemMigrator/JiraExport/IJiraProvider.cs +++ b/src/WorkItemMigrator/JiraExport/IJiraProvider.cs @@ -32,5 +32,6 @@ public interface IJiraProvider string GetCustomId(string propertyName); Task>> DownloadAttachments(JiraRevision rev); + IEnumerable GetCommitRepositories(string issueId); } } diff --git a/src/WorkItemMigrator/JiraExport/JiraCommandLine.cs b/src/WorkItemMigrator/JiraExport/JiraCommandLine.cs index be12218e..a48d4a3a 100644 --- a/src/WorkItemMigrator/JiraExport/JiraCommandLine.cs +++ b/src/WorkItemMigrator/JiraExport/JiraCommandLine.cs @@ -89,7 +89,9 @@ private void ExecuteMigration(CommandOption user, CommandOption password, Comman UserMappingFile = config.UserMappingFile != null ? Path.Combine(migrationWorkspace, config.UserMappingFile) : string.Empty, AttachmentsDir = Path.Combine(migrationWorkspace, config.AttachmentsFolder), JQL = config.Query, - UsingJiraCloud = config.UsingJiraCloud + UsingJiraCloud = config.UsingJiraCloud, + IncludeCommits = config.IncludeCommits, + RepositoryMap = config.RepositoryMap }; var jiraServiceWrapper = new JiraServiceWrapper(jiraSettings); diff --git a/src/WorkItemMigrator/JiraExport/JiraCommit.cs b/src/WorkItemMigrator/JiraExport/JiraCommit.cs new file mode 100644 index 00000000..e259c053 --- /dev/null +++ b/src/WorkItemMigrator/JiraExport/JiraCommit.cs @@ -0,0 +1,11 @@ +using System; + +namespace JiraExport +{ + public class JiraCommit + { + public string Repository { get; set; } + public string Id { get; set; } + public DateTime AuthorTimestamp { get; set; } + } +} \ No newline at end of file diff --git a/src/WorkItemMigrator/JiraExport/JiraItem.cs b/src/WorkItemMigrator/JiraExport/JiraItem.cs index adef109d..ea749d37 100644 --- a/src/WorkItemMigrator/JiraExport/JiraItem.cs +++ b/src/WorkItemMigrator/JiraExport/JiraItem.cs @@ -116,6 +116,38 @@ private static List BuildRevisions(JiraItem jiraItem, IJiraProvide List commentRevisions = BuildCommentRevisions(jiraItem, jiraProvider); listOfRevisions.AddRange(commentRevisions); + + var settings = jiraProvider.GetSettings(); + if (settings.IncludeCommits) + { + var commitRepositories = jiraProvider.GetCommitRepositories(jiraItem.Id); + foreach (var respository in commitRepositories) + { + var commits = respository.SelectTokens(".commits[*]"); + foreach (JToken commit in commits) + { + var commitCreatedOn = commit.ExValue("$.authorTimestamp"); + var commitAuthor = GetAuthor(commit as JObject); + var jiraCommit = commit.ToObject(); + var repositoryName = respository.SelectToken("$.name").Value(); + if (string.IsNullOrEmpty(repositoryName)) + { + continue; + } + + var hasRespositoryTarget = settings.RepositoryMap.Repositories.Exists(r => r.Source == repositoryName && !string.IsNullOrEmpty(r.Target)); + if (!hasRespositoryTarget) + { + continue; + } + + jiraCommit.Repository = repositoryName; + var commitRevision = new JiraRevision(jiraItem) { Time = commitCreatedOn, Author = commitAuthor, Fields = new Dictionary(), Commit = new RevisionAction() { ChangeType = RevisionChangeType.Added, Value = jiraCommit } }; + listOfRevisions.Add(commitRevision); + } + } + } + listOfRevisions.Sort(); foreach (var revAndI in listOfRevisions.Select((r, i) => (r, i))) @@ -524,6 +556,7 @@ private static string[] ParseCustomField(string fieldName, JToken value, IJiraPr public string Key { get { return RemoteIssue.ExValue("$.key"); } } public string Type { get { return RemoteIssue.ExValue("$.fields.issuetype.name")?.Trim(); } } + public string Id { get { return RemoteIssue.ExValue("$.id"); } } public string EpicParent { get diff --git a/src/WorkItemMigrator/JiraExport/JiraMapper.cs b/src/WorkItemMigrator/JiraExport/JiraMapper.cs index 015fe6fc..901d469f 100644 --- a/src/WorkItemMigrator/JiraExport/JiraMapper.cs +++ b/src/WorkItemMigrator/JiraExport/JiraMapper.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; - using Common.Config; using Migration.Common; @@ -55,6 +54,7 @@ internal WiItem Map(JiraItem issue) return null; } } + return wiItem; } @@ -180,6 +180,39 @@ internal Dictionary> InitializeFieldMappings( return mappingPerWiType; } + internal WiCommit MapCommit(JiraRevision jiraRevision) + { + if (jiraRevision == null) + throw new ArgumentNullException(nameof(jiraRevision)); + + if (jiraRevision.Commit == null) + { + return null; + } + + var jiraCommit = jiraRevision.Commit.Value; + var respositoryTarget = jiraCommit.Repository; + + var respositoryOverride = _config + .RepositoryMap + .Repositories? + .Find(r => r.Source == respositoryTarget)? + .Target; + + if (!string.IsNullOrEmpty(respositoryOverride)) + { + respositoryTarget = respositoryOverride; + } + + var commit = new WiCommit() + { + Id = jiraCommit.Id, + Repository = respositoryTarget, + }; + + return commit; + } + internal List MapLinks(JiraRevision r) { if (r == null) @@ -303,6 +336,7 @@ internal WiRevision MapRevision(JiraRevision r) List attachments = MapAttachments(r); List fields = MapFields(r); List links = MapLinks(r); + var commit = MapCommit(r); return new WiRevision() { @@ -313,7 +347,8 @@ internal WiRevision MapRevision(JiraRevision r) Attachments = attachments, Fields = fields, Links = links, - AttachmentReferences = attachments.Any() + AttachmentReferences = attachments.Any(), + Commit = commit }; } diff --git a/src/WorkItemMigrator/JiraExport/JiraProvider.cs b/src/WorkItemMigrator/JiraExport/JiraProvider.cs index 88144adf..cee7ccf3 100644 --- a/src/WorkItemMigrator/JiraExport/JiraProvider.cs +++ b/src/WorkItemMigrator/JiraExport/JiraProvider.cs @@ -441,5 +441,11 @@ private string GetItemFromFieldCache(string propertyName, ILookup GetCommitRepositories(string issueId) + { + var response = (JObject)_jiraServiceWrapper.RestClient.ExecuteRequestAsync(Method.GET, $"/rest/dev-status/latest/issue/detail?issueId={issueId}&applicationType=stash&dataType=repository").Result; + return response.SelectTokens("$.detail[*].repositories[*]").Cast(); + } } } diff --git a/src/WorkItemMigrator/JiraExport/JiraRevision.cs b/src/WorkItemMigrator/JiraExport/JiraRevision.cs index c30df2e1..f7aa35a1 100644 --- a/src/WorkItemMigrator/JiraExport/JiraRevision.cs +++ b/src/WorkItemMigrator/JiraExport/JiraRevision.cs @@ -32,6 +32,7 @@ public class JiraRevision : ISourceRevision, IComparable public List> LinkActions { get; set; } public List> AttachmentActions { get; set; } + public RevisionAction Commit { get; set; } public JiraItem ParentItem { get; private set; } public int Index { get; set; } diff --git a/src/WorkItemMigrator/JiraExport/JiraSettings.cs b/src/WorkItemMigrator/JiraExport/JiraSettings.cs index 49424581..86cdd4fa 100644 --- a/src/WorkItemMigrator/JiraExport/JiraSettings.cs +++ b/src/WorkItemMigrator/JiraExport/JiraSettings.cs @@ -1,4 +1,6 @@  +using Migration.Common.Config; + namespace JiraExport { public class JiraSettings @@ -14,6 +16,8 @@ public class JiraSettings public string AttachmentsDir { get; set; } public string JQL { get; set; } public bool UsingJiraCloud { get; set; } + public bool IncludeCommits { get; set; } + public RepositoryMap RepositoryMap { get; set; } public JiraSettings(string userID, string pass, string url, string project) { diff --git a/src/WorkItemMigrator/JiraExport/jira-export.csproj b/src/WorkItemMigrator/JiraExport/jira-export.csproj index 5725ccd8..bfa309d4 100644 --- a/src/WorkItemMigrator/JiraExport/jira-export.csproj +++ b/src/WorkItemMigrator/JiraExport/jira-export.csproj @@ -115,6 +115,7 @@ + diff --git a/src/WorkItemMigrator/Migration.Common/Config/ConfigJson.cs b/src/WorkItemMigrator/Migration.Common/Config/ConfigJson.cs index e31a6644..cd7652df 100644 --- a/src/WorkItemMigrator/Migration.Common/Config/ConfigJson.cs +++ b/src/WorkItemMigrator/Migration.Common/Config/ConfigJson.cs @@ -54,6 +54,9 @@ public class ConfigJson [JsonProperty(PropertyName = "process-template")] public string ProcessTemplate { get; set; } = "Scrum"; + [JsonProperty(PropertyName = "repository-map")] + public RepositoryMap RepositoryMap { get; set; } + [JsonProperty(PropertyName = "type-map", Required = Required.Always)] public TypeMap TypeMap { get; set; } @@ -63,13 +66,16 @@ public class ConfigJson [JsonProperty(PropertyName = "rendered-fields")] public string[] RenderedFields { get; set; } = new string[] { "description", "comment" }; - [JsonProperty(PropertyName = "using-jira-cloud")] public bool UsingJiraCloud { get; set; } = true; [JsonProperty(PropertyName = "include-link-comments")] public bool IncludeLinkComments { get; set; } = true; + [JsonProperty(PropertyName = "sleep-time-between-revision-import-milliseconds")] public int SleepTimeBetweenRevisionImportMilliseconds { get; set; } = 0; + + [JsonProperty(PropertyName = "include-commits")] + public bool IncludeCommits { get; set; } = true; } } \ No newline at end of file diff --git a/src/WorkItemMigrator/Migration.Common/Config/ConfigReaderJson.cs b/src/WorkItemMigrator/Migration.Common/Config/ConfigReaderJson.cs index 70f9c1b0..c0dba56d 100644 --- a/src/WorkItemMigrator/Migration.Common/Config/ConfigReaderJson.cs +++ b/src/WorkItemMigrator/Migration.Common/Config/ConfigReaderJson.cs @@ -87,6 +87,15 @@ public ConfigJson DeserializeText(string input) } result.TypeMap.Types.AddRange(types); + if(obj.ContainsKey("repository-map")) + { + var repositories = obj.SelectToken("repository-map.repository").Select(li => li.ToObject()).ToList(); + if (result.RepositoryMap.Repositories == null) + { + result.RepositoryMap.Repositories = new List(); + } + result.RepositoryMap.Repositories.AddRange(repositories); + } } catch (Exception) { diff --git a/src/WorkItemMigrator/Migration.Common/Config/Repository.cs b/src/WorkItemMigrator/Migration.Common/Config/Repository.cs new file mode 100644 index 00000000..88abfe38 --- /dev/null +++ b/src/WorkItemMigrator/Migration.Common/Config/Repository.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json; + +namespace Migration.Common.Config +{ + public class Repository + { + [JsonProperty("target", Required = Required.Always)] + public string Target { get; set; } + + [JsonProperty("source", Required = Required.Always)] + public string Source { get; set; } + } +} \ No newline at end of file diff --git a/src/WorkItemMigrator/Migration.Common/Config/RepositoryMap.cs b/src/WorkItemMigrator/Migration.Common/Config/RepositoryMap.cs new file mode 100644 index 00000000..6fd33c68 --- /dev/null +++ b/src/WorkItemMigrator/Migration.Common/Config/RepositoryMap.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace Migration.Common.Config +{ + public class RepositoryMap + { + public List Repositories { get; set; } + } +} \ No newline at end of file diff --git a/src/WorkItemMigrator/Migration.Common/Migration.Common.csproj b/src/WorkItemMigrator/Migration.Common/Migration.Common.csproj index 0d971e46..4c45a6cf 100644 --- a/src/WorkItemMigrator/Migration.Common/Migration.Common.csproj +++ b/src/WorkItemMigrator/Migration.Common/Migration.Common.csproj @@ -64,6 +64,8 @@ + + diff --git a/src/WorkItemMigrator/Migration.WIContract/Migration.WIContract.csproj b/src/WorkItemMigrator/Migration.WIContract/Migration.WIContract.csproj index 11c19ca0..18e74406 100644 --- a/src/WorkItemMigrator/Migration.WIContract/Migration.WIContract.csproj +++ b/src/WorkItemMigrator/Migration.WIContract/Migration.WIContract.csproj @@ -46,6 +46,7 @@ + diff --git a/src/WorkItemMigrator/Migration.WIContract/WiCommit.cs b/src/WorkItemMigrator/Migration.WIContract/WiCommit.cs new file mode 100644 index 00000000..5c5612cf --- /dev/null +++ b/src/WorkItemMigrator/Migration.WIContract/WiCommit.cs @@ -0,0 +1,13 @@ +namespace Migration.WIContract +{ + public class WiCommit + { + public string Id { get; set; } + public string Repository { get; set; } + + public override string ToString() + { + return $"[{Repository}]{Id}"; + } + } +} diff --git a/src/WorkItemMigrator/Migration.WIContract/WiRevision.cs b/src/WorkItemMigrator/Migration.WIContract/WiRevision.cs index 864d41d6..01ac5e71 100644 --- a/src/WorkItemMigrator/Migration.WIContract/WiRevision.cs +++ b/src/WorkItemMigrator/Migration.WIContract/WiRevision.cs @@ -28,6 +28,7 @@ public WiRevision() public List Fields { get; set; } public List Links { get; set; } public List Attachments { get; set; } + public WiCommit Commit { get; set; } [DefaultValue(false)] public bool AttachmentReferences { get; set; } = false; diff --git a/src/WorkItemMigrator/WorkItemImport/Agent.cs b/src/WorkItemMigrator/WorkItemImport/Agent.cs index d2e28995..8eee4e32 100644 --- a/src/WorkItemMigrator/WorkItemImport/Agent.cs +++ b/src/WorkItemMigrator/WorkItemImport/Agent.cs @@ -120,7 +120,18 @@ public bool ImportRevision(WiRevision rev, WorkItem wi, Settings settings) } } - _witClientUtils.SaveWorkItemFields(wi); + // rev with a commit won't have meaningful information, skip saving fields + if (rev.Commit != null) + { + if (settings.IncludeCommits) + { + _witClientUtils.SaveWorkItemArtifacts(rev, wi, settings); + } + } + else + { + _witClientUtils.SaveWorkItemFields(wi); + } if (wi.Id.HasValue) { @@ -579,7 +590,6 @@ private bool ApplyAndSaveLinks(WiRevision rev, WorkItem wi, bool addLinkComments return success; } - #endregion } } \ No newline at end of file diff --git a/src/WorkItemMigrator/WorkItemImport/ImportCommandLine.cs b/src/WorkItemMigrator/WorkItemImport/ImportCommandLine.cs index e75343e7..57ebd6c6 100644 --- a/src/WorkItemMigrator/WorkItemImport/ImportCommandLine.cs +++ b/src/WorkItemMigrator/WorkItemImport/ImportCommandLine.cs @@ -91,7 +91,8 @@ private void ExecuteMigration(CommandOption token, CommandOption url, CommandOpt BaseIterationPath = config.BaseIterationPath ?? string.Empty, // Root iteration path that will prefix each iteration IgnoreFailedLinks = config.IgnoreFailedLinks, ProcessTemplate = config.ProcessTemplate, - IncludeLinkComments = config.IncludeLinkComments + IncludeLinkComments = config.IncludeLinkComments, + IncludeCommits = config.IncludeCommits }; // initialize Azure DevOps/TFS connection. Creates/fetches project, fills area and iteration caches. diff --git a/src/WorkItemMigrator/WorkItemImport/Settings.cs b/src/WorkItemMigrator/WorkItemImport/Settings.cs index a67363c7..168ed5d5 100644 --- a/src/WorkItemMigrator/WorkItemImport/Settings.cs +++ b/src/WorkItemMigrator/WorkItemImport/Settings.cs @@ -17,5 +17,6 @@ public Settings(string account, string project, string pat) public bool IgnoreFailedLinks { get; internal set; } public string ProcessTemplate { get; internal set; } public bool IncludeLinkComments { get; internal set; } + public bool IncludeCommits { get; internal set; } } } \ No newline at end of file diff --git a/src/WorkItemMigrator/WorkItemImport/WitClient/JsonPatchDocUtils.cs b/src/WorkItemMigrator/WorkItemImport/WitClient/JsonPatchDocUtils.cs index e3e6a727..93c633fb 100644 --- a/src/WorkItemMigrator/WorkItemImport/WitClient/JsonPatchDocUtils.cs +++ b/src/WorkItemMigrator/WorkItemImport/WitClient/JsonPatchDocUtils.cs @@ -6,6 +6,18 @@ namespace WorkItemImport.WitClient { public static class JsonPatchDocUtils { + public class PatchOperationValue + { + public string Rel { get; set; } + public string Url { get; set; } + public Attributes Attributes { get; set; } + } + + public class Attributes + { + public string Name { get; set; } + } + public static JsonPatchOperation CreateJsonFieldPatchOp(Operation op, string key, object value) { if (string.IsNullOrEmpty(key)) @@ -20,5 +32,38 @@ public static JsonPatchOperation CreateJsonFieldPatchOp(Operation op, string key Value = value }; } + + public static JsonPatchOperation CreateJsonArtifactLinkPatchOp(Operation op, string project, string repository, string commitId) + { + if (string.IsNullOrEmpty(commitId)) + { + throw new ArgumentException(nameof(commitId)); + } + + if (string.IsNullOrEmpty(project)) + { + throw new ArgumentException(nameof(project)); + } + + if (string.IsNullOrEmpty(repository)) + { + throw new ArgumentException(nameof(repository)); + } + + return new JsonPatchOperation() + { + Operation = op, + Path = "/relations/-", + Value = new PatchOperationValue + { + Rel = "ArtifactLink", + Url = $"vstfs:///Git/Commit/{project}/{repository}/{commitId}", + Attributes = new Attributes + { + Name = "Fixed in Commit" + } + } + }; + } } } diff --git a/src/WorkItemMigrator/WorkItemImport/WitClient/WitClientUtils.cs b/src/WorkItemMigrator/WorkItemImport/WitClient/WitClientUtils.cs index 29b0c5a6..ace9317a 100644 --- a/src/WorkItemMigrator/WorkItemImport/WitClient/WitClientUtils.cs +++ b/src/WorkItemMigrator/WorkItemImport/WitClient/WitClientUtils.cs @@ -557,6 +557,42 @@ public void SaveWorkItemFields(WorkItem wi) } } + public void SaveWorkItemArtifacts(WiRevision rev, WorkItem wi, Settings settings) + { + if (wi == null) + { + throw new ArgumentException(nameof(wi)); + } + + if (rev.Commit == null) + { + return; + } + + var patchDocument = new JsonPatchDocument + { + JsonPatchDocUtils.CreateJsonArtifactLinkPatchOp(Operation.Add, settings.Project, rev.Commit.Repository, rev.Commit.Id), + JsonPatchDocUtils.CreateJsonFieldPatchOp(Operation.Add, WiFieldReference.ChangedDate, rev.Time), + JsonPatchDocUtils.CreateJsonFieldPatchOp(Operation.Add, WiFieldReference.ChangedBy, rev.Author) + }; + + try + { + if (wi.Id.HasValue) + _witClientWrapper.UpdateWorkItem(patchDocument, wi.Id.Value); + else + throw new MissingFieldException($"Work item ID was null: {wi.Url}"); + } + catch (AggregateException ex) + { + foreach (Exception ex2 in ex.InnerExceptions) + { + Logger.Log(LogLevel.Error, ex2.Message); + } + Logger.Log(LogLevel.Error, "Work Item " + wi.Id + " failed to save."); + } + } + private void CorrectImagePath(WorkItem wi, WiItem wiItem, WiRevision rev, ref string textField, ref bool isUpdated, IsAttachmentMigratedDelegate isAttachmentMigratedDelegate) { if (wi == null) diff --git a/src/WorkItemMigrator/WorkItemImport/wi-import.csproj b/src/WorkItemMigrator/WorkItemImport/wi-import.csproj index 39a09e31..c11577de 100644 --- a/src/WorkItemMigrator/WorkItemImport/wi-import.csproj +++ b/src/WorkItemMigrator/WorkItemImport/wi-import.csproj @@ -14,6 +14,7 @@ + false publish\ true Disk @@ -26,7 +27,6 @@ true 0 1.0.0.%2a - false false true diff --git a/src/WorkItemMigrator/tests/Migration.Jira-Export.Tests/JiraItemTests.cs b/src/WorkItemMigrator/tests/Migration.Jira-Export.Tests/JiraItemTests.cs index 54c79cc0..8447532a 100644 --- a/src/WorkItemMigrator/tests/Migration.Jira-Export.Tests/JiraItemTests.cs +++ b/src/WorkItemMigrator/tests/Migration.Jira-Export.Tests/JiraItemTests.cs @@ -8,7 +8,6 @@ using System.Diagnostics.CodeAnalysis; using System.Collections.Generic; using System.Linq; -using Migration.Common.Config; using System; using System.Web; using Atlassian.Jira; diff --git a/src/WorkItemMigrator/tests/Migration.Jira-Export.Tests/JiraMapperTests.cs b/src/WorkItemMigrator/tests/Migration.Jira-Export.Tests/JiraMapperTests.cs index 941e52e5..d7174cb2 100644 --- a/src/WorkItemMigrator/tests/Migration.Jira-Export.Tests/JiraMapperTests.cs +++ b/src/WorkItemMigrator/tests/Migration.Jira-Export.Tests/JiraMapperTests.cs @@ -266,6 +266,14 @@ private JiraMapper createJiraMapper() linkMap.Links.AddRange(new Link[] { epicLinkMap, parentLinkMap }); cjson.LinkMap = linkMap; + RepositoryMap repositoryMap = new RepositoryMap(); + repositoryMap.Repositories = new List(); + Repository repository = new Repository(); + repository.Source = "Sample Repository"; + repository.Target = "Destination Repository"; + repositoryMap.Repositories.Add(repository); + cjson.RepositoryMap = repositoryMap; + JiraMapper sut = new JiraMapper(provider, cjson); return sut; diff --git a/src/WorkItemMigrator/tests/Migration.Wi-Import.Tests/WitClient/JsonPatchDocUtilsTests.cs b/src/WorkItemMigrator/tests/Migration.Wi-Import.Tests/WitClient/JsonPatchDocUtilsTests.cs index 19702e42..a557612d 100644 --- a/src/WorkItemMigrator/tests/Migration.Wi-Import.Tests/WitClient/JsonPatchDocUtilsTests.cs +++ b/src/WorkItemMigrator/tests/Migration.Wi-Import.Tests/WitClient/JsonPatchDocUtilsTests.cs @@ -8,6 +8,9 @@ using System.Diagnostics.CodeAnalysis; using WorkItemImport.WitClient; +using Newtonsoft.Json.Linq; +using Microsoft.VisualStudio.Services.Common; +using static WorkItemImport.WitClient.JsonPatchDocUtils; namespace Migration.Wi_Import.Tests { @@ -33,18 +36,45 @@ public void When_calling_create_json_field_patch_op_with_empty_args_Then_an_exce Throws.InstanceOf()); } + [Test] + public void When_calling_create_json_artifact_link_field_patch_op_with_empty_args_Then_an_exception_is_thrown() + { + Assert.That( + () => JsonPatchDocUtils.CreateJsonArtifactLinkPatchOp(Operation.Add, null, null, null), + Throws.InstanceOf()); + } + [Test] public void When_calling_create_json_field_patch_op_Then_a_correct_op_is_returned() { - JsonPatchOperation jsonPatchOp = JsonPatchDocUtils.CreateJsonFieldPatchOp(Operation.Add, "key", "value"); + string key = "key"; + string value = "value"; + JsonPatchOperation jsonPatchOp = JsonPatchDocUtils.CreateJsonFieldPatchOp(Operation.Add, key, value); Assert.Multiple(() => { Assert.That(jsonPatchOp.Operation == Operation.Add); - Assert.That(jsonPatchOp.Path == "/fields/key"); - Assert.That(jsonPatchOp.Value.ToString() == "value"); + Assert.That(jsonPatchOp.Path == "/fields/" + key); + Assert.That(jsonPatchOp.Value.ToString() == value); }); + } + [Test] + public void When_calling_create_json_artifact_link_field_patch_op_Then_a_correct_op_is_returned() + { + string project = "project"; + string repository = "repository"; + string commitId = "commitId"; + JsonPatchOperation jsonPatchOp = JsonPatchDocUtils.CreateJsonArtifactLinkPatchOp(Operation.Add, project, repository, commitId); + PatchOperationValue artifactLink = jsonPatchOp.Value as PatchOperationValue; + + Assert.Multiple(() => + { + Assert.AreEqual(Operation.Add, jsonPatchOp.Operation); + Assert.AreEqual("/relations/-", jsonPatchOp.Path); + Assert.AreEqual("ArtifactLink", artifactLink.Rel); + Assert.AreEqual($"vstfs:///Git/Commit/{project}/{repository}/{commitId}", artifactLink.Url); + }); } } } \ No newline at end of file diff --git a/src/WorkItemMigrator/tests/Migration.Wi-Import.Tests/WitClient/WitClientUtilsTests.cs b/src/WorkItemMigrator/tests/Migration.Wi-Import.Tests/WitClient/WitClientUtilsTests.cs index 074a2211..81e9fded 100644 --- a/src/WorkItemMigrator/tests/Migration.Wi-Import.Tests/WitClient/WitClientUtilsTests.cs +++ b/src/WorkItemMigrator/tests/Migration.Wi-Import.Tests/WitClient/WitClientUtilsTests.cs @@ -68,9 +68,21 @@ public WorkItem UpdateWorkItem(JsonPatchDocument patchDocument, int workItemId) else if (op.Path.StartsWith("/relations/")) { string rel = op.Value.GetType().GetProperty("rel")?.GetValue(op.Value, null).ToString(); + if(rel == null) + { + rel = op.Value.GetType().GetProperty("Rel")?.GetValue(op.Value, null).ToString(); + } string url = op.Value.GetType().GetProperty("url")?.GetValue(op.Value, null).ToString(); + if (url == null) + { + url = op.Value.GetType().GetProperty("Url")?.GetValue(op.Value, null).ToString(); + } object attributes = op.Value.GetType().GetProperty("attributes")?.GetValue(op.Value, null); - string comment = attributes.GetType().GetProperty("comment")?.GetValue(attributes, null).ToString(); + if (attributes == null) + { + attributes = op.Value.GetType().GetProperty("Attributes")?.GetValue(op.Value, null); + } + string comment = attributes?.GetType().GetProperty("comment")?.GetValue(attributes, null).ToString(); WorkItemRelation wiRelation = new WorkItemRelation(); wiRelation.Rel = rel; @@ -1003,6 +1015,51 @@ public void When_calling_save_workitem_fields_with_populated_workitem_Then_worki }); } + [Test] + public void When_calling_save_workitem_artifacts_with_empty_args_Then_an_exception_is_thrown() + { + MockedWitClientWrapper witClientWrapper = new MockedWitClientWrapper(); + WitClientUtils wiUtils = new WitClientUtils(witClientWrapper); + + Assert.That( + () => wiUtils.SaveWorkItemArtifacts(null, null, null), + Throws.InstanceOf()); + } + + [Test] + public void When_calling_save_workitem_artifacts_with_populated_workitem_Then_workitem_is_updated_in_store() + { + MockedWitClientWrapper witClientWrapper = new MockedWitClientWrapper(); + WitClientUtils wiUtils = new WitClientUtils(witClientWrapper); + + WorkItem createdWI = wiUtils.CreateWorkItem("User Story"); + createdWI.Fields[WiFieldReference.ChangedDate] = DateTime.Now; + + WiRevision revision = new WiRevision(); + revision.Commit = new WiCommit(); + revision.Commit.Repository = "repository"; + revision.Commit.Id = "1234567890"; + + Settings settings = new Settings("account", "project", "pat"); + + // Perform save + wiUtils.SaveWorkItemArtifacts(revision, createdWI, settings); + + WorkItem updatedWI = null; + + if (createdWI.Id.HasValue) + { + updatedWI = wiUtils.GetWorkItem(createdWI.Id.Value); + } + + // Assertions + Assert.Multiple(() => + { + Assert.That(updatedWI.Relations.First().Rel, Is.EqualTo("ArtifactLink")); + Assert.That(updatedWI.Relations.First().Url, Is.EqualTo("vstfs:///Git/Commit/project/repository/1234567890")); + }); + } + [Test] public void EncodeFileNameUsingJiraStandard_encodes_file_name_with_special_characters() {