tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

lastchange.py (14623B)


      1 #!/usr/bin/env python3
      2 # Copyright 2012 The Chromium Authors
      3 # Use of this source code is governed by a BSD-style license that can be
      4 # found in the LICENSE file.
      5 
      6 """
      7 lastchange.py -- Chromium revision fetching utility.
      8 """
      9 
     10 import argparse
     11 import collections
     12 import datetime
     13 import logging
     14 import os
     15 import subprocess
     16 import sys
     17 
     18 _THIS_DIR = os.path.abspath(os.path.dirname(__file__))
     19 _ROOT_DIR = os.path.abspath(
     20    os.path.join(_THIS_DIR, "..", "..", "third_party/depot_tools"))
     21 
     22 sys.path.insert(0, _ROOT_DIR)
     23 
     24 import gclient_utils
     25 
     26 VersionInfo = collections.namedtuple(
     27    "VersionInfo", ("revision_id", "revision", "commit_position", "timestamp"))
     28 _EMPTY_VERSION_INFO = VersionInfo('0' * 40, '0' * 40, '', 0)
     29 
     30 class GitError(Exception):
     31  pass
     32 
     33 # This function exists for compatibility with logic outside this
     34 # repository that uses this file as a library.
     35 # TODO(eliribble) remove this function after it has been ported into
     36 # the repositories that depend on it
     37 def RunGitCommand(directory, command):
     38  """
     39  Launches git subcommand.
     40 
     41  Errors are swallowed.
     42 
     43  Returns:
     44    A process object or None.
     45  """
     46  command = ['git'] + command
     47  # Force shell usage under cygwin. This is a workaround for
     48  # mysterious loss of cwd while invoking cygwin's git.
     49  # We can't just pass shell=True to Popen, as under win32 this will
     50  # cause CMD to be used, while we explicitly want a cygwin shell.
     51  if sys.platform == 'cygwin':
     52    command = ['sh', '-c', ' '.join(command)]
     53  try:
     54    proc = subprocess.Popen(command,
     55                            stdout=subprocess.PIPE,
     56                            stderr=subprocess.PIPE,
     57                            cwd=directory,
     58                            shell=(sys.platform=='win32'))
     59    return proc
     60  except OSError as e:
     61    logging.error('Command %r failed: %s' % (' '.join(command), e))
     62    return None
     63 
     64 
     65 def _RunGitCommand(directory, command):
     66  """Launches git subcommand.
     67 
     68  Returns:
     69    The stripped stdout of the git command.
     70  Raises:
     71    GitError on failure, including a nonzero return code.
     72  """
     73  command = ['git'] + command
     74  # Force shell usage under cygwin. This is a workaround for
     75  # mysterious loss of cwd while invoking cygwin's git.
     76  # We can't just pass shell=True to Popen, as under win32 this will
     77  # cause CMD to be used, while we explicitly want a cygwin shell.
     78  if sys.platform == 'cygwin':
     79    command = ['sh', '-c', ' '.join(command)]
     80  try:
     81    logging.info("Executing '%s' in %s", ' '.join(command), directory)
     82    proc = subprocess.Popen(command,
     83                            stdout=subprocess.PIPE,
     84                            stderr=subprocess.PIPE,
     85                            cwd=directory,
     86                            shell=(sys.platform=='win32'))
     87    stdout, stderr = tuple(x.decode(encoding='utf_8')
     88                           for x in proc.communicate())
     89    stdout = stdout.strip()
     90    stderr = stderr.strip()
     91    logging.debug("returncode: %d", proc.returncode)
     92    logging.debug("stdout: %s", stdout)
     93    logging.debug("stderr: %s", stderr)
     94    if proc.returncode != 0 or not stdout:
     95      raise GitError((
     96          "Git command '{}' in {} failed: "
     97          "rc={}, stdout='{}' stderr='{}'").format(
     98          " ".join(command), directory, proc.returncode, stdout, stderr))
     99    return stdout
    100  except OSError as e:
    101    raise GitError("Git command 'git {}' in {} failed: {}".format(
    102        " ".join(command), directory, e))
    103 
    104 
    105 def GetMergeBase(directory, ref):
    106  """
    107  Return the merge-base of HEAD and ref.
    108 
    109  Args:
    110    directory: The directory containing the .git directory.
    111    ref: The ref to use to find the merge base.
    112  Returns:
    113    The git commit SHA of the merge-base as a string.
    114  """
    115  logging.debug("Calculating merge base between HEAD and %s in %s",
    116                ref, directory)
    117  command = ['merge-base', 'HEAD', ref]
    118  return _RunGitCommand(directory, command)
    119 
    120 
    121 def FetchGitRevision(directory, commit_filter, start_commit="HEAD"):
    122  """
    123  Fetch the Git hash (and Cr-Commit-Position if any) for a given directory.
    124 
    125  Args:
    126    directory: The directory containing the .git directory.
    127    commit_filter: A filter to supply to grep to filter commits
    128    start_commit: A commit identifier. The result of this function
    129      will be limited to only consider commits before the provided
    130      commit.
    131  Returns:
    132    A VersionInfo object. On error all values will be 0.
    133  """
    134  hash_ = ''
    135 
    136  git_args = ['log', '-1', '--format=%H %ct']
    137  if commit_filter is not None:
    138    git_args.append('--grep=' + commit_filter)
    139 
    140  git_args.append(start_commit)
    141 
    142  output = _RunGitCommand(directory, git_args)
    143  hash_, commit_timestamp = output.split()
    144  if not hash_:
    145    return VersionInfo('0', '0', '', 0)
    146 
    147  revision = hash_
    148  pos = ''
    149  output = _RunGitCommand(directory, ['cat-file', 'commit', hash_])
    150  for line in reversed(output.splitlines()):
    151    if line.startswith('Cr-Commit-Position:'):
    152      pos = line.rsplit()[-1].strip()
    153      logging.debug("Found Cr-Commit-Position '%s'", pos)
    154      revision = "{}-{}".format(hash_, pos)
    155      break
    156  return VersionInfo(hash_, revision, pos, int(commit_timestamp))
    157 
    158 
    159 def GetHeaderGuard(path):
    160  """
    161  Returns the header #define guard for the given file path.
    162  This treats everything after the last instance of "src/" as being a
    163  relevant part of the guard. If there is no "src/", then the entire path
    164  is used.
    165  """
    166  src_index = path.rfind('src/')
    167  if src_index != -1:
    168    guard = path[src_index + 4:]
    169  else:
    170    guard = path
    171  guard = guard.upper()
    172  return guard.replace('/', '_').replace('.', '_').replace('\\', '_') + '_'
    173 
    174 
    175 def GetCommitPositionHeaderContents(path, define_prefix, version_info):
    176  """
    177  Returns what the contents of the header file should be that indicate the
    178  commit position number of given version.
    179  """
    180  header_guard = GetHeaderGuard(path)
    181 
    182  commit_position_number = ''
    183  commit_position_ref = ''
    184  if version_info.commit_position:
    185    ref_and_number = version_info.commit_position.split('@', 2)
    186    if len(ref_and_number) == 2:
    187      commit_position_ref = ref_and_number[0]
    188      commit_position_number = ref_and_number[1][2:-1]
    189 
    190  header_contents = """/* Generated by lastchange.py, do not edit.*/
    191 
    192 #ifndef %(header_guard)s
    193 #define %(header_guard)s
    194 
    195 #define %(define)s_COMMIT_POSITION_IS_MAIN %(is_main)s
    196 #define %(define)s_COMMIT_POSITION_NUMBER "%(commit_position_number)s"
    197 
    198 #endif  // %(header_guard)s
    199 """ % {
    200      'header_guard': header_guard,
    201      'define': define_prefix,
    202      'is_main': ('1' if commit_position_ref == 'refs/heads/main' else '0'),
    203      'commit_position_number': commit_position_number,
    204  }
    205 
    206  return header_contents
    207 
    208 
    209 def GetHeaderContents(path, define, version):
    210  """
    211  Returns what the contents of the header file should be that indicate the given
    212  revision.
    213  """
    214  header_guard = GetHeaderGuard(path)
    215 
    216  header_contents = """/* Generated by lastchange.py, do not edit.*/
    217 
    218 #ifndef %(header_guard)s
    219 #define %(header_guard)s
    220 
    221 #define %(define)s "%(version)s"
    222 
    223 #endif  // %(header_guard)s
    224 """
    225  header_contents = header_contents % { 'header_guard': header_guard,
    226                                        'define': define,
    227                                        'version': version }
    228  return header_contents
    229 
    230 
    231 def GetGitTopDirectory(source_dir):
    232  """Get the top git directory - the directory that contains the .git directory.
    233 
    234  Args:
    235    source_dir: The directory to search.
    236  Returns:
    237    The output of "git rev-parse --show-toplevel" as a string
    238  """
    239  return _RunGitCommand(source_dir, ['rev-parse', '--show-toplevel'])
    240 
    241 
    242 def WriteIfChanged(file_name, contents):
    243  """
    244  Writes the specified contents to the specified file_name
    245  iff the contents are different than the current contents.
    246  Returns if new data was written.
    247  """
    248  try:
    249    old_contents = open(file_name, 'r').read()
    250  except EnvironmentError:
    251    pass
    252  else:
    253    if contents == old_contents:
    254      return False
    255    os.unlink(file_name)
    256  open(file_name, 'w').write(contents)
    257  return True
    258 
    259 
    260 def GetVersion(source_dir, commit_filter, merge_base_ref):
    261  """
    262  Returns the version information for the given source directory.
    263  """
    264  if 'BASE_COMMIT_SUBMISSION_MS' in os.environ:
    265    return GetVersionInfoFromEnv()
    266 
    267  if gclient_utils.IsEnvCog():
    268    return _EMPTY_VERSION_INFO
    269 
    270  git_top_dir = None
    271  try:
    272    git_top_dir = GetGitTopDirectory(source_dir)
    273  except GitError as e:
    274    logging.warning("Failed to get git top directory from '%s': %s", source_dir,
    275                    e)
    276 
    277  merge_base_sha = 'HEAD'
    278  if git_top_dir and merge_base_ref:
    279    try:
    280      merge_base_sha = GetMergeBase(git_top_dir, merge_base_ref)
    281    except GitError as e:
    282      logging.error(
    283          "You requested a --merge-base-ref value of '%s' but no "
    284          "merge base could be found between it and HEAD. Git "
    285          "reports: %s", merge_base_ref, e)
    286      return None
    287 
    288  version_info = None
    289  if git_top_dir:
    290    try:
    291      version_info = FetchGitRevision(git_top_dir, commit_filter,
    292                                      merge_base_sha)
    293    except GitError as e:
    294      logging.error("Failed to get version info: %s", e)
    295 
    296  if not version_info:
    297    logging.warning(
    298        "Falling back to a version of 0.0.0 to allow script to "
    299        "finish. This is normal if you are bootstrapping a new environment "
    300        "or do not have a git repository for any other reason. If not, this "
    301        "could represent a serious error.")
    302    # Use a dummy revision that has the same length as a Git commit hash,
    303    # same as what we use in build/util/LASTCHANGE.dummy.
    304    version_info = _EMPTY_VERSION_INFO
    305 
    306  return version_info
    307 
    308 
    309 def GetVersionInfoFromEnv():
    310  """
    311  Returns the version information from the environment.
    312  """
    313  hash = os.environ.get('BASE_COMMIT_HASH', _EMPTY_VERSION_INFO.revision)
    314  timestamp = int(
    315      os.environ.get('BASE_COMMIT_SUBMISSION_MS',
    316                     _EMPTY_VERSION_INFO.timestamp)) / 1000
    317  return VersionInfo(hash, hash, '', int(timestamp))
    318 
    319 
    320 def main(argv=None):
    321  if argv is None:
    322    argv = sys.argv
    323 
    324  parser = argparse.ArgumentParser(usage="lastchange.py [options]")
    325  parser.add_argument("-m", "--version-macro",
    326                    help=("Name of C #define when using --header. Defaults to "
    327                          "LAST_CHANGE."))
    328  parser.add_argument("-o",
    329                      "--output",
    330                      metavar="FILE",
    331                      help=("Write last change to FILE. "
    332                            "Can be combined with other file-output-related "
    333                            "options to write multiple files."))
    334  parser.add_argument("--header",
    335                      metavar="FILE",
    336                      help=("Write last change to FILE as a C/C++ header. "
    337                            "Can be combined with other file-output-related "
    338                            "options to write multiple files."))
    339  parser.add_argument("--commit-position-header",
    340                      metavar="FILE",
    341                      help=("Write the commit position to FILE as a C/C++ "
    342                            "header. Can be combined with other file-output-"
    343                            "related options to write multiple files."))
    344  parser.add_argument("--revision",
    345                      metavar="FILE",
    346                      help=("Write last change to FILE as a one-line revision. "
    347                            "Can be combined with other file-output-related "
    348                            "options to write multiple files."))
    349  parser.add_argument("--merge-base-ref",
    350                    default=None,
    351                    help=("Only consider changes since the merge "
    352                          "base between HEAD and the provided ref"))
    353  parser.add_argument("--revision-id-only", action='store_true',
    354                    help=("Output the revision as a VCS revision ID only (in "
    355                          "Git, a 40-character commit hash, excluding the "
    356                          "Cr-Commit-Position)."))
    357  parser.add_argument("--print-only", action="store_true",
    358                    help=("Just print the revision string. Overrides any "
    359                          "file-output-related options."))
    360  parser.add_argument("-s", "--source-dir", metavar="DIR",
    361                    help="Use repository in the given directory.")
    362  parser.add_argument("--filter", metavar="REGEX",
    363                    help=("Only use log entries where the commit message "
    364                          "matches the supplied filter regex. Defaults to "
    365                          "'^Change-Id:' to suppress local commits."),
    366                    default='^Change-Id:')
    367 
    368  args, extras = parser.parse_known_args(argv[1:])
    369 
    370  logging.basicConfig(level=logging.WARNING)
    371 
    372  out_file = args.output
    373  header = args.header
    374  revision = args.revision
    375  commit_filter = args.filter
    376  commit_position_header = args.commit_position_header
    377 
    378  while len(extras) and out_file is None:
    379    if out_file is None:
    380      out_file = extras.pop(0)
    381  if extras:
    382    sys.stderr.write('Unexpected arguments: %r\n\n' % extras)
    383    parser.print_help()
    384    sys.exit(2)
    385 
    386  source_dir = args.source_dir or os.path.dirname(os.path.abspath(__file__))
    387 
    388  version_info = GetVersion(source_dir, commit_filter, args.merge_base_ref)
    389 
    390  revision_string = version_info.revision
    391  if args.revision_id_only:
    392    revision_string = version_info.revision_id
    393 
    394  if args.print_only:
    395    print(revision_string)
    396  else:
    397    lastchange_year = datetime.datetime.fromtimestamp(
    398        version_info.timestamp, datetime.timezone.utc).year
    399    contents_lines = [
    400        "LASTCHANGE=%s" % revision_string,
    401        "LASTCHANGE_YEAR=%s" % lastchange_year,
    402    ]
    403    contents = '\n'.join(contents_lines) + '\n'
    404    if not (out_file or header or commit_position_header or revision):
    405      sys.stdout.write(contents)
    406    else:
    407      if out_file:
    408        committime_file = out_file + '.committime'
    409        out_changed = WriteIfChanged(out_file, contents)
    410        if out_changed or not os.path.exists(committime_file):
    411          with open(committime_file, 'w') as timefile:
    412            timefile.write(str(version_info.timestamp))
    413      if header:
    414        WriteIfChanged(header,
    415                       GetHeaderContents(header, args.version_macro,
    416                                         revision_string))
    417      if commit_position_header:
    418        WriteIfChanged(
    419            commit_position_header,
    420            GetCommitPositionHeaderContents(commit_position_header,
    421                                            args.version_macro, version_info))
    422      if revision:
    423        WriteIfChanged(revision, revision_string)
    424 
    425  return 0
    426 
    427 
    428 if __name__ == '__main__':
    429  sys.exit(main())