text: #!/usr/bin/env python3
  # -*- coding: utf-8 -*-
  
  """A utility script for automating the beets release process.
  """
  import click
  import os
  import re
  import subprocess
  from contextlib import contextmanager
  import datetime
  
  BASE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
  CHANGELOG = os.path.join(BASE, 'docs', 'changelog.rst')
  
  
  @contextmanager
  def chdir(d):
      """A context manager that temporary changes the working directory.
      """
      olddir = os.getcwd()
      os.chdir(d)
      yield
      os.chdir(olddir)
  
  
  @click.group()
  def release():
      pass
  
  
  # Locations (filenames and patterns) of the version number.
  VERSION_LOCS = [
      (
          os.path.join(BASE, 'beets', '__init__.py'),
          [
              (
                  r'__version__\s*=\s*u[\'"]([0-9\.]+)[\'"]',
                  "__version__ = u'{version}'",
              )
          ]
      ),
      (
          os.path.join(BASE, 'docs', 'conf.py'),
          [
              (
                  r'version\s*=\s*[\'"]([0-9\.]+)[\'"]',
                  "version = '{minor}'",
              ),
              (
                  r'release\s*=\s*[\'"]([0-9\.]+)[\'"]',
                  "release = '{version}'",
              ),
          ]
      ),
      (
          os.path.join(BASE, 'setup.py'),
          [
              (
                  r'\s*version\s*=\s*[\'"]([0-9\.]+)[\'"]',
                  "    version='{version}',",
              )
          ]
      ),
  ]
  
  GITHUB_USER = 'beetbox'
  GITHUB_REPO = 'beets'
  
  
  def bump_version(version):
      """Update the version number in setup.py, docs config, changelog,
      and root module.
      """
      version_parts = [int(p) for p in version.split('.')]
      assert len(version_parts) == 3, "invalid version number"
      minor = '{}.{}'.format(*version_parts)
      major = '{}'.format(*version_parts)
  
      # Replace the version each place where it lives.
      for filename, locations in VERSION_LOCS:
          # Read and transform the file.
          out_lines = []
          with open(filename) as f:
              found = False
              for line in f:
                  for pattern, template in locations:
                      match = re.match(pattern, line)
                      if match:
                          # Check that this version is actually newer.
                          old_version = match.group(1)
                          old_parts = [int(p) for p in old_version.split('.')]
                          assert version_parts > old_parts, \
                              "version must be newer than {}".format(
                                  old_version
                              )
  
                          # Insert the new version.
                          out_lines.append(template.format(
                              version=version,
                              major=major,
                              minor=minor,
                          ) + '\n')
  
                          found = True
                          break
  
                  else:
                      # Normal line.
                      out_lines.append(line)
  
              if not found:
                  print("No pattern found in {}".format(filename))
  
          # Write the file back.
          with open(filename, 'w') as f:
              f.write(''.join(out_lines))
  
      # Generate bits to insert into changelog.
      header_line = '{} (in development)'.format(version)
      header = '\n\n' + header_line + '\n' + '-' * len(header_line) + '\n\n'
      header += 'Changelog goes here!\n'
  
      # Insert into the right place.
      with open(CHANGELOG) as f:
          contents = f.read()
      location = contents.find('\n\n')  # First blank line.
      contents = contents[:location] + header + contents[location:]
  
      # Write back.
      with open(CHANGELOG, 'w') as f:
          f.write(contents)
  
  
  @release.command()
  @click.argument('version')
  def bump(version):
      """Bump the version number.
      """
      bump_version(version)
  
  
  def get_latest_changelog():
      """Extract the first section of the changelog.
      """
      started = False
      lines = []
      with open(CHANGELOG) as f:
          for line in f:
              if re.match(r'^--+$', line.strip()):
                  # Section boundary. Start or end.
                  if started:
                      # Remove last line, which is the header of the next
                      # section.
                      del lines[-1]
                      break
                  else:
                      started = True
  
              elif started:
                  lines.append(line)
      return ''.join(lines).strip()
  
  
  def rst2md(text):
      """Use Pandoc to convert text from ReST to Markdown.
      """
      pandoc = subprocess.Popen(
          ['pandoc', '--from=rst', '--to=markdown', '--no-wrap'],
          stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE
      )
      stdout, _ = pandoc.communicate(text.encode('utf-8'))
      md = stdout.decode('utf-8').strip()
  
      # Fix up odd spacing in lists.
      return re.sub(r'^-   ', '- ', md, flags=re.M)
  
  
  def changelog_as_markdown():
      """Get the latest changelog entry as hacked up Markdown.
      """
      rst = get_latest_changelog()
  
      # Replace plugin links with plugin names.
      rst = re.sub(r':doc:`/plugins/(\w+)`', r'``\1``', rst)
  
      # References with text.
      rst = re.sub(r':ref:`([^<]+)(<[^>]+>)`', r'\1', rst)
  
      # Other backslashes with verbatim ranges.
      rst = re.sub(r'(\s)`([^`]+)`([^_])', r'\1``\2``\3', rst)
  
      # Command links with command names.
      rst = re.sub(r':ref:`(\w+)-cmd`', r'``\1``', rst)
  
      # Bug numbers.
      rst = re.sub(r':bug:`(\d+)`', r'#\1', rst)
  
      # Users.
      rst = re.sub(r':user:`(\w+)`', r'@\1', rst)
  
      # Convert with Pandoc.
      md = rst2md(rst)
  
      # Restore escaped issue numbers.
      md = re.sub(r'\\#(\d+)\b', r'#\1', md)
  
      return md
  
  
  @release.command()
  def changelog():
      """Get the most recent version's changelog as Markdown.
      """
      print(changelog_as_markdown())
  
  
  def get_version(index=0):
      """Read the current version from the changelog.
      """
      with open(CHANGELOG) as f:
          cur_index = 0
          for line in f:
              match = re.search(r'^\d+\.\d+\.\d+', line)
              if match:
                  if cur_index == index:
                      return match.group(0)
                  else:
                      cur_index += 1
  
  
  @release.command()
  def version():
      """Display the current version.
      """
      print(get_version())
  
  
  @release.command()
  def datestamp():
      """Enter today's date as the release date in the changelog.
      """
      dt = datetime.datetime.now()
      stamp = '({} {}, {})'.format(dt.strftime('%B'), dt.day, dt.year)
      marker = '(in development)'
  
      lines = []
      underline_length = None
      with open(CHANGELOG) as f:
          for line in f:
              if marker in line:
                  # The header line.
                  line = line.replace(marker, stamp)
                  lines.append(line)
                  underline_length = len(line.strip())
              elif underline_length:
                  # This is the line after the header. Rewrite the dashes.
                  lines.append('-' * underline_length + '\n')
                  underline_length = None
              else:
                  lines.append(line)
  
      with open(CHANGELOG, 'w') as f:
          for line in lines:
              f.write(line)
  
  
  @release.command()
  def prep():
      """Run all steps to prepare a release.
  
      - Tag the commit.
      - Build the sdist package.
      - Generate the Markdown changelog to ``changelog.md``.
      - Bump the version number to the next version.
      """
      cur_version = get_version()
  
      # Tag.
      subprocess.check_output(['git', 'tag', 'v{}'.format(cur_version)])
  
      # Build.
      with chdir(BASE):
          subprocess.check_call(['python2', 'setup.py', 'sdist'])
  
      # Generate Markdown changelog.
      cl = changelog_as_markdown()
      with open(os.path.join(BASE, 'changelog.md'), 'w') as f:
          f.write(cl)
  
      # Version number bump.
      # FIXME It should be possible to specify this as an argument.
      version_parts = [int(n) for n in cur_version.split('.')]
      version_parts[-1] += 1
      next_version = u'.'.join(map(str, version_parts))
      bump_version(next_version)
  
  
  @release.command()
  def publish():
      """Unleash a release unto the world.
  
      - Push the tag to GitHub.
      - Upload to PyPI.
      """
      version = get_version(1)
  
      # Push to GitHub.
      with chdir(BASE):
          subprocess.check_call(['git', 'push'])
          subprocess.check_call(['git', 'push', '--tags'])
  
      # Upload to PyPI.
      path = os.path.join(BASE, 'dist', 'beets-{}.tar.gz'.format(version))
      subprocess.check_call(['twine', 'upload', path])
  
  
  @release.command()
  def ghrelease():
      """Create a GitHub release using the `github-release` command-line
      tool.
  
      Reads the changelog to upload from `changelog.md`. Uploads the
      tarball from the `dist` directory.
      """
      version = get_version(1)
      tag = 'v' + version
  
      # Load the changelog.
      with open(os.path.join(BASE, 'changelog.md')) as f:
          cl_md = f.read()
  
      # Create the release.
      subprocess.check_call([
          'github-release', 'release',
          '-u', GITHUB_USER, '-r', GITHUB_REPO,
          '--tag', tag,
          '--name', '{} {}'.format(GITHUB_REPO, version),
          '--description', cl_md,
      ])
  
      # Attach the release tarball.
      tarball = os.path.join(BASE, 'dist', 'beets-{}.tar.gz'.format(version))
      subprocess.check_call([
          'github-release', 'upload',
          '-u', GITHUB_USER, '-r', GITHUB_REPO,
          '--tag', tag,
          '--name', os.path.basename(tarball),
          '--file', tarball,
      ])
  
  
  if __name__ == '__main__':
      release()
  