#!/usr/bin/env python3
# ==========================================================================
#              ____        _ _     _     _____           _
#             | __ ) _   _(_) | __| |   |_   _|__   ___ | |___
#             |  _ \| | | | | |/ _` |_____| |/ _ \ / _ \| / __|
#             | |_) | |_| | | | (_| |_____| | (_) | (_) | \__ \
#             |____/ \__,_|_|_|\__,_|     |_|\___/ \___/|_|___/
#
#                           --- Build-Tools ---
#                https://www.nntb.no/~dreibh/system-tools/
# ==========================================================================
#
# Version-Bump
# Copyright (C) 2018-2026 by Thomas Dreibholz
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
# Contact: thomas.dreibholz@gmail.com

import glob
import os
import re
import subprocess
import sys
import time
import urllib.error
import urllib.request

from typing import Final, TextIO


# Set current Debian standards version here
# (See https://www.debian.org/doc/debian-policy/ for latest version!)
DEBIAN_STANDARDS_VERSION="4.7.2"


# ###### Show difference between two files ##################################
def showDiff(a : str, b : str) -> None:
   try:
      subprocess.run( [ 'diff', '--color=always', a, b ] )
   except Exception as e:
      sys.stderr.write('ERROR: Diff run failed: ' + str(e) + '\n')
      sys.exit(1)


# ###### Apply update #######################################################
def applyUpdate(new : str, old : str) -> None:
   sys.stdout.write('Updating ' + old + ' ...\n')
   os.rename(new, old)



# ###### Main program #######################################################

# ====== Read packaging configuration =======================================
packageMaintainer     : str | None           = None
packageMakeDist       : str | None           = None
packagingConfFileName : Final[str]           = 'packaging.conf'
match                 : re.Match[str] | None = None
re_package_maintainer = re.compile(r'^(MAINTAINER=\")(.*)(\".*$)')
re_package_makedist   = re.compile(r'^(MAKE_DIST=\")(.*)(\".*$)')
try:
   packagingConfFile = open(packagingConfFileName, 'r', encoding='utf-8')
   packagingConfFileContents = packagingConfFile.readlines()
   for line in packagingConfFileContents:
      match = re_package_maintainer.match(line)
      if match is not None:
         packageMaintainer = match.group(2)
      else:
         match = re_package_makedist.match(line)
         if match is not None:
            packageMakeDist = match.group(2)
   packagingConfFile.close()
except Exception as e:
   sys.stderr.write('ERROR: Unable to read ' + packagingConfFileName + ': ' + str(e) + '\n')
   sys.exit(1)
if packageMaintainer is None:
   sys.stderr.write('ERROR: Unable to find MAINTAINER in ' + packagingConfFileName + '!\n')
   sys.exit(1)
elif packageMakeDist is None:
   sys.stderr.write('ERROR: Unable to find MAKE_DIST in ' + packagingConfFileName + '!\n')
   sys.exit(1)


# ====== Read Debian configuration ==========================================
debianChangeLogFileName : Final[str] = 'debian/changelog'
debianControlFileName   : Final[str] = 'debian/control'
debianPackage           : str | None = None
debianVersionString     : str | None = None
debianStandardsVersion  : str | None = None
debianDistribution      : str | None = None
debianITP               : int | None = None
debianPackageStatus     : str | None = None

if os.path.isfile(debianChangeLogFileName):
   re_debian_version = re.compile(r'^([a-zA-Z0-9-+]+)[ \t]*\((\d+:|)(\d+)\.(\d+)\.(\d+)(.*|)-(\d|\d[a-zA-Z-+~]+\d|)\)[ \t]*([a-zA-Z-+]+)[ \t]*;')
   re_debian_itp1    = re.compile(r'^ * .*ITP.*Closes: #([0-9]+).*$')
   re_debian_itp2    = re.compile(r'^ * .*Closes: #([0-9]+).*ITP.*$')
   try:
      debianChangeLogFile = open(debianChangeLogFileName, 'r', encoding='utf-8')
      debianChangeLogFileContents = debianChangeLogFile.readlines()
      n = 0
      for line in debianChangeLogFileContents:
         n = n + 1
         if n == 1:
            match = re_debian_version.match(line)
            if match is not None:
                debianPackage          = match.group(1)
                debianVersionPrefix    = match.group(2)
                debianVersionMajor     = int(match.group(3))
                debianVersionMinor     = int(match.group(4))
                debianVersionPatch     = int(match.group(5))
                debianVersionExtra     = match.group(6)
                debianVersionPackaging = match.group(7)
                debianDistribution     = match.group(8)
                debianVersionString    = str(debianVersionMajor) + '.' + \
                                        str(debianVersionMinor) + '.' + \
                                        str(debianVersionPatch) + debianVersionExtra
         elif n > 1:
            match = re_debian_itp1.match(line)
            if match is None:
               match = re_debian_itp2.match(line)
            if match is not None:
               # print('ITP: ' + line)
               debianITP = int(match.group(1))
               break
      debianChangeLogFile.close()
   except Exception as e:
      sys.stderr.write('ERROR: Unable to read ' + debianChangeLogFileName + ': ' + str(e) + '\n')
      sys.exit(1)

   if debianPackage is None:
      sys.stderr.write('ERROR: Cannot find required package versioning details in ' + debianChangeLogFileName + '!\n')
      sys.exit(1)

   re_debian_standards_version = re.compile(r'^Standards-Version:[ \t]*([0-9\.]*)[ \t]*$')
   try:
      debianControlFile = open(debianControlFileName, 'r', encoding='utf-8')
      debianControlFileContents = debianControlFile.readlines()
      for line in debianControlFileContents:
         match = re_debian_standards_version.match(line)
         if match is not None:
            debianStandardsVersion = match.group(1)
      debianControlFile.close()
   except Exception as e:
      sys.stderr.write('ERROR: Unable to read ' + debianControlFileName + ': ' + str(e) + '\n')
      sys.exit(1)


# ====== Read RPM configuration =============================================
rpmSpecFileName  = None
rpmPackage       = None
rpmVersionString = None

rpmSpecFileNames = glob.glob('rpm/*.spec')
if len(rpmSpecFileNames) > 0:
   rpmSpecFileName     = rpmSpecFileNames[0]
   rpmVersionPackaging = None
   re_rpm_name    = re.compile(r'^(Name:[ \t]*)(\S+)')
   re_rpm_version = re.compile(r'^(Version:[ \t]*)(\d+)\.(\d+)\.(\d+)(.*|)')
   re_rpm_release = re.compile(r'^(Release:[ \t]*)(\d+)')
   try:
      rpmSpecFile = open(rpmSpecFileName, 'r', encoding='utf-8')
      rpmSpecFileContents = rpmSpecFile.readlines()
      for line in rpmSpecFileContents:
         match = re_rpm_version.match(line)
         if match is not None:
            rpmVersionMajor = int(match.group(2))
            rpmVersionMinor = int(match.group(3))
            rpmVersionPatch = int(match.group(4))
            rpmVersionExtra = match.group(5)
            rpmVersionString = str(rpmVersionMajor) + '.' + \
                               str(rpmVersionMinor) + '.' + \
                               str(rpmVersionPatch) + rpmVersionExtra
         else:
            match = re_rpm_release.match(line)
            if match is not None:
               rpmVersionPackaging = int(match.group(2))
            else:
               match = re_rpm_name.match(line)
               if match is not None:
                  rpmPackage = match.group(2)
      rpmSpecFile.close()
   except Exception as e:
      sys.stderr.write('ERROR: Unable to read ' + rpmSpecFileName + ': ' + str(e) + '\n')
      sys.exit(1)


# ====== Read FreeBSD configuration =========================================
freeBSDMakefileName  : str | None = None
freeBSDVersionString : str | None = None
freeBSDMakefileNames : list[str]  = glob.glob('freebsd/*/Makefile')
if len(freeBSDMakefileNames) > 0:
   freeBSDMakefileName = freeBSDMakefileNames[0]
   re_freebsd_version = re.compile(r'^(DISTVERSION=[ \t]*)(\d+)\.(\d+)\.(\d+)(.*|)')
   try:
      freeBSDMakefileFile = open(freeBSDMakefileName, 'r', encoding='utf-8')
      freeBSDMakefileFileContents = freeBSDMakefileFile.readlines()
      for line in freeBSDMakefileFileContents:
         match = re_freebsd_version.match(line)
         if match is not None:
            freeBSDVersionMajor = int(match.group(2))
            freeBSDVersionMinor = int(match.group(3))
            freeBSDVersionPatch = int(match.group(4))
            freeBSDVersionExtra = match.group(5)
            freeBSDVersionString = str(freeBSDVersionMajor) + '.' + \
                                   str(freeBSDVersionMinor) + '.' + \
                                   str(freeBSDVersionPatch) + freeBSDVersionExtra
      freeBSDMakefileFile.close()
   except Exception as e:
      sys.stderr.write('ERROR: Unable to read ' + freeBSDMakefileName + ': ' + str(e) + '\n')
      sys.exit(1)


# ====== Read CMakeLists.txt configuration ==================================
cmakeFileName      : Final[str] = 'CMakeLists.txt'
cmakeFoundVersion  : bool       = False
cmakePackage       : str | None = None
cmakeVersionMajor  : int | None = None
cmakeVersionMinor  : int | None = None
cmakeVersionPatch  : int | None = None
cmakeVersionExtra  : str | None = None
cmakeVersionString : str | None = None

if os.path.isfile(cmakeFileName):
   re_cmake_project   = re.compile(r'[ \t]*PROJECT[ \t]*\(([a-zA-Z0-9-+]+)')
   re_cmakefile_major = re.compile(r'^[Ss][Ee][Tt]\(BUILD_MAJOR[ \t]*"(\d+)"[ \t]*\)')
   re_cmakefile_minor = re.compile(r'^[Ss][Ee][Tt]\(BUILD_MINOR[ \t]*"(\d+)"[ \t]*\)')
   re_cmakefile_patch = re.compile(r'^[Ss][Ee][Tt]\(BUILD_PATCH[ \t]*"(\d+)(~[a-zA-Z0-9\.+]+|)"[ \t]*\)')
   try:
      cmakeFile = open(cmakeFileName, 'r', encoding='utf-8')
      cmakeFileContents = cmakeFile.readlines()
      for line in cmakeFileContents:
         match = re_cmakefile_major.match(line)
         if match is not None:
            cmakeVersionMajor = int(match.group(1))
         else:
            match = re_cmakefile_minor.match(line)
            if match is not None:
               cmakeVersionMinor = int(match.group(1))
            else:
               match = re_cmakefile_patch.match(line)
               if match is not None:
                  cmakeVersionPatch = int(match.group(1))
                  cmakeVersionExtra = match.group(2)
               else:
                  match = re_cmake_project.match(line)
                  if match is not None:
                     cmakePackage = match.group(1)
      cmakeFile.close()
      if (cmakeVersionMajor is not None) and \
         (cmakeVersionMinor is not None) and \
         (cmakeVersionPatch is not None) and \
         (cmakeVersionExtra is not None):
         cmakeVersionString = str(cmakeVersionMajor) + '.' + \
                              str(cmakeVersionMinor) + '.' + \
                              str(cmakeVersionPatch) + cmakeVersionExtra
   except Exception as e:
      sys.stderr.write('ERROR: Unable to read ' + cmakeFileName + ': ' + str(e) + '\n')
      sys.exit(1)


# ====== Read configure.ac configuration ====================================
autoconfFoundVersion  : bool       = False
autoconfPackage       : str | None = None
autoconfVersionMajor  : int | None = None
autoconfVersionMinor  : int | None = None
autoconfVersionPatch  : int | None = None
autoconfVersionExtra  : str | None = None
autoconfVersionString : str | None = None

for autoconfFileName in [ 'configure.ac', 'configure.in' ]:
   if os.path.isfile(autoconfFileName):
      break

if os.path.isfile(autoconfFileName):
   re_autoconffile_version = re.compile(r'^AC_INIT\([ \t]*\[(.*)\][ \t]*,[ \t]*\[(\d).(\d).(\d+)([~+][a-zA-Z0-9\.+]+|)\][ \t]*,[ \t]*\[(.*)\][ \t]*\)')
   try:
      autoconfFile = open(autoconfFileName, 'r', encoding='utf-8')
      autoconfFileContents = autoconfFile.readlines()
      for line in autoconfFileContents:
         match = re_autoconffile_version.match(line)
         if match is not None:
            autoconfPackage      = match.group(1)
            autoconfVersionMajor = int(match.group(2))
            autoconfVersionMinor = int(match.group(3))
            autoconfVersionPatch = int(match.group(4))
            autoconfVersionExtra = match.group(5)
            autoconfVersionString = str(autoconfVersionMajor) + '.' + \
                                    str(autoconfVersionMinor) + '.' + \
                                    str(autoconfVersionPatch) + autoconfVersionExtra
            break
      autoconfFile.close()
   except Exception as e:
      sys.stderr.write('ERROR: Unable to read ' + autoconfFileName + ': ' + str(e) + '\n')
      sys.exit(1)


# ====== Read configure.ac configuration ====================================
otherFoundVersion  = False
otherPackage       = None
otherVersionMajor  = None
otherVersionMinor  = None
otherVersionPatch  = None
otherVersionExtra  = None
otherVersionString = None

for otherFileName in [ 'version' ]:
   if os.path.isfile(otherFileName):
      break

if os.path.isfile(otherFileName):
   re_otherfile_version = re.compile(r'(\S+) (\d).(\d).(\d+)([~+][a-zA-Z0-9\.+]+|)')
   try:
      otherFile = open(otherFileName, 'r', encoding='utf-8')
      otherFileContents = otherFile.readlines()
      line = otherFileContents[0]
      match = re_otherfile_version.match(line)
      if match is not None:
         otherPackage      = match.group(1)
         otherVersionMajor = int(match.group(2))
         otherVersionMinor = int(match.group(3))
         otherVersionPatch = int(match.group(4))
         otherVersionExtra = match.group(5)
         otherVersionString = str(otherVersionMajor) + '.' + \
                                 str(otherVersionMinor) + '.' + \
                                 str(otherVersionPatch) + otherVersionExtra
      otherFile.close()
   except Exception as e:
      sys.stderr.write('ERROR: Unable to read ' + otherFileName + ': ' + str(e) + '\n')
      sys.exit(1)


# ====== Check versions =====================================================
versionMajor : int = 0
versionMinor : int = 0
versionPatch : int = 0
versionExtra : str = ""
if cmakeVersionString is not None:
   assert cmakeVersionMajor is not None
   assert cmakeVersionMinor is not None
   assert cmakeVersionPatch is not None
   assert cmakeVersionExtra is not None
   package       = cmakePackage
   versionString = cmakeVersionString
   versionMajor  = cmakeVersionMajor
   versionMinor  = cmakeVersionMinor
   versionPatch  = cmakeVersionPatch
   versionExtra  = cmakeVersionExtra
elif autoconfVersionString is not None:
   assert autoconfVersionMajor is not None
   assert autoconfVersionMinor is not None
   assert autoconfVersionPatch is not None
   assert autoconfVersionExtra is not None
   package       = autoconfPackage
   versionString = autoconfVersionString
   versionMajor  = autoconfVersionMajor
   versionMinor  = autoconfVersionMinor
   versionPatch  = autoconfVersionPatch
   versionExtra  = autoconfVersionExtra
elif otherVersionString is not None:
   assert otherVersionMajor is not None
   assert otherVersionMinor is not None
   assert otherVersionPatch is not None
   assert otherVersionExtra is not None
   package       = otherPackage
   versionString = otherVersionString
   versionMajor  = otherVersionMajor
   versionMinor  = otherVersionMinor
   versionPatch  = otherVersionPatch
   versionExtra  = otherVersionExtra
else:
   sys.stderr.write('ERROR: Unable to find version in ' + cmakeFileName + ', ' + autoconfFileName + ' or ' + otherFileName + '!\n')
   sys.exit(1)

if cmakeVersionString is not None:
   sys.stdout.write('Version from CMakefile file:         ' + cmakeVersionString + '   (in ' + cmakeFileName + ')\n')
elif autoconfVersionString is not None:
   sys.stdout.write('Version from autoconf/automake file: ' + autoconfVersionString + '   (in ' + autoconfFileName + ')\n')

if debianVersionString is not None:
   sys.stdout.write('Version from Debian changelog file:  ' + debianVersionString + '   (in ' + debianChangeLogFileName + ')\n')
   if debianVersionString != versionString:
      sys.stderr.write('ERROR: Debian version does not match build version!\n')
      sys.exit(1)

if ( (rpmSpecFileName is not None) and (rpmVersionString is not None) ):
   sys.stdout.write('Version from RPM spec file:          ' + rpmVersionString + '   (in ' + rpmSpecFileName + ')\n')
   if rpmVersionString != versionString:
      sys.stderr.write('ERROR: RPM version does not match build version!\n')
      sys.exit(1)

if ( (freeBSDMakefileName is not None) and (freeBSDVersionString is not None) ):
   sys.stdout.write('Version from FreeBSD ports Makefile: ' + freeBSDVersionString + '   (in ' + freeBSDMakefileName + ')\n')
   if freeBSDVersionString != versionString:
      sys.stderr.write('WARNING: FreeBSD ports version does not match build version!\n')

if package is None:
   if debianPackage is not None:
      package = debianPackage
   elif rpmPackage is not None:
      package = rpmPackage
   else:
      sys.stderr.write('ERROR: Unable to find package name!\n')
      sys.exit(1)


# ====== Version bump =======================================================
for i in range(1, len(sys.argv)):
   if (sys.argv[i] == '-M') or (sys.argv[i] == '--major'):
      versionMajor = versionMajor + 1
      versionMinor = 0
      versionPatch = 0
      versionExtra = ""

   elif (sys.argv[i] == '-m') or (sys.argv[i] == '--minor'):
      versionMinor = versionMinor + 1
      versionPatch = 0
      versionExtra = ""

   elif (sys.argv[i] == '-p') or (sys.argv[i] == '--patch'):
      if versionExtra == "":
         versionPatch = versionPatch + 1
      versionExtra = ""

   elif (sys.argv[i] == '-e') or (sys.argv[i] == '--extra') or (sys.argv[i][0:8] == '--extra='):
      if ((versionExtra == '') or (sys.argv[i][8:] != '')):
         if sys.argv[i][8:] == '':
            versionExtra = '~alpha1.0'
         else:
            if ((sys.argv[i][8:9] == '~') or (sys.argv[i][8:9] == '+')):
               versionExtra = sys.argv[i][8:]
            else:
               versionExtra = '~' + sys.argv[i][8:]
            match = re.match(r'^([~+].*)(\d+)$', versionExtra)
            if match is None:
               versionExtra = versionExtra + '0'
      else:
         match = re.match(r'^([~+].*[^\d])(\d+)$', versionExtra)
         if match is not None:
            extra : int = int(match.group(2)) + 1
            versionExtra = match.group(1) + str(extra)
         else:
            sys.stderr.write('ERROR: Unable to increment extra version ' + versionExtra + '!\n')
            sys.exit(1)

   elif (sys.argv[i] == '-r') or (sys.argv[i] == '--release'):
      versionExtra = ''

   elif sys.argv[i][0:15] == '--distribution=':
      sys.stderr.write('Replacing distribution ' + str(debianDistribution) + ' by ' + sys.argv[i][15:] + '.\n')
      debianDistribution = sys.argv[i][15:]

   else:
      sys.stderr.write('Usage: ' + sys.argv[0] + ' [-M|--major | -m|--minor | -p|--patch  -e|--extra|--extra=label | -r|--release] [--distribution=Ubuntu/Debian distribution]\n')
      sys.exit(1)


# ======= Check new version =================================================
versionString = str(versionMajor) + '.' + str(versionMinor) + '.' + \
                str(versionPatch) + versionExtra
sys.stdout.write('New version:                         ' + versionString + '\n')

if cmakeVersionString is not None:
   if (versionMajor == cmakeVersionMajor) and \
      (versionMinor == cmakeVersionMinor) and \
      (versionPatch == cmakeVersionPatch) and \
      (versionExtra == cmakeVersionExtra):
      sys.stdout.write('No change -> exiting.\n')
      sys.exit(1)
elif autoconfVersionString is not None:
   if (versionMajor == autoconfVersionMajor) and \
      (versionMinor == autoconfVersionMinor) and \
      (versionPatch == autoconfVersionPatch) and \
      (versionExtra == autoconfVersionExtra):
      sys.stdout.write('No change -> exiting.\n')
      sys.exit(1)


# ====== Check, whether ITP has been fulfilled  =============================
if ( (debianPackage is not None) and (debianITP is not None) ):
   sys.stdout.write('Searching package in Debian (ITP #' + str(debianITP) + ') ... ')
   try:
      webFile = urllib.request.urlopen('https://tracker.debian.org/pkg/' + debianPackage)
      webFileContents = webFile.read().decode('utf-8')
      webFile.close()
      # Package *is* or *was* in Debian?

      if re.search('package is gone', webFileContents):
         # Package was in Debian => There is need for a new ITP entry!
         debianPackageStatus = 'GONE'
         sys.stdout.write('GONE!\n')
      else:
         # Package is still in Debian!
         debianPackageStatus = 'EXISTING'
         debianITP           = None   # No need to move the ITP entry!
         sys.stdout.write('YES -> nothing to do\n')
   except urllib.error.HTTPError as e:
      if e.code == 404:
         # => Package is and was not in Debian!
         sys.stdout.write('NO -> ITP\n')
         debianPackageStatus = 'ITP'
elif debianPackage is not None:
   sys.stdout.write('No Debian ITP found.\n')


# ====== Update CMakeLists.txt ==============================================
if cmakeVersionString is not None:
   cmakeFileNew = open(cmakeFileName + '.new', 'w', encoding='utf-8')

   updatedMajor = False
   updatedMinor = False
   updatedPatch = False
   for line in cmakeFileContents:
      match = re_cmakefile_major.match(line)
      if match is not None:
         cmakeFileNew.write('SET(BUILD_MAJOR "' + str(versionMajor) + '")\n')
         updatedMajor = True
      else:
         match = re_cmakefile_minor.match(line)
         if match is not None:
            cmakeFileNew.write('SET(BUILD_MINOR "' + str(versionMinor) + '")\n')
            updatedMinor = True
         else:
            match = re_cmakefile_patch.match(line)
            if match is not None:
               cmakeFileNew.write('SET(BUILD_PATCH "' + str(versionPatch) + versionExtra + '")\n')
               updatedPatch = True
            else:
               cmakeFileNew.write(line)

   cmakeFileNew.close()
   if (updatedMajor == False) or (updatedMinor == False) or (updatedPatch == False):
      sys.stderr.write('ERROR: ' + cmakeFileName + ' update failed! Check entries!\n')
      sys.exit(1)


# ====== Update configure.ac ================================================
elif autoconfVersionString is not None:
   autoconfFileNew = open(autoconfFileName + '.new', 'w', encoding='utf-8')

   updatedMajor = False
   updatedMinor = False
   updatedPatch = False
   for line in autoconfFileContents:
      match = re_autoconffile_version.match(line)
      if match is not None:
         autoconfFileNew.write('AC_INIT([' + match.group(1) + '], ' +
                               '[' + str(versionMajor) + '.' + str(versionMinor) + '.' + str(versionPatch) + versionExtra + '], ' +
                               '[' + match.group(6) + '])\n')
         updated = True
      else:
         autoconfFileNew.write(line)

   autoconfFileNew.close()
   if (updated == False):
      sys.stderr.write('ERROR: ' + autoconfFileName + ' update failed! Check entries!\n')
      sys.exit(1)


# ====== Update debian/changelog ============================================
if debianVersionString is not None:
   assert debianPackage       is not None
   assert debianVersionPrefix is not None
   assert debianDistribution  is not None
   debianNewChangeLogEntry : bool   = (cmakeVersionExtra == '') or (autoconfVersionExtra == '')
   debianNewChangeLogFile  : TextIO = open(debianChangeLogFileName + '.new', 'w', encoding='utf-8')

   nn              : int        = 0
   ee              : int        = 0
   inFirstEntry    : bool       = True
   re_empty_line   : re.Pattern[str] = re.compile(r'^[\s]*$')
   re_end_of_entry : re.Pattern[str] = re.compile(r'^ -- .* +[\d][\d][\d][\d]')
   for line in debianChangeLogFileContents:
      nn = nn + 1

       # ------ Update existing entry ---------------------------------------
      if nn == 1:
         match = re.search('[a-zA-Z-+~]+', debianVersionPackaging)
         if match is not None:
            newVersionPackaging = '1' + match.group(0) + '1'
         else:
            newVersionPackaging = '1'
         debianNewChangeLogFile.write(debianPackage + ' (' + debianVersionPrefix + versionString + '-' + newVersionPackaging + ') ' + \
                                      debianDistribution + '; urgency=medium\n')

         # ------ Create new entry ------------------------------------------
         if debianNewChangeLogEntry == True:
            now = time.strftime('%a, %d %b %Y %H:%M:%S %z', time.localtime())
            debianNewChangeLogFile.write('\n')
            debianNewChangeLogFile.write('  * New upstream release.\n')
            if debianITP is not None:
               debianNewChangeLogFile.write('  * Closes: #' + str(debianITP) + ' (ITP).\n')
            if ((debianStandardsVersion is not None) and
                (debianStandardsVersion != DEBIAN_STANDARDS_VERSION)):
               debianNewChangeLogFile.write('  * debian/control: Updated standards version to ' + DEBIAN_STANDARDS_VERSION + '.\n')
            debianNewChangeLogFile.write('\n')
            debianNewChangeLogFile.write(' -- ' + packageMaintainer + '  ' + now + '\n\n')
            inFirstEntry = False


      if ((nn > 1) or (debianNewChangeLogEntry == True)):
         # ------ Not a new entry: update maintainer and time ---------------
         if ((inFirstEntry == True) and (debianNewChangeLogEntry == False)):
            match = re_empty_line.match(line)
            if match is not None:
               ee = ee + 1
               if (ee == 2) and (debianITP is not None):
                  debianNewChangeLogFile.write('  * Closes: #' + str(debianITP) + ' (ITP).\n')
               if ((ee == 2) and
                   (debianStandardsVersion is not None) and
                   (debianStandardsVersion != DEBIAN_STANDARDS_VERSION)):
                  debianNewChangeLogFile.write('  * debian/control: Updated standards version to ' + DEBIAN_STANDARDS_VERSION + '.\n')
            match = re_end_of_entry.match(line)
            if match is not None:
               now = time.strftime('%a, %d %b %Y %H:%M:%S %z', time.localtime())
               debianNewChangeLogFile.write(' -- ' + packageMaintainer + '  ' + now + '\n')
               inFirstEntry = False
               continue

         # ------ Is there is an existing ITP entry, do not add it ----------
         # The new ITP entry has been written above!
         if debianITP is not None:
            match = re_debian_itp1.match(line)
            if match is None:
               match = re_debian_itp2.match(line)
            if match is not None:
               # Drop old ITP line. Only the latest entry should contain an ITP!
               continue

         # ------ Else: just copy existing line -----------------------------
         debianNewChangeLogFile.write(line)

   debianNewChangeLogFile.close()

   # ------ Update standards version in debian/control ----------------------
   if ((debianStandardsVersion is not None) and
       (debianStandardsVersion != DEBIAN_STANDARDS_VERSION)):

      debianNewControlFile = open(debianControlFileName + '.new', 'w', encoding='utf-8')

      for line in debianControlFileContents:
         match = re_debian_standards_version.match(line)
         if match is not None:
            debianNewControlFile.write('Standards-Version: ' + DEBIAN_STANDARDS_VERSION + '\n')
         else:
            debianNewControlFile.write(line)

      debianNewControlFile.close()


# ====== Update RPM spec file ===============================================
if ( (rpmSpecFileName is not None) and (rpmVersionString is not None) ):
   rpmSpecFileNew = open(rpmSpecFileName + '.new', 'w', encoding='utf-8')

   re_rpm_changelog = re.compile(r'^%changelog[ \t]*$')
   inChangeLog = False

   for line in rpmSpecFileContents:
      if inChangeLog == False:
         match = re_rpm_version.match(line)
         if match is not None:
            rpmSpecFileNew.write(match.group(1) + versionString + '\n')
         else:
            match = re_rpm_release.match(line)
            if match is not None:
               rpmSpecFileNew.write(match.group(1) + '1\n')
            else:
               match = re_rpm_changelog.match(line)
               if match is not None:
                  inChangeLog = True
               # ------ Copy line from original file ------------------------
               rpmSpecFileNew.write(line)

      # ------ Update ChangeLog ---------------------------------------------
      else:
         if versionExtra == '':
            now = time.strftime('%a %b %d %Y', time.localtime())
            rpmSpecFileNew.write('* ' + now + ' ' + packageMaintainer + ' - ' + versionString + '-1\n')
            rpmSpecFileNew.write('- New upstream release.\n')
         rpmSpecFileNew.write(line)
         inChangeLog = False

   rpmSpecFileNew.close()


# ====== Update FreeBSD ports Makefile ======================================
if ( (freeBSDMakefileName is not None) and (freeBSDVersionString is not None) ):
   freeBSDMakefileFileNew = open(freeBSDMakefileName + '.new', 'w', encoding='utf-8')

   re_rpm_changelog = re.compile(r'^%changelog[ \t]*$')
   inChangeLog = False

   for line in freeBSDMakefileFileContents:
      match = re_freebsd_version.match(line)
      if match is not None:
         freeBSDMakefileFileNew.write(match.group(1) + versionString + '\n')
      else:
         # ------ Copy line from original file ------------------------------
         freeBSDMakefileFileNew.write(line)

   freeBSDMakefileFileNew.close()


# ====== Update version file ================================================
if otherVersionString is not None:
   assert otherFileName is not None
   assert otherPackage is not None
   otherFileNew = open(otherFileName + '.new', 'w', encoding='utf-8')
   otherFileNew.write(otherPackage + ' ' +
                      str(versionMajor) + '.' + str(versionMinor) + '.' + str(versionPatch) + versionExtra + '\n')
   otherFileNew.close()


# ====== Check result =======================================================
if cmakeVersionString is not None:
   showDiff(cmakeFileName, cmakeFileName + '.new')
elif autoconfVersionString is not None:
   showDiff(autoconfFileName, autoconfFileName + '.new')

if debianVersionString is not None:
   showDiff(debianChangeLogFileName, debianChangeLogFileName + '.new')
   if debianStandardsVersion != DEBIAN_STANDARDS_VERSION:
      showDiff(debianControlFileName, debianControlFileName + '.new')
if rpmSpecFileName is not None:
   showDiff(rpmSpecFileName, rpmSpecFileName + '.new')
if freeBSDMakefileName is not None:
   showDiff(freeBSDMakefileName, freeBSDMakefileName + '.new')


# ====== Apply changes ======================================================
applyChanges = input('\x1b[34mApply update? [yes/no]?\x1b[0m ')
if ((applyChanges != 'yes') and (applyChanges != 'y')):
   sys.exit(0)

# ------ Update files -------------------------------------------------------
if cmakeVersionString is not None:
   applyUpdate(cmakeFileName + '.new', cmakeFileName)
elif autoconfVersionString is not None:
   applyUpdate(autoconfFileName + '.new', autoconfFileName)

if debianVersionString is not None:
   applyUpdate(debianChangeLogFileName + '.new', debianChangeLogFileName)
   if debianStandardsVersion != DEBIAN_STANDARDS_VERSION:
      applyUpdate(debianControlFileName + '.new', debianControlFileName)
if ( (rpmSpecFileName is not None) and (rpmVersionString is not None) ):
   applyUpdate(rpmSpecFileName + '.new', rpmSpecFileName)
if ( (freeBSDMakefileName is not None) and (freeBSDVersionString is not None) ):
   applyUpdate(freeBSDMakefileName + '.new', freeBSDMakefileName)
if otherVersionString is not None:
   applyUpdate(otherFileName + '.new', otherFileName)
sys.stdout.write('Done!\n')

# ------ Release version ----------------------------------------------------
if versionExtra == '':
   sys.stdout.write('New release!\n')

   # ------ Update changelog ------------------------------------------------
   pathname = os.path.dirname(sys.argv[0])
   if os.path.isfile("ChangeLog"):
      sys.stdout.write('Updating ChangeLog ...\n')
      subprocess.call('git log -v . >ChangeLog.new && mv ChangeLog.new ChangeLog', shell = True)

   # ------ Create Git tag --------------------------------------------------
   newGitTag = package + '-' + versionString
   applyChanges = input('\x1b[34mCommit and create signed Git tag ' + newGitTag + '? [yes/no]?\x1b[0m ')
   if ((applyChanges == 'yes') or (applyChanges == 'y')):
      subprocess.call([ 'git', 'clean', '-df' ])
      subprocess.call([ 'git', 'commit', '-a', '-m', 'New release ' + newGitTag + '.' ])
      subprocess.call([ 'git', 'tag', '-s', newGitTag, '-m', 'New release ' + newGitTag + '.' ])
