summaryrefslogtreecommitdiff
path: root/morphlib/plugins/add_binary_plugin.py
blob: dee1d9c49f0d36b003bf55714fbf7b91aa15be1d (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
# Copyright (C) 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.


import cliapp
import logging
import os
import re
import urlparse

import morphlib


class AddBinaryPlugin(cliapp.Plugin):

    '''Add a subcommand for dealing with large binary files.'''

    def enable(self):
        self.app.add_subcommand(
            'add-binary', self.add_binary, arg_synopsis='FILENAME...')

    def disable(self):
        pass

    def add_binary(self, binaries):
        '''Add a binary file to the current repository.

        Command line argument:

        * `FILENAME...` is the binaries to be added to the repository.

        This checks for the existence of a .gitfat file in the repository. If
        there is one then a line is added to .gitattributes telling it that
        the given binary should be handled by git-fat. If there is no .gitfat
        file then it is created, with the rsync remote pointing at the correct
        directory on the Trove host. A line is then added to .gitattributes to
        say that the given binary should be handled by git-fat.

        Example:

            morph add-binary big_binary.tar.gz

        '''
        if not binaries:
            raise morphlib.Error('add-binary must get at least one argument')

        gd = morphlib.gitdir.GitDirectory(os.getcwd(), search_for_root=True)
        gd.fat_init()
        if not gd.has_fat():
            self._make_gitfat(gd)
        self._handle_binaries(binaries, gd)
        logging.info('Staged binaries for commit')

    def _handle_binaries(self, binaries, gd):
        '''Add a filter for the given file, and then add it to the repo.'''
        # begin by ensuring all paths given are relative to the root directory
        files = [gd.get_relpath(os.path.realpath(binary))
                 for binary in binaries]

        # escape special characters and whitespace
        escaped = []
        for path in files:
            path = self.escape_glob(path)
            path = self.escape_whitespace(path)
            escaped.append(path)

        # now add any files that aren't already mentioned in .gitattributes to
        # the file so that git fat knows what to do
        attr_path = gd.join_path('.gitattributes')
        if '.gitattributes' in gd.list_files():
            with open(attr_path, 'r') as attributes:
                current = set(f.split()[0] for f in attributes)
        else:
            current = set()
        to_add = set(escaped) - current

        # if we don't need to change .gitattributes then we can just do
        # `git add <binaries>`
        if not to_add:
            gd.get_index().add_files_from_working_tree(files)
            return

        with open(attr_path, 'a') as attributes:
            for path in to_add:
                attributes.write('%s filter=fat -crlf\n' % path)

        # we changed .gitattributes, so need to stage it for committing
        files.append(attr_path)
        gd.get_index().add_files_from_working_tree(files)

    def _make_gitfat(self, gd):
        '''Make .gitfat point to the rsync directory for the repo.'''
        remote = gd.get_remote('origin')
        if not remote.get_push_url():
            raise Exception(
                'Remote `origin` does not have a push URL defined.')
        url = urlparse.urlparse(remote.get_push_url())
        if url.scheme != 'ssh':
            raise Exception(
                'Push URL for `origin` is not an SSH URL: %s' % url.geturl())
        fat_store = '%s:%s' % (url.netloc, url.path)
        fat_path = gd.join_path('.gitfat')
        with open(fat_path, 'w+') as gitfat:
            gitfat.write('[rsync]\n')
            gitfat.write('remote = %s' % fat_store)
        gd.get_index().add_files_from_working_tree([fat_path])

    def escape_glob(self, path):
        '''Escape glob metacharacters in a path and return the result.'''
        metachars = re.compile('([*?[])')
        path = metachars.sub(r'[\1]', path)
        return path

    def escape_whitespace(self, path):
        '''Substitute whitespace with [[:space:]] and return the result.'''
        whitespace = re.compile('([ \n\r\t])')
        path = whitespace.sub(r'[[:space:]]', path)
        return path