summaryrefslogtreecommitdiff
path: root/morphlib/plugins/add_binary_plugin.py
blob: 45edae4c812cde6c917489a9d2ca05a25cf58fc8 (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
# Copyright (C) 2014-2015  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, see <http://www.gnu.org/licenses/>.


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