Commit 5f2e9a96 authored by Eli Ribble's avatar Eli Ribble Committed by Commit Bot

Reland: "Add --merge-base-ref option to lastchange.py"

Reland commit f4f6c5ff
Revert was 172ccc05

The previous attempt at a reland changed FetchGitRevision
which caused breakage when
chrome/test/chromedriver/embed_version_in_cpp.py attempted to call this
function. https://crrev.com/c/1459921 was recently merged which
completely removed embed_version_in_cpp.py which should allow
this change to re-land without modification from the previous
attempt at a reland.

Change-Id: I0dbd7436938ae27167a9381415277d95d0d8312f
Bug: 917159
Reviewed-on: https://chromium-review.googlesource.com/c/1460454Reviewed-by: default avatarDirk Pranke <dpranke@chromium.org>
Reviewed-by: default avatarAndrii Shyshkalov <tandrii@chromium.org>
Commit-Queue: Eli Ribble <eliribble@chromium.org>
Cr-Commit-Position: refs/heads/master@{#633730}
parent 339d75c2
...@@ -7,20 +7,23 @@ ...@@ -7,20 +7,23 @@
lastchange.py -- Chromium revision fetching utility. lastchange.py -- Chromium revision fetching utility.
""" """
import re
import logging
import argparse import argparse
import collections
import logging
import os import os
import subprocess import subprocess
import sys import sys
class VersionInfo(object): VersionInfo = collections.namedtuple("VersionInfo",
def __init__(self, revision_id, full_revision_string, timestamp): ("revision_id", "revision", "timestamp"))
self.revision_id = revision_id
self.revision = full_revision_string
self.timestamp = timestamp
class GitError(Exception):
pass
# This function exists for compatibility with logic outside this
# repository that uses this file as a library.
# TODO(eliribble) remove this function after it has been ported into
# the repositories that depend on it
def RunGitCommand(directory, command): def RunGitCommand(directory, command):
""" """
Launches git subcommand. Launches git subcommand.
...@@ -49,53 +52,95 @@ def RunGitCommand(directory, command): ...@@ -49,53 +52,95 @@ def RunGitCommand(directory, command):
return None return None
def FetchGitRevision(directory, git_log_filter): def _RunGitCommand(directory, command):
"""Launches git subcommand.
Returns:
The stripped stdout of the git command.
Raises:
GitError on failure, including a nonzero return code.
""" """
Fetch the Git hash (and Cr-Commit-Position if any) for a given directory. command = ['git'] + command
# Force shell usage under cygwin. This is a workaround for
# mysterious loss of cwd while invoking cygwin's git.
# We can't just pass shell=True to Popen, as under win32 this will
# cause CMD to be used, while we explicitly want a cygwin shell.
if sys.platform == 'cygwin':
command = ['sh', '-c', ' '.join(command)]
try:
logging.info("Executing '%s' in %s", ' '.join(command), directory)
proc = subprocess.Popen(command,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
cwd=directory,
shell=(sys.platform=='win32'))
stdout, stderr = proc.communicate()
stdout = stdout.strip()
logging.debug("returncode: %d", proc.returncode)
logging.debug("stdout: %s", stdout)
logging.debug("stderr: %s", stderr)
if proc.returncode != 0 or not stdout:
raise GitError((
"Git command 'git {}' in {} failed: "
"rc={}, stdout='{}' stderr='{}'").format(
" ".join(command), directory, proc.returncode, stdout, stderr))
return stdout
except OSError as e:
raise GitError("Git command 'git {}' in {} failed: {}".format(
" ".join(command), directory, e))
Errors are swallowed.
Args: def GetMergeBase(directory, ref):
git_log_filter: a string to be used for filtering git log result. """
Return the merge-base of HEAD and ref.
Args:
directory: The directory containing the .git directory.
ref: The ref to use to find the merge base.
Returns: Returns:
A VersionInfo object or None on error. The git commit SHA of the merge-base as a string.
""" """
hsh = '' logging.debug("Calculating merge base between HEAD and %s in %s",
git_args = ['log', '-1', '--format=%H %ct'] ref, directory)
if git_log_filter is not None: command = ['merge-base', 'HEAD', ref]
git_args.append('--grep=' + git_log_filter) return _RunGitCommand(directory, command)
proc = RunGitCommand(directory, git_args)
if proc:
output = proc.communicate()[0].strip() def FetchGitRevision(directory, commit_filter, start_commit="HEAD"):
if proc.returncode == 0 and output:
hsh, ct = output.split()
else:
logging.error('Git error: rc=%d, output=%r' %
(proc.returncode, output))
if not hsh:
return None
pos = ''
proc = RunGitCommand(directory, ['cat-file', 'commit', hsh])
if proc:
output = proc.communicate()[0]
if proc.returncode == 0 and output:
for line in reversed(output.splitlines()):
if line.startswith('Cr-Commit-Position:'):
pos = line.rsplit()[-1].strip()
break
return VersionInfo(hsh, '%s-%s' % (hsh, pos), int(ct))
def FetchVersionInfo(directory=None, git_log_filter=None):
""" """
Returns the last change (as a VersionInfo object) Fetch the Git hash (and Cr-Commit-Position if any) for a given directory.
from some appropriate revision control system.
Args:
directory: The directory containing the .git directory.
commit_filter: A filter to supply to grep to filter commits
start_commit: A commit identifier. The result of this function
will be limited to only consider commits before the provided
commit.
Returns:
A VersionInfo object. On error all values will be 0.
""" """
version_info = FetchGitRevision(directory, git_log_filter) hash_ = ''
if not version_info:
version_info = VersionInfo('0', '0', 0) git_args = ['log', '-1', '--format=%H %ct']
return version_info if commit_filter is not None:
git_args.append('--grep=' + commit_filter)
git_args.append(start_commit)
output = _RunGitCommand(directory, git_args)
hash_, commit_timestamp = output.split()
if not hash_:
return VersionInfo('0', '0', 0)
revision = hash_
output = _RunGitCommand(directory, ['cat-file', 'commit', hash_])
for line in reversed(output.splitlines()):
if line.startswith('Cr-Commit-Position:'):
pos = line.rsplit()[-1].strip()
logging.debug("Found Cr-Commit-Position '%s'", pos)
revision = "{}-{}".format(hash_, pos)
break
return VersionInfo(hash_, revision, int(commit_timestamp))
def GetHeaderGuard(path): def GetHeaderGuard(path):
...@@ -136,6 +181,17 @@ def GetHeaderContents(path, define, version): ...@@ -136,6 +181,17 @@ def GetHeaderContents(path, define, version):
return header_contents return header_contents
def GetGitTopDirectory(source_dir):
"""Get the top git directory - the directory that contains the .git directory.
Args:
source_dir: The directory to search.
Returns:
The output of "git rev-parse --show-toplevel" as a string
"""
return _RunGitCommand(source_dir, ['rev-parse', '--show-toplevel'])
def WriteIfChanged(file_name, contents): def WriteIfChanged(file_name, contents):
""" """
Writes the specified contents to the specified file_name Writes the specified contents to the specified file_name
...@@ -160,20 +216,23 @@ def main(argv=None): ...@@ -160,20 +216,23 @@ def main(argv=None):
parser = argparse.ArgumentParser(usage="lastchange.py [options]") parser = argparse.ArgumentParser(usage="lastchange.py [options]")
parser.add_argument("-m", "--version-macro", parser.add_argument("-m", "--version-macro",
help="Name of C #define when using --header. Defaults to " + help=("Name of C #define when using --header. Defaults to "
"LAST_CHANGE.", "LAST_CHANGE."))
default="LAST_CHANGE")
parser.add_argument("-o", "--output", metavar="FILE", parser.add_argument("-o", "--output", metavar="FILE",
help="Write last change to FILE. " + help=("Write last change to FILE. "
"Can be combined with --header to write both files.") "Can be combined with --header to write both files."))
parser.add_argument("--header", metavar="FILE", parser.add_argument("--header", metavar="FILE",
help=("Write last change to FILE as a C/C++ header. " help=("Write last change to FILE as a C/C++ header. "
"Can be combined with --output to write both files.")) "Can be combined with --output to write both files."))
parser.add_argument("--merge-base-ref",
default=None,
help=("Only consider changes since the merge "
"base between HEAD and the provided ref"))
parser.add_argument("--revision-id-only", action='store_true', parser.add_argument("--revision-id-only", action='store_true',
help=("Output the revision as a VCS revision ID only (in " help=("Output the revision as a VCS revision ID only (in "
"Git, a 40-character commit hash, excluding the " "Git, a 40-character commit hash, excluding the "
"Cr-Commit-Position).")) "Cr-Commit-Position)."))
parser.add_argument("--print-only", action='store_true', parser.add_argument("--print-only", action="store_true",
help=("Just print the revision string. Overrides any " help=("Just print the revision string. Overrides any "
"file-output-related options.")) "file-output-related options."))
parser.add_argument("-s", "--source-dir", metavar="DIR", parser.add_argument("-s", "--source-dir", metavar="DIR",
...@@ -183,13 +242,14 @@ def main(argv=None): ...@@ -183,13 +242,14 @@ def main(argv=None):
"matches the supplied filter regex. Defaults to " "matches the supplied filter regex. Defaults to "
"'^Change-Id:' to suppress local commits."), "'^Change-Id:' to suppress local commits."),
default='^Change-Id:') default='^Change-Id:')
args, extras = parser.parse_known_args(argv[1:]) args, extras = parser.parse_known_args(argv[1:])
logging.basicConfig(level=logging.WARNING) logging.basicConfig(level=logging.WARNING)
out_file = args.output out_file = args.output
header = args.header header = args.header
git_log_filter=args.filter commit_filter=args.filter
while len(extras) and out_file is None: while len(extras) and out_file is None:
if out_file is None: if out_file is None:
...@@ -199,19 +259,37 @@ def main(argv=None): ...@@ -199,19 +259,37 @@ def main(argv=None):
parser.print_help() parser.print_help()
sys.exit(2) sys.exit(2)
if args.source_dir: source_dir = args.source_dir or os.path.dirname(os.path.abspath(__file__))
src_dir = args.source_dir try:
git_top_dir = GetGitTopDirectory(source_dir)
except GitError as e:
logging.error("Failed to get git top directory from '%s': %s",
source_dir, e)
return 2
if args.merge_base_ref:
try:
merge_base_sha = GetMergeBase(git_top_dir, args.merge_base_ref)
except GitError as e:
logging.error("You requested a --merge-base-ref value of '%s' but no "
"merge base could be found between it and HEAD. Git "
"reports: %s", args.merge_base_ref, e)
return 3
else: else:
src_dir = os.path.dirname(os.path.abspath(__file__)) merge_base_sha = 'HEAD'
try:
version_info = FetchGitRevision(git_top_dir, commit_filter, merge_base_sha)
except GitError as e:
logging.error("Failed to get version info: %s")
return 1
version_info = FetchVersionInfo(directory=src_dir,
git_log_filter=git_log_filter)
revision_string = version_info.revision revision_string = version_info.revision
if args.revision_id_only: if args.revision_id_only:
revision_string = version_info.revision_id revision_string = version_info.revision_id
if args.print_only: if args.print_only:
print revision_string print(revision_string)
else: else:
contents = "LASTCHANGE=%s\n" % revision_string contents = "LASTCHANGE=%s\n" % revision_string
if not out_file and not args.header: if not out_file and not args.header:
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment