xref: /OK3568_Linux_fs/yocto/scripts/combo-layer (revision 4882a59341e53eb6f0b4789bf948001014eff981)
1*4882a593Smuzhiyun#!/usr/bin/env python3
2*4882a593Smuzhiyun# ex:ts=4:sw=4:sts=4:et
3*4882a593Smuzhiyun# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
4*4882a593Smuzhiyun#
5*4882a593Smuzhiyun# Copyright 2011 Intel Corporation
6*4882a593Smuzhiyun# Authored-by:  Yu Ke <ke.yu@intel.com>
7*4882a593Smuzhiyun#               Paul Eggleton <paul.eggleton@intel.com>
8*4882a593Smuzhiyun#               Richard Purdie <richard.purdie@intel.com>
9*4882a593Smuzhiyun#
10*4882a593Smuzhiyun# SPDX-License-Identifier: GPL-2.0-only
11*4882a593Smuzhiyun#
12*4882a593Smuzhiyun
13*4882a593Smuzhiyunimport fnmatch
14*4882a593Smuzhiyunimport os, sys
15*4882a593Smuzhiyunimport optparse
16*4882a593Smuzhiyunimport logging
17*4882a593Smuzhiyunimport subprocess
18*4882a593Smuzhiyunimport tempfile
19*4882a593Smuzhiyunimport configparser
20*4882a593Smuzhiyunimport re
21*4882a593Smuzhiyunimport copy
22*4882a593Smuzhiyunimport pipes
23*4882a593Smuzhiyunimport shutil
24*4882a593Smuzhiyunfrom string import Template
25*4882a593Smuzhiyunfrom functools import reduce
26*4882a593Smuzhiyun
27*4882a593Smuzhiyun__version__ = "0.2.1"
28*4882a593Smuzhiyun
29*4882a593Smuzhiyundef logger_create():
30*4882a593Smuzhiyun    logger = logging.getLogger("")
31*4882a593Smuzhiyun    loggerhandler = logging.StreamHandler()
32*4882a593Smuzhiyun    loggerhandler.setFormatter(logging.Formatter("[%(asctime)s] %(message)s","%H:%M:%S"))
33*4882a593Smuzhiyun    logger.addHandler(loggerhandler)
34*4882a593Smuzhiyun    logger.setLevel(logging.INFO)
35*4882a593Smuzhiyun    return logger
36*4882a593Smuzhiyun
37*4882a593Smuzhiyunlogger = logger_create()
38*4882a593Smuzhiyun
39*4882a593Smuzhiyundef get_current_branch(repodir=None):
40*4882a593Smuzhiyun    try:
41*4882a593Smuzhiyun        if not os.path.exists(os.path.join(repodir if repodir else '', ".git")):
42*4882a593Smuzhiyun            # Repo not created yet (i.e. during init) so just assume master
43*4882a593Smuzhiyun            return "master"
44*4882a593Smuzhiyun        branchname = runcmd("git symbolic-ref HEAD 2>/dev/null", repodir).strip()
45*4882a593Smuzhiyun        if branchname.startswith("refs/heads/"):
46*4882a593Smuzhiyun            branchname = branchname[11:]
47*4882a593Smuzhiyun        return branchname
48*4882a593Smuzhiyun    except subprocess.CalledProcessError:
49*4882a593Smuzhiyun        return ""
50*4882a593Smuzhiyun
51*4882a593Smuzhiyunclass Configuration(object):
52*4882a593Smuzhiyun    """
53*4882a593Smuzhiyun    Manages the configuration
54*4882a593Smuzhiyun
55*4882a593Smuzhiyun    For an example config file, see combo-layer.conf.example
56*4882a593Smuzhiyun
57*4882a593Smuzhiyun    """
58*4882a593Smuzhiyun    def __init__(self, options):
59*4882a593Smuzhiyun        for key, val in options.__dict__.items():
60*4882a593Smuzhiyun            setattr(self, key, val)
61*4882a593Smuzhiyun
62*4882a593Smuzhiyun        def readsection(parser, section, repo):
63*4882a593Smuzhiyun            for (name, value) in parser.items(section):
64*4882a593Smuzhiyun                if value.startswith("@"):
65*4882a593Smuzhiyun                    self.repos[repo][name] = eval(value.strip("@"))
66*4882a593Smuzhiyun                else:
67*4882a593Smuzhiyun                    # Apply special type transformations for some properties.
68*4882a593Smuzhiyun                    # Type matches the RawConfigParser.get*() methods.
69*4882a593Smuzhiyun                    types = {'signoff': 'boolean', 'update': 'boolean', 'history': 'boolean'}
70*4882a593Smuzhiyun                    if name in types:
71*4882a593Smuzhiyun                        value = getattr(parser, 'get' + types[name])(section, name)
72*4882a593Smuzhiyun                    self.repos[repo][name] = value
73*4882a593Smuzhiyun
74*4882a593Smuzhiyun        def readglobalsection(parser, section):
75*4882a593Smuzhiyun            for (name, value) in parser.items(section):
76*4882a593Smuzhiyun                if name == "commit_msg":
77*4882a593Smuzhiyun                    self.commit_msg_template = value
78*4882a593Smuzhiyun
79*4882a593Smuzhiyun        logger.debug("Loading config file %s" % self.conffile)
80*4882a593Smuzhiyun        self.parser = configparser.ConfigParser()
81*4882a593Smuzhiyun        with open(self.conffile) as f:
82*4882a593Smuzhiyun            self.parser.read_file(f)
83*4882a593Smuzhiyun
84*4882a593Smuzhiyun        # initialize default values
85*4882a593Smuzhiyun        self.commit_msg_template = "Automatic commit to update last_revision"
86*4882a593Smuzhiyun
87*4882a593Smuzhiyun        self.repos = {}
88*4882a593Smuzhiyun        for repo in self.parser.sections():
89*4882a593Smuzhiyun            if repo == "combo-layer-settings":
90*4882a593Smuzhiyun                # special handling for global settings
91*4882a593Smuzhiyun                readglobalsection(self.parser, repo)
92*4882a593Smuzhiyun            else:
93*4882a593Smuzhiyun                self.repos[repo] = {}
94*4882a593Smuzhiyun                readsection(self.parser, repo, repo)
95*4882a593Smuzhiyun
96*4882a593Smuzhiyun        # Load local configuration, if available
97*4882a593Smuzhiyun        self.localconffile = None
98*4882a593Smuzhiyun        self.localparser = None
99*4882a593Smuzhiyun        self.combobranch = None
100*4882a593Smuzhiyun        if self.conffile.endswith('.conf'):
101*4882a593Smuzhiyun            lcfile = self.conffile.replace('.conf', '-local.conf')
102*4882a593Smuzhiyun            if os.path.exists(lcfile):
103*4882a593Smuzhiyun                # Read combo layer branch
104*4882a593Smuzhiyun                self.combobranch = get_current_branch()
105*4882a593Smuzhiyun                logger.debug("Combo layer branch is %s" % self.combobranch)
106*4882a593Smuzhiyun
107*4882a593Smuzhiyun                self.localconffile = lcfile
108*4882a593Smuzhiyun                logger.debug("Loading local config file %s" % self.localconffile)
109*4882a593Smuzhiyun                self.localparser = configparser.ConfigParser()
110*4882a593Smuzhiyun                with open(self.localconffile) as f:
111*4882a593Smuzhiyun                    self.localparser.readfp(f)
112*4882a593Smuzhiyun
113*4882a593Smuzhiyun                for section in self.localparser.sections():
114*4882a593Smuzhiyun                    if '|' in section:
115*4882a593Smuzhiyun                        sectionvals = section.split('|')
116*4882a593Smuzhiyun                        repo = sectionvals[0]
117*4882a593Smuzhiyun                        if sectionvals[1] != self.combobranch:
118*4882a593Smuzhiyun                            continue
119*4882a593Smuzhiyun                    else:
120*4882a593Smuzhiyun                        repo = section
121*4882a593Smuzhiyun                    if repo in self.repos:
122*4882a593Smuzhiyun                        readsection(self.localparser, section, repo)
123*4882a593Smuzhiyun
124*4882a593Smuzhiyun    def update(self, repo, option, value, initmode=False):
125*4882a593Smuzhiyun        # If the main config has the option already, that is what we
126*4882a593Smuzhiyun        # are expected to modify.
127*4882a593Smuzhiyun        if self.localparser and not self.parser.has_option(repo, option):
128*4882a593Smuzhiyun            parser = self.localparser
129*4882a593Smuzhiyun            section = "%s|%s" % (repo, self.combobranch)
130*4882a593Smuzhiyun            conffile = self.localconffile
131*4882a593Smuzhiyun            if initmode and not parser.has_section(section):
132*4882a593Smuzhiyun                parser.add_section(section)
133*4882a593Smuzhiyun        else:
134*4882a593Smuzhiyun            parser = self.parser
135*4882a593Smuzhiyun            section = repo
136*4882a593Smuzhiyun            conffile = self.conffile
137*4882a593Smuzhiyun        parser.set(section, option, value)
138*4882a593Smuzhiyun        with open(conffile, "w") as f:
139*4882a593Smuzhiyun            parser.write(f)
140*4882a593Smuzhiyun        self.repos[repo][option] = value
141*4882a593Smuzhiyun
142*4882a593Smuzhiyun    def sanity_check(self, initmode=False):
143*4882a593Smuzhiyun        required_options=["src_uri", "local_repo_dir", "dest_dir", "last_revision"]
144*4882a593Smuzhiyun        if initmode:
145*4882a593Smuzhiyun            required_options.remove("last_revision")
146*4882a593Smuzhiyun        msg = ""
147*4882a593Smuzhiyun        missing_options = []
148*4882a593Smuzhiyun        for name in self.repos:
149*4882a593Smuzhiyun            for option in required_options:
150*4882a593Smuzhiyun                if option not in self.repos[name]:
151*4882a593Smuzhiyun                    msg = "%s\nOption %s is not defined for component %s" %(msg, option, name)
152*4882a593Smuzhiyun                    missing_options.append(option)
153*4882a593Smuzhiyun            # Sanitize dest_dir so that we do not have to deal with edge cases
154*4882a593Smuzhiyun            # (unset, empty string, double slashes) in the rest of the code.
155*4882a593Smuzhiyun            # It not being set will still be flagged as error because it is
156*4882a593Smuzhiyun            # listed as required option above; that could be changed now.
157*4882a593Smuzhiyun            dest_dir = os.path.normpath(self.repos[name].get("dest_dir", "."))
158*4882a593Smuzhiyun            self.repos[name]["dest_dir"] = "." if not dest_dir else dest_dir
159*4882a593Smuzhiyun        if msg != "":
160*4882a593Smuzhiyun            logger.error("configuration file %s has the following error: %s" % (self.conffile,msg))
161*4882a593Smuzhiyun            if self.localconffile and 'last_revision' in missing_options:
162*4882a593Smuzhiyun                logger.error("local configuration file %s may be missing configuration for combo branch %s" % (self.localconffile, self.combobranch))
163*4882a593Smuzhiyun            sys.exit(1)
164*4882a593Smuzhiyun
165*4882a593Smuzhiyun        # filterdiff is required by action_splitpatch, so check its availability
166*4882a593Smuzhiyun        if subprocess.call("which filterdiff > /dev/null 2>&1", shell=True) != 0:
167*4882a593Smuzhiyun            logger.error("ERROR: patchutils package is missing, please install it (e.g. # apt-get install patchutils)")
168*4882a593Smuzhiyun            sys.exit(1)
169*4882a593Smuzhiyun
170*4882a593Smuzhiyundef runcmd(cmd,destdir=None,printerr=True,out=None,env=None):
171*4882a593Smuzhiyun    """
172*4882a593Smuzhiyun        execute command, raise CalledProcessError if fail
173*4882a593Smuzhiyun        return output if succeed
174*4882a593Smuzhiyun    """
175*4882a593Smuzhiyun    logger.debug("run cmd '%s' in %s" % (cmd, os.getcwd() if destdir is None else destdir))
176*4882a593Smuzhiyun    if not out:
177*4882a593Smuzhiyun        out = tempfile.TemporaryFile()
178*4882a593Smuzhiyun        err = out
179*4882a593Smuzhiyun    else:
180*4882a593Smuzhiyun        err = tempfile.TemporaryFile()
181*4882a593Smuzhiyun    try:
182*4882a593Smuzhiyun        subprocess.check_call(cmd, stdout=out, stderr=err, cwd=destdir, shell=isinstance(cmd, str), env=env or os.environ)
183*4882a593Smuzhiyun    except subprocess.CalledProcessError as e:
184*4882a593Smuzhiyun        err.seek(0)
185*4882a593Smuzhiyun        if printerr:
186*4882a593Smuzhiyun            logger.error("%s" % err.read())
187*4882a593Smuzhiyun        raise e
188*4882a593Smuzhiyun
189*4882a593Smuzhiyun    err.seek(0)
190*4882a593Smuzhiyun    output = err.read().decode('utf-8')
191*4882a593Smuzhiyun    logger.debug("output: %s" % output.replace(chr(0), '\\0'))
192*4882a593Smuzhiyun    return output
193*4882a593Smuzhiyun
194*4882a593Smuzhiyundef action_sync_revs(conf, args):
195*4882a593Smuzhiyun    """
196*4882a593Smuzhiyun    Update the last_revision config option for each repo with the latest
197*4882a593Smuzhiyun    revision in the remote's branch. Useful if multiple people are using
198*4882a593Smuzhiyun    combo-layer.
199*4882a593Smuzhiyun    """
200*4882a593Smuzhiyun    repos = get_repos(conf, args[1:])
201*4882a593Smuzhiyun
202*4882a593Smuzhiyun    for name in repos:
203*4882a593Smuzhiyun        repo = conf.repos[name]
204*4882a593Smuzhiyun        ldir = repo['local_repo_dir']
205*4882a593Smuzhiyun        branch = repo.get('branch', "master")
206*4882a593Smuzhiyun        runcmd("git fetch", ldir)
207*4882a593Smuzhiyun        lastrev = runcmd('git rev-parse origin/%s' % branch, ldir).strip()
208*4882a593Smuzhiyun        print("Updating %s to %s" % (name, lastrev))
209*4882a593Smuzhiyun        conf.update(name, "last_revision", lastrev)
210*4882a593Smuzhiyun
211*4882a593Smuzhiyundef action_init(conf, args):
212*4882a593Smuzhiyun    """
213*4882a593Smuzhiyun        Clone component repositories
214*4882a593Smuzhiyun        Check git is initialised; if not, copy initial data from component repos
215*4882a593Smuzhiyun    """
216*4882a593Smuzhiyun    for name in conf.repos:
217*4882a593Smuzhiyun        ldir = conf.repos[name]['local_repo_dir']
218*4882a593Smuzhiyun        if not os.path.exists(ldir):
219*4882a593Smuzhiyun            logger.info("cloning %s to %s" %(conf.repos[name]['src_uri'], ldir))
220*4882a593Smuzhiyun            subprocess.check_call("git clone %s %s" % (conf.repos[name]['src_uri'], ldir), shell=True)
221*4882a593Smuzhiyun    if not os.path.exists(".git"):
222*4882a593Smuzhiyun        runcmd("git init")
223*4882a593Smuzhiyun        if conf.history:
224*4882a593Smuzhiyun            # Need a common ref for all trees.
225*4882a593Smuzhiyun            runcmd('git commit -m "initial empty commit" --allow-empty')
226*4882a593Smuzhiyun            startrev = runcmd('git rev-parse master').strip()
227*4882a593Smuzhiyun
228*4882a593Smuzhiyun        for name in conf.repos:
229*4882a593Smuzhiyun            repo = conf.repos[name]
230*4882a593Smuzhiyun            ldir = repo['local_repo_dir']
231*4882a593Smuzhiyun            branch = repo.get('branch', "master")
232*4882a593Smuzhiyun            lastrev = repo.get('last_revision', None)
233*4882a593Smuzhiyun            if lastrev and lastrev != "HEAD":
234*4882a593Smuzhiyun                initialrev = lastrev
235*4882a593Smuzhiyun                if branch:
236*4882a593Smuzhiyun                    if not check_rev_branch(name, ldir, lastrev, branch):
237*4882a593Smuzhiyun                        sys.exit(1)
238*4882a593Smuzhiyun                logger.info("Copying data from %s at specified revision %s..." % (name, lastrev))
239*4882a593Smuzhiyun            else:
240*4882a593Smuzhiyun                lastrev = None
241*4882a593Smuzhiyun                initialrev = branch
242*4882a593Smuzhiyun                logger.info("Copying data from %s..." % name)
243*4882a593Smuzhiyun            # Sanity check initialrev and turn it into hash (required for copying history,
244*4882a593Smuzhiyun            # because resolving a name ref only works in the component repo).
245*4882a593Smuzhiyun            rev = runcmd('git rev-parse %s' % initialrev, ldir).strip()
246*4882a593Smuzhiyun            if rev != initialrev:
247*4882a593Smuzhiyun                try:
248*4882a593Smuzhiyun                    refs = runcmd('git show-ref -s %s' % initialrev, ldir).split('\n')
249*4882a593Smuzhiyun                    if len(set(refs)) > 1:
250*4882a593Smuzhiyun                        # Happens for example when configured to track
251*4882a593Smuzhiyun                        # "master" and there is a refs/heads/master. The
252*4882a593Smuzhiyun                        # traditional behavior from "git archive" (preserved
253*4882a593Smuzhiyun                        # here) it to choose the first one. This might not be
254*4882a593Smuzhiyun                        # intended, so at least warn about it.
255*4882a593Smuzhiyun                        logger.warning("%s: initial revision '%s' not unique, picking result of rev-parse = %s" %
256*4882a593Smuzhiyun                                    (name, initialrev, refs[0]))
257*4882a593Smuzhiyun                        initialrev = rev
258*4882a593Smuzhiyun                except:
259*4882a593Smuzhiyun                    # show-ref fails for hashes. Skip the sanity warning in that case.
260*4882a593Smuzhiyun                    pass
261*4882a593Smuzhiyun                initialrev = rev
262*4882a593Smuzhiyun            dest_dir = repo['dest_dir']
263*4882a593Smuzhiyun            if dest_dir != ".":
264*4882a593Smuzhiyun                extract_dir = os.path.join(os.getcwd(), dest_dir)
265*4882a593Smuzhiyun                if not os.path.exists(extract_dir):
266*4882a593Smuzhiyun                    os.makedirs(extract_dir)
267*4882a593Smuzhiyun            else:
268*4882a593Smuzhiyun                extract_dir = os.getcwd()
269*4882a593Smuzhiyun            file_filter = repo.get('file_filter', "")
270*4882a593Smuzhiyun            exclude_patterns = repo.get('file_exclude', '').split()
271*4882a593Smuzhiyun            def copy_selected_files(initialrev, extract_dir, file_filter, exclude_patterns, ldir,
272*4882a593Smuzhiyun                                    subdir=""):
273*4882a593Smuzhiyun                # When working inside a filtered branch which had the
274*4882a593Smuzhiyun                # files already moved, we need to prepend the
275*4882a593Smuzhiyun                # subdirectory to all filters, otherwise they would
276*4882a593Smuzhiyun                # not match.
277*4882a593Smuzhiyun                if subdir == '.':
278*4882a593Smuzhiyun                    subdir = ''
279*4882a593Smuzhiyun                elif subdir:
280*4882a593Smuzhiyun                    subdir = os.path.normpath(subdir)
281*4882a593Smuzhiyun                    file_filter = ' '.join([subdir + '/' + x for x in file_filter.split()])
282*4882a593Smuzhiyun                    exclude_patterns = [subdir + '/' + x for x in exclude_patterns]
283*4882a593Smuzhiyun                # To handle both cases, we cd into the target
284*4882a593Smuzhiyun                # directory and optionally tell tar to strip the path
285*4882a593Smuzhiyun                # prefix when the files were already moved.
286*4882a593Smuzhiyun                subdir_components = len(subdir.split(os.path.sep)) if subdir else 0
287*4882a593Smuzhiyun                strip=('--strip-components=%d' % subdir_components) if subdir else ''
288*4882a593Smuzhiyun                # TODO: file_filter wild cards do not work (and haven't worked before either), because
289*4882a593Smuzhiyun                # a) GNU tar requires a --wildcards parameter before turning on wild card matching.
290*4882a593Smuzhiyun                # b) The semantic is not as intendend (src/*.c also matches src/foo/bar.c,
291*4882a593Smuzhiyun                #    in contrast to the other use of file_filter as parameter of "git archive"
292*4882a593Smuzhiyun                #    where it only matches .c files directly in src).
293*4882a593Smuzhiyun                files = runcmd("git archive %s %s | tar -x -v %s -C %s %s" %
294*4882a593Smuzhiyun                               (initialrev, subdir,
295*4882a593Smuzhiyun                                strip, extract_dir, file_filter),
296*4882a593Smuzhiyun                               ldir)
297*4882a593Smuzhiyun                if exclude_patterns:
298*4882a593Smuzhiyun                    # Implement file removal by letting tar create the
299*4882a593Smuzhiyun                    # file and then deleting it in the file system
300*4882a593Smuzhiyun                    # again. Uses the list of files created by tar (easier
301*4882a593Smuzhiyun                    # than walking the tree).
302*4882a593Smuzhiyun                    for file in files.split('\n'):
303*4882a593Smuzhiyun                        if file.endswith(os.path.sep):
304*4882a593Smuzhiyun                            continue
305*4882a593Smuzhiyun                        for pattern in exclude_patterns:
306*4882a593Smuzhiyun                            if fnmatch.fnmatch(file, pattern):
307*4882a593Smuzhiyun                                os.unlink(os.path.join(*([extract_dir] + ['..'] * subdir_components + [file])))
308*4882a593Smuzhiyun                                break
309*4882a593Smuzhiyun
310*4882a593Smuzhiyun            if not conf.history:
311*4882a593Smuzhiyun                copy_selected_files(initialrev, extract_dir, file_filter, exclude_patterns, ldir)
312*4882a593Smuzhiyun            else:
313*4882a593Smuzhiyun                # First fetch remote history into local repository.
314*4882a593Smuzhiyun                # We need a ref for that, so ensure that there is one.
315*4882a593Smuzhiyun                refname = "combo-layer-init-%s" % name
316*4882a593Smuzhiyun                runcmd("git branch -f %s %s" % (refname, initialrev), ldir)
317*4882a593Smuzhiyun                runcmd("git fetch %s %s" % (ldir, refname))
318*4882a593Smuzhiyun                runcmd("git branch -D %s" % refname, ldir)
319*4882a593Smuzhiyun                # Make that the head revision.
320*4882a593Smuzhiyun                runcmd("git checkout -b %s %s" % (name, initialrev))
321*4882a593Smuzhiyun                # Optional: cut the history by replacing the given
322*4882a593Smuzhiyun                # start point(s) with commits providing the same
323*4882a593Smuzhiyun                # content (aka tree), but with commit information that
324*4882a593Smuzhiyun                # makes it clear that this is an artifically created
325*4882a593Smuzhiyun                # commit and nothing the original authors had anything
326*4882a593Smuzhiyun                # to do with.
327*4882a593Smuzhiyun                since_rev = repo.get('since_revision', '')
328*4882a593Smuzhiyun                if since_rev:
329*4882a593Smuzhiyun                    committer = runcmd('git var GIT_AUTHOR_IDENT').strip()
330*4882a593Smuzhiyun                    # Same time stamp, no name.
331*4882a593Smuzhiyun                    author = re.sub('.* (\d+ [+-]\d+)', r'unknown <unknown> \1', committer)
332*4882a593Smuzhiyun                    logger.info('author %s' % author)
333*4882a593Smuzhiyun                    for rev in since_rev.split():
334*4882a593Smuzhiyun                        # Resolve in component repo...
335*4882a593Smuzhiyun                        rev = runcmd('git log --oneline --no-abbrev-commit -n1 %s' % rev, ldir).split()[0]
336*4882a593Smuzhiyun                        # ... and then get the tree in current
337*4882a593Smuzhiyun                        # one. The commit should be in both repos with
338*4882a593Smuzhiyun                        # the same tree, but better check here.
339*4882a593Smuzhiyun                        tree = runcmd('git show -s --pretty=format:%%T %s' % rev).strip()
340*4882a593Smuzhiyun                        with tempfile.NamedTemporaryFile(mode='wt') as editor:
341*4882a593Smuzhiyun                            editor.write('''cat >$1 <<EOF
342*4882a593Smuzhiyuntree %s
343*4882a593Smuzhiyunauthor %s
344*4882a593Smuzhiyuncommitter %s
345*4882a593Smuzhiyun
346*4882a593Smuzhiyun%s: squashed import of component
347*4882a593Smuzhiyun
348*4882a593SmuzhiyunThis commit copies the entire set of files as found in
349*4882a593Smuzhiyun%s %s
350*4882a593Smuzhiyun
351*4882a593SmuzhiyunFor more information about previous commits, see the
352*4882a593Smuzhiyunupstream repository.
353*4882a593Smuzhiyun
354*4882a593SmuzhiyunCommit created by combo-layer.
355*4882a593SmuzhiyunEOF
356*4882a593Smuzhiyun''' % (tree, author, committer, name, name, since_rev))
357*4882a593Smuzhiyun                            editor.flush()
358*4882a593Smuzhiyun                            os.environ['GIT_EDITOR'] = 'sh %s' % editor.name
359*4882a593Smuzhiyun                            runcmd('git replace --edit %s' % rev)
360*4882a593Smuzhiyun
361*4882a593Smuzhiyun                # Optional: rewrite history to change commit messages or to move files.
362*4882a593Smuzhiyun                if 'hook' in repo or dest_dir != ".":
363*4882a593Smuzhiyun                    filter_branch = ['git', 'filter-branch', '--force']
364*4882a593Smuzhiyun                    with tempfile.NamedTemporaryFile(mode='wt') as hookwrapper:
365*4882a593Smuzhiyun                        if 'hook' in repo:
366*4882a593Smuzhiyun                            # Create a shell script wrapper around the original hook that
367*4882a593Smuzhiyun                            # can be used by git filter-branch. Hook may or may not have
368*4882a593Smuzhiyun                            # an absolute path.
369*4882a593Smuzhiyun                            hook = repo['hook']
370*4882a593Smuzhiyun                            hook = os.path.join(os.path.dirname(conf.conffile), '..', hook)
371*4882a593Smuzhiyun                            # The wrappers turns the commit message
372*4882a593Smuzhiyun                            # from stdin into a fake patch header.
373*4882a593Smuzhiyun                            # This is good enough for changing Subject
374*4882a593Smuzhiyun                            # and commit msg body with normal
375*4882a593Smuzhiyun                            # combo-layer hooks.
376*4882a593Smuzhiyun                            hookwrapper.write('''set -e
377*4882a593Smuzhiyuntmpname=$(mktemp)
378*4882a593Smuzhiyuntrap "rm $tmpname" EXIT
379*4882a593Smuzhiyunecho -n 'Subject: [PATCH] ' >>$tmpname
380*4882a593Smuzhiyuncat >>$tmpname
381*4882a593Smuzhiyunif ! [ $(tail -c 1 $tmpname | od -A n -t x1) == '0a' ]; then
382*4882a593Smuzhiyun    echo >>$tmpname
383*4882a593Smuzhiyunfi
384*4882a593Smuzhiyunecho '---' >>$tmpname
385*4882a593Smuzhiyun%s $tmpname $GIT_COMMIT %s
386*4882a593Smuzhiyuntail -c +18 $tmpname | head -c -4
387*4882a593Smuzhiyun''' % (hook, name))
388*4882a593Smuzhiyun                            hookwrapper.flush()
389*4882a593Smuzhiyun                            filter_branch.extend(['--msg-filter', 'bash %s' % hookwrapper.name])
390*4882a593Smuzhiyun                        if dest_dir != ".":
391*4882a593Smuzhiyun                            parent = os.path.dirname(dest_dir)
392*4882a593Smuzhiyun                            if not parent:
393*4882a593Smuzhiyun                                parent = '.'
394*4882a593Smuzhiyun                            # May run outside of the current directory, so do not assume that .git exists.
395*4882a593Smuzhiyun                            filter_branch.extend(['--tree-filter', 'mkdir -p .git/tmptree && find . -mindepth 1 -maxdepth 1 ! -name .git -print0 | xargs -0 -I SOURCE mv SOURCE .git/tmptree && mkdir -p %s && mv .git/tmptree %s' % (parent, dest_dir)])
396*4882a593Smuzhiyun                        filter_branch.append('HEAD')
397*4882a593Smuzhiyun                        runcmd(filter_branch)
398*4882a593Smuzhiyun                        runcmd('git update-ref -d refs/original/refs/heads/%s' % name)
399*4882a593Smuzhiyun                repo['rewritten_revision'] = runcmd('git rev-parse HEAD').strip()
400*4882a593Smuzhiyun                repo['stripped_revision'] = repo['rewritten_revision']
401*4882a593Smuzhiyun                # Optional filter files: remove everything and re-populate using the normal filtering code.
402*4882a593Smuzhiyun                # Override any potential .gitignore.
403*4882a593Smuzhiyun                if file_filter or exclude_patterns:
404*4882a593Smuzhiyun                    runcmd('git rm -rf .')
405*4882a593Smuzhiyun                    if not os.path.exists(extract_dir):
406*4882a593Smuzhiyun                        os.makedirs(extract_dir)
407*4882a593Smuzhiyun                    copy_selected_files('HEAD', extract_dir, file_filter, exclude_patterns, '.',
408*4882a593Smuzhiyun                                        subdir=dest_dir)
409*4882a593Smuzhiyun                    runcmd('git add --all --force .')
410*4882a593Smuzhiyun                    if runcmd('git status --porcelain'):
411*4882a593Smuzhiyun                        # Something to commit.
412*4882a593Smuzhiyun                        runcmd(['git', 'commit', '-m',
413*4882a593Smuzhiyun                                '''%s: select file subset
414*4882a593Smuzhiyun
415*4882a593SmuzhiyunFiles from the component repository were chosen based on
416*4882a593Smuzhiyunthe following filters:
417*4882a593Smuzhiyunfile_filter = %s
418*4882a593Smuzhiyunfile_exclude = %s''' % (name, file_filter or '<empty>', repo.get('file_exclude', '<empty>'))])
419*4882a593Smuzhiyun                        repo['stripped_revision'] = runcmd('git rev-parse HEAD').strip()
420*4882a593Smuzhiyun
421*4882a593Smuzhiyun            if not lastrev:
422*4882a593Smuzhiyun                lastrev = runcmd('git rev-parse %s' % initialrev, ldir).strip()
423*4882a593Smuzhiyun                conf.update(name, "last_revision", lastrev, initmode=True)
424*4882a593Smuzhiyun
425*4882a593Smuzhiyun        if not conf.history:
426*4882a593Smuzhiyun            runcmd("git add .")
427*4882a593Smuzhiyun        else:
428*4882a593Smuzhiyun            # Create Octopus merge commit according to http://stackoverflow.com/questions/10874149/git-octopus-merge-with-unrelated-repositoies
429*4882a593Smuzhiyun            runcmd('git checkout master')
430*4882a593Smuzhiyun            merge = ['git', 'merge', '--no-commit']
431*4882a593Smuzhiyun            for name in conf.repos:
432*4882a593Smuzhiyun                repo = conf.repos[name]
433*4882a593Smuzhiyun                # Use branch created earlier.
434*4882a593Smuzhiyun                merge.append(name)
435*4882a593Smuzhiyun                # Root all commits which have no parent in the common
436*4882a593Smuzhiyun                # ancestor in the new repository.
437*4882a593Smuzhiyun                for start in runcmd('git log --pretty=format:%%H --max-parents=0 %s --' % name).split('\n'):
438*4882a593Smuzhiyun                    runcmd('git replace --graft %s %s' % (start, startrev))
439*4882a593Smuzhiyun            try:
440*4882a593Smuzhiyun                runcmd(merge)
441*4882a593Smuzhiyun            except Exception as error:
442*4882a593Smuzhiyun                logger.info('''Merging component repository history failed, perhaps because of merge conflicts.
443*4882a593SmuzhiyunIt may be possible to commit anyway after resolving these conflicts.
444*4882a593Smuzhiyun
445*4882a593Smuzhiyun%s''' % error)
446*4882a593Smuzhiyun            # Create MERGE_HEAD and MERGE_MSG. "git merge" itself
447*4882a593Smuzhiyun            # does not create MERGE_HEAD in case of a (harmless) failure,
448*4882a593Smuzhiyun            # and we want certain auto-generated information in the
449*4882a593Smuzhiyun            # commit message for future reference and/or automation.
450*4882a593Smuzhiyun            with open('.git/MERGE_HEAD', 'w') as head:
451*4882a593Smuzhiyun                with open('.git/MERGE_MSG', 'w') as msg:
452*4882a593Smuzhiyun                    msg.write('repo: initial import of components\n\n')
453*4882a593Smuzhiyun                    # head.write('%s\n' % startrev)
454*4882a593Smuzhiyun                    for name in conf.repos:
455*4882a593Smuzhiyun                        repo = conf.repos[name]
456*4882a593Smuzhiyun                        # <upstream ref> <rewritten ref> <rewritten + files removed>
457*4882a593Smuzhiyun                        msg.write('combo-layer-%s: %s %s %s\n' % (name,
458*4882a593Smuzhiyun                                                                  repo['last_revision'],
459*4882a593Smuzhiyun                                                                  repo['rewritten_revision'],
460*4882a593Smuzhiyun                                                                  repo['stripped_revision']))
461*4882a593Smuzhiyun                        rev = runcmd('git rev-parse %s' % name).strip()
462*4882a593Smuzhiyun                        head.write('%s\n' % rev)
463*4882a593Smuzhiyun
464*4882a593Smuzhiyun        if conf.localconffile:
465*4882a593Smuzhiyun            localadded = True
466*4882a593Smuzhiyun            try:
467*4882a593Smuzhiyun                runcmd("git rm --cached %s" % conf.localconffile, printerr=False)
468*4882a593Smuzhiyun            except subprocess.CalledProcessError:
469*4882a593Smuzhiyun                localadded = False
470*4882a593Smuzhiyun            if localadded:
471*4882a593Smuzhiyun                localrelpath = os.path.relpath(conf.localconffile)
472*4882a593Smuzhiyun                runcmd("grep -q %s .gitignore || echo %s >> .gitignore" % (localrelpath, localrelpath))
473*4882a593Smuzhiyun                runcmd("git add .gitignore")
474*4882a593Smuzhiyun                logger.info("Added local configuration file %s to .gitignore", localrelpath)
475*4882a593Smuzhiyun        logger.info("Initial combo layer repository data has been created; please make any changes if desired and then use 'git commit' to make the initial commit.")
476*4882a593Smuzhiyun    else:
477*4882a593Smuzhiyun        logger.info("Repository already initialised, nothing to do.")
478*4882a593Smuzhiyun
479*4882a593Smuzhiyun
480*4882a593Smuzhiyundef check_repo_clean(repodir):
481*4882a593Smuzhiyun    """
482*4882a593Smuzhiyun        check if the repo is clean
483*4882a593Smuzhiyun        exit if repo is dirty
484*4882a593Smuzhiyun    """
485*4882a593Smuzhiyun    output=runcmd("git status --porcelain", repodir)
486*4882a593Smuzhiyun    r = re.compile('\?\? patch-.*/')
487*4882a593Smuzhiyun    dirtyout = [item for item in output.splitlines() if not r.match(item)]
488*4882a593Smuzhiyun    if dirtyout:
489*4882a593Smuzhiyun        logger.error("git repo %s is dirty, please fix it first", repodir)
490*4882a593Smuzhiyun        sys.exit(1)
491*4882a593Smuzhiyun
492*4882a593Smuzhiyundef check_patch(patchfile):
493*4882a593Smuzhiyun    f = open(patchfile, 'rb')
494*4882a593Smuzhiyun    ln = f.readline()
495*4882a593Smuzhiyun    of = None
496*4882a593Smuzhiyun    in_patch = False
497*4882a593Smuzhiyun    beyond_msg = False
498*4882a593Smuzhiyun    pre_buf = b''
499*4882a593Smuzhiyun    while ln:
500*4882a593Smuzhiyun        if not beyond_msg:
501*4882a593Smuzhiyun            if ln == b'---\n':
502*4882a593Smuzhiyun                if not of:
503*4882a593Smuzhiyun                    break
504*4882a593Smuzhiyun                in_patch = False
505*4882a593Smuzhiyun                beyond_msg = True
506*4882a593Smuzhiyun            elif ln.startswith(b'--- '):
507*4882a593Smuzhiyun                # We have a diff in the commit message
508*4882a593Smuzhiyun                in_patch = True
509*4882a593Smuzhiyun                if not of:
510*4882a593Smuzhiyun                    print('WARNING: %s contains a diff in its commit message, indenting to avoid failure during apply' % patchfile)
511*4882a593Smuzhiyun                    of = open(patchfile + '.tmp', 'wb')
512*4882a593Smuzhiyun                    of.write(pre_buf)
513*4882a593Smuzhiyun                    pre_buf = b''
514*4882a593Smuzhiyun            elif in_patch and not ln[0] in b'+-@ \n\r':
515*4882a593Smuzhiyun                in_patch = False
516*4882a593Smuzhiyun        if of:
517*4882a593Smuzhiyun            if in_patch:
518*4882a593Smuzhiyun                of.write(b' ' + ln)
519*4882a593Smuzhiyun            else:
520*4882a593Smuzhiyun                of.write(ln)
521*4882a593Smuzhiyun        else:
522*4882a593Smuzhiyun            pre_buf += ln
523*4882a593Smuzhiyun        ln = f.readline()
524*4882a593Smuzhiyun    f.close()
525*4882a593Smuzhiyun    if of:
526*4882a593Smuzhiyun        of.close()
527*4882a593Smuzhiyun        os.rename(of.name, patchfile)
528*4882a593Smuzhiyun
529*4882a593Smuzhiyundef drop_to_shell(workdir=None):
530*4882a593Smuzhiyun    if not sys.stdin.isatty():
531*4882a593Smuzhiyun        print("Not a TTY so can't drop to shell for resolution, exiting.")
532*4882a593Smuzhiyun        return False
533*4882a593Smuzhiyun
534*4882a593Smuzhiyun    shell = os.environ.get('SHELL', 'bash')
535*4882a593Smuzhiyun    print('Dropping to shell "%s"\n' \
536*4882a593Smuzhiyun          'When you are finished, run the following to continue:\n' \
537*4882a593Smuzhiyun          '       exit    -- continue to apply the patches\n' \
538*4882a593Smuzhiyun          '       exit 1  -- abort\n' % shell);
539*4882a593Smuzhiyun    ret = subprocess.call([shell], cwd=workdir)
540*4882a593Smuzhiyun    if ret != 0:
541*4882a593Smuzhiyun        print("Aborting")
542*4882a593Smuzhiyun        return False
543*4882a593Smuzhiyun    else:
544*4882a593Smuzhiyun        return True
545*4882a593Smuzhiyun
546*4882a593Smuzhiyundef check_rev_branch(component, repodir, rev, branch):
547*4882a593Smuzhiyun    try:
548*4882a593Smuzhiyun        actualbranch = runcmd("git branch --contains %s" % rev, repodir, printerr=False)
549*4882a593Smuzhiyun    except subprocess.CalledProcessError as e:
550*4882a593Smuzhiyun        if e.returncode == 129:
551*4882a593Smuzhiyun            actualbranch = ""
552*4882a593Smuzhiyun        else:
553*4882a593Smuzhiyun            raise
554*4882a593Smuzhiyun
555*4882a593Smuzhiyun    if not actualbranch:
556*4882a593Smuzhiyun        logger.error("%s: specified revision %s is invalid!" % (component, rev))
557*4882a593Smuzhiyun        return False
558*4882a593Smuzhiyun
559*4882a593Smuzhiyun    branches = []
560*4882a593Smuzhiyun    branchlist = actualbranch.split("\n")
561*4882a593Smuzhiyun    for b in branchlist:
562*4882a593Smuzhiyun        branches.append(b.strip().split(' ')[-1])
563*4882a593Smuzhiyun
564*4882a593Smuzhiyun    if branch not in branches:
565*4882a593Smuzhiyun        logger.error("%s: specified revision %s is not on specified branch %s!" % (component, rev, branch))
566*4882a593Smuzhiyun        return False
567*4882a593Smuzhiyun    return True
568*4882a593Smuzhiyun
569*4882a593Smuzhiyundef get_repos(conf, repo_names):
570*4882a593Smuzhiyun    repos = []
571*4882a593Smuzhiyun    for name in repo_names:
572*4882a593Smuzhiyun        if name.startswith('-'):
573*4882a593Smuzhiyun            break
574*4882a593Smuzhiyun        else:
575*4882a593Smuzhiyun            repos.append(name)
576*4882a593Smuzhiyun    for repo in repos:
577*4882a593Smuzhiyun        if not repo in conf.repos:
578*4882a593Smuzhiyun            logger.error("Specified component '%s' not found in configuration" % repo)
579*4882a593Smuzhiyun            sys.exit(1)
580*4882a593Smuzhiyun
581*4882a593Smuzhiyun    if not repos:
582*4882a593Smuzhiyun        repos = [ repo for repo in conf.repos if conf.repos[repo].get("update", True) ]
583*4882a593Smuzhiyun
584*4882a593Smuzhiyun    return repos
585*4882a593Smuzhiyun
586*4882a593Smuzhiyundef action_pull(conf, args):
587*4882a593Smuzhiyun    """
588*4882a593Smuzhiyun        update the component repos only
589*4882a593Smuzhiyun    """
590*4882a593Smuzhiyun    repos = get_repos(conf, args[1:])
591*4882a593Smuzhiyun
592*4882a593Smuzhiyun    # make sure all repos are clean
593*4882a593Smuzhiyun    for name in repos:
594*4882a593Smuzhiyun        check_repo_clean(conf.repos[name]['local_repo_dir'])
595*4882a593Smuzhiyun
596*4882a593Smuzhiyun    for name in repos:
597*4882a593Smuzhiyun        repo = conf.repos[name]
598*4882a593Smuzhiyun        ldir = repo['local_repo_dir']
599*4882a593Smuzhiyun        branch = repo.get('branch', "master")
600*4882a593Smuzhiyun        logger.info("update branch %s of component repo %s in %s ..." % (branch, name, ldir))
601*4882a593Smuzhiyun        if not conf.hard_reset:
602*4882a593Smuzhiyun            # Try to pull only the configured branch. Beware that this may fail
603*4882a593Smuzhiyun            # when the branch is currently unknown (for example, after reconfiguring
604*4882a593Smuzhiyun            # combo-layer). In that case we need to fetch everything and try the check out
605*4882a593Smuzhiyun            # and pull again.
606*4882a593Smuzhiyun            try:
607*4882a593Smuzhiyun                runcmd("git checkout %s" % branch, ldir, printerr=False)
608*4882a593Smuzhiyun            except subprocess.CalledProcessError:
609*4882a593Smuzhiyun                output=runcmd("git fetch", ldir)
610*4882a593Smuzhiyun                logger.info(output)
611*4882a593Smuzhiyun                runcmd("git checkout %s" % branch, ldir)
612*4882a593Smuzhiyun                runcmd("git pull --ff-only", ldir)
613*4882a593Smuzhiyun            else:
614*4882a593Smuzhiyun                output=runcmd("git pull --ff-only", ldir)
615*4882a593Smuzhiyun                logger.info(output)
616*4882a593Smuzhiyun        else:
617*4882a593Smuzhiyun            output=runcmd("git fetch", ldir)
618*4882a593Smuzhiyun            logger.info(output)
619*4882a593Smuzhiyun            runcmd("git checkout %s" % branch, ldir)
620*4882a593Smuzhiyun            runcmd("git reset --hard FETCH_HEAD", ldir)
621*4882a593Smuzhiyun
622*4882a593Smuzhiyundef action_update(conf, args):
623*4882a593Smuzhiyun    """
624*4882a593Smuzhiyun        update the component repos
625*4882a593Smuzhiyun        either:
626*4882a593Smuzhiyun           generate the patch list
627*4882a593Smuzhiyun           apply the generated patches
628*4882a593Smuzhiyun        or:
629*4882a593Smuzhiyun           re-creates the entire component history and merges them
630*4882a593Smuzhiyun           into the current branch with a merge commit
631*4882a593Smuzhiyun    """
632*4882a593Smuzhiyun    components = [arg.split(':')[0] for arg in args[1:]]
633*4882a593Smuzhiyun    revisions = {}
634*4882a593Smuzhiyun    for arg in args[1:]:
635*4882a593Smuzhiyun        if ':' in arg:
636*4882a593Smuzhiyun            a = arg.split(':', 1)
637*4882a593Smuzhiyun            revisions[a[0]] = a[1]
638*4882a593Smuzhiyun    repos = get_repos(conf, components)
639*4882a593Smuzhiyun
640*4882a593Smuzhiyun    # make sure combo repo is clean
641*4882a593Smuzhiyun    check_repo_clean(os.getcwd())
642*4882a593Smuzhiyun
643*4882a593Smuzhiyun    # Check whether we keep the component histories. Must be
644*4882a593Smuzhiyun    # set either via --history command line parameter or consistently
645*4882a593Smuzhiyun    # in combo-layer.conf. Mixing modes is (currently, and probably
646*4882a593Smuzhiyun    # permanently because it would be complicated) not supported.
647*4882a593Smuzhiyun    if conf.history:
648*4882a593Smuzhiyun        history = True
649*4882a593Smuzhiyun    else:
650*4882a593Smuzhiyun        history = None
651*4882a593Smuzhiyun        for name in repos:
652*4882a593Smuzhiyun            repo = conf.repos[name]
653*4882a593Smuzhiyun            repo_history = repo.get('history', False)
654*4882a593Smuzhiyun            if history is None:
655*4882a593Smuzhiyun                history = repo_history
656*4882a593Smuzhiyun            elif history != repo_history:
657*4882a593Smuzhiyun                logger.error("'history' property is set inconsistently")
658*4882a593Smuzhiyun                sys.exit(1)
659*4882a593Smuzhiyun
660*4882a593Smuzhiyun    # Step 1: update the component repos
661*4882a593Smuzhiyun    if conf.nopull:
662*4882a593Smuzhiyun        logger.info("Skipping pull (-n)")
663*4882a593Smuzhiyun    else:
664*4882a593Smuzhiyun        action_pull(conf, ['arg0'] + components)
665*4882a593Smuzhiyun
666*4882a593Smuzhiyun    if history:
667*4882a593Smuzhiyun        update_with_history(conf, components, revisions, repos)
668*4882a593Smuzhiyun    else:
669*4882a593Smuzhiyun        update_with_patches(conf, components, revisions, repos)
670*4882a593Smuzhiyun
671*4882a593Smuzhiyundef update_with_patches(conf, components, revisions, repos):
672*4882a593Smuzhiyun    import uuid
673*4882a593Smuzhiyun    patch_dir = "patch-%s" % uuid.uuid4()
674*4882a593Smuzhiyun    if not os.path.exists(patch_dir):
675*4882a593Smuzhiyun        os.mkdir(patch_dir)
676*4882a593Smuzhiyun
677*4882a593Smuzhiyun    for name in repos:
678*4882a593Smuzhiyun        revision = revisions.get(name, None)
679*4882a593Smuzhiyun        repo = conf.repos[name]
680*4882a593Smuzhiyun        ldir = repo['local_repo_dir']
681*4882a593Smuzhiyun        dest_dir = repo['dest_dir']
682*4882a593Smuzhiyun        branch = repo.get('branch', "master")
683*4882a593Smuzhiyun        repo_patch_dir = os.path.join(os.getcwd(), patch_dir, name)
684*4882a593Smuzhiyun
685*4882a593Smuzhiyun        # Step 2: generate the patch list and store to patch dir
686*4882a593Smuzhiyun        logger.info("Generating patches from %s..." % name)
687*4882a593Smuzhiyun        top_revision = revision or branch
688*4882a593Smuzhiyun        if not check_rev_branch(name, ldir, top_revision, branch):
689*4882a593Smuzhiyun            sys.exit(1)
690*4882a593Smuzhiyun        if dest_dir != ".":
691*4882a593Smuzhiyun            prefix = "--src-prefix=a/%s/ --dst-prefix=b/%s/" % (dest_dir, dest_dir)
692*4882a593Smuzhiyun        else:
693*4882a593Smuzhiyun            prefix = ""
694*4882a593Smuzhiyun        if repo['last_revision'] == "":
695*4882a593Smuzhiyun            logger.info("Warning: last_revision of component %s is not set, starting from the first commit" % name)
696*4882a593Smuzhiyun            patch_cmd_range = "--root %s" % top_revision
697*4882a593Smuzhiyun            rev_cmd_range = top_revision
698*4882a593Smuzhiyun        else:
699*4882a593Smuzhiyun            if not check_rev_branch(name, ldir, repo['last_revision'], branch):
700*4882a593Smuzhiyun                sys.exit(1)
701*4882a593Smuzhiyun            patch_cmd_range = "%s..%s" % (repo['last_revision'], top_revision)
702*4882a593Smuzhiyun            rev_cmd_range = patch_cmd_range
703*4882a593Smuzhiyun
704*4882a593Smuzhiyun        file_filter = repo.get('file_filter',".")
705*4882a593Smuzhiyun
706*4882a593Smuzhiyun        # Filter out unwanted files
707*4882a593Smuzhiyun        exclude = repo.get('file_exclude', '')
708*4882a593Smuzhiyun        if exclude:
709*4882a593Smuzhiyun            for path in exclude.split():
710*4882a593Smuzhiyun                p = "%s/%s" % (dest_dir, path) if dest_dir != '.' else path
711*4882a593Smuzhiyun                file_filter += " ':!%s'" % p
712*4882a593Smuzhiyun
713*4882a593Smuzhiyun        patch_cmd = "git format-patch -N %s --output-directory %s %s -- %s" % \
714*4882a593Smuzhiyun            (prefix,repo_patch_dir, patch_cmd_range, file_filter)
715*4882a593Smuzhiyun        output = runcmd(patch_cmd, ldir)
716*4882a593Smuzhiyun        logger.debug("generated patch set:\n%s" % output)
717*4882a593Smuzhiyun        patchlist = output.splitlines()
718*4882a593Smuzhiyun
719*4882a593Smuzhiyun        rev_cmd = "git rev-list --no-merges %s -- %s" % (rev_cmd_range, file_filter)
720*4882a593Smuzhiyun        revlist = runcmd(rev_cmd, ldir).splitlines()
721*4882a593Smuzhiyun
722*4882a593Smuzhiyun        # Step 3: Call repo specific hook to adjust patch
723*4882a593Smuzhiyun        if 'hook' in repo:
724*4882a593Smuzhiyun            # hook parameter is: ./hook patchpath revision reponame
725*4882a593Smuzhiyun            count=len(revlist)-1
726*4882a593Smuzhiyun            for patch in patchlist:
727*4882a593Smuzhiyun                runcmd("%s %s %s %s" % (repo['hook'], patch, revlist[count], name))
728*4882a593Smuzhiyun                count=count-1
729*4882a593Smuzhiyun
730*4882a593Smuzhiyun        # Step 4: write patch list and revision list to file, for user to edit later
731*4882a593Smuzhiyun        patchlist_file = os.path.join(os.getcwd(), patch_dir, "patchlist-%s" % name)
732*4882a593Smuzhiyun        repo['patchlist'] = patchlist_file
733*4882a593Smuzhiyun        f = open(patchlist_file, 'w')
734*4882a593Smuzhiyun        count=len(revlist)-1
735*4882a593Smuzhiyun        for patch in patchlist:
736*4882a593Smuzhiyun            f.write("%s %s\n" % (patch, revlist[count]))
737*4882a593Smuzhiyun            check_patch(os.path.join(patch_dir, patch))
738*4882a593Smuzhiyun            count=count-1
739*4882a593Smuzhiyun        f.close()
740*4882a593Smuzhiyun
741*4882a593Smuzhiyun    # Step 5: invoke bash for user to edit patch and patch list
742*4882a593Smuzhiyun    if conf.interactive:
743*4882a593Smuzhiyun        print('You may now edit the patch and patch list in %s\n' \
744*4882a593Smuzhiyun              'For example, you can remove unwanted patch entries from patchlist-*, so that they will be not applied later' % patch_dir);
745*4882a593Smuzhiyun        if not drop_to_shell(patch_dir):
746*4882a593Smuzhiyun            sys.exit(1)
747*4882a593Smuzhiyun
748*4882a593Smuzhiyun    # Step 6: apply the generated and revised patch
749*4882a593Smuzhiyun    apply_patchlist(conf, repos)
750*4882a593Smuzhiyun    runcmd("rm -rf %s" % patch_dir)
751*4882a593Smuzhiyun
752*4882a593Smuzhiyun    # Step 7: commit the updated config file if it's being tracked
753*4882a593Smuzhiyun    commit_conf_file(conf, components)
754*4882a593Smuzhiyun
755*4882a593Smuzhiyundef conf_commit_msg(conf, components):
756*4882a593Smuzhiyun    # create the "components" string
757*4882a593Smuzhiyun    component_str = "all components"
758*4882a593Smuzhiyun    if len(components) > 0:
759*4882a593Smuzhiyun        # otherwise tell which components were actually changed
760*4882a593Smuzhiyun        component_str = ", ".join(components)
761*4882a593Smuzhiyun
762*4882a593Smuzhiyun    # expand the template with known values
763*4882a593Smuzhiyun    template = Template(conf.commit_msg_template)
764*4882a593Smuzhiyun    msg = template.substitute(components = component_str)
765*4882a593Smuzhiyun    return msg
766*4882a593Smuzhiyun
767*4882a593Smuzhiyundef commit_conf_file(conf, components, commit=True):
768*4882a593Smuzhiyun    relpath = os.path.relpath(conf.conffile)
769*4882a593Smuzhiyun    try:
770*4882a593Smuzhiyun        output = runcmd("git status --porcelain %s" % relpath, printerr=False)
771*4882a593Smuzhiyun    except:
772*4882a593Smuzhiyun        # Outside the repository
773*4882a593Smuzhiyun        output = None
774*4882a593Smuzhiyun    if output:
775*4882a593Smuzhiyun        if output.lstrip().startswith("M"):
776*4882a593Smuzhiyun            logger.info("Committing updated configuration file")
777*4882a593Smuzhiyun            if commit:
778*4882a593Smuzhiyun                msg = conf_commit_msg(conf, components)
779*4882a593Smuzhiyun                runcmd('git commit -m'.split() + [msg, relpath])
780*4882a593Smuzhiyun            else:
781*4882a593Smuzhiyun                runcmd('git add %s' % relpath)
782*4882a593Smuzhiyun            return True
783*4882a593Smuzhiyun    return False
784*4882a593Smuzhiyun
785*4882a593Smuzhiyundef apply_patchlist(conf, repos):
786*4882a593Smuzhiyun    """
787*4882a593Smuzhiyun        apply the generated patch list to combo repo
788*4882a593Smuzhiyun    """
789*4882a593Smuzhiyun    for name in repos:
790*4882a593Smuzhiyun        repo = conf.repos[name]
791*4882a593Smuzhiyun        lastrev = repo["last_revision"]
792*4882a593Smuzhiyun        prevrev = lastrev
793*4882a593Smuzhiyun
794*4882a593Smuzhiyun        # Get non-blank lines from patch list file
795*4882a593Smuzhiyun        patchlist = []
796*4882a593Smuzhiyun        if os.path.exists(repo['patchlist']) or not conf.interactive:
797*4882a593Smuzhiyun            # Note: we want this to fail here if the file doesn't exist and we're not in
798*4882a593Smuzhiyun            # interactive mode since the file should exist in this case
799*4882a593Smuzhiyun            with open(repo['patchlist']) as f:
800*4882a593Smuzhiyun                for line in f:
801*4882a593Smuzhiyun                    line = line.rstrip()
802*4882a593Smuzhiyun                    if line:
803*4882a593Smuzhiyun                        patchlist.append(line)
804*4882a593Smuzhiyun
805*4882a593Smuzhiyun        ldir = conf.repos[name]['local_repo_dir']
806*4882a593Smuzhiyun        branch = conf.repos[name].get('branch', "master")
807*4882a593Smuzhiyun        branchrev = runcmd("git rev-parse %s" % branch, ldir).strip()
808*4882a593Smuzhiyun
809*4882a593Smuzhiyun        if patchlist:
810*4882a593Smuzhiyun            logger.info("Applying patches from %s..." % name)
811*4882a593Smuzhiyun            linecount = len(patchlist)
812*4882a593Smuzhiyun            i = 1
813*4882a593Smuzhiyun            for line in patchlist:
814*4882a593Smuzhiyun                patchfile = line.split()[0]
815*4882a593Smuzhiyun                lastrev = line.split()[1]
816*4882a593Smuzhiyun                patchdisp = os.path.relpath(patchfile)
817*4882a593Smuzhiyun                if os.path.getsize(patchfile) == 0:
818*4882a593Smuzhiyun                    logger.info("(skipping %d/%d %s - no changes)" % (i, linecount, patchdisp))
819*4882a593Smuzhiyun                else:
820*4882a593Smuzhiyun                    cmd = "git am --keep-cr %s-p1 %s" % ('-s ' if repo.get('signoff', True) else '', patchfile)
821*4882a593Smuzhiyun                    logger.info("Applying %d/%d: %s" % (i, linecount, patchdisp))
822*4882a593Smuzhiyun                    try:
823*4882a593Smuzhiyun                        runcmd(cmd)
824*4882a593Smuzhiyun                    except subprocess.CalledProcessError:
825*4882a593Smuzhiyun                        logger.info('Running "git am --abort" to cleanup repo')
826*4882a593Smuzhiyun                        runcmd("git am --abort")
827*4882a593Smuzhiyun                        logger.error('"%s" failed' % cmd)
828*4882a593Smuzhiyun                        logger.info("Please manually apply patch %s" % patchdisp)
829*4882a593Smuzhiyun                        logger.info("Note: if you exit and continue applying without manually applying the patch, it will be skipped")
830*4882a593Smuzhiyun                        if not drop_to_shell():
831*4882a593Smuzhiyun                            if prevrev != repo['last_revision']:
832*4882a593Smuzhiyun                                conf.update(name, "last_revision", prevrev)
833*4882a593Smuzhiyun                            sys.exit(1)
834*4882a593Smuzhiyun                prevrev = lastrev
835*4882a593Smuzhiyun                i += 1
836*4882a593Smuzhiyun            # Once all patches are applied, we should update
837*4882a593Smuzhiyun            # last_revision to the branch head instead of the last
838*4882a593Smuzhiyun            # applied patch. The two are not necessarily the same when
839*4882a593Smuzhiyun            # the last commit is a merge commit or when the patches at
840*4882a593Smuzhiyun            # the branch head were intentionally excluded.
841*4882a593Smuzhiyun            #
842*4882a593Smuzhiyun            # If we do not do that for a merge commit, the next
843*4882a593Smuzhiyun            # combo-layer run will only exclude patches reachable from
844*4882a593Smuzhiyun            # one of the merged branches and try to re-apply patches
845*4882a593Smuzhiyun            # from other branches even though they were already
846*4882a593Smuzhiyun            # copied.
847*4882a593Smuzhiyun            #
848*4882a593Smuzhiyun            # If patches were intentionally excluded, the next run will
849*4882a593Smuzhiyun            # present them again instead of skipping over them. This
850*4882a593Smuzhiyun            # may or may not be intended, so the code here is conservative
851*4882a593Smuzhiyun            # and only addresses the "head is merge commit" case.
852*4882a593Smuzhiyun            if lastrev != branchrev and \
853*4882a593Smuzhiyun               len(runcmd("git show --pretty=format:%%P --no-patch %s" % branch, ldir).split()) > 1:
854*4882a593Smuzhiyun                lastrev = branchrev
855*4882a593Smuzhiyun        else:
856*4882a593Smuzhiyun            logger.info("No patches to apply from %s" % name)
857*4882a593Smuzhiyun            lastrev = branchrev
858*4882a593Smuzhiyun
859*4882a593Smuzhiyun        if lastrev != repo['last_revision']:
860*4882a593Smuzhiyun            conf.update(name, "last_revision", lastrev)
861*4882a593Smuzhiyun
862*4882a593Smuzhiyundef action_splitpatch(conf, args):
863*4882a593Smuzhiyun    """
864*4882a593Smuzhiyun        generate the commit patch and
865*4882a593Smuzhiyun        split the patch per repo
866*4882a593Smuzhiyun    """
867*4882a593Smuzhiyun    logger.debug("action_splitpatch")
868*4882a593Smuzhiyun    if len(args) > 1:
869*4882a593Smuzhiyun        commit = args[1]
870*4882a593Smuzhiyun    else:
871*4882a593Smuzhiyun        commit = "HEAD"
872*4882a593Smuzhiyun    patchdir = "splitpatch-%s" % commit
873*4882a593Smuzhiyun    if not os.path.exists(patchdir):
874*4882a593Smuzhiyun        os.mkdir(patchdir)
875*4882a593Smuzhiyun
876*4882a593Smuzhiyun    # filerange_root is for the repo whose dest_dir is root "."
877*4882a593Smuzhiyun    # and it should be specified by excluding all other repo dest dir
878*4882a593Smuzhiyun    # like "-x repo1 -x repo2 -x repo3 ..."
879*4882a593Smuzhiyun    filerange_root = ""
880*4882a593Smuzhiyun    for name in conf.repos:
881*4882a593Smuzhiyun        dest_dir = conf.repos[name]['dest_dir']
882*4882a593Smuzhiyun        if dest_dir != ".":
883*4882a593Smuzhiyun            filerange_root = '%s -x "%s/*"' % (filerange_root, dest_dir)
884*4882a593Smuzhiyun
885*4882a593Smuzhiyun    for name in conf.repos:
886*4882a593Smuzhiyun        dest_dir = conf.repos[name]['dest_dir']
887*4882a593Smuzhiyun        patch_filename = "%s/%s.patch" % (patchdir, name)
888*4882a593Smuzhiyun        if dest_dir == ".":
889*4882a593Smuzhiyun            cmd = "git format-patch -n1 --stdout %s^..%s | filterdiff -p1 %s > %s" % (commit, commit, filerange_root, patch_filename)
890*4882a593Smuzhiyun        else:
891*4882a593Smuzhiyun            cmd = "git format-patch --no-prefix -n1 --stdout %s^..%s -- %s > %s" % (commit, commit, dest_dir, patch_filename)
892*4882a593Smuzhiyun        runcmd(cmd)
893*4882a593Smuzhiyun        # Detect empty patches (including those produced by filterdiff above
894*4882a593Smuzhiyun        # that contain only preamble text)
895*4882a593Smuzhiyun        if os.path.getsize(patch_filename) == 0 or runcmd("filterdiff %s" % patch_filename) == "":
896*4882a593Smuzhiyun            os.remove(patch_filename)
897*4882a593Smuzhiyun            logger.info("(skipping %s - no changes)", name)
898*4882a593Smuzhiyun        else:
899*4882a593Smuzhiyun            logger.info(patch_filename)
900*4882a593Smuzhiyun
901*4882a593Smuzhiyundef update_with_history(conf, components, revisions, repos):
902*4882a593Smuzhiyun    '''Update all components with full history.
903*4882a593Smuzhiyun
904*4882a593Smuzhiyun    Works by importing all commits reachable from a component's
905*4882a593Smuzhiyun    current head revision.  If those commits are rooted in an already
906*4882a593Smuzhiyun    imported commit, their content gets mixed with the content of the
907*4882a593Smuzhiyun    combined repo of that commit (new or modified files overwritten,
908*4882a593Smuzhiyun    removed files removed).
909*4882a593Smuzhiyun
910*4882a593Smuzhiyun    The last commit is an artificial merge commit that merges all the
911*4882a593Smuzhiyun    updated components into the combined repository.
912*4882a593Smuzhiyun
913*4882a593Smuzhiyun    The HEAD ref only gets updated at the very end. All intermediate work
914*4882a593Smuzhiyun    happens in a worktree which will get garbage collected by git eventually
915*4882a593Smuzhiyun    after a failure.
916*4882a593Smuzhiyun    '''
917*4882a593Smuzhiyun    # Remember current HEAD and what we need to add to it.
918*4882a593Smuzhiyun    head = runcmd("git rev-parse HEAD").strip()
919*4882a593Smuzhiyun    additional_heads = {}
920*4882a593Smuzhiyun
921*4882a593Smuzhiyun    # Track the mapping between original commit and commit in the
922*4882a593Smuzhiyun    # combined repo. We do not have to distinguish between components,
923*4882a593Smuzhiyun    # because commit hashes are different anyway. Often we can
924*4882a593Smuzhiyun    # skip find_revs() entirely (for example, when all new commits
925*4882a593Smuzhiyun    # are derived from the last imported revision).
926*4882a593Smuzhiyun    #
927*4882a593Smuzhiyun    # Using "head" (typically the merge commit) instead of the actual
928*4882a593Smuzhiyun    # commit for the component leads to a nicer history in the combined
929*4882a593Smuzhiyun    # repo.
930*4882a593Smuzhiyun    old2new_revs = {}
931*4882a593Smuzhiyun    for name in repos:
932*4882a593Smuzhiyun        repo = conf.repos[name]
933*4882a593Smuzhiyun        revision = repo['last_revision']
934*4882a593Smuzhiyun        if revision:
935*4882a593Smuzhiyun            old2new_revs[revision] = head
936*4882a593Smuzhiyun
937*4882a593Smuzhiyun    def add_p(parents):
938*4882a593Smuzhiyun        '''Insert -p before each entry.'''
939*4882a593Smuzhiyun        parameters = []
940*4882a593Smuzhiyun        for p in parents:
941*4882a593Smuzhiyun            parameters.append('-p')
942*4882a593Smuzhiyun            parameters.append(p)
943*4882a593Smuzhiyun        return parameters
944*4882a593Smuzhiyun
945*4882a593Smuzhiyun    # Do all intermediate work with a separate work dir and index,
946*4882a593Smuzhiyun    # chosen via env variables (can't use "git worktree", it is too
947*4882a593Smuzhiyun    # new). This is useful (no changes to current work tree unless the
948*4882a593Smuzhiyun    # update succeeds) and required (otherwise we end up temporarily
949*4882a593Smuzhiyun    # removing the combo-layer hooks that we currently use when
950*4882a593Smuzhiyun    # importing a new component).
951*4882a593Smuzhiyun    #
952*4882a593Smuzhiyun    # Not cleaned up after a failure at the moment.
953*4882a593Smuzhiyun    wdir = os.path.join(os.getcwd(), ".git", "combo-layer")
954*4882a593Smuzhiyun    windex = wdir + ".index"
955*4882a593Smuzhiyun    if os.path.isdir(wdir):
956*4882a593Smuzhiyun        shutil.rmtree(wdir)
957*4882a593Smuzhiyun    os.mkdir(wdir)
958*4882a593Smuzhiyun    wenv = copy.deepcopy(os.environ)
959*4882a593Smuzhiyun    wenv["GIT_WORK_TREE"] = wdir
960*4882a593Smuzhiyun    wenv["GIT_INDEX_FILE"] = windex
961*4882a593Smuzhiyun    # This one turned out to be needed in practice.
962*4882a593Smuzhiyun    wenv["GIT_OBJECT_DIRECTORY"] = os.path.join(os.getcwd(), ".git", "objects")
963*4882a593Smuzhiyun    wargs = {"destdir": wdir, "env": wenv}
964*4882a593Smuzhiyun
965*4882a593Smuzhiyun    for name in repos:
966*4882a593Smuzhiyun        revision = revisions.get(name, None)
967*4882a593Smuzhiyun        repo = conf.repos[name]
968*4882a593Smuzhiyun        ldir = repo['local_repo_dir']
969*4882a593Smuzhiyun        dest_dir = repo['dest_dir']
970*4882a593Smuzhiyun        branch = repo.get('branch', "master")
971*4882a593Smuzhiyun        hook = repo.get('hook', None)
972*4882a593Smuzhiyun        largs = {"destdir": ldir, "env": None}
973*4882a593Smuzhiyun        file_include = repo.get('file_filter', '').split()
974*4882a593Smuzhiyun        file_include.sort() # make sure that short entries like '.' come first.
975*4882a593Smuzhiyun        file_exclude = repo.get('file_exclude', '').split()
976*4882a593Smuzhiyun
977*4882a593Smuzhiyun        def include_file(file):
978*4882a593Smuzhiyun            if not file_include:
979*4882a593Smuzhiyun                # No explicit filter set, include file.
980*4882a593Smuzhiyun                return True
981*4882a593Smuzhiyun            for filter in file_include:
982*4882a593Smuzhiyun                if filter == '.':
983*4882a593Smuzhiyun                    # Another special case: include current directory and thus all files.
984*4882a593Smuzhiyun                    return True
985*4882a593Smuzhiyun                if os.path.commonprefix((filter, file)) == filter:
986*4882a593Smuzhiyun                    # Included in directory or direct file match.
987*4882a593Smuzhiyun                    return True
988*4882a593Smuzhiyun                # Check for wildcard match *with* allowing * to match /, i.e.
989*4882a593Smuzhiyun                # src/*.c does match src/foobar/*.c. That's not how it is done elsewhere
990*4882a593Smuzhiyun                # when passing the filtering to "git archive", but it is unclear what
991*4882a593Smuzhiyun                # the intended semantic is (the comment on file_exclude that "append a * wildcard
992*4882a593Smuzhiyun                # at the end" to match the full content of a directories implies that
993*4882a593Smuzhiyun                # slashes are indeed not special), so here we simply do what's easy to
994*4882a593Smuzhiyun                # implement in Python.
995*4882a593Smuzhiyun                logger.debug('fnmatch(%s, %s)' % (file, filter))
996*4882a593Smuzhiyun                if fnmatch.fnmatchcase(file, filter):
997*4882a593Smuzhiyun                    return True
998*4882a593Smuzhiyun            return False
999*4882a593Smuzhiyun
1000*4882a593Smuzhiyun        def exclude_file(file):
1001*4882a593Smuzhiyun            for filter in file_exclude:
1002*4882a593Smuzhiyun                if fnmatch.fnmatchcase(file, filter):
1003*4882a593Smuzhiyun                    return True
1004*4882a593Smuzhiyun            return False
1005*4882a593Smuzhiyun
1006*4882a593Smuzhiyun        def file_filter(files):
1007*4882a593Smuzhiyun            '''Clean up file list so that only included files remain.'''
1008*4882a593Smuzhiyun            index = 0
1009*4882a593Smuzhiyun            while index < len(files):
1010*4882a593Smuzhiyun                file = files[index]
1011*4882a593Smuzhiyun                if not include_file(file) or exclude_file(file):
1012*4882a593Smuzhiyun                    del files[index]
1013*4882a593Smuzhiyun                else:
1014*4882a593Smuzhiyun                    index += 1
1015*4882a593Smuzhiyun
1016*4882a593Smuzhiyun
1017*4882a593Smuzhiyun        # Generate the revision list.
1018*4882a593Smuzhiyun        logger.info("Analyzing commits from %s..." % name)
1019*4882a593Smuzhiyun        top_revision = revision or branch
1020*4882a593Smuzhiyun        if not check_rev_branch(name, ldir, top_revision, branch):
1021*4882a593Smuzhiyun            sys.exit(1)
1022*4882a593Smuzhiyun
1023*4882a593Smuzhiyun        last_revision = repo['last_revision']
1024*4882a593Smuzhiyun        rev_list_args = "--full-history --sparse --topo-order --reverse"
1025*4882a593Smuzhiyun        if not last_revision:
1026*4882a593Smuzhiyun            logger.info("Warning: last_revision of component %s is not set, starting from the first commit" % name)
1027*4882a593Smuzhiyun            rev_list_args = rev_list_args + ' ' + top_revision
1028*4882a593Smuzhiyun        else:
1029*4882a593Smuzhiyun            if not check_rev_branch(name, ldir, last_revision, branch):
1030*4882a593Smuzhiyun                sys.exit(1)
1031*4882a593Smuzhiyun            rev_list_args = "%s %s..%s" % (rev_list_args, last_revision, top_revision)
1032*4882a593Smuzhiyun
1033*4882a593Smuzhiyun            # By definition, the current HEAD contains the latest imported
1034*4882a593Smuzhiyun            # commit of each component. We use that as initial mapping even
1035*4882a593Smuzhiyun            # though the commits do not match exactly because
1036*4882a593Smuzhiyun            # a) it always works (in contrast to find_revs, which relies on special
1037*4882a593Smuzhiyun            #    commit messages)
1038*4882a593Smuzhiyun            # b) it is faster than find_revs, which will only be called on demand
1039*4882a593Smuzhiyun            #    and can be skipped entirely in most cases
1040*4882a593Smuzhiyun            # c) last but not least, the combined history looks nicer when all
1041*4882a593Smuzhiyun            #    new commits are rooted in the same merge commit
1042*4882a593Smuzhiyun            old2new_revs[last_revision] = head
1043*4882a593Smuzhiyun
1044*4882a593Smuzhiyun        # We care about all commits (--full-history and --sparse) and
1045*4882a593Smuzhiyun        # we want reconstruct the topology and thus do not care
1046*4882a593Smuzhiyun        # about ordering by time (--topo-order). We ask for the ones
1047*4882a593Smuzhiyun        # we need to import first to be listed first (--reverse).
1048*4882a593Smuzhiyun        revs = runcmd("git rev-list %s" % rev_list_args, **largs).split()
1049*4882a593Smuzhiyun        logger.debug("To be imported: %s" % revs)
1050*4882a593Smuzhiyun        # Now 'revs' contains all revisions reachable from the top revision.
1051*4882a593Smuzhiyun        # All revisions derived from the 'last_revision' definitely are new,
1052*4882a593Smuzhiyun        # whereas the others may or may not have been imported before. For
1053*4882a593Smuzhiyun        # a linear history in the component, that second set will be empty.
1054*4882a593Smuzhiyun        # To distinguish between them, we also get the shorter list
1055*4882a593Smuzhiyun        # of revisions starting at the ancestor.
1056*4882a593Smuzhiyun        if last_revision:
1057*4882a593Smuzhiyun            ancestor_revs = runcmd("git rev-list --ancestry-path %s" % rev_list_args, **largs).split()
1058*4882a593Smuzhiyun        else:
1059*4882a593Smuzhiyun            ancestor_revs = []
1060*4882a593Smuzhiyun        logger.debug("Ancestors: %s" % ancestor_revs)
1061*4882a593Smuzhiyun
1062*4882a593Smuzhiyun        # Now import each revision.
1063*4882a593Smuzhiyun        logger.info("Importing commits from %s..." % name)
1064*4882a593Smuzhiyun        def import_rev(rev):
1065*4882a593Smuzhiyun            global scanned_revs
1066*4882a593Smuzhiyun
1067*4882a593Smuzhiyun            # If it is part of the new commits, we definitely need
1068*4882a593Smuzhiyun            # to import it. Otherwise we need to check, we might have
1069*4882a593Smuzhiyun            # imported it before. If it was imported and we merely
1070*4882a593Smuzhiyun            # fail to find it because commit messages did not track
1071*4882a593Smuzhiyun            # the mapping, then we end up importing it again. So
1072*4882a593Smuzhiyun            # combined repos using "updating with history" really should
1073*4882a593Smuzhiyun            # enable the "From ... rev:" commit header modifications.
1074*4882a593Smuzhiyun            if rev not in ancestor_revs and rev not in old2new_revs and not scanned_revs:
1075*4882a593Smuzhiyun                logger.debug("Revision %s triggers log analysis." % rev)
1076*4882a593Smuzhiyun                find_revs(old2new_revs, head)
1077*4882a593Smuzhiyun                scanned_revs = True
1078*4882a593Smuzhiyun            new_rev = old2new_revs.get(rev, None)
1079*4882a593Smuzhiyun            if new_rev:
1080*4882a593Smuzhiyun                return new_rev
1081*4882a593Smuzhiyun
1082*4882a593Smuzhiyun            # If the commit is not in the original list of revisions
1083*4882a593Smuzhiyun            # to be imported, then it must be a parent of one of those
1084*4882a593Smuzhiyun            # commits and it was skipped during earlier imports or not
1085*4882a593Smuzhiyun            # found. Importing such merge commits leads to very ugly
1086*4882a593Smuzhiyun            # history (long cascade of merge commits which all point
1087*4882a593Smuzhiyun            # to to older commits) when switching from "update via
1088*4882a593Smuzhiyun            # patches" to "update with history".
1089*4882a593Smuzhiyun            #
1090*4882a593Smuzhiyun            # We can avoid importing merge commits if all non-merge commits
1091*4882a593Smuzhiyun            # reachable from it were already imported. In that case we
1092*4882a593Smuzhiyun            # can root the new commits in the current head revision.
1093*4882a593Smuzhiyun            def is_imported(prev):
1094*4882a593Smuzhiyun                parents = runcmd("git show --no-patch --pretty=format:%P " + prev, **largs).split()
1095*4882a593Smuzhiyun                if len(parents) > 1:
1096*4882a593Smuzhiyun                    for p in parents:
1097*4882a593Smuzhiyun                        if not is_imported(p):
1098*4882a593Smuzhiyun                            logger.debug("Must import %s because %s is not imported." % (rev, p))
1099*4882a593Smuzhiyun                            return False
1100*4882a593Smuzhiyun                    return True
1101*4882a593Smuzhiyun                elif prev in old2new_revs:
1102*4882a593Smuzhiyun                    return True
1103*4882a593Smuzhiyun                else:
1104*4882a593Smuzhiyun                    logger.debug("Must import %s because %s is not imported." % (rev, prev))
1105*4882a593Smuzhiyun                    return False
1106*4882a593Smuzhiyun            if rev not in revs and is_imported(rev):
1107*4882a593Smuzhiyun                old2new_revs[rev] = head
1108*4882a593Smuzhiyun                return head
1109*4882a593Smuzhiyun
1110*4882a593Smuzhiyun            # Need to import rev. Collect some information about it.
1111*4882a593Smuzhiyun            logger.debug("Importing %s" % rev)
1112*4882a593Smuzhiyun            (parents, author_name, author_email, author_timestamp, body) = \
1113*4882a593Smuzhiyun                runcmd("git show --no-patch --pretty=format:%P%x00%an%x00%ae%x00%at%x00%B " + rev, **largs).split(chr(0))
1114*4882a593Smuzhiyun            parents = parents.split()
1115*4882a593Smuzhiyun            if parents:
1116*4882a593Smuzhiyun                # Arbitrarily pick the first parent as base. It may or may not have
1117*4882a593Smuzhiyun                # been imported before. For example, if the parent is a merge commit
1118*4882a593Smuzhiyun                # and previously the combined repository used patching as update
1119*4882a593Smuzhiyun                # method, then the actual merge commit parent never was imported.
1120*4882a593Smuzhiyun                # To cover this, We recursively import parents.
1121*4882a593Smuzhiyun                parent = parents[0]
1122*4882a593Smuzhiyun                new_parent = import_rev(parent)
1123*4882a593Smuzhiyun                # Clean index and working tree. TODO: can we combine this and the
1124*4882a593Smuzhiyun                # next into one command with less file IO?
1125*4882a593Smuzhiyun                # "git reset --hard" does not work, it changes HEAD of the parent
1126*4882a593Smuzhiyun                # repo, which we wanted to avoid. Probably need to keep
1127*4882a593Smuzhiyun                # track of the rev that corresponds to the index and use apply_commit().
1128*4882a593Smuzhiyun                runcmd("git rm -q --ignore-unmatch -rf .", **wargs)
1129*4882a593Smuzhiyun                # Update index and working tree to match the parent.
1130*4882a593Smuzhiyun                runcmd("git checkout -q -f %s ." % new_parent, **wargs)
1131*4882a593Smuzhiyun            else:
1132*4882a593Smuzhiyun                parent = None
1133*4882a593Smuzhiyun                # Clean index and working tree.
1134*4882a593Smuzhiyun                runcmd("git rm -q --ignore-unmatch -rf .", **wargs)
1135*4882a593Smuzhiyun
1136*4882a593Smuzhiyun            # Modify index and working tree such that it mirrors the commit.
1137*4882a593Smuzhiyun            apply_commit(parent, rev, largs, wargs, dest_dir, file_filter=file_filter)
1138*4882a593Smuzhiyun
1139*4882a593Smuzhiyun            # Now commit.
1140*4882a593Smuzhiyun            new_tree = runcmd("git write-tree", **wargs).strip()
1141*4882a593Smuzhiyun            env = copy.deepcopy(wenv)
1142*4882a593Smuzhiyun            env['GIT_AUTHOR_NAME'] = author_name
1143*4882a593Smuzhiyun            env['GIT_AUTHOR_EMAIL'] = author_email
1144*4882a593Smuzhiyun            env['GIT_AUTHOR_DATE'] = author_timestamp
1145*4882a593Smuzhiyun            if hook:
1146*4882a593Smuzhiyun                # Need to turn the verbatim commit message into something resembling a patch header
1147*4882a593Smuzhiyun                # for the hook.
1148*4882a593Smuzhiyun                with tempfile.NamedTemporaryFile(mode='wt', delete=False) as patch:
1149*4882a593Smuzhiyun                    patch.write('Subject: [PATCH] ')
1150*4882a593Smuzhiyun                    patch.write(body)
1151*4882a593Smuzhiyun                    patch.write('\n---\n')
1152*4882a593Smuzhiyun                    patch.close()
1153*4882a593Smuzhiyun                    runcmd([hook, patch.name, rev, name])
1154*4882a593Smuzhiyun                    with open(patch.name) as f:
1155*4882a593Smuzhiyun                        body = f.read()[len('Subject: [PATCH] '):][:-len('\n---\n')]
1156*4882a593Smuzhiyun
1157*4882a593Smuzhiyun            # We can skip non-merge commits that did not change any files. Those are typically
1158*4882a593Smuzhiyun            # the result of file filtering, although they could also have been introduced
1159*4882a593Smuzhiyun            # intentionally upstream, in which case we drop some information here.
1160*4882a593Smuzhiyun            if len(parents) == 1:
1161*4882a593Smuzhiyun                parent_rev = import_rev(parents[0])
1162*4882a593Smuzhiyun                old_tree = runcmd("git show -s --pretty=format:%T " + parent_rev, **wargs).strip()
1163*4882a593Smuzhiyun                commit = old_tree != new_tree
1164*4882a593Smuzhiyun                if not commit:
1165*4882a593Smuzhiyun                    new_rev = parent_rev
1166*4882a593Smuzhiyun            else:
1167*4882a593Smuzhiyun                commit = True
1168*4882a593Smuzhiyun            if commit:
1169*4882a593Smuzhiyun                new_rev = runcmd("git commit-tree".split() + add_p([import_rev(p) for p in parents]) +
1170*4882a593Smuzhiyun                                 ["-m", body, new_tree],
1171*4882a593Smuzhiyun                                 env=env).strip()
1172*4882a593Smuzhiyun            old2new_revs[rev] = new_rev
1173*4882a593Smuzhiyun
1174*4882a593Smuzhiyun            return new_rev
1175*4882a593Smuzhiyun
1176*4882a593Smuzhiyun        if revs:
1177*4882a593Smuzhiyun            for rev in revs:
1178*4882a593Smuzhiyun                import_rev(rev)
1179*4882a593Smuzhiyun            # Remember how to update our current head. New components get added,
1180*4882a593Smuzhiyun            # updated components get the delta between current head and the updated component
1181*4882a593Smuzhiyun            # applied.
1182*4882a593Smuzhiyun            additional_heads[old2new_revs[revs[-1]]] = head if repo['last_revision'] else None
1183*4882a593Smuzhiyun            repo['last_revision'] = revs[-1]
1184*4882a593Smuzhiyun
1185*4882a593Smuzhiyun    # Now construct the final merge commit. We create the tree by
1186*4882a593Smuzhiyun    # starting with the head and applying the changes from each
1187*4882a593Smuzhiyun    # components imported head revision.
1188*4882a593Smuzhiyun    if additional_heads:
1189*4882a593Smuzhiyun        runcmd("git reset --hard", **wargs)
1190*4882a593Smuzhiyun        for rev, base in additional_heads.items():
1191*4882a593Smuzhiyun            apply_commit(base, rev, wargs, wargs, None)
1192*4882a593Smuzhiyun
1193*4882a593Smuzhiyun        # Commit with all component branches as parents as well as the previous head.
1194*4882a593Smuzhiyun        logger.info("Writing final merge commit...")
1195*4882a593Smuzhiyun        msg = conf_commit_msg(conf, components)
1196*4882a593Smuzhiyun        new_tree = runcmd("git write-tree", **wargs).strip()
1197*4882a593Smuzhiyun        new_rev = runcmd("git commit-tree".split() +
1198*4882a593Smuzhiyun                         add_p([head] + list(additional_heads.keys())) +
1199*4882a593Smuzhiyun                         ["-m", msg, new_tree],
1200*4882a593Smuzhiyun                         **wargs).strip()
1201*4882a593Smuzhiyun        # And done! This is the first time we change the HEAD in the actual work tree.
1202*4882a593Smuzhiyun        runcmd("git reset --hard %s" % new_rev)
1203*4882a593Smuzhiyun
1204*4882a593Smuzhiyun        # Update and stage the (potentially modified)
1205*4882a593Smuzhiyun        # combo-layer.conf, but do not commit separately.
1206*4882a593Smuzhiyun        for name in repos:
1207*4882a593Smuzhiyun            repo = conf.repos[name]
1208*4882a593Smuzhiyun            rev = repo['last_revision']
1209*4882a593Smuzhiyun            conf.update(name, "last_revision", rev)
1210*4882a593Smuzhiyun        if commit_conf_file(conf, components, False):
1211*4882a593Smuzhiyun            # Must augment the previous commit.
1212*4882a593Smuzhiyun            runcmd("git commit --amend -C HEAD")
1213*4882a593Smuzhiyun
1214*4882a593Smuzhiyun
1215*4882a593Smuzhiyunscanned_revs = False
1216*4882a593Smuzhiyundef find_revs(old2new, head):
1217*4882a593Smuzhiyun    '''Construct mapping from original commit hash to commit hash in
1218*4882a593Smuzhiyun    combined repo by looking at the commit messages. Depends on the
1219*4882a593Smuzhiyun    "From ... rev: ..." convention.'''
1220*4882a593Smuzhiyun    logger.info("Analyzing log messages to find previously imported commits...")
1221*4882a593Smuzhiyun    num_known = len(old2new)
1222*4882a593Smuzhiyun    log = runcmd("git log --grep='From .* rev: [a-fA-F0-9][a-fA-F0-9]*' --pretty=format:%H%x00%B%x00 " + head).split(chr(0))
1223*4882a593Smuzhiyun    regex = re.compile(r'From .* rev: ([a-fA-F0-9]+)')
1224*4882a593Smuzhiyun    for new_rev, body in zip(*[iter(log)]* 2):
1225*4882a593Smuzhiyun        # Use the last one, in the unlikely case there are more than one.
1226*4882a593Smuzhiyun        rev = regex.findall(body)[-1]
1227*4882a593Smuzhiyun        if rev not in old2new:
1228*4882a593Smuzhiyun            old2new[rev] = new_rev.strip()
1229*4882a593Smuzhiyun    logger.info("Found %d additional commits, leading to: %s" % (len(old2new) - num_known, old2new))
1230*4882a593Smuzhiyun
1231*4882a593Smuzhiyun
1232*4882a593Smuzhiyundef apply_commit(parent, rev, largs, wargs, dest_dir, file_filter=None):
1233*4882a593Smuzhiyun    '''Compare revision against parent, remove files deleted in the
1234*4882a593Smuzhiyun    commit, re-write new or modified ones. Moves them into dest_dir.
1235*4882a593Smuzhiyun    Optionally filters files.
1236*4882a593Smuzhiyun    '''
1237*4882a593Smuzhiyun    if not dest_dir:
1238*4882a593Smuzhiyun        dest_dir = "."
1239*4882a593Smuzhiyun    # -r recurses into sub-directories, given is the full overview of
1240*4882a593Smuzhiyun    # what changed.  We do not care about copy/edits or renames, so we
1241*4882a593Smuzhiyun    # can disable those with --no-renames (but we still parse them,
1242*4882a593Smuzhiyun    # because it was not clear from git documentation whether C and M
1243*4882a593Smuzhiyun    # lines can still occur).
1244*4882a593Smuzhiyun    logger.debug("Applying changes between %s and %s in %s" % (parent, rev, largs["destdir"]))
1245*4882a593Smuzhiyun    delete = []
1246*4882a593Smuzhiyun    update = []
1247*4882a593Smuzhiyun    if parent:
1248*4882a593Smuzhiyun        # Apply delta.
1249*4882a593Smuzhiyun        changes = runcmd("git diff-tree --no-commit-id --no-renames --name-status -r --raw -z %s %s" % (parent, rev), **largs).split(chr(0))
1250*4882a593Smuzhiyun        for status, name in zip(*[iter(changes)]*2):
1251*4882a593Smuzhiyun            if status[0] in "ACMRT":
1252*4882a593Smuzhiyun                update.append(name)
1253*4882a593Smuzhiyun            elif status[0] in "D":
1254*4882a593Smuzhiyun                delete.append(name)
1255*4882a593Smuzhiyun            else:
1256*4882a593Smuzhiyun                logger.error("Unknown status %s of file %s in revision %s" % (status, name, rev))
1257*4882a593Smuzhiyun                sys.exit(1)
1258*4882a593Smuzhiyun    else:
1259*4882a593Smuzhiyun        # Copy all files.
1260*4882a593Smuzhiyun        update.extend(runcmd("git ls-tree -r --name-only -z %s" % rev, **largs).split(chr(0)))
1261*4882a593Smuzhiyun
1262*4882a593Smuzhiyun    # Include/exclude files as define in the component config.
1263*4882a593Smuzhiyun    # Both updated and deleted file lists get filtered, because it might happen
1264*4882a593Smuzhiyun    # that a file gets excluded, pulled from a different component, and then the
1265*4882a593Smuzhiyun    # excluded file gets deleted. In that case we must keep the copy.
1266*4882a593Smuzhiyun    if file_filter:
1267*4882a593Smuzhiyun        file_filter(update)
1268*4882a593Smuzhiyun        file_filter(delete)
1269*4882a593Smuzhiyun
1270*4882a593Smuzhiyun    # We export into a tar archive here and extract with tar because it is simple (no
1271*4882a593Smuzhiyun    # need to implement file and symlink writing ourselves) and gives us some degree
1272*4882a593Smuzhiyun    # of parallel IO. The downside is that we have to pass the list of files via
1273*4882a593Smuzhiyun    # command line parameters - hopefully there will never be too many at once.
1274*4882a593Smuzhiyun    if update:
1275*4882a593Smuzhiyun        target = os.path.join(wargs["destdir"], dest_dir)
1276*4882a593Smuzhiyun        if not os.path.isdir(target):
1277*4882a593Smuzhiyun            os.makedirs(target)
1278*4882a593Smuzhiyun        quoted_target = pipes.quote(target)
1279*4882a593Smuzhiyun        # os.sysconf('SC_ARG_MAX') is lying: running a command with
1280*4882a593Smuzhiyun        # string length 629343 already failed with "Argument list too
1281*4882a593Smuzhiyun        # long" although SC_ARG_MAX = 2097152. "man execve" explains
1282*4882a593Smuzhiyun        # the limitations, but those are pretty complicated. So here
1283*4882a593Smuzhiyun        # we just hard-code a fixed value which is more likely to work.
1284*4882a593Smuzhiyun        max_cmdsize = 64 * 1024
1285*4882a593Smuzhiyun        while update:
1286*4882a593Smuzhiyun            quoted_args = []
1287*4882a593Smuzhiyun            unquoted_args = []
1288*4882a593Smuzhiyun            cmdsize = 100 + len(quoted_target)
1289*4882a593Smuzhiyun            while update:
1290*4882a593Smuzhiyun                quoted_next = pipes.quote(update[0])
1291*4882a593Smuzhiyun                size_next = len(quoted_next) + len(dest_dir) + 1
1292*4882a593Smuzhiyun                logger.debug('cmdline length %d + %d < %d?' % (cmdsize, size_next, os.sysconf('SC_ARG_MAX')))
1293*4882a593Smuzhiyun                if cmdsize + size_next < max_cmdsize:
1294*4882a593Smuzhiyun                    quoted_args.append(quoted_next)
1295*4882a593Smuzhiyun                    unquoted_args.append(update.pop(0))
1296*4882a593Smuzhiyun                    cmdsize += size_next
1297*4882a593Smuzhiyun                else:
1298*4882a593Smuzhiyun                    logger.debug('Breaking the cmdline at length %d' % cmdsize)
1299*4882a593Smuzhiyun                    break
1300*4882a593Smuzhiyun            logger.debug('Final cmdline length %d / %d' % (cmdsize, os.sysconf('SC_ARG_MAX')))
1301*4882a593Smuzhiyun            cmd = "git archive %s %s | tar -C %s -xf -" % (rev, ' '.join(quoted_args), quoted_target)
1302*4882a593Smuzhiyun            logger.debug('First cmdline length %d' % len(cmd))
1303*4882a593Smuzhiyun            runcmd(cmd, **largs)
1304*4882a593Smuzhiyun            cmd = "git add -f".split() + [os.path.join(dest_dir, x) for x in unquoted_args]
1305*4882a593Smuzhiyun            logger.debug('Second cmdline length %d' % reduce(lambda x, y: x + len(y), cmd, 0))
1306*4882a593Smuzhiyun            runcmd(cmd, **wargs)
1307*4882a593Smuzhiyun    if delete:
1308*4882a593Smuzhiyun        for path in delete:
1309*4882a593Smuzhiyun            if dest_dir:
1310*4882a593Smuzhiyun                path = os.path.join(dest_dir, path)
1311*4882a593Smuzhiyun        runcmd("git rm -f --ignore-unmatch".split() + [os.path.join(dest_dir, x) for x in delete], **wargs)
1312*4882a593Smuzhiyun
1313*4882a593Smuzhiyundef action_error(conf, args):
1314*4882a593Smuzhiyun    logger.info("invalid action %s" % args[0])
1315*4882a593Smuzhiyun
1316*4882a593Smuzhiyunactions = {
1317*4882a593Smuzhiyun    "init": action_init,
1318*4882a593Smuzhiyun    "update": action_update,
1319*4882a593Smuzhiyun    "pull": action_pull,
1320*4882a593Smuzhiyun    "splitpatch": action_splitpatch,
1321*4882a593Smuzhiyun    "sync-revs": action_sync_revs,
1322*4882a593Smuzhiyun}
1323*4882a593Smuzhiyun
1324*4882a593Smuzhiyundef main():
1325*4882a593Smuzhiyun    parser = optparse.OptionParser(
1326*4882a593Smuzhiyun        version = "Combo Layer Repo Tool version %s" % __version__,
1327*4882a593Smuzhiyun        usage = """%prog [options] action
1328*4882a593Smuzhiyun
1329*4882a593SmuzhiyunCreate and update a combination layer repository from multiple component repositories.
1330*4882a593Smuzhiyun
1331*4882a593SmuzhiyunAction:
1332*4882a593Smuzhiyun  init                   initialise the combo layer repo
1333*4882a593Smuzhiyun  update [components]    get patches from component repos and apply them to the combo repo
1334*4882a593Smuzhiyun  pull [components]      just pull component repos only
1335*4882a593Smuzhiyun  sync-revs [components] update the config file's last_revision for each repository
1336*4882a593Smuzhiyun  splitpatch [commit]    generate commit patch and split per component, default commit is HEAD""")
1337*4882a593Smuzhiyun
1338*4882a593Smuzhiyun    parser.add_option("-c", "--conf", help = "specify the config file (conf/combo-layer.conf is the default).",
1339*4882a593Smuzhiyun               action = "store", dest = "conffile", default = "conf/combo-layer.conf")
1340*4882a593Smuzhiyun
1341*4882a593Smuzhiyun    parser.add_option("-i", "--interactive", help = "interactive mode, user can edit the patch list and patches",
1342*4882a593Smuzhiyun               action = "store_true", dest = "interactive", default = False)
1343*4882a593Smuzhiyun
1344*4882a593Smuzhiyun    parser.add_option("-D", "--debug", help = "output debug information",
1345*4882a593Smuzhiyun               action = "store_true", dest = "debug", default = False)
1346*4882a593Smuzhiyun
1347*4882a593Smuzhiyun    parser.add_option("-n", "--no-pull", help = "skip pulling component repos during update",
1348*4882a593Smuzhiyun               action = "store_true", dest = "nopull", default = False)
1349*4882a593Smuzhiyun
1350*4882a593Smuzhiyun    parser.add_option("--hard-reset",
1351*4882a593Smuzhiyun               help = "instead of pull do fetch and hard-reset in component repos",
1352*4882a593Smuzhiyun               action = "store_true", dest = "hard_reset", default = False)
1353*4882a593Smuzhiyun
1354*4882a593Smuzhiyun    parser.add_option("-H", "--history", help = "import full history of components during init",
1355*4882a593Smuzhiyun                      action = "store_true", default = False)
1356*4882a593Smuzhiyun
1357*4882a593Smuzhiyun    options, args = parser.parse_args(sys.argv)
1358*4882a593Smuzhiyun
1359*4882a593Smuzhiyun    # Dispatch to action handler
1360*4882a593Smuzhiyun    if len(args) == 1:
1361*4882a593Smuzhiyun        logger.error("No action specified, exiting")
1362*4882a593Smuzhiyun        parser.print_help()
1363*4882a593Smuzhiyun    elif args[1] not in actions:
1364*4882a593Smuzhiyun        logger.error("Unsupported action %s, exiting\n" % (args[1]))
1365*4882a593Smuzhiyun        parser.print_help()
1366*4882a593Smuzhiyun    elif not os.path.exists(options.conffile):
1367*4882a593Smuzhiyun        logger.error("No valid config file, exiting\n")
1368*4882a593Smuzhiyun        parser.print_help()
1369*4882a593Smuzhiyun    else:
1370*4882a593Smuzhiyun        if options.debug:
1371*4882a593Smuzhiyun            logger.setLevel(logging.DEBUG)
1372*4882a593Smuzhiyun        confdata = Configuration(options)
1373*4882a593Smuzhiyun        initmode = (args[1] == 'init')
1374*4882a593Smuzhiyun        confdata.sanity_check(initmode)
1375*4882a593Smuzhiyun        actions.get(args[1], action_error)(confdata, args[1:])
1376*4882a593Smuzhiyun
1377*4882a593Smuzhiyunif __name__ == "__main__":
1378*4882a593Smuzhiyun    try:
1379*4882a593Smuzhiyun        ret = main()
1380*4882a593Smuzhiyun    except Exception:
1381*4882a593Smuzhiyun        ret = 1
1382*4882a593Smuzhiyun        import traceback
1383*4882a593Smuzhiyun        traceback.print_exc()
1384*4882a593Smuzhiyun    sys.exit(ret)
1385