I need to produce a list of Work Items from a production build, which is triggered by a merge. I use the below to get a list of changes, after pulling the Changeset Id from the build I care about
Uri tfsUri = new Uri("http://leecrp-tfs1:8080/tfs");
TfsTeamProjectCollection tfs = TfsTeamProjectCollectionFactory.GetTeamProjectCollection(tfsUri);
VersionControlServer vcs = tfs.GetService<VersionControlServer>();
List<String> changedThings = new List<String>();
Build bld = _builds.Where(x => x.BuildNumber == buildNumber).First();
if(bld != null)
{
int.TryParse(bld.SourceVersion, out int csId);
if (csId > 0)
{
Changeset changeset = vcs.GetChangeset(csId);
foreach(Microsoft.TeamFoundation.VersionControl.Client.Change change in changeset.Changes)
{
changedThings.Add(change.Item.ServerItem);
}
}
}
if(changedThings != null && changedThings.Count > 0)
{
lbChanges.DataSource = changedThings;
}
However, there does not appear to be anything in the object about the source of each check-in. As stated, I primarily need the Work Items so I can link back to our JIRA tickets later.
I have tried the following, but it returns nothing
IEnumerable<ExtendedMerge> mergeDetails = vcs.QueryMergesExtended(new ItemSpec(#"$/Project/Branch", RecursionType.Full), VersionSpec.Latest, VersionSpec.ParseSingleSpec(csId.ToString(), null), VersionSpec.ParseSingleSpec(csId.ToString(), null));
As it turns out, I just had to slightly modify an earlier attempt. The below gets me the merge details
VersionSpec versionSpec = VersionSpec.ParseSingleSpec(csId.ToString(), null);
ChangesetMergeDetails results = vcs.QueryMergesWithDetails(#"$/Project/SourceBranch", VersionSpec.Latest, 0, #"$/Project/TargetBranch", VersionSpec.Latest, 0, versionSpec, versionSpec, RecursionType.Full);
From there I use an excessive amount of foreach loops to get what I want:
foreach(Changeset thing in results.Changesets)
{
foreach(WorkItem workItem in thing.WorkItems.Distinct())
{
changedItems.Add(workItem.Id.ToString());
foreach (Revision revision in workItem.Revisions)
{
foreach (Field field in workItem.Fields)
{
if(field.Name == "History")
{
string thingICareAbout = revision.Fields[field.Name].Value.ToString();
if (thingICareAbout.Contains("TFS4JIRA"))
{
wiHistory.Add(ParseJIRT(thingICareAbout));
}
}
}
}
}
}
Related
We have a plugin in tfs. This plugin notifies us when any work item changes. We can get work item details like id, changeset list as follows. But we need work item related branch name also.
Could you please help me, is there any way to get related branch name when workitem changed in below code.
Thanks
public EventNotificationStatus ProcessEvent(
IVssRequestContext requestContext,
NotificationType notificationType,
object notificationEventArgs,
out int statusCode,
out string statusMessage,
out ExceptionPropertyCollection properties)
{
statusCode = 0;
properties = null;
statusMessage = String.Empty;
try
{
if (notificationType == NotificationType.Notification && notificationEventArgs is WorkItemChangedEvent)
{
WorkItemChangedEvent ev = notificationEventArgs as WorkItemChangedEvent;
int workItemId = Convert.ToInt32(ev.CoreFields.IntegerFields[0].NewValue);
WriteLog("1WorkItemChangedEventHandler WorkItem " + ev.WorkItemTitle + " - Id " + workItemId.ToString() + " was modified");
}
}
catch (Exception)
{
}
return EventNotificationStatus.ActionPermitted;
}
You can use workItemStore.GetWorkItem(workItemId ) to get all work item info. Check the similar question here: How can I get a reference to the TFS WorkItem in a WorkItemChangedEvent?
If you use git, you can try the following example to get branch reference:
int workitemId = YOUR_ID;
WorkItemStore wiStore = new WorkItemStore("{YOUR_URI}");
WorkItem wi = wiStore.GetWorkItem(workitemId);
foreach (Link link in wi.Links)
{
if (link.BaseType == BaseLinkType.ExternalLink)
{
var brList = ((ExternalLink)link).LinkedArtifactUri.Split(new string[] { "%2F" }, StringSplitOptions.RemoveEmptyEntries).ToList();
brList.RemoveAt(0); //remove project and repo giuds
brList.RemoveAt(0);
brList[0] = brList[0].Replace("GB", ""); // remove GB prefix
Console.WriteLine("Branch: {0}", string.Join("/", brList));
}
}
I'm wanting to 'Create copy of work item' which is available via the UI, ideally via the API.
I know how to create a new work item, but the feature in the UI to connect all current parent links / related links, and all other details is quite useful.
Creating via this API is here: https://learn.microsoft.com/en-us/rest/api/azure/devops/wit/work%20items/create?view=azure-devops-rest-5.1
Any help would be greatly appreciated.
We cannot just copy a work item because it contains system fields that we should skip. Additionally your process may have some rules that may block some fields on the creation step. Here is the small example to clone a work item through REST API with https://www.nuget.org/packages/Microsoft.TeamFoundationServer.Client:
class Program
{
static string[] systemFields = { "System.IterationId", "System.ExternalLinkCount", "System.HyperLinkCount", "System.AttachedFileCount", "System.NodeName",
"System.RevisedDate", "System.ChangedDate", "System.Id", "System.AreaId", "System.AuthorizedAs", "System.State", "System.AuthorizedDate", "System.Watermark",
"System.Rev", "System.ChangedBy", "System.Reason", "System.WorkItemType", "System.CreatedDate", "System.CreatedBy", "System.History", "System.RelatedLinkCount",
"System.BoardColumn", "System.BoardColumnDone", "System.BoardLane", "System.CommentCount", "System.TeamProject"}; //system fields to skip
static string[] customFields = { "Microsoft.VSTS.Common.ActivatedDate", "Microsoft.VSTS.Common.ActivatedBy", "Microsoft.VSTS.Common.ResolvedDate",
"Microsoft.VSTS.Common.ResolvedBy", "Microsoft.VSTS.Common.ResolvedReason", "Microsoft.VSTS.Common.ClosedDate", "Microsoft.VSTS.Common.ClosedBy",
"Microsoft.VSTS.Common.StateChangeDate"}; //unneeded fields to skip
const string ChildRefStr = "System.LinkTypes.Hierarchy-Forward"; //should be only one parent
static void Main(string[] args)
{
string pat = "<pat>"; //https://learn.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate
string orgUrl = "https://dev.azure.com/<org>";
string newProjectName = "";
int wiIdToClone = 0;
VssConnection connection = new VssConnection(new Uri(orgUrl), new VssBasicCredential(string.Empty, pat));
var witClient = connection.GetClient<WorkItemTrackingHttpClient>();
CloneWorkItem(witClient, wiIdToClone, newProjectName, true);
}
private static void CloneWorkItem(WorkItemTrackingHttpClient witClient, int wiIdToClone, string NewTeamProject = "", bool CopyLink = false)
{
WorkItem wiToClone = (CopyLink) ? witClient.GetWorkItemAsync(wiIdToClone, expand: WorkItemExpand.Relations).Result
: witClient.GetWorkItemAsync(wiIdToClone).Result;
string teamProjectName = (NewTeamProject != "") ? NewTeamProject : wiToClone.Fields["System.TeamProject"].ToString();
string wiType = wiToClone.Fields["System.WorkItemType"].ToString();
JsonPatchDocument patchDocument = new JsonPatchDocument();
foreach (var key in wiToClone.Fields.Keys) //copy fields
if (!systemFields.Contains(key) && !customFields.Contains(key))
if (NewTeamProject == "" ||
(NewTeamProject != "" && key != "System.AreaPath" && key != "System.IterationPath")) //do not copy area and iteration into another project
patchDocument.Add(new JsonPatchOperation()
{
Operation = Operation.Add,
Path = "/fields/" + key,
Value = wiToClone.Fields[key]
});
if (CopyLink) //copy links
foreach (var link in wiToClone.Relations)
{
if (link.Rel != ChildRefStr)
{
patchDocument.Add(new JsonPatchOperation()
{
Operation = Operation.Add,
Path = "/relations/-",
Value = new
{
rel = link.Rel,
url = link.Url
}
});
}
}
WorkItem clonedWi = witClient.CreateWorkItemAsync(patchDocument, teamProjectName, wiType).Result;
Console.WriteLine("New work item: " + clonedWi.Id);
}
}
Link to full project: https://github.com/ashamrai/AzureDevOpsExtensions/tree/master/CustomNetTasks/CloneWorkItem
I'm trying to create a plugin in TFS 2017 ,that checks for Associated workitems before check-in and rejects the check-in if it does not meet few conditions. This was working for One Collection and One TeamProject. Now I'm trying to implement this this across multiple collections and team projects and it throws the following error "TF30063: You are not authorized to access "https://tfs.com//TeamProject" I'm pasting the code below, Please let me know what the possible issue would be .
{
// Logic Goes here ...
bool isNullComment = false;
bool isDevBranchChange = false;
bool isCodeReviewRequest = false;
bool isCodeReviewResponse = false;
bool isTaskAssociated = false;
//Read all submitted items
var changes = notification.GetSubmittedItems(requestContext);
//Check if the changes have any DEV branch changes
isDevBranchChange = changes.Any(change => change.ToUpper().Contains("/DEV/"));
if (isDevBranchChange)
{
isNullComment = string.IsNullOrEmpty(notification.Comment.ToString());
var assoWorkItems = notification.NotificationInfo.WorkItemInfo.Select(x => x.Id); //Read all associated workitem id's
TfsTeamProjectCollection connection = new TfsTeamProjectCollection(GetTfsUri(requestContext));
WorkItemStore wiStore = connection.GetService<WorkItemStore>();
//check if any of associated workitem has code review
foreach (int id in assoWorkItems)
{
WorkItem wi = wiStore.GetWorkItem(id);
// Microsoft.TeamFoundation.WorkItemTracking.WebApi.Models.WorkItem wi = witClient.GetWorkItemAsync(id).Result;
if (wi.Type.Name.ToUpper().Equals("CODE REVIEW REQUEST"))
{
isCodeReviewRequest = true;
}
if (wi.Type.Name.ToUpper().Equals("TASK"))
{
isTaskAssociated = true;
}
if (wi.Type.Name.ToUpper().Equals("CODE REVIEW RESPONSE"))
{
isCodeReviewResponse = true;
}
}
if (!isCodeReviewRequest || isNullComment || !isCodeReviewResponse || !isTaskAssociated)
{
statusMessage = "Check-in Rejected. Check-in Comment, Task, Code Review Request and Code Review Response workitems are Required. \nFor more information, please contact TFS Administrators: vsalm#abc.com";
return EventNotificationStatus.ActionDenied;
}
}
}
catch (Exception exception)
{
statusMessage = "Caught an Exception" + exception.Message;
return EventNotificationStatus.ActionDenied;
// Any exception, TFS will disable it. So, Log it and eat it.
TeamFoundationApplicationCore.LogException("DecisionPoint plugin encountered the following error while processing events", exception);
}
}
return EventNotificationStatus.ActionPermitted;
}
private Uri GetTfsUri(IVssRequestContext _requestContext)
{
var locationService = _requestContext.GetService<ILocationService>();
return new Uri(locationService.GetServerAccessMapping(_requestContext).AccessPoint + "/" + _requestContext.ServiceHost.Name);
}
}
}
This is what I see in the test results in the Test Manager: some attachments (1) are on the test steps (in this case - on a shared step). Some (2) are on a result level. In the report I'm working on, I can extract information about attachments (2), but fail to get information about (1). Any suggestions?
In the debugger I can see, that the steps are iterated, I can see, i.e. "Verify the stuff" string etc, but the step has 0 attachments.
For reference, this is most of the code I am using to extract test case results:
foreach (ITestCaseResult testCaseResult in resutlsToDisplay)
{
project.TestCases.Find(test.Id);
var testRun = testCaseResult.GetTestRun();
var testPlanEntry = project.TestPlans.Find(testRun.TestPlanId);
var artifacts = testCaseResult.QueryAssociatedWorkItemArtifacts();
if (testPlanEntry.Name != testPlan)
continue;
var testResult = new TestResult // CUSTOM CLASS
{
TestConfiguration = testCaseResult.TestConfigurationName,
TestRun = testRun.Id,
DateCreated = testRun.DateCreated,
DateStarted = testRun.DateStarted,
DateCompleted = testRun.DateCompleted,
Result = testCaseResult.Outcome.ToString(),
TestResultComment = testRun.Comment,
RunBy = testCaseResult.RunBy == null ? "" : testCaseResult.RunBy.DisplayName,
Build = testRun.BuildNumber ?? ""
};
foreach (var iteration in testCaseResult.Iterations)
{
var iterationResult = testResult.Clone();
iterationResult.ResultAttachmentHtml = getAttachments(iteration.Attachments, testCase.TestCaseId.ToString(), string.Empty, iteration.IterationId.ToString()); // HERE I GET THE ATTACHMENTS OF TYPE 2
iterationResult.Iteration = iteration.IterationId;
iterationResult.IterationComment = iteration.Comment;
foreach (var step in steps)
{
var stepCopy = step.Clone();
iterationResult.Steps.Add(stepCopy);
var actionResult = iteration.FindActionResult(stepCopy.TestStep);
if (actionResult == null)
continue;
stepCopy.Result.Comment = actionResult.ErrorMessage;
stepCopy.Result.Result = actionResult.Outcome.ToString();
stepCopy.Result.AttachmentHtml = getAttachments(actionResult.Attachments, testCase.TestCaseId.ToString(), step.Number, iteration.IterationId.ToString()); // HERE I DO NOT GET ATTACHMENTS OF TYPE 1 - WHY?
}
config.TestCaseResult.TestResults.Add(iterationResult);
}
} //end foreach testCaseResult in resutlsToDisplay
I think I figured it out eventually.
The ITestCaseResult contains ITestIterationResultCollection Iterations.
The ITestIterationResult contains TestActionResultCollection of ITestActionResult.
ITestActionResult can be either ITestStepResult or ISharedStepResult.
If it is ITestStepResult, then it has Attachments collection right there.
If it is ISharedStepResult, however, then it has its own TestActionResultCollection. So this is the one that I have to iterate to find if each member has Attachments.
The code to iterate over steps, including shared steps, looks like this:
foreach (ITestIterationResult iteration in testCaseResult.Iterations)
{
var iterationResult = testResult.Clone();
iterationResult.ResultAttachmentHtml = getAttachments(iteration.Attachments,
testCase.TestCaseId.ToString(),
string.Empty, iteration.IterationId.ToString());
iterationResult.Iteration = iteration.IterationId;
iterationResult.IterationComment = iteration.Comment;
foreach (ITestActionResult action in iteration.Actions)
{
if (action is ITestStepResult)
{
GetStepResults(steps, action, iterationResult, iteration, getAttachments, testCase, false);
}
else if (action is ISharedStepResult)
{
ISharedStepResult result = action as ISharedStepResult;
foreach (var sharedAction in result.Actions)
{
if (sharedAction is ITestStepResult)
{
GetStepResults(steps, sharedAction, iterationResult, iteration, getAttachments, testCase, true);
}
}
}
}
I am having issues with my application. I have a db table for a print queue. When I read from that table in a loop, once I add that record to the view model, I then want to delete it from the database...this would be the most efficient way to do it, but EF barks:
An entity object cannot be referenced by multiple instances of IEntityChangeTracker.
I've tried using multiple contexts... but that didn't seem to work either. I've seen articles like Rick Strahl's, but frankly it was above my level of understanding, and not exactly sure if it helps my issue here and seemed quite an in depth solution for something as simple as this.
Is there a simple way to accomplish what I am trying to achieve here?
Here is my code:
public List<InventoryContainerLabelViewModel> CreateLabelsViewModel(int intFacilityId)
{
var printqRep = new Repository<InventoryContainerPrintQueue>(new InventoryMgmtContext());
var printqRepDelete = new Repository<InventoryContainerPrintQueue>(new InventoryMgmtContext());
IQueryable<InventoryContainerPrintQueue> labels =
printqRep.SearchFor(x => x.FacilityId == intFacilityId);
List<InventoryContainerLabelViewModel> labelsViewModel = new List<InventoryContainerLabelViewModel>();
if (labels.Count() > 0)
{
//Get printq record
foreach (InventoryContainerPrintQueue label in labels)
{
IEnumerable<InventoryContainerDetail> icDtls =
label.InventoryContainerHeader.InventoryContainerDetails;
//Get print details
foreach (InventoryContainerDetail icDtl in icDtls)
{
labelsViewModel.Add(new InventoryContainerLabelViewModel()
{
...
populate view model here
}
);//Add label to view model
} //for each IC detail
//Delete the printq record
printqRepDelete.Delete(label); <======== Error Here
} //foreach label loop
}//label count > 0
return labelsViewModel.ToList();
}
In the end, I added a column to the printq table for status, then in the the loop updated it to processed, then called a separate method to delete it.
public List<InventoryContainerLabelViewModel> CreateLabelsViewModel(int intFacilityId)
{
InventoryMgmtContext dbContext = new InventoryMgmtContext();
var printqRep = new Repository<InventoryContainerPrintQueue>(dbContext);
IEnumerable<InventoryContainerPrintQueue> unprintedPrtqRecs =
printqRep.SearchFor(x => x.FacilityId == intFacilityId && x.Printed == false);
List<InventoryContainerLabelViewModel> labelsViewModel = new List<InventoryContainerLabelViewModel>();
if (unprintedPrtqRecs.Count() > 0)
{
//Get printq record
foreach (InventoryContainerPrintQueue unprintedPrtqRec in unprintedPrtqRecs)
{
IEnumerable<InventoryContainerDetail> icDtls =
unprintedPrtqRec.InventoryContainerHeader.InventoryContainerDetails;
//Get container details to print
foreach (InventoryContainerDetail icDtl in icDtls)
{
labelsViewModel.Add(new InventoryContainerLabelViewModel()
{
...
}
);//Get IC details and create view model
} //for each IC detail
unprintedPrtqRec.Printed = true;
printqRep.Update(unprintedPrtqRec, unprintedPrtqRec, false);
} //foreach label loop
//Commit updated to Printed status to db
dbContext.SaveChanges();
}//label count > 0
return labelsViewModel;
}
public ActionConfirmation<int> DeletePrintQRecs(int intFacilityId)
{
InventoryMgmtContext dbContext = new InventoryMgmtContext();
var printqRep = new Repository<InventoryContainerPrintQueue>(dbContext);
IEnumerable<InventoryContainerPrintQueue> printedPrtqRecs =
printqRep.SearchFor(x => x.FacilityId == intFacilityId && x.Printed == true);
foreach (InventoryContainerPrintQueue printedPrtqRec in printedPrtqRecs)
{
//Delete the printq record
printqRep.Delete(printedPrtqRec, false);
}
//Save Changes on all deletes
ActionConfirmation<int> result;
try
{
dbContext.SaveChanges();
result = ActionConfirmation<int>.CreateSuccessConfirmation(
"All Label Print Q records deleted successfully.",
1);
}
catch (Exception ex)
{
result = ActionConfirmation<int>.CreateFailureConfirmation(
string.Format("An error occured attempting to {0}. The error was: {2}.",
"delete Label Print Q records",
ex.Message),
1
);
}
return result;
}