The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.
#!/usr/bin/env python
#
# ====================================================================
#    Licensed to the Apache Software Foundation (ASF) under one
#    or more contributor license agreements.  See the NOTICE file
#    distributed with this work for additional information
#    regarding copyright ownership.  The ASF licenses this file
#    to you under the Apache License, Version 2.0 (the
#    "License"); you may not use this file except in compliance
#    with the License.  You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing,
#    software distributed under the License is distributed on an
#    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
#    KIND, either express or implied.  See the License for the
#    specific language governing permissions and limitations
#    under the License.
# ====================================================================

# TODO: if this was part of core subversion, we'd have all sorts of nifty
#       checks, and could use a lot of existing code.

import os
import re
import sys
import shutil
import sqlite3


def usage():
  print("""usage: %s WC_SRC TARGET

Detatch the working copy subdirectory given by WC_SRC to TARGET.  This is
equivalent to copying WC_SRC to TARGET, but it inserts a new set of Subversion
metadata into TARGET/.svn, making TARGET a proper independent working copy.
""" % sys.argv[0])
  sys.exit(1)


def find_wcroot(wcdir):
  wcroot = os.path.abspath(wcdir)
  old_wcroot = ''
  while wcroot != old_wcroot:
    if os.path.exists(os.path.join(wcroot, '.svn', 'wc.db')):
      return wcroot

    old_wcroot = wcroot
    wcroot = os.path.dirname(wcroot)

  return None


def  migrate_sqlite(wc_src, target, wcroot):
  src_conn = sqlite3.connect(os.path.join(wcroot, '.svn', 'wc.db'))
  dst_conn = sqlite3.connect(os.path.join(target, '.svn', 'wc.db'))

  local_relsrc = os.path.relpath(wc_src, wcroot)

  src_c = src_conn.cursor()
  dst_c = dst_conn.cursor()

  # We're only going to attempt this if there are no locks or work queue
  # items in the source database
  ### This could probably be tightened up, but for now this suffices
  src_c.execute('select count(*) from wc_lock')
  count = int(src_c.fetchone()[0])
  assert count == 0

  src_c.execute('select count(*) from work_queue')
  count = int(src_c.fetchone()[0])
  assert count == 0

  # Copy over the schema
  src_c.execute('pragma user_version')
  user_version = src_c.fetchone()[0]
  # We only know how to handle format 29 working copies
  assert user_version == 29
  ### For some reason, sqlite doesn't like to parameterize the pragma statement
  dst_c.execute('pragma user_version = %d' % user_version)

  src_c.execute('select name, sql from sqlite_master')
  for row in src_c:
    if not row[0].startswith('sqlite_'):
      dst_c.execute(row[1])

  # Insert wcroot row
  dst_c.execute('insert into wcroot (id, local_abspath) values (?, ?)',
                (1, None))

  # Copy repositories rows
  ### Perhaps prune the repositories based upon the new NODES set?
  src_c.execute('select * from repository')
  for row in src_c:
    dst_c.execute('insert into repository values (?, ?, ?)',
                  row)

  # Copy the root node
  src_c.execute('select * from nodes where local_relpath = ?',
                (local_relsrc,))
  row = list(src_c.fetchone())
  row[1] = ''
  row[3] = None
  dst_c.execute('''insert into nodes values
                  (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
                   ?, ?, ?, ?, ?, ?, ?, ?)''', row)

  # Copy children nodes rows
  src_c.execute('select * from nodes where local_relpath like ?',
                (local_relsrc + '/%', ))
  for row in src_c:
    row = list(row)
    row[1] = row[1][len(local_relsrc) + 1:]
    row[3] = row[3][len(local_relsrc) + 1:]
    dst_c.execute('''insert into nodes values
                  (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
                   ?, ?, ?, ?, ?, ?, ?, ?)''',
                  row)

  # Copy root actual_node
  src_c.execute('select * from actual_node where local_relpath = ?',
                (local_relsrc, ))
  row = src_c.fetchone()
  if row:
    row = list(row)
    row[1] = ''
    row[2] = None
    dst_c.execute('''insert into actual_node values
                     (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''', row)

  src_c.execute('select * from actual_node where local_relpath like ?',
                (local_relsrc + '/%', ))
  for row in src_c:
    row = list(row)
    row[1] = row[1][len(local_relsrc) + 1:]
    row[2] = row[2][len(local_relsrc) + 1:]
    dst_c.execute('''insert into actual_node values
                     (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''', row)

  # Hard to know which locks we care about, so just copy 'em all (there aren't
  # likely to be many)
  src_c.execute('select * from lock')
  for row in src_c:
    dst_c.execute('insert into locks values (?, ?, ?, ?, ?, ?)', row)

  # EXTERNALS
  src_c.execute('select * from externals where local_relpath = ?',
                (local_relsrc, ))
  row = src_c.fetchone()
  if row:
    row = list(row)
    row[1] = ''
    row[2] = None
    dst_c.execute('''insert into externals values
                     (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''', row)

  src_c.execute('select * from externals where local_relpath like ?',
                (local_relsrc + '/%', ))
  for row in src_c:
    row = list(row)
    row[1] = row[1][len(local_relsrc) + 1:]
    row[2] = row[2][len(local_relsrc) + 1:]
    dst_c.execute('''insert into externals values
                     (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''', row)

  dst_conn.commit()
  src_conn.close()
  dst_conn.close()


def migrate_pristines(wc_src, target, wcroot):
  src_conn = sqlite3.connect(os.path.join(wcroot, '.svn', 'wc.db'))
  dst_conn = sqlite3.connect(os.path.join(target, '.svn', 'wc.db'))

  src_c = src_conn.cursor()
  dst_c = dst_conn.cursor()

  regex = re.compile('\$((?:md5 *)|(?:sha1))\$(.*)')
  src_proot = os.path.join(wcroot, '.svn', 'pristine')
  target_proot = os.path.join(target, '.svn', 'pristine')

  checksums = {}

  # Grab anything which needs a pristine
  src_c.execute('''select checksum from nodes
                   union
                   select older_checksum from actual_node
                   union
                   select left_checksum from actual_node
                   union
                   select right_checksum from actual_node''')
  for row in src_c:
    if row[0]:
      match = regex.match(row[0])
      assert match

      pristine = match.group(2)
      if pristine in checksums:
        checksums[pristine] += 1
      else:
        checksums[pristine] = 1

  for pristine, count in checksums.items():
    # Copy the pristines themselves over
    pdir = os.path.join(target_proot, pristine[0:2])
    if not os.path.exists(pdir):
      os.mkdir(pdir)
    path = os.path.join(pristine[0:2], pristine + '.svn-base')
    if os.path.exists(os.path.join(target_proot, path)):
      dst_c.execute
    else:
      shutil.copy2(os.path.join(src_proot, path),
                   os.path.join(target_proot, path))

    src_c.execute('select size, md5_checksum from pristine where checksum=?',
                  ('$sha1$' + pristine, ) )
    (size, md5) = src_c.fetchone()

    # Insert a db row for the pristine
    dst_c.execute('insert into pristine values (?, NULL, ?, ?, ?)',
                  ('$sha1$' + pristine, size, count, md5))

  dst_conn.commit()
  src_conn.close()
  dst_conn.close()


def migrate_metadata(wc_src, target, wcroot):
  # Make paths
  os.mkdir(os.path.join(target, '.svn'))
  os.mkdir(os.path.join(target, '.svn', 'tmp'))
  os.mkdir(os.path.join(target, '.svn', 'pristine'))
  open(os.path.join(target, '.svn', 'format'), 'w').write('12')
  open(os.path.join(target, '.svn', 'entries'), 'w').write('12')

  # Two major bits: sqlite data and pristines
  migrate_sqlite(wc_src, os.path.abspath(target), wcroot)
  migrate_pristines(wc_src, target, wcroot)


def main():
  if len(sys.argv) < 3:
    usage()

  wc_src = os.path.normpath(sys.argv[1])
  if not os.path.isdir(wc_src):
    print("%s does not exist or is not a directory" % wc_src)
    sys.exit(1)

  target = os.path.normpath(sys.argv[2])
  if os.path.exists(target):
    print("Target '%s' already exists" % target)
    sys.exit(1)

  wcroot = find_wcroot(wc_src)
  if not wcroot:
    print("'%s' is not part of a working copy" % wc_src)
    sys.exit(1)

  # Use the OS to copy the subdirectory over to the target
  shutil.copytree(wc_src, target)

  # Now migrate the worky copy data
  migrate_metadata(wc_src, target, wcroot)


if __name__ == '__main__':
  raise Exception("""This script is unfinished and not ready to be used on live data.
    Trust us.""")
  main()