def internalpatch(orig, ui, repo, patchobj, strip, prefix=b'', files=None, eolmode=b'strict', similarity=0): if files is None: files = set() r = orig(ui, repo, patchobj, strip, prefix=prefix, files=files, eolmode=eolmode, similarity=similarity) fakenow = ui.config(b'fakepatchtime', b'fakenow') if fakenow: # parsing 'fakenow' in YYYYmmddHHMM format makes comparison between # 'fakenow' value and 'touch -t YYYYmmddHHMM' argument easy fakenow = dateutil.parsedate(fakenow, [b'%Y%m%d%H%M'])[0] for f in files: repo.wvfs.utime(f, (fakenow, fakenow)) return r
def fakewrite(ui, func): # fake "now" of 'pack_dirstate' only if it is invoked while 'func' fakenow = ui.config(b'fakedirstatewritetime', b'fakenow') if not fakenow: # Execute original one, if fakenow isn't configured. This is # useful to prevent subrepos from executing replaced one, # because replacing 'parsers.pack_dirstate' is also effective # in subrepos. return func() # parsing 'fakenow' in YYYYmmddHHMM format makes comparison between # 'fakenow' value and 'touch -t YYYYmmddHHMM' argument easy fakenow = dateutil.parsedate(fakenow, [b'%Y%m%d%H%M'])[0] orig_pack_dirstate = parsers.pack_dirstate orig_dirstate_getfsnow = dirstate._getfsnow wrapper = lambda *args: pack_dirstate(fakenow, orig_pack_dirstate, *args) parsers.pack_dirstate = wrapper dirstate._getfsnow = lambda *args: fakenow try: return func() finally: parsers.pack_dirstate = orig_pack_dirstate dirstate._getfsnow = orig_dirstate_getfsnow
def _find_mbox_date(mbox, root, order): if order == 'latest': keys = _order_keys_date(mbox) msg = mbox[keys[-1]] else: # new msg = mbox[root] return parsedate(msg['date'])
def fetch(ui, repo, source=b'default', **opts): """pull changes from a remote repository, merge new changes if needed. This finds all changes from the repository at the specified path or URL and adds them to the local repository. If the pulled changes add a new branch head, the head is automatically merged, and the result of the merge is committed. Otherwise, the working directory is updated to include the new changes. When a merge is needed, the working directory is first updated to the newly pulled changes. Local changes are then merged into the pulled changes. To switch the merge order, use --switch-parent. See :hg:`help dates` for a list of formats valid for -d/--date. Returns 0 on success. """ opts = pycompat.byteskwargs(opts) date = opts.get(b'date') if date: opts[b'date'] = dateutil.parsedate(date) parent = repo.dirstate.p1() branch = repo.dirstate.branch() try: branchnode = repo.branchtip(branch) except error.RepoLookupError: branchnode = None if parent != branchnode: raise error.Abort( _(b'working directory not at branch tip'), hint=_(b"use 'hg update' to check out branch tip"), ) wlock = lock = None try: wlock = repo.wlock() lock = repo.lock() cmdutil.bailifchanged(repo) bheads = repo.branchheads(branch) bheads = [head for head in bheads if len(repo[head].children()) == 0] if len(bheads) > 1: raise error.Abort( _(b'multiple heads in this branch ' b'(use "hg heads ." and "hg merge" to merge)')) other = hg.peer(repo, opts, ui.expandpath(source)) ui.status( _(b'pulling from %s\n') % util.hidepassword(ui.expandpath(source))) revs = None if opts[b'rev']: try: revs = [other.lookup(rev) for rev in opts[b'rev']] except error.CapabilityError: err = _(b"other repository doesn't support revision lookup, " b"so a rev cannot be specified.") raise error.Abort(err) # Are there any changes at all? modheads = exchange.pull(repo, other, heads=revs).cgresult if modheads == 0: return 0 # Is this a simple fast-forward along the current branch? newheads = repo.branchheads(branch) newchildren = repo.changelog.nodesbetween([parent], newheads)[2] if len(newheads) == 1 and len(newchildren): if newchildren[0] != parent: return hg.update(repo, newchildren[0]) else: return 0 # Are there more than one additional branch heads? newchildren = [n for n in newchildren if n != parent] newparent = parent if newchildren: newparent = newchildren[0] hg.clean(repo, newparent) newheads = [n for n in newheads if n != newparent] if len(newheads) > 1: ui.status( _(b'not merging with %d other new branch heads ' b'(use "hg heads ." and "hg merge" to merge them)\n') % (len(newheads) - 1)) return 1 if not newheads: return 0 # Otherwise, let's merge. err = False if newheads: # By default, we consider the repository we're pulling # *from* as authoritative, so we merge our changes into # theirs. if opts[b'switch_parent']: firstparent, secondparent = newparent, newheads[0] else: firstparent, secondparent = newheads[0], newparent ui.status( _(b'updating to %d:%s\n') % (repo.changelog.rev(firstparent), short(firstparent))) hg.clean(repo, firstparent) p2ctx = repo[secondparent] ui.status( _(b'merging with %d:%s\n') % (p2ctx.rev(), short(secondparent))) err = hg.merge(p2ctx, remind=False) if not err: # we don't translate commit messages message = cmdutil.logmessage( ui, opts) or (b'Automated merge with %s' % util.removeauth(other.url())) editopt = opts.get(b'edit') or opts.get(b'force_editor') editor = cmdutil.getcommiteditor(edit=editopt, editform=b'fetch') n = repo.commit(message, opts[b'user'], opts[b'date'], editor=editor) ui.status( _(b'new changeset %d:%s merges remote changes with local\n') % (repo.changelog.rev(n), short(n))) return err finally: release(lock, wlock)
def _dosign(ui, repo, *revs, **opts): mygpg = newgpg(ui, **opts) opts = pycompat.byteskwargs(opts) sigver = b"0" sigmessage = b"" date = opts.get(b'date') if date: opts[b'date'] = dateutil.parsedate(date) if revs: nodes = [repo.lookup(n) for n in revs] else: nodes = [ node for node in repo.dirstate.parents() if node != hgnode.nullid ] if len(nodes) > 1: raise error.Abort( _(b'uncommitted merge - please provide a specific revision')) if not nodes: nodes = [repo.changelog.tip()] for n in nodes: hexnode = hgnode.hex(n) ui.write( _(b"signing %d:%s\n") % (repo.changelog.rev(n), hgnode.short(n))) # build data data = node2txt(repo, n, sigver) sig = mygpg.sign(data) if not sig: raise error.Abort(_(b"error while signing")) sig = binascii.b2a_base64(sig) sig = sig.replace(b"\n", b"") sigmessage += b"%s %s %s\n" % (hexnode, sigver, sig) # write it if opts[b'local']: repo.vfs.append(b"localsigs", sigmessage) return if not opts[b"force"]: msigs = match.exact([b'.hgsigs']) if any(repo.status(match=msigs, unknown=True, ignored=True)): raise error.Abort( _(b"working copy of .hgsigs is changed "), hint=_(b"please commit .hgsigs manually"), ) sigsfile = repo.wvfs(b".hgsigs", b"ab") sigsfile.write(sigmessage) sigsfile.close() if b'.hgsigs' not in repo.dirstate: repo[None].add([b".hgsigs"]) if opts[b"no_commit"]: return message = opts[b'message'] if not message: # we don't translate commit messages message = b"\n".join([ b"Added signature for changeset %s" % hgnode.short(n) for n in nodes ]) try: editor = cmdutil.getcommiteditor(editform=b'gpg.sign', **pycompat.strkwargs(opts)) repo.commit(message, opts[b'user'], opts[b'date'], match=msigs, editor=editor) except ValueError as inst: raise error.Abort(pycompat.bytestr(inst))
def createlog(ui, directory=None, root="", rlog=True, cache=None): '''Collect the CVS rlog''' # Because we store many duplicate commit log messages, reusing strings # saves a lot of memory and pickle storage space. _scache = {} def scache(s): "return a shared version of a string" return _scache.setdefault(s, s) ui.status(_('collecting CVS rlog\n')) log = [] # list of logentry objects containing the CVS state # patterns to match in CVS (r)log output, by state of use re_00 = re.compile(b'RCS file: (.+)$') re_01 = re.compile(b'cvs \\[r?log aborted\\]: (.+)$') re_02 = re.compile(b'cvs (r?log|server): (.+)\n$') re_03 = re.compile(b"(Cannot access.+CVSROOT)|" b"(can't create temporary directory.+)$") re_10 = re.compile(b'Working file: (.+)$') re_20 = re.compile(b'symbolic names:') re_30 = re.compile(b'\t(.+): ([\\d.]+)$') re_31 = re.compile(b'----------------------------$') re_32 = re.compile(b'=======================================' b'======================================$') re_50 = re.compile(b'revision ([\\d.]+)(\s+locked by:\s+.+;)?$') re_60 = re.compile(br'date:\s+(.+);\s+author:\s+(.+);\s+state:\s+(.+?);' br'(\s+lines:\s+(\+\d+)?\s+(-\d+)?;)?' br'(\s+commitid:\s+([^;]+);)?' br'(.*mergepoint:\s+([^;]+);)?') re_70 = re.compile(b'branches: (.+);$') file_added_re = re.compile(br'file [^/]+ was (initially )?added on branch') prefix = '' # leading path to strip of what we get from CVS if directory is None: # Current working directory # Get the real directory in the repository try: prefix = open(os.path.join('CVS','Repository'), 'rb').read().strip() directory = prefix if prefix == ".": prefix = "" except IOError: raise logerror(_('not a CVS sandbox')) if prefix and not prefix.endswith(pycompat.ossep): prefix += pycompat.ossep # Use the Root file in the sandbox, if it exists try: root = open(os.path.join('CVS','Root'), 'rb').read().strip() except IOError: pass if not root: root = encoding.environ.get('CVSROOT', '') # read log cache if one exists oldlog = [] date = None if cache: cachedir = os.path.expanduser('~/.hg.cvsps') if not os.path.exists(cachedir): os.mkdir(cachedir) # The cvsps cache pickle needs a uniquified name, based on the # repository location. The address may have all sort of nasties # in it, slashes, colons and such. So here we take just the # alphanumeric characters, concatenated in a way that does not # mix up the various components, so that # :pserver:user@server:/path # and # /pserver/user/server/path # are mapped to different cache file names. cachefile = root.split(":") + [directory, "cache"] cachefile = ['-'.join(re.findall(br'\w+', s)) for s in cachefile if s] cachefile = os.path.join(cachedir, '.'.join([s for s in cachefile if s])) if cache == 'update': try: ui.note(_('reading cvs log cache %s\n') % cachefile) oldlog = pickle.load(open(cachefile, 'rb')) for e in oldlog: if not (util.safehasattr(e, 'branchpoints') and util.safehasattr(e, 'commitid') and util.safehasattr(e, 'mergepoint')): ui.status(_('ignoring old cache\n')) oldlog = [] break ui.note(_('cache has %d log entries\n') % len(oldlog)) except Exception as e: ui.note(_('error reading cache: %r\n') % e) if oldlog: date = oldlog[-1].date # last commit date as a (time,tz) tuple date = dateutil.datestr(date, '%Y/%m/%d %H:%M:%S %1%2') # build the CVS commandline cmd = ['cvs', '-q'] if root: cmd.append('-d%s' % root) p = util.normpath(getrepopath(root)) if not p.endswith('/'): p += '/' if prefix: # looks like normpath replaces "" by "." prefix = p + util.normpath(prefix) else: prefix = p cmd.append(['log', 'rlog'][rlog]) if date: # no space between option and date string cmd.append('-d>%s' % date) cmd.append(directory) # state machine begins here tags = {} # dictionary of revisions on current file with their tags branchmap = {} # mapping between branch names and revision numbers rcsmap = {} state = 0 store = False # set when a new record can be appended cmd = [procutil.shellquote(arg) for arg in cmd] ui.note(_("running %s\n") % (' '.join(cmd))) ui.debug("prefix=%r directory=%r root=%r\n" % (prefix, directory, root)) pfp = procutil.popen(' '.join(cmd), 'rb') peek = util.fromnativeeol(pfp.readline()) while True: line = peek if line == '': break peek = util.fromnativeeol(pfp.readline()) if line.endswith('\n'): line = line[:-1] #ui.debug('state=%d line=%r\n' % (state, line)) if state == 0: # initial state, consume input until we see 'RCS file' match = re_00.match(line) if match: rcs = match.group(1) tags = {} if rlog: filename = util.normpath(rcs[:-2]) if filename.startswith(prefix): filename = filename[len(prefix):] if filename.startswith('/'): filename = filename[1:] if filename.startswith('Attic/'): filename = filename[6:] else: filename = filename.replace('/Attic/', '/') state = 2 continue state = 1 continue match = re_01.match(line) if match: raise logerror(match.group(1)) match = re_02.match(line) if match: raise logerror(match.group(2)) if re_03.match(line): raise logerror(line) elif state == 1: # expect 'Working file' (only when using log instead of rlog) match = re_10.match(line) assert match, _('RCS file must be followed by working file') filename = util.normpath(match.group(1)) state = 2 elif state == 2: # expect 'symbolic names' if re_20.match(line): branchmap = {} state = 3 elif state == 3: # read the symbolic names and store as tags match = re_30.match(line) if match: rev = [int(x) for x in match.group(2).split('.')] # Convert magic branch number to an odd-numbered one revn = len(rev) if revn > 3 and (revn % 2) == 0 and rev[-2] == 0: rev = rev[:-2] + rev[-1:] rev = tuple(rev) if rev not in tags: tags[rev] = [] tags[rev].append(match.group(1)) branchmap[match.group(1)] = match.group(2) elif re_31.match(line): state = 5 elif re_32.match(line): state = 0 elif state == 4: # expecting '------' separator before first revision if re_31.match(line): state = 5 else: assert not re_32.match(line), _('must have at least ' 'some revisions') elif state == 5: # expecting revision number and possibly (ignored) lock indication # we create the logentry here from values stored in states 0 to 4, # as this state is re-entered for subsequent revisions of a file. match = re_50.match(line) assert match, _('expected revision number') e = logentry(rcs=scache(rcs), file=scache(filename), revision=tuple([int(x) for x in match.group(1).split('.')]), branches=[], parent=None, commitid=None, mergepoint=None, branchpoints=set()) state = 6 elif state == 6: # expecting date, author, state, lines changed match = re_60.match(line) assert match, _('revision must be followed by date line') d = match.group(1) if d[2] == '/': # Y2K d = '19' + d if len(d.split()) != 3: # cvs log dates always in GMT d = d + ' UTC' e.date = dateutil.parsedate(d, ['%y/%m/%d %H:%M:%S', '%Y/%m/%d %H:%M:%S', '%Y-%m-%d %H:%M:%S']) e.author = scache(match.group(2)) e.dead = match.group(3).lower() == 'dead' if match.group(5): if match.group(6): e.lines = (int(match.group(5)), int(match.group(6))) else: e.lines = (int(match.group(5)), 0) elif match.group(6): e.lines = (0, int(match.group(6))) else: e.lines = None if match.group(7): # cvs 1.12 commitid e.commitid = match.group(8) if match.group(9): # cvsnt mergepoint myrev = match.group(10).split('.') if len(myrev) == 2: # head e.mergepoint = 'HEAD' else: myrev = '.'.join(myrev[:-2] + ['0', myrev[-2]]) branches = [b for b in branchmap if branchmap[b] == myrev] assert len(branches) == 1, ('unknown branch: %s' % e.mergepoint) e.mergepoint = branches[0] e.comment = [] state = 7 elif state == 7: # read the revision numbers of branches that start at this revision # or store the commit log message otherwise m = re_70.match(line) if m: e.branches = [tuple([int(y) for y in x.strip().split('.')]) for x in m.group(1).split(';')] state = 8 elif re_31.match(line) and re_50.match(peek): state = 5 store = True elif re_32.match(line): state = 0 store = True else: e.comment.append(line) elif state == 8: # store commit log message if re_31.match(line): cpeek = peek if cpeek.endswith('\n'): cpeek = cpeek[:-1] if re_50.match(cpeek): state = 5 store = True else: e.comment.append(line) elif re_32.match(line): state = 0 store = True else: e.comment.append(line) # When a file is added on a branch B1, CVS creates a synthetic # dead trunk revision 1.1 so that the branch has a root. # Likewise, if you merge such a file to a later branch B2 (one # that already existed when the file was added on B1), CVS # creates a synthetic dead revision 1.1.x.1 on B2. Don't drop # these revisions now, but mark them synthetic so # createchangeset() can take care of them. if (store and e.dead and e.revision[-1] == 1 and # 1.1 or 1.1.x.1 len(e.comment) == 1 and file_added_re.match(e.comment[0])): ui.debug('found synthetic revision in %s: %r\n' % (e.rcs, e.comment[0])) e.synthetic = True if store: # clean up the results and save in the log. store = False e.tags = sorted([scache(x) for x in tags.get(e.revision, [])]) e.comment = scache('\n'.join(e.comment)) revn = len(e.revision) if revn > 3 and (revn % 2) == 0: e.branch = tags.get(e.revision[:-1], [None])[0] else: e.branch = None # find the branches starting from this revision branchpoints = set() for branch, revision in branchmap.iteritems(): revparts = tuple([int(i) for i in revision.split('.')]) if len(revparts) < 2: # bad tags continue if revparts[-2] == 0 and revparts[-1] % 2 == 0: # normal branch if revparts[:-2] == e.revision: branchpoints.add(branch) elif revparts == (1, 1, 1): # vendor branch if revparts in e.branches: branchpoints.add(branch) e.branchpoints = branchpoints log.append(e) rcsmap[e.rcs.replace('/Attic/', '/')] = e.rcs if len(log) % 100 == 0: ui.status(stringutil.ellipsis('%d %s' % (len(log), e.file), 80) + '\n') log.sort(key=lambda x: (x.rcs, x.revision)) # find parent revisions of individual files versions = {} for e in sorted(oldlog, key=lambda x: (x.rcs, x.revision)): rcs = e.rcs.replace('/Attic/', '/') if rcs in rcsmap: e.rcs = rcsmap[rcs] branch = e.revision[:-1] versions[(e.rcs, branch)] = e.revision for e in log: branch = e.revision[:-1] p = versions.get((e.rcs, branch), None) if p is None: p = e.revision[:-2] e.parent = p versions[(e.rcs, branch)] = e.revision # update the log cache if cache: if log: # join up the old and new logs log.sort(key=lambda x: x.date) if oldlog and oldlog[-1].date >= log[0].date: raise logerror(_('log cache overlaps with new log entries,' ' re-run without cache.')) log = oldlog + log # write the new cachefile ui.note(_('writing cvs log cache %s\n') % cachefile) pickle.dump(log, open(cachefile, 'wb')) else: log = oldlog ui.status(_('%d log entries\n') % len(log)) encodings = ui.configlist('convert', 'cvsps.logencoding') if encodings: def revstr(r): # this is needed, because logentry.revision is a tuple of "int" # (e.g. (1, 2) for "1.2") return '.'.join(pycompat.maplist(pycompat.bytestr, r)) for entry in log: comment = entry.comment for e in encodings: try: entry.comment = comment.decode( pycompat.sysstr(e)).encode('utf-8') if ui.debugflag: ui.debug("transcoding by %s: %s of %s\n" % (e, revstr(entry.revision), entry.file)) break except UnicodeDecodeError: pass # try next encoding except LookupError as inst: # unknown encoding, maybe raise error.Abort(inst, hint=_('check convert.cvsps.logencoding' ' configuration')) else: raise error.Abort(_("no encoding can transcode" " CVS log message for %s of %s") % (revstr(entry.revision), entry.file), hint=_('check convert.cvsps.logencoding' ' configuration')) hook.hook(ui, None, "cvslog", True, log=log) return log
def getdate(n): if n not in dates: dates[n] = dateutil.parsedate(self.commitcache[n].date) return dates[n]
def email(ui, repo, *revs, **opts): """send changesets by email By default, diffs are sent in the format generated by :hg:`export`, one per message. The series starts with a "[PATCH 0 of N]" introduction, which describes the series as a whole. Each patch email has a Subject line of "[PATCH M of N] ...", using the first line of the changeset description as the subject text. The message contains two or three parts. First, the changeset description. With the -d/--diffstat option, if the diffstat program is installed, the result of running diffstat on the patch is inserted. Finally, the patch itself, as generated by :hg:`export`. With the -d/--diffstat or --confirm options, you will be presented with a final summary of all messages and asked for confirmation before the messages are sent. By default the patch is included as text in the email body for easy reviewing. Using the -a/--attach option will instead create an attachment for the patch. With -i/--inline an inline attachment will be created. You can include a patch both as text in the email body and as a regular or an inline attachment by combining the -a/--attach or -i/--inline with the --body option. With -B/--bookmark changesets reachable by the given bookmark are selected. With -o/--outgoing, emails will be generated for patches not found in the destination repository (or only those which are ancestors of the specified revisions if any are provided) With -b/--bundle, changesets are selected as for --outgoing, but a single email containing a binary Mercurial bundle as an attachment will be sent. Use the ``patchbomb.bundletype`` config option to control the bundle type as with :hg:`bundle --type`. With -m/--mbox, instead of previewing each patchbomb message in a pager or sending the messages directly, it will create a UNIX mailbox file with the patch emails. This mailbox file can be previewed with any mail user agent which supports UNIX mbox files. With -n/--test, all steps will run, but mail will not be sent. You will be prompted for an email recipient address, a subject and an introductory message describing the patches of your patchbomb. Then when all is done, patchbomb messages are displayed. In case email sending fails, you will find a backup of your series introductory message in ``.hg/last-email.txt``. The default behavior of this command can be customized through configuration. (See :hg:`help patchbomb` for details) Examples:: hg email -r 3000 # send patch 3000 only hg email -r 3000 -r 3001 # send patches 3000 and 3001 hg email -r 3000:3005 # send patches 3000 through 3005 hg email 3000 # send patch 3000 (deprecated) hg email -o # send all patches not in default hg email -o DEST # send all patches not in DEST hg email -o -r 3000 # send all ancestors of 3000 not in default hg email -o -r 3000 DEST # send all ancestors of 3000 not in DEST hg email -B feature # send all ancestors of feature bookmark hg email -b # send bundle of all patches not in default hg email -b DEST # send bundle of all patches not in DEST hg email -b -r 3000 # bundle of all ancestors of 3000 not in default hg email -b -r 3000 DEST # bundle of all ancestors of 3000 not in DEST hg email -o -m mbox && # generate an mbox file... mutt -R -f mbox # ... and view it with mutt hg email -o -m mbox && # generate an mbox file ... formail -s sendmail \\ # ... and use formail to send from the mbox -bm -t < mbox # ... using sendmail Before using this command, you will need to enable email in your hgrc. See the [email] section in hgrc(5) for details. """ opts = pycompat.byteskwargs(opts) _charsets = mail._charsets(ui) bundle = opts.get(b'bundle') date = opts.get(b'date') mbox = opts.get(b'mbox') outgoing = opts.get(b'outgoing') rev = opts.get(b'rev') bookmark = opts.get(b'bookmark') if not (opts.get(b'test') or mbox): # really sending mail.validateconfig(ui) if not (revs or rev or outgoing or bundle or bookmark): raise error.Abort( _(b'specify at least one changeset with -B, -r or -o')) if outgoing and bundle: raise error.Abort( _(b"--outgoing mode always on with --bundle;" b" do not re-specify --outgoing")) cmdutil.check_at_most_one_arg(opts, b'rev', b'bookmark') if outgoing or bundle: if len(revs) > 1: raise error.Abort(_(b"too many destinations")) if revs: dest = revs[0] else: dest = None revs = [] if rev: if revs: raise error.Abort(_(b'use only one form to specify the revision')) revs = rev elif bookmark: if bookmark not in repo._bookmarks: raise error.Abort(_(b"bookmark '%s' not found") % bookmark) revs = scmutil.bookmarkrevs(repo, bookmark) revs = scmutil.revrange(repo, revs) if outgoing: revs = _getoutgoing(repo, dest, revs) if bundle: opts[b'revs'] = [b"%d" % r for r in revs] # check if revision exist on the public destination publicurl = repo.ui.config(b'patchbomb', b'publicurl') if publicurl: repo.ui.debug(b'checking that revision exist in the public repo\n') try: publicpeer = hg.peer(repo, {}, publicurl) except error.RepoError: repo.ui.write_err( _(b'unable to access public repo: %s\n') % publicurl) raise if not publicpeer.capable(b'known'): repo.ui.debug(b'skipping existence checks: public repo too old\n') else: out = [repo[r] for r in revs] known = publicpeer.known(h.node() for h in out) missing = [] for idx, h in enumerate(out): if not known[idx]: missing.append(h) if missing: if len(missing) > 1: msg = _(b'public "%s" is missing %s and %i others') msg %= (publicurl, missing[0], len(missing) - 1) else: msg = _(b'public url %s is missing %s') msg %= (publicurl, missing[0]) missingrevs = [ctx.rev() for ctx in missing] revhint = b' '.join( b'-r %s' % h for h in repo.set(b'heads(%ld)', missingrevs)) hint = _(b"use 'hg push %s %s'") % (publicurl, revhint) raise error.Abort(msg, hint=hint) # start if date: start_time = dateutil.parsedate(date) else: start_time = dateutil.makedate() def genmsgid(id): return _msgid(id[:20], int(start_time[0])) # deprecated config: patchbomb.from sender = (opts.get(b'from') or ui.config(b'email', b'from') or ui.config(b'patchbomb', b'from') or prompt(ui, b'From', ui.username())) if bundle: stropts = pycompat.strkwargs(opts) bundledata = _getbundle(repo, dest, **stropts) bundleopts = stropts.copy() bundleopts.pop('bundle', None) # already processed msgs = _getbundlemsgs(repo, sender, bundledata, **bundleopts) else: msgs = _getpatchmsgs(repo, sender, revs, **pycompat.strkwargs(opts)) showaddrs = [] def getaddrs(header, ask=False, default=None): configkey = header.lower() opt = header.replace(b'-', b'_').lower() addrs = opts.get(opt) if addrs: showaddrs.append(b'%s: %s' % (header, b', '.join(addrs))) return mail.addrlistencode(ui, addrs, _charsets, opts.get(b'test')) # not on the command line: fallback to config and then maybe ask addr = ui.config(b'email', configkey) or ui.config( b'patchbomb', configkey) if not addr: specified = ui.hasconfig(b'email', configkey) or ui.hasconfig( b'patchbomb', configkey) if not specified and ask: addr = prompt(ui, header, default=default) if addr: showaddrs.append(b'%s: %s' % (header, addr)) return mail.addrlistencode(ui, [addr], _charsets, opts.get(b'test')) elif default: return mail.addrlistencode(ui, [default], _charsets, opts.get(b'test')) return [] to = getaddrs(b'To', ask=True) if not to: # we can get here in non-interactive mode raise error.Abort(_(b'no recipient addresses provided')) cc = getaddrs(b'Cc', ask=True, default=b'') bcc = getaddrs(b'Bcc') replyto = getaddrs(b'Reply-To') confirm = ui.configbool(b'patchbomb', b'confirm') confirm |= bool(opts.get(b'diffstat') or opts.get(b'confirm')) if confirm: ui.write(_(b'\nFinal summary:\n\n'), label=b'patchbomb.finalsummary') ui.write((b'From: %s\n' % sender), label=b'patchbomb.from') for addr in showaddrs: ui.write(b'%s\n' % addr, label=b'patchbomb.to') for m, subj, ds in msgs: ui.write((b'Subject: %s\n' % subj), label=b'patchbomb.subject') if ds: ui.write(ds, label=b'patchbomb.diffstats') ui.write(b'\n') if ui.promptchoice( _(b'are you sure you want to send (yn)?$$ &Yes $$ &No')): raise error.Abort(_(b'patchbomb canceled')) ui.write(b'\n') parent = opts.get(b'in_reply_to') or None # angle brackets may be omitted, they're not semantically part of the msg-id if parent is not None: parent = encoding.strfromlocal(parent) if not parent.startswith('<'): parent = '<' + parent if not parent.endswith('>'): parent += '>' sender_addr = eutil.parseaddr(encoding.strfromlocal(sender))[1] sender = mail.addressencode(ui, sender, _charsets, opts.get(b'test')) sendmail = None firstpatch = None progress = ui.makeprogress(_(b'sending'), unit=_(b'emails'), total=len(msgs)) for i, (m, subj, ds) in enumerate(msgs): try: m['Message-Id'] = genmsgid(m['X-Mercurial-Node']) if not firstpatch: firstpatch = m['Message-Id'] m['X-Mercurial-Series-Id'] = firstpatch except TypeError: m['Message-Id'] = genmsgid('patchbomb') if parent: m['In-Reply-To'] = parent m['References'] = parent if not parent or 'X-Mercurial-Node' not in m: parent = m['Message-Id'] m['User-Agent'] = 'Mercurial-patchbomb/%s' % util.version().decode() m['Date'] = eutil.formatdate(start_time[0], localtime=True) start_time = (start_time[0] + 1, start_time[1]) m['From'] = sender m['To'] = ', '.join(to) if cc: m['Cc'] = ', '.join(cc) if bcc: m['Bcc'] = ', '.join(bcc) if replyto: m['Reply-To'] = ', '.join(replyto) if opts.get(b'test'): ui.status(_(b'displaying '), subj, b' ...\n') ui.pager(b'email') generator = mail.Generator(ui, mangle_from_=False) try: generator.flatten(m, False) ui.write(b'\n') except IOError as inst: if inst.errno != errno.EPIPE: raise else: if not sendmail: sendmail = mail.connect(ui, mbox=mbox) ui.status(_(b'sending '), subj, b' ...\n') progress.update(i, item=subj) if not mbox: # Exim does not remove the Bcc field del m['Bcc'] fp = stringio() generator = mail.Generator(fp, mangle_from_=False) generator.flatten(m, False) alldests = to + bcc + cc sendmail(sender_addr, alldests, fp.getvalue()) progress.complete()
def ilist(ui, repo, **opts): """List issues associated with the project""" # Process options show_all = opts['all'] properties = [] match_date, date_match = False, lambda x: True if opts['date']: match_date, date_match = True, matchdate(opts['date']) order = 'new' if opts['order']: order = opts['order'] # Formats formats = _read_formats(ui) # Find issues issues_dir = ui.config('artemis', 'issues', default=default_issues_dir) issues_path = os.path.join(repo.root, issues_dir) if not os.path.exists(issues_path): return issues = glob.glob(os.path.join(issues_path, '*')) _create_all_missing_dirs(issues_path, issues) # Process filter if opts['filter']: filters = glob.glob(os.path.join(issues_path, filter_prefix + '*')) config = ConfigParser.SafeConfigParser() config.read(filters) if not config.has_section(opts['filter']): ui.write('No filter %s defined\n' % opts['filter']) else: properties += config.items(opts['filter']) cmd_properties = _get_properties(opts['property']) list_properties = [p[0] for p in cmd_properties if len(p) == 1] list_properties_dict = {} properties += filter(lambda p: len(p) > 1, cmd_properties) summaries = [] for issue in issues: mbox = mailbox.Maildir(issue, factory=mailbox.MaildirMessage) root = _find_root_key(mbox) if not root: continue property_match = True for property, value in properties: if value: property_match = property_match and (mbox[root][property] == value) else: property_match = property_match and (property not in mbox[root]) if not show_all and (not properties or not property_match) and ( properties or mbox[root]['State'].upper() in [f.upper() for f in state['resolved']]): continue if match_date and not date_match(parsedate(mbox[root]['date'])[0]): continue if not list_properties: summaries.append(( _summary_line(mbox, root, issue[len(issues_path) + 1:], formats), # +1 for trailing / _find_mbox_date(mbox, root, order))) else: for lp in list_properties: if lp in mbox[root]: list_properties_dict.setdefault(lp, set()).add(mbox[root][lp]) if not list_properties: summaries.sort(lambda (s1, d1), (s2, d2): cmp(d2, d1)) for s, d in summaries: ui.write(s + '\n') else: for lp in list_properties_dict.keys(): ui.write("%s:\n" % lp) for value in sorted(list_properties_dict[lp]): ui.write(" %s\n" % value)
def _order_keys_date(mbox): keys = mbox.keys() root = _find_root_key(mbox) keys.sort(lambda k1, k2: -(k1 == root) or cmp(parsedate(mbox[k1]['date']), parsedate(mbox[k2]['date']))) return keys