make_patch.py
Go to the documentation of this file.
1 #!/usr/bin/env python
2 
3 import os, sys, re
4 from subprocess import Popen, PIPE, STDOUT
5 from fnmatch import fnmatch
6 import logging
7 
8 def command(cmd, *args, **kwargs):
9  """
10  Simple wrapper to execute a command and return standard output and standard error.
11  """
12  d = {"stdout": PIPE, "stderr": PIPE}
13  d.update(kwargs)
14  cmd = [cmd] + list(args)
15  logging.debug("Execute command: %r %r", " ".join(cmd), kwargs)
16  proc = apply(Popen, (cmd,), d)
17  return proc.communicate()
18 
19 cmt = lambda *args, **kwargs: apply(command, ("cmt",) + args, kwargs)
20 cvs = lambda *args, **kwargs: apply(command, ("cvs",) + args, kwargs)
21 svn = lambda *args, **kwargs: apply(command, ("svn",) + args, kwargs)
22 
24  """
25  Find the local packages the current one depends on (using 'cmt broadcast').
26  Returns a list of pairs ("package name","path to the cmt directory").
27  """
28  # make cmt print one line per package with python syntax
29  if not sys.platform.startswith("win"):
30  pkg_dirs = "[\n" + cmt("broadcast",r'echo "(\"<package>\", \"$PWD\"),"')[0] + ']'
31  else:
32  pkg_dirs = "[\n" + cmt("broadcast",r'echo ("<package>", r"%<package>root%\cmt"),')[0] + ']'
33  # Clean up a bit the output (actually needed only on Windows because of the newlines)
34  pkg_dirs = "\n".join([l.strip() for l in pkg_dirs.splitlines() if not l.startswith("#")])
35  return eval(pkg_dirs)
36 
37 def matches(filename, patterns):
38  """
39  Returns True if any of the specified glob patterns (list of strings) matched
40  the string 'filename'.
41  """
42  for p in patterns:
43  if fnmatch(filename, p):
44  logging.debug("Excluding file: %r", filename)
45  return True
46  return False
47 
48 def expand_dirs(files, basepath = ""):
49  """
50  Replace the entries in files that correspond to directories with the list of
51  files in those directories.
52  """
53  if basepath:
54  lb = len(basepath)+1
55  else:
56  lb = 0
57  newlist = []
58  for f in files:
59  base = os.path.join(basepath, f)
60  if os.path.isdir(base):
61  for root, ds, fs in os.walk(base):
62  for ff in fs:
63  newlist.append(os.path.join(root,ff)[lb:])
64  else:
65  newlist.append(f)
66  return newlist
67 
69  if os.path.isdir(os.path.join(cwd, "CVS")):
70  return cvs("diff", "-upN", cwd = cwd)
71  else:
72  # special treatment to show new files in a way compatible with CVS
73  out, err = svn("status", cwd = cwd)
74 
75  newfiles = [ l
76  for l in out.splitlines()
77  if l.startswith("? ") ]
78  out, err = svn("diff", cwd = cwd)
79  if newfiles:
80  out = "\n".join(newfiles) + "\n" + out
81  return out, err
82 
83 def diff_pkg(name, cmtdir, exclusions = []):
84  """
85  Return the patch data for a package.
86  """
87  rootdir = os.path.dirname(cmtdir)
88  out, err = revision_diff_cmd(cwd = rootdir)
89  # extract new files
90  new_files = [ l.split(None,1)[1]
91  for l in out.splitlines()
92  if l.startswith("? ") ]
93  new_files = expand_dirs(new_files, rootdir)
94  new_files = [ f
95  for f in new_files
96  if not matches(f, exclusions) ]
97  # make diff segments for added files
98  for f in new_files:
99  logging.info("Added file %r", f)
100  #out += "diff -u -p -N %s\n" % os.path.basename(f)
101  #out += command("diff", "-upN", "/dev/null", f,
102  # cwd = rootdir)[0]
103  out += "Index: %s\n" % f
104  out += "===================================================================\n"
105  out += command("diff", "-upN", "/dev/null", f,
106  cwd = rootdir)[0]
107  # extract removed files
108  removed_files = [ l.split()[-1]
109  for l in err.splitlines()
110  if "cannot find" in l ]
111  removed_files = [ f
112  for f in removed_files
113  if not matches(f, exclusions) ]
114  # make diff segments for removed files (more tricky)
115  for f in removed_files:
116  logging.info("Removed file %r", f)
117  # retrieve the original content from CVS
118  orig = cvs("up", "-p", f,
119  cwd = rootdir)[0]
120  out += "diff -u -p -N %s\n" % os.path.basename(f)
121  out += "--- %s\t1 Jan 1970 00:00:00 -0000\n" % f
122  out += "+++ /dev/null\t1 Jan 1970 00:00:00 -0000\n"
123  lines = orig.splitlines()
124  out += "@@ -1,%d +0,0 @@\n" % len(lines)
125  for l in lines:
126  out += '-%s\n' % l
127  # Fix the paths to have the package names
128  rex = re.compile(r"^(Index: |\? |\+\+\+ |--- (?!/dev/null))", re.MULTILINE)
129  out = rex.sub(r"\1%s/" % name, out)
130  return out
131 
132 def main():
133  from optparse import OptionParser
134  parser = OptionParser(description = "Produce a patch file from a CMT project. "
135  "The patch contains the changes with respect "
136  "to the CVS repository, including new files "
137  "that are present only locally. Run the script "
138  "from the cmt directory of a package." )
139  parser.add_option("-x", "--exclude", action="append", type="string",
140  metavar="PATTERN", dest="exclusions",
141  help="Pattern to exclude new files from the patch")
142  parser.add_option("-o", "--output", action="store", type="string",
143  help="Name of the file to send the output to. Standard "
144  "output is used if not specified")
145  parser.add_option("-v", "--verbose", action="store_true",
146  help="Print some progress information on standard error")
147  parser.add_option("--debug", action="store_true",
148  help="Print debug information on standard error")
149  parser.set_defaults(exclusions = [])
150 
151  opts, args = parser.parse_args()
152 
153  if opts.debug:
154  logging.basicConfig(level = logging.DEBUG)
155  elif opts.verbose:
156  logging.basicConfig(level = logging.INFO)
157 
158  # default exclusions
159  opts.exclusions += [ "*.py[co]",
160  "*.patch",
161  "cmt/cleanup.*",
162  "cmt/setup.*",
163  "cmt/*.make",
164  "cmt/Makefile",
165  "cmt/*.nmake",
166  "cmt/*.nmakesav",
167  "cmt/NMake",
168  "cmt/install*.history",
169  "cmt/build.*.log",
170  "cmt/version.cmt",
171  "genConf",
172  "slc3_ia32_gcc323*",
173  "slc4_ia32_gcc34*",
174  "slc4_amd64_gcc34*",
175  "slc4_amd64_gcc43*",
176  "win32_vc71*",
177  "i686-slc[34567]-[ig]cc*",
178  "i686-slc[34567]-clang*",
179  "x86_64-slc[34567]-[ig]cc*",
180  "x86_64-slc[34567]-clang*",
181  ".eclipse",
182  ]
183  if "CMTCONFIG" in os.environ:
184  opts.exclusions.append(os.environ["CMTCONFIG"])
185 
186  # check if we are in the cmt directory before broadcasting
187  if not (os.path.basename(os.getcwd()) == "cmt" and os.path.exists("requirements")):
188  logging.error("This script must be executed from the cmt directory of a package.")
189  return 1
190 
191  pkgs = broadcast_packages()
192  num_pkgs = len(pkgs)
193  count = 0
194 
195  patch = ""
196  for name, path in pkgs:
197  count += 1
198  logging.info("Processing %s from %s (%d/%d)",
199  name, os.path.dirname(path), count, num_pkgs)
200  patch += diff_pkg(name, path, opts.exclusions)
201 
202  if sys.platform.startswith("win"):
203  # fix newlines chars
204  patch = patch.replace("\r","")
205 
206  if opts.output:
207  logging.info("Writing patch file %r", opts.output)
208  open(opts.output,"w").write(patch)
209  else:
210  sys.stdout.write(patch)
211  return 0
212 
213 if __name__ == "__main__":
214  sys.exit(main())
def expand_dirs
Definition: make_patch.py:48
def command(cmd, args, kwargs)
Definition: make_patch.py:8
tuple cvs
Definition: make_patch.py:20
def revision_diff_cmd(cwd)
Definition: make_patch.py:68
def broadcast_packages()
Definition: make_patch.py:23
def matches(filename, patterns)
Definition: make_patch.py:37
tuple svn
Definition: make_patch.py:21
tuple cmt
Definition: make_patch.py:19
def main()
Definition: make_patch.py:132
def diff_pkg
Definition: make_patch.py:83