# CVS support

# Copyright (c) 2020-2023 Andreas Gustafsson.  All rights reserved.
# Please refer to the file COPYRIGHT for detailed copyright information.

from __future__ import print_function

import os
import re
import sys
import html

import bracket
from bracket import branch_name, cno2ts, config, config_fn, config_get, db_put, \
    format_time_delta, get_dates, libdir, libpydir, lock, repo_lock_fn, runv, \
    ts2cno_loose, remove_email
from bracket import Commit, NoCommitAtTimestamp, RepositoryUpdateFailed
from utils import rcs2ts, read_file, ts2cvs, ts2rcs, mkdir_p
from report import link_if

### Begin private functions

# Read a file containing a hex-encoded integer per line.  Return an array.

def read_hex_file(fn):
    f = open(fn, 'r')
    r = [int(s.rstrip(), 16) for s in f.readlines()]
    f.close()
    return r

# tag is "dates" or "offsets"

def read_hex_file_2(tag):
    fn = os.path.join(config['db_dir'], tag + '.' + branch_name(config)+ '.hex')
    a = read_hex_file(fn)
    if len(a) == 0:
        raise RuntimeError("%s is empty, this can't be right" % fn)
    return a

### Begin vc API

def default_branch():
    return 'HEAD'

def setup():
    mkdir_p(os.path.join(config['repo_root'], 'CVSROOT'))
    # Enable local keyword expansion
    cvs_config_fn = os.path.join(config['repo_root'], 'CVSROOT/config')
    if not os.path.exists(cvs_config_fn):
        with open(cvs_config_fn) as f:
            print("tag=NetBSD", file = f)

def update_repo_module(module):
    # Possiby retry to work around the issue of systems ticket
    # "[NetBSD.org #160795] rsync from anoncvs fails repeatedly"
    while True:
        r = runv(['/bin/sh',
                  os.path.join(libpydir, 'vc/xcvs/update-repo.sh'),
                  module],
                 verbose = False)
        if r != 0:
            # rsync failed
            if not int(config_get('update_repo_retry', '0')):
                raise RepositoryUpdateFailed()
            print('retry...', end=' ')
            sys.stdout.flush()
        else:
            # rsync succeeded
            break

def index_repo():
    r = runv(['/bin/sh', os.path.join(libpydir, 'vc/xcvs/index-repo.sh'),
              os.path.abspath(config_fn)],
             verbose = False)
    if r != 0:
        raise RepositoryUpdateFailed()

def read_dates():
    global offsets
    bracket.dates = read_hex_file_2('dates')
    offsets = read_hex_file_2('offsets')

def checkout(branch, module, ts, builddir, logfd):
    def run_cvs(args):
        return runv([config['cvs'], '-d', config['repo_root']] + args,
                    verbose = False,
                    cwd = builddir, stdout = logfd, stderr = logfd)
    if branch == 'HEAD':
        status = run_cvs(['checkout', '-D', ts2cvs(ts)] + [module])
    else:
        if False:
            # https://stackoverflow.com/questions/32547990/cvs-check-out-branch-at-specific-date
            # https://gcc.gnu.org/ml/gcc/2002-12/msg01341.html
            db_put(ts, 'cvs_checkout_strategy', 'gcc_merge')
            branchpoint = branch + '-base'
            status = \
                run_cvs(['checkout', '-r', branchpoint, module]) + \
                run_cvs(['update', '-j' + branchpoint, '-j' + branch + ':' + ts2cvs(ts)])
        else:
            # Per the CVS documentation
            db_put(ts, 'cvs_checkout_strategy', 'cvs_manpage')
            status = run_cvs(['checkout', '-r', branch + ':' + ts2cvs(ts), module])

    return status

def last_safe_commit_ts():
    dates = get_dates()
    cno = len(dates) - 1
    rsync_begin_ts = int(read_file(os.path.join(config['repo_root'], 'rsync_begin_stamp'), mode = 't'))
    margin = int(config_get('rsync_margin', '1200'))
    while True:
        ts = cno2ts(cno)
        delta = ts - rsync_begin_ts
        if delta < -margin:
            print("using commit %i from %s (%s start of rsync)" % (cno, ts2rcs(ts), format_time_delta(delta)))
            return ts
        print("ignoring commit %i from %s (%s start of rsync)" % (cno, ts2rcs(ts), format_time_delta(delta)))
        cno = cno - 1
    raise RuntimeError("last_safe_commit_ts found no safe commit")

def get_commits(ts0, ts1):
    global offsets

    # Get the dates, offsets, and commits all under the same lock
    # for consistency
    repo_lock = lock(repo_lock_fn(), exclusive = False, verbose = True)

    bracket.read_dates(do_lock = False)

    try:
        cno = ts2cno_loose(ts0)
        if cno + 1 >= len(offsets):
            return []
        offset = offsets[cno + 1]
    except NoCommitAtTimestamp:
        print("warning: no commit at timestamp")
        offset = 0

    r0 = ts2rcs(ts0)
    r1 = ts2rcs(ts1)

    fn = os.path.join(config['db_dir'], 'commits.' + branch_name(config))
    f = open(fn, 'r')
    f.seek(offset)
    commits = []
    # Regexp to split the line into fields, dealing with embedded
    # spaces in the filename only
    pattern = re.compile(r'([^ ]+) ([^ ]+) (.+) ([^ ]+)')
    for line in f:
        try:
            match = re.match(pattern, line.rstrip())
            tuple = match.groups()
            (date, committer, file, rev) = tuple
        except Exception as e:
            print("could not parse %s line '%s': %s" % (fn, line, str(e)), file=sys.stderr)
            raise
        if not (date > r0):
             print("warning: get_commits expected", date, ">", r0)
             continue
        #assert(date > r0)
        if date > r1:
            break
        commit = Commit()
        commit.timestamp = rcs2ts(date)
        commit.committer = committer
        # strip the ,v extension
        file_no_v = re.sub(r',v$', r'', file)
        commit.files = [file_no_v]
        commit.revision = rev
        commits.append(commit)
    f.close()
    del repo_lock
    return commits

def format_commit_html(c):
    url = 'http://cvsweb.netbsd.org/bsdweb.cgi/' + c.files[0] + '#rev' + c.revision
    return ' '.join([
        'commit',
        ts2rcs(c.timestamp),
        html.escape(remove_email(c.committer)),
        link_if(url, url, c.files[0] + " " + c.revision)
    ])

def format_commit_email(c):
    return "    " + " ".join([ts2rcs(c.timestamp), remove_email(c.committer), c.files[0], c.revision]) + "\n"
