11using System . Xml . XPath ;
22using DotNet . DocsTools . GitHubObjects ;
33using DotNet . DocsTools . GraphQLQueries ;
4+ using Org . BouncyCastle . Asn1 . Ocsp ;
45
56namespace Quest2GitHub ;
67
@@ -22,6 +23,7 @@ namespace Quest2GitHub;
2223/// <param name="areaPath">The area path for work items from this repo</param>
2324/// <param name="importTriggerLabelText">The text of the label that triggers an import</param>
2425/// <param name="importedLabelText">The text of the label that indicates an issue has been imported</param>
26+ /// <param name="removeLinkItemText">The text of the label that indicates an issue should be removed</param>
2527/// <param name="parentNodes">A dictionary of label / parent ID pairs.</param>
2628/// <remarks>
2729/// The OAuth token takes precedence over the GitHub token, if both are
@@ -36,6 +38,7 @@ public class QuestGitHubService(
3638 string areaPath ,
3739 string importTriggerLabelText ,
3840 string importedLabelText ,
41+ string removeLinkItemText ,
3942 List < ParentForLabel > parentNodes ,
4043 IEnumerable < LabelToTagMap > tagMap ) : IDisposable
4144{
@@ -46,6 +49,7 @@ public class QuestGitHubService(
4649
4750 private GitHubLabel ? _importTriggerLabel ;
4851 private GitHubLabel ? _importedLabel ;
52+ private GitHubLabel ? _removeLinkedItemLabel ;
4953 private QuestIteration [ ] ? _allIterations ;
5054
5155 /// <summary>
@@ -57,7 +61,7 @@ public class QuestGitHubService(
5761 /// <returns></returns>
5862 public async Task ProcessIssues ( string organization , string repository , int duration )
5963 {
60- if ( _importTriggerLabel is null || _importedLabel is null )
64+ if ( _importTriggerLabel is null || _importedLabel is null || _removeLinkedItemLabel is null )
6165 {
6266 await RetrieveLabelIdsAsync ( organization , repository ) ;
6367 }
@@ -97,23 +101,26 @@ async Task ProcessItems(IAsyncEnumerable<QuestIssueOrPullRequest> items)
97101 {
98102 await foreach ( QuestIssueOrPullRequest item in items )
99103 {
100- if ( item . Labels . Any ( l => ( l . Id == _importTriggerLabel ? . Id ) || ( l . Id == _importedLabel ? . Id ) ) )
104+ if ( item . Labels . Any ( l => ( l . Id == _importTriggerLabel ? . Id ) || ( l . Id == _importedLabel ? . Id ) || ( l . Id == _removeLinkedItemLabel ? . Id ) ) )
101105 {
106+ bool request = item . Labels . Any ( l => l . Id == _importTriggerLabel ? . Id ) ;
107+ bool sequestered = item . Labels . Any ( l => l . Id == _importedLabel ? . Id ) ;
108+ bool vanquished = item . Labels . Any ( l => l . Id == _removeLinkedItemLabel ? . Id ) ;
109+ // Only query AzDo if needed:
110+ QuestWorkItem ? questItem = ( request || sequestered || vanquished )
111+ ? await FindLinkedWorkItemAsync ( item )
112+ : null ;
102113 var issueProperties = new WorkItemProperties ( item , _allIterations , tagMap , parentNodes ) ;
103114
104115 Console . WriteLine ( $ "{ item . Number } : { item . Title } , { issueProperties . IssueLogString } ") ;
105- // Console.WriteLine(item);
106- QuestWorkItem ? questItem = await FindLinkedWorkItemAsync ( item ) ;
107- if ( questItem != null )
116+ Task workDone = ( request , sequestered , vanquished , questItem ) switch
108117 {
109- await QuestWorkItem . UpdateWorkItemAsync ( questItem , item , _azdoClient , _ospoClient , issueProperties ) ;
110- }
111- else
112- {
113- questItem = await LinkIssueAsync ( item , issueProperties ) ;
114- // Because some fields can't be set in the initial creation, update the item.
115- await QuestWorkItem . UpdateWorkItemAsync ( questItem , item , _azdoClient , _ospoClient , issueProperties ) ;
116- }
118+ ( false , false , false , _ ) => Task . CompletedTask , // No labels. Do nothing.
119+ ( _, _, true , null ) => Task . CompletedTask , // Unlink, but no link. Do nothing.
120+ ( _, _, false , null ) => LinkIssueAsync ( item , issueProperties ) , // No link, but one of the link labels was applied.
121+ ( _, _, true , not null ) => questItem . RemoveWorkItem ( item , _azdoClient , issueProperties ) , // Unlink.
122+ ( _, _, false , not null ) => questItem . UpdateWorkItemAsync ( item , _azdoClient , _ospoClient , issueProperties ) , // update
123+ } ;
117124 totalImport ++ ;
118125 }
119126 else
@@ -157,7 +164,7 @@ async IAsyncEnumerable<QuestIssueOrPullRequest> QueryAllOpenIssuesOrPullRequests
157164 /// <returns>A task representing the current operation</returns>
158165 public async Task ProcessIssue ( string gitHubOrganization , string gitHubRepository , int issueNumber )
159166 {
160- if ( _importTriggerLabel is null || _importedLabel is null )
167+ if ( _importTriggerLabel is null || _importedLabel is null || _removeLinkedItemLabel is null )
161168 {
162169 await RetrieveLabelIdsAsync ( gitHubOrganization , gitHubRepository ) ;
163170 }
@@ -183,41 +190,23 @@ public async Task ProcessIssue(string gitHubOrganization, string gitHubRepositor
183190 // Evaluate the labels to determine the right action.
184191 bool request = ghIssue . Labels . Any ( l => l . Id == _importTriggerLabel ? . Id ) ;
185192 bool sequestered = ghIssue . Labels . Any ( l => l . Id == _importedLabel ? . Id ) ;
193+ bool vanquished = ghIssue . Labels . Any ( l => l . Id == _removeLinkedItemLabel ? . Id ) ;
186194 // Only query AzDo if needed:
187- QuestWorkItem ? questItem = ( request || sequestered )
195+ QuestWorkItem ? questItem = ( request || sequestered || vanquished )
188196 ? await FindLinkedWorkItemAsync ( ghIssue )
189197 : null ;
190198
191199 var issueProperties = new WorkItemProperties ( ghIssue , _allIterations , tagMap , parentNodes ) ;
192200
193- // The order here is important to avoid a race condition that causes
194- // an issue to be triggered multiple times.
195- // First, if an issue is open and the trigger label is added, link or
196- // update. Update is safe, because it will only update the quest issue's
197- // state or assigned field. That can't trigger a new GH action run.
198- if ( request )
199- {
200- if ( questItem is null )
201- {
202- questItem = await LinkIssueAsync ( ghIssue , issueProperties ) ;
203- }
204- else if ( questItem is not null )
205- {
206- // This allows a human to force a manual update: just add the trigger label.
207- // Note that it updates even if the item is closed.
208- await QuestWorkItem . UpdateWorkItemAsync ( questItem , ghIssue , _azdoClient , _ospoClient , issueProperties ) ;
209-
210- }
211- // Next, if the item is already linked, consider any updates.
212- // It's important that adding the linked label is the last
213- // mutation done in the linking process. That way, the GH Action
214- // does get triggered again. The second trigger will check for any updates
215- // a human made to assigned or state while the initial run was taking place.
216- }
217- else if ( sequestered && questItem is not null )
201+ Task workDone = ( request , sequestered , vanquished , questItem ) switch
218202 {
219- await QuestWorkItem . UpdateWorkItemAsync ( questItem , ghIssue , _azdoClient , _ospoClient , issueProperties ) ;
220- }
203+ ( false , false , false , _ ) => Task . CompletedTask , // No labels. Do nothing.
204+ ( _, _, true , null ) => Task . CompletedTask , // Unlink, but no link. Do nothing.
205+ ( _, _, false , null ) => LinkIssueAsync ( ghIssue , issueProperties ) , // No link, but one of the link labels was applied.
206+ ( _, _, true , not null ) => questItem . RemoveWorkItem ( ghIssue , _azdoClient , issueProperties ) , // Unlink.
207+ ( _, _, false , not null ) => questItem . UpdateWorkItemAsync ( ghIssue , _azdoClient , _ospoClient , issueProperties ) , // update
208+ } ;
209+ await workDone ;
221210 }
222211
223212 /// <summary>
@@ -322,6 +311,9 @@ private async Task<QuestWorkItem> LinkIssueAsync(QuestIssueOrPullRequest issueOr
322311 var prMutation = new Mutation < SequesteredPullRequestMutation , SequesterVariables > ( ghClient ) ;
323312 await prMutation . PerformMutation ( new SequesterVariables ( pr . Id , _importTriggerLabel ? . Id ?? "" , _importedLabel ? . Id ?? "" , updatedBody ) ) ;
324313 }
314+
315+ // Because some fields can't be set when an item is created, go through an update cycle:
316+ await questItem . UpdateWorkItemAsync ( issueOrPullRequest , _azdoClient , _ospoClient , issueProperties ) ;
325317 return questItem ;
326318 }
327319 else
@@ -338,6 +330,7 @@ private async Task RetrieveLabelIdsAsync(string org, string repo)
338330 {
339331 if ( label . Name == importTriggerLabelText ) _importTriggerLabel = label ;
340332 if ( label . Name == importedLabelText ) _importedLabel = label ;
333+ if ( label . Name == removeLinkItemText ) _removeLinkedItemLabel = label ;
341334 }
342335 }
343336
0 commit comments