/// <summary> /// Gets the list of work items that should be migrated. /// </summary> public async Task <IReadOnlyList <WorkItemSummary> > GetWorkItemsAsync(Func <int, bool> includePredicate) { ArgValidate.IsNotNull(includePredicate, nameof(includePredicate)); string issuesListUrl = string.Format(IssuesListUrlTemplate, project, includeClosedWorkItems); var workItemSummaries = new List <WorkItemSummary>(); // Get the first page of work items. PagedWorkItemList workItemList = await DownloadWorkItemSummaryPage(issuesListUrl); int totalWorkItems = workItemList.TotalItems; int itemsRetrieved = workItemList.WorkItemSummaries.Count; workItemSummaries.AddRange(workItemList.WorkItemSummaries); // Continue getting work item pages until we have all of them. while (itemsRetrieved < totalWorkItems) { string pagedUrl = string.Format(IssuesListUrlWithPagingTemplate, project, itemsRetrieved, includeClosedWorkItems); workItemList = await DownloadWorkItemSummaryPage(pagedUrl); itemsRetrieved += workItemList.WorkItemSummaries.Count; workItemSummaries.AddRange(workItemList.WorkItemSummaries); } // Now that we have the full list, consult the predicate to trim the list and return that. return(workItemSummaries.Where(w => includePredicate(w.Id)).ToArray()); }
/// <summary> /// Builds a list of strings where each string should be added as a comment to the GitHub issue. /// </summary> public static IEnumerable <string> GetFormattedComments(WorkItemDetails workItemDetails) { ArgValidate.IsNotNull(workItemDetails, nameof(workItemDetails)); var stringBuilder = new StringBuilder(); // Format and return all work item comments. foreach (WorkItemComment comment in workItemDetails.Comments.EmptyIfNull()) { string postedBy = comment.PostedBy; if (string.IsNullOrEmpty(postedBy)) { postedBy = CodePlexStrings.UnknownUser; } string commentHeading = string.Format(Resources.CommentPostedByPersonXOnDateY, postedBy, comment.PostedDate.ToString("d")); stringBuilder .AppendLine(commentHeading) .AppendLine(comment.Message) .AppendLine(); yield return(stringBuilder.ToString()); stringBuilder.Clear(); } // Format closing comment if any is specified. string closingComment = GetFormattedClosingComment(workItemDetails); if (!string.IsNullOrEmpty(closingComment)) { yield return(closingComment); } }
public async Task <IResponse> Send(IRequest request, CancellationToken cancellationToken = default(CancellationToken)) { ArgValidate.IsNotNull(request, nameof(request)); lock (lockObject) { DateTimeOffset requestTimeStamp = DateTimeOffset.UtcNow; // If we hit the request rate limit then we check to see if the oldest/first request on record occurred // more than one time interval ago. If this is the case we process the request immediately. Otherwise, // we wait until one time interval since our oldest request on record elapses. if (requestTimeStamps.Count == maxRequestsPerTimeInterval) { DateTimeOffset oldestRequestTimeStamp = requestTimeStamps.Peek(); TimeSpan timeSinceOldestRequest = requestTimeStamp - oldestRequestTimeStamp; if (timeSinceOldestRequest < timeInterval) { Task.Delay(oldestRequestTimeStamp + timeInterval - requestTimeStamp).Wait(cancellationToken); } requestTimeStamps.Dequeue(); } // We need to add the time *right now* cause this is (almost) the time the current request is getting processed. requestTimeStamps.Enqueue(DateTimeOffset.UtcNow); } return(await httpClient.Send(request, cancellationToken)); }
/// <summary> /// Builds a string that is a hyper link in Markdown. /// </summary> public static string HyperLink(string anchorText, Uri url) { ArgValidate.IsNotNullNotEmpty(anchorText, nameof(anchorText)); ArgValidate.IsNotNull(url, nameof(url)); return($"[{anchorText}]({url})"); }
/// <summary> /// Checks that the string is not null or empty. Throws either ArgumentNullExcpetion or ArgumentException. /// </summary> public static void IsNotNullNotEmpty(string s, string argName) { ArgValidate.IsNotNull(s, argName); if (string.IsNullOrEmpty(s)) { throw new ArgumentException(message: Resources.StringCannotBeEmpty, paramName: argName); } }
/// <summary> /// Creates a <see cref="CodePlexWorkItemReader"/> object. /// </summary> public CodePlexWorkItemReader(string project, bool includeClosedWorkItems, IHttpClient httpClient) { ArgValidate.IsNotNullNotEmptyNotWhiteSpace(project, nameof(project)); ArgValidate.IsNotNull(httpClient, nameof(httpClient)); this.project = project; this.includeClosedWorkItems = includeClosedWorkItems; this.httpClient = httpClient; }
/// <summary> /// Writes some of the work item information to the console. /// </summary> public Task WriteWorkItemAsync(WorkItemDetails workItemDetails) { ArgValidate.IsNotNull(workItemDetails, nameof(workItemDetails)); ArgValidate.IsNotNull(workItemDetails.WorkItem, nameof(workItemDetails.WorkItem)); string output = $"{workItemDetails.WorkItem.Id}{Environment.NewLine}{workItemDetails.WorkItem.Summary}{Environment.NewLine}{workItemDetails.WorkItem.Description}{Environment.NewLine}{Environment.NewLine}"; Console.WriteLine(output); return(Task.FromResult(false)); }
/// <summary> /// Creates a <see cref="GitHubRepoIssueReaderWriter"/> object. This object is used to interface with GitHub to manage issues that are being migrated. /// </summary> public GitHubRepoIssueReaderWriter(string repoOwner, string repo, IIssuesClient issues, ISearchClient search) { ArgValidate.IsNotNullNotEmptyNotWhiteSpace(repoOwner, nameof(repoOwner)); ArgValidate.IsNotNullNotEmptyNotWhiteSpace(repo, nameof(repo)); ArgValidate.IsNotNull(issues, nameof(issues)); ArgValidate.IsNotNull(search, nameof(search)); this.repoOwner = repoOwner; this.repo = repo; this.issues = issues; this.search = search; }
/// <summary> /// Searches for issue in GitHub and updates it. /// </summary> /// <remarks> /// This is only expected to happen if the initial write failed on a previous run. /// </remarks> public Task UpdateWorkItemAsync(WorkItemDetails workItemDetails) { ArgValidate.IsNotNull(workItemDetails, nameof(workItemDetails)); ArgValidate.IsNotNull(workItemDetails.WorkItem, nameof(workItemDetails.WorkItem)); return(InvokeAsync( async() => { Issue issue = await GetCorrespondingIssueAsync(workItemDetails.WorkItem.Id); await UpdateIssueAsync(issue, workItemDetails); return true; // This return statement is only here because we're expected to return Task<T>. })); }
/// <summary> /// Writes <paramref name="workItemDetails"/> as a new issue to GitHub. /// </summary> public Task WriteWorkItemAsync(WorkItemDetails workItemDetails) { ArgValidate.IsNotNull(workItemDetails, nameof(workItemDetails)); ArgValidate.IsNotNull(workItemDetails.WorkItem, nameof(workItemDetails.WorkItem)); return(InvokeAsync( async() => { Issue createdIssue = await CreateNewIssueAsync(workItemDetails.WorkItem, workItemDetails.FileAttachments); await SetIssueDetailsAsync(createdIssue, workItemDetails); return true; // This return statement is only here because we're expected to return Task<T>. })); }
public RateLimitingHttpClientAdapter(Octokit.Internal.IHttpClient httpClient, TimeSpan timeInterval, int maxRequestsPerTimeInterval) { ArgValidate.IsNotNull(httpClient, nameof(httpClient)); ArgValidate.IsInRange(maxRequestsPerTimeInterval, nameof(maxRequestsPerTimeInterval), min: 1, max: Int32.MaxValue); if (timeInterval <= TimeSpan.Zero) { throw new ArgumentException(message: Resources.TimeIntervalMustBeGreaterThanZero, paramName: nameof(timeInterval)); } this.httpClient = httpClient; this.timeInterval = timeInterval; this.maxRequestsPerTimeInterval = maxRequestsPerTimeInterval; requestTimeStamps = new Queue <DateTimeOffset>(); lockObject = new object(); }
/// <summary> /// Gets the details about an individual work item. /// </summary> public async Task <WorkItemDetails> GetWorkItemAsync(WorkItemSummary workItem) { ArgValidate.IsNotNull(workItem, nameof(workItem)); WorkItemDetails workItemToReturn = null; string detailedUrl = string.Format(IssuesDetailsUrlTemplate, project, workItem.Id); try { string workItemJson = await httpClient.DownloadStringAsync(detailedUrl); workItemToReturn = JsonConvert.DeserializeObject <WorkItemDetails>(workItemJson); } catch (JsonReaderException ex) { // If the object coming back from CodePlex cannot be parsed, something went wrong // on network so indicate to the calling method this could be retried. throw new HttpRequestFailedException(ex.Message, ex); } return(workItemToReturn); }
/// <summary> /// Builds the string to write as the main body of a GitHub issue. /// </summary> public static string GetFormattedWorkItemBody(WorkItem workItem, IEnumerable <WorkItemFileAttachment> attachments) { ArgValidate.IsNotNull(workItem, nameof(workItem)); var stringBuilder = new StringBuilder() .AppendLine(workItem.PlainDescription); string attachmentLinks = GetFormattedAttachmentHyperlinks(attachments); if (!string.IsNullOrEmpty(attachmentLinks)) { stringBuilder .AppendLine() .Append(attachmentLinks); } return(stringBuilder .AppendLine() .Append(GetFormattedCodePlexWorkItemDetails(workItem)) .ToString()); }
/// <summary> /// Returns the CodePlex work item ID from the body of a GitHub issue. /// </summary> public static int GetCodePlexWorkItemId(string issueBody) { ArgValidate.IsNotNull(issueBody, nameof(issueBody)); Match match = CodePlexWorkItemIdRegex.Match(issueBody); if (match.Success) { string idString = match.Groups["ID"].Value; try { return(Convert.ToInt32(idString)); } catch (Exception ex) when(ex is FormatException || ex is OverflowException) { throw new WorkItemIdentificationException(string.Format(Resources.InvalidCodePlexWorkItemId, idString), ex); } } throw new WorkItemIdentificationException(Resources.CodePlexWorkItemIdNotFoundInGitHubIssueBody); }
/// <summary> /// Checks whether the work item is closed. /// </summary> public static bool IsClosed(this WorkItem workItem) { ArgValidate.IsNotNull(workItem, nameof(workItem)); return(string.Equals(workItem.Status?.Name, CodePlexStrings.Closed, StringComparison.OrdinalIgnoreCase)); }
/// <summary> /// Migrates all work items from <paramref name="source"/> to <paramref name="destination"/>. /// </summary> public static async Task MigrateAsync( IWorkItemSource source, IWorkItemDestination destination, MigrationSettings settings, ILogger logger) { ArgValidate.IsNotNull(source, nameof(source)); ArgValidate.IsNotNull(destination, nameof(destination)); ArgValidate.IsNotNull(logger, nameof(logger)); ArgValidate.IsNotNull(settings, nameof(settings)); logger.LogMessage(LogLevel.Info, Resources.BeginMigrationMessage); try { // Retrive all work items that have already been migrated. logger.LogMessage(LogLevel.Info, Resources.LookupMigratedWorkItemsMessage); IReadOnlyList <MigratedWorkItem> migratedWorkItems = await destination.GetMigratedWorkItemsAsync(); IDictionary <int, MigrationState> migratedWorkItemState = GetWorkItemDictionary(migratedWorkItems); // Update the skip list to include any work items the user indicated we should not retrieve. AddSkipItemsToMigrateState(settings.WorkItemsToSkip, migratedWorkItemState); // Get the list of potential work items to migrate. logger.LogMessage(LogLevel.Info, Resources.LookupWorkItemsToMigrate); IReadOnlyList <WorkItemSummary> notMigratedWorkItems = await source.GetWorkItemsAsync(id => GetMigrationState(migratedWorkItemState, id) != MigrationState.Migrated); // Get the actual list of work items to migrate taking into account the limit specified in migration settings. int countToMigrate = settings.MaxItemsToMigrate == -1 ? notMigratedWorkItems.Count : settings.MaxItemsToMigrate; logger.LogMessage(LogLevel.Info, Resources.StartingMigrationOfXWorkItems, countToMigrate); logger.LogMessage(LogLevel.Warning, Resources.ProgressOfMigrationWillBeSlow); IEnumerable <WorkItemSummary> workItemsToMigrate = notMigratedWorkItems.Take(countToMigrate); int migratedWorkItemCount = 0; // Download the details from CodePlex and push it to the destination. foreach (WorkItemSummary workItemSummary in workItemsToMigrate) { logger.LogMessage(LogLevel.Trace, Resources.LookupIndividualWorkItem, workItemSummary.Id); WorkItemDetails item = null; await RetryAsync( async() => item = await source.GetWorkItemAsync(workItemSummary), settings.MaxRetryCount, settings.RetryDelay); if (GetMigrationState(migratedWorkItemState, workItemSummary.Id) == MigrationState.PartiallyMigrated) { logger.LogMessage(LogLevel.Trace, Resources.UpdatingWorkItem, workItemSummary.Id); await RetryAsync(() => destination.UpdateWorkItemAsync(item), settings.MaxRetryCount, settings.RetryDelay); } else { logger.LogMessage(LogLevel.Trace, Resources.AddingWorkItem, workItemSummary.Id); await RetryAsync(() => destination.WriteWorkItemAsync(item), settings.MaxRetryCount, settings.RetryDelay); } logger.LogMessage(LogLevel.Info, Resources.SuccessfullyMigratedWorkItemIdXTitleY, workItemSummary.Id, ++migratedWorkItemCount, countToMigrate, workItemSummary.Title); } } catch (Exception ex) { logger.LogMessage(LogLevel.Error, Resources.LogExceptionMessage, ex.Message); throw; } logger.LogMessage(LogLevel.Info, Resources.MigrationCompletedSuccessfully); }