summaryrefslogtreecommitdiff
path: root/cerbero/utils/git.py
blob: 9223c519b690fb68f6eb66aef536c3052b9bbe0e (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
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
# cerbero - a multi-platform build system for Open Source software
# Copyright (C) 2012 Andoni Morales Alastruey <ylatuya@gmail.com>
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Library General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library 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
# Library General Public License for more details.
#
# You should have received a copy of the GNU Library General Public
# License along with this library; if not, write to the
# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
# Boston, MA 02111-1307, USA.

import os
import shutil
import time
import cerbero.utils.messages as m

from cerbero.config import Platform
from cerbero.utils import shell, _
from cerbero.errors import FatalError

if shell.PLATFORM == Platform.WINDOWS:
    # We do not want the MSYS2 Git because it mucks consumption by Cargo
    GIT = shutil.which('git', path=shell.get_path_minus_msys(os.environ['PATH']))
else:
    GIT = 'git'


def ensure_user_is_set(git_dir, logfile=None):
    # Set the user configuration for this repository
    # so that commands that need the account's default identity
    # (e.g., git commit), will not fail.
    try:
        shell.new_call([GIT, 'config', 'user.email'], git_dir, logfile=logfile)
    except FatalError:
        shell.new_call([GIT, 'config', 'user.email', 'cerbero@gstreamer.freedesktop.org'], git_dir, logfile=logfile)

    try:
        shell.new_call([GIT, 'config', 'user.name'], git_dir, logfile=logfile)
    except FatalError:
        shell.new_call([GIT, 'config', 'user.name', 'Cerbero Build System'], git_dir, logfile=logfile)


def init(git_dir, logfile=None):
    """
    Initialize a git repository with 'git init'

    @param git_dir: path of the git repository
    @type git_dir: str
    """
    os.makedirs(git_dir, exist_ok=True)
    shell.new_call([GIT, 'init'], git_dir, logfile=logfile)
    ensure_user_is_set(git_dir, logfile=logfile)


def clean(git_dir, logfile=None):
    """
    Clean a git respository with clean -dfx

    @param git_dir: path of the git repository
    @type git_dir: str
    """
    return shell.new_call([GIT, 'clean', '-dfx'], git_dir, logfile=logfile)


def list_tags(git_dir):
    """
    List all tags

    @param git_dir: path of the git repository
    @type git_dir: str
    @param fail: raise an error if the command failed
    @type fail: false
    @return: list of tag names (str)
    @rtype: list
    """
    return shell.check_output([GIT, 'tag', '-l'], cmd_dir=git_dir).strip().splitlines()


def create_tag(git_dir, tagname, tagdescription, commit, logfile=None):
    """
    Create a tag using commit

    @param git_dir: path of the git repository
    @type git_dir: str
    @param tagname: name of the tag to create
    @type tagname: str
    @param tagdescription: the tag description
    @type tagdescription: str
    @param commit: the tag commit to use
    @type commit: str
    @param fail: raise an error if the command failed
    @type fail: false
    """

    shell.new_call([GIT, 'tag', '-s', tagname, '-m', tagdescription, commit], cmd_dir=git_dir, logfile=logfile)
    return shell.new_call([GIT, 'push', 'origin', tagname], cmd_dir=git_dir, logfile=logfile)


def delete_tag(git_dir, tagname, logfile=None):
    """
    Delete a tag

    @param git_dir: path of the git repository
    @type git_dir: str
    @param tagname: name of the tag to delete
    @type tagname: str
    @param fail: raise an error if the command failed
    @type fail: false
    """
    return shell.new_call([GIT, '-d', tagname], cmd_dir=git_dir, logfile=logfile)


async def fetch(git_dir, fail=True, logfile=None):
    """
    Fetch all refs from all the remotes

    @param git_dir: path of the git repository
    @type git_dir: str
    @param fail: raise an error if the command failed
    @type fail: false
    """
    # git 1.9 introduced the possibility to fetch both branches and tags at the
    # same time when using --tags: https://stackoverflow.com/a/20608181.
    # centOS 7 ships with git 1.8.3.1, hence for old git versions, we need to
    # run two separate commands.
    cmd = [GIT, 'fetch', '--all']
    ret = await shell.async_call(cmd, cmd_dir=git_dir, fail=fail, logfile=logfile, cpu_bound=False)
    if ret != 0:
        return ret
    cmd.append('--tags')
    # To avoid "would clobber existing tag" error
    cmd.append('-f')
    return await shell.async_call(cmd, cmd_dir=git_dir, fail=fail, logfile=logfile, cpu_bound=False)


async def submodules_update(git_dir, src_dir=None, fail=True, offline=False, logfile=None):
    """
    Update submodules asynchronously from local directory

    @param git_dir: path of the git repository
    @type git_dir: str
    @param src_dir: path or base URI of the source directory
    @type src_dir: src
    @param fail: raise an error if the command failed
    @type fail: false
    @param offline: don't use the network
    @type offline: false
    """
    if not os.path.exists(os.path.join(git_dir, '.gitmodules')):
        m.log(_('.gitmodules does not exist in %s. No need to fetch submodules.') % git_dir, logfile)
        return

    if src_dir:
        config = shell.check_output(
            [GIT, 'config', '--file=.gitmodules', '--list'], fail=False, cmd_dir=git_dir, logfile=logfile
        )
        config_array = [s.split('=', 1) for s in config.splitlines()]
        for c in config_array:
            if c[0].startswith('submodule.') and c[0].endswith('.path'):
                submodule = c[0][len('submodule.') : -len('.path')]
                shell.new_call(
                    [
                        GIT,
                        'config',
                        '--file=.gitmodules',
                        'submodule.{}.url'.format(submodule),
                        os.path.join(src_dir, c[1]),
                    ],
                    cmd_dir=git_dir,
                    logfile=logfile,
                )
    shell.new_call([GIT, 'submodule', 'init'], cmd_dir=git_dir, logfile=logfile)
    if src_dir or not offline:
        await shell.async_call([GIT, 'submodule', 'sync'], cmd_dir=git_dir, logfile=logfile, cpu_bound=False)
        await shell.async_call(
            [GIT, 'submodule', 'update'], cmd_dir=git_dir, fail=fail, logfile=logfile, cpu_bound=False
        )
    else:
        await shell.async_call(
            [GIT, 'submodule', 'update', '--no-fetch'], cmd_dir=git_dir, fail=fail, logfile=logfile, cpu_bound=False
        )
    if src_dir:
        for c in config_array:
            if c[0].startswith('submodule.') and c[0].endswith('.url'):
                shell.new_call([GIT, 'config', '--file=.gitmodules', c[0], c[1]], cmd_dir=git_dir, logfile=logfile)
        await shell.async_call([GIT, 'submodule', 'sync'], cmd_dir=git_dir, logfile=logfile, cpu_bound=False)


async def checkout(git_dir, commit, logfile=None):
    """
    Reset a git repository to a given commit

    @param git_dir: path of the git repository
    @type git_dir: str
    @param commit: the commit to checkout
    @type commit: str
    """
    cmd = [GIT, 'reset', '--hard', commit]
    return await shell.async_call(cmd, git_dir, logfile=logfile, cpu_bound=False)


def get_hash(git_dir, commit, logfile=None):
    """
    Get a commit hash from a valid commit.
    Can be used to check if a commit exists

    @param git_dir: path of the git repository
    @type git_dir: str
    @param commit: the commit to log
    @type commit: str
    """
    if not os.path.isdir(os.path.join(git_dir, '.git')):
        # If a recipe's source type is switched from tarball to git, then we
        # can get called from built_version() when the directory isn't git.
        # Return a fixed string + unix time to trigger a full fetch.
        return 'not-git-' + str(time.time())
    return shell.check_output(
        [GIT, 'rev-parse', commit], cmd_dir=git_dir, fail=False, quiet=True, logfile=logfile
    ).rstrip()


def get_hash_is_ancestor(git_dir, commit, logfile=None):
    if not os.path.isdir(os.path.join(git_dir, '.git')):
        return False
    ret = shell.new_call(
        [GIT, 'merge-base', '--is-ancestor', commit, 'HEAD'], cmd_dir=git_dir, fail=False, logfile=logfile
    )
    return ret == 0


async def local_checkout(git_dir, local_git_dir, commit, logfile=None, use_submodules=True):
    """
    Clone a repository for a given commit in a different location

    @param git_dir: destination path of the git repository
    @type git_dir: str
    @param local_git_dir: path of the source git repository
    @type local_git_dir: str
    @param commit: the commit to checkout
    @type commit: false
    """
    branch_name = 'cerbero_build'
    await shell.async_call([GIT, 'checkout', commit, '-B', branch_name], local_git_dir, logfile=logfile)
    await shell.async_call([GIT, 'clone', local_git_dir, '-s', '-b', branch_name, '.'], git_dir, logfile=logfile)
    ensure_user_is_set(git_dir, logfile=logfile)
    if use_submodules:
        await submodules_update(git_dir, local_git_dir, logfile=logfile)


def add_remote(git_dir, name, url, logfile=None):
    """
    Add a remote to a git repository

    @param git_dir: destination path of the git repository
    @type git_dir: str
    @param name: name of the remote
    @type name: str
    @param url: url of the remote
    @type url: str
    """
    try:
        shell.new_call([GIT, 'remote', 'add', name, url], git_dir, logfile=logfile)
    except Exception:
        shell.new_call([GIT, 'remote', 'set-url', name, url], git_dir, logfile=logfile)


def check_line_endings(platform):
    """
    Checks if on windows we don't use the automatic line endings conversion
    as it breaks everything

    @param platform: the host platform
    @type platform: L{cerbero.config.Platform}
    @return: true if git config is core.autorlf=false
    @rtype: bool
    """
    if platform != Platform.WINDOWS:
        return True
    val = shell.check_output([GIT, 'config', '--get', 'core.autocrlf'], fail=False)
    if 'false' in val.lower():
        return True
    return False


def init_directory(git_dir, logfile=None):
    """
    Initialize a git repository with the contents
    of a directory

    @param git_dir: path of the git repository
    @type git_dir: str
    """
    init(git_dir, logfile=logfile)
    shell.new_call([GIT, 'add', '--force', '-A', '.'], git_dir, logfile=logfile)
    # Check if we need to commit anything. This can happen when extract failed
    # or was cancelled somehow last time but the source tree was setup
    # correctly and git commit succeeded.
    ret = shell.new_call([GIT, 'diff', '--quiet', 'HEAD'], git_dir, logfile=logfile, fail=False)
    if ret > 0:
        shell.new_call([GIT, 'commit', '-m', 'Initial commit'], git_dir, logfile=logfile)


def apply_patch(patch, git_dir, logfile=None):
    """
    Applies a commit patch usign 'git am'
    of a directory

    @param git_dir: path of the git repository
    @type git_dir: str
    @param patch: path of the patch file
    @type patch: str
    """
    shell.new_call([GIT, 'am', '--ignore-whitespace', patch], git_dir, logfile=logfile)