/// <summary> /// Initializes a new instance of the <see cref="Orchestrator"/> class. /// </summary> /// <param name="log">Cake log instance.</param> /// <param name="pullRequestSystem">Object for accessing pull request system. /// <c>null</c> if only issues should be read.</param> /// <param name="settings">Settings.</param> public Orchestrator( ICakeLog log, IPullRequestSystem pullRequestSystem, ReportIssuesToPullRequestSettings settings) { #pragma warning disable SA1123 // Do not place regions within elements #region DupFinder Exclusion #pragma warning restore SA1123 // Do not place regions within elements log.NotNull(nameof(log)); pullRequestSystem.NotNull(nameof(pullRequestSystem)); settings.NotNull(nameof(settings)); this.log = log; this.pullRequestSystem = pullRequestSystem; this.settings = settings; #endregion // Initialize pull request system. this.log.Verbose("Initialize pull request system..."); this.pullRequestSystemInitialized = this.pullRequestSystem.Initialize(this.settings); if (!this.pullRequestSystemInitialized) { this.log.Warning("Error initializing the pull request system."); } }
/// <summary> /// Returns threads that should be reopened. /// </summary> /// <param name="existingThreads">Existing discussion threads on the pull request.</param> /// <param name="issueComments">Issues and their related existing comments on the pull request.</param> /// <param name="reportIssuesToPullRequestSettings">Settings for posting the issues.</param> /// <returns>List of threads which should be reopened.</returns> private IEnumerable <IPullRequestDiscussionThread> GetThreadsToReopen( IList <IPullRequestDiscussionThread> existingThreads, IDictionary <IIssue, IssueCommentInfo> issueComments, ReportIssuesToPullRequestSettings reportIssuesToPullRequestSettings) { existingThreads.NotNull(nameof(existingThreads)); issueComments.NotNull(nameof(issueComments)); reportIssuesToPullRequestSettings.NotNull(nameof(reportIssuesToPullRequestSettings)); var resolvedComments = new HashSet <IPullRequestDiscussionComment>( issueComments.Values.SelectMany(x => x.ResolvedComments)); var result = existingThreads.Where( thread => thread.Status == PullRequestDiscussionStatus.Resolved && thread.CommentSource == reportIssuesToPullRequestSettings.CommentSource && thread.Comments.Any(x => resolvedComments.Contains(x))).ToList(); this.log.Verbose( "Found {0} existing thread(s) that are resolved but still have an open issue.", result.Count); return(result); }
/// <summary> /// Initializes a new instance of the <see cref="IssueFilterer"/> class. /// </summary> /// <param name="log">The Cake log instance.</param> /// <param name="pullRequestSystem">Pull request system to use.</param> /// <param name="settings">Settings to use.</param> public IssueFilterer( ICakeLog log, IPullRequestSystem pullRequestSystem, ReportIssuesToPullRequestSettings settings) { #pragma warning disable SA1123 // Do not place regions within elements #region DupFinder Exclusion #pragma warning restore SA1123 // Do not place regions within elements log.NotNull(nameof(log)); pullRequestSystem.NotNull(nameof(pullRequestSystem)); settings.NotNull(nameof(settings)); this.log = log; this.pullRequestSystem = pullRequestSystem; this.settings = settings; #endregion }
public static PullRequestIssueResult ReportIssuesToPullRequest( this ICakeContext context, IIssueProvider issueProvider, IPullRequestSystem pullRequestSystem, ReportIssuesToPullRequestSettings settings) { context.NotNull(nameof(context)); issueProvider.NotNull(nameof(issueProvider)); pullRequestSystem.NotNull(nameof(pullRequestSystem)); settings.NotNull(nameof(settings)); return (context.ReportIssuesToPullRequest( new List <IIssueProvider> { issueProvider }, pullRequestSystem, settings)); }
/// <summary> /// Marks resolved comment threads created by this logic with active issues as active. /// </summary> /// <param name="discussionThreadsCapability">Pull request system capability for working with discussion threads.</param> /// <param name="existingThreads">Existing discussion threads on the pull request.</param> /// <param name="issueComments">Issues and their related existing comments on the pull request.</param> /// <param name="reportIssuesToPullRequestSettings">Settings for posting the issues.</param> private void ReopenExistingComments( ISupportDiscussionThreads discussionThreadsCapability, IList <IPullRequestDiscussionThread> existingThreads, IDictionary <IIssue, IssueCommentInfo> issueComments, ReportIssuesToPullRequestSettings reportIssuesToPullRequestSettings) { existingThreads.NotNull(nameof(existingThreads)); issueComments.NotNull(nameof(issueComments)); reportIssuesToPullRequestSettings.NotNull(nameof(reportIssuesToPullRequestSettings)); if (!existingThreads.Any()) { this.log.Verbose("No existings threads to reopen."); return; } var threadsToReopen = this.GetThreadsToReopen(existingThreads, issueComments, reportIssuesToPullRequestSettings).ToList(); this.log.Verbose("Reopen {0} threads...", threadsToReopen.Count); discussionThreadsCapability.ReopenDiscussionThreads(threadsToReopen); }
/// <summary> /// Returns existing matching comments from the pull request for a list of issues. /// </summary> /// <param name="reportIssuesToPullRequestSettings">Settings to use.</param> /// <param name="issues">Issues for which matching comments should be found.</param> /// <param name="existingThreads">Existing discussion threads on the pull request.</param> /// <returns>Dictionary containing issues and its associated matching comments on the pull request.</returns> private IDictionary <IIssue, IssueCommentInfo> GetCommentsForIssue( ReportIssuesToPullRequestSettings reportIssuesToPullRequestSettings, IList <IIssue> issues, IList <IPullRequestDiscussionThread> existingThreads) { issues.NotNull(nameof(issues)); existingThreads.NotNull(nameof(existingThreads)); var stopwatch = new Stopwatch(); stopwatch.Start(); var result = new Dictionary <IIssue, IssueCommentInfo>(); foreach (var issue in issues) { var(activeComments, wontFixComments, resolvedComments) = this.GetMatchingComments( reportIssuesToPullRequestSettings, issue, existingThreads); if (activeComments.Any() || wontFixComments.Any() || resolvedComments.Any()) { var issueCommentInfo = new IssueCommentInfo( activeComments, wontFixComments, resolvedComments); result.Add(issue, issueCommentInfo); } } this.log.Verbose("Built a issue to comment dictionary in {0} ms", stopwatch.ElapsedMilliseconds); return(result); }
public static PullRequestIssueResult ReportIssuesToPullRequest( this ICakeContext context, IEnumerable <IIssue> issues, IPullRequestSystem pullRequestSystem, ReportIssuesToPullRequestSettings settings) { context.NotNull(nameof(context)); pullRequestSystem.NotNull(nameof(pullRequestSystem)); settings.NotNull(nameof(settings)); // ReSharper disable once PossibleMultipleEnumeration issues.NotNullOrEmptyElement(nameof(issues)); var orchestrator = new Orchestrator( context.Log, pullRequestSystem, settings); // ReSharper disable once PossibleMultipleEnumeration return(orchestrator.Run(issues)); }
IEnumerable <IPullRequestDiscussionComment> resolvedComments) GetMatchingComments( ReportIssuesToPullRequestSettings reportIssuesToPullRequestSettings, IIssue issue, IList <IPullRequestDiscussionThread> existingThreads) { issue.NotNull(nameof(issue)); existingThreads.NotNull(nameof(existingThreads)); // Select threads that point to the same file and have been marked with the given comment source. var matchingThreads = (from thread in existingThreads where thread != null && FilePathsAreMatching(issue, thread) && thread.CommentSource == reportIssuesToPullRequestSettings.CommentSource select thread).ToList(); if (matchingThreads.Any()) { this.log.Verbose( "Found {0} matching thread(s) for the issue at {1} line {2}", matchingThreads.Count, issue.AffectedFileRelativePath, issue.Line); } var activeComments = new List <IPullRequestDiscussionComment>(); var wontFixComments = new List <IPullRequestDiscussionComment>(); var resolvedComments = new List <IPullRequestDiscussionComment>(); foreach (var thread in matchingThreads) { // Select comments from this thread that are not deleted and that match the given message. var matchingComments = (from comment in thread.Comments where comment != null && !comment.IsDeleted && comment.Content == issue.Message select comment).ToList(); if (matchingComments.Any()) { this.log.Verbose( "Found {0} matching comment(s) for the issue at {1} line {2}", matchingComments.Count, issue.AffectedFileRelativePath, issue.Line); } if (thread.Status == PullRequestDiscussionStatus.Active) { activeComments.AddRange(matchingComments); } else if (thread.Status == PullRequestDiscussionStatus.Resolved) { if (thread.Resolution == PullRequestDiscussionResolution.WontFix) { wontFixComments.AddRange(matchingComments); } else if (thread.Resolution == PullRequestDiscussionResolution.Resolved) { resolvedComments.AddRange(matchingComments); } } } return(activeComments, wontFixComments, resolvedComments); }
/// <summary> /// Posts new issues, ignoring duplicate comments and resolves comments that were open in an old iteration /// of the pull request. /// </summary> /// <param name="reportIssuesToPullRequestSettings">Settings for posting the issues.</param> /// <param name="issues">Issues to post.</param> /// <returns>Issues reported to the pull request.</returns> private IEnumerable <IIssue> PostAndResolveComments( ReportIssuesToPullRequestSettings reportIssuesToPullRequestSettings, IList <IIssue> issues) { issues.NotNull(nameof(issues)); IDictionary <IIssue, IssueCommentInfo> issueComments = null; var discussionThreadsCapability = this.pullRequestSystem.GetCapability <ISupportDiscussionThreads>(); if (discussionThreadsCapability != null) { this.log.Information("Fetching existing threads and comments..."); var existingThreads = discussionThreadsCapability.FetchDiscussionThreads( reportIssuesToPullRequestSettings.CommentSource).ToList(); issueComments = this.GetCommentsForIssue( reportIssuesToPullRequestSettings, issues, existingThreads); // Comments that were created by this logic but do not have corresponding issues can be marked as 'Resolved'. this.ResolveExistingComments( discussionThreadsCapability, existingThreads, issueComments, reportIssuesToPullRequestSettings); // Comments that were created by this logic and are resolved, but still have a corresponding issue need to be reopened. this.ReopenExistingComments( discussionThreadsCapability, existingThreads, issueComments, reportIssuesToPullRequestSettings); } if (!issues.Any()) { this.log.Information("No new issues were posted"); return(new List <IIssue>()); } // Filter issues which should not be posted. var issueFilterer = new IssueFilterer(this.log, this.pullRequestSystem, reportIssuesToPullRequestSettings); var remainingIssues = issueFilterer.FilterIssues(issues, issueComments).ToList(); if (remainingIssues.Any()) { var formattedMessages = from issue in remainingIssues select string.Format( CultureInfo.InvariantCulture, " Rule: {0} Line: {1} File: {2}", issue.Rule, issue.Line, issue.AffectedFileRelativePath); this.log.Verbose( "Posting {0} issue(s):\n{1}", remainingIssues.Count, string.Join(Environment.NewLine, formattedMessages)); if (!string.IsNullOrWhiteSpace(reportIssuesToPullRequestSettings.CommitId)) { var checkCommitIdCapability = this.pullRequestSystem.GetCapability <ISupportCheckingCommitId>(); if (checkCommitIdCapability != null && !checkCommitIdCapability.IsCurrentCommitId(reportIssuesToPullRequestSettings.CommitId)) { this.log.Information( "Skipping posting of issues since commit {0} is outdated. Current commit is {1}", reportIssuesToPullRequestSettings.CommitId, checkCommitIdCapability.GetLastSourceCommitId()); return(new List <IIssue>()); } } this.pullRequestSystem.PostDiscussionThreads( remainingIssues, reportIssuesToPullRequestSettings.CommentSource); } else { this.log.Information("All issues were filtered. Nothing new to post."); } return(remainingIssues); }