# This file is part of Buildbot.  Buildbot 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, version 2.
#
# 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, write to the Free Software Foundation, Inc., 51
# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# Copyright Buildbot Team Members

import json
import os
import random
import re
import shlex
import string
import sys
import time

from twisted.cred import credentials
from twisted.internet import defer
from twisted.internet import protocol
from twisted.internet import reactor
from twisted.internet import task
from twisted.internet import utils
from twisted.python import log
from twisted.python import runtime
from twisted.python.procutils import which
from twisted.spread import pb

from buildbot.process.results import SUCCESS
from buildbot.process.results import Results
from buildbot.util import bytes2unicode
from buildbot.util import now
from buildbot.util import unicode2bytes
from buildbot.util.eventual import fireEventually


class SourceStamp:

    def __init__(self, branch, revision, patch, repository=''):
        self.branch = branch
        self.revision = revision
        self.patch = patch
        self.repository = repository


def output(*msg):
    print(' '.join([str(m)for m in msg]))


class SourceStampExtractor:

    def __init__(self, treetop, branch, repository):
        self.treetop = treetop
        self.repository = repository
        self.branch = branch
        exes = which(self.vcexe)
        if not exes:
            output("Could not find executable '{}'.".format(self.vcexe))
            sys.exit(1)
        self.exe = exes[0]

    def dovc(self, cmd):
        """This accepts the arguments of a command, without the actual
        command itself."""
        env = os.environ.copy()
        env['LC_ALL'] = "C"
        d = utils.getProcessOutputAndValue(self.exe, cmd, env=env,
                                           path=self.treetop)
        d.addCallback(self._didvc, cmd)
        return d

    def _didvc(self, res, cmd):
        (stdout, stderr, code) = res
        # 'bzr diff' sets rc=1 if there were any differences.
        # cvs does something similar, so don't bother requiring rc=0.
        return stdout

    def get(self):
        """Return a Deferred that fires with a SourceStamp instance."""
        d = self.getBaseRevision()
        d.addCallback(self.getPatch)
        d.addCallback(self.done)
        return d

    def readPatch(self, diff, patchlevel):
        if not diff:
            diff = None
        self.patch = (patchlevel, bytes2unicode(diff))

    def done(self, res):
        if not self.repository:
            self.repository = self.treetop
        # TODO: figure out the branch and project too
        ss = SourceStamp(bytes2unicode(self.branch), self.baserev, self.patch,
                         repository=self.repository)
        return ss


class CVSExtractor(SourceStampExtractor):
    patchlevel = 0
    vcexe = "cvs"

    def getBaseRevision(self):
        # this depends upon our local clock and the repository's clock being
        # reasonably synchronized with each other. We express everything in
        # UTC because the '%z' format specifier for strftime doesn't always
        # work.
        self.baserev = time.strftime("%Y-%m-%d %H:%M:%S +0000",
                                     time.gmtime(now()))
        return defer.succeed(None)

    def getPatch(self, res):
        # the -q tells CVS to not announce each directory as it works
        if self.branch is not None:
            # 'cvs diff' won't take both -r and -D at the same time (it
            # ignores the -r). As best I can tell, there is no way to make
            # cvs give you a diff relative to a timestamp on the non-trunk
            # branch. A bare 'cvs diff' will tell you about the changes
      
      # relative to your checked-out versions, but I know of no way to
            # find out what those checked-out versions are.
            output("Sorry, CVS 'try' builds don't w
ork with branches")
            sys.exit(1)
        args = ['-q', 'diff', '-u', '-D', self.baserev]
        d = self.dovc(args)
        d.addCallback(self.readPatch, self.patchlevel)
        return d


class SVNExtractor(SourceStampExtractor):
    patchlevel = 0
    vcexe = "svn"

    def getBaseRevision(self):
        d = self.dovc(["status", "-u"])
        d.addCallback(self.parseStatus)
        return d

    def parseStatus(self, res):
        # svn shows the base revision for each file that has been modified or
        # which needs an update. You can update each file to a different
        # version, so each file is displayed with its individual base
        # revision. It also shows the repository-wide latest revision number
        # on the last line ("Status against revision: \d+").

        # for our purposes, we use the latest revision number as the "base"
        # revision, and get a diff against that. This means we will get
        # reverse-diffs for local files that need updating, but the resulting
        # tree will still be correct. The only weirdness is that the baserev
        # that we emit may be different than the version of the tree that we
        # first checked out.

        # to do this differently would probably involve scanning the revision
        # numbers to find the max (or perhaps the min) revision, and then
        # using that as a base.

        for line in res.split(b"\n"):
            m = re.search(br'^Status against revision:\s+(\d+)', line)
            if m:
                self.baserev = m.group(1)
                return
        output(
            b"Could not find 'Status against revision' in SVN output: " + res)
        sys.exit(1)

    def getPatch(self, res):
        d = self.dovc(["diff", "-r{}".format(self.baserev)])
        d.addCallback(self.readPatch, self.patchlevel)
        return d


class BzrExtractor(SourceStampExtractor):
    patchlevel = 0
    vcexe = "bzr"

    def getBaseRevision(self):
        d = self.dovc(["revision-info", "-rsubmit:"])
        d.addCallback(self.get_revision_number)
        return d

    def get_revision_number(self, out):
        revno, revid = out.split()
        self.baserev = 'revid:' + revid
        return

    def getPatch(self, res):
        d = self.dovc(["diff", "-r{}..".format(self.baserev)])
        d.addCallback(self.readPatch, self.patchlevel)
        return d


class MercurialExtractor(SourceStampExtractor):
    patchlevel = 1
    vcexe = "hg"

    def _didvc(self, res, cmd):
        (stdout, stderr, code) = res

        if code:
            cs = ' '.join(['hg'] + cmd)
            if stderr:
                stderr = '\n' + stderr.rstrip()
            raise RuntimeError("{} returned {} {}".format(cs, code, stderr))

        return stdout

    @defer.inlineCallbacks
    def getBaseRevision(self):
        upstream = ""
        if self.repository:
            upstream = "r'{}'".format(self.repository)
        output = ''
        try:
            output = yield self.dovc(["log", "--template", "{node}\\n", "-r",
                                      "max(::. - outgoing({}))".format(upstream)])
        except RuntimeError:
            # outgoing() will abort if no default-push/default path is
            # configured
            if upstream:
                raise
            # fall back to current working directory parent
            output = yield self.dovc(["log", "--template", "{node}\\n", "-r", "p1()"])
        m = re.search(br'^(\w+)', output)
        if not m:
            raise RuntimeError(
                "Revision {!r} is not in the right format".format(output))
        self.baserev = m.group(0)

    def getPatch(self, res):
        d = self.dovc(["diff", "-r", self.baserev])
        