1# -*- mode: python; coding: utf-8 -*-
2# :Progetto: vcpx -- Dual working directory
3# :Creato:   dom 20 giu 2004 11:02:01 CEST
4# :Autore:   Lele Gaifax <lele@nautilus.homeip.net>
5# :Licenza:  GNU General Public License
6#
7
8"""
9The easiest way to propagate changes from one VC control system to one
10of an another kind is having a single directory containing a live
11working copy shared between the two VC systems.
12
13In a slightly more elaborated way, the source and the target system may
14use separate directories, that gets rsynced when needed.
15
16This module implements `DualWorkingDir`, which instances have a
17`source` and `target` properties offering the right capabilities to do
18the job.
19"""
20from __future__ import absolute_import
21
22__docformat__ = 'reStructuredText'
23
24from .source import UpdatableSourceWorkingDir, InvocationError
25from .target import SynchronizableTargetWorkingDir
26from .shwrap import ExternalCommand
27from datetime import datetime
28
29IGNORED_METADIRS = []
30
31class DualWorkingDir(UpdatableSourceWorkingDir, SynchronizableTargetWorkingDir):
32    """
33    Dual working directory, one that is under two different VC systems at
34    the same time.
35
36    This class reimplements the two interfaces, dispatching the right method
37    to the right backend.
38    """
39
40    def __init__(self, source_repo, target_repo):
41        global IGNORED_METADIRS
42        from os.path import sep
43
44        self.source = source_repo.workingDir()
45        self.target = target_repo.workingDir()
46
47        sbdir = self.source.repository.basedir.rstrip(sep)+sep
48        tbdir = self.target.repository.basedir.rstrip(sep)+sep
49        if sbdir == tbdir:
50            shared = True
51        elif tbdir.startswith(sbdir):
52            raise InvocationError('Target base directory "%s" cannot be a '
53                                  'subdirectory of source directory "%s"' %(
54                (tbdir, sbdir)))
55        elif sbdir.startswith(tbdir):
56            shared = True
57        else:
58            shared = False
59        self.shared_basedirs = shared
60        self.source.shared_basedirs = shared
61        self.target.shared_basedirs = shared
62
63        IGNORED_METADIRS = [_f for _f in [source_repo.METADIR,
64                                         target_repo.METADIR] if _f]
65        IGNORED_METADIRS.extend(source_repo.EXTRA_METADIRS)
66        IGNORED_METADIRS.extend(target_repo.EXTRA_METADIRS)
67
68        self.source.prepareSourceRepository()
69        self.target.prepareTargetRepository()
70
71        # UpdatableSourceWorkingDir
72
73        self.getPendingChangesets = self.source.getPendingChangesets
74        self.checkoutUpstreamRevision = self.source.checkoutUpstreamRevision
75
76        # SynchronizableTargetWorkingDir
77
78        self.prepareWorkingDirectory = self.target.prepareWorkingDirectory
79
80    def setStateFile(self, state_file):
81        """
82        Set the state file used to store the revision and pending changesets.
83        """
84
85        self.source.setStateFile(state_file)
86        self.target.setStateFile(state_file)
87
88    def setLogfile(self, logfile):
89        """
90        Set the name of the logfile, just to ignore it.
91        """
92
93        self.target.logfile = logfile
94
95    def applyPendingChangesets(self, applyable=None, replay=None, applied=None):
96        def pre_replay(changeset):
97            if applyable and not applyable(changeset):
98                return
99            return self.target._prepareToReplayChangeset(changeset)
100
101        return self.source.applyPendingChangesets(replay=self.replayChangeset,
102                                                  applyable=pre_replay,
103                                                  applied=applied)
104
105    def importFirstRevision(self, source_repo, changeset, initial):
106        if not self.shared_basedirs:
107            self._syncTargetWithSource()
108        self.target.importFirstRevision(source_repo, changeset, initial)
109
110    def replayChangeset(self, changeset):
111        if not self.shared_basedirs:
112            self._saveRenamedTargets(changeset)
113            self._syncTargetWithSource()
114        self.target.replayChangeset(changeset)
115
116    def _syncTargetWithSource(self):
117        cmd = ['rsync', '--archive']
118        now = datetime.now()
119        if hasattr(self, '_last_rsync'):
120            last = self._last_rsync
121            if not (now-last).seconds:
122                cmd.append('--ignore-times')
123        self._last_rsync = now
124        for M in IGNORED_METADIRS:
125            cmd.extend(['--exclude', M])
126
127        rsync = ExternalCommand(command=cmd)
128        rsync.execute(self.source.repository.basedir+'/', self.target.repository.basedir)
129
130    def _saveRenamedTargets(self, changeset):
131        """
132        Save old names from `rename`, before rsync replace it with new file.
133        """
134
135        from os.path import join, exists
136        from os import rename
137
138        for e in changeset.entries:
139            if e.action_kind == e.RENAMED:
140                absold = join(self.target.repository.basedir, e.old_name)
141                if exists(absold):
142                    rename(absold, absold + '-TAILOR-HACKED-OLD-NAME')
143