# HG changeset patch # User Matt Mackall # Date 1174629861 18000 # Node ID 94bb953b43e501ddc467613b33485b9e9d4000c4 # Parent cd7b36b7869cda3f0b98b7913540c3aa7a421de4# Parent bda63383d5291fcfd3560501320318d7970bef3b Merge with crew diff --git a/hgext/patchbomb.py b/hgext/patchbomb.py --- a/hgext/patchbomb.py +++ b/hgext/patchbomb.py @@ -86,7 +86,13 @@ def patchbomb(ui, repo, *revs, **opts): The message contains two or three body parts. First, the rest of the changeset description. Next, (optionally) if the diffstat program is installed, the result of running diffstat on the patch. - Finally, the patch itself, as generated by "hg export".''' + Finally, the patch itself, as generated by "hg export". + + With --outgoing, emails will be generated for patches not + found in the target repository (or only those which are + ancestors of the specified revisions if any are provided) + ''' + def prompt(prompt, default = None, rest = ': ', empty_ok = False): if default: prompt += ' [%s]' % default prompt += rest @@ -165,6 +171,36 @@ def patchbomb(ui, repo, *revs, **opts): msg['X-Mercurial-Node'] = node return msg + def outgoing(dest, revs): + '''Return the revisions present locally but not in dest''' + dest = ui.expandpath(dest or 'default-push', dest or 'default') + revs = [repo.lookup(rev) for rev in revs] + other = hg.repository(ui, dest) + ui.status(_('comparing with %s\n') % dest) + o = repo.findoutgoing(other) + if not o: + ui.status(_("no changes found\n")) + return [] + o = repo.changelog.nodesbetween(o, revs or None)[0] + return [str(repo.changelog.rev(r)) for r in o] + + # option handling + commands.setremoteconfig(ui, opts) + if opts.get('outgoing'): + if len(revs) > 1: + raise util.Abort(_("too many destinations")) + dest = revs and revs[0] or None + revs = [] + + if opts.get('rev'): + if revs: + raise util.Abort(_('use only one form to specify the revision')) + revs = opts.get('rev') + + if opts.get('outgoing'): + revs = outgoing(dest, opts.get('rev')) + + # start start_time = util.makedate() def genmsgid(id): @@ -299,7 +335,9 @@ cmdtable = { ('', 'plain', None, 'omit hg patch header'), ('n', 'test', None, 'print messages that would be sent'), ('m', 'mbox', '', 'write messages to mbox file instead of sending them'), + ('o', 'outgoing', None, _('send changes not found in the target repository')), + ('r', 'rev', [], _('a revision to send')), ('s', 'subject', '', 'subject of first message (intro or single patch)'), - ('t', 'to', [], 'email addresses of recipients')], - "hg email [OPTION]... [REV]...") + ('t', 'to', [], 'email addresses of recipients')] + commands.remoteopts, + "hg email [OPTION]... [DEST]...") } diff --git a/mercurial/commands.py b/mercurial/commands.py --- a/mercurial/commands.py +++ b/mercurial/commands.py @@ -1476,15 +1476,21 @@ def import_(ui, repo, patch1, *patches, text/plain body parts before first diff are added to commit message. - If imported patch was generated by hg export, user and description + If the imported patch was generated by hg export, user and description from patch override values from message headers and body. Values given on command line with -m and -u override these. + If --exact is specified, import will set the working directory + to the parent of each patch before applying it, and will abort + if the resulting changeset has a different ID than the one + recorded in the patch. This may happen due to character set + problems or other deficiencies in the text patch format. + To read a patch from standard input, use patch name "-". """ patches = (patch1,) + patches - if not opts['force']: + if opts.get('exact') or not opts['force']: bail_if_changed(repo) d = opts["base"] @@ -1498,10 +1504,10 @@ def import_(ui, repo, patch1, *patches, if pf == '-': ui.status(_("applying patch from stdin\n")) - tmpname, message, user, date = patch.extract(ui, sys.stdin) + tmpname, message, user, date, nodeid, p1, p2 = patch.extract(ui, sys.stdin) else: ui.status(_("applying %s\n") % p) - tmpname, message, user, date = patch.extract(ui, file(pf)) + tmpname, message, user, date, nodeid, p1, p2 = patch.extract(ui, file(pf)) if tmpname is None: raise util.Abort(_('no diffs found')) @@ -1519,13 +1525,36 @@ def import_(ui, repo, patch1, *patches, message = None ui.debug(_('message:\n%s\n') % message) + wp = repo.workingctx().parents() + if opts.get('exact'): + if not nodeid or not p1: + raise util.Abort(_('not a mercurial patch')) + p1 = repo.lookup(p1) + p2 = repo.lookup(p2 or hex(nullid)) + + if p1 != wp[0].node(): + hg.clean(repo, p1, wlock=wlock) + repo.dirstate.setparents(p1, p2) + elif p2: + try: + p1 = repo.lookup(p1) + p2 = repo.lookup(p2) + if p1 == wp[0].node(): + repo.dirstate.setparents(p1, p2) + except RepoError: + pass + files = {} try: fuzz = patch.patch(tmpname, ui, strip=strip, cwd=repo.root, files=files) finally: files = patch.updatedir(ui, repo, files, wlock=wlock) - repo.commit(files, message, user, date, wlock=wlock, lock=lock) + n = repo.commit(files, message, user, date, wlock=wlock, lock=lock) + if opts.get('exact'): + if hex(n) != nodeid: + repo.rollback() + raise util.Abort(_('patch is damaged or loses information')) finally: os.unlink(tmpname) @@ -2771,7 +2800,9 @@ table = { 'meaning as the corresponding patch option')), ('b', 'base', '', _('base path')), ('f', 'force', None, - _('skip check for outstanding uncommitted changes'))] + commitopts, + _('skip check for outstanding uncommitted changes')), + ('', 'exact', None, + _('apply patch to the nodes from which it was generated'))] + commitopts, _('hg import [-p NUM] [-m MESSAGE] [-f] PATCH...')), "incoming|in": (incoming, [('M', 'no-merges', None, _('do not show merges')), diff --git a/mercurial/patch.py b/mercurial/patch.py --- a/mercurial/patch.py +++ b/mercurial/patch.py @@ -33,11 +33,11 @@ def copyfile(src, dst, basedir=None): def extract(ui, fileobj): '''extract patch from data read from fileobj. - patch can be normal patch or contained in email message. + patch can be a normal patch or contained in an email message. - return tuple (filename, message, user, date). any item in returned - tuple can be None. if filename is None, fileobj did not contain - patch. caller must unlink filename when done.''' + return tuple (filename, message, user, date, node, p1, p2). + Any item in the returned tuple can be None. If filename is None, + fileobj did not contain a patch. Caller must unlink filename when done.''' # attempt to detect the start of a patch # (this heuristic is borrowed from quilt) @@ -54,6 +54,8 @@ def extract(ui, fileobj): user = msg['From'] # should try to parse msg['Date'] date = None + nodeid = None + parents = [] if message: if message.startswith('[PATCH'): @@ -97,6 +99,10 @@ def extract(ui, fileobj): ui.debug('From: %s\n' % user) elif line.startswith("# Date "): date = line[7:] + elif line.startswith("# Node ID "): + nodeid = line[10:] + elif line.startswith("# Parent "): + parents.append(line[10:]) elif line == '---' and 'git-send-email' in msg['X-Mailer']: ignoretext = True if not line.startswith('# ') and not ignoretext: @@ -117,8 +123,10 @@ def extract(ui, fileobj): tmpfp.close() if not diffs_seen: os.unlink(tmpname) - return None, message, user, date - return tmpname, message, user, date + return None, message, user, date, None, None, None + p1 = parents and parents.pop(0) or None + p2 = parents and parents.pop(0) or None + return tmpname, message, user, date, nodeid, p1, p2 GP_PATCH = 1 << 0 # we have to run patch GP_FILTER = 1 << 1 # there's some copy/rename operation