summaryrefslogtreecommitdiffstats
path: root/chromium/tools/merge_common.py
blob: 634e19cc03d9dcfa57a9838cdb09cd7871ed2e72 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
# Copyright (C) 2012 The Android Open Source Project
#
# 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.

"""Common data/functions for the Chromium merging scripts."""

import logging
import os
import re
import subprocess


REPOSITORY_ROOT = os.path.join(os.environ['ANDROID_BUILD_TOP'],
                               'external/chromium_org')


# Whitelist of projects that need to be merged to build WebView. We don't need
# the other upstream repositories used to build the actual Chrome app.
# Different stages of the merge process need different ways of looking at the
# list, so we construct different combinations below.

THIRD_PARTY_PROJECTS_WITH_FLAT_HISTORY = [
    'third_party/WebKit',
]

THIRD_PARTY_PROJECTS_WITH_FULL_HISTORY = [
    'sdch/open-vcdiff',
    'testing/gtest',
    'third_party/angle',
    'third_party/boringssl/src',
    'third_party/brotli/src',
    'third_party/eyesfree/src/android/java/src/com/googlecode/eyesfree/braille',
    'third_party/freetype',
    'third_party/icu',
    'third_party/leveldatabase/src',
    'third_party/libaddressinput/src',
    'third_party/libjingle/source/talk',
    'third_party/libjpeg_turbo',
    'third_party/libphonenumber/src/phonenumbers',
    'third_party/libphonenumber/src/resources',
    'third_party/libsrtp',
    'third_party/libvpx',
    'third_party/libyuv',
    'third_party/mesa/src',
    'third_party/openmax_dl',
    'third_party/opus/src',
    'third_party/ots',
    'third_party/sfntly/cpp/src',
    'third_party/skia',
    'third_party/smhasher/src',
    'third_party/usrsctp/usrsctplib',
    'third_party/webrtc',
    'third_party/yasm/source/patched-yasm',
    'tools/grit',
    'tools/gyp',
    'v8',
]

PROJECTS_WITH_FLAT_HISTORY = ['.'] + THIRD_PARTY_PROJECTS_WITH_FLAT_HISTORY
PROJECTS_WITH_FULL_HISTORY = THIRD_PARTY_PROJECTS_WITH_FULL_HISTORY

THIRD_PARTY_PROJECTS = (THIRD_PARTY_PROJECTS_WITH_FLAT_HISTORY +
                        THIRD_PARTY_PROJECTS_WITH_FULL_HISTORY)

ALL_PROJECTS = ['.'] + THIRD_PARTY_PROJECTS
assert(set(ALL_PROJECTS) ==
       set(PROJECTS_WITH_FLAT_HISTORY + PROJECTS_WITH_FULL_HISTORY))

# Directories to be removed when flattening history.
PRUNE_WHEN_FLATTENING = {
    'third_party/WebKit': [
        'LayoutTests',
    ],
}


# Only projects that have their history flattened can have directories pruned.
assert all(p in PROJECTS_WITH_FLAT_HISTORY for p in PRUNE_WHEN_FLATTENING)


class MergeError(Exception):
  """Used to signal an error that prevents the merge from being completed."""


class CommandError(MergeError):
  """This exception is raised when a process run by GetCommandStdout fails."""

  def __init__(self, returncode, cmd, cwd, stdout, stderr):
    super(CommandError, self).__init__()
    self.returncode = returncode
    self.cmd = cmd
    self.cwd = cwd
    self.stdout = stdout
    self.stderr = stderr

  def __str__(self):
    return ("Command '%s' returned non-zero exit status %d. cwd was '%s'.\n\n"
            "===STDOUT===\n%s\n===STDERR===\n%s\n" %
            (self.cmd, self.returncode, self.cwd, self.stdout, self.stderr))


class TemporaryMergeError(MergeError):
  """A merge error that can potentially be resolved by trying again later."""


def Abbrev(commitish):
  """Returns the abbrev commitish for a given Git SHA."""
  return commitish[:12]


def GetCommandStdout(args, cwd=REPOSITORY_ROOT, ignore_errors=False):
  """Gets stdout from runnng the specified shell command.

  Similar to subprocess.check_output() except that it can capture stdout and
  stderr separately for better error reporting.

  Args:
    args: The command and its arguments as an iterable.
    cwd: The working directory to use. Defaults to REPOSITORY_ROOT.
    ignore_errors: Ignore the command's return code and stderr.
  Returns:
    A concatenation of stdout + stderr from running the command.
  Raises:
    CommandError: if the command exited with a nonzero status.
  """
  p = subprocess.Popen(args=args, cwd=cwd, stdout=subprocess.PIPE,
                       stderr=subprocess.PIPE)
  stdout, stderr = p.communicate()
  output = stdout + ('\n===STDERR===\n' if stderr and stdout else '') + stderr
  if p.returncode == 0 or ignore_errors:
    return output
  else:
    raise CommandError(p.returncode, ' '.join(args), cwd, stdout, stderr)


def CheckNoConflictsAndCommitMerge(commit_message, unattended=False,
                                   cwd=REPOSITORY_ROOT):
  """Checks for conflicts and commits once they are resolved.

  Certain conflicts are resolved automatically; if any remain, the user is
  prompted to resolve them. The user can specify a custom commit message.

  Args:
    commit_message: The default commit message.
    unattended: If running unattended, abort on conflicts.
    cwd: Working directory to use.
  Raises:
    TemporaryMergeError: If there are conflicts in unattended mode.
  """
  status = GetCommandStdout(['git', 'status', '--porcelain'], cwd=cwd)
  conflicts_deleted_by_us = re.findall(r'^(?:DD|DU) ([^\n]+)$', status,
                                       flags=re.MULTILINE)
  if conflicts_deleted_by_us:
    logging.info('Keeping ours for the following locally deleted files.\n  %s',
                 '\n  '.join(conflicts_deleted_by_us))
    GetCommandStdout(['git', 'rm', '-rf', '--ignore-unmatch'] +
                     conflicts_deleted_by_us, cwd=cwd)

  # If upstream renames a file we have deleted then it will conflict, but
  # we shouldn't just blindly delete these files as they may have been renamed
  # into a directory we don't delete. Let them get re-added; they will get
  # re-deleted if they are still in a directory we delete.
  conflicts_renamed_by_them = re.findall(r'^UA ([^\n]+)$', status,
                                         flags=re.MULTILINE)
  if conflicts_renamed_by_them:
    logging.info('Adding theirs for the following locally deleted files.\n %s',
                 '\n  '.join(conflicts_renamed_by_them))
    GetCommandStdout(['git', 'add', '-f'] + conflicts_renamed_by_them, cwd=cwd)

  while True:
    status = GetCommandStdout(['git', 'status', '--porcelain'], cwd=cwd)
    conflicts = re.findall(r'^((DD|AU|UD|UA|DU|AA|UU) [^\n]+)$', status,
                           flags=re.MULTILINE)
    if not conflicts:
      break
    if unattended:
      GetCommandStdout(['git', 'reset', '--hard'], cwd=cwd)
      raise TemporaryMergeError('Cannot resolve merge conflicts.')
    conflicts_string = '\n'.join([x[0] for x in conflicts])
    new_commit_message = raw_input(
        ('The following conflicts exist and must be resolved.\n\n%s\n\nWhen '
         'done, enter a commit message or press enter to use the default '
         '(\'%s\').\n\n') % (conflicts_string, commit_message))
    if new_commit_message:
      commit_message = new_commit_message

  GetCommandStdout(['git', 'commit', '-m', commit_message], cwd=cwd)