Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal for Update board column function #957

Draft
wants to merge 6 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions src/WorkItemMigrator/WorkItemImport/BoardColumnCollector.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Migration.WIContract;

namespace WorkItemImport
{
/// <summary>
/// Implementations of this interface will track the value of a field over multiple revisions for each item. They can
/// then be queried for the 'current' value of the field as per the latest revision they have seen the a field in.
/// </summary>
/// <typeparam name="T">The type used when storing and retrieving the field's value.</typeparam>
public interface IFieldCollector<T>
{
/// <summary>
/// Collects the field values in a similar way to <see cref="CollectValues"/> but also tests <see cref="ExecutionPlan.ExecutionItem.IsFinal"/>. For non-final
/// revisions, the collected fields are also removed from the Fields array. For final revisions, adds or updates
/// the Field to contain the latest value of each collected field (whether collected from this revision or a previous one).
/// Usually implemented as a combination of <see cref="GetCurrentValue"/> and <see cref="CollectValues"/>
/// </summary>
/// <param name="executionItem">The execution item containing the revision to process.</param>
/// <remarks> It is acceptable for a revision to not contain a value for the field(s) of interest. </remarks>
void ProcessFields(ExecutionPlan.ExecutionItem executionItem);

/// <summary>
/// Return the 'current' value collected for the field(s) as per the latest revision of the given work item (that specified a value for this field).
/// </summary>
/// <param name="workItemId"></param>
/// <returns></returns>
T GetCurrentValue(string workItemId);

/// <summary>
/// Collect field value(s) from the given revision, updating the internal collection with the latest value(s).
/// The revision provided in each call is considered to overwrite value(s) collected during previous calls.
/// This method does not modify the revision - unlike <see cref="ProcessFields"/>.
/// </summary>
/// <param name="revision">The work item revision to collect field value(s) from.</param>
/// <remarks> It is acceptable for a revision to not contain a value for the field(s) of interest. </remarks>
void CollectValues(WiRevision revision);
}

/// <summary>
/// Collects System.BoardColumn values from each revision in order to provide the final value for the last revision.
/// Expected usage is to call ProcessFields on each revision, which will cause BoardColumn to be removed from all revisions except
/// the last, and the final value to be updated/inserted into the BoardColumn field of the final revision.
/// </summary>
public class BoardColumnCollector : IFieldCollector<string>
{
private readonly Dictionary<string, string> _collection = new Dictionary<string, string>();

/// <inheritdoc/>
public void CollectValues(WiRevision revision)
{
var boardColumn = revision?.Fields?.FirstOrDefault(f => f.ReferenceName == WiFieldReference.BoardColumn)?.Value as string;
if (!string.IsNullOrEmpty(boardColumn))
{
_collection[revision.ParentOriginId] = boardColumn;
}
}

/// <inheritdoc/>
public string GetCurrentValue(string workItemId)
{
return _collection.TryGetValue(workItemId, out var value) ? value : null;
}

/// <summary>
/// Collects the BoardColumn value and removes the field. Except in the final revision the field is explicitly inserted with the latest value.
/// </summary>
/// <param name="executionItem">The execution item containing the revision to process.</param>
/// <inheritdoc/>
public void ProcessFields(ExecutionPlan.ExecutionItem executionItem)
{
CollectValues(executionItem.Revision);
if (executionItem.IsFinal)
{
var boardColumnValue = GetCurrentValue(executionItem.OriginId);
if (!string.IsNullOrWhiteSpace(boardColumnValue))
{
executionItem.Revision.Fields.RemoveAll(i => i.ReferenceName == WiFieldReference.BoardColumn);
executionItem.Revision.Fields.Add(new WiField { ReferenceName = WiFieldReference.BoardColumn, Value = boardColumnValue });
}
}
else
{
executionItem.Revision.Fields.RemoveAll(f => f.ReferenceName == WiFieldReference.BoardColumn);
}
}
}
}
4 changes: 3 additions & 1 deletion src/WorkItemMigrator/WorkItemImport/ExecutionPlan.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ public class ExecutionItem
public int WiId { get; set; } = -1;
public WiRevision Revision { get; set; }
public string WiType { get; internal set; }
/// <summary> Is this the final revision for the work item? </summary>
public bool IsFinal { get; set; } = false;

public override string ToString()
{
Expand All @@ -34,7 +36,7 @@ private ExecutionItem TransformToExecutionItem(RevisionReference revRef)
var item = _context.GetItem(revRef.OriginId);
var rev = item.Revisions[revRef.RevIndex];
rev.Time = revRef.Time;
return new ExecutionItem() { OriginId = item.OriginId, WiId = item.WiId, WiType = item.Type, Revision = rev };
return new ExecutionItem() { OriginId = item.OriginId, WiId = item.WiId, WiType = item.Type, Revision = rev, IsFinal = revRef.IsFinal };
}

public bool TryPop(out ExecutionItem nextItem)
Expand Down
8 changes: 8 additions & 0 deletions src/WorkItemMigrator/WorkItemImport/ExecutionPlanBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,19 @@ private IEnumerable<RevisionReference> BuildExecutionPlanFromDir()
foreach (var wi in _context.EnumerateAllItems())
{
Logger.Log(LogLevel.Debug, $"Analyzing item '{wi.OriginId}'.");
RevisionReference lastRevision = null;
foreach (var rev in wi.Revisions)
{
var revRef = new RevisionReference() { OriginId = wi.OriginId, RevIndex = rev.Index, Time = rev.Time };
lastRevision = revRef;
actionPlan.Add(revRef);
}

if (lastRevision != null)
{
// mark the last revision as final.
lastRevision.IsFinal = true;
}
}
actionPlan.Sort();

Expand Down
8 changes: 6 additions & 2 deletions src/WorkItemMigrator/WorkItemImport/ImportCommandLine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ private bool ExecuteMigration(CommandOption token, CommandOption url, CommandOpt

var executionBuilder = new ExecutionPlanBuilder(context);
var plan = executionBuilder.BuildExecutionPlan();
var boardColumnCollector = new BoardColumnCollector();

itemCount = plan.ReferenceQueue.AsEnumerable().Select(x => x.OriginId).Distinct().Count();
revisionCount = plan.ReferenceQueue.Count;
Expand All @@ -132,6 +133,8 @@ private bool ExecuteMigration(CommandOption token, CommandOption url, CommandOpt

importedItems++;

boardColumnCollector.ProcessFields(executionItem);

if (config.IgnoreEmptyRevisions &&
executionItem.Revision.Fields.Count == 0 &&
executionItem.Revision.Links.Count == 0 &&
Expand All @@ -141,9 +144,9 @@ private bool ExecuteMigration(CommandOption token, CommandOption url, CommandOpt
continue;
}

agent.ImportRevision(executionItem.Revision, wi, settings);
agent.ImportRevision(executionItem.Revision, wi, settings, executionItem.IsFinal);

// Artifical wait (optional) to avoid throttling for ADO Services
// Artificial wait (optional) to avoid throttling for ADO Services
if (config.SleepTimeBetweenRevisionImportMilliseconds > 0)
{
Thread.Sleep(config.SleepTimeBetweenRevisionImportMilliseconds);
Expand Down Expand Up @@ -184,6 +187,7 @@ private bool ExecuteMigration(CommandOption token, CommandOption url, CommandOpt
return succeeded;
}


private static void BeginSession(string configFile, ConfigJson config, bool force, Agent agent, int itemsCount, int revisionCount)
{
var toolVersion = VersionInfo.GetVersionInfo();
Expand Down
4 changes: 3 additions & 1 deletion src/WorkItemMigrator/WorkItemImport/RevisionReference.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ public sealed class RevisionReference : IComparable<RevisionReference>, IEquatab
public string OriginId { get; set; }
public int RevIndex { get; set; }
public DateTime Time { get; set; }
/// <summary> Is this the final/latest revision for the work item? </summary>
public bool IsFinal { get; set; } = false;

public int CompareTo(RevisionReference other)
{
Expand All @@ -21,7 +23,7 @@ public int CompareTo(RevisionReference other)

public bool Equals(RevisionReference other)
{
return OriginId.Equals(other.OriginId, StringComparison.InvariantCultureIgnoreCase) && RevIndex == other.RevIndex;
return OriginId.Equals(other?.OriginId, StringComparison.InvariantCultureIgnoreCase) && RevIndex == other?.RevIndex;
}

public override bool Equals(object obj)
Expand Down
32 changes: 29 additions & 3 deletions src/WorkItemMigrator/WorkItemImport/WitClient/WitClientUtils.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
using Microsoft.TeamFoundation.WorkItemTracking.WebApi.Models;
using Microsoft.TeamFoundation.Work.WebApi;
using Microsoft.TeamFoundation.WorkItemTracking.WebApi.Models;
using Microsoft.VisualStudio.Services.WebApi;
using Microsoft.VisualStudio.Services.WebApi.Patch;
using Microsoft.VisualStudio.Services.WebApi.Patch.Json;
using Migration.Common;
using Migration.Common.Config;
using Migration.Common.Log;
using Migration.WIContract;
using System;
Expand Down Expand Up @@ -625,20 +627,44 @@ public void SaveWorkItemFields(WorkItem wi, Settings settings)
{
throw new ArgumentException(nameof(wi));
}

if (wi.Fields.TryGetValue(WiFieldReference.BoardColumn, out var value))
{
wi.Fields.Remove(WiFieldReference.BoardColumn); // the API refuses to update this field directly
var itemType = wi.Fields[WiFieldReference.WorkItemType]?.ToString(); // used for logging
var kanbanField = wi.Fields.Keys.FirstOrDefault(k => k.EndsWith("_Kanban.Column")); // kanban field is the real board-column field

if (wi.Rev == 0)
{
Logger.Log(LogLevel.Warning, $"Work Item {wi.Id}, rev {wi.Rev} - BoardColumn can not be set to '{value}' because " +
$"items with only one revision are not supported by this feature.");
}
else if (string.IsNullOrWhiteSpace(kanbanField))
{
Logger.Log(LogLevel.Warning, $"Work Item {wi.Id}, rev {wi.Rev} - BoardColumn can not be set to '{value}' because " +
$"items of type '{itemType}' are not supported by this feature.");
}
else
{
Logger.Log(LogLevel.Debug, $"Work Item {wi.Id}, rev {wi.Rev} - Setting BoardColumn to '{value}'.");
wi.Fields[kanbanField] = value;
}
}

// Build json patch document from fields
JsonPatchDocument patchDocument = new JsonPatchDocument();
foreach (string key in wi.Fields.Keys)
{
if (new string[] {
WiFieldReference.BoardColumn,
WiFieldReference.BoardColumnDone,
WiFieldReference.BoardLane,
}.Contains(key))
{
Logger.Log(LogLevel.Debug, $"Work Item {wi.Id} importing field {key} is not supported.");
continue;
Alexander-Hjelm marked this conversation as resolved.
Show resolved Hide resolved
}

object val = wi.Fields[key];

if (val == null || val.ToString() == "")
{
patchDocument.Add(
Expand Down
1 change: 1 addition & 0 deletions src/WorkItemMigrator/WorkItemImport/wi-import.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="BoardColumnCollector.cs" />
<Compile Include="WitClient\JsonPatchDocUtils.cs" />
<Compile Include="WitClient\IWitClientWrapper.cs" />
<Compile Include="WitClient\WitClientWrapper.cs" />
Expand Down
Loading
Loading