> import argparse
> import copy
> import json
> import os
> import re
> import sys
> from shutil import copyfile
> from pprint import pprint
> from typing import Union, Dict, AnyStr, Type, Any
> import git
> from git.exc import GitCommandError
  
> from .labels import (
>     get_all_branch_types,
>     get_branch_types,
>     get_ignore_commits_exact_words,
>     get_ignore_commits_prefixes,
>     get_other_branch_type,
>     get_other_branch_type_key,
>     get_unreleased,
>     get_unreleased_key,
>     get_unreleased_key_label,
> )
> from .helpers import project_dir
> from .patterns import (
>     REGEX_PATTERN_BRANCH_NAME,
>     REGEX_PATTERN_COMMIT,
>     REGEX_PATTERN_COMMIT_LINE,
>     REGEX_PATTERN_MERGED_BRANCH_NAME,
>     REGEX_PATTERN_TAG,
> )
  
> __author__ = 'Artur Barseghyan'
> __copyright__ = '2019 Artur Barseghyan'
> __license__ = 'GPL-2.0-only OR LGPL-2.0-or-later'
> __all__ = (
>     'generate_changelog',
>     'generate_changelog_cli',
>     'get_branch_type',
>     'get_logs',
>     'json_changelog',
>     'json_changelog_cli',
>     'make_config_file',
>     'make_config_file_cli',
>     'prepare_changelog',
>     'prepare_releases_changelog',
>     'validate_between',
> )
  
> PRETTY_FORMAT = '{' \
>                 '"commit_hash": "%H", ' \
>                 '"commit_abbr": "%h", ' \
>                 '"datetime": "%ci", ' \
>                 '"title": "%s",' \
>                 '"author": "%an", ' \
>                 '"author_email": "%ae", ' \
>                 '"merge": "%P"' \
>                 '}'
  
> BRANCH_TYPE_OTHER = get_other_branch_type_key()  # 'other'
> BRANCH_TYPES = get_all_branch_types()
  # BRANCH_TYPES = {
  #     'other': "Other",
  #     'feature': "Features",
  #     'bugfix': "Bugfixes",
  #     'hotfix': "Hotfixes",
  #     'deprecation': "Deprecations",
  # }
> TICKET_NUMBER_OTHER = 'other'
  # UNRELEASED = 'unreleased'
> UNRELEASED, UNRELEASED_LABEL = get_unreleased_key_label()
  
> IGNORE_COMMITS_EXACT_WORDS = get_ignore_commits_exact_words()
  
  
> def get_repository(path: str = None):
>     if not (path and os.path.exists(path) and os.path.isdir(path)):
>         path = os.getcwd()
  
>     return git.Git(path)
  
  
> def get_logs(between: str = None, path: str = None) -> Dict[str, Any]:
>     """Get lots of logs.
  
>     :param between:
>     :param path:
>     :return:
>     """
>     repository = get_repository(path)
>     lower: Union[str, Type[None]] = None
>     upper: Union[str, Type[None]] = None
>     if between:
>         try:
>             rev_list_text = repository.rev_list(between)
>             rev_list = rev_list_text.split('\n')
>             if len(rev_list) >= 2:
>                 upper = rev_list[0]
>                 lower = rev_list[-1]
!         except GitCommandError as err:
!             pass
  
      # Merges log
>     text_log_merges_args = []
>     if lower and upper:
          # Note, that ~1 addition make sure the lower range is inclusive
>         text_log_merges_args.append(
>             "{}~1..{}".format(lower, upper)
>         )
>     text_log_merges_args.extend([
>         "--pretty={}".format(PRETTY_FORMAT),
>         "--source",
          # "--all",
>         "--merges",
>     ])
>     text_log_merges = repository.log(*text_log_merges_args)
>     log_merges = text_log_merges.split("\n")
  
      # Commits log
>     text_log_args = []
>     if lower and upper:
          # Note, that ~1 addition make sure the lower range is inclusive
>         text_log_args.append(
>             "{}~1..{}".format(lower, upper)
>         )
>     text_log_args.extend([
>         "--pretty={}".format(PRETTY_FORMAT),
>         "--source",
          # "--all"  # TODO: remove
>     ])
  
>     text_log = repository.log(*text_log_args)
>     log = text_log.split("\n")
  
      # Tags log
>     text_log_tags_args = []
      # if lower and upper:
      #     text_log_tags_args.append(
      #         "{}..{}".format(lower, upper)
      #     )
>     text_log_tags_args.extend([
>         "--tags",
>         "--source",
>         "--oneline"
>     ])
>     text_log_tags = repository.log(*text_log_tags_args)
>     log_tags = text_log_tags.split("\n")
>     commit_tags = dict([s.split(' ', 1)[0].split('\t') for s in log_tags])
  
>     return {
>         'TEXT_LOG_MERGES': text_log_merges,
>         'LOG_MERGES': log_merges,
>         'TEXT_LOG': text_log,
>         'LOG': log,
>         'TEXT_LOG_TAGS': text_log_tags,
>         'LOG_TAGS': log_tags,
>         'COMMIT_TAGS': commit_tags,
>     }
  
  
> def get_branch_type(branch_type: AnyStr) -> str:
>     """Get branch type.
  
>     :param branch_type:
>     :return:
>     """
>     branch_type = branch_type.lower()
>     return branch_type if branch_type in BRANCH_TYPES else BRANCH_TYPE_OTHER
  
  
  # def generate_empty_tree() -> Dict[str, dict]:
  #     """Generate empty tree.
  #
  #     Example:
  #
  #         {
  #             'feature': {},
  #             'bugfix': {},
  #             'hotfix': {},
  #             'deprecation': {},
  #             'other': {
  #                 TICKET_NUMBER_OTHER: {
  #                     # 'title': '',
  #                     'commits': {}
  #                 }
  #             },
  #         }
  #
  #     :return:
  #     """
  #     empty_tree = {}
  #     for key, value in BRANCH_TYPES.items():
  #         empty_tree.update({key: {}})
  #
  #     empty_tree[BRANCH_TYPE_OTHER][TICKET_NUMBER_OTHER] = {'commits': {}}
  #     return empty_tree
  
  
> def prepare_changelog(
>     between: str = None,
>     unique_commit_messages: bool = False,
>     path: str = None
> ) -> Dict[
>         str, Dict[str, Dict[str, Union[str, Dict[str, Union[str, str]]]]]
> ]:
>     """Prepare changelog.
  
>     :param between:
>     :param unique_commit_messages:
>     :param path:
>     :return:
>     """
      # tree = generate_empty_tree()
>     tree = {}
  
>     cur_branch = None
>     cur_branch_type = None
>     branch_types = {}
  
>     logs = get_logs(between=between, path=path)
  
      # First fill feature branches only
>     for json_entry in logs['LOG_MERGES']:
>         try:
>             entry = json.loads(json_entry)
!         except json.decoder.JSONDecodeError:
!             continue  # TODO: fix this (when commit message contains " symbols)
>         merge_commit = True if ' ' in entry['merge'] else False
>         if merge_commit:
>             match = re.match(REGEX_PATTERN_MERGED_BRANCH_NAME, entry['title'])
  
              # Skip strange feature branches
>             if not match:
!                 continue
  
              # If no pattern found, use other branch
>             try:
>                 branch_type = get_branch_type(match.group('branch_type'))
!             except AttributeError:
!                 branch_type = BRANCH_TYPE_OTHER
  
              # If no pattern found, use other ticket number
>             try:
>                 ticket_number = match.group('ticket_number')
!             except AttributeError:
!                 ticket_number = TICKET_NUMBER_OTHER
  
>             branch_title = match.group('branch_title')
  
              # For normal tree
>             release = logs['COMMIT_TAGS'].get(entry['commit_abbr'])
  
>             if branch_type not in tree:
>                 tree[branch_type] = {}
  
>             if ticket_number not in tree[branch_type]:
>                 tree[branch_type][ticket_number] = {
>                     'commit_hash': entry['commit_hash'],
>                     'commit_abbr': entry['commit_abbr'],
>                     'ticket_number': ticket_number,
>                     'branch_type': branch_type,
>                     'slug': branch_title,
>                     'title': branch_title.replace('-', ' ').title(),
>                     'commits': {},
>                     'release': release,
>                 }
>             branch_types.update({ticket_number: branch_type})
  
      # Now go through commits
>     for json_entry in filter(None, logs['LOG']):
>         try:
>             entry = json.loads(json_entry)
!         except json.decoder.JSONDecodeError:
!             continue  # TODO: fix this (when commit message contains " symbols)
>         merge_commit = True if ' ' in entry['merge'] else False
>         if merge_commit:
>             match = re.match(REGEX_PATTERN_MERGED_BRANCH_NAME, entry['title'])
  
              # Skip strange feature branches
>             if not match:
!                 continue
  
>             try:
>                 branch_type = get_branch_type(match.group('branch_type'))
!             except AttributeError:
!                 branch_type = BRANCH_TYPE_OTHER
  
>             try:
>                 ticket_number = match.group('ticket_number')
!             except AttributeError:
!                 ticket_number = TICKET_NUMBER_OTHER
  
>             branch_title = match.group('branch_title')
>             cur_branch = copy.copy(ticket_number)
>             cur_branch_type = copy.copy(branch_type)
>         else:
>             match = re.match(REGEX_PATTERN_COMMIT_LINE, entry['title'])
>             try:
>                 ticket_number = match.group('ticket_number')
>                 commit_message = match.group('commit_message')
!             except AttributeError:
!                 ticket_number = ''
!                 commit_message = entry['title'][:]
  
              # Ignore the following messages
>             if commit_message.lower() in IGNORE_COMMITS_EXACT_WORDS:
!                 continue
  
>             for ignore_prefix in get_ignore_commits_prefixes():
>                 if commit_message.lower().startswith(ignore_prefix):
!                     continue
  
>             commit_hash = commit_message \
>                 if unique_commit_messages \
>                 else entry['commit_hash']
  
>             release = logs['COMMIT_TAGS'].get(entry['commit_abbr'])
  
>             if cur_branch:
  
>                 if cur_branch_type not in tree:
!                     tree[cur_branch_type] = {}
  
>                 if cur_branch not in tree[cur_branch_type]:
!                     tree[cur_branch_type][cur_branch] = {}
  
>                 if 'commits' not in tree[cur_branch_type][cur_branch]:
!                     tree[cur_branch_type][cur_branch]['commits'] = {}
  
>                 if cur_branch == ticket_number:
>                     tree[cur_branch_type][cur_branch]['commits'][commit_hash] = {  # NOQA
>                         'commit_hash': entry['commit_hash'],
>                         'commit_abbr': entry['commit_abbr'],
>                         'author': entry['author'],
>                         'date': entry['datetime'],
>                         'ticket_number': ticket_number,
>                         'title': commit_message,
>                     }
>                 else:
>                     other_branch_type = branch_types.get(
>                         ticket_number,
>                         BRANCH_TYPE_OTHER
>                     )
>                     try:
>                         if other_branch_type not in tree:
>                             tree[other_branch_type] = {}
  
>                         if ticket_number not in tree[other_branch_type]:
>                             tree[other_branch_type][ticket_number] = {}
  
>                         if 'commits' not in tree[other_branch_type][ticket_number]:
>                             tree[other_branch_type][ticket_number]['commits'] = {}
  
>                         tree[other_branch_type][ticket_number]['commits'][commit_hash] = {  # NOQA
>                             'commit_hash': entry['commit_hash'],
>                             'commit_abbr': entry['commit_abbr'],
>                             'author': entry['author'],
>                             'date': entry['datetime'],
>                             'ticket_number': ticket_number,
>                             'title': commit_message,
>                         }
!                     except:
                          # TODO: Anything here?
!                         pass
!             else:
!                 if BRANCH_TYPE_OTHER not in tree:
!                     tree[BRANCH_TYPE_OTHER] = {}
  
!                 if TICKET_NUMBER_OTHER not in tree[BRANCH_TYPE_OTHER]:
!                     tree[BRANCH_TYPE_OTHER][TICKET_NUMBER_OTHER] = {}
  
!                 if 'commits' not in tree[BRANCH_TYPE_OTHER][TICKET_NUMBER_OTHER]:
!                     tree[BRANCH_TYPE_OTHER][TICKET_NUMBER_OTHER]['commits'] = {}
  
!                 tree[BRANCH_TYPE_OTHER][TICKET_NUMBER_OTHER]['commits'][commit_hash] = {  # NOQA
!                     'commit_hash': entry['commit_hash'],
!                     'commit_abbr': entry['commit_abbr'],
!                     'author': entry['author'],
!                     'date': entry['datetime'],
!                     'ticket_number': ticket_number,
!                     'title': commit_message,
!                 }
  
>     return tree
  
  
> def prepare_releases_changelog(
>     between: str = None,
>     unique_commit_messages: bool = False,
>     path: str = None
> ) -> Dict[
>         str, Dict[str, Dict[str, Union[str, Dict[str, Union[str, str]]]]]
> ]:
>     """Prepare releases changelog.
  
>     :param between:
>     :param unique_commit_messages:
>     :param path:
>     :return:
>     """
>     logs = get_logs(between=between, path=path)
>     releases = [UNRELEASED] + [tag for tag in logs['COMMIT_TAGS'].values()]
      # releases_tree = {tag: generate_empty_tree() for tag in releases}
>     releases_tree = {}
  
>     cur_branch = None
>     cur_branch_type = None
>     branch_types = {}
  
      # First fill feature branches only
>     for json_entry in logs['LOG_MERGES']:
>         try:
>             entry = json.loads(json_entry)
!         except json.decoder.JSONDecodeError:
!             continue  # TODO: fix this (when commit message contains " symbols)
>         merge_commit = True if ' ' in entry['merge'] else False
>         if merge_commit:
>             match = re.match(REGEX_PATTERN_MERGED_BRANCH_NAME, entry['title'])
  
              # Skip strange feature branches
>             if not match:
!                 continue
  
              # If no pattern found, use other branch
>             try:
>                 branch_type = get_branch_type(match.group('branch_type'))
!             except AttributeError:
!                 branch_type = BRANCH_TYPE_OTHER
  
              # If no pattern found, use other ticket number
>             try:
>                 ticket_number = match.group('ticket_number')
!             except AttributeError:
!                 ticket_number = TICKET_NUMBER_OTHER
  
>             branch_title = match.group('branch_title')
  
              # For normal tree
>             release = logs['COMMIT_TAGS'].get(entry['commit_abbr'])
>             if not release:
!                 release = UNRELEASED
  
>             if release not in releases_tree:
>                 releases_tree[release] = {}
  
>             if branch_type not in releases_tree[release]:
>                 releases_tree[release][branch_type] = {}
  
>             if ticket_number not in releases_tree[release][branch_type]:
>                 releases_tree[release][branch_type][ticket_number] = {
>                     'commit_hash': entry['commit_hash'],
>                     'commit_abbr': entry['commit_abbr'],
>                     'ticket_number': ticket_number,
>                     'branch_type': branch_type,
>                     'slug': branch_title,
>                     'title': branch_title.replace('-', ' ').title(),
>                     'commits': {},
>                     'release': release,
>                 }
>             branch_types.update({ticket_number: branch_type})
  
      # Now go through commits
>     for json_entry in filter(None, logs['LOG']):
>         try:
>             entry = json.loads(json_entry)
!         except json.decoder.JSONDecodeError:
!             continue  # TODO: fix this (when commit message contains " symbols)
  
>         merge_commit = True if ' ' in entry['merge'] else False
>         if merge_commit:
>             match = re.match(REGEX_PATTERN_MERGED_BRANCH_NAME, entry['title'])
  
              # Skip strange feature branches
>             if not match:
!                 continue
  
>             try:
>                 branch_type = get_branch_type(match.group('branch_type'))
!             except AttributeError:
!                 branch_type = BRANCH_TYPE_OTHER
  
>             try:
>                 ticket_number = match.group('ticket_number')
!             except AttributeError:
!                 ticket_number = TICKET_NUMBER_OTHER
  
>             branch_title = match.group('branch_title')
>             cur_branch = copy.copy(ticket_number)
>             cur_branch_type = copy.copy(branch_type)
>         else:
>             match = re.match(REGEX_PATTERN_COMMIT_LINE, entry['title'])
>             try:
>                 ticket_number = match.group('ticket_number')
>                 commit_message = match.group('commit_message')
!             except AttributeError:
!                 ticket_number = ''
!                 commit_message = entry['title'][:]
  
              # Ignore the following messages
>             if commit_message.lower() in IGNORE_COMMITS_EXACT_WORDS:
!                 continue
  
>             for ignore_prefix in get_ignore_commits_prefixes():
>                 if commit_message.lower().startswith(ignore_prefix):
!                     continue
  
>             commit_hash = commit_message \
>                 if unique_commit_messages \
>                 else entry['commit_hash']
  
>             release = logs['COMMIT_TAGS'].get(entry['commit_abbr'])
>             if not release:
!                 release = UNRELEASED
  
>             if cur_branch:
  
>                 if release not in releases_tree:
!                     releases_tree[release] = {}
  
>                 if cur_branch_type not in releases_tree[release]:
!                     releases_tree[release][cur_branch_type] = {}
  
>                 if cur_branch not in releases_tree[release][cur_branch_type]:
!                     releases_tree[release][cur_branch_type][cur_branch] = {}
  
>                 if 'commits' not in releases_tree[release][cur_branch_type][cur_branch]:
!                     releases_tree[release][cur_branch_type][cur_branch]['commits'] = {}
  
>                 if cur_branch == ticket_number:
>                     releases_tree[release][cur_branch_type][cur_branch]['commits'][commit_hash] = {  # NOQA
>                         'commit_hash': entry['commit_hash'],
>                         'commit_abbr': entry['commit_abbr'],
>                         'author': entry['author'],
>                         'date': entry['datetime'],
>                         'ticket_number': ticket_number,
>                         'title': commit_message,
>                     }
>                 else:
>                     other_branch_type = branch_types.get(
>                         ticket_number,
>                         BRANCH_TYPE_OTHER
>                     )
>                     try:
>                         if release not in releases_tree:
!                             releases_tree[release] = {}
  
>                         if other_branch_type not in releases_tree[release]:
>                             releases_tree[release][other_branch_type] = {}
  
>                         if ticket_number not in releases_tree[release][other_branch_type]:
>                             releases_tree[release][other_branch_type][ticket_number] = {}
  
>                         if 'commits' not in releases_tree[release][other_branch_type][ticket_number]:
>                             releases_tree[release][other_branch_type][ticket_number]['commits'] = {}
  
>                         releases_tree[release][other_branch_type][ticket_number]['commits'][commit_hash] = {  # NOQA
>                             'commit_hash': entry['commit_hash'],
>                             'commit_abbr': entry['commit_abbr'],
>                             'author': entry['author'],
>                             'date': entry['datetime'],
>                             'ticket_number': ticket_number,
>                             'title': commit_message,
>                         }
!                     except:
                          # TODO: Anything here?
!                         pass
!             else:
!                 if release not in releases_tree:
!                     releases_tree[release] = {}
  
!                 if BRANCH_TYPE_OTHER not in releases_tree[release]:
!                     releases_tree[release][BRANCH_TYPE_OTHER] = {}
  
!                 if TICKET_NUMBER_OTHER not in releases_tree[release][BRANCH_TYPE_OTHER]:
!                     releases_tree[release][BRANCH_TYPE_OTHER][TICKET_NUMBER_OTHER] = {}
  
!                 if 'commits' not in releases_tree[release][BRANCH_TYPE_OTHER][TICKET_NUMBER_OTHER]:
!                     releases_tree[release][BRANCH_TYPE_OTHER][TICKET_NUMBER_OTHER]['commits'] = {}
  
!                 releases_tree[release][BRANCH_TYPE_OTHER][TICKET_NUMBER_OTHER]['commits'][commit_hash] = {  # NOQA
!                     'commit_hash': entry['commit_hash'],
!                     'commit_abbr': entry['commit_abbr'],
!                     'author': entry['author'],
!                     'date': entry['datetime'],
!                     'ticket_number': ticket_number,
!                     'title': commit_message,
!                 }
  
>     return releases_tree
  
  
> def validate_between(between: str = None) -> bool:
>     """Validate between.
  
>     :param between:
>     :return:
>     """
>     if between:
>         pass  # TODO
>     return True
  
  
  # def get_latest_release(path: str = None) -> str:
  #     """Get latest release.
  #
  #     Command:
  #
  #         git describe --match '*.*' --abbr=0
  #
  #     :return:
  #     """
  #     repository = get_repository(path)
  #     return repository.describe('--match', '*.*', '--abbr=0')
  
  
> def get_latest_releases(limit: int = 2, path: str = None) -> list:
>     """Get latest <limit> releases.
  
>     Command:
  
>         git tag --sort=-version:refname -l '*.*' | head -n <limit>
  
>     :return:
>     """
>     repository = get_repository(path)
>     return repository.tag(
>         '--sort=-version:refname', '--list', '*.*'
>     ).split('\n')[:limit]
  
  
> def json_changelog(between: str = None,
>                    include_other: bool = True,
>                    show_releases: bool = False,
>                    latest_release: bool = False,
>                    path: str = None):
>     if latest_release:
>         latest_two_releases = get_latest_releases(limit=2, path=path)
>         latest_two_releases = latest_two_releases[::-1]
>         if len(latest_two_releases):
>             between = '..'.join(latest_two_releases)
  
>     if not show_releases:
>         tree = prepare_changelog(
>             between=between,
>             unique_commit_messages=True,
>             path=path
>         )
>         return tree
>     else:
>         releases_tree = prepare_releases_changelog(
>             between=between,
>             unique_commit_messages=True,
>             path=path
>         )
>         return releases_tree
  
  
> def json_changelog_cli() -> Type[None]:
>     """Generate changelog (JSON format)."""
>     parser = argparse.ArgumentParser(description='Generate JSON changelog')
>     parser.add_argument(
>         'between',
>         nargs='?',
>         default=None,
>         help="Range, might be tag or a commit or a branch.",
>     )
>     parser.add_argument(
>         '--no-other',
>         dest="no_other",
>         default=False,
>         action='store_true',
>         help="No `Other` section",
>     )
>     parser.add_argument(
>         '--show-releases',
>         dest="show_releases",
>         default=False,
>         action='store_true',
>         help="Show releases",
>     )
>     parser.add_argument(
>         '--latest-release',
>         dest="latest_release",
>         default=False,
>         action='store_true',
>         help="Generate changelog for the latest release only",
>     )
>     args = parser.parse_args(sys.argv[1:])
>     between = args.between if validate_between(args.between) else None
>     include_other = not args.no_other
>     show_releases = args.show_releases
>     latest_release = args.latest_release
  
>     print(
>         json_changelog(
>             between=between,
>             include_other=include_other,
>             show_releases=show_releases,
>             latest_release=latest_release
>         )
>     )
  
  
> def generate_changelog(between: str = None,
>                        include_other: bool = True,
>                        show_releases: bool = False,
>                        latest_release: bool = False,
>                        path: str = None) -> str:
>     """Generate changelog (markdown format)."""
  
      # if show_latest_release and between:
      #     raise Exception(
      #         "--show-latest-release can't be used in combination with specific"
      #         "tags/commits/branches range."
      #     )
>     if latest_release:
>         latest_two_releases = get_latest_releases(limit=2, path=path)
>         latest_two_releases = latest_two_releases[::-1]
>         if len(latest_two_releases):
>             between = '..'.join(latest_two_releases)
  
>     changelog = []
  
>     if not show_releases:
>         tree = prepare_changelog(
>             between=between,
>             unique_commit_messages=True,
>             path=path
>         )
>         for branch_type, tickets in tree.items():
              # Skip adding orphaned commits if explicitly asked not to.
>             if branch_type == BRANCH_TYPE_OTHER and not include_other:
>                 continue
  
              # Do not add branch type if no related branches found
>             if tickets:
>                 changelog.append(
>                     "\n**{}**".format(BRANCH_TYPES.get(branch_type))
>                 )
  
              # Add tickets
>             for ticket_number, ticket_data in tickets.items():
>                 if branch_type != BRANCH_TYPE_OTHER:
>                     changelog.append(
>                         "\n*{} {}*\n".format(
>                             ticket_number,
>                             ticket_data['title']
>                         )
>                     )
!                 else:
!                     changelog.append('')
  
>                 for commit_hash, commit_data in ticket_data['commits'].items():
>                     changelog.append(
>                         "- {} [{}]".format(
>                             commit_data['title'],
>                             commit_data['author']
>                         )
>                     )
>     else:
>         releases_tree = prepare_releases_changelog(
>             between=between,
>             unique_commit_messages=True,
>             path=path
>         )
>         for release, branches in releases_tree.items():
>             release_label = UNRELEASED_LABEL \
>                 if release == UNRELEASED \
>                 else release
  
>             changelog.append("\n### {}".format(release_label))
>             for branch_type, tickets in branches.items():
                  # Skip adding orphaned commits if explicitly asked not to.
>                 if branch_type == BRANCH_TYPE_OTHER and not include_other:
>                     continue
  
                  # Do not add branch type if no related branches found
>                 if tickets:
>                     changelog.append(
>                         "\n**{}**".format(BRANCH_TYPES.get(branch_type))
>                     )
  
                  # Add tickets
>                 for ticket_number, ticket_data in tickets.items():
>                     if branch_type != BRANCH_TYPE_OTHER:
>                         if 'title' not in ticket_data:
!                             continue
>                         changelog.append(
>                             "\n*{} {}*\n".format(ticket_number,
>                                                  ticket_data['title'])
>                         )
!                     else:
!                         changelog.append('')
  
>                     for commit_hash, commit_data in ticket_data['commits'].items():  # NOQA
>                         changelog.append(
>                             "- {} [{}]".format(
>                                 commit_data['title'],
>                                 commit_data['author']
>                             )
>                         )
  
>     return '\n'.join(changelog)
  
  
> def generate_changelog_cli() -> Type[None]:
>     parser = argparse.ArgumentParser(description='Generate changelog')
>     parser.add_argument(
>         'between',
>         nargs='?',
>         default=None,
>         help="Range, might be tag or a commit or a branch.",
>     )
>     parser.add_argument(
>         '--no-other',
>         dest="no_other",
>         default=False,
>         action='store_true',
>         help="No `Other` section",
>     )
>     parser.add_argument(
>         '--show-releases',
>         dest="show_releases",
>         default=False,
>         action='store_true',
>         help="Show releases",
>     )
>     parser.add_argument(
>         '--latest-release',
>         dest="latest_release",
>         default=False,
>         action='store_true',
>         help="Generate changelog for the latest release only",
>     )
>     args = parser.parse_args(sys.argv[1:])
>     between = args.between if validate_between(args.between) else None
>     include_other = not args.no_other
>     show_releases = args.show_releases
>     latest_release = args.latest_release
  
>     print(
>         generate_changelog(
>             between=between,
>             include_other=include_other,
>             show_releases=show_releases,
>             latest_release=latest_release
>         )
>     )
  
  
> def make_config_file() -> bool:
>     """Make (or overwrite) config file.
  
>     :return:
>     """
>     source_filename = project_dir('.matyan.ini')
>     dest_filename = os.path.join(os.getcwd(), '.matyan.ini')
>     try:
>         copyfile(source_filename, dest_filename)
>         return True
!     except Exception as err:
!         return False
  
  
> def make_config_file_cli():
>     return not make_config_file()
