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,
)
|