18
18
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
25
from bzrlib.trace import info, mutter
27
from debian_bundle.changelog import Changelog
29
from bzrlib.plugins.builddeb.errors import (MissingChangelogError,
31
from bzrlib.trace import mutter
33
from debian_bundle import deb822
34
from debian_bundle.changelog import Changelog, ChangelogParseError
41
from bzrlib.transport import (
42
do_catching_redirections,
45
from bzrlib.plugins.builddeb.errors import (
46
MissingChangelogError,
34
52
def recursive_copy(fromdir, todir):
35
"""Copy the contents of fromdir to todir. Like shutil.copytree, but the
36
destination directory must already exist with this method, rather than
37
not exists for shutil."""
38
mutter("Copying %s to %s", fromdir, todir)
39
for entry in os.listdir(fromdir):
40
path = os.path.join(fromdir, entry)
41
if os.path.isdir(path):
42
tosubdir = os.path.join(todir, entry)
43
if not os.path.exists(tosubdir):
45
recursive_copy(path, tosubdir)
47
shutil.copy(path, todir)
50
def find_changelog(t, merge):
53
"""Copy the contents of fromdir to todir.
55
Like shutil.copytree, but the destination directory must already exist
56
with this method, rather than not exists for shutil.
58
mutter("Copying %s to %s", fromdir, todir)
59
for entry in os.listdir(fromdir):
60
path = os.path.join(fromdir, entry)
61
if os.path.isdir(path):
62
tosubdir = os.path.join(todir, entry)
63
if not os.path.exists(tosubdir):
65
recursive_copy(path, tosubdir)
67
shutil.copy(path, todir)
70
def find_changelog(t, merge, max_blocks=1):
71
"""Find the changelog in the given tree.
73
First looks for 'debian/changelog'. If "merge" is true will also
76
The returned changelog is created with 'allow_empty_author=True'
77
as some people do this but still want to build.
78
'max_blocks' defaults to 1 to try and prevent old broken
79
changelog entries from causing the command to fail,
81
"larstiq" is a subset of "merge" mode. It indicates that the
82
'.bzr' dir is at the same level as 'changelog' etc., rather
83
than being at the same level as 'debian/'.
85
:param t: the Tree to look in.
86
:param merge: whether this is a "merge" package.
87
:param max_blocks: Number of max_blocks to parse (defaults to 1)
88
:return: (changelog, larstiq) where changelog is the Changelog,
89
and larstiq is a boolean indicating whether the file is at
90
'changelog' if merge was given, False otherwise.
51
92
changelog_file = 'debian/changelog'
55
if not t.has_filename(changelog_file):
57
#Assume LarstiQ's layout (.bzr in debian/)
58
changelog_file = 'changelog'
60
if not t.has_filename(changelog_file):
61
raise MissingChangelogError("debian/changelog or changelog")
63
raise MissingChangelogError("debian/changelog")
65
if merge and t.has_filename('changelog'):
66
if (t.kind(t.path2id('debian')) == 'symlink' and
67
t.get_symlink_target(t.path2id('debian')) == '.'):
68
changelog_file = 'changelog'
70
mutter("Using '%s' to get package information", changelog_file)
71
changelog_id = t.path2id(changelog_file)
72
if changelog_id is None:
73
raise AddChangelogError(changelog_file)
74
contents = t.get_file_text(changelog_id)
96
if not t.has_filename(changelog_file):
98
#Assume LarstiQ's layout (.bzr in debian/)
99
changelog_file = 'changelog'
101
if not t.has_filename(changelog_file):
102
raise MissingChangelogError('"debian/changelog" or '
105
raise MissingChangelogError('"debian/changelog"')
106
elif merge and t.has_filename('changelog'):
107
# If it is a "larstiq" pacakge and debian is a symlink to
108
# "." then it will have found debian/changelog. Try and detect
110
if (t.kind(t.path2id('debian')) == 'symlink' and
111
t.get_symlink_target(t.path2id('debian')) == '.'):
112
changelog_file = 'changelog'
114
mutter("Using '%s' to get package information", changelog_file)
115
changelog_id = t.path2id(changelog_file)
116
if changelog_id is None:
117
raise AddChangelogError(changelog_file)
118
contents = t.get_file_text(changelog_id)
77
121
changelog = Changelog()
78
changelog.parse_changelog(contents, max_blocks=1, allow_empty_author=True)
123
changelog.parse_changelog(contents, max_blocks=max_blocks, allow_empty_author=True)
124
except ChangelogParseError, e:
125
raise UnparseableChangelog(str(e))
79
126
return changelog, larstiq
129
def strip_changelog_message(changes):
130
"""Strip a changelog message like debcommit does.
132
Takes a list of changes from a changelog entry and applies a transformation
133
so the message is well formatted for a commit message.
135
:param changes: a list of lines from the changelog entry
136
:return: another list of lines with blank lines stripped from the start
137
and the spaces the start of the lines split if there is only one logical
142
while changes and changes[-1] == '':
144
while changes and changes[0] == '':
147
whitespace_column_re = re.compile(r' |\t')
148
changes = map(lambda line: whitespace_column_re.sub('', line, 1), changes)
150
leader_re = re.compile(r'[ \t]*[*+-] ')
151
count = len(filter(leader_re.match, changes))
153
return map(lambda line: leader_re.sub('', line, 1).lstrip(), changes)
81
158
def tarball_name(package, version):
82
"""Return the name of the .orig.tar.gz for the given package and version."""
84
return "%s_%s.orig.tar.gz" % (package, str(version))
159
"""Return the name of the .orig.tar.gz for the given package and version.
161
:param package: the name of the source package.
162
:param version: the upstream version of the package.
163
:return: a string that is the name of the upstream tarball to use.
165
return "%s_%s.orig.tar.gz" % (package, str(version))
86
168
def get_snapshot_revision(upstream_version):
87
"""Return the upstream revision specifier if specified in the upstream version or None. """
88
match = re.search("~bzr([0-9]+)$", upstream_version)
90
return match.groups()[0]
91
match = re.search("(?:~|\\+)svn([0-9]+)$", upstream_version)
93
return "svn:%s" % match.groups()[0]
96
# vim: ts=2 sts=2 sw=2
169
"""Return the upstream revision specifier if specified in the upstream version.
171
When packaging an upstream snapshot some people use +vcsnn or ~vcsnn to indicate
172
what revision number of the upstream VCS was taken for the snapshot. This given
173
an upstream version number this function will return an identifier of the
174
upstream revision if it appears to be a snapshot. The identifier is a string
175
containing a bzr revision spec, so it can be transformed in to a revision.
177
:param upstream_version: a string containing the upstream version number.
178
:return: a string containing a revision specifier for the revision of the
179
upstream branch that the snapshot was taken from, or None if it doesn't
180
appear to be a snapshot.
182
match = re.search("(?:~|\\+)bzr([0-9]+)$", upstream_version)
183
if match is not None:
184
return match.groups()[0]
185
match = re.search("(?:~|\\+)svn([0-9]+)$", upstream_version)
186
if match is not None:
187
return "svn:%s" % match.groups()[0]
191
def get_export_upstream_revision(config, version=None):
193
if version is not None:
194
rev = get_snapshot_revision(str(version.upstream_version))
196
rev = config._get_best_opt('export-upstream-revision')
197
if rev is not None and version is not None:
198
rev = rev.replace('$UPSTREAM_VERSION',
199
str(version.upstream_version))
203
def suite_to_distribution(suite):
204
"""Infer the distribution from a suite.
206
When passed the name of a suite (anything in the distributions field of
207
a changelog) it will infer the distribution from that (i.e. Debian or
210
:param suite: the string containing the suite
211
:return: "debian", "ubuntu", or None if the distribution couldn't be inferred.
213
debian_releases = ('woody', 'sarge', 'etch', 'lenny', 'squeeze', 'stable',
214
'testing', 'unstable', 'experimental', 'frozen', 'sid')
215
debian_targets = ('', '-security', '-proposed-updates', '-backports')
216
ubuntu_releases = ('warty', 'hoary', 'breezy', 'dapper', 'edgy',
217
'feisty', 'gutsy', 'hardy', 'intrepid', 'jaunty', 'karmic',
219
ubuntu_targets = ('', '-proposed', '-updates', '-security', '-backports')
220
all_debian = [r + t for r in debian_releases for t in debian_targets]
221
all_ubuntu = [r + t for r in ubuntu_releases for t in ubuntu_targets]
222
if suite in all_debian:
224
if suite in all_ubuntu:
229
def lookup_distribution(distribution_or_suite):
230
"""Get the distribution name based on a distribtion or suite name.
232
:param distribution_or_suite: a string that is either the name of
233
a distribution or a suite.
234
:return: a string with a distribution name or None.
236
if distribution_or_suite.lower() in ("debian", "ubuntu"):
237
return distribution_or_suite.lower()
238
return suite_to_distribution(distribution_or_suite)
241
def move_file_if_different(source, target, md5sum):
242
if os.path.exists(target):
243
if os.path.samefile(source, target):
246
target_f = open(target)
248
for line in target_f:
249
t_md5sum.update(line)
252
if t_md5sum.hexdigest() == md5sum:
254
shutil.move(source, target)
257
def write_if_different(contents, target):
259
md5sum.update(contents)
260
fd, temp_path = tempfile.mkstemp("builddeb-rename-")
261
fobj = os.fdopen(fd, "wd")
267
move_file_if_different(temp_path, target, md5sum.hexdigest())
269
if os.path.exists(temp_path):
273
def _download_part(name, base_transport, target_dir, md5sum):
274
part_base_dir, part_path = urlutils.split(name)
276
if part_base_dir != '':
277
f_t = base_transport.clone(part_base_dir)
278
f_f = f_t.get(part_path)
280
target_path = os.path.join(target_dir, part_path)
281
fd, temp_path = tempfile.mkstemp(prefix="builldeb-")
282
fobj = os.fdopen(fd, "wb")
285
shutil.copyfileobj(f_f, fobj)
288
move_file_if_different(temp_path, target_path, md5sum)
290
if os.path.exists(temp_path):
297
filename, transport = open_transport(path)
298
return open_file_via_transport(filename, transport)
301
def open_transport(path):
302
"""Obtain an appropriate transport instance for the given path."""
303
base_dir, path = urlutils.split(path)
304
transport = get_transport(base_dir)
305
return (path, transport)
308
def open_file_via_transport(filename, transport):
309
"""Open a file using the transport, follow redirects as necessary."""
310
def open_file(transport):
311
return transport.get(filename)
312
def follow_redirection(transport, e, redirection_notice):
313
mutter(redirection_notice)
314
_filename, redirected_transport = open_transport(e.target)
315
return redirected_transport
317
result = do_catching_redirections(open_file, transport, follow_redirection)
321
def _dget(cls, dsc_location, target_dir):
322
if not os.path.isdir(target_dir):
323
raise errors.NotADirectory(target_dir)
324
path, dsc_t = open_transport(dsc_location)
325
dsc_contents = open_file_via_transport(path, dsc_t).read()
326
dsc = cls(dsc_contents)
327
for file_details in dsc['files']:
328
name = file_details['name']
329
_download_part(name, dsc_t, target_dir, file_details['md5sum'])
330
target_file = os.path.join(target_dir, path)
331
write_if_different(dsc_contents, target_file)
335
def dget(dsc_location, target_dir):
336
return _dget(deb822.Dsc, dsc_location, target_dir)
339
def dget_changes(changes_location, target_dir):
340
return _dget(deb822.Changes, changes_location, target_dir)
343
def get_parent_dir(target):
344
parent = os.path.dirname(target)
345
if os.path.basename(target) == '':
346
parent = os.path.dirname(parent)
350
def find_bugs_fixed(changes, branch, _lplib=None):
352
from bzrlib.plugins.builddeb import launchpad as _lplib
354
for change in changes:
355
for match in re.finditer("closes:\s*(?:bug)?\#?\s?\d+"
356
"(?:,\s*(?:bug)?\#?\s?\d+)*", change,
358
closes_list = match.group(0)
359
for match in re.finditer("\d+", closes_list):
360
bug_url = bugtracker.get_bug_url("deb", branch,
362
bugs.append(bug_url + " fixed")
363
lp_bugs = _lplib.ubuntu_bugs_for_debian_bug(match.group(0))
364
if len(lp_bugs) == 1:
365
bug_url = bugtracker.get_bug_url("lp", branch,
367
bugs.append(bug_url + " fixed")
368
for match in re.finditer("lp:\s+\#\d+(?:,\s*\#\d+)*",
369
change, re.IGNORECASE):
370
closes_list = match.group(0)
371
for match in re.finditer("\d+", closes_list):
372
bug_url = bugtracker.get_bug_url("lp", branch,
374
bugs.append(bug_url + " fixed")
375
deb_bugs = _lplib.debian_bugs_for_ubuntu_bug(match.group(0))
376
if len(deb_bugs) == 1:
377
bug_url = bugtracker.get_bug_url("deb", branch,
379
bugs.append(bug_url + " fixed")
383
def find_extra_authors(changes):
384
extra_author_re = re.compile(r"\s*\[([^\]]+)]\s*", re.UNICODE)
386
for change in changes:
387
# Parse out any extra authors.
388
match = extra_author_re.match(change.decode("utf-8"))
389
if match is not None:
390
new_author = match.group(1).strip()
391
already_included = False
392
for author in authors:
393
if author.startswith(new_author):
394
already_included = True
396
if not already_included:
397
authors.append(new_author)
401
def find_thanks(changes):
402
thanks_re = re.compile(r"[tT]hank(?:(?:s)|(?:you))(?:\s*to)?"
403
"((?:\s+(?:(?:[A-Z]\.)|(?:[A-Z]\w+(?:-[A-Z]\w+)*)))+"
404
"(?:\s+<[^@>]+@[^@>]+>)?)",
407
changes_str = " ".join(changes).decode("utf-8")
408
for match in thanks_re.finditer(changes_str):
411
thanks_str = match.group(1).strip()
412
thanks_str = re.sub(r"\s+", " ", thanks_str)
413
thanks.append(thanks_str)
417
def get_commit_info_from_changelog(changelog, branch, _lplib=None):
418
"""Retrieves the messages from the last section of debian/changelog.
420
Reads the latest stanza of debian/changelog and returns the
421
text of the changes in that section. It also returns other
422
information about the change, including the authors of the change,
423
anyone that is thanked, and the bugs that are declared fixed by it.
425
:return: a tuple (message, authors, thanks, bugs). message is the
426
commit message that should be used. authors is a list of strings,
427
with those that contributed to the change, thanks is a list
428
of string, with those who were thanked in the changelog entry.
429
bugs is a list of bug URLs like for --fixes.
430
If the information is not available then any can be None.
436
if changelog._blocks:
437
block = changelog._blocks[0]
438
authors = [block.author.decode("utf-8")]
439
changes = strip_changelog_message(block.changes())
440
authors += find_extra_authors(changes)
441
bugs = find_bugs_fixed(changes, branch, _lplib=_lplib)
442
thanks = find_thanks(changes)
443
message = "\n".join(changes).replace("\r", "")
444
return (message, authors, thanks, bugs)
447
def find_last_distribution(changelog):
448
"""Find the last changelog that was used in a changelog.
450
This will skip stanzas with the 'UNRELEASED' distribution.
452
:param changelog: Changelog to analyze
454
for block in changelog._blocks:
455
distribution = block.distributions.split(" ")[0]
456
if distribution != "UNRELEASED":
461
def subprocess_setup():
462
# Python installs a SIGPIPE handler by default. This is usually not what
463
# non-Python subprocesses expect.
464
# Many, many thanks to Colin Watson
465
signal.signal(signal.SIGPIPE, signal.SIG_DFL)