text: #!/usr/bin/env python
  # -*- coding: utf-8 -*-
  
  from __future__ import print_function
  from __future__ import absolute_import
  
  import locale
  import re
  import os
  import os.path
  import sys
  import glob
  import textwrap
  import datetime
  import collections
  import traceback
  import contextlib
  import itertools
  import errno
  
  from subprocess import Popen, PIPE
  
  try:
      import pystache
  except ImportError:  ## pragma: no cover
      pystache = None
  
  try:
      import mako
  except ImportError:  ## pragma: no cover
      mako = None
  
  
  __version__ = "3.0.4"  ## replaced by autogen.sh
  
  DEBUG = None
  
  
  ##
  ## Platform and python compatibility
  ##
  
  PY_VERSION = float("%d.%d" % sys.version_info[0:2])
  PY3 = PY_VERSION >= 3
  
  try:
      basestring
  except NameError:
      basestring = str  ## pylint: disable=redefined-builtin
  
  WIN32 = sys.platform == 'win32'
  if WIN32:
      PLT_CFG = {
          'close_fds': False,
      }
  else:
      PLT_CFG = {
          'close_fds': True,
      }
  
  ##
  ##
  ##
  
  if WIN32 and not PY3:
  
      ## Sorry about the following, all this code is to ensure full
      ## compatibility with python 2.7 under windows about sending unicode
      ## command-line
  
      import ctypes
      import subprocess
      import _subprocess
      from ctypes import byref, windll, c_char_p, c_wchar_p, c_void_p, \
           Structure, sizeof, c_wchar, WinError
      from ctypes.wintypes import BYTE, WORD, LPWSTR, BOOL, DWORD, LPVOID, \
           HANDLE
  
  
      ##
      ## Types
      ##
  
      CREATE_UNICODE_ENVIRONMENT = 0x00000400
      LPCTSTR = c_char_p
      LPTSTR = c_wchar_p
      LPSECURITY_ATTRIBUTES = c_void_p
      LPBYTE  = ctypes.POINTER(BYTE)
  
      class STARTUPINFOW(Structure):
          _fields_ = [
              ("cb",              DWORD),  ("lpReserved",    LPWSTR),
              ("lpDesktop",       LPWSTR), ("lpTitle",       LPWSTR),
              ("dwX",             DWORD),  ("dwY",           DWORD),
              ("dwXSize",         DWORD),  ("dwYSize",       DWORD),
              ("dwXCountChars",   DWORD),  ("dwYCountChars", DWORD),
              ("dwFillAtrribute", DWORD),  ("dwFlags",       DWORD),
              ("wShowWindow",     WORD),   ("cbReserved2",   WORD),
              ("lpReserved2",     LPBYTE), ("hStdInput",     HANDLE),
              ("hStdOutput",      HANDLE), ("hStdError",     HANDLE),
          ]
  
      LPSTARTUPINFOW = ctypes.POINTER(STARTUPINFOW)
  
  
      class PROCESS_INFORMATION(Structure):
          _fields_ = [
              ("hProcess",         HANDLE), ("hThread",          HANDLE),
              ("dwProcessId",      DWORD),  ("dwThreadId",       DWORD),
          ]
  
      LPPROCESS_INFORMATION = ctypes.POINTER(PROCESS_INFORMATION)
  
  
      class DUMMY_HANDLE(ctypes.c_void_p):
  
          def __init__(self, *a, **kw):
              super(DUMMY_HANDLE, self).__init__(*a, **kw)
              self.closed = False
  
          def Close(self):
              if not self.closed:
                  windll.kernel32.CloseHandle(self)
                  self.closed = True
  
          def __int__(self):
              return self.value
  
  
      CreateProcessW = windll.kernel32.CreateProcessW
      CreateProcessW.argtypes = [
          LPCTSTR, LPTSTR, LPSECURITY_ATTRIBUTES,
          LPSECURITY_ATTRIBUTES, BOOL, DWORD, LPVOID, LPCTSTR,
          LPSTARTUPINFOW, LPPROCESS_INFORMATION,
      ]
      CreateProcessW.restype = BOOL
  
  
      ##
      ## Patched functions/classes
      ##
  
      def CreateProcess(executable, args, _p_attr, _t_attr,
                        inherit_handles, creation_flags, env, cwd,
                        startup_info):
          """Create a process supporting unicode executable and args for win32
  
          Python implementation of CreateProcess using CreateProcessW for Win32
  
          """
  
          si = STARTUPINFOW(
              dwFlags=startup_info.dwFlags,
              wShowWindow=startup_info.wShowWindow,
              cb=sizeof(STARTUPINFOW),
              ## XXXvlab: not sure of the casting here to ints.
              hStdInput=int(startup_info.hStdInput),
              hStdOutput=int(startup_info.hStdOutput),
              hStdError=int(startup_info.hStdError),
          )
  
          wenv = None
          if env is not None:
              ## LPCWSTR seems to be c_wchar_p, so let's say CWSTR is c_wchar
              env = (unicode("").join([
                  unicode("%s=%s\0") % (k, v)
                  for k, v in env.items()])) + unicode("\0")
              wenv = (c_wchar * len(env))()
              wenv.value = env
  
          pi = PROCESS_INFORMATION()
          creation_flags |= CREATE_UNICODE_ENVIRONMENT
  
          if CreateProcessW(executable, args, None, None,
                            inherit_handles, creation_flags,
                            wenv, cwd, byref(si), byref(pi)):
              return (DUMMY_HANDLE(pi.hProcess), DUMMY_HANDLE(pi.hThread),
                      pi.dwProcessId, pi.dwThreadId)
          raise WinError()
  
  
      class Popen(subprocess.Popen):
          """This superseeds Popen and corrects a bug in cPython 2.7 implem"""
  
          def _execute_child(self, args, executable, preexec_fn, close_fds,
                             cwd, env, universal_newlines,
                             startupinfo, creationflags, shell, to_close,
                             p2cread, p2cwrite,
                             c2pread, c2pwrite,
                             errread, errwrite):
              """Code from part of _execute_child from Python 2.7 (9fbb65e)
  
              There are only 2 little changes concerning the construction of
              the the final string in shell mode: we preempt the creation of
              the command string when shell is True, because original function
              will try to encode unicode args which we want to avoid to be able to
              sending it as-is to ``CreateProcess``.
  
              """
              if not isinstance(args, subprocess.types.StringTypes):
                  args = subprocess.list2cmdline(args)
  
              if startupinfo is None:
                  startupinfo = subprocess.STARTUPINFO()
              if shell:
                  startupinfo.dwFlags |= _subprocess.STARTF_USESHOWWINDOW
                  startupinfo.wShowWindow = _subprocess.SW_HIDE
                  comspec = os.environ.get("COMSPEC", unicode("cmd.exe"))
                  args = unicode('{} /c "{}"').format(comspec, args)
                  if (_subprocess.GetVersion() >= 0x80000000 or
                          os.path.basename(comspec).lower() == "command.com"):
                      w9xpopen = self._find_w9xpopen()
                      args = unicode('"%s" %s') % (w9xpopen, args)
                      creationflags |= _subprocess.CREATE_NEW_CONSOLE
  
              super(Popen, self)._execute_child(args, executable,
                  preexec_fn, close_fds, cwd, env, universal_newlines,
                  startupinfo, creationflags, False, to_close, p2cread,
                  p2cwrite, c2pread, c2pwrite, errread, errwrite)
  
      _subprocess.CreateProcess = CreateProcess
  
  
  ##
  ## Help and usage strings
  ##
  
  usage_msg = """
    %(exname)s {-h|--help}
    %(exname)s {-v|--version}
    %(exname)s [--debug|-d] [REVLIST]"""
  
  description_msg = """\
  Run this command in a git repository to output a formatted changelog
  """
  
  epilog_msg = """\
  %(exname)s uses a config file to filter meaningful commit or do some
  formatting in commit messages thanks to a config file.
  
  Config file location will be resolved in this order:
    - in shell environment variable GITCHANGELOG_CONFIG_FILENAME
    - in git configuration: ``git config gitchangelog.rc-path``
    - as '.%(exname)s.rc' in the root of the current git repository
  
  """
  
  
  ##
  ## Shell command helper functions
  ##
  
  def stderr(msg):
      print(msg, file=sys.stderr)
  
  
  def err(msg):
      stderr("Error: " + msg)
  
  
  def warn(msg):
      stderr("Warning: " + msg)
  
  
  def die(msg=None, errlvl=1):
      if msg:
          stderr(msg)
      sys.exit(errlvl)
  
  
  class ShellError(Exception):
  
      def __init__(self, msg, errlvl=None, command=None, out=None, err=None):
          self.errlvl = errlvl
          self.command = command
          self.out = out
          self.err = err
          super(ShellError, self).__init__(msg)
  
  
  @contextlib.contextmanager
  def set_cwd(directory):
      curdir = os.getcwd()
      os.chdir(directory)
      try:
          yield
      finally:
          os.chdir(curdir)
  
  
  def format_last_exception(prefix="  | "):
      """Format the last exception for display it in tests.
  
      This allows to raise custom exception, without loosing the context of what
      caused the problem in the first place:
  
      >>> def f():
      ...     raise Exception("Something terrible happened")
      >>> try:  ## doctest: +ELLIPSIS
      ...     f()
      ... except Exception:
      ...     formated_exception = format_last_exception()
      ...     raise ValueError('Oups, an error occured:\\n%s'
      ...         % formated_exception)
      Traceback (most recent call last):
      ...
      ValueError: Oups, an error occured:
        | Traceback (most recent call last):
      ...
        | Exception: Something terrible happened
  
      """
  
      return '\n'.join(
          str(prefix + line)
          for line in traceback.format_exc().strip().split('\n'))
  
  
  ##
  ## config file functions
  ##
  
  _config_env = {
      'WIN32': WIN32,
      'PY3': PY3,
  }
  
  
  def available_in_config(f):
      _config_env[f.__name__] = f
      return f
  
  
  def load_config_file(filename, default_filename=None,
                       fail_if_not_present=True):
      """Loads data from a config file."""
  
      config = _config_env.copy()
      for fname in [default_filename, filename]:
          if fname and os.path.exists(fname):
              if not os.path.isfile(fname):
                  die("config file path '%s' exists but is not a file !"
                      % (fname, ))
              content = file_get_contents(fname)
              try:
                  code = compile(content, fname, 'exec')
                  exec(code, config)  ## pylint: disable=exec-used
              except SyntaxError as e:
                  die('Syntax error in config file: %s\n%s'
                      'File %s, line %i'
                      % (str(e),
                         (indent(e.text.rstrip(), "  | ") + "\n")  if e.text else "",
                         e.filename, e.lineno))
          else:
              if fail_if_not_present:
                  die('%s config file is not found and is required.' % (fname, ))
  
      return config
  
  
  ##
  ## Text functions
  ##
  
  @available_in_config
  class TextProc(object):
  
      def __init__(self, fun):
          self.fun = fun
          if hasattr(fun, "__name__"):
              self.__name__ = fun.__name__
  
      def __call__(self, text):
          return self.fun(text)
  
      def __or__(self, value):
          if isinstance(value, TextProc):
              return TextProc(lambda text: value.fun(self.fun(text)))
          import inspect
          (_frame, filename, lineno, _function_name, lines, _index) = \
                  inspect.stack()[1]
          raise SyntaxError("Invalid syntax in config file",
              (filename, lineno, 0,
               "Invalid chain with a non TextProc element %r:\n%s"
               % (value, indent("".join(lines).strip(), "  | "))))
  
  
  def set_if_empty(text, msg="No commit message."):
      if len(text):
          return text
      return msg
  
  
  @TextProc
  def ucfirst(msg):
      if len(msg) == 0:
          return msg
      return msg[0].upper() + msg[1:]
  
  
  @TextProc
  def final_dot(msg):
      if len(msg) and msg[-1].isalnum():
          return msg + "."
      return msg
  
  
  def indent(text, chars="  ", first=None):
      """Return text string indented with the given chars
  
      >>> string = 'This is first line.\\nThis is second line\\n'
  
      >>> print(indent(string, chars="| "))  # doctest: +NORMALIZE_WHITESPACE
      | This is first line.
      | This is second line
      |
  
      >>> print(indent(string, first="- "))  # doctest: +NORMALIZE_WHITESPACE
      - This is first line.
        This is second line
  
  
      >>> string = 'This is first line.\\n\\nThis is second line'
      >>> print(indent(string, first="- "))  # doctest: +NORMALIZE_WHITESPACE
      - This is first line.
      <BLANKLINE>
        This is second line
  
      """
      if first:
          first_line = text.split("\n")[0]
          rest = '\n'.join(text.split("\n")[1:])
          return '\n'.join([(first + first_line).rstrip(),
                            indent(rest, chars=chars)])
      return '\n'.join([(chars + line).rstrip()
                        for line in text.split('\n')])
  
  
  def paragraph_wrap(text, regexp="\n\n"):
      r"""Wrap text by making sure that paragraph are separated correctly
  
          >>> string = 'This is first paragraph which is quite long don\'t you \
          ... think ? Well, I think so.\n\nThis is second paragraph\n'
  
          >>> print(paragraph_wrap(string)) # doctest: +NORMALIZE_WHITESPACE
          This is first paragraph which is quite long don't you think ? Well, I
          think so.
          This is second paragraph
  
      Notice that that each paragraph has been wrapped separately.
  
      """
      regexp = re.compile(regexp, re.MULTILINE)
      return "\n".join("\n".join(textwrap.wrap(paragraph.strip()))
                       for paragraph in regexp.split(text)).strip()
  
  
  def curryfy(f):
      return lambda *a, **kw: TextProc(lambda txt: f(txt, *a, **kw))
  
  ## these are curryfied version of their lower case definition
  
  Indent = curryfy(indent)
  Wrap = curryfy(paragraph_wrap)
  ReSub = lambda p, r, **k: TextProc(lambda txt: re.sub(p, r, txt, **k))
  noop = TextProc(lambda txt: txt)
  strip = TextProc(lambda txt: txt.strip())
  SetIfEmpty = curryfy(set_if_empty)
  
  for _label in ("Indent", "Wrap", "ReSub", "noop", "final_dot",
                "ucfirst", "strip", "SetIfEmpty"):
      _config_env[_label] = locals()[_label]
  
  ##
  ## File
  ##
  
  def file_get_contents(filename):
      with open(filename) as f:
          out = f.read()
      if not PY3:
          if not isinstance(out, unicode):
              out = out.decode(_preferred_encoding)
          ## remove encoding declaration (for some reason, python 2.7
          ## don't like it).
          out = re.sub(r"^(\s*#.*\s*)coding[:=]\s*([-\w.]+\s*;?\s*)",
                       r"\1", out, re.DOTALL)
  
      return out
  
  
  def file_put_contents(filename, string):
      """Write string to filename."""
      if PY3:
          fopen = open(filename, 'w', newline='')
      else:
          fopen = open(filename, 'wb')
  
      with fopen as f:
          f.write(string)
  
  
  ##
  ## Inferring revision
  ##
  
  def _file_regex_match(filename, pattern, **kw):
      if not os.path.isfile(filename):
          raise IOError("Can't open file '%s'." % filename)
      file_content = file_get_contents(filename)
      match = re.search(pattern, file_content, **kw)
      if match is None:
          stderr("file content: %r" % file_content)
          if isinstance(pattern, type(re.compile(''))):
              pattern = pattern.pattern
          raise ValueError(
              "Regex %s did not match any substring in '%s'."
              % (pattern, filename))
      return match
  
  
  @available_in_config
  def FileFirstRegexMatch(filename, pattern):
      def _call():
          match = _file_regex_match(filename, pattern)
          dct = match.groupdict()
          if dct:
              if "rev" not in dct:
                  warn("Named pattern used, but no one are named 'rev'. "
                       "Using full match.")
                  return match.group(0)
              if dct['rev'] is None:
                  die("Named pattern used, but it was not valued.")
              return dct['rev']
          return match.group(0)
      return _call
  
  
  @available_in_config
  def Caret(l):
      def _call():
          return "^%s" % eval_if_callable(l)
      return _call
  ##
  ## System functions
  ##
  
  ## Note that locale.getpreferredencoding() does NOT follow
  ## PYTHONIOENCODING by default, but ``sys.stdout.encoding`` does. In
  ## PY2, ``sys.stdout.encoding`` without PYTHONIOENCODING set does not
  ## get any values set in subshells.  However, if _preferred_encoding
  ## is not set to utf-8, it leads to encoding errors.
  _preferred_encoding = os.environ.get("PYTHONIOENCODING") or \
                        locale.getpreferredencoding()
  DEFAULT_GIT_LOG_ENCODING = 'utf-8'
  
  
  class Phile(object):
      """File like API to read fields separated by any delimiters
  
      It'll take care of file decoding to unicode.
  
      This is an adaptor on a file object.
  
          >>> if PY3:
          ...     from io import BytesIO
          ...     def File(s):
          ...         _obj = BytesIO()
          ...         _obj.write(s.encode(_preferred_encoding))
          ...         _obj.seek(0)
          ...         return _obj
          ... else:
          ...     from cStringIO import StringIO as File
  
          >>> f = Phile(File("a-b-c-d"))
  
      Read provides an iterator:
  
          >>> def show(l):
          ...     print(", ".join(l))
          >>> show(f.read(delimiter="-"))
          a, b, c, d
  
      You can change the buffersize loaded into memory before outputing
      your changes. It should not change the iterator output:
  
          >>> f = Phile(File("é-à-ü-d"), buffersize=3)
          >>> len(list(f.read(delimiter="-")))
          4
  
          >>> f = Phile(File("foo-bang-yummy"), buffersize=3)
          >>> show(f.read(delimiter="-"))
          foo, bang, yummy
  
          >>> f = Phile(File("foo-bang-yummy"), buffersize=1)
          >>> show(f.read(delimiter="-"))
          foo, bang, yummy
  
      """
  
      def __init__(self, filename, buffersize=4096, encoding=_preferred_encoding):
          self._file = filename
          self._buffersize = buffersize
          self._encoding = encoding
  
      def read(self, delimiter="\n"):
          buf = ""
          if PY3:
              delimiter = delimiter.encode(_preferred_encoding)
              buf = buf.encode(_preferred_encoding)
          while True:
              chunk = self._file.read(self._buffersize)
              if not chunk:
                  yield buf.decode(self._encoding)
                  return
              records = chunk.split(delimiter)
              records[0] = buf + records[0]
              for record in records[:-1]:
                  yield record.decode(self._encoding)
              buf = records[-1]
  
      def write(self, buf):
          if PY3:
              buf = buf.encode(self._encoding)
          return self._file.write(buf)
  
      def close(self):
          return self._file.close()
  
  
  class Proc(Popen):
  
      def __init__(self, command, env=None, encoding=_preferred_encoding):
          super(Proc, self).__init__(
              command, shell=True,
              stdin=PIPE, stdout=PIPE, stderr=PIPE,
              close_fds=PLT_CFG['close_fds'], env=env,
              universal_newlines=False)
  
          self.stdin = Phile(self.stdin, encoding=encoding)
          self.stdout = Phile(self.stdout, encoding=encoding)
          self.stderr = Phile(self.stderr, encoding=encoding)
  
  
  def cmd(command, env=None, shell=True):
  
      p = Popen(command, shell=shell,
                stdin=PIPE, stdout=PIPE, stderr=PIPE,
                close_fds=PLT_CFG['close_fds'], env=env,
                universal_newlines=False)
      out, err = p.communicate()
      return (
          out.decode(getattr(sys.stdout, "encoding", None) or
                        _preferred_encoding),
          err.decode(getattr(sys.stderr, "encoding", None) or
                        _preferred_encoding),
          p.returncode)
  
  
  @available_in_config
  def wrap(command, ignore_errlvls=[0], env=None, shell=True):
      """Wraps a shell command and casts an exception on unexpected errlvl
  
      >>> wrap('/tmp/lsdjflkjf') # doctest: +ELLIPSIS +IGNORE_EXCEPTION_DETAIL
      Traceback (most recent call last):
      ...
      ShellError: Wrapped command '/tmp/lsdjflkjf' exited with errorlevel 127.
        stderr:
        | /bin/sh: .../tmp/lsdjflkjf: not found
  
      >>> print(wrap('echo hello'),  end='')
      hello
  
      >>> print(wrap('echo hello && false'),
      ...       end='')  # doctest: +ELLIPSIS +IGNORE_EXCEPTION_DETAIL
      Traceback (most recent call last):
      ...
      ShellError: Wrapped command 'echo hello && false' exited with errorlevel 1.
        stdout:
        | hello
  
      """
  
      out, err, errlvl = cmd(command, env=env, shell=shell)
  
      if errlvl not in ignore_errlvls:
  
          formatted = []
          if out:
              if out.endswith('\n'):
                  out = out[:-1]
              formatted.append("stdout:\n%s" % indent(out, "| "))
          if err:
              if err.endswith('\n'):
                  err = err[:-1]
              formatted.append("stderr:\n%s" % indent(err, "| "))
          msg = '\n'.join(formatted)
  
          raise ShellError("Wrapped command %r exited with errorlevel %d.\n%s"
                           % (command, errlvl, indent(msg, chars="  ")),
                           errlvl=errlvl, command=command, out=out, err=err)
      return out
  
  
  @available_in_config
  def swrap(command, **kwargs):
      """Same as ``wrap(...)`` but strips the output."""
  
      return wrap(command, **kwargs).strip()
  
  
  ##
  ## git information access
  ##
  
  class SubGitObjectMixin(object):
  
      def __init__(self, repos):
          self._repos = repos
  
      @property
      def git(self):
          """Simple delegation to ``repos`` original method."""
          return self._repos.git
  
  
  GIT_FORMAT_KEYS = {
      'sha1': "%H",
      'sha1_short': "%h",
      'subject': "%s",
      'author_name': "%an",
      'author_email': "%ae",
      'author_date': "%ad",
      'author_date_timestamp': "%at",
      'committer_name': "%cn",
      'committer_date_timestamp': "%ct",
      'raw_body': "%B",
      'body': "%b",
  }
  
  GIT_FULL_FORMAT_STRING = "%x00".join(GIT_FORMAT_KEYS.values())
  
  REGEX_RFC822_KEY_VALUE = \
      r'(^|\n)(?P<key>[A-Z]\w+(-\w+)*): (?P<value>[^\n]*(\n\s+[^\n]*)*)'
  REGEX_RFC822_POSTFIX = \
      r'(%s)+$' % REGEX_RFC822_KEY_VALUE
  
  
  class GitCommit(SubGitObjectMixin):
      r"""Represent a Git Commit and expose through its attribute many information
  
      Let's create a fake GitRepos:
  
          >>> from minimock import Mock
          >>> repos = Mock("gitRepos")
  
      Initialization:
  
          >>> repos.git = Mock("gitRepos.git")
          >>> repos.git.log.mock_returns_func = \
          ...     lambda *a, **kwargs: "\x00".join([{
          ...             'sha1': "000000",
          ...             'sha1_short': "000",
          ...             'subject': SUBJECT,
          ...             'author_name': "John Smith",
          ...             'author_date': "Tue Feb 14 20:31:22 2017 +0700",
          ...             'author_email': "john.smith@example.com",
          ...             'author_date_timestamp': "0",   ## epoch
          ...             'committer_name': "Alice Wang",
          ...             'committer_date_timestamp': "0", ## epoch
          ...             'raw_body': "my subject\n\n%s" % BODY,
          ...             'body': BODY,
          ...         }[key] for key in GIT_FORMAT_KEYS.keys()])
          >>> repos.git.rev_list.mock_returns = "123456"
  
      Query, by attributes or items:
  
          >>> SUBJECT = "fee fie foh"
          >>> BODY = "foo foo foo"
  
          >>> head = GitCommit(repos, "HEAD")
          >>> head.subject
          Called gitRepos.git.log(...'HEAD'...)
          'fee fie foh'
          >>> head.author_name
          'John Smith'
  
      Notice that on the second call, there's no need to call again git log as
      all the values have already been computed.
  
      Trailer
      =======
  
      ``GitCommit`` offers a simple direct API to trailer values. These
      are like RFC822's header value but are at the end of body:
  
          >>> BODY = '''\
          ... Stuff in the body
          ... Change-id: 1234
          ... Value-X: Supports multi
          ...   line values'''
  
          >>> head = GitCommit(repos, "HEAD")
          >>> head.trailer_change_id
          Called gitRepos.git.log(...'HEAD'...)
          '1234'
          >>> head.trailer_value_x
          'Supports multi\nline values'
  
      Notice how the multi-line value was unindented.
      In case of multiple values, these are concatened in lists:
  
          >>> BODY = '''\
          ... Stuff in the body
          ... Co-Authored-By: Bob
          ... Co-Authored-By: Alice
          ... Co-Authored-By: Jack
          ... '''
  
          >>> head = GitCommit(repos, "HEAD")
          >>> head.trailer_co_authored_by
          Called gitRepos.git.log(...'HEAD'...)
          ['Bob', 'Alice', 'Jack']
  
  
      Special values
      ==============
  
      Authors
      -------
  
          >>> BODY = '''\
          ... Stuff in the body
          ... Co-Authored-By: Bob
          ... Co-Authored-By: Alice
          ... Co-Authored-By: Jack
          ... '''
  
          >>> head = GitCommit(repos, "HEAD")
          >>> head.author_names
          Called gitRepos.git.log(...'HEAD'...)
          ['Alice', 'Bob', 'Jack', 'John Smith']
  
      Notice that they are printed in alphabetical order.
  
      """
  
      def __init__(self, repos, identifier):
          super(GitCommit, self).__init__(repos)
          self.identifier = identifier
          self._trailer_parsed = False
  
      def __getattr__(self, label):
          """Completes commits attributes upon request."""
          attrs = GIT_FORMAT_KEYS.keys()
          if label not in attrs:
              try:
                  return self.__dict__[label]
              except KeyError:
                  if self._trailer_parsed:
                      raise AttributeError(label)
  
          identifier = self.identifier
  
          ## Compute only missing information
          missing_attrs = [l for l in attrs if l not in self.__dict__]
          ## some commit can be already fully specified (see ``mk_commit``)
          if missing_attrs:
              aformat = "%x00".join(GIT_FORMAT_KEYS[l]
                                    for l in missing_attrs)
              try:
                  ret = self.git.log([identifier, "--max-count=1",
                                     "--pretty=format:%s" % aformat, "--"])
              except ShellError:
                  if DEBUG:
                      raise
                  raise ValueError("Given commit identifier %r doesn't exists"
                                   % self.identifier)
              attr_values = ret.split("\x00")
              for attr, value in zip(missing_attrs, attr_values):
                  setattr(self, attr, value.strip())
  
          ## Let's interpret RFC822-like header keys that could be in the body
          match = re.search(REGEX_RFC822_POSTFIX, self.body)
          if match is not None:
              pos = match.start()
              postfix = self.body[pos:]
              self.body = self.body[:pos]
              for match in re.finditer(REGEX_RFC822_KEY_VALUE, postfix):
                  dct = match.groupdict()
                  key = dct["key"].replace("-", "_").lower()
                  if "\n" in dct["value"]:
                      first_line, remaining = dct["value"].split('\n', 1)
                      value = "%s\n%s" % (first_line,
                                          textwrap.dedent(remaining))
                  else:
                      value = dct["value"]
                  try:
                      prev_value = self.__dict__["trailer_%s" % key]
                  except KeyError:
                      setattr(self, "trailer_%s" % key, value)
                  else:
                      setattr(self, "trailer_%s" % key,
                              prev_value + [value, ]
                              if isinstance(prev_value, list)
                              else [prev_value, value, ])
          self._trailer_parsed = True
          return getattr(self, label)
  
      @property
      def author_names(self):
          return [re.sub(r'^([^<]+)<[^>]+>\s*$', r'\1', author).strip()
                  for author in self.authors]
  
      @property
      def authors(self):
          co_authors = getattr(self, 'trailer_co_authored_by', [])
          co_authors = co_authors if isinstance(co_authors, list) \
                       else [co_authors]
          return sorted(co_authors +
                        ["%s <%s>" % (self.author_name, self.author_email)])
  
      @property
      def date(self):
          d = datetime.datetime.utcfromtimestamp(
              float(self.author_date_timestamp))
          return d.strftime('%Y-%m-%d')
  
      @property
      def has_annotated_tag(self):
          try:
              self.git.rev_parse(['%s^{tag}' % self.identifier, "--"])
              return True
          except ShellError as e:
              if e.errlvl != 128:
                  raise
              return False
  
      @property
      def tagger_date_timestamp(self):
          if not self.has_annotated_tag:
              raise ValueError("Can't access 'tagger_date_timestamp' on commit without annotated tag.")
          tagger_date_utc = self.git.for_each_ref(
              'refs/tags/%s' % self.identifier, format='%(taggerdate:raw)')
          return tagger_date_utc.split(" ", 1)[0]
  
      @property
      def tagger_date(self):
          d = datetime.datetime.utcfromtimestamp(
              float(self.tagger_date_timestamp))
          return d.strftime('%Y-%m-%d')
  
      def __le__(self, value):
          if not isinstance(value, GitCommit):
              value = self._repos.commit(value)
          try:
              self.git.merge_base(value.sha1, is_ancestor=self.sha1)
              return True
          except ShellError as e:
              if e.errlvl != 1:
                  raise
              return False
  
      def __lt__(self, value):
          if not isinstance(value, GitCommit):
              value = self._repos.commit(value)
          return self <= value and self != value
  
      def __eq__(self, value):
          if not isinstance(value, GitCommit):
              value = self._repos.commit(value)
          return self.sha1 == value.sha1
  
      def __hash__(self):
          return hash(self.sha1)
  
      def __repr__(self):
          return "<%s %r>" % (self.__class__.__name__, self.identifier)
  
  
  def normpath(path, cwd=None):
      """path can be absolute or relative, if relative it uses the cwd given as
      param.
  
      """
      if os.path.isabs(path):
          return path
      cwd = cwd if cwd else os.getcwd()
      return os.path.normpath(os.path.join(cwd, path))
  
  
  class GitConfig(SubGitObjectMixin):
      """Interface to config values of git
  
      Let's create a fake GitRepos:
  
          >>> from minimock import Mock
          >>> repos = Mock("gitRepos")
  
      Initialization:
  
          >>> cfg = GitConfig(repos)
  
      Query, by attributes or items:
  
          >>> repos.git.config.mock_returns = "bar"
          >>> cfg.foo
          Called gitRepos.git.config('foo')
          'bar'
          >>> cfg["foo"]
          Called gitRepos.git.config('foo')
          'bar'
          >>> cfg.get("foo")
          Called gitRepos.git.config('foo')
          'bar'
          >>> cfg["foo.wiz"]
          Called gitRepos.git.config('foo.wiz')
          'bar'
  
      Notice that you can't use attribute search in subsection as ``cfg.foo.wiz``
      That's because in git config files, you can have a value attached to
      an element, and this element can also be a section.
  
      Nevertheless, you can do:
  
          >>> getattr(cfg, "foo.wiz")
          Called gitRepos.git.config('foo.wiz')
          'bar'
  
      Default values
      --------------
  
      get item, and getattr default values can be used:
  
          >>> del repos.git.config.mock_returns
          >>> repos.git.config.mock_raises = ShellError('Key not found',
          ...                                           errlvl=1, out="", err="")
  
          >>> getattr(cfg, "foo", "default")
          Called gitRepos.git.config('foo')
          'default'
  
          >>> cfg["foo"]  ## doctest: +ELLIPSIS
          Traceback (most recent call last):
          ...
          KeyError: 'foo'
  
          >>> getattr(cfg, "foo")  ## doctest: +ELLIPSIS
          Traceback (most recent call last):
          ...
          AttributeError...
  
          >>> cfg.get("foo", "default")
          Called gitRepos.git.config('foo')
          'default'
  
          >>> print("%r" % cfg.get("foo"))
          Called gitRepos.git.config('foo')
          None
  
      """
  
      def __init__(self, repos):
          super(GitConfig, self).__init__(repos)
  
      def __getattr__(self, label):
          try:
              res = self.git.config(label)
          except ShellError as e:
              if e.errlvl == 1 and e.out == "":
                  raise AttributeError("key %r is not found in git config."
                                       % label)
              raise
          return res
  
      def get(self, label, default=None):
          return getattr(self, label, default)
  
      def __getitem__(self, label):
          try:
              return getattr(self, label)
          except AttributeError:
              raise KeyError(label)
  
  
  class GitCmd(SubGitObjectMixin):
  
      def __getattr__(self, label):
          label = label.replace("_", "-")
  
          def dir_swrap(command, **kwargs):
              with set_cwd(self._repos._orig_path):
                  return swrap(command, **kwargs)
  
          def method(*args, **kwargs):
              if (len(args) == 1 and not isinstance(args[0], basestring)):
                  return dir_swrap(
                      ['git', label, ] + args[0],
                      shell=False,
                      env=kwargs.get("env", None))
              cli_args = []
              for key, value in kwargs.items():
                  cli_key = (("-%s" if len(key) == 1 else "--%s")
                             % key.replace("_", "-"))
                  if isinstance(value, bool):
                      cli_args.append(cli_key)
                  else:
                      cli_args.append(cli_key)
                      cli_args.append(value)
  
              cli_args.extend(args)
  
              return dir_swrap(['git', label, ] + cli_args, shell=False)
          return method
  
  
  class GitRepos(object):
  
      def __init__(self, path):
  
          ## Saving this original path to ensure all future git commands
          ## will be done from this location.
          self._orig_path = os.path.abspath(path)
  
          ## verify ``git`` command is accessible:
          try:
              self._git_version = self.git.version()
          except ShellError:
              if DEBUG:
                  raise
              raise EnvironmentError(
                  "Required ``git`` command not found or broken in $PATH. "
                  "(calling ``git version`` failed.)")
  
          ## verify that we are in a git repository
          try:
              self.git.remote()
          except ShellError:
              if DEBUG:
                  raise
              raise EnvironmentError(
                  "Not in a git repository. (calling ``git remote`` failed.)")
  
          self.bare = self.git.rev_parse(is_bare_repository=True) == "true"
          self.toplevel = (None if self.bare else
                           self.git.rev_parse(show_toplevel=True))
          self.gitdir = normpath(self.git.rev_parse(git_dir=True),
                                 cwd=self._orig_path)
  
      @classmethod
      def create(cls, directory, *args, **kwargs):
          os.mkdir(directory)
          return cls.init(directory, *args, **kwargs)
  
      @classmethod
      def init(cls, directory, user=None, email=None):
          with set_cwd(directory):
              wrap("git init .")
          self = cls(directory)
          if user:
              self.git.config("user.name", user)
          if email:
              self.git.config("user.email", email)
          return self
  
      def commit(self, identifier):
          return GitCommit(self, identifier)
  
      @property
      def git(self):
          return GitCmd(self)
  
      @property
      def config(self):
          return GitConfig(self)
  
      def tags(self, contains=None):
          """String list of repository's tag names
  
          Current tag order is committer date timestamp of tagged commit.
          No firm reason for that, and it could change in future version.
  
          """
          if contains:
              tags = self.git.tag(contains=contains).split("\n")
          else:
              tags = self.git.tag().split("\n")
          ## Should we use new version name sorting ?  refering to :
          ## ``git tags --sort -v:refname`` in git version >2.0.
          ## Sorting and reversing with command line is not available on
          ## git version <2.0
          return sorted([self.commit(tag) for tag in tags if tag != ''],
                        key=lambda x: int(x.committer_date_timestamp))
  
      def log(self, includes=["HEAD", ], excludes=[], include_merge=True,
              encoding=_preferred_encoding):
          """Reverse chronological list of git repository's commits
  
          Note: rev lists can be GitCommit instance list or identifier list.
  
          """
  
          refs = {'includes': includes,
                  'excludes': excludes}
          for ref_type in ('includes', 'excludes'):
              for idx, ref in enumerate(refs[ref_type]):
                  if not isinstance(ref, GitCommit):
                      refs[ref_type][idx] = self.commit(ref)
  
          ## --topo-order: don't mix commits from separate branches.
          plog = Proc("git log --stdin -z --topo-order --pretty=format:%s %s --"
                      % (GIT_FULL_FORMAT_STRING,
                         '--no-merges' if not include_merge else ''),
                      encoding=encoding)
          for ref in refs["includes"]:
              plog.stdin.write("%s\n" % ref.sha1)
  
          for ref in refs["excludes"]:
              plog.stdin.write("^%s\n" % ref.sha1)
          plog.stdin.close()
  
          def mk_commit(dct):
              """Creates an already set commit from a dct"""
              c = self.commit(dct["sha1"])
              for k, v in dct.items():
                  setattr(c, k, v)
              return c
  
          values = plog.stdout.read("\x00")
  
          try:
              while True:  ## next(values) will eventualy raise a StopIteration
                  yield mk_commit(dict([(key, next(values))
                                        for key in GIT_FORMAT_KEYS]))
          except StopIteration:
              pass  ## since 3.7, we are not allowed anymore to trickle down
                    ## StopIteration.
          finally:
              plog.stdout.close()
              plog.stderr.close()
  
  
  def first_matching(section_regexps, string):
      for section, regexps in section_regexps:
          if regexps is None:
              return section
          for regexp in regexps:
              if re.search(regexp, string) is not None:
                  return section
  
  
  def ensure_template_file_exists(label, template_name):
      """Return template file path given a label hint and the template name
  
      Template name can be either a filename with full path,
      if this is the case, the label is of no use.
  
      If ``template_name`` does not refer to an existing file,
      then ``label`` is used to find a template file in the
      the bundled ones.
  
      """
  
      try:
          template_path = GitRepos(os.getcwd()).config.get(
              "gitchangelog.template-path")
      except ShellError as e:
          stderr(
              "Error parsing git config: %s."
              " Won't be able to read 'template-path' if defined."
              % (str(e)))
          template_path = None
  
      if template_path:
          path_file = path_label = template_path
      else:
          path_file = os.getcwd()
          path_label = os.path.join(os.path.dirname(os.path.realpath(__file__)),
                                    "templates", label)
  
      for ftn in [os.path.join(path_file, template_name),
                  os.path.join(path_label, "%s.tpl" % template_name)]:
          if os.path.isfile(ftn):
              return ftn
  
      templates = glob.glob(os.path.join(path_label, "*.tpl"))
      if len(templates) > 0:
          msg = ("These are the available %s templates:" % label)
          msg += "\n - " + \
                 "\n - ".join(os.path.basename(f).split(".")[0]
                              for f in templates)
          msg += "\nTemplates are located in %r" % path_label
      else:
          msg = "No available %s templates found in %r." \
                % (label, path_label)
      die("Error: Invalid %s template name %r.\n" % (label, template_name) +
          "%s" % msg)
  
  
  ##
  ## Output Engines
  ##
  
  @available_in_config
  def rest_py(data, opts={}):
      """Returns ReStructured Text changelog content from data"""
  
      def rest_title(label, char="="):
          return (label.strip() + "\n") + (char * len(label) + "\n")
  
      def render_version(version):
          title = "%s (%s)" % (version["tag"], version["date"]) \
                  if version["tag"] else \
                  opts["unreleased_version_label"]
          s = rest_title(title, char="-")
  
          sections = version["sections"]
          nb_sections = len(sections)
          for section in sections:
  
              section_label = section["label"] if section.get("label", None) \
                              else "Other"
  
              if not (section_label == "Other" and nb_sections == 1):
                  s += "\n" + rest_title(section_label, "~")
  
              for commit in section["commits"]:
                  s += render_commit(commit)
          return s
  
      def render_commit(commit, opts=opts):
          subject = commit["subject"]
          subject += " [%s]" % (", ".join(commit["authors"]), )
  
          entry = indent('\n'.join(textwrap.wrap(subject)),
                         first="- ").strip() + "\n"
  
          if commit["body"]:
              entry += "\n" + indent(commit["body"])
              entry += "\n"
  
          return entry
  
      if data["title"]:
          yield rest_title(data["title"], char="=") + "\n\n"
  
      for version in data["versions"]:
          if len(version["sections"]) > 0:
              yield render_version(version) + "\n\n"
  
  
  ## formatter engines
  
  if pystache:
  
      @available_in_config
      def mustache(template_name):
          """Return a callable that will render a changelog data structure
  
          returned callable must take 2 arguments ``data`` and ``opts``.
  
          """
          template_path = ensure_template_file_exists("mustache", template_name)
  
          template = file_get_contents(template_path)
  
          def stuffed_versions(versions, opts):
              for version in versions:
                  title = "%s (%s)" % (version["tag"], version["date"]) \
                          if version["tag"] else \
                          opts["unreleased_version_label"]
                  version["label"] = title
                  version["label_chars"] = list(version["label"])
                  for section in version["sections"]:
                      section["label_chars"] = list(section["label"])
                      section["display_label"] = \
                          not (section["label"] == "Other" and
                               len(version["sections"]) == 1)
                      for commit in section["commits"]:
                          commit["author_names_joined"] = ", ".join(
                              commit["authors"])
                          commit["body_indented"] = indent(commit["body"])
                  yield version
  
          def renderer(data, opts):
  
              ## mustache is very simple so we need to add some intermediate
              ## values
              data["general_title"] = True if data["title"] else False
              data["title_chars"] = list(data["title"]) if data["title"] else []
  
              data["versions"] = stuffed_versions(data["versions"], opts)
  
              return pystache.render(template, data)
  
          return renderer
  
  else:
  
      @available_in_config
      def mustache(template_name):  ## pylint: disable=unused-argument
          die("Required 'pystache' python module not found.")
  
  
  if mako:
  
      import mako.template ## pylint: disable=wrong-import-position
  
      mako_env = dict((f.__name__, f) for f in (ucfirst, indent, textwrap,
                                                paragraph_wrap))
  
      @available_in_config
      def makotemplate(template_name):
          """Return a callable that will render a changelog data structure
  
          returned callable must take 2 arguments ``data`` and ``opts``.
  
          """
          template_path = ensure_template_file_exists("mako", template_name)
  
          template = mako.template.Template(filename=template_path)
  
          def renderer(data, opts):
              kwargs = mako_env.copy()
              kwargs.update({"data": data,
                             "opts": opts})
              return template.render(**kwargs)
  
          return renderer
  
  else:
  
      @available_in_config
      def makotemplate(template_name):  ## pylint: disable=unused-argument
          die("Required 'mako' python module not found.")
  
  
  ##
  ## Publish action
  ##
  
  @available_in_config
  def stdout(content):
      for chunk in content:
          safe_print(chunk)
  @available_in_config
  def FileInsertAtFirstRegexMatch(filename, pattern, flags=0,
                                  idx=lambda m: m.start()):
  
      def write_content(f, content):
          for content_line in content:
              f.write(content_line)
  
      def _wrapped(content):
          index = idx(_file_regex_match(filename, pattern, flags=flags))
          offset = 0
          new_offset = 0
          postfix = False
  
          with open(filename + "~", "w") as dst:
              with open(filename, "r") as src:
                  for line in src:
                      if postfix:
                          dst.write(line)
                          continue
                      new_offset = offset + len(line)
                      if new_offset < index:
                          offset = new_offset
                          dst.write(line)
                          continue
                      dst.write(line[0:index - offset])
                      write_content(dst, content)
                      dst.write(line[index - offset:])
                      postfix = True
              if not postfix:
                  write_content(dst, content)
          if WIN32:
              os.remove(filename)
          os.rename(filename + "~", filename)
  
      return _wrapped
  
  
  @available_in_config
  def FileRegexSubst(filename, pattern, replace, flags=0):
  
      replace = re.sub(r'\\([0-9+])', r'\\g<\1>', replace)
  
      def _wrapped(content):
          src = file_get_contents(filename)
          ## Protect replacement pattern against the following expansion of '\o'
          src = re.sub(
                  pattern,
                  replace.replace(r'\o', "".join(content).replace('\\', '\\\\')),
                  src, flags=flags)
          if not PY3:
              src = src.encode(_preferred_encoding)
          file_put_contents(filename, src)
  
      return _wrapped
  
  
  ##
  ## Data Structure
  ##
  
  def versions_data_iter(repository, revlist=None,
                         ignore_regexps=[],
                         section_regexps=[(None, '')],
                         tag_filter_regexp=r"\d+\.\d+(\.\d+)?",
                         include_merge=True,
                         body_process=lambda x: x,
                         subject_process=lambda x: x,
                         log_encoding=DEFAULT_GIT_LOG_ENCODING,
                         warn=warn,        ## Mostly used for test
                         ):
      """Returns an iterator through versions data structures
  
      (see ``gitchangelog.rc.reference`` file for more info)
  
      :param repository: target ``GitRepos`` object
      :param revlist: list of strings that git log understands as revlist
      :param ignore_regexps: list of regexp identifying ignored commit messages
      :param section_regexps: regexps identifying sections
      :param tag_filter_regexp: regexp to match tags used as version
      :param include_merge: whether to include merge commits in the log or not
      :param body_process: text processing object to apply to body
      :param subject_process: text processing object to apply to subject
      :param log_encoding: the encoding used in git logs
      :param warn: callable to output warnings, mocked by tests
  
      :returns: iterator of versions data_structures
  
      """
  
      revlist = revlist or []
  
      ## Hash to speedup lookups
      versions_done = {}
      excludes = [rev[1:]
                  for rev in repository.git.rev_parse([
                      "--rev-only", ] + revlist + ["--", ]).split("\n")
                  if rev.startswith("^")] if revlist else []
  
      revs = repository.git.rev_list(*revlist).split("\n") if revlist else []
      revs = [rev for rev in revs if rev != ""]
  
      if revlist and not revs:
          die("No commits matching given revlist: %s" % (" ".join(revlist), ))
  
      tags = [tag
              for tag in repository.tags(contains=revs[-1] if revs else None)
              if re.match(tag_filter_regexp, tag.identifier)]
  
      tags.append(repository.commit("HEAD"))
  
      if revlist:
          max_rev = repository.commit(revs[0])
          new_tags = []
          for tag in tags:
              new_tags.append(tag)
              if max_rev <= tag:
                  break
          tags = new_tags
      else:
          max_rev = tags[-1]
  
      section_order = [k for k, _v in section_regexps]
  
      tags = list(reversed(tags))
  
      ## Get the changes between tags (releases)
      for idx, tag in enumerate(tags):
  
          ## New version
          current_version = {
              "date": tag.tagger_date if tag.has_annotated_tag else tag.date,
              "commit_date": tag.date,
              "tagger_date": tag.tagger_date if tag.has_annotated_tag else None,
              "tag": tag.identifier if tag.identifier != "HEAD" else None,
              "commit": tag,
          }
  
          sections = collections.defaultdict(list)
          commits = repository.log(
              includes=[min(tag, max_rev)],
              excludes=tags[idx + 1:] + excludes,
              include_merge=include_merge,
              encoding=log_encoding)
  
          for commit in commits:
              if any(re.search(pattern, commit.subject) is not None
                     for pattern in ignore_regexps):
                  continue
  
              matched_section = first_matching(section_regexps, commit.subject)
  
              ## Finally storing the commit in the matching section
  
              sections[matched_section].append({
                  "author": commit.author_name,
                  "authors": commit.author_names,
                  "subject": subject_process(commit.subject),
                  "body": body_process(commit.body),
                  "commit": commit,
              })
  
          ## Flush current version
          current_version["sections"] = [{"label": k, "commits": sections[k]}
                                         for k in section_order
                                         if k in sections]
          if len(current_version["sections"]) != 0:
              yield current_version
          versions_done[tag] = current_version
  
  
  def changelog(output_engine=None,
                unreleased_version_label="unreleased",
                warn=warn,        ## Mostly used for test
                **kwargs):
      """Returns a string containing the changelog of given repository
  
      This function returns a string corresponding to the template rendered with
      the changelog data tree.
  
      (see ``gitchangelog.rc.sample`` file for more info)
  
      For an exact list of arguments, see the arguments of
      ``versions_data_iter(..)``.
  
      :param unreleased_version_label: version label for untagged commits
      :param output_engine: callable to render the changelog data
      :param warn: callable to output warnings, mocked by tests
  
      :returns: content of changelog
  
      """
  
      opts = {
          'unreleased_version_label': unreleased_version_label,
      }
  
      ## Setting main container of changelog elements
      title = None if kwargs.get("revlist") else "Changelog"
      data = {"title": title,
              "versions": []}
  
      versions = versions_data_iter(warn=warn, **kwargs)
  
      ## poke once in versions to know if there's at least one:
      try:
          first_version = next(versions)
      except StopIteration:
          warn("Empty changelog. No commits were elected to be used as entry.")
          data["versions"] = []
      else:
          data["versions"] = itertools.chain([first_version], versions)
  
      return output_engine(data=data, opts=opts)
  
  ##
  ## Manage obsolete options
  ##
  
  _obsolete_options_managers = []
  
  
  def obsolete_option_manager(fun):
      _obsolete_options_managers.append(fun)
  
  
  @obsolete_option_manager
  def obsolete_replace_regexps(config):
      """This option was superseeded by the ``subject_process`` option.
  
      Each regex replacement you had could be translated in a
      ``ReSub(pattern, replace)`` in the ``subject_process`` pipeline.
  
      """
      if "replace_regexps" in config:
          for pattern, replace in config["replace_regexps"].items():
              config["subject_process"] = \
                  ReSub(pattern, replace) | \
                  config.get("subject_process", ucfirst | final_dot)
  
  
  @obsolete_option_manager
  def obsolete_body_split_regexp(config):
      """This option was superseeded by the ``body_process`` option.
  
      The split regex can now be sent as a ``Wrap(regex)`` text process
      instruction in the ``body_process`` pipeline.
  
      """
      if "body_split_regex" in config:
          config["body_process"] = Wrap(config["body_split_regex"]) | \
                                   config.get("body_process", noop)
  
  
  def manage_obsolete_options(config):
      for man in _obsolete_options_managers:
          man(config)
  
  
  ##
  ## Command line parsing
  ##
  
  def parse_cmd_line(usage, description, epilog, exname, version):
  
      import argparse
      kwargs = dict(usage=usage,
                    description=description,
                    epilog="\n" + epilog,
                    prog=exname,
                    formatter_class=argparse.RawTextHelpFormatter)
  
      try:
          parser = argparse.ArgumentParser(version=version, **kwargs)
      except TypeError:  ## compat with argparse from python 3.4
          parser = argparse.ArgumentParser(**kwargs)
          parser.add_argument('-v', '--version',
                              help="show program's version number and exit",
                              action="version", version=version)
  
      parser.add_argument('-d', '--debug',
                          help="Enable debug mode (show full tracebacks).",
                          action="store_true", dest="debug")
      parser.add_argument('revlist', nargs='*', action="store", default=[])
  
      ## Remove "show" as first argument for compatibility reason.
  
      argv = []
      for i, arg in enumerate(sys.argv[1:]):
          if arg.startswith("-"):
              argv.append(arg)
              continue
          if arg == "show":
              warn("'show' positional argument is deprecated.")
              argv += sys.argv[i + 2:]
              break
          else:
              argv += sys.argv[i + 1:]
              break
  
      return parser.parse_args(argv)
  
  
  eval_if_callable = lambda v: v() if callable(v) else v
  
  
  def get_revision(repository, config, opts):
      if opts.revlist:
          revs = opts.revlist
      else:
          revs = config.get("revs")
          if revs:
              revs = eval_if_callable(revs)
              if not isinstance(revs, list):
                  die("Invalid type for 'revs' in config file. "
                      "A 'list' type is required, and a %r was given."
                      % type(revs).__name__)
              revs = [eval_if_callable(rev)
                      for rev in revs]
          else:
              tags = repository.tags()
              # Find the last full release
              rel_tags = [t for t in tags if '-rc' not in t.identifier]
              if len(rel_tags) == 0:
                  revs = []
              else:
                  last_rel = rel_tags[-1].identifier
                  last_rel_filter = f'^{last_rel}'
                  rc_tags = [t.identifier.split('-')[0] for t in tags if '-rc' in t.identifier]
                  rc_tags = [t for t in rc_tags if t > last_rel]
                  if len(rc_tags) > 0:
                      current_rc = rc_tags[-1]
                      revs = [last_rel_filter, 'HEAD']
                      config['unreleased_version_label'] = current_rc
                  else:
                      #compute the next revision
                      last_rel_segmented = last_rel_filter.replace('^', '').split('.')
                      #easy case is new month
                      if ((int(last_rel_segmented[0]) < datetime.date.today().year) or
                          (int(last_rel_segmented[0]) == datetime.date.today().year and int(last_rel_segmented[1]) < datetime.date.today().month)):
                          current_rc = f'{datetime.date.today().year}.{datetime.date.today().month}.1'
                          revs = [last_rel_filter, 'HEAD']
                          config['unreleased_version_label'] = current_rc
                      else:
                          current_rc = f'{last_rel_segmented[0]}.{last_rel_segmented[1]}.{int(last_rel_segmented[2]) + 1}'
                          revs = [last_rel_filter, 'HEAD']
                          config['unreleased_version_label'] = current_rc
  
  
      for rev in revs:
          if not isinstance(rev, basestring):
              die("Invalid type for revision in revs list from config file. "
                  "'str' type is required, and a %r was given."
                  % type(rev).__name__)
          try:
              repository.git.rev_parse([rev, "--rev_only", "--"])
          except ShellError:
              if DEBUG:
                  raise
              die("Revision %r is not valid." % rev)
  
      if revs == ["HEAD", ]:
          return []
      return revs
  
  
  def get_log_encoding(repository, config):
  
      log_encoding = config.get("log_encoding", None)
      if log_encoding is None:
          try:
              log_encoding = repository.config.get("i18n.logOuputEncoding")
          except ShellError as e:
              warn(
                  "Error parsing git config: %s."
                  " Couldn't check if 'i18n.logOuputEncoding' was set."
                  % (str(e)))
  
      ## Final defaults coming from git defaults
      return log_encoding or DEFAULT_GIT_LOG_ENCODING
  
  
  ##
  ## Config Manager
  ##
  
  class Config(dict):
  
      def __getitem__(self, label):
          if label not in self.keys():
              die("Missing value in config file for key '%s'." % label)
          return super(Config, self).__getitem__(label)
  
  
  ##
  ## Safe print
  ##
  
  def safe_print(content):
      if not PY3:
          if isinstance(content, unicode):
              content = content.encode(_preferred_encoding)
  
      try:
          print(content, end='')
          sys.stdout.flush()
      except UnicodeEncodeError:
          if DEBUG:
              raise
          ## XXXvlab: should use $COLUMNS in bash and for windows:
          ## http://stackoverflow.com/questions/14978548
          stderr(paragraph_wrap(textwrap.dedent("""\
              UnicodeEncodeError:
                There was a problem outputing the resulting changelog to
                your console.
  
                This probably means that the changelog contains characters
                that can't be translated to characters in your current charset
                (%s).
              """) % sys.stdout.encoding))
          if WIN32 and PY_VERSION < 3.6 and sys.stdout.encoding != 'utf-8':
              ## As of PY 3.6, encoding is now ``utf-8`` regardless of
              ## PYTHONIOENCODING
              ## https://www.python.org/dev/peps/pep-0528/
              stderr("  You might want to try to fix that by setting "
                     "PYTHONIOENCODING to 'utf-8'.")
          exit(1)
      except IOError as e:
          if e.errno == 0 and not PY3 and WIN32:
              ## Yes, had a strange IOError Errno 0 after outputing string
              ## that contained UTF-8 chars on Windows and PY2.7
              pass  ## Ignoring exception
          elif ((WIN32 and e.errno == 22) or              ## Invalid argument
                (not WIN32 and e.errno == errno.EPIPE)):  ## Broken Pipe
              ## Nobody is listening anymore to stdout it seems. Let's bailout.
              if PY3:
                  try:
                      ## Called only to generate exception and have a chance at
                      ## ignoring it. Otherwise this happens upon exit, and gets
                      ## some error message printed on stderr.
                      sys.stdout.close()
                  except BrokenPipeError:  ## expected outcome on linux
                      pass
                  except OSError as e2:
                      if e2.errno != 22:   ## expected outcome on WIN32
                          raise
              ## Yay ! stdout is closed we can now exit safely.
              exit(0)
          else:
              raise
  
  
  ##
  ## Main
  ##
  
  def fill_in_interfaces_defaults(config):
      config.setdefault('ignore_regexps', [
          r'@wip', r'!wip',
          r'^([Mm]eta:)',
          r'^(.{3,3}\s*:)?\s*[I]nitial commit.?\s*$',
          r'^$',  ## ignore commits with empty messages
      ])
      config.setdefault('section_regexps', [
          ('Features', [
              r'^([jJ]ira|[qQ][aA]):([^\n]*)$',
           ]),
          ('Technical', [
              r'^[dD]ev:([^\n]*)$',
           ]),
          ('Operations', [
              r'^[oO]ps:([^\n]*)$',
           ]),
          ('Other', None ## Match all lines
           ),
      ])
      config.setdefault('body_process', ReSub(r'((^|\n)[A-Z]\w+(-\w+)*: .*(\n\s+.*)*)+$', r'') | strip)
      config.setdefault('subject_process',  (strip |
          ReSub(r'^([Dd]ev|[Qq][Aa]|[Jj]ira|[Oo]ps):([^\n@]*)(@[a-z]+\s+)*$', r'\2') |
          SetIfEmpty("No commit message.") | ucfirst | final_dot))
      config.setdefault('tag_filter_regexp', r'^[0-9]+\.[0-9]+\.[0-9]+$')
      config.setdefault('unreleased_version_label', 'unreleased')
      config.setdefault('output_engine',  makotemplate("InterfacesMarkdown"))
      config.setdefault('include_merge', False)
      return config
  
  
  def main():
  
      global DEBUG
      ## Basic environment infos
  
      reference_config = os.path.join(
          os.path.dirname(os.path.realpath(__file__)),
          "gitchangelog.rc.reference")
  
      basename = os.path.basename(sys.argv[0])
      if basename.endswith(".py"):
          basename = basename[:-3]
  
      debug_varname = "DEBUG_%s" % basename.upper()
      DEBUG = os.environ.get(debug_varname, False)
  
      i = lambda x: x % {'exname': basename}
  
      opts = parse_cmd_line(usage=i(usage_msg),
                            description=i(description_msg),
                            epilog=i(epilog_msg),
                            exname=basename,
                            version=__version__)
      DEBUG = DEBUG or opts.debug
  
      try:
          repository = GitRepos(".")
      except EnvironmentError as e:
          if DEBUG:
              raise
          try:
              die(str(e))
          except Exception as e2:
              die(repr(e2))
  
      try:
          gc_rc = repository.config.get("gitchangelog.rc-path")
      except ShellError as e:
          stderr(
              "Error parsing git config: %s."
              " Won't be able to read 'rc-path' if defined."
              % (str(e)))
          gc_rc = None
  
      gc_rc = normpath(gc_rc, cwd=repository.toplevel) if gc_rc else None
  
      ## config file lookup resolution
      for enforce_file_existence, fun in [
          (True, lambda: os.environ.get('GITCHANGELOG_CONFIG_FILENAME')),
          (True, lambda: gc_rc),
          (False,
               lambda: (os.path.join(repository.toplevel, ".%s.rc" % basename))
                        if not repository.bare else None)]:
          changelogrc = fun()
          if changelogrc:
              if not os.path.exists(changelogrc):
                  if enforce_file_existence:
                      die("File %r does not exists." % changelogrc)
                  else:
                      continue  ## changelogrc valued, but file does not exists
              else:
                  break
  
      ## config file may lookup for templates relative to the toplevel
      ## of git repository
      os.chdir(repository.toplevel)
  
      config = load_config_file(
          os.path.expanduser(changelogrc),
          default_filename=None, ## changing this to use our custom fallbacks
          fail_if_not_present=False)
  
      config = Config(config)
  
      ## Stuff for interfaces.  Done in code to make it more portable than loading a file/env vars.
      config = fill_in_interfaces_defaults(config)
  
  
      log_encoding = get_log_encoding(repository, config)
      revlist = get_revision(repository, config, opts)
      config['unreleased_version_label'] = eval_if_callable(
          config['unreleased_version_label'])
      manage_obsolete_options(config)
  
      try:
          content = changelog(
              repository=repository, revlist=revlist,
              ignore_regexps=config['ignore_regexps'],
              section_regexps=config['section_regexps'],
              unreleased_version_label=config['unreleased_version_label'],
              tag_filter_regexp=config['tag_filter_regexp'],
              output_engine=config.get("output_engine", None),
              include_merge=config.get("include_merge", True),
              body_process=config.get("body_process", noop),
              subject_process=config.get("subject_process", noop),
              log_encoding=log_encoding,
          )
  
          if isinstance(content, basestring):
              content = content.splitlines(True)
  
          config.get("publish", stdout)(content)
  
      except KeyboardInterrupt:
          if DEBUG:
              err("Keyboard interrupt received while running '%s':"
                  % (basename, ))
              stderr(format_last_exception())
          else:
              err("Keyboard Interrupt. Bailing out.")
          exit(130)  ## Actual SIGINT as bash process convention.
      except Exception as e:  ## pylint: disable=broad-except
          if DEBUG:
              err("Exception while running '%s':"
                  % (basename, ))
              stderr(format_last_exception())
          else:
              message = "%s" % e
              err(message)
              stderr("  (set %s environment variable, "
                     "or use ``--debug`` to see full traceback)" %
                     (debug_varname, ))
          exit(255)
  
  
  ##
  ## Launch program
  ##
  
  if __name__ == "__main__":
      main()
  