File: rich_history.py

package info (click to toggle)
git-ubuntu 1.1-2
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 1,688 kB
  • sloc: python: 13,378; sh: 480; makefile: 2
file content (203 lines) | stat: -rw-r--r-- 7,570 bytes parent folder | download
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
import logging
import os
import subprocess
import sys

import pygit2

import gitubuntu.git_repository
import gitubuntu.spec as spec


class MultipleParentError(ValueError): pass
class BaseNotFoundError(RuntimeError): pass


def generate_single_parents(upload_tag):
    """Yield each successive parent of the upload tag provided

    But only as long as each parent is alone. As soon as a commit with multiple
    parents is spotted, an exception is raised.

    :param pygit2.Reference upload_tag: the upload tag from which to find
        parents.
    :raises MultipleParentError: if multiple parents for a commit are found.

    Yields a series of pygit2.Commit objects.
    """
    commit = upload_tag.peel(pygit2.Commit)
    while True:
        parents = commit.parents
        if not parents:
            return
        if len(parents) > 1:
            raise MultipleParentError()
        commit = parents[0]
        yield commit


def get_upload_tag_base(repo, upload_tag, commit_map):
    """Find the base import tag of an upload tag

    The base import tag is defined as the first import tag found as the upload
    tag's parents are successively examined.

    :param GitUbuntuRepository repo: the git repository
    :param pygit2.Reference upload_tag: the upload tag to examine
    :param dict(pygit2.Oid, pygit2.Reference): a mapping of commit hashes to
        their import tags, for all import tags that exist.
    :returns: the base import tag for the given upload tag
    :rtype: pygit2.Reference
    :raises BaseNotFoundError: if a base import tag cannot be found
    :raises MultipleParentError: if multiple parents for a commit are found
    """
    for parent in generate_single_parents(upload_tag):
        try:
            return commit_map[parent.id]
        except KeyError:
            pass
    raise BaseNotFoundError()


def export_upload_tag(repo, path, upload_tag, commit_map):
    """Export a single upload tag for later reconstruction

    If successful, two files will be created in the output directory: one with
    extension '.base', and one with extension '.pick'. The base name of these
    files is the version string from the upload tag. The '.base' file will
    contain a single LF-terminated string which is the version string from the
    import tag on which the upload tag patchset is based. The '.pick' file will
    contain the patchset described as a series of commit hash strings that can
    be picked in order, one on each line, as described by 'git rev-list
    --reverse'.

    Note that the upload and import tag names are already escaped according to
    dep14, so the output filenames will have their version parts escaped
    likewise.

    :param GitUbuntuRepository repo: the git repository.
    :param str path: the directory to export to.
    :param pygit2.Reference upload_tag: the upload tag to export.
    :param dict(pygit2.Oid, pygit2.Reference): a mapping of commit hashes to
        their import tags, for all import tags that exist.
    :raises BaseNotFoundError: if a base import tag cannot be found.
    :raises MultipleParentError: if multiple parents for a commit are found.
    :raises subprocess.CalledProcessError: if the underlying call to "git
        rev-list" fails.
    :returns: None.
    """
    import_tag = get_upload_tag_base(repo, upload_tag, commit_map)
    import_name = import_tag.name.split('/')[-1]
    export_name = upload_tag.name.split('/')[-1]

    with open(os.path.join(path, export_name) + '.base', 'w') as f:
        print(import_name, file=f)

    env = gitubuntu.git_repository._derive_git_cli_env(
        pygit2_repo=repo.raw_repo,
        initial_env=repo._initial_env,
        update_env=repo.env,
    )
    with open(os.path.join(path, export_name) + '.pick', 'w') as f:
        subprocess.check_call(
            [
                'git',
                'rev-list',
                '--reverse',
                '%s..%s' % (import_tag.name, upload_tag.name),
            ],
            env=env,
            stdout=f,
        )


def export_all(repo, path, namespace='importer'):
    """Export all upload tags for later reconstruction

    See the docstring of export_upload_tag() for the output format.

    :param GitUbuntuRepository repo: the git repository.
    :param str path: the directory to export to.
    :param str namespace: the namespace under which the import and upload tags
        can be found.
    :raises BaseNotFoundError: if a base import tag cannot be found.
    :raises MultipleParentError: if multiple parents for a commit are found.
    :returns: None.
    """
    import_tag_prefix = 'refs/tags/%s/import/' % namespace
    upload_tag_prefix = 'refs/tags/%s/upload/' % namespace

    commit_map = {}
    for ref in repo.references:
        if ref.name.startswith(import_tag_prefix):
            commit_map[ref.peel(pygit2.Commit).id] = ref

    for ref in repo.references:
        # only upload tags
        if not ref.name.startswith(upload_tag_prefix):
            continue

        try:
            export_upload_tag(repo, path, ref, commit_map)
        except MultipleParentError:
            logging.warning("Multiple parents exporting %s" % ref.name)
        except BaseNotFoundError:
            logging.warning("No base found exporting %s" % ref.name)


def import_single(repo, path, version, namespace='importer'):
    """Import rich history, creating a corresponding upload tag

    :param GitUbuntuRepository repo: the git repository.
    :param str path: the directory to import from.
    :param str version: the package version for which rich history should be
        imported.
    :param str namespace: the namespace under which the import and upload tags
        can be found.
    :raises FileNotFoundError: if rich history cannot be found for this
        version.
    :raises BaseNotFoundError: if a base import tag cannot be found.
    :raises subprocess.CalledProcessError: if any of the underlying calls to
        "git cherry-pick" or "git tag" fail.
    :returns: None.
    """
    version_name = gitubuntu.git_repository.git_dep14_tag(version)
    with open(os.path.join(path, version_name) + '.base', 'r') as f:
        base_version = f.read().strip()
    import_tag_name = 'refs/tags/%s/import/%s' % (namespace, base_version)

    # Ensure that the base import tag exists
    try:
        repo.raw_repo.lookup_reference(import_tag_name)
    except KeyError as e:
        raise BaseNotFoundError() from e

    with open(os.path.join(path, version_name) + '.pick', 'r') as pick:
        with repo.temporary_worktree(import_tag_name, 'rich-history-import'):
            env = {
                'GIT_COMMITTER_NAME': spec.SYNTHESIZED_COMMITTER_NAME,
                'GIT_COMMITTER_EMAIL': spec.SYNTHESIZED_COMMITTER_EMAIL,
            }
            for commit_string in pick:
                subprocess.check_call(
                    [
                        'git',
                        'cherry-pick',
                        '--ff',
                        '--allow-empty',
                        '--allow-empty-message',
                        commit_string.strip(),
                    ],
                    env=env,
                )
            upload_tag_name = '%s/upload/%s' % (namespace, version_name)
            subprocess.check_call(
                [
                    'git',
                    'tag',
                    '-a',
                    '-m', 'Previous upload tag automatically rebased',
                    upload_tag_name,
                ],
                env=env,
            )