1# quilt.py -- Quilt patch handling 2# Copyright (C) 2011 Canonical Ltd. 3# Copyright (C) 2019 Jelmer Verooij <jelmer@jelmer.uk> 4# 5# Breezy is free software; you can redistribute it and/or modify 6# it under the terms of the GNU General Public License as published by 7# the Free Software Foundation; either version 2 of the License, or 8# (at your option) any later version. 9# 10# Breezy is distributed in the hope that it will be useful, 11# but WITHOUT ANY WARRANTY; without even the implied warranty of 12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13# GNU General Public License for more details. 14# 15# You should have received a copy of the GNU General Public License 16# along with Breezy; if not, write to the Free Software 17# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 18# 19 20"""Quilt patch handling.""" 21 22import errno 23import os 24import signal 25import subprocess 26from ... import ( 27 errors, 28 osutils, 29 trace, 30 ) 31 32 33DEFAULT_PATCHES_DIR = 'patches' 34DEFAULT_SERIES_FILE = 'series' 35 36 37class QuiltError(errors.BzrError): 38 39 _fmt = "An error (%(retcode)d) occurred running quilt: %(stderr)s%(extra)s" 40 41 def __init__(self, retcode, stdout, stderr): 42 self.retcode = retcode 43 self.stderr = stderr 44 if stdout is not None: 45 self.extra = "\n\n%s" % stdout 46 else: 47 self.extra = "" 48 self.stdout = stdout 49 50 51class QuiltNotInstalled(errors.BzrError): 52 53 _fmt = "Quilt is not installed." 54 55 56def run_quilt( 57 args, working_dir, series_file=None, patches_dir=None, quiet=None): 58 """Run quilt. 59 60 :param args: Arguments to quilt 61 :param working_dir: Working dir 62 :param series_file: Optional path to the series file 63 :param patches_dir: Optional path to the patches 64 :param quilt: Whether to be quiet (quilt stderr not to terminal) 65 :raise QuiltError: When running quilt fails 66 """ 67 def subprocess_setup(): 68 signal.signal(signal.SIGPIPE, signal.SIG_DFL) 69 env = {} 70 if patches_dir is not None: 71 env["QUILT_PATCHES"] = patches_dir 72 else: 73 env["QUILT_PATCHES"] = os.path.join(working_dir, DEFAULT_PATCHES_DIR) 74 if series_file is not None: 75 env["QUILT_SERIES"] = series_file 76 else: 77 env["QUILT_SERIES"] = DEFAULT_SERIES_FILE 78 # Hide output if -q is in use. 79 if quiet is None: 80 quiet = trace.is_quiet() 81 if not quiet: 82 stderr = subprocess.STDOUT 83 else: 84 stderr = subprocess.PIPE 85 command = ["quilt"] + args 86 trace.mutter("running: %r", command) 87 if not os.path.isdir(working_dir): 88 raise AssertionError("%s is not a valid directory" % working_dir) 89 try: 90 proc = subprocess.Popen( 91 command, cwd=working_dir, env=env, 92 stdin=subprocess.PIPE, preexec_fn=subprocess_setup, 93 stdout=subprocess.PIPE, stderr=stderr) 94 except OSError as e: 95 if e.errno != errno.ENOENT: 96 raise 97 raise QuiltNotInstalled() 98 (stdout, stderr) = proc.communicate() 99 if proc.returncode not in (0, 2): 100 if stdout is not None: 101 stdout = stdout.decode() 102 if stderr is not None: 103 stderr = stderr.decode() 104 raise QuiltError(proc.returncode, stdout, stderr) 105 if stdout is None: 106 return "" 107 return stdout 108 109 110def quilt_pop_all( 111 working_dir, patches_dir=None, series_file=None, quiet=None, 112 force=False, refresh=False): 113 """Pop all patches. 114 115 :param working_dir: Directory to work in 116 :param patches_dir: Optional patches directory 117 :param series_file: Optional series file 118 """ 119 args = ["pop", "-a"] 120 if force: 121 args.append("-f") 122 if refresh: 123 args.append("--refresh") 124 return run_quilt( 125 args, working_dir=working_dir, 126 patches_dir=patches_dir, series_file=series_file, quiet=quiet) 127 128 129def quilt_pop(working_dir, patch, patches_dir=None, series_file=None, quiet=None): 130 """Pop a patch. 131 132 :param working_dir: Directory to work in 133 :param patch: Patch to apply 134 :param patches_dir: Optional patches directory 135 :param series_file: Optional series file 136 """ 137 return run_quilt( 138 ["pop", patch], working_dir=working_dir, 139 patches_dir=patches_dir, series_file=series_file, quiet=quiet) 140 141 142def quilt_push_all(working_dir, patches_dir=None, series_file=None, quiet=None, 143 force=False, refresh=False): 144 """Push all patches. 145 146 :param working_dir: Directory to work in 147 :param patches_dir: Optional patches directory 148 :param series_file: Optional series file 149 """ 150 args = ["push", "-a"] 151 if force: 152 args.append("-f") 153 if refresh: 154 args.append("--refresh") 155 return run_quilt( 156 args, working_dir=working_dir, 157 patches_dir=patches_dir, series_file=series_file, quiet=quiet) 158 159 160def quilt_push(working_dir, patch, patches_dir=None, series_file=None, 161 quiet=None, force=False, refresh=False): 162 """Push a patch. 163 164 :param working_dir: Directory to work in 165 :param patch: Patch to push 166 :param patches_dir: Optional patches directory 167 :param series_file: Optional series file 168 :param force: Force push 169 :param refresh: Refresh 170 """ 171 args = [] 172 if force: 173 args.append("-f") 174 if refresh: 175 args.append("--refresh") 176 return run_quilt( 177 ["push", patch] + args, working_dir=working_dir, 178 patches_dir=patches_dir, series_file=series_file, quiet=quiet) 179 180 181def quilt_delete(working_dir, patch, patches_dir=None, series_file=None, 182 remove=False): 183 """Delete a patch. 184 185 :param working_dir: Directory to work in 186 :param patch: Patch to push 187 :param patches_dir: Optional patches directory 188 :param series_file: Optional series file 189 :param remove: Remove the patch file as well 190 """ 191 args = [] 192 if remove: 193 args.append("-r") 194 return run_quilt( 195 ["delete", patch] + args, working_dir=working_dir, 196 patches_dir=patches_dir, series_file=series_file) 197 198 199def quilt_upgrade(working_dir): 200 return run_quilt(["upgrade"], working_dir=working_dir) 201 202 203def quilt_applied(tree): 204 """Find the list of applied quilt patches. 205 206 """ 207 try: 208 return [patch.rstrip(b"\n").decode(osutils._fs_enc) 209 for patch in tree.get_file_lines(".pc/applied-patches") 210 if patch.strip() != b""] 211 except errors.NoSuchFile: 212 return [] 213 except (IOError, OSError) as e: 214 if e.errno == errno.ENOENT: 215 # File has already been removed 216 return [] 217 raise 218 219 220def quilt_unapplied(working_dir, patches_dir=None, series_file=None): 221 """Find the list of unapplied quilt patches. 222 223 :param working_dir: Directory to work in 224 :param patches_dir: Optional patches directory 225 :param series_file: Optional series file 226 """ 227 working_dir = os.path.abspath(working_dir) 228 if patches_dir is None: 229 patches_dir = os.path.join(working_dir, DEFAULT_PATCHES_DIR) 230 try: 231 unapplied_patches = run_quilt( 232 ["unapplied"], 233 working_dir=working_dir, patches_dir=patches_dir, 234 series_file=series_file).splitlines() 235 patch_names = [] 236 for patch in unapplied_patches: 237 patch = patch.decode(osutils._fs_enc) 238 patch_names.append(os.path.relpath(patch, patches_dir)) 239 return patch_names 240 except QuiltError as e: 241 if e.retcode == 1: 242 return [] 243 raise 244 245 246def quilt_series(tree, series_path): 247 """Find the list of patches. 248 249 :param tree: Tree to read from 250 """ 251 try: 252 return [patch.rstrip(b"\n").decode(osutils._fs_enc) for patch in 253 tree.get_file_lines(series_path) 254 if patch.strip() != b""] 255 except (IOError, OSError) as e: 256 if e.errno == errno.ENOENT: 257 # File has already been removed 258 return [] 259 raise 260 except errors.NoSuchFile: 261 return [] 262