[debrepatch] 01/02: Make the interface to debpatch(1) more like patch(1)

Ximin Luo infinity0 at debian.org
Thu Nov 17 18:01:47 UTC 2016


This is an automated email from the git hooks/post-receive script.

infinity0 pushed a commit to branch master
in repository debrepatch.

commit df44cf6820ca4c68e28526581a8418a114340eb8
Author: Ximin Luo <infinity0 at debian.org>
Date:   Thu Nov 17 19:00:56 2016 +0100

    Make the interface to debpatch(1) more like patch(1)
---
 debpatch   | 167 ++++++++++++++++++++++++++++++++++++++++++++-----------------
 debrepatch |  46 ++---------------
 2 files changed, 125 insertions(+), 88 deletions(-)

diff --git a/debpatch b/debpatch
index a34b7b8..3aedb53 100755
--- a/debpatch
+++ b/debpatch
@@ -3,22 +3,26 @@
 Apply a debdiff, but more smartly to avoid conflicts
 Specifically, it's smarter in how it applies updates to d/changelog.
 
-Depends on dpkg-dev, devscripts, python3-unidiff.
+Depends on dpkg-dev, devscripts, python3-unidiff, quilt.
 """
 
 import argparse
+import hashlib
 import io
 import logging
 import os
 import random
 import unidiff
+import shutil
 import subprocess
 import sys
+import tempfile
 
 dirname = os.path.dirname
 basename = os.path.basename
 C = subprocess.check_call
 
+# this can be any valid value, it doesn't appear in the final output
 DCH_DUMMY_TAIL = "\n -- debpatch dummy tool <infinity0 at debian.org>  Thu, 01 Jan 1970 00:00:00 +0000\n\n"
 TRY_ENCODINGS = ["utf-8", "latin-1"]
 DISTRIBUTION_DEFAULT = "experimental"
@@ -57,10 +61,13 @@ def read_dch_patch(dch_patch):
     target = read_dch(target_str)
     return source_version, target
 
-def apply_dch_patch(source_file, current, patch_name, old_version, target):
-    if patch_name in current["Changes"]:
+def apply_dch_patch(source_file, current, patch_name, old_version, target, dry_run):
+    # Do not change this text, unless you also add logic to detect markers from
+    # previously-released versions.
+    marker = "Patch '%s' applied by debpatch(1)" % patch_name
+    if marker in current["Changes"]:
         logging.info("patch %s already applied to d/changelog", patch_name)
-        return
+        return target["Version"]
 
     dch_args = []
     dch_env = dict(os.environ)
@@ -77,12 +84,17 @@ def apply_dch_patch(source_file, current, patch_name, old_version, target):
             (old_version, target["Version"]))
         logging.warn("will give -n to `dch` instead of trying to be smart; feel free to make me smarter")
         dch_args.append("-n")
+        version = None
     else:
         version_suffix = target["Version"][len(old_version):]
         version = current["Version"] + version_suffix
         logging.info("using version %s based on suffix %s", version, version_suffix)
         dch_args.extend(["-v", version])
 
+    if dry_run:
+        return version if version else subprocess.check_output(["sh", "-c",
+            "EDITOR=cat dch -n 2>/dev/null | dpkg-parsechangelog -l- -SVersion"]).decode("utf-8").rstrip()
+
     dch_args += ["--force-distribution", "-D", target["Distribution"]]
     dch_args += ["-u", target["Urgency"]]
     if "Maintainer" in target:
@@ -96,7 +108,7 @@ def apply_dch_patch(source_file, current, patch_name, old_version, target):
     # TODO: make this a bit more atomic; if any of the C()s fail we should rewind
     token = "DEBPATCH PLACEHOLDER %s DELETEME" % random.randint(0, 2**64)
     C(["dch", "-c", source_file] + dch_args +
-      ["Patch %s automatically applied by debpatch(1)" % patch_name])
+      [marker])
     C(["dch", "-c", source_file, "-a", token], env=dch_env)
     C(["sed", "-e", "/%s/c\\\n%s" % (token, changes.replace("\n", "\\\n")), "-i", source_file])
 
@@ -127,45 +139,13 @@ def apply_patch_str(patch_name, patch_str):
         call_patch(patch_str, "--dry-run")
         raise ValueError("patch %s doesn't apply!", patch_name)
 
-def main(args):
-    parser = argparse.ArgumentParser(
-        description='Apply a debdiff, but more smartly to avoid conflicts.')
-    parser.add_argument('-v', '--verbose', action="store_true",
-        help='Output more information')
-    parser.add_argument('-c', '--changelog', default='debian/changelog',
-        help='Path to debian/changelog; default: %(default)s')
-    parser.add_argument('-C', '--directory',
-        help="Apply patches from this directory; should be the top-level source dir")
-    parser.add_argument('-i', '--interactive', action="store_true",
-        help="Run the python REPL after processing.")
-    parser.add_argument('--source-version', action="store_true",
-        help="Don't apply the patch; instead print out the version of the Debian "
-        "package that it is supposed to be applied to, or nothing if this can't be "
-        "read from the debian/changelog hunk.")
-    parser.add_argument('patch_file')
-    args = parser.parse_args(args)
-
-    if args.verbose:
-        logging.getLogger().setLevel(logging.DEBUG)
-
-    patch_name = basename(args.patch_file)
+def debpatch(patch, patch_name, args):
     if len(patch_name) > 60:
         # this messes with our dch "already applied" logic detection, sorry
         raise ValueError("pick a shorter patch name; sorry")
 
-    for enc in TRY_ENCODINGS:
-        try:
-            patch = unidiff.PatchSet.from_filename(args.patch_file, encoding=enc)
-            break
-        except:
-            if enc == TRY_ENCODINGS[-1]:
-                raise
-            else:
-                continue
-
-    # change directory before applying patches
-    if args.directory:
-        os.chdir(args.directory)
+    # don't change anything if...
+    dry_run = args.target_version
 
     changelog = list(filter(lambda x: is_dch(x.path), patch))
     if not changelog:
@@ -188,19 +168,114 @@ def main(args):
             print(old_version)
         return
 
-    # TODO: if we patched any patch in d/patches, then we should refresh *that*
-    # patch here. this is hard, but it would help us to avoid refreshing *all*
-    # patches in ./debrepatch, hopefully ending up with a smaller debdiff.
-    apply_patch_str(patch_name, str(patch))
+    if not dry_run:
+        apply_patch_str(patch_name, str(patch))
 
-    # only apply d/changelog patch if the rest of the patch applies
+    # only apply d/changelog patch if the rest of the patch applied
     with open(args.changelog) as fp:
         current = read_dch(fp.read())
-    apply_dch_patch(args.changelog, current, patch_name, old_version, target)
+    new_version = apply_dch_patch(args.changelog, current, patch_name, old_version, target, dry_run)
+    if args.target_version:
+        print(new_version)
+        return
 
     if args.interactive:
         import code
         code.interact(local=locals())
 
+def main(args):
+    parser = argparse.ArgumentParser(
+        description='Apply a debdiff, but more smartly to avoid conflicts.')
+    parser.add_argument('-v', '--verbose', action="store_true",
+        help='Output more information')
+    parser.add_argument('-c', '--changelog', default='debian/changelog',
+        help='Path to debian/changelog; default: %(default)s')
+    parser.add_argument('-i', '--interactive', action="store_true",
+        help="Run the python REPL after processing.")
+    parser.add_argument('--source-version', action="store_true",
+        help="Don't apply the patch; instead print out the version of the "
+        "package that it is supposed to be applied to, or nothing if the patch "
+        "does not specify a source version.")
+    parser.add_argument('--target-version', action="store_true",
+        help="Don't apply the patch; instead print out the new version of the "
+        "package debpatch(1) would generate, when the patch is applied to the "
+        "the given target package specified by the other arguments.")
+    parser.add_argument('orig_dsc_or_dir', nargs='?', default=".",
+        help="Target to apply the patch to. This can either be an unpacked "
+        "source tree, or a .dsc file. In the former case, the directory is "
+        "modified in-place; in the latter case, a second .dsc is created. "
+        "Default: %(default)s")
+    parser.add_argument('patch_file', nargs='?', default="/dev/stdin",
+        help="Patch file to apply, in the format output by debdiff(1). "
+        "Default: %(default)s")
+    group1 = parser.add_argument_group('Options for .dsc patch targets')
+    group1.add_argument('--no-clean', action="store_true",
+        help="Don't clean temporary directories after a failure, so you can "
+        "examine what failed.")
+    group1.add_argument('--quilt-refresh', action="store_true",
+        help="If the building of the new source package fails, try to refresh "
+        "patches using quilt(1) then try building it again.")
+    args = parser.parse_args(args)
+    #print(args)
+
+    if args.verbose:
+        logging.getLogger().setLevel(logging.DEBUG)
+
+    with open(args.patch_file, 'rb') as fp:
+        data = fp.read()
+    for enc in TRY_ENCODINGS:
+        try:
+            patch = unidiff.PatchSet(data.splitlines(keepends=True), encoding=enc)
+            break
+        except:
+            if enc == TRY_ENCODINGS[-1]:
+                raise
+            else:
+                continue
+
+    patch_name = '%s:%s' % (
+        basename(args.patch_file),
+        hashlib.sha256(data).hexdigest()[:20 if args.patch_file == '/dev/stdin' else 8])
+    quiet = args.source_version or args.target_version
+    dry_run = args.source_version or args.target_version
+    stdout = subprocess.DEVNULL if quiet else None # user can redirect stderr themselves
+
+    # change directory before applying patches
+    if os.path.isdir(args.orig_dsc_or_dir):
+        os.chdir(args.orig_dsc_or_dir)
+        debpatch(patch, patch_name, args)
+    elif os.path.isfile(args.orig_dsc_or_dir):
+        parts = os.path.splitext(os.path.basename(args.orig_dsc_or_dir))
+        if parts[1] != ".dsc":
+            raise ValueError("unrecognised patch target: %s" % args.orig_dsc_or_dir)
+        temp_dir = tempfile.mkdtemp()
+        try:
+            builddir = os.path.join(temp_dir, parts[0]) # dpkg-source doesn't like existing dirs
+            C(["dpkg-source", "-x", "--skip-patches", args.orig_dsc_or_dir, builddir], stdout=stdout)
+            origdir = os.getcwd()
+            os.chdir(builddir)
+            debpatch(patch, patch_name, args)
+            if dry_run:
+                return
+            os.chdir(origdir)
+            try:
+                C(["dpkg-source", "-b", builddir])
+            except subprocess.CalledProcessError:
+                if args.quilt_refresh:
+                    C(["sh", "-c", """
+set -ex
+export QUILT_PATCHES=debian/patches
+while quilt push; do quilt refresh; done
+"""
+                        ], cwd=builddir)
+                    C(["dpkg-source", "-b", builddir])
+                else:
+                    raise
+        finally:
+            if args.no_clean:
+                logging.warn("you should clean up temp files in %s", temp_dir)
+            else:
+                shutil.rmtree(temp_dir)
+
 if __name__ == "__main__":
     sys.exit(main(sys.argv[1:]))
diff --git a/debrepatch b/debrepatch
index 2ac605f..23f358a 100755
--- a/debrepatch
+++ b/debrepatch
@@ -1,21 +1,19 @@
 #!/bin/sh
 # Apply a debdiff to the latest version of a package, and maybe run a command.
 #
-# Depends: dpkg-dev, quilt
+# Depends: dpkg-dev
 #
 scriptdir="$(readlink -f "$(dirname "$0")")"
 debpatch="$scriptdir/debpatch"
 set -e
 
 USAGE="Usage: $0 [-hyuf] [-p dir] [-b dir] SOURCE_PACKAGE [post_patch_command [args ...]]"
-force_yes=false
 auto_update=false
 force_postpatch=false
 patchdir="./patches"
 builddir="./build"
 while getopts 'hyufb:p:' o; do
     case $o in
-    y )     force_yes=true;;
     u )     auto_update=true;;
     f )     force_postpatch=true;;
     p )     patchdir="$OPTARG";;
@@ -29,8 +27,6 @@ mentioned in d/changelog from the debdiff, then run a command from inside the
 unpacked and patched package source directory.
 
   -h            This help text.
-  -y            Answer "yes" to all questions, e.g. automatically trying to
-                refresh d/patches when a naive source-build fails.
   -u            Run "apt-get update" first; requires sudo.
   -f            Run post_patch_command even if there was no version change,
                 i.e. the current version is the same as what is mentioned in
@@ -48,11 +44,6 @@ log() {
     # TODO: add some colours based on $1
     echo >&2 "$0: $2"
 }
-maybe_ask() { read -p "$1 (press enter to continue) " x >&2; }
-if $force_yes; then
-    maybe_ask() { log I "$1"; }
-fi
-export QUILT_PATCHES=debian/patches
 
 test -n "$1" || { log E "$USAGE"; exit 2; }
 srcpkg="$1"
@@ -82,44 +73,15 @@ oldver="$(LC_ALL="en_US.utf8" apt-get source -s "$srcpkg" | sed -n -e "s/.*versi
 srcdir="$srcpkg-$(echo "$oldver" | sed -re 's/([^:]*:)?(.*)-.*/\2/')"
 apt-get source "$srcpkg"
 olddsc="$(ls -1 *.dsc)" # TODO: probably more correct to use $oldver
-cd "$srcdir"
-"$debpatch" -v "$patchfile"
-
-build_dsc() {
-    # we give "-nc" because:
-    #
-    # (1) some packages use build-dependencies in the "clean" rule and we'd
-    # otherwise have to install them.
-    # (2) this might interfere with our ability to retry build_dsc in case it
-    # fails - e.g. because "clean" might remove source files or whatever.
-    #
-    # Since we did nothing to the package, "-nc" is fine and we can ignore
-    # dpkg-buildpackage's warning about "might contain undesired files".
-    dpkg-buildpackage -d -nc -uc -us -S
-}
-
-refreshed_patches=false
-if ! build_dsc; then
-    maybe_ask "*** Failed to create source package, will refresh patches and try again."
-    { quilt pop -afq || true; while quilt push -q; do quilt refresh; done; } >/dev/null
-    refreshed_patches=true
-
-    build_dsc
-fi
-
-version="$(dpkg-parsechangelog -c1 -SVersion | sed -e 's/^[0-9]*://')"
-newdsc="${srcpkg}_${version}.dsc"
-cd ..
+rm -rf "$srcdir"
+"$debpatch" --verbose --no-clean "$olddsc" "$patchfile"
+newdsc="$(ls -1 *.dsc | grep -v -F "$olddsc")"
 
 log I "$srcpkg patched successfully"
 ls -l "$olddsc" "$newdsc"
 
 debdiff "$olddsc" "$newdsc" > "$patchdir/$srcpkg.patch.new" || true
 log I "updated patch written to $patchdir/$srcpkg.patch.new"
-if $refreshed_patches; then
-    log W "however, we had to refresh d/patches in order to build $newdsc;"
-    log W "you should review the updated patch for unnecessary cruft before re-submitting it."
-fi
 
 if [ -z "$*" ]; then exit 0; fi
 

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/reproducible/debrepatch.git



More information about the Reproducible-commits mailing list