Skip to content
Permalink
Browse files
[git-webkit] Add identifiers to 'log' and 'blame'
https://bugs.webkit.org/show_bug.cgi?id=228027
<rdar://problem/80691164>

Reviewed by Dewei Zhu.

* Scripts/libraries/webkitscmpy/setup.py: Bump version.
* Scripts/libraries/webkitscmpy/webkitscmpy/__init__.py: Ditto.
* Scripts/libraries/webkitscmpy/webkitscmpy/local/scm.py:
(Scm.from_path): Pass all kwargs to local SCM object.
* Scripts/libraries/webkitscmpy/webkitscmpy/program/__init__.py:
(main): Add Blame and Log commands.
* Scripts/libraries/webkitscmpy/webkitscmpy/program/blame.py: Added.
(Blame): Invoke pager.
* Scripts/libraries/webkitscmpy/webkitscmpy/program/command.py:
(FilteredCommand): Shared command that handles replace hashes/revisions in
an SCM command with identifiers.
(FilteredCommand.parser): Allow user to change the prefered commit representation.
(FilteredCommand.pager): Pass output through more if called from a terminal.
(FilteredCommand.main): Modify output of provided command to replace commit representation
with the preffered commit representation.
* Scripts/libraries/webkitscmpy/webkitscmpy/program/log.py: Added.
(Log): Invoke pager.
* Scripts/libraries/webkitscmpy/webkitscmpy/test/log_unittest.py: Added.
(TestLog):
(TestLog.setUp):
(TestLog.test_git):
(TestLog.test_git_svn):
(TestLog.test_git):


Canonical link: https://commits.webkit.org/240074@main
git-svn-id: https://svn.webkit.org/repository/webkit/trunk@280436 268f45cc-cd09-0410-ab3c-d52691b4dbfc
  • Loading branch information
JonWBedard committed Jul 29, 2021
1 parent 3a99e6f commit d79a58ae5b82bca87253bc4a915c80e487c0cf62
Showing 10 changed files with 556 additions and 9 deletions.
@@ -1,3 +1,35 @@
2021-07-29 Jonathan Bedard <jbedard@apple.com>

[git-webkit] Add identifiers to 'log' and 'blame'
https://bugs.webkit.org/show_bug.cgi?id=228027
<rdar://problem/80691164>

Reviewed by Dewei Zhu.

* Scripts/libraries/webkitscmpy/setup.py: Bump version.
* Scripts/libraries/webkitscmpy/webkitscmpy/__init__.py: Ditto.
* Scripts/libraries/webkitscmpy/webkitscmpy/local/scm.py:
(Scm.from_path): Pass all kwargs to local SCM object.
* Scripts/libraries/webkitscmpy/webkitscmpy/program/__init__.py:
(main): Add Blame and Log commands.
* Scripts/libraries/webkitscmpy/webkitscmpy/program/blame.py: Added.
(Blame): Invoke pager.
* Scripts/libraries/webkitscmpy/webkitscmpy/program/command.py:
(FilteredCommand): Shared command that handles replace hashes/revisions in
an SCM command with identifiers.
(FilteredCommand.parser): Allow user to change the prefered commit representation.
(FilteredCommand.pager): Pass output through more if called from a terminal.
(FilteredCommand.main): Modify output of provided command to replace commit representation
with the preffered commit representation.
* Scripts/libraries/webkitscmpy/webkitscmpy/program/log.py: Added.
(Log): Invoke pager.
* Scripts/libraries/webkitscmpy/webkitscmpy/test/log_unittest.py: Added.
(TestLog):
(TestLog.setUp):
(TestLog.test_git):
(TestLog.test_git_svn):
(TestLog.test_git):

2021-07-29 Jonathan Bedard <jbedard@apple.com>

[webkitcorepy] Catch AttributeError when getting password
@@ -29,7 +29,7 @@ def readme():

setup(
name='webkitscmpy',
version='0.15.0',
version='1.0.0',
description='Library designed to interact with git and svn repositories.',
long_description=readme(),
classifiers=[
@@ -46,7 +46,7 @@ def _maybe_add_webkitcorepy_path():
"Please install webkitcorepy with `pip install webkitcorepy --extra-index-url <package index URL>`"
)

version = Version(0, 15, 0)
version = Version(1, 0, 0)

AutoInstall.register(Package('fasteners', Version(0, 15, 0)))
AutoInstall.register(Package('monotonic', Version(1, 5)))
@@ -45,13 +45,13 @@ def executable(cls, program):
return os.path.realpath(path)

@classmethod
def from_path(cls, path, contributors=None):
def from_path(cls, path, contributors=None, **kwargs):
from webkitscmpy import local

if local.Git.is_checkout(path):
return local.Git(path, contributors=contributors)
return local.Git(path, contributors=contributors, **kwargs)
if local.Svn.is_checkout(path):
return local.Svn(path, contributors=contributors)
return local.Svn(path, contributors=contributors, **kwargs)
raise OSError("'{}' is not a known SCM type".format(path))

def __init__(self, path, dev_branches=None, prod_branches=None, contributors=None, id=None):
@@ -131,6 +131,15 @@ def __init__(self, path='/.invalid-svn', datafile=None, remote=None, utc_offset=
end=int(args[3].split(':')[0]),
begin=int(args[3].split(':')[-1]),
) if self.connected else mocks.ProcessCompletion(returncode=1)
), mocks.Subprocess.Route(
self.executable, 'log',
cwd=self.path,
generator=lambda *args, **kwargs:
self._log_range(
branch='trunk',
begin=self.commits['trunk'][0].revision,
end=self.commits['trunk'][-1].revision,
) if self.connected else mocks.ProcessCompletion(returncode=1)
), mocks.Subprocess.Route(
self.executable, 'up', '-r', re.compile(r'\d+'),
cwd=self.path,
@@ -24,17 +24,19 @@
import logging
import os

from webkitcorepy import arguments, log as webkitcorepy_log
from webkitscmpy import local, log, remote

from .blame import Blame
from .canonicalize import Canonicalize
from .clean import Clean
from .command import Command
from .checkout import Checkout
from .find import Find, Info
from .log import Log
from .pull import Pull
from .setup_git_svn import SetupGitSvn

from webkitcorepy import arguments, log as webkitcorepy_log
from webkitscmpy import local, log, remote


def main(args=None, path=None, loggers=None, contributors=None, identifier_template=None, subversion=None):
logging.basicConfig(level=logging.WARNING)
@@ -61,7 +63,7 @@ def main(args=None, path=None, loggers=None, contributors=None, identifier_templ

subparsers = parser.add_subparsers(help='sub-command help')

programs = [Find, Info, Checkout, Canonicalize, Pull, Clean]
programs = [Find, Info, Checkout, Canonicalize, Pull, Clean, Log, Blame]
if subversion:
programs.append(SetupGitSvn)

@@ -0,0 +1,45 @@
#!/usr/bin/env python3

# Copyright (C) 2021 Apple Inc. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

import sys

from webkitscmpy import local
from webkitscmpy.program.command import FilteredCommand


class Blame(FilteredCommand):
name = 'blame'
help = "Filter raw output of 'git blame' or 'svn blame' to replace native commit representation with identifiers"

@classmethod
def main(cls, args, repository, **kwargs):
return cls.pager(args, repository, file=__file__, **kwargs)


if __name__ == '__main__':
sys.exit(Blame.main(
sys.argv[3:],
local.Scm.from_path(path=sys.argv[1], cached=True),
representation=sys.argv[2],
))
@@ -20,7 +20,14 @@
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

import os
import re
import subprocess
import sys
import time

from webkitscmpy.commit import Commit
from whichcraft import which


class Command(object):
@@ -38,3 +45,204 @@ def parser(cls, parser, loggers=None):
def main(cls, args, repository, **kwargs):
sys.stderr.write('No command specified\n')
return -1


class FilteredCommand(object):
IDENTIFIER = 'identifier'
HASH = 'hash'
REVISION = 'revision'

GIT_HEADER_RE = re.compile(r'^commit (?P<hash>[a-f0-9A-F]+)')
SVN_HEADER_RE = re.compile(r'^(?P<revision>r/d+) | ')

REVISION_RES = (re.compile(r'^(?P<revision>\d+)\s'), re.compile(r'(?P<revision>[rR]\d+)'))
HASH_RES = (re.compile(r'^(?P<hash>[a-f0-9A-F]{8}[a-f0-9A-F]+)\s'), re.compile(r'(?P<hash>[a-f0-9A-F]{8}[a-f0-9A-F]+)'))
IDENTIFIER_RES = (re.compile(r'^(?P<identifier>(\d+\.)?\d+@\S*)'), re.compile(r'(?P<identifier>(\d+\.)?\d+@\S*)'))
NO_FILTER_RES = [re.compile(r' Canonical link:'), re.compile(r' git-svn-id:')]

REPLACE_MODE = 0
APPEND_MODE = 1
HEADER_MODE = 2

@classmethod
def parser(cls, parser, loggers=None):
parser.add_argument(
'args', nargs='*',
type=str, default=None,
help='Arguments to be passed to tbe native source-code management tool',
)
parser.add_argument(
'--identifier', '-i',
help='Represent commits as identifiers',
dest='representation',
action='store_const', const=cls.IDENTIFIER,
default=cls.IDENTIFIER,
)
parser.add_argument(
'--hash',
help='Represent commits as hashes',
dest='representation',
action='store_const', const=cls.HASH,
default=cls.IDENTIFIER,
)
parser.add_argument(
'--revision', '-r',
help='Represent commits as revisions',
dest='representation',
action='store_const', const=cls.REVISION,
default=cls.IDENTIFIER,
)
parser.add_argument(
'--representation', type=str,
help='Change method used to represent a commit (identifier/hash/revision)',
dest='representation',
default=cls.IDENTIFIER,
)

@classmethod
def pager(cls, args, repository, file=None, **kwargs):
if not repository.path:
sys.stderr.write("Cannot run '{}' on remote repository\n".format(cls.name))
return 1

if not getattr(repository, 'cache', None) and getattr(repository, 'Cache', None):
repository.cache = repository.Cache(repository)

# If we're a terminal, rely on 'more' to display output
if sys.stdin.isatty() and not isinstance(args, list) and file:
environ = os.environ
environ['PYTHONPATH'] = ':'.join(sys.path)

child = subprocess.Popen(
[sys.executable, file, repository.root_path, args.representation] + args.args,
env=environ,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
more = subprocess.Popen([which('more'), '-F'], stdin=child.stdout)

try:
while more.poll() is None and not child.poll():
time.sleep(0.25)
finally:
child.kill()
more.kill()
child_error = child.stderr.read()
if child_error:
sys.stderr.buffer.write(b'\n' + child_error)

return child.returncode

return FilteredCommand.main(args, repository, command=cls.name, **kwargs)

@classmethod
def main(cls, args, repository, command=None, representation=None, **kwargs):
if not repository.path:
sys.stderr.write("Cannot run '{}' on remote repository\n".format(command))
return 1

cache = getattr(repository, 'cache', None)
if not cache:
sys.stderr.write('No cache available, cannot performantly map commit references\n')
return 1

if not isinstance(args, list):
representation = representation or args.representation
args = args.args
else:
representation = representation or cls.IDENTIFIER

if representation not in (cls.IDENTIFIER, cls.HASH, cls.REVISION):
sys.stderr.write("'{}' is not a valid commit representation\n".format(representation))
return 1

for index in range(len(args)):
parsed = Commit.parse(args[index], do_assert=False)
if not parsed:
continue
replacement = None
if repository.is_svn:
replacement = repository.cache.to_revision(hash=parsed.hash, identifier=str(parsed) if parsed.identifier else None)
if repository.is_git:
replacement = repository.cache.to_hash(revision=parsed.revision, identifier=str(parsed) if parsed.identifier else None)
if replacement:
args[index] = replacement

log_output = subprocess.Popen(
[repository.executable(), command] + args,
cwd=repository.root_path,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
**(dict(encoding='utf-8') if sys.version_info > (3, 0) else dict())
)
log_output.poll()

def replace_line(match, mode=cls.APPEND_MODE, **kwargs):
reference = kwargs.get(representation)
if not reference:
reference = getattr(cache, 'to_{}'.format(representation), lambda **kwargs: None)(**kwargs)

if reference:
if isinstance(reference, int):
reference = 'r{}'.format(reference)
if representation == 'hash':
reference = reference[:Commit.HASH_LABEL_SIZE]
if mode == cls.APPEND_MODE:
reference = '{} ({})'.format(match.group(1), reference)
if mode == cls.HEADER_MODE:
alternates = [] if match.group(1).startswith(reference) else [match.group(1)]
for repr in {'revision', 'hash', 'identifier'} - {'hash' if repository.is_git else 'revision', representation}:
if repr in kwargs:
continue
other = getattr(cache, 'to_{}'.format(repr), lambda **kwargs: None)(**kwargs)
if not other:
continue
if other == 'hash':
other = other[:Commit.HASH_LABEL_SIZE]
alternates.append('r{}'.format(other) if isinstance(other, int) else other)
reference = '{} ({})'.format(reference, ', '.join(alternates))
return match.group(0).replace(match.group(1), reference)
return match.group(0)

res = {}
if representation != cls.HASH:
res[cls.HASH] = cls.HASH_RES
if representation != cls.REVISION:
res[cls.REVISION] = cls.REVISION_RES
if representation != cls.IDENTIFIER:
res[cls.IDENTIFIER] = cls.IDENTIFIER_RES

header_re = cls.GIT_HEADER_RE if repository.is_git else cls.SVN_HEADER_RE

try:
line = log_output.stdout.readline()
while line:
header = header_re.sub(
lambda match: replace_line(match, mode=cls.HEADER_MODE, **{'hash' if repository.is_git else 'revision': match.group(1)}),
line,
)
if header != line:
sys.stdout.write(header)
line = log_output.stdout.readline()
continue

for repr, regexs in res.items():
line = regexs[0].sub(
lambda match: replace_line(match, mode=cls.REPLACE_MODE, **{repr: match.group(1)}),
line,
)

if not any(r.match(line) for r in cls.NO_FILTER_RES):
for repr, regexs in res.items():
line = regexs[1].sub(
lambda match: replace_line(match, mode=cls.APPEND_MODE, **{repr: match.group(1)}),
line,
)

sys.stdout.write(line)

line = log_output.stdout.readline()

finally:
log_output.kill()
return log_output.returncode

0 comments on commit d79a58a

Please sign in to comment.