/// <summary> /// Adds an attachment to code review. /// </summary> /// <param name="context"> Database context. </param> /// <param name="userName"> User alias. </param> /// <param name="cl"> Change list to modify. </param> /// <param name="link"> The file or web page URL. </param> /// <param name="linkDescr"> The text (optional). </param> private static void AddAttachment(CodeReviewDataContext context, string userName, string cl, string link, string linkDescr) { var changeListQuery = from ch in context.ChangeLists where ch.CL.Equals(cl) && ch.UserName.Equals(userName) && ch.Stage == 0 select ch.Id; if (changeListQuery.Count() != 1) { Console.WriteLine("No active change in database."); return; } int cid = changeListQuery.Single(); int? result = null; context.AddAttachment(cid, linkDescr, link, ref result); Console.WriteLine("Attachment submitted."); }
/// <summary> /// Main driver for the code review submission tool. /// </summary> /// <param name="context"> The database context. </param> /// <param name="sd"> Source control client. </param> /// <param name="sourceControlInstanceId"> The ID of source control instance to use. /// This is an ID of a record in the database that is unique for a given CL namespace.</param> /// <param name="changeList"> CL. </param> /// <param name="reviewers"> The list of people to who to send the code review request. </param> /// <param name="invitees"> The list of people who are invited to participate in the code review /// (but need to positively acknowledge the invitation by choosing to review the code). </param> /// <param name="link"> Optionally, a link to a file or a web page to be displayed in CL page. </param> /// <param name="linkDescr"> An optional description of the said link. </param> /// <param name="description"> An optional description of the changelist, overrides any description /// from the source control tool. </param> /// <param name="bugTracker">The server for accessing bugs.</param> /// <param name="bugIds">List of bugs to associate with review page.</param> /// <param name="force"> If branched files are included, confirms the submission even if there /// are too many files. </param> /// <param name="includeBranchedFiles"> </param> /// <param name="preview">If true, do not commit changes.</param> private static void ProcessCodeReview( string databaseServer, ISourceControl sd, int sourceControlInstanceId, string changeList, List<string> reviewers, List<string> invitees, string link, string linkDescr, string description, IBugServer bugServer, List<string> bugIds, bool force, bool includeBranchedFiles, bool preview, string impersonateUserName) { Change change = sd.GetChange(changeList, includeBranchedFiles); if (change == null) return; changeList = change.ChangeListFriendlyName ?? changeList; if (change == null) return; if (includeBranchedFiles && !force) { int branchedFiles = 0; foreach (SourceControl.ChangeFile file in change.Files) { if (file.IsText && (file.Action == SourceControl.ChangeFile.SourceControlAction.BRANCH || file.Action == SourceControl.ChangeFile.SourceControlAction.INTEGRATE)) ++branchedFiles; } if (branchedFiles > MaximumIntegratedFiles) { Console.WriteLine("There are {0} branched/integrated files in this change.", branchedFiles); Console.WriteLine("Including the full text of so many files in review may increase the size"); Console.WriteLine("of the review database considerably."); Console.Write("Are you sure you want to proceed (Yes/No)? "); string response = Console.ReadLine(); Console.WriteLine("NOTE: In the future you can override this check by specifying --force"); Console.WriteLine("on the command line."); if (response[0] != 'y' && response[0] != 'Y') return; } } if (!VerifyDiffIntegrity(change)) return; CodeReviewDataContext context = new CodeReviewDataContext("Data Source=" + databaseServer + ";Initial Catalog=CodeReview;Integrated Security=True"); var existingReviewQuery = from rv in context.ChangeLists where rv.CL == changeList && rv.SourceControlId == sourceControlInstanceId select rv; // is this a new review, or a refresh of an existing one? bool isNewReview = (existingReviewQuery.Count() == 0); int? changeId = null; if (description == null) description = change.Description; context.Connection.Open(); using (context.Connection) using (context.Transaction = context.Connection.BeginTransaction(System.Data.IsolationLevel.Snapshot)) { // This more like "GetOrAddChangeList", as it returns the id of any pre-existing changelist // matching 'changeList'. if (impersonateUserName == null) { context.AddChangeList( sourceControlInstanceId, change.SdClientName, changeList, description, change.TimeStamp.ToUniversalTime(), ref changeId); } else { var changeListDb = (from c in context.ChangeLists where c.SourceControlId == sourceControlInstanceId && c.UserName == impersonateUserName && c.UserClient == change.SdClientName && c.CL == changeList select c).FirstOrDefault(); if (changeListDb == null) { changeListDb = new ChangeList() { SourceControlId = sourceControlInstanceId, UserName = impersonateUserName, UserClient = change.SdClientName, CL = changeList, Description = description, TimeStamp = change.TimeStamp.ToUniversalTime(), Stage = 0 }; context.ChangeLists.InsertOnSubmit(changeListDb); context.SubmitChanges(); // Not actually submitted until transaction completes. } changeId = changeListDb.Id; } // Get the list of files corresponding to this changelist already on the server. var dbChangeFiles = (from fl in context.ChangeFiles where fl.ChangeListId == changeId && fl.IsActive select fl) .OrderBy(file => file.ServerFileName) .GetEnumerator(); var inChangeFiles = (from fl in change.Files select fl) .OrderBy(file => file.ServerFileName) .GetEnumerator(); bool dbChangeFilesValid = dbChangeFiles.MoveNext(); bool inChangeFilesValid = inChangeFiles.MoveNext(); // Uses bitwise OR to ensure that both MoveNext methods are invoked. FileExistsIn existsIn = FileExistsIn.Neither; while (dbChangeFilesValid || inChangeFilesValid) { int comp; if (!dbChangeFilesValid) // No more files in database comp = 1; else if (!inChangeFilesValid) // No more files in change list. comp = -1; else comp = string.Compare(dbChangeFiles.Current.ServerFileName, inChangeFiles.Current.ServerFileName); if (comp < 0) // We have a file in DB, but not in source control. Delete it from DB. { Console.WriteLine("File {0} has been dropped from the change list.", dbChangeFiles.Current.ServerFileName); context.RemoveFile(dbChangeFiles.Current.Id); dbChangeFilesValid = dbChangeFiles.MoveNext(); existsIn = FileExistsIn.Database; continue; } SourceControl.ChangeFile file = inChangeFiles.Current; int? fid = null; if (comp > 0) // File in source control, but not in DB { Console.WriteLine("Adding file {0}", file.ServerFileName); context.AddFile(changeId, file.LocalFileName, file.ServerFileName, ref fid); existsIn = FileExistsIn.Change; } else // Both files are here. Need to check the versions. { fid = dbChangeFiles.Current.Id; existsIn = FileExistsIn.Both; } bool haveBase = (from bv in context.FileVersions where bv.FileId == fid && bv.Revision == file.Revision && bv.IsRevisionBase select bv).Count() > 0; var versionQuery = from fv in context.FileVersions where fv.FileId == fid && fv.Revision == file.Revision orderby fv.Id descending select fv; var version = versionQuery.FirstOrDefault(); bool haveVersion = false; if (version != null && version.Action == (int)file.Action && BodiesEqual(NormalizeLineEndings(file.Data), NormalizeLineEndings(version.Text))) haveVersion = true; int? vid = null; if (!haveBase && file.IsText && (file.Action == SourceControl.ChangeFile.SourceControlAction.EDIT || (file.Action == SourceControl.ChangeFile.SourceControlAction.INTEGRATE && includeBranchedFiles))) { string fileBody; DateTime? dateTime; fileBody = sd.GetFile( file.OriginalServerFileName == null ? file.ServerFileName : file.OriginalServerFileName, file.Revision, out dateTime); if (fileBody == null) { Console.WriteLine("ERROR: Could not retrieve {0}#{1}", file.ServerFileName, file.Revision); return; } Console.WriteLine("Adding base revision for {0}#{1}", file.ServerFileName, file.Revision); context.AddVersion(fid, file.Revision, (int)file.Action, dateTime, true, true, true, fileBody, ref vid); } else { // Do this so we print the right thing. haveBase = true; } if (!haveVersion) { if (file.Action == SourceControl.ChangeFile.SourceControlAction.DELETE) { context.AddVersion( fid, file.Revision, (int)file.Action, null, file.IsText, false, false, null, ref vid); } else if ((file.Action == SourceControl.ChangeFile.SourceControlAction.RENAME) || !file.IsText) { context.AddVersion(fid, file.Revision, (int)file.Action, file.LastModifiedTime, file.IsText, false, false, null, ref vid); } else if (file.Action == SourceControl.ChangeFile.SourceControlAction.ADD || file.Action == SourceControl.ChangeFile.SourceControlAction.BRANCH) { context.AddVersion(fid, file.Revision, (int)file.Action, file.LastModifiedTime, file.IsText, true, false, file.Data, ref vid); } else if (file.Action == SourceControl.ChangeFile.SourceControlAction.EDIT || file.Action == SourceControl.ChangeFile.SourceControlAction.INTEGRATE) { context.AddVersion(fid, file.Revision, (int)file.Action, file.LastModifiedTime, file.IsText, false, false, file.Data, ref vid); } string textFlag = file.IsText ? "text" : "binary"; string action; switch (file.Action) { case SourceControl.ChangeFile.SourceControlAction.ADD: action = "add"; break; case SourceControl.ChangeFile.SourceControlAction.EDIT: action = "edit"; break; case SourceControl.ChangeFile.SourceControlAction.DELETE: action = "delete"; break; case SourceControl.ChangeFile.SourceControlAction.BRANCH: action = "branch"; break; case SourceControl.ChangeFile.SourceControlAction.INTEGRATE: action = "integrate"; break; case SourceControl.ChangeFile.SourceControlAction.RENAME: action = "rename"; break; default: action = "unknown"; break; } if (version != null && vid == version.Id) { // The file was already there. This happens sometimes because SQL rountrip (to database // and back) is not an identity: somtimes the non-graphical characters change depending // on the database code page. But if the database has returned a number, and this number // is the same as the previous version id, we know that the file has not really been added. haveVersion = true; } else { Console.WriteLine("Added version for {0}#{1}({2}, {3})", file.ServerFileName, file.Revision, textFlag, action); } } if (haveBase && haveVersion) Console.WriteLine("{0} already exists in the database.", file.ServerFileName); if ((existsIn & FileExistsIn.Database) == FileExistsIn.Database) dbChangeFilesValid = dbChangeFiles.MoveNext(); if ((existsIn & FileExistsIn.Change) == FileExistsIn.Change) inChangeFilesValid = inChangeFiles.MoveNext(); existsIn = FileExistsIn.Neither; } foreach (string reviewer in reviewers) { int? reviewId = null; context.AddReviewer(reviewer, changeId.Value, ref reviewId); } foreach (string invitee in invitees) { context.AddReviewRequest(changeId.Value, invitee); } if (link != null) { int? attachmentId = null; context.AddAttachment(changeId.Value, linkDescr, link, ref attachmentId); } if (preview) context.Transaction.Rollback(); else context.Transaction.Commit(); } var reviewSiteUrl = Environment.GetEnvironmentVariable("REVIEW_SITE_URL"); if (reviewSiteUrl != null) { var reviewPage = reviewSiteUrl; if (!reviewPage.EndsWith("/")) reviewPage += "/"; reviewPage += @"default.aspx?cid=" + changeId.ToString(); Console.WriteLine("Change {0} is ready for review, and may be viewed at", changeList); Console.WriteLine(" {0}", reviewPage); var allBugIds = Enumerable.Union(change.BugIds, bugIds); if (bugServer != null && allBugIds.Count() > 0) { Console.WriteLine("Connecting to TFS Work Item Server"); if (bugServer.Connect()) { foreach (var bugId in allBugIds) { var bug = bugServer.GetBug(bugId); if (bug.AddLink(new Uri(reviewPage), null)) Console.WriteLine("Bug {0} has been linked to review page.", bugId); } } } } else { Console.WriteLine("Change {0} is ready for review.", changeList); if (isNewReview) { if (reviewers.Count == 0 && invitees.Count == 0) { Console.WriteLine("Note: no reviewers specified. You can add them later using this utility."); } else { Console.WriteLine("If the mail notifier is enabled, the reviewers will shortly receive mail"); Console.WriteLine("asking them to review your changes."); } } else { Console.WriteLine("Note: existing reviewers will not be immediately informed of this update."); Console.WriteLine("To ask them to re-review your updated changes, you can visit the review website"); Console.WriteLine("and submit a response."); } } if (preview) Console.WriteLine("In preview mode -- no actual changes committed."); }