<reponame>mcgreevy/chromium-infra
# Copyright 2013 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Contains the revert_patchset view."""

import datetime
import hashlib
import re

from codereview import decorators as deco
from codereview import exceptions
from codereview import invert_patches
from codereview import models
from codereview import views
from codereview.responses import HttpTextResponse

from google.appengine.ext import ndb
from google.appengine.runtime import DeadlineExceededError

from django.core.urlresolvers import reverse
from django.http import HttpResponseRedirect


# Constants for patch checks.
LARGE_PATCH_CHARS_THRESHOLD = 500000
MAX_LARGE_PATCHES_REVERSIBLE = 5


ERROR_MSG_POSTPEND = ' Please revert manually.\nSorry for the inconvenience.'


def _get_revert_description(request, revert_reason, reviewers, original_issue,
                            original_patch_num, original_patchset_id):
  """Creates and returns a description for the revert CL."""
  revert_description = []
  # Contain link to original CL.
  original_issue_link = request.build_absolute_uri(
      reverse('codereview.views.show', args=[original_issue.key.id()]))
  revert_description.append('%s (patchset #%s id:%s of %s )' % (
      _get_revert_subject(original_issue.subject), original_patch_num,
      original_patchset_id, original_issue_link))
  # Display the reason for reverting.
  revert_description.append('')  # Extra new line to separate sections.
  revert_description.append('Reason for revert:')
  revert_description.append('%s' % revert_reason)

  # Add the original issue's decription.
  revert_description.append('')  # Extra new line to separate sections.
  revert_description.append('Original issue\'s description:')
  for line in original_issue.description.split('\n'):
    revert_description.append('> %s' % line)

  # TBR original author + reviewers.
  revert_description.append('')  # Extra new line to separate sections.
  revert_description.append('TBR=%s' % ','.join(
      [str(reviewer) for reviewer in reviewers]))

  if models.can_skip_checks_on_revert(original_issue.get_time_since_landed()):
    revert_description.append(
        '# Skipping CQ checks because original CL landed less than '
        '%d days ago.' % models.REVERT_MAX_AGE_FOR_CHECKS_BYPASSING.days)
    # Do not run presubmit on the revert CL.
    revert_description.append('NOPRESUBMIT=true')
    # Skip tree status checks.
    revert_description.append('NOTREECHECKS=true')
    # Do not run trybots on the revert CL.
    revert_description.append('NOTRY=true')
  else:
    revert_description.append(
        '# Not skipping CQ checks because original CL landed more than '
        '%d days ago.' % models.REVERT_MAX_AGE_FOR_CHECKS_BYPASSING.days)

  # Check to see if the original description contains "BUG=" if it does then
  # use it in the revert description.
  match_bugline = re.search(
    r'^\s*(BUG=.*)$', original_issue.description or '', re.M | re.I)
  if match_bugline:
    revert_description.append('%s' % match_bugline.groups(0))

  return '\n'.join(revert_description)


def _get_revert_subject(original_subject):
  """Creates and returns a subject for the revert CL.

  If the original CL was a revert CL then 'Revert of' is replaced with
  'Reland of'.
  If the original CL was a reland CL then 'Reland of' is replaced
  with 'Revert of'.
  All other CLs get 'Revert of' prefixed to the subject.
  """
  if original_subject.startswith('Revert of '):
    # This is going to be a revert of a revert, i.e. a reland.
    original_subject = original_subject.lstrip('Revert of ')
    return 'Reland of %s' % original_subject
  elif original_subject.startswith('Reland of '):
    # This is going to be a revert of a reland, i.e. a revert.
    original_subject = original_subject.lstrip('Reland of ')
  return 'Revert of %s' % original_subject


def check_patches_reversable(patches):
  """Check to see if the specified patches are reversible.

    Returns True if it is else throws an Error with why it is not reversible.
  """
  large_patches = 0
  for patch in patches:

    # If the patch status is 'None' it could be due to an incompletely uploaded
    # original patch.
    if not patch.status:
      raise exceptions.RevertError('Found a None patch status. The original '
          'patchset may have been incompletely uploaded.')

    # Collect the number of large patches.
    if len(patch.text) > LARGE_PATCH_CHARS_THRESHOLD:
      large_patches += 1

    # Only git patches are supported.
    diff_header = invert_patches.split_header(patch.text)[0]
    if not invert_patches.is_git_diff_header(diff_header):
      raise exceptions.RevertError('Can only invert Git patches.')

  if large_patches > MAX_LARGE_PATCHES_REVERSIBLE:
    raise exceptions.RevertError('The Patchset is too large to invert.')
  return True


@deco.xsrf_required
@deco.patchset_editor_required
def revert_patchset(request):
  """/api/<issue>/<patchset>/revert_patchset - Create an inverted changeset."""

  if request.method != 'POST':
    return HttpTextResponse(
        '/revert_issue only supports POST.', status=403)

  # Find the patches of the issue we want to revert.
  original_issue = request.issue
  original_patch_num = len(list(original_issue.patchsets))
  original_patchset = request.patchset
  original_patchset_id = original_patchset.key.id()
  original_patches = original_patchset.patches

  # Make sure the original issue is closed.
  assert original_issue.closed, 'The original issue must be closed.'

  # Validate that all original patches are supported by /revert_patchset.
  try:
    check_patches_reversable(original_patches)
  except exceptions.RevertError, e:
    return HttpTextResponse(e.message + ERROR_MSG_POSTPEND, status=404)

  # Create the new revert issue to use as the key in the new patchset.
  first_id, _ = models.Issue.allocate_ids(1)
  issue_key = ndb.Key(models.Issue, first_id)

  reviewers = original_issue.reviewers
  original_owner = original_issue.owner
  # Add the original_owner to the list of reviewers if different from the
  # current user.
  if original_owner.email() != request.user.email():
    reviewers.append(original_owner.email())
  # Remove current user from the reviewers.
  if request.user.email() in reviewers:
    reviewers.remove(request.user.email())

  # Datastructure that will hold all pending Issue, PatchSet, Patches and
  # Contents of the revert Issue.
  pending_commits = []

  subject = _get_revert_subject(original_issue.subject)
  revert_reason = request.POST['revert_reason']
  revert_cq = request.POST['revert_cq'] == '1'
  description = _get_revert_description(
      request, revert_reason, reviewers, original_issue, original_patch_num,
      original_patchset_id)
  issue = models.Issue(subject=subject,
                       description=description,
                       project=original_issue.project,
                       base=original_issue.base,
                       target_ref=original_issue.target_ref,
                       repo_guid=original_issue.repo_guid,
                       reviewers=reviewers,
                       cc=original_issue.cc,
                       private=original_issue.private,
                       n_comments=0,
                       commit=False,  # Do not check the commit box yet.
                       key=issue_key)
  pending_commits.append(issue)

  # Create the new revert patchset to use as the key in the new patches.
  ps_id, _ = models.PatchSet.allocate_ids(1, parent=issue.key)
  ps_key = ndb.Key(models.PatchSet, ps_id, parent=issue.key)
  patchset = models.PatchSet(
      issue_key=issue.key,
      url=None,
      key=ps_key)
  patchset_data = []  # Will be populated with the diff texts of all patches.
  pending_commits.append(patchset)

  # Loop through all the original patches and create inversions.
  for original_patch in original_patches:

    try:
      patch = original_patch.make_inverted(patchset)
    except exceptions.FetchError, e:
      return HttpTextResponse(
          'Revert failed: ' + e.message + '\nThis sometimes happens when a '
          'file\'s contents are not uploaded because it is too large.' +
          ERROR_MSG_POSTPEND, status=500)
    except DeadlineExceededError:
      return HttpTextResponse(
          'The patchset is too large to invert. Please revert manually.\n'
          'Sorry for the inconvenience.', status=500)
    except exceptions.RevertError, e:
      return HttpTextResponse(e.message + ERROR_MSG_POSTPEND, status=404)

    # Find the original content and patched content.
    if original_patch.is_binary:
      original_content = original_patch.content_key.get()
      original_patched_content = (
          original_patch.patched_content_key.get()
          if original_patch.patched_content_key else None)
    else:
      original_content = original_patch.get_content()
      original_patched_content = original_patch.get_patched_content()

    # Allocate keys for content and patched_content.
    content_id, _ = models.Content.allocate_ids(1, parent=patch.key)
    content_key = ndb.Key(models.Content, content_id, parent=patch.key)
    patched_content_id, _ = models.Content.allocate_ids(1, parent=patch.key)
    patched_content_key = ndb.Key(
      models.Content, patched_content_id, parent=patch.key)

    if original_patched_content:
      content = models.Content(
        key=content_key,
        text=original_patched_content.text,
        data=original_patched_content.data,
        checksum=original_patched_content.checksum,
        is_uploaded=original_patched_content.is_uploaded,
        is_bad=original_patched_content.is_bad,
        file_too_large=original_patched_content.file_too_large)
    elif (original_content and
          invert_patches.is_patch_binary_copy_modify_with_no_change(
              original_patch)):
      # For binary patches with 100% similarity indexes use the original content
      # as the content. See crbug.com/525625
      content = models.Content(
        key=content_key,
        text=original_content.text,
        data=original_content.data,
        checksum=original_content.checksum,
        is_uploaded=original_content.is_uploaded,
        is_bad=original_content.is_bad,
        file_too_large=original_content.file_too_large)
    else:
      # Create an empty content if there is no patched_content.
      empty_data = ''
      content = models.Content(
        key=content_key,
        text='',
        data=empty_data,
        checksum=hashlib.md5(empty_data).hexdigest(),
        is_uploaded=True,
        is_bad=False,
        file_too_large=False)

    if original_patch.status != invert_patches.COPIED_AND_MODIFIED_STATUS:
      patched_content = models.Content(
          key=patched_content_key,
          text=original_content.text,
          data=original_content.data,
          checksum=original_content.checksum,
          is_uploaded=original_content.is_uploaded,
          is_bad=original_content.is_bad,
          file_too_large=original_content.file_too_large)
    else:
      # Create an empty patched content if it is an 'A +' status. This is
      # because the inverted status becomes a 'D' request.
      empty_data = ''
      patched_content = models.Content(
        key=patched_content_key,
        text='',
        data=empty_data,
        checksum=hashlib.md5(empty_data).hexdigest(),
        is_uploaded=True,
        is_bad=False,
        file_too_large=False)

    pending_commits.append(content)
    pending_commits.append(patched_content)

    patch.content_key = content.key
    patch.patched_content_key = patched_content.key
    pending_commits.append(patch)

    # Encode the unicode patch.txt to later put in patchset.data which is stored
    # as ndb.Blob (str). For more context see crbug.com/527325
    patch_text = patch.text.encode('utf-8')
    # Append the text of this patch to the patchset's data. Strip out ending
    # new line (if it exists) because one will be added during the below join.
    patchset_data.append(patch_text.rstrip('\n'))

  # Add all gathered patch texts to the patchset's data field.
  patchset.data = '\n'.join(patchset_data)
  # Patchset data needs to end with '\n' for 'git cl patch' to work. For more
  # context see: crbug.com/479881#c12
  if not patchset.data.endswith('\n'):
    patchset.data += '\n'

  # Commit the gathered revert Issue, PatchSet, Patches and Contents.
  _db_commit_all_pending_commits(pending_commits)

  # Notify the original issue that a revert issue has been created.
  revert_issue_link = request.build_absolute_uri(
      reverse('codereview.views.show', args=[issue.key.id()]))
  revert_message = (
      'A revert of this CL (patchset #%s id:%s) has been created in %s by '
      '%s.\n\nThe reason for reverting is: %s.' % (
          original_patch_num, original_patchset_id, revert_issue_link,
          request.user.email(), revert_reason))
  views.make_message(request, original_issue, revert_message,
                     send_mail=True).put()

  # Post a message saying who CQ'd the revert. This is critical for CQ, which is
  # using this message to determine when the attempt has been started.
  if revert_cq:
    views.make_message(
        request, issue, 'The CQ bit was checked by %s' % request.user.email(),
        auto_generated=True, send_mail=False).put()

  # Notify the revert issue recipients.
  views.make_message(request, issue, 'Created %s' % subject,
                     send_mail=True).put()

  # Now that all patchsets and patches have been committed check the commit box
  # if the revert_cq checkbox was checked.
  if revert_cq:
    issue.commit = True
  issue.put()

  if request.POST.get('no_redirect'):
    return HttpTextResponse(str(issue.key.id()), status=200)
  else:
    return HttpResponseRedirect(reverse('codereview.views.show',
                                        args=[issue.key.id()]))


def _db_commit_all_pending_commits(pending_commits):
  """Puts all pending commits into the DB."""
  for pending_commit in pending_commits:
    pending_commit.put()

# Copyright 2013 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Contains the revert_patchset view."""

import datetime
import hashlib
import re

from codereview import decorators as deco
from codereview import exceptions
from codereview import invert_patches
from codereview import models
from codereview import views
from codereview.responses import HttpTextResponse

from google.appengine.ext import ndb
from google.appengine.runtime import DeadlineExceededError

from django.core.urlresolvers import reverse
from django.http import HttpResponseRedirect


# Constants for patch checks.
LARGE_PATCH_CHARS_THRESHOLD = 500000
MAX_LARGE_PATCHES_REVERSIBLE = 5


ERROR_MSG_POSTPEND = ' Please revert manually.\nSorry for the inconvenience.'


def _get_revert_description(request, revert_reason, reviewers, original_issue,
                            original_patch_num, original_patchset_id):
  """Creates and returns a description for the revert CL."""
  revert_description = []
  # Contain link to original CL.
  original_issue_link = request.build_absolute_uri(
      reverse('codereview.views.show', args=[original_issue.key.id()]))
  revert_description.append('%s (patchset #%s id:%s of %s )' % (
      _get_revert_subject(original_issue.subject), original_patch_num,
      original_patchset_id, original_issue_link))
  # Display the reason for reverting.
  revert_description.append('')  # Extra new line to separate sections.
  revert_description.append('Reason for revert:')
  revert_description.append('%s' % revert_reason)

  # Add the original issue's decription.
  revert_description.append('')  # Extra new line to separate sections.
  revert_description.append('Original issue\'s description:')
  for line in original_issue.description.split('\n'):
    revert_description.append('> %s' % line)

  # TBR original author + reviewers.
  revert_description.append('')  # Extra new line to separate sections.
  revert_description.append('TBR=%s' % ','.join(
      [str(reviewer) for reviewer in reviewers]))

  if models.can_skip_checks_on_revert(original_issue.get_time_since_landed()):
    revert_description.append(
        '# Skipping CQ checks because original CL landed less than '
        '%d days ago.' % models.REVERT_MAX_AGE_FOR_CHECKS_BYPASSING.days)
    # Do not run presubmit on the revert CL.
    revert_description.append('NOPRESUBMIT=true')
    # Skip tree status checks.
    revert_description.append('NOTREECHECKS=true')
    # Do not run trybots on the revert CL.
    revert_description.append('NOTRY=true')
  else:
    revert_description.append(
        '# Not skipping CQ checks because original CL landed more than '
        '%d days ago.' % models.REVERT_MAX_AGE_FOR_CHECKS_BYPASSING.days)

  # Check to see if the original description contains "BUG=" if it does then
  # use it in the revert description.
  match_bugline = re.search(
    r'^\s*(BUG=.*)$', original_issue.description or '', re.M | re.I)
  if match_bugline:
    revert_description.append('%s' % match_bugline.groups(0))

  return '\n'.join(revert_description)


def _get_revert_subject(original_subject):
  """Creates and returns a subject for the revert CL.

  If the original CL was a revert CL then 'Revert of' is replaced with
  'Reland of'.
  If the original CL was a reland CL then 'Reland of' is replaced
  with 'Revert of'.
  All other CLs get 'Revert of' prefixed to the subject.
  """
  if original_subject.startswith('Revert of '):
    # This is going to be a revert of a revert, i.e. a reland.
    original_subject = original_subject.lstrip('Revert of ')
    return 'Reland of %s' % original_subject
  elif original_subject.startswith('Reland of '):
    # This is going to be a revert of a reland, i.e. a revert.
    original_subject = original_subject.lstrip('Reland of ')
  return 'Revert of %s' % original_subject


def check_patches_reversable(patches):
  """Check to see if the specified patches are reversible.

    Returns True if it is else throws an Error with why it is not reversible.
  """
  large_patches = 0
  for patch in patches:

    # If the patch status is 'None' it could be due to an incompletely uploaded
    # original patch.
    if not patch.status:
      raise exceptions.RevertError('Found a None patch status. The original '
          'patchset may have been incompletely uploaded.')

    # Collect the number of large patches.
    if len(patch.text) > LARGE_PATCH_CHARS_THRESHOLD:
      large_patches += 1

    # Only git patches are supported.
    diff_header = invert_patches.split_header(patch.text)[0]
    if not invert_patches.is_git_diff_header(diff_header):
      raise exceptions.RevertError('Can only invert Git patches.')

  if large_patches > MAX_LARGE_PATCHES_REVERSIBLE:
    raise exceptions.RevertError('The Patchset is too large to invert.')
  return True


@deco.xsrf_required
@deco.patchset_editor_required
def revert_patchset(request):
  """/api/<issue>/<patchset>/revert_patchset - Create an inverted changeset."""

  if request.method != 'POST':
    return HttpTextResponse(
        '/revert_issue only supports POST.', status=403)

  # Find the patches of the issue we want to revert.
  original_issue = request.issue
  original_patch_num = len(list(original_issue.patchsets))
  original_patchset = request.patchset
  original_patchset_id = original_patchset.key.id()
  original_patches = original_patchset.patches

  # Make sure the original issue is closed.
  assert original_issue.closed, 'The original issue must be closed.'

  # Validate that all original patches are supported by /revert_patchset.
  try:
    check_patches_reversable(original_patches)
  except exceptions.RevertError, e:
    return HttpTextResponse(e.message + ERROR_MSG_POSTPEND, status=404)

  # Create the new revert issue to use as the key in the new patchset.
  first_id, _ = models.Issue.allocate_ids(1)
  issue_key = ndb.Key(models.Issue, first_id)

  reviewers = original_issue.reviewers
  original_owner = original_issue.owner
  # Add the original_owner to the list of reviewers if different from the
  # current user.
  if original_owner.email() != request.user.email():
    reviewers.append(original_owner.email())
  # Remove current user from the reviewers.
  if request.user.email() in reviewers:
    reviewers.remove(request.user.email())

  # Datastructure that will hold all pending Issue, PatchSet, Patches and
  # Contents of the revert Issue.
  pending_commits = []

  subject = _get_revert_subject(original_issue.subject)
  revert_reason = request.POST['revert_reason']
  revert_cq = request.POST['revert_cq'] == '1'
  description = _get_revert_description(
      request, revert_reason, reviewers, original_issue, original_patch_num,
      original_patchset_id)
  issue = models.Issue(subject=subject,
                       description=description,
                       project=original_issue.project,
                       base=original_issue.base,
                       target_ref=original_issue.target_ref,
                       repo_guid=original_issue.repo_guid,
                       reviewers=reviewers,
                       cc=original_issue.cc,
                       private=original_issue.private,
                       n_comments=0,
                       commit=False,  # Do not check the commit box yet.
                       key=issue_key)
  pending_commits.append(issue)

  # Create the new revert patchset to use as the key in the new patches.
  ps_id, _ = models.PatchSet.allocate_ids(1, parent=issue.key)
  ps_key = ndb.Key(models.PatchSet, ps_id, parent=issue.key)
  patchset = models.PatchSet(
      issue_key=issue.key,
      url=None,
      key=ps_key)
  patchset_data = []  # Will be populated with the diff texts of all patches.
  pending_commits.append(patchset)

  # Loop through all the original patches and create inversions.
  for original_patch in original_patches:

    try:
      patch = original_patch.make_inverted(patchset)
    except exceptions.FetchError, e:
      return HttpTextResponse(
          'Revert failed: ' + e.message + '\nThis sometimes happens when a '
          'file\'s contents are not uploaded because it is too large.' +
          ERROR_MSG_POSTPEND, status=500)
    except DeadlineExceededError:
      return HttpTextResponse(
          'The patchset is too large to invert. Please revert manually.\n'
          'Sorry for the inconvenience.', status=500)
    except exceptions.RevertError, e:
      return HttpTextResponse(e.message + ERROR_MSG_POSTPEND, status=404)

    # Find the original content and patched content.
    if original_patch.is_binary:
      original_content = original_patch.content_key.get()
      original_patched_content = (
          original_patch.patched_content_key.get()
          if original_patch.patched_content_key else None)
    else:
      original_content = original_patch.get_content()
      original_patched_content = original_patch.get_patched_content()

    # Allocate keys for content and patched_content.
    content_id, _ = models.Content.allocate_ids(1, parent=patch.key)
    content_key = ndb.Key(models.Content, content_id, parent=patch.key)
    patched_content_id, _ = models.Content.allocate_ids(1, parent=patch.key)
    patched_content_key = ndb.Key(
      models.Content, patched_content_id, parent=patch.key)

    if original_patched_content:
      content = models.Content(
        key=content_key,
        text=original_patched_content.text,
        data=original_patched_content.data,
        checksum=original_patched_content.checksum,
        is_uploaded=original_patched_content.is_uploaded,
        is_bad=original_patched_content.is_bad,
        file_too_large=original_patched_content.file_too_large)
    elif (original_content and
          invert_patches.is_patch_binary_copy_modify_with_no_change(
              original_patch)):
      # For binary patches with 100% similarity indexes use the original content
      # as the content. See crbug.com/525625
      content = models.Content(
        key=content_key,
        text=original_content.text,
        data=original_content.data,
        checksum=original_content.checksum,
        is_uploaded=original_content.is_uploaded,
        is_bad=original_content.is_bad,
        file_too_large=original_content.file_too_large)
    else:
      # Create an empty content if there is no patched_content.
      empty_data = ''
      content = models.Content(
        key=content_key,
        text='',
        data=empty_data,
        checksum=hashlib.md5(empty_data).hexdigest(),
        is_uploaded=True,
        is_bad=False,
        file_too_large=False)

    if original_patch.status != invert_patches.COPIED_AND_MODIFIED_STATUS:
      patched_content = models.Content(
          key=patched_content_key,
          text=original_content.text,
          data=original_content.data,
          checksum=original_content.checksum,
          is_uploaded=original_content.is_uploaded,
          is_bad=original_content.is_bad,
          file_too_large=original_content.file_too_large)
    else:
      # Create an empty patched content if it is an 'A +' status. This is
      # because the inverted status becomes a 'D' request.
      empty_data = ''
      patched_content = models.Content(
        key=patched_content_key,
        text='',
        data=empty_data,
        checksum=hashlib.md5(empty_data).hexdigest(),
        is_uploaded=True,
        is_bad=False,
        file_too_large=False)

    pending_commits.append(content)
    pending_commits.append(patched_content)

    patch.content_key = content.key
    patch.patched_content_key = patched_content.key
    pending_commits.append(patch)

    # Encode the unicode patch.txt to later put in patchset.data which is stored
    # as ndb.Blob (str). For more context see crbug.com/527325
    patch_text = patch.text.encode('utf-8')
    # Append the text of this patch to the patchset's data. Strip out ending
    # new line (if it exists) because one will be added during the below join.
    patchset_data.append(patch_text.rstrip('\n'))

  # Add all gathered patch texts to the patchset's data field.
  patchset.data = '\n'.join(patchset_data)
  # Patchset data needs to end with '\n' for 'git cl patch' to work. For more
  # context see: crbug.com/479881#c12
  if not patchset.data.endswith('\n'):
    patchset.data += '\n'

  # Commit the gathered revert Issue, PatchSet, Patches and Contents.
  _db_commit_all_pending_commits(pending_commits)

  # Notify the original issue that a revert issue has been created.
  revert_issue_link = request.build_absolute_uri(
      reverse('codereview.views.show', args=[issue.key.id()]))
  revert_message = (
      'A revert of this CL (patchset #%s id:%s) has been created in %s by '
      '%s.\n\nThe reason for reverting is: %s.' % (
          original_patch_num, original_patchset_id, revert_issue_link,
          request.user.email(), revert_reason))
  views.make_message(request, original_issue, revert_message,
                     send_mail=True).put()

  # Post a message saying who CQ'd the revert. This is critical for CQ, which is
  # using this message to determine when the attempt has been started.
  if revert_cq:
    views.make_message(
        request, issue, 'The CQ bit was checked by %s' % request.user.email(),
        auto_generated=True, send_mail=False).put()

  # Notify the revert issue recipients.
  views.make_message(request, issue, 'Created %s' % subject,
                     send_mail=True).put()

  # Now that all patchsets and patches have been committed check the commit box
  # if the revert_cq checkbox was checked.
  if revert_cq:
    issue.commit = True
  issue.put()

  if request.POST.get('no_redirect'):
    return HttpTextResponse(str(issue.key.id()), status=200)
  else:
    return HttpResponseRedirect(reverse('codereview.views.show',
                                        args=[issue.key.id()]))


def _db_commit_all_pending_commits(pending_commits):
  """Puts all pending commits into the DB."""
  for pending_commit in pending_commits:
    pending_commit.put()
# Copyright 2013 Google Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # #     http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. Contains the revert_patchset view. # Constants for patch checks. Creates and returns a description for the revert CL. # Contain link to original CL. #%s id:%s of %s )' % ( # Display the reason for reverting. # Extra new line to separate sections. # Add the original issue's decription. # Extra new line to separate sections. # TBR original author + reviewers. # Extra new line to separate sections. # Do not run presubmit on the revert CL. # Skip tree status checks. # Do not run trybots on the revert CL. # Check to see if the original description contains "BUG=" if it does then # use it in the revert description. Creates and returns a subject for the revert CL.    If the original CL was a revert CL then 'Revert of' is replaced with   'Reland of'.   If the original CL was a reland CL then 'Reland of' is replaced   with 'Revert of'.   All other CLs get 'Revert of' prefixed to the subject. # This is going to be a revert of a revert, i.e. a reland. # This is going to be a revert of a reland, i.e. a revert. Check to see if the specified patches are reversible.      Returns True if it is else throws an Error with why it is not reversible. # If the patch status is 'None' it could be due to an incompletely uploaded # original patch. # Collect the number of large patches. # Only git patches are supported. /api/<issue>/<patchset>/revert_patchset - Create an inverted changeset. # Find the patches of the issue we want to revert. # Make sure the original issue is closed. # Validate that all original patches are supported by /revert_patchset. # Create the new revert issue to use as the key in the new patchset. # Add the original_owner to the list of reviewers if different from the # current user. # Remove current user from the reviewers. # Datastructure that will hold all pending Issue, PatchSet, Patches and # Contents of the revert Issue. # Do not check the commit box yet. # Create the new revert patchset to use as the key in the new patches. # Will be populated with the diff texts of all patches. # Loop through all the original patches and create inversions. # Find the original content and patched content. # Allocate keys for content and patched_content. # For binary patches with 100% similarity indexes use the original content # as the content. See crbug.com/525625 # Create an empty content if there is no patched_content. # Create an empty patched content if it is an 'A +' status. This is # because the inverted status becomes a 'D' request. # Encode the unicode patch.txt to later put in patchset.data which is stored # as ndb.Blob (str). For more context see crbug.com/527325 # Append the text of this patch to the patchset's data. Strip out ending # new line (if it exists) because one will be added during the below join. # Add all gathered patch texts to the patchset's data field. # Patchset data needs to end with '\n' for 'git cl patch' to work. For more # context see: crbug.com/479881#c12 # Commit the gathered revert Issue, PatchSet, Patches and Contents. # Notify the original issue that a revert issue has been created. #%s id:%s) has been created in %s by ' # Post a message saying who CQ'd the revert. This is critical for CQ, which is # using this message to determine when the attempt has been started. # Notify the revert issue recipients. # Now that all patchsets and patches have been committed check the commit box # if the revert_cq checkbox was checked. Puts all pending commits into the DB.