Title: [280864] trunk/Tools
Revision
280864
Author
jbed...@apple.com
Date
2021-08-10 14:35:31 -0700 (Tue, 10 Aug 2021)

Log Message

[git-webkit] Color `log` output
https://bugs.webkit.org/show_bug.cgi?id=228662
<rdar://problem/81344181>

Reviewed by Dewei Zhu.

* Scripts/libraries/webkitcorepy/setup.py: Bump version.
* Scripts/libraries/webkitcorepy/webkitcorepy/__init__.py: Bump version, export Terminal.
* Scripts/libraries/webkitcorepy/webkitcorepy/terminal.py: Added.
(Terminal):
(Terminal.assert_writeable_stream): Assert that the provided object is a writable stream.
(Terminal.supports_color): Check if the provided file supports colored output.
(Terminal.isatty): Check if a stream is an atty, both according to the stream itself and our override.
(Terminal.override_atty): Allow callers to override and declare explicitly if a stream is an atty.
(Terminal.Text): Group text colors and styles.
(Terminal.Style): Group functions modifying style of a stream.
(Terminal.Style.enabled): Check if styled printing is enabled on a stream.
(Terminal.Style.disable): Disable styled printing on a stream.
(Terminal.Style.enable): Enable styled printing on a stream.
(Terminal.Style.is_styled): Check if a stream is currently styled.
(Terminal.Style.__init__): Construct a text style.
(Terminal.Style.__repr__): Output the terminal characters styling a stream.
(Terminal.Style.set): Apply style to a stream.
(Terminal.Style.apply): Apply style to a stream, unapply when exiting context.
* Scripts/libraries/webkitscmpy/setup.py:
* Scripts/libraries/webkitscmpy/webkitscmpy/__init__.py:
* Scripts/libraries/webkitscmpy/webkitscmpy/program/blame.py: Pass isatty to child process.
* Scripts/libraries/webkitscmpy/webkitscmpy/program/command.py:
(FilteredCommand.pager): Pass isatty to child process, ask 'more' to display colors.
(FilteredCommand.main): Color header and error.
* Scripts/libraries/webkitscmpy/webkitscmpy/program/log.py: Pass isatty to child process.
* Scripts/libraries/webkitscmpy/webkitscmpy/test/log_unittest.py: Override atty behavior.

Modified Paths

Diff

Modified: trunk/Tools/ChangeLog (280863 => 280864)


--- trunk/Tools/ChangeLog	2021-08-10 21:28:35 UTC (rev 280863)
+++ trunk/Tools/ChangeLog	2021-08-10 21:35:31 UTC (rev 280864)
@@ -1,3 +1,38 @@
+2021-08-10  Jonathan Bedard  <jbed...@apple.com>
+
+        [git-webkit] Color `log` output
+        https://bugs.webkit.org/show_bug.cgi?id=228662
+        <rdar://problem/81344181>
+
+        Reviewed by Dewei Zhu.
+
+        * Scripts/libraries/webkitcorepy/setup.py: Bump version.
+        * Scripts/libraries/webkitcorepy/webkitcorepy/__init__.py: Bump version, export Terminal.
+        * Scripts/libraries/webkitcorepy/webkitcorepy/terminal.py: Added.
+        (Terminal):
+        (Terminal.assert_writeable_stream): Assert that the provided object is a writable stream.
+        (Terminal.supports_color): Check if the provided file supports colored output.
+        (Terminal.isatty): Check if a stream is an atty, both according to the stream itself and our override.
+        (Terminal.override_atty): Allow callers to override and declare explicitly if a stream is an atty.
+        (Terminal.Text): Group text colors and styles.
+        (Terminal.Style): Group functions modifying style of a stream.
+        (Terminal.Style.enabled): Check if styled printing is enabled on a stream.
+        (Terminal.Style.disable): Disable styled printing on a stream.
+        (Terminal.Style.enable): Enable styled printing on a stream.
+        (Terminal.Style.is_styled): Check if a stream is currently styled.
+        (Terminal.Style.__init__): Construct a text style.
+        (Terminal.Style.__repr__): Output the terminal characters styling a stream.
+        (Terminal.Style.set): Apply style to a stream.
+        (Terminal.Style.apply): Apply style to a stream, unapply when exiting context.
+        * Scripts/libraries/webkitscmpy/setup.py:
+        * Scripts/libraries/webkitscmpy/webkitscmpy/__init__.py:
+        * Scripts/libraries/webkitscmpy/webkitscmpy/program/blame.py: Pass isatty to child process.
+        * Scripts/libraries/webkitscmpy/webkitscmpy/program/command.py:
+        (FilteredCommand.pager): Pass isatty to child process, ask 'more' to display colors.
+        (FilteredCommand.main): Color header and error.
+        * Scripts/libraries/webkitscmpy/webkitscmpy/program/log.py: Pass isatty to child process.
+        * Scripts/libraries/webkitscmpy/webkitscmpy/test/log_unittest.py: Override atty behavior.
+
 2021-08-10  Ben Nham  <n...@apple.com>
 
         Fix nested resource load tracepoints

Modified: trunk/Tools/Scripts/libraries/webkitcorepy/setup.py (280863 => 280864)


--- trunk/Tools/Scripts/libraries/webkitcorepy/setup.py	2021-08-10 21:28:35 UTC (rev 280863)
+++ trunk/Tools/Scripts/libraries/webkitcorepy/setup.py	2021-08-10 21:35:31 UTC (rev 280864)
@@ -30,7 +30,7 @@
 
 setup(
     name='webkitcorepy',
-    version='0.8.1',
+    version='0.8.2',
     description='Library containing various Python support classes and functions.',
     long_description=readme(),
     classifiers=[

Modified: trunk/Tools/Scripts/libraries/webkitcorepy/webkitcorepy/__init__.py (280863 => 280864)


--- trunk/Tools/Scripts/libraries/webkitcorepy/webkitcorepy/__init__.py	2021-08-10 21:28:35 UTC (rev 280863)
+++ trunk/Tools/Scripts/libraries/webkitcorepy/webkitcorepy/__init__.py	2021-08-10 21:35:31 UTC (rev 280864)
@@ -40,7 +40,7 @@
 from webkitcorepy.measure_time import MeasureTime
 from webkitcorepy.nested_fuzzy_dict import NestedFuzzyDict
 
-version = Version(0, 8, 1)
+version = Version(0, 8, 2)
 
 from webkitcorepy.autoinstall import Package, AutoInstall
 if sys.version_info > (3, 0):

Modified: trunk/Tools/Scripts/libraries/webkitcorepy/webkitcorepy/terminal.py (280863 => 280864)


--- trunk/Tools/Scripts/libraries/webkitcorepy/webkitcorepy/terminal.py	2021-08-10 21:28:35 UTC (rev 280863)
+++ trunk/Tools/Scripts/libraries/webkitcorepy/webkitcorepy/terminal.py	2021-08-10 21:35:31 UTC (rev 280864)
@@ -20,10 +20,20 @@
 # (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 contextlib
+import io
 import sys
 
+from webkitcorepy import StringIO
 
+if sys.version_info > (3, 0):
+    file = io.IOBase
+
+
 class Terminal(object):
+    _atty_overrides = {}
+    colors = True
+
     @classmethod
     def input(cls, *args, **kwargs):
         return (input if sys.version_info > (3, 0) else raw_input)(*args, **kwargs)
@@ -57,3 +67,167 @@
                 response = default
 
         return response
+
+    @classmethod
+    def assert_writeable_stream(cls, target):
+        if not isinstance(target, (io.IOBase, file, StringIO)):
+            raise ValueError('{} is not an IO object'.format(target))
+        if not isinstance(target, StringIO) and not (getattr(target, 'writable', None) and target.writable()) and 'w' not in getattr(target, 'mode', 'r'):
+            raise ValueError('{} is an IO object, but is not writable {}'.format(target, ))
+
+    @classmethod
+    def supports_color(cls, file):
+        if not cls.colors:
+            return False
+        return cls.isatty(file)
+
+    @classmethod
+    def isatty(cls, file):
+        try:
+            return cls._atty_overrides.get(file.fileno(), file.isatty())
+        except (io.UnsupportedOperation, AttributeError):
+            return cls._atty_overrides.get(str(file), file.isatty())
+
+    @classmethod
+    @contextlib.contextmanager
+    def override_atty(cls, target, isatty=True):
+        if not isinstance(target, (io.IOBase, file, StringIO)):
+            raise ValueError('{} is not an IO object'.format(target))
+
+        try:
+            key = target.fileno()
+        except (io.UnsupportedOperation, AttributeError):
+            key = str(target)
+
+        previous = cls._atty_overrides.get(key)
+        cls._atty_overrides[key] = isatty
+
+        try:
+            yield
+        finally:
+            if previous is None:
+                del cls._atty_overrides[key]
+            else:
+                cls._atty_overrides[key] = previous
+
+    class Text(object):
+        value = lambda value: '\033[{}m'.format(value)
+
+        reset = value(0)
+
+        styles = [value(1), value(4), value(5), value(8)]
+        bold, underline, blink, concealed = styles
+
+        colors = [value(30), value(31), value(32), value(33), value(34), value(35), value(36), value(37)]
+        black, red, green, yellow, blue, magenta, cyan, white = colors
+
+        backgroundColors = [value(40), value(41), value(42), value(43), value(44), value(45), value(46), value(47)]
+        blackBackground, redBackground, greenBackground, yellowBackground, blueBackground, magentaBackground, cyanBackground, whiteBackground = colors
+
+    class Style(object):
+        top = {}
+        _disabled = set()
+        _is_styled = set()
+
+        @classmethod
+        def enabled(cls, file):
+            Terminal.assert_writeable_stream(file)
+
+            try:
+                if not Terminal.supports_color(file) or not file.fileno():
+                    return False
+            except (io.UnsupportedOperation, AttributeError):
+                return False
+            return file.fileno() not in cls._disabled
+
+        @classmethod
+        def disable(cls, file):
+            Terminal.assert_writeable_stream(file)
+            if not cls.enabled(file):
+                return
+            cls._disabled.add(file.fileno())
+            Terminal.Style().set(file)
+
+        @classmethod
+        def enable(cls, file):
+            Terminal.assert_writeable_stream(file)
+            if cls.enabled(file):
+                return
+
+            cls._disabled.discard(file.fileno())
+            if cls.top.get(file.fileno()):
+                cls.top.get(file.fileno()).set(file)
+
+        @classmethod
+        def is_styled(cls, file):
+            try:
+                return file.fileno() in cls._is_styled
+            except (io.UnsupportedOperation, AttributeError):
+                return False
+
+        def __init__(self, style=None, color=None, backgroundColor=None):
+            if style and style not in Terminal.Text.styles:
+                raise ValueError('{} is not a recognized terminal text style'.format(style))
+            self.style = style
+
+            if color and color not in Terminal.Text.colors:
+                raise ValueError('{} is not a recognized terminal color'.format(color))
+            self.color = color
+
+            if backgroundColor and backgroundColor not in Terminal.Text.backgroundColors:
+                raise ValueError('{} is not a recognized terminal background color'.format(backgroundColor))
+            self.backgroundColor = backgroundColor
+
+        def __repr__(self):
+            result = Terminal.Text.reset
+            if self.style:
+                result += self.style
+            if self.color:
+                result += self.color
+            if self.backgroundColor:
+                result += self.backgroundColor
+            return result
+
+        def set(self, file):
+            Terminal.assert_writeable_stream(file)
+            will_style = True
+            if not self.style and not self.color and not self.backgroundColor:
+                will_style = False
+            elif not self.enabled(file):
+                will_style = False
+
+            if will_style:
+                self._is_styled.add(file.fileno())
+                file.write(str(self))
+                return
+
+            if self.is_styled(file):
+                file.write(Terminal.Text.reset)
+            try:
+                self._is_styled.discard(file.fileno())
+            except (io.UnsupportedOperation, AttributeError):
+                pass
+
+        @contextlib.contextmanager
+        def apply(self, target):
+            Terminal.assert_writeable_stream(target)
+            try:
+                previous = self.top.get(target.fileno())
+            except (io.UnsupportedOperation, AttributeError):
+                try:
+                    yield
+                finally:
+                    return
+
+            self.top[target.fileno()] = self
+            self.set(target)
+
+            try:
+                yield
+            finally:
+                if previous:
+                    previous.set(target)
+                    self.top[target.fileno()] = previous
+                else:
+                    Terminal.Style().set(target)
+                    del self.top[target.fileno()]

Modified: trunk/Tools/Scripts/libraries/webkitscmpy/setup.py (280863 => 280864)


--- trunk/Tools/Scripts/libraries/webkitscmpy/setup.py	2021-08-10 21:28:35 UTC (rev 280863)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/setup.py	2021-08-10 21:35:31 UTC (rev 280864)
@@ -29,7 +29,7 @@
 
 setup(
     name='webkitscmpy',
-    version='1.1.0',
+    version='1.1.1',
     description='Library designed to interact with git and svn repositories.',
     long_description=readme(),
     classifiers=[

Modified: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/__init__.py (280863 => 280864)


--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/__init__.py	2021-08-10 21:28:35 UTC (rev 280863)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/__init__.py	2021-08-10 21:35:31 UTC (rev 280864)
@@ -46,7 +46,7 @@
         "Please install webkitcorepy with `pip install webkitcorepy --extra-index-url <package index URL>`"
     )
 
-version = Version(1, 1, 0)
+version = Version(1, 1, 1)
 
 AutoInstall.register(Package('fasteners', Version(0, 15, 0)))
 AutoInstall.register(Package('monotonic', Version(1, 5)))

Modified: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/program/blame.py (280863 => 280864)


--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/program/blame.py	2021-08-10 21:28:35 UTC (rev 280863)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/program/blame.py	2021-08-10 21:35:31 UTC (rev 280864)
@@ -39,7 +39,8 @@
 
 if __name__ == '__main__':
     sys.exit(Blame.main(
-        sys.argv[3:],
+        sys.argv[4:],
         local.Scm.from_path(path=sys.argv[1], cached=True),
         representation=sys.argv[2],
+        isatty=sys.argv[3] == 'isatty',
     ))

Modified: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/program/command.py (280863 => 280864)


--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/program/command.py	2021-08-10 21:28:35 UTC (rev 280863)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/program/command.py	2021-08-10 21:35:31 UTC (rev 280864)
@@ -27,6 +27,7 @@
 import sys
 import time
 
+from webkitcorepy import Terminal
 from webkitscmpy.commit import Commit
 from whichcraft import which
 
@@ -110,32 +111,35 @@
             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:
+        if Terminal.isatty(sys.stdin) 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,
+                [sys.executable, file, repository.root_path, args.representation, 'isatty' if Terminal.isatty(sys.stdout) else 'notatty'] + args.args,
                 env=environ,
                 cwd=os.getcwd(),
                 stdout=subprocess.PIPE,
                 stderr=subprocess.PIPE,
             )
-            more = subprocess.Popen([which('more')] + ['-F'] if platform.system() == 'Darwin' else [], stdin=child.stdout)
+            more = subprocess.Popen([which('more')] + ['-F', '-R'] if platform.system() == 'Darwin' else [], stdin=child.stdout)
 
             try:
                 while more.poll() is None and not child.poll():
                     time.sleep(0.25)
             finally:
-                child.kill()
-                more.kill()
+                if child.returncode is None:
+                    child.kill()
+                if more.returncode is None:
+                    more.kill()
                 child_error = child.stderr.read()
                 if child_error:
-                    sys.stderr.buffer.write(b'\n' + child_error)
+                    (sys.stderr.buffer if sys.version_info > (3, 0) else sys.stderr).write(b'\n' + child_error)
 
             return child.returncode
 
-        return FilteredCommand.main(args, repository, command=cls.name, **kwargs)
+        with Terminal.override_atty(sys.stdout, isatty=kwargs.get('isatty')), Terminal.override_atty(sys.stderr, isatty=kwargs.get('isatty')):
+            return FilteredCommand.main(args, repository, command=cls.name, **kwargs)
 
     @classmethod
     def main(cls, args, repository, command=None, representation=None, **kwargs):
@@ -234,7 +238,13 @@
                     line,
                 )
                 if header != line:
-                    sys.stdout.write(header)
+                    with Terminal.Style(color=Terminal.Text.yellow, style=Terminal.Text.bold).apply(sys.stdout):
+                        sys.stdout.write(' '.join(header.split(' ')[:2]))
+
+                    sys.stdout.write(' ')
+                    with Terminal.Style(color=Terminal.Text.red).apply(sys.stdout):
+                        sys.stdout.write(' '.join(header.split(' ')[2:]))
+
                     line = log_output.stdout.readline()
                     continue
 
@@ -257,6 +267,8 @@
 
         finally:
             if not log_output.returncode:
-                sys.stderr.write(log_output.stderr.read())
-            log_output.kill()
+                with Terminal.Style(color=Terminal.Text.red).apply(sys.stderr):
+                    sys.stderr.write(log_output.stderr.read())
+            if log_output.returncode is None:
+                log_output.kill()
         return log_output.returncode

Modified: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/program/log.py (280863 => 280864)


--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/program/log.py	2021-08-10 21:28:35 UTC (rev 280863)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/program/log.py	2021-08-10 21:35:31 UTC (rev 280864)
@@ -24,6 +24,7 @@
 
 import sys
 
+from webkitcorepy import Terminal
 from webkitscmpy import local
 from webkitscmpy.program.command import FilteredCommand
 
@@ -34,12 +35,15 @@
 
     @classmethod
     def main(cls, args, repository, **kwargs):
+        config = getattr(repository, 'config', lambda: {})()
+        Terminal.colors = config.get('color.diff', config.get('color.ui', 'auto')) != 'false'
         return cls.pager(args, repository, file=__file__, **kwargs)
 
 
 if __name__ == '__main__':
     sys.exit(Log.main(
-        sys.argv[3:],
+        sys.argv[4:],
         local.Scm.from_path(path=sys.argv[1], cached=True),
         representation=sys.argv[2],
+        isatty=sys.argv[3] == 'isatty',
     ))

Modified: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/test/log_unittest.py (280863 => 280864)


--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/test/log_unittest.py	2021-08-10 21:28:35 UTC (rev 280863)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/test/log_unittest.py	2021-08-10 21:35:31 UTC (rev 280864)
@@ -20,13 +20,10 @@
 # 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 json
 import os
-import shutil
-import tempfile
+import sys
 
-from datetime import datetime
-from webkitcorepy import OutputCapture, testing
+from webkitcorepy import OutputCapture, Terminal, testing
 from webkitcorepy.mocks import Time as MockTime
 from webkitscmpy import program, mocks
 
@@ -40,7 +37,7 @@
         os.mkdir(os.path.join(self.path, '.svn'))
 
     def test_git(self):
-        with OutputCapture() as captured, mocks.local.Git(self.path), mocks.local.Svn(), MockTime:
+        with OutputCapture() as captured, mocks.local.Git(self.path), mocks.local.Svn(), MockTime, Terminal.override_atty(sys.stdin, isatty=False):
             self.assertEqual(-1, program.main(
                 args=('log', 'main'),
                 path=self.path,
@@ -81,7 +78,7 @@
         )
 
     def test_git_svn(self):
-        with OutputCapture() as captured, mocks.local.Git(self.path, git_svn=True), mocks.local.Svn(), MockTime:
+        with OutputCapture() as captured, mocks.local.Git(self.path, git_svn=True), mocks.local.Svn(), MockTime, Terminal.override_atty(sys.stdin, isatty=False):
             self.assertEqual(-1, program.main(
                 args=('log', 'main'),
                 path=self.path,
@@ -127,7 +124,7 @@
             )
 
     def test_git_svn_revision(self):
-        with OutputCapture() as captured, mocks.local.Git(self.path, git_svn=True), mocks.local.Svn(), MockTime:
+        with OutputCapture() as captured, mocks.local.Git(self.path, git_svn=True), mocks.local.Svn(), MockTime, Terminal.override_atty(sys.stdin, isatty=False):
             self.assertEqual(-1, program.main(
                 args=('log', 'main', '--revision'),
                 path=self.path,
@@ -174,7 +171,7 @@
             )
 
     def test_svn(self):
-        with OutputCapture() as captured, mocks.local.Git(), mocks.local.Svn(self.path), MockTime:
+        with OutputCapture() as captured, mocks.local.Git(), mocks.local.Svn(self.path), MockTime, Terminal.override_atty(sys.stdin, isatty=False):
             self.assertEqual(-1, program.main(
                 args=('log',),
                 path=self.path,
_______________________________________________
webkit-changes mailing list
webkit-changes@lists.webkit.org
https://lists.webkit.org/mailman/listinfo/webkit-changes

Reply via email to