summaryrefslogtreecommitdiff
path: root/morphlib/sysbranchdir.py
blob: b8953c2faebbbef58853093c64c1d3232a2de33e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
# Copyright (C) 2013-2014  Codethink Limited
#
# This program 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 of the License.
#
# 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.
#
# =*= License: GPL-2 =*=


import cliapp
import os
import urlparse
import uuid

import morphlib


class SystemBranchDirectoryAlreadyExists(morphlib.Error):

    def __init__(self, root_directory):
        self.msg = (
            "%s: File exists" % 
            root_directory)


class NotInSystemBranch(morphlib.Error):

    def __init__(self, dirname):
        self.msg = (
            "Can't find the system branch directory.\n"
            "Morph must be built and deployed within "
            "the system branch checkout.")


class SystemBranchDirectory(object):

    '''A directory containing a checked out system branch.'''

    def __init__(self,
        root_directory, root_repository_url, system_branch_name):
        self.root_directory = os.path.abspath(root_directory)
        self.root_repository_url = root_repository_url
        self.system_branch_name = system_branch_name

    @property
    def _magic_path(self):
        return os.path.join(self.root_directory, '.morph-system-branch')

    @property
    def _config_path(self):
        return os.path.join(self._magic_path, 'config')

    def set_config(self, key, value):
        '''Set a configuration key/value pair.'''
        cliapp.runcmd(['git', 'config', '-f', self._config_path, key, value])

    def get_config(self, key):
        '''Get a configuration value for a given key.'''
        value = cliapp.runcmd(['git', 'config', '-f', self._config_path, key])
        return value.strip()

    def get_git_directory_name(self, repo_url):
        '''Return directory pathname for a given git repository.

        If the URL is a real one (not aliased), the schema and leading //
        are removed from it, as is a .git suffix.

        Any colons in the URL path or network location are replaced
        with slashes, so that directory paths do not contain colons.
        This avoids problems with PYTHONPATH, PATH, and other things
        that use colon as a separator.

        '''

        # Parse the URL. If the path component is absolute, we assume
        # it's a real URL; otherwise, an aliased URL.
        parts = urlparse.urlparse(repo_url)

        if os.path.isabs(parts.path):
            # Remove .git suffix, if any.
            path = parts.path
            if path.endswith('.git'):
                path = path[:-len('.git')]

            # Add the domain name etc (netloc). Ignore any other parts.
            # Note that we _know_ the path starts with a slash, so we avoid
            # adding one here.
            relative = '%s%s' % (parts.netloc, path)
        else:
            relative = repo_url

        # Replace colons with slashes.
        relative = '/'.join(relative.split(':'))

        # Remove anyleading slashes, or os.path.join below will only
        # use the relative part (since it's absolute, not relative).
        relative = relative.lstrip('/')

        return os.path.join(self.root_directory, relative)

    def get_filename(self, repo_url, relative):
        '''Return full pathname to a file in a checked out repository.

        This is a convenience function.

        '''

        return os.path.join(self.get_git_directory_name(repo_url), relative)

    def clone_cached_repo(self, cached_repo, checkout_ref):
        '''Clone a cached git repository into the system branch directory.

        The cloned repository will NOT have the system branch's git branch
        checked out: instead, checkout_ref is checked out (this is for
        backwards compatibility with older implementation of "morph
        branch"; it may change later). The system branch's git branch
        is NOT created: the caller will need to do that. Submodules are
        NOT checked out.

        The "origin" remote will be set to follow the cached repository's
        upstream. Remotes are not updated.

        '''

        # Do the clone.
        dirname = self.get_git_directory_name(cached_repo.original_name)
        gd = morphlib.gitdir.clone_from_cached_repo(
            cached_repo, dirname, checkout_ref)

        # Remember the repo name we cloned from in order to be able
        # to identify the repo again later using the same name, even
        # if the user happens to rename the directory.
        gd.set_config('morph.repository', cached_repo.original_name)

        # Create a UUID for the clone. We will use this for naming
        # temporary refs, e.g. for building.
        gd.set_config('morph.uuid', uuid.uuid4().hex)

        # Configure the "origin" remote to use the upstream git repository,
        # and not the locally cached copy.
        resolver = morphlib.repoaliasresolver.RepoAliasResolver(
            cached_repo.app.settings['repo-alias'])
        remote = gd.get_remote('origin')
        remote.set_fetch_url(resolver.pull_url(cached_repo.url))
        gd.set_config(
            'url.%s.pushInsteadOf' %
                resolver.push_url(cached_repo.original_name),
            resolver.pull_url(cached_repo.url))

        return gd

    def list_git_directories(self):
        '''List all git directories in a system branch directory.

        The list will contain zero or more GitDirectory objects.

        '''

        return (morphlib.gitdir.GitDirectory(dirname)
                for dirname in
                morphlib.util.find_leaves(self.root_directory, '.git'))

    # Not covered by unit tests, since testing the functionality spans
    # multiple modules and only tests useful output with a full system
    # branch, so it is instead covered by integration tests.
    def load_all_morphologies(self, loader): # pragma: no cover
        gd_name = self.get_git_directory_name(self.root_repository_url)
        gd = morphlib.gitdir.GitDirectory(gd_name)
        mf = morphlib.morphologyfinder.MorphologyFinder(gd)
        for filename in (f for f in mf.list_morphologies()
                         if not gd.is_symlink(f)):
            text = mf.read_morphology(filename)
            m = loader.load_from_string(text, filename=filename)
            m.repo_url = self.root_repository_url
            m.ref = self.system_branch_name
            yield m


def create(root_directory, root_repository_url, system_branch_name):
    '''Create a new system branch directory on disk.

    Return a SystemBranchDirectory object that represents the directory.

    The directory MUST NOT exist already. If it does,
    SystemBranchDirectoryAlreadyExists is raised.

    Note that this does NOT check out the root repository, or do any
    other git cloning.

    '''

    if os.path.exists(root_directory):
        raise SystemBranchDirectoryAlreadyExists(root_directory)

    magic_dir = os.path.join(root_directory, '.morph-system-branch')
    os.makedirs(root_directory)
    os.mkdir(magic_dir)

    sb = SystemBranchDirectory(
        root_directory, root_repository_url, system_branch_name)
    sb.set_config('branch.name', system_branch_name)
    sb.set_config('branch.root', root_repository_url)
    sb.set_config('branch.uuid', uuid.uuid4().hex)

    return sb


def open(root_directory):
    '''Open an existing system branch directory.'''

    # Ugly hack follows.
    sb = SystemBranchDirectory(root_directory, None, None)
    root_repository_url = sb.get_config('branch.root')
    system_branch_name = sb.get_config('branch.name')

    return SystemBranchDirectory(
        root_directory, root_repository_url, system_branch_name)


def open_from_within(dirname):
    '''Open a system branch directory, given any directory.

    The directory can be within the system branch root directory,
    or it can be a parent, in some cases. If each parent on the
    path from dirname to the system branch root directory has no
    siblings, this function will find it.

    '''

    root_directory = morphlib.util.find_root(
        dirname, '.morph-system-branch')
    if root_directory is None:
        root_directory = morphlib.util.find_leaf(
            dirname, '.morph-system-branch')
    if root_directory is None:
        raise NotInSystemBranch(dirname)
    return morphlib.sysbranchdir.open(root_directory)