Автоматические запросы CodeReview в TFS 2013

TFS2013 имеет очень удобный функционал Code Review - каждый член команды может сделать запрос на ревью для одного или нескольких членов команды. Мы задались вопросом можно ли такие Code Review создавать автоматически при комите в систему контроля версий.

Для того, чтобы реализовать эту функциональность мы воспользовались серверными событиями (TFS Server Side Events). Их суть предельно проста - мы создаем объект-подписчик, метод которого будет вызываться, когда в TFS что-то происходит, и если тип события тот, который нас устраивает, то обрабатываем эту ситуацию.

Для того, чтобы создать такой объект-подписчик достаточно создать новую сборку, в которой создать объект, реализующий интерфейс ISubscriber из пространства имен Microsoft.TeamFoundation.Framework.Server.

Предварительно следует добавить ссылки на следующие сборки:

  • C:\Program Files (x86)\Microsoft Visual Studio 12.0\Common7\IDE\ReferenceAssemblies\v2.0\Microsoft.TeamFoundation.Client.dll
  • C:\Program Files (x86)\Microsoft Visual Studio 12.0\Common7\IDE\ReferenceAssemblies\v2.0\Microsoft.TeamFoundation.Common.dll
  • C:\Program Files\Microsoft Team Foundation Server 12.0\Tools\Microsoft.TeamFoundation.Framework.Server.dll
  • C:\Program Files\Microsoft Team Foundation Server 12.0\Tools\Microsoft.TeamFoundation.Server.Core.dll
  • C:\Program Files\Microsoft Team Foundation Server 12.0\Tools\Microsoft.TeamFoundation.VersionControl.Server.dll
  • C:\Program Files (x86)\Microsoft Visual Studio 12.0\Common7\IDE\ReferenceAssemblies\v2.0\Microsoft.TeamFoundation.WorkItemTracking.Client.dll
  • C:\Program Files (x86)\Microsoft Visual Studio 12.0\Common7\IDE\ReferenceAssemblies\v4.5\Microsoft.VisualStudio.Services.WebApi.dll

Эти сборки содержат необходимые нам объекты, включая интерфейс ISubscriber. Заглушка нашего класса будет выглядеть следующим образом:

public class CodeReviewSubscriber : ISubscriber
{
  public string Name
  {
    get { return "Automatic Code Reviews"; }
  }

  public Type[] SubscribedTypes()
  {
    return new Type[] { };
  }

  public SubscriberPriority Priority
  {
    get { return SubscriberPriority.Normal; }
  }

  public EventNotificationStatus ProcessEvent(TeamFoundationRequestContext requestContext, NotificationType notificationType, object notificationEventArgs, out int statusCode, out string statusMessage, out Microsoft.TeamFoundation.Common.ExceptionPropertyCollection properties)
  {
    // ...
  }
}

Для того, чтобы наш подписчик стал реагировать на комиты, необходимо чтобы метод SubscribedTypes() возвращал соответствующий тип уведомления.

Итого, пустая заглушка будет выглядеть так —

public class CodeReviewSubscriber : ISubscriber
{
  public string Name
  {
    get { return "Automatic Code Reviews"; }
  }

  public Type[] SubscribedTypes()
  {
    return new Type[] { typeof(CheckinNotification) };
  }

  public SubscriberPriority Priority
  {
    get { return SubscriberPriority.Normal; }
  }

  public EventNotificationStatus ProcessEvent(TeamFoundationRequestContext requestContext, NotificationType notificationType, object notificationEventArgs, out int statusCode, out string statusMessage, out Microsoft.TeamFoundation.Common.ExceptionPropertyCollection properties)
  {
    statusCode = 0;
    statusMessage = String.Empty;
    properties = null;

    return EventNotificationStatus.ActionPermitted;
  }
}

Добавим логику создания запроса на ревью, получим что-то подобное на это —

public class CodeReviewSubscriber : ISubscriber
{
  private const string LogFilePath = @"C:\log\tfs.log";
  private const string ProjectName = @"TestProject";
  private const string ReviewerName = @"Sergey";

  public string Name
  {
    get { return "Automatic Code Reviews"; }
  }

  public Type[] SubscribedTypes()
  {
    return new[] { typeof(CheckinNotification) };
  }

  public SubscriberPriority Priority
  {
    get { return SubscriberPriority.Normal; }
  }

  public EventNotificationStatus ProcessEvent(TeamFoundationRequestContext requestContext, NotificationType notificationType, object notificationEventArgs, out int statusCode, out string statusMessage, out Microsoft.TeamFoundation.Common.ExceptionPropertyCollection properties)
  {
    try
    {
      if ((notificationType == NotificationType.Notification) &&
          (notificationEventArgs is CheckinNotification))
      {
        CheckinNotification checkinNotification = (CheckinNotification)notificationEventArgs;

        TeamFoundationLocationService tfsService = requestContext.GetService<TeamFoundationLocationService>();
        Uri tfsServiceUri = tfsService.GetSelfReferenceUri(requestContext, tfsService.GetDefaultAccessMapping(requestContext));
        TfsTeamProjectCollection tfsServiceCollection = new TfsTeamProjectCollection(tfsServiceUri);


        IIdentityManagementService identityManagementService = tfsServiceCollection.GetService<IIdentityManagementService>();
        TeamFoundationIdentity identity = identityManagementService.ReadIdentity(IdentitySearchFactor.AccountName, checkinNotification.ChangesetOwner.UniqueName, MembershipQuery.None, ReadIdentityOptions.None);
        TfsTeamProjectCollection projectCollection = new TfsTeamProjectCollection(tfsServiceCollection.Uri, identity.Descriptor);

        WorkItemStore workitemStore = projectCollection.GetService<WorkItemStore>();

        Project project = workitemStore.Projects[ProjectName];

        CreateReviewRequest(project, checkinNotification, workitemStore);
      }
    }
    catch (Exception ex)
    {
      WriteLog(ex.ToString());
    }

    statusCode = 0;
    statusMessage = String.Empty;
    properties = null;

    return EventNotificationStatus.ActionPermitted;
  }

  private void CreateReviewRequest(Project project, CheckinNotification checkinNotification, WorkItemStore workitemStore)
  {
    WorkItemType type = project.WorkItemTypes["Code Review Response"];
    WorkItem workItem = new WorkItem(type)
    {
      Title = checkinNotification.Comment
    };
    workItem.Fields["System.AssignedTo"].Value = ReviewerName;
    workItem.Fields["System.State"].Value = "Requested";
    workItem.Fields["System.Reason"].Value = "New";

    workItem.Validate();
    workItem.Save();

    int workitemId = workItem.Id;

    type = project.WorkItemTypes["Code Review Request"];
    workItem = new WorkItem(type) {Title = checkinNotification.Comment};
    workItem.Fields["System.AssignedTo"].Value = checkinNotification.ChangesetOwner.DisplayName;
    workItem.Fields["Microsoft.VSTS.CodeReview.ContextType"].Value = "Changeset";
    workItem.Fields["Microsoft.VSTS.CodeReview.Context"].Value = checkinNotification.Changeset;
    workItem.Fields["System.AreaPath"].Value = project.Name;
    workItem.Fields["System.IterationPath"].Value = project.Name;
    workItem.Fields["System.State"].Value = "Requested";
    workItem.Fields["System.Reason"].Value = "New";
    WorkItemLinkTypeEnd linkTypeEnd = workitemStore.WorkItemLinkTypes.LinkTypeEnds["Child"];
    workItem.Links.Add(new RelatedLink(linkTypeEnd, workitemId));

    workItem.Validate();

    workItem.Save();
  }

  private void WriteLog(string message)
  {
    File.AppendAllText(LogFilePath, message);
    File.AppendAllText(LogFilePath, Environment.NewLine);
  }
}

Этот код далек от совершенства, но показывает суть происходящего процесса.

Теперь, когда наш подписчик готов, необходимо вклинить его в процесс работы TFS. Для этого полученную сборку следует поместить в папку C:\Program Files\Microsoft Team Foundation Server 12.0\Application Tier\Web Services\bin\Plugins (на сервере).

Теперь для каждого комита в TFS вы получите запрос на ревью кода.