Skip to content

Commit 9e0270e

Browse files
Merge pull request #648 from solidify/feature/create-workItems-in-past
Feature/create work items in past
2 parents 638d32a + 7e93e2a commit 9e0270e

File tree

12 files changed

+350
-114
lines changed

12 files changed

+350
-114
lines changed

src/WorkItemMigrator/WorkItemImport/Agent.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,9 @@ public WorkItem GetWorkItem(int wiId)
5656
return _witClientUtils.GetWorkItem(wiId);
5757
}
5858

59-
public WorkItem CreateWorkItem(string type)
59+
public WorkItem CreateWorkItem(string type, DateTime createdDate, string createdBy)
6060
{
61-
return _witClientUtils.CreateWorkItem(type);
61+
return _witClientUtils.CreateWorkItem(type, createdDate, createdBy);
6262
}
6363

6464
public bool ImportRevision(WiRevision rev, WorkItem wi)

src/WorkItemMigrator/WorkItemImport/ImportCommandLine.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ private void ExecuteMigration(CommandOption token, CommandOption url, CommandOpt
122122
if (executionItem.WiId > 0)
123123
wi = agent.GetWorkItem(executionItem.WiId);
124124
else
125-
wi = agent.CreateWorkItem(executionItem.WiType);
125+
wi = agent.CreateWorkItem(executionItem.WiType, executionItem.Revision.Time, executionItem.Revision.Author);
126126

127127
Logger.Log(LogLevel.Info, $"Processing {importedItems + 1}/{revisionCount} - wi '{(wi.Id > 0 ? wi.Id.ToString() : "Initial revision")}', jira '{executionItem.OriginId}, rev {executionItem.Revision.Index}'.");
128128

src/WorkItemMigrator/WorkItemImport/WitClient/IWitClientWrapper.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using System.Collections.Generic;
1+
using System;
2+
using System.Collections.Generic;
23
using Microsoft.TeamFoundation.Core.WebApi;
34
using Microsoft.TeamFoundation.WorkItemTracking.WebApi.Models;
45
using Microsoft.VisualStudio.Services.WebApi.Patch.Json;
@@ -8,7 +9,7 @@ namespace WorkItemImport
89
{
910
public interface IWitClientWrapper
1011
{
11-
WorkItem CreateWorkItem(string wiType);
12+
WorkItem CreateWorkItem(string wiType, DateTime? createdDate = null, string createdBy = "");
1213
WorkItem GetWorkItem(int wiId);
1314
WorkItem UpdateWorkItem(JsonPatchDocument patchDocument, int workItemId);
1415
TeamProject GetProject(string projectId);
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
using Microsoft.VisualStudio.Services.WebApi.Patch;
2+
using Microsoft.VisualStudio.Services.WebApi.Patch.Json;
3+
using System;
4+
5+
namespace WorkItemImport.WitClient
6+
{
7+
public static class JsonPatchDocUtils
8+
{
9+
public static JsonPatchOperation CreateJsonFieldPatchOp(Operation op, string key, object value)
10+
{
11+
if(string.IsNullOrEmpty(key))
12+
{
13+
throw new ArgumentException(nameof(key));
14+
}
15+
16+
if (value == null)
17+
{
18+
throw new ArgumentException(nameof(value));
19+
}
20+
21+
return new JsonPatchOperation()
22+
{
23+
Operation = op,
24+
Path = "/fields/" + key,
25+
Value = value
26+
};
27+
}
28+
}
29+
}

src/WorkItemMigrator/WorkItemImport/WitClient/WitClientUtils.cs

Lines changed: 67 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
using Migration.Common;
1212
using Migration.Common.Log;
1313
using Migration.WIContract;
14+
using WorkItemImport.WitClient;
1415

1516
namespace WorkItemImport
1617
{
@@ -25,9 +26,9 @@ public WitClientUtils(IWitClientWrapper witClientWrapper)
2526

2627
public delegate V IsAttachmentMigratedDelegate<in T, U, out V>(T input, out U output);
2728

28-
public WorkItem CreateWorkItem(string type)
29+
public WorkItem CreateWorkItem(string type, DateTime? createdDate = null, string createdBy = "")
2930
{
30-
return _witClientWrapper.CreateWorkItem(type);
31+
return _witClientWrapper.CreateWorkItem(type, createdDate, createdBy);
3132
}
3233

3334
public bool IsDuplicateWorkItemLink(IEnumerable<WorkItemRelation> links, WorkItemRelation relatedLink)
@@ -202,12 +203,17 @@ public void EnsureDateFields(WiRevision rev, WorkItem wi)
202203
}
203204
if (!rev.Fields.HasAnyByRefName(WiFieldReference.ChangedDate))
204205
{
205-
if (DateTime.Parse(wi.Fields[WiFieldReference.ChangedDate].ToString()) == rev.Time)
206+
DateTime workItemChangedDate = (DateTime)(wi.Fields[WiFieldReference.ChangedDate]);
207+
if (workItemChangedDate.ToUniversalTime() == rev.Time.ToUniversalTime())
206208
{
207209
rev.Fields.Add(new WiField() { ReferenceName = WiFieldReference.ChangedDate, Value = rev.Time.AddMilliseconds(1).ToString("o") });
208210
}
209211
else
210-
rev.Fields.Add(new WiField() { ReferenceName = WiFieldReference.ChangedDate, Value = rev.Time.ToString("o") });
212+
{
213+
// ADO can add a few milliseconds to work item createdDate when adding an attachment, hence adding more here to the revision time
214+
rev.Fields.Add(new WiField() { ReferenceName = WiFieldReference.ChangedDate, Value = rev.Time.AddMilliseconds(5).ToString("o") });
215+
wi.Fields[WiFieldReference.ChangedDate] = rev.Time.AddMilliseconds(5);
216+
}
211217
}
212218

213219
}
@@ -450,16 +456,27 @@ public void SaveWorkItemAttachments(WiRevision rev, WorkItem wi)
450456
throw new ArgumentException(nameof(wi));
451457
}
452458

459+
// Calculate UpdatedTime
460+
DateTime attachmentUpdatedDate = rev.Time;
461+
DateTime workItemChangedDate = (DateTime)wi.Fields[WiFieldReference.ChangedDate];
462+
if (workItemChangedDate.ToUniversalTime() > rev.Time.ToUniversalTime())
463+
{
464+
attachmentUpdatedDate = workItemChangedDate;
465+
// The work item ChangeDate is altered when saving the attachment, make sure the Revision time does too.
466+
// Otherwise it will not be an increased ChangedDate and we'll get an exception
467+
rev.Time = workItemChangedDate;
468+
}
469+
453470
// Save attachments
454471
foreach (WiAttachment attachment in rev.Attachments)
455472
{
456473
if (attachment.Change == ReferenceChangeType.Added)
457474
{
458-
AddSingleAttachmentToWorkItemAndSave(attachment, wi);
475+
AddSingleAttachmentToWorkItemAndSave(attachment, wi, attachmentUpdatedDate, rev.Author);
459476
}
460477
else if (attachment.Change == ReferenceChangeType.Removed)
461478
{
462-
RemoveSingleAttachmentFromWorkItemAndSave(attachment, wi);
479+
RemoveSingleAttachmentFromWorkItemAndSave(attachment, wi, attachmentUpdatedDate, rev.Author);
463480
}
464481
}
465482
}
@@ -476,23 +493,16 @@ public void SaveWorkItemFields(WorkItem wi)
476493
foreach (string key in wi.Fields.Keys)
477494
{
478495
if (new string[] {
479-
WiFieldReference.ChangedDate,
480496
WiFieldReference.BoardColumn,
481497
WiFieldReference.BoardColumnDone,
482498
WiFieldReference.BoardLane,
483499
}.Contains(key))
484500
continue;
485501

486-
487502
object val = wi.Fields[key];
488503

489504
patchDocument.Add(
490-
new JsonPatchOperation()
491-
{
492-
Operation = Operation.Add,
493-
Path = "/fields/" + key,
494-
Value = val
495-
}
505+
JsonPatchDocUtils.CreateJsonFieldPatchOp(Operation.Add, key, val)
496506
);
497507
}
498508

@@ -593,7 +603,7 @@ private void CorrectActivatedByAndActivatedDate(WiRevision rev, WorkItem wi)
593603
}
594604
}
595605

596-
private void AddSingleAttachmentToWorkItemAndSave(WiAttachment att, WorkItem wi)
606+
private void AddSingleAttachmentToWorkItemAndSave(WiAttachment att, WorkItem wi, DateTime? changedDate = null, string changedBy = "")
597607
{
598608
// Upload attachment
599609
AttachmentReference attachment = _witClientWrapper.CreateAttachment(att);
@@ -621,6 +631,30 @@ private void AddSingleAttachmentToWorkItemAndSave(WiAttachment att, WorkItem wi)
621631
}
622632
};
623633

634+
if (changedDate != null)
635+
{
636+
DateTime workItemChangedDate = (DateTime)wi.Fields[WiFieldReference.ChangedDate];
637+
if (changedDate.Value.ToUniversalTime() >= workItemChangedDate.ToUniversalTime())
638+
{
639+
attachmentPatchDocument.Add(
640+
JsonPatchDocUtils.CreateJsonFieldPatchOp(Operation.Add, WiFieldReference.ChangedDate, changedDate)
641+
);
642+
}
643+
else
644+
{
645+
attachmentPatchDocument.Add(
646+
JsonPatchDocUtils.CreateJsonFieldPatchOp(Operation.Add, WiFieldReference.ChangedDate, workItemChangedDate.AddMilliseconds(1))
647+
);
648+
}
649+
}
650+
651+
if (!string.IsNullOrEmpty(changedBy))
652+
{
653+
attachmentPatchDocument.Add(
654+
JsonPatchDocUtils.CreateJsonFieldPatchOp(Operation.Add, WiFieldReference.ChangedBy, changedBy)
655+
);
656+
}
657+
624658
var attachments = wi.Relations?.Where(r => r.Rel == "AttachedFile") ?? new List<WorkItemRelation>();
625659
var previousAttachmentsCount = attachments.Count();
626660

@@ -637,9 +671,12 @@ private void AddSingleAttachmentToWorkItemAndSave(WiAttachment att, WorkItem wi)
637671
Logger.Log(LogLevel.Info, "");
638672

639673
wi.Relations = result.Relations;
674+
675+
// While updating the work item, the changed date can be increased, hence we take it over
676+
wi.Fields[WiFieldReference.ChangedDate] = result.Fields[WiFieldReference.ChangedDate];
640677
}
641678

642-
private void RemoveSingleAttachmentFromWorkItemAndSave(WiAttachment att, WorkItem wi)
679+
private void RemoveSingleAttachmentFromWorkItemAndSave(WiAttachment att, WorkItem wi, DateTime changedDate = default, string changedBy = default)
643680
{
644681
WorkItemRelation existingAttachmentRelation =
645682
wi.Relations?.SingleOrDefault(
@@ -669,6 +706,20 @@ private void RemoveSingleAttachmentFromWorkItemAndSave(WiAttachment att, WorkIte
669706
}
670707
};
671708

709+
if (changedDate != default)
710+
{
711+
attachmentPatchDocument.Add(
712+
JsonPatchDocUtils.CreateJsonFieldPatchOp(Operation.Add, WiFieldReference.ChangedDate, changedDate)
713+
);
714+
}
715+
716+
if (changedBy != default)
717+
{
718+
attachmentPatchDocument.Add(
719+
JsonPatchDocUtils.CreateJsonFieldPatchOp(Operation.Add, WiFieldReference.ChangedBy, changedBy)
720+
);
721+
}
722+
672723
IEnumerable<WorkItemRelation> existingAttachments = wi.Relations?.Where(r => r.Rel == "AttachedFile") ?? new List<WorkItemRelation>();
673724
int previousAttachmentsCount = existingAttachments.Count();
674725

src/WorkItemMigrator/WorkItemImport/WitClient/WitClientWrapper.cs

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
using Microsoft.VisualStudio.Services.WebApi.Patch.Json;
1212
using Migration.Common.Log;
1313
using Migration.WIContract;
14+
using WorkItemImport.WitClient;
1415

1516
namespace WorkItemImport
1617
{
@@ -30,22 +31,39 @@ public WitClientWrapper(string collectionUri, string project, string personalAcc
3031
TeamProject = ProjectClient.GetProject(project).Result;
3132
}
3233

33-
public WorkItem CreateWorkItem(string wiType)
34+
public WorkItem CreateWorkItem(string wiType, DateTime? createdDate = null, string createdBy = "")
3435
{
3536
JsonPatchDocument patchDoc = new JsonPatchDocument
3637
{
37-
new JsonPatchOperation()
38-
{
39-
Operation = Operation.Add,
40-
Path = "/fields/"+WiFieldReference.Title,
41-
Value = "[Placeholder Name]"
42-
}
38+
JsonPatchDocUtils.CreateJsonFieldPatchOp(Operation.Add, WiFieldReference.Title, "[Placeholder Name]")
4339
};
4440

41+
if (createdDate != null)
42+
{
43+
patchDoc.Add(
44+
JsonPatchDocUtils.CreateJsonFieldPatchOp(Operation.Add, WiFieldReference.CreatedDate, createdDate)
45+
);
46+
47+
patchDoc.Add(
48+
JsonPatchDocUtils.CreateJsonFieldPatchOp(Operation.Add, WiFieldReference.ChangedDate, createdDate)
49+
);
50+
}
51+
52+
if (!string.IsNullOrEmpty(createdBy))
53+
{
54+
patchDoc.Add(
55+
JsonPatchDocUtils.CreateJsonFieldPatchOp(Operation.Add, WiFieldReference.CreatedBy, createdBy)
56+
);
57+
58+
patchDoc.Add(
59+
JsonPatchDocUtils.CreateJsonFieldPatchOp(Operation.Add, WiFieldReference.ChangedBy, createdBy)
60+
);
61+
}
62+
4563
WorkItem wiOut;
4664
try
4765
{
48-
wiOut = WitClient.CreateWorkItemAsync(document:patchDoc, project:TeamProject.Name, type:wiType, bypassRules:false, expand:WorkItemExpand.All).Result;
66+
wiOut = WitClient.CreateWorkItemAsync(document:patchDoc, project:TeamProject.Name, type:wiType, bypassRules:true, expand:WorkItemExpand.All).Result;
4967
} catch (Exception e)
5068
{
5169
Logger.Log(LogLevel.Error, "Error when creating new Work item: " + e.Message);

src/WorkItemMigrator/WorkItemImport/wi-import.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@
178178
<Reference Include="System.Xml" />
179179
</ItemGroup>
180180
<ItemGroup>
181+
<Compile Include="WitClient\JsonPatchDocUtils.cs" />
181182
<Compile Include="WitClient\IWitClientWrapper.cs" />
182183
<Compile Include="WitClient\WitClientWrapper.cs" />
183184
<Compile Include="WitClient\WitClientUtils.cs" />

src/WorkItemMigrator/tests/Migration.Common.Tests/RevisionUtilityTests.cs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,10 @@ public void When_calling_hasanybyrefname_when_list_contains_matching_refname_The
9797
{
9898
WiField field = new WiField();
9999
field.ReferenceName = "name";
100-
List<WiField> list = new List<WiField>();
101-
list.Add(field);
100+
List<WiField> list = new List<WiField>
101+
{
102+
field
103+
};
102104

103105
bool expected = true;
104106
bool actual = RevisionUtility.HasAnyByRefName(list, "name");
@@ -111,8 +113,10 @@ public void When_calling_hasanybyrefname_when_list_does_not_contain_matching_ref
111113
{
112114
WiField field = new WiField();
113115
field.ReferenceName = "anothername";
114-
List<WiField> list = new List<WiField>();
115-
list.Add(field);
116+
List<WiField> list = new List<WiField>
117+
{
118+
field
119+
};
116120

117121
bool expected = false;
118122
bool actual = RevisionUtility.HasAnyByRefName(list, "name");

src/WorkItemMigrator/tests/Migration.Jira-Export.Tests/RevisionUtils/FieldMapperUtilsTests.cs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -357,8 +357,10 @@ public void When_calling_map_rendered_value_with_valid_input_Then_expected_outpu
357357
attachment.Url = "https://example.com";
358358
revisionAction.Value = attachment;
359359

360-
jiraRevision.AttachmentActions = new List<RevisionAction<JiraAttachment>>();
361-
jiraRevision.AttachmentActions.Add(revisionAction);
360+
jiraRevision.AttachmentActions = new List<RevisionAction<JiraAttachment>>
361+
{
362+
revisionAction
363+
};
362364

363365

364366
var actualOutput = FieldMapperUtils.MapRenderedValue(jiraRevision, sourceField, false, customFieldName, configJson);
@@ -389,8 +391,10 @@ public void When_calling_map_rendered_value_with_invalid_input_Then_expected_fal
389391
attachment.Url = "https://example.com";
390392
revisionAction.Value = attachment;
391393

392-
jiraRevision.AttachmentActions = new List<RevisionAction<JiraAttachment>>();
393-
jiraRevision.AttachmentActions.Add(revisionAction);
394+
jiraRevision.AttachmentActions = new List<RevisionAction<JiraAttachment>>
395+
{
396+
revisionAction
397+
};
394398

395399

396400
var actualOutput = FieldMapperUtils.MapRenderedValue(jiraRevision, sourceField, false, customFieldName, configJson);

0 commit comments

Comments
 (0)