def __init__(self, patchpath, repo, pf=None, rev=None): """ Read patch context from file :param pf: currently ignored The provided handle is used to read the patch and the patchpath contains the name of the patch. The handle is NOT closed. """ self._path = patchpath self._patchname = os.path.basename(patchpath) self._repo = repo self._rev = rev or 'patch' self._status = [[], [], []] self._fileorder = [] self._user = '' self._desc = '' self._branch = '' self._node = node.nullid self._identity = node.nullid self._mtime = None self._fsize = 0 self._parseerror = None self._phase = 'draft' try: self._mtime = os.path.getmtime(patchpath) self._fsize = os.path.getsize(patchpath) ph = mq.patchheader(self._path) self._ph = ph hash = util.sha1(self._path) hash.update(str(self._mtime)) self._identity = hash.digest() except EnvironmentError: self._date = util.makedate() return try: self._branch = ph.branch or '' self._node = binascii.unhexlify(ph.nodeid) if self._repo.ui.configbool('mq', 'secret'): self._phase = 'secret' except TypeError: pass except AttributeError: # hacks to try to deal with older versions of mq.py self._branch = '' ph.diffstartline = len(ph.comments) if ph.message: ph.diffstartline += 1 except error.ConfigError: pass self._user = ph.user or '' self._desc = ph.message and '\n'.join(ph.message).strip() or '' try: self._date = ph.date and util.parsedate(ph.date) or util.makedate() except error.Abort: self._date = util.makedate()
def __init__(self, patchpath, repo, pf=None, rev=None): """ Read patch context from file :param pf: currently ignored The provided handle is used to read the patch and the patchpath contains the name of the patch. The handle is NOT closed. """ self._path = patchpath if rev: assert isinstance(rev, str) self._patchname = rev else: self._patchname = os.path.basename(patchpath) self._repo = repo self._rev = rev or 'patch' self._status = [[], [], []] self._fileorder = [] self._user = '' self._desc = '' self._branch = '' self._node = node.nullid self._mtime = None self._fsize = 0 self._parseerror = None self._phase = 'draft' try: self._mtime = os.path.getmtime(patchpath) self._fsize = os.path.getsize(patchpath) ph = mq.patchheader(self._path) self._ph = ph except EnvironmentError: self._date = util.makedate() return try: self._branch = ph.branch or '' self._node = binascii.unhexlify(ph.nodeid) if self._repo.ui.configbool('mq', 'secret'): self._phase = 'secret' except TypeError: pass except AttributeError: # hacks to try to deal with older versions of mq.py self._branch = '' ph.diffstartline = len(ph.comments) if ph.message: ph.diffstartline += 1 except error.ConfigError: pass self._user = ph.user or '' self._desc = ph.message and '\n'.join(ph.message).strip() or '' try: self._date = ph.date and util.parsedate(ph.date) or util.makedate() except error.Abort: self._date = util.makedate()
def __init__(self, patchpath, repo, pf=None, rev=None): """ Read patch context from file :param pf: currently ignored The provided handle is used to read the patch and the patchpath contains the name of the patch. The handle is NOT closed. """ self._path = patchpath self._patchname = os.path.basename(patchpath) self._repo = repo self._rev = rev or 'patch' self._status = [[], [], []] self._fileorder = [] self._user = '' self._desc = '' self._branch = '' self._node = node.nullid self._identity = node.nullid self._mtime = None self._fsize = 0 self._parseerror = None try: self._mtime = os.path.getmtime(patchpath) self._fsize = os.path.getsize(patchpath) ph = mq.patchheader(self._path) self._ph = ph hash = util.sha1(self._path) hash.update(str(self._mtime)) self._identity = hash.digest() except EnvironmentError: self._date = util.makedate() return try: self._branch = ph.branch or '' self._node = binascii.unhexlify(ph.nodeid) except TypeError: pass except AttributeError: # hacks to try to deal with older versions of mq.py self._branch = '' ph.diffstartline = len(ph.comments) if ph.message: ph.diffstartline += 1 self._user = ph.user or '' self._date = ph.date and util.parsedate(ph.date) or util.makedate() self._desc = ph.message and '\n'.join(ph.message).strip() or ''
def record(self, namespace, name, oldhashes, newhashes): """Record a new journal entry * namespace: an opaque string; this can be used to filter on the type of recorded entries. * name: the name defining this entry; for bookmarks, this is the bookmark name. Can be filtered on when retrieving entries. * oldhashes and newhashes: each a single binary hash, or a list of binary hashes. These represent the old and new position of the named item. """ if not isinstance(oldhashes, list): oldhashes = [oldhashes] if not isinstance(newhashes, list): newhashes = [newhashes] entry = journalentry(util.makedate(), self.user, self.command, namespace, name, oldhashes, newhashes) vfs = self.vfs if self.sharedvfs is not None: # write to the shared repository if this feature is being # shared between working copies. if sharednamespaces.get(namespace) in self.sharedfeatures: vfs = self.sharedvfs self._write(vfs, entry)
def setupheaderopts(ui, opts): """sets the user and date; copied from mq""" def do(opt, val): if not opts.get(opt) and opts.get('current' + opt): opts[opt] = val do('user', ui.username()) do('date', '%d %d' % util.makedate())
def chash(manifest, files, desc, p1, p2, user, date, extra): """Compute changeset hash from the changeset pieces.""" user = user.strip() if "\n" in user: raise error.RevlogError( _("username %s contains a newline") % repr(user)) # strip trailing whitespace and leading and trailing empty lines desc = '\n'.join([l.rstrip() for l in desc.splitlines()]).strip('\n') user, desc = encoding.fromlocal(user), encoding.fromlocal(desc) if date: parseddate = "%d %d" % util.parsedate(date) else: parseddate = "%d %d" % util.makedate() extra = extra.copy() if 'signature' in extra: del extra['signature'] if extra.get("branch") in ("default", ""): del extra["branch"] if extra: extra = changelog.encodeextra(extra) parseddate = "%s %s" % (parseddate, extra) l = [hex(manifest), user, parseddate] + sorted(files) + ["", desc] text = "\n".join(l) return revlog.hash(text, p1, p2)
def filelog(orig, web, req, tmpl): """Wraps webcommands.filelog to provide pushlog metadata to template.""" if hasattr(web.repo, 'pushlog'): class _tmpl(object): def __init__(self): self.defaults = tmpl.defaults def __call__(self, *args, **kwargs): self.args = args self.kwargs = kwargs return self class _ctx(object): def __init__(self, hex): self._hex = hex def hex(self): return self._hex t = orig(web, req, _tmpl()) for entry in t.kwargs['entries']: push = web.repo.pushlog.pushfromchangeset(_ctx(entry['node'])) if push: entry['pushid'] = push.pushid entry['pushdate'] = util.makedate(push.when) else: entry['pushid'] = None entry['pushdate'] = None return tmpl(*t.args, **t.kwargs) else: return orig(web, req, tmpl)
def entries(sortcolumn="", descending=False, subdir="", **map): rows = [] parity = common.paritygen(stripecount) for repo in Repository.objects.has_view_permission(request.user): contact = smart_str(repo.owner.get_full_name()) lastchange = (common.get_mtime(repo.location), util.makedate()[1]) row = dict(contact=contact or "unknown", contact_sort=contact.upper() or "unknown", name=smart_str(repo.name), name_sort=smart_str(repo.name), url=repo.get_absolute_url(), description=smart_str(repo.description) or "unknown", description_sort=smart_str(repo.description.upper()) or "unknown", lastchange=lastchange, lastchange_sort=lastchange[1]-lastchange[0], archives=archivelist(u, "tip", url)) if (not sortcolumn or (sortcolumn, descending) == sortdefault): # fast path for unsorted output row['parity'] = parity.next() yield row else: rows.append((row["%s_sort" % sortcolumn], row)) if rows: rows.sort() if descending: rows.reverse() for key, row in rows: row['parity'] = parity.next() yield row
def maxWidthValueForColumn(self, col): if self.graph is None: return 'XXXX' column = self._columns[col] if column == 'Rev': return '8' * len(str(len(self.repo))) + '+' if column == 'Node': return '8' * 12 + '+' if column in ('LocalTime', 'UTCTime'): return hglib.displaytime(util.makedate()) if column in ('Tags', 'Latest tags'): try: return sorted(self.repo.tags().keys(), key=lambda x: len(x))[-1][:10] except IndexError: pass if column == 'Branch': try: return sorted(self.repo.branchtags().keys(), key=lambda x: len(x))[-1] except IndexError: pass if column == 'Filename': return self.filename if column == 'Graph': res = self.col2x(self.graph.max_cols) return min(res, 150) if column == 'Changes': return 'Changes' # Fall through for Description return None
def maxWidthValueForColumn(self, col): if self.graph is None: return "XXXX" column = self._columns[col] if column == "Rev": return "8" * len(str(len(self.repo))) + "+" if column == "Node": return "8" * 12 + "+" if column in ("LocalTime", "UTCTime"): return hglib.displaytime(util.makedate()) if column == "Tags": try: return sorted(self.repo.tags().keys(), key=lambda x: len(x))[-1][:10] except IndexError: pass if column == "Branch": try: return sorted(self.repo.branchtags().keys(), key=lambda x: len(x))[-1] except IndexError: pass if column == "Filename": return self.filename if column == "Graph": res = self.col2x(self.graph.max_cols) return min(res, 150) if column == "Changes": return "Changes" # Fall through for Description return None
def chash(manifest, files, desc, p1, p2, user, date, extra): """Compute changeset hash from the changeset pieces.""" user = user.strip() if "\n" in user: raise error.RevlogError(_("username %s contains a newline") % repr(user)) # strip trailing whitespace and leading and trailing empty lines desc = '\n'.join([l.rstrip() for l in desc.splitlines()]).strip('\n') user, desc = encoding.fromlocal(user), encoding.fromlocal(desc) if date: parseddate = "%d %d" % util.parsedate(date) else: parseddate = "%d %d" % util.makedate() extra = extra.copy() if 'signature' in extra: del extra['signature'] if extra.get("branch") in ("default", ""): del extra["branch"] if extra: extra = changelog.encodeextra(extra) parseddate = "%s %s" % (parseddate, extra) l = [hex(manifest), user, parseddate] + sorted(files) + ["", desc] text = "\n".join(l) return revlog.hash(text, p1, p2)
def filelog(orig, web, req, tmpl): """Wraps webcommands.filelog to provide pushlog metadata to template.""" if hasattr(web.repo, 'pushlog'): class _tmpl(object): def __init__(self): self.defaults = tmpl.defaults def __call__(self, *args, **kwargs): self.args = args self.kwargs = kwargs return self class _ctx(object): def __init__(self, hex): self._hex = hex def hex(self): return self._hex t = orig(web, req, _tmpl()) for entry in t.kwargs['entries']: pushinfo = web.repo.pushlog.pushfromchangeset(_ctx(entry['node'])) entry['pushid'] = pushinfo[0] entry['pushdate'] = util.makedate(pushinfo[2]) return tmpl(*t.args, **t.kwargs) else: return orig(web, req, tmpl)
def makepatch(ui, repo, name=None, pats=[], opts={}): """sets up the call for attic.createpatch and makes the call""" s = repo.attic force = opts.get('force') if name and s.exists(name) and name != s.applied and not force: raise util.Abort(_('attempting to overwrite existing patch')) if name and s.applied and name != s.applied and not force: raise util.Abort(_('a different patch is active')) if not name: name = s.applied if not name: raise util.Abort(_('you need to supply a patch name')) date, user, message = None, None, '' if s.applied: data = patch.extract(ui, open(s.join(s.applied), 'r')) tmpname, message, user, date, branch, nodeid, p1, p2 = data os.unlink(tmpname) msg = cmdutil.logmessage(opts) if not msg: msg = message if opts.get('edit'): msg = ui.edit(msg, ui.username()) setupheaderopts(ui, opts) if opts.get('user'): user = opts['user'] if not user: user = ui.username() if opts.get('date'): date = opts['date'] if not date: date = util.makedate() date = util.parsedate(date) s.createpatch(repo, name, msg, user, date, pats, opts)
def record(self, namespace, name, oldhashes, newhashes): """Record a new journal entry * namespace: an opaque string; this can be used to filter on the type of recorded entries. * name: the name defining this entry; for bookmarks, this is the bookmark name. Can be filtered on when retrieving entries. * oldhashes and newhashes: each a single binary hash, or a list of binary hashes. These represent the old and new position of the named item. """ if not isinstance(oldhashes, list): oldhashes = [oldhashes] if not isinstance(newhashes, list): newhashes = [newhashes] entry = journalentry( util.makedate(), self.user, self.command, namespace, name, oldhashes, newhashes) vfs = self.vfs if self.sharedvfs is not None: # write to the shared repository if this feature is being # shared between working copies. if sharednamespaces.get(namespace) in self.sharedfeatures: vfs = self.sharedvfs self._write(vfs, entry)
def template_pushheaddates(repo, ctx, **args): """:pushheaddates: List of date information. The dates this changeset was pushed to various trees as a push head.""" node = ctx.node() pushes = repo.changetracker.pushes_for_changeset(ctx.node()) return [util.makedate(p[2]) for p in pushes if str(p[4]) == node]
def makepatch(ui, repo, name=None, pats=[], opts={}): """sets up the call for attic.createpatch and makes the call""" s = repo.attic force = opts.get('force') if name and s.exists(name) and name != s.applied and not force: raise util.Abort(_('attempting to overwrite existing patch')) if name and s.applied and name != s.applied and not force: raise util.Abort(_('a different patch is active')) if not name: name = s.applied if not name: raise util.Abort(_('you need to supply a patch name')) date, user, message = None, None, '' if s.applied: data = patch.extract(ui, open(s.join(s.applied), 'r')) tmpname, message, user, date, branch, nodeid, p1, p2 = data os.unlink(tmpname) msg = cmdutil.logmessage(opts) if not msg: msg = message if opts.get('edit'): msg = ui.edit(msg, ui.username()) setupheaderopts(ui, opts) if opts.get('user'): user=opts['user'] if not user: user = ui.username() if opts.get('date'): date=opts['date'] if not date: date = util.makedate() date = util.parsedate(date) s.createpatch(repo, name, msg, user, date, pats, opts)
def rawentries(subdir="", **map): descend = self.ui.configbool('web', 'descend', True) for name, path in self.repos: if not name.startswith(subdir): continue name = name[len(subdir):] if not descend and '/' in name: continue u = self.ui.copy() try: u.readconfig(os.path.join(path, '.hg', 'hgrc')) except Exception, e: u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e)) continue def get(section, name, default=None): return u.config(section, name, default, untrusted=True) if u.configbool("web", "hidden", untrusted=True): continue if not self.read_allowed(u, req): continue parts = [name] if 'PATH_INFO' in req.env: parts.insert(0, req.env['PATH_INFO'].rstrip('/')) if req.env['SCRIPT_NAME']: parts.insert(0, req.env['SCRIPT_NAME']) url = re.sub(r'/+', '/', '/'.join(parts) + '/') # update time with local timezone try: r = hg.repository(self.ui, path) except error.RepoError: u.warn(_('error accessing repository at %s\n') % path) continue try: d = (get_mtime(r.spath), util.makedate()[1]) except OSError: continue contact = get_contact(get) description = get("web", "description", "") name = get("web", "name", name) row = dict(contact=contact or "unknown", contact_sort=contact.upper() or "unknown", name=name, name_sort=name, url=url, description=description or "unknown", description_sort=description.upper() or "unknown", lastchange=d, lastchange_sort=d[1] - d[0], archives=archivelist(u, "tip", url)) yield row
def template_firstpushdate(repo, ctx, **args): """:firstpushdate: Date information. The date of the first push of this changeset.""" pushes = list(repo.changetracker.pushes_for_changeset(ctx.node())) if not pushes: return None return util.makedate(pushes[0][2])
def entries(sortcolumn="", descending=False, subdir="", **map): rows = [] parity = paritygen(self.stripecount) for name, path in self.repos: if not name.startswith(subdir): continue name = name[len(subdir):] u = self.ui.copy() try: u.readconfig(os.path.join(path, '.hg', 'hgrc')) except Exception, e: u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e)) continue def get(section, name, default=None): return u.config(section, name, default, untrusted=True) if u.configbool("web", "hidden", untrusted=True): continue if not self.read_allowed(u, req): continue parts = [name] if 'PATH_INFO' in req.env: parts.insert(0, req.env['PATH_INFO'].rstrip('/')) if req.env['SCRIPT_NAME']: parts.insert(0, req.env['SCRIPT_NAME']) m = re.match('((?:https?://)?)(.*)', '/'.join(parts)) # squish repeated slashes out of the path component url = m.group(1) + re.sub('/+', '/', m.group(2)) + '/' # update time with local timezone try: d = (get_mtime(path), util.makedate()[1]) except OSError: continue contact = get_contact(get) description = get("web", "description", "") name = get("web", "name", name) row = dict(contact=contact or "unknown", contact_sort=contact.upper() or "unknown", name=name, name_sort=name, url=url, description=description or "unknown", description_sort=description.upper() or "unknown", lastchange=d, lastchange_sort=d[1] - d[0], archives=archivelist(u, "tip", url)) if (not sortcolumn or (sortcolumn, descending) == sortdefault): # fast path for unsorted output row['parity'] = parity.next() yield row else: rows.append((row["%s_sort" % sortcolumn], row))
def rawentries(subdir="", **map): descend = self.ui.configbool('web', 'descend', True) for name, path in self.repos: if not name.startswith(subdir): continue name = name[len(subdir):] if not descend and '/' in name: continue u = self.ui.copy() try: u.readconfig(os.path.join(path, '.hg', 'hgrc')) except Exception, e: u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e)) continue def get(section, name, default=None): return u.config(section, name, default, untrusted=True) if u.configbool("web", "hidden", untrusted=True): continue if not self.read_allowed(u, req): continue parts = [name] if 'PATH_INFO' in req.env: parts.insert(0, req.env['PATH_INFO'].rstrip('/')) if req.env['SCRIPT_NAME']: parts.insert(0, req.env['SCRIPT_NAME']) url = re.sub(r'/+', '/', '/'.join(parts) + '/') # update time with local timezone try: r = hg.repository(self.ui, path) except error.RepoError: u.warn(_('error accessing repository at %s\n') % path) continue try: d = (get_mtime(r.spath), util.makedate()[1]) except OSError: continue contact = get_contact(get) description = get("web", "description", "") name = get("web", "name", name) row = dict(contact=contact or "unknown", contact_sort=contact.upper() or "unknown", name=name, name_sort=name, url=url, description=description or "unknown", description_sort=description.upper() or "unknown", lastchange=d, lastchange_sort=d[1]-d[0], archives=archivelist(u, "tip", url)) yield row
def entries(sortcolumn="", descending=False, subdir="", **map): rows = [] parity = paritygen(self.stripecount) for name, path in self.repos: if not name.startswith(subdir): continue name = name[len(subdir):] u = self.ui.copy() try: u.readconfig(os.path.join(path, '.hg', 'hgrc')) except Exception, e: u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e)) continue def get(section, name, default=None): return u.config(section, name, default, untrusted=True) if u.configbool("web", "hidden", untrusted=True): continue if not self.read_allowed(u, req): continue parts = [name] if 'PATH_INFO' in req.env: parts.insert(0, req.env['PATH_INFO'].rstrip('/')) if req.env['SCRIPT_NAME']: parts.insert(0, req.env['SCRIPT_NAME']) m = re.match('((?:https?://)?)(.*)', '/'.join(parts)) # squish repeated slashes out of the path component url = m.group(1) + re.sub('/+', '/', m.group(2)) + '/' # update time with local timezone try: d = (get_mtime(path), util.makedate()[1]) except OSError: continue contact = get_contact(get) description = get("web", "description", "") name = get("web", "name", name) row = dict(contact=contact or "unknown", contact_sort=contact.upper() or "unknown", name=name, name_sort=name, url=url, description=description or "unknown", description_sort=description.upper() or "unknown", lastchange=d, lastchange_sort=d[1]-d[0], archives=archivelist(u, "tip", url)) if (not sortcolumn or (sortcolumn, descending) == sortdefault): # fast path for unsorted output row['parity'] = parity.next() yield row else: rows.append((row["%s_sort" % sortcolumn], row))
def addpushmetadata(repo, ctx, d): if not hasattr(repo, 'pushlog'): return pushinfo = repo.pushlog.pushfromchangeset(ctx) if pushinfo: d['pushid'] = pushinfo[0] d['pushuser'] = pushinfo[1] d['pushdate'] = util.makedate(pushinfo[2]) d['pushnodes'] = pushinfo[3] d['pushhead'] = pushinfo[3][-1]
def addpushmetadata(repo, ctx, d): if not hasattr(repo, 'pushlog'): return push = repo.pushlog.pushfromchangeset(ctx) if push: d['pushid'] = push.pushid d['pushuser'] = push.user d['pushdate'] = util.makedate(push.when) d['pushnodes'] = push.nodes d['pushhead'] = push.nodes[-1]
def __call__(self, *args, **kwargs): for entry in kwargs.get('entries', []): push = web.repo.pushlog.pushfromnode(bin(entry['node'])) if push: entry['pushid'] = push.pushid entry['pushdate'] = util.makedate(push.when) else: entry['pushid'] = None entry['pushdate'] = None return super(tmplwrapper, self).__call__(*args, **kwargs)
def timetravel(ui, repo): "Change date of commit." ctx = repo[None].p1() while ctx.phase(): ctx = ctx.p1() parent = ctx date = util.makedate() update_node, strip_nodes = copy_branch(repo, ctx, parent, date) if update_node: hg.update(repo, update_node) if strip_nodes: repair.strip(ui, repo, strip_nodes)
def __init__(self, parent=None): QDialog.__init__(self, parent) self.setWindowFlags(Qt.Window) self.setWindowIcon(qtlib.geticon("hg-add")) self.setWindowTitle(_('New Patch Branch')) def AddField(var, label, optional=False): hbox = QHBoxLayout() SP = QSizePolicy le = QLineEdit() le.setSizePolicy(SP(SP.Expanding, SP.Fixed)) if optional: cb = QCheckBox(label) le.setEnabled(False) cb.toggled.connect(le.setEnabled) hbox.addWidget(cb) setattr(self, var + 'cb', cb) else: hbox.addWidget(QLabel(label)) hbox.addWidget(le) setattr(self, var + 'le', le) return hbox def DialogButtons(): BB = QDialogButtonBox bb = QDialogButtonBox(BB.Ok | BB.Cancel) bb.accepted.connect(self.accept) bb.rejected.connect(self.reject) bb.button(BB.Ok).setDefault(True) bb.button(BB.Cancel).setDefault(False) self.commitButton = bb.button(BB.Ok) self.commitButton.setText(_('Commit', 'action button')) self.bb = bb return bb layout = QVBoxLayout() layout.setContentsMargins(2, 2, 2, 2) self.setLayout(layout) layout.addLayout(AddField('patchname', _('Patch name:'))) layout.addLayout( AddField('patchtext', _('Patch message:'), optional=True)) layout.addLayout(AddField('patchdate', _('Patch date:'), optional=True)) layout.addLayout(AddField('patchuser', _('Patch user:'), optional=True)) layout.addWidget(DialogButtons()) self.patchdatele.setText( hglib.tounicode(hglib.displaytime(util.makedate())))
def listcmd(ui, repo, pats, opts): """subcommand that displays the list of shelves""" pats = set(pats) width = 80 if not ui.plain(): width = ui.termwidth() namelabel = 'shelve.newest' for mtime, name in listshelves(repo): sname = util.split(name)[1] if pats and sname not in pats: continue ui.write(sname, label=namelabel) namelabel = 'shelve.name' if ui.quiet: ui.write('\n') continue ui.write(' ' * (16 - len(sname))) used = 16 age = '(%s)' % templatefilters.age(util.makedate(mtime), abbrev=True) ui.write(age, label='shelve.age') ui.write(' ' * (12 - len(age))) used += 12 fp = open(name + '.patch', 'rb') try: while True: line = fp.readline() if not line: break if not line.startswith('#'): desc = line.rstrip() if ui.formatted(): desc = util.ellipsis(desc, width - used) ui.write(desc) break ui.write('\n') if not (opts['patch'] or opts['stat']): continue difflines = fp.readlines() if opts['patch']: for chunk, label in patch.difflabel(iter, difflines): ui.write(chunk, label=label) if opts['stat']: for chunk, label in patch.diffstatui(difflines, width=width, git=True): ui.write(chunk, label=label) finally: fp.close()
def __init__(self, parent=None): QDialog.__init__(self, parent) self.setWindowFlags(Qt.Window) self.setWindowIcon(qtlib.geticon("fileadd")) self.setWindowTitle(_('New Patch Branch')) def AddField(var, label, optional=False): hbox = QHBoxLayout() SP = QSizePolicy le = QLineEdit() le.setSizePolicy(SP(SP.Expanding, SP.Fixed)) if optional: cb = QCheckBox(label) le.setEnabled(False) cb.toggled.connect(le.setEnabled) hbox.addWidget(cb) setattr(self, var+'cb', cb) else: hbox.addWidget(QLabel(label)) hbox.addWidget(le) setattr(self, var+'le', le) return hbox def DialogButtons(): BB = QDialogButtonBox bb = QDialogButtonBox(BB.Ok|BB.Cancel) bb.accepted.connect(self.accept) bb.rejected.connect(self.reject) bb.button(BB.Ok).setDefault(True) bb.button(BB.Cancel).setDefault(False) self.commitButton = bb.button(BB.Ok) self.commitButton.setText(_('Commit', 'action button')) self.bb = bb return bb layout = QVBoxLayout() layout.setContentsMargins(2, 2, 2, 2) self.setLayout(layout) layout.addLayout(AddField('patchname',_('Patch name:'))) layout.addLayout(AddField('patchtext',_('Patch message:'), optional=True)) layout.addLayout(AddField('patchdate',_('Patch date:'), optional=True)) layout.addLayout(AddField('patchuser',_('Patch user:'), optional=True)) layout.addWidget(DialogButtons()) self.patchdatele.setText( hglib.tounicode(hglib.displaytime(util.makedate())))
def _obsstorecreate(orig, self, tr, prec, succs=(), flag=0, parents=None, date=None, metadata=None, ui=None): # make "prec in succs" in-marker cycle check a no-op succs = _nocontainslist(succs) # we need to resolve default date if date is None: if ui is not None: date = ui.configdate('devel', 'default-date') if date is None: date = util.makedate() # if prec is a successor of an existing marker, make default date bigger so # the old marker won't revive the predecessor accidentally. This helps tests # where date are always (0, 0) markers = self.predecessors.get(prec) if markers: maxdate = max(m[4] for m in markers) maxdate = (maxdate[0] + 1, maxdate[1]) if maxdate > date: date = maxdate return orig(self, tr, prec, succs, flag, parents, date, metadata, ui)
def maxWidthValueForColumn(self, column): if column == RevColumn: return '8' * len(str(len(self.repo))) + '+' if column == NodeColumn: return '8' * 12 + '+' if column in (LocalDateColumn, UtcDateColumn): return hglib.displaytime(util.makedate()) if column in (TagsColumn, LatestTagColumn): try: return sorted(self.repo.tags().keys(), key=lambda x: len(x))[-1][:10] except IndexError: pass if column == BranchColumn: try: return sorted(self.repo.branchmap(), key=lambda x: len(x))[-1] except IndexError: pass if column == FileColumn: return self._filename if column == ChangesColumn: return 'Changes' # Fall through for DescColumn return None
def maxWidthValueForColumn(self, column): if column == RevColumn: return '8' * len(str(len(self.repo))) + '+' if column == NodeColumn: return '8' * 12 + '+' if column in (LocalDateColumn, UtcDateColumn): return hglib.displaytime(util.makedate()) if column in (TagsColumn, LatestTagColumn): try: return sorted(self.repo.tags().keys(), key=lambda x: len(x))[-1][:10] except IndexError: pass if column == BranchColumn: try: return sorted(self.repo.branchmap(), key=lambda x: len(x))[-1] except IndexError: pass if self._hasFileColumn() and column == FileColumn: return self._filename if column == ChangesColumn: return 'Changes' # Fall through for DescColumn return None
def entries(sortcolumn="", descending=False, subdir="", **map): def sessionvars(**map): fields = [] if 'style' in req.form: style = req.form['style'][0] if style != get('web', 'style', ''): fields.append(('style', style)) separator = url[-1] == '?' and ';' or '?' for name, value in fields: yield dict(name=name, value=value, separator=separator) separator = ';' rows = [] parity = paritygen(self.stripecount) for name, path in self.repos: if not name.startswith(subdir): continue name = name[len(subdir):] u = ui.ui(parentui=self.parentui) try: u.readconfig(os.path.join(path, '.hg', 'hgrc')) except Exception, e: u.warn(_('error reading %s/.hg/hgrc: %s\n' % (path, e))) continue def get(section, name, default=None): return u.config(section, name, default, untrusted=True) if u.configbool("web", "hidden", untrusted=True): continue parts = [name] if 'PATH_INFO' in req.env: parts.insert(0, req.env['PATH_INFO'].rstrip('/')) if req.env['SCRIPT_NAME']: parts.insert(0, req.env['SCRIPT_NAME']) url = ('/'.join(parts).replace("//", "/")) + '/' # update time with local timezone try: d = (get_mtime(path), util.makedate()[1]) except OSError: continue contact = get_contact(get) description = get("web", "description", "") name = get("web", "name", name) row = dict(contact=contact or "unknown", contact_sort=contact.upper() or "unknown", name=name, name_sort=name, url=url, description=description or "unknown", description_sort=description.upper() or "unknown", lastchange=d, lastchange_sort=d[1] - d[0], sessionvars=sessionvars, archives=archivelist(u, "tip", url)) if (not sortcolumn or (sortcolumn, descending) == self.repos_sorted): # fast path for unsorted output row['parity'] = parity.next() yield row else: rows.append((row["%s_sort" % sortcolumn], row))
def patchbomb(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 -c/--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. 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. 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. If the PAGER environment variable is set, your pager will be fired up once for each patchbomb message, so you can verify everything is alright. In case email sending fails, you will find a backup of your series introductory message in ``.hg/last-email.txt``. 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 # 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. ''' _charsets = mail._charsets(ui) bundle = opts.get('bundle') date = opts.get('date') mbox = opts.get('mbox') outgoing = opts.get('outgoing') rev = opts.get('rev') # internal option used by pbranches patches = opts.get('patches') def getoutgoing(dest, revs): '''Return the revisions present locally but not in dest''' dest = ui.expandpath(dest or 'default-push', dest or 'default') dest, branches = hg.parseurl(dest) revs, checkout = hg.addbranchrevs(repo, repo, branches, revs) other = hg.peer(repo, opts, dest) ui.status(_('comparing with %s\n') % util.hidepassword(dest)) common, _anyinc, _heads = discovery.findcommonincoming(repo, other) nodes = revs and map(repo.lookup, revs) or revs o = repo.changelog.findmissing(common, heads=nodes) if not o: ui.status(_("no changes found\n")) return [] return [str(repo.changelog.rev(r)) for r in o] def getpatches(revs): for r in scmutil.revrange(repo, revs): output = cStringIO.StringIO() cmdutil.export(repo, [r], fp=output, opts=patch.diffopts(ui, opts)) yield output.getvalue().split('\n') def getbundle(dest): tmpdir = tempfile.mkdtemp(prefix='hg-email-bundle-') tmpfn = os.path.join(tmpdir, 'bundle') try: commands.bundle(ui, repo, tmpfn, dest, **opts) fp = open(tmpfn, 'rb') data = fp.read() fp.close() return data finally: try: os.unlink(tmpfn) except: pass os.rmdir(tmpdir) if not (opts.get('test') or mbox): # really sending mail.validateconfig(ui) if not (revs or rev or outgoing or bundle or patches): raise util.Abort(_('specify at least one changeset with -r or -o')) if outgoing and bundle: raise util.Abort(_("--outgoing mode always on with --bundle;" " do not re-specify --outgoing")) if outgoing or bundle: if len(revs) > 1: raise util.Abort(_("too many destinations")) dest = revs and revs[0] or None revs = [] if rev: if revs: raise util.Abort(_('use only one form to specify the revision')) revs = rev if outgoing: revs = getoutgoing(dest, rev) if bundle: opts['revs'] = revs # start if date: start_time = util.parsedate(date) else: start_time = util.makedate() def genmsgid(id): return '<%s.%s@%s>' % (id[:20], int(start_time[0]), socket.getfqdn()) def getdescription(body, sender): if opts.get('desc'): body = open(opts.get('desc')).read() else: ui.write(_('\nWrite the introductory message for the ' 'patch series.\n\n')) body = ui.edit(body, sender) # Save series description in case sendmail fails msgfile = repo.opener('last-email.txt', 'wb') msgfile.write(body) msgfile.close() return body def getpatchmsgs(patches, patchnames=None): msgs = [] ui.write(_('This patch series consists of %d patches.\n\n') % len(patches)) # build the intro message, or skip it if the user declines if introwanted(opts, len(patches)): msg = makeintro(patches) if msg: msgs.append(msg) # are we going to send more than one message? numbered = len(msgs) + len(patches) > 1 # now generate the actual patch messages name = None for i, p in enumerate(patches): if patchnames: name = patchnames[i] msg = makepatch(ui, repo, p, opts, _charsets, i + 1, len(patches), numbered, name) msgs.append(msg) return msgs def makeintro(patches): tlen = len(str(len(patches))) flag = opts.get('flag') or '' if flag: flag = ' ' + ' '.join(flag) prefix = '[PATCH %0*d of %d%s]' % (tlen, 0, len(patches), flag) subj = (opts.get('subject') or prompt(ui, 'Subject: ', rest=prefix, default='')) if not subj: return None # skip intro if the user doesn't bother subj = prefix + ' ' + subj body = '' if opts.get('diffstat'): # generate a cumulative diffstat of the whole patch series diffstat = patch.diffstat(sum(patches, [])) body = '\n' + diffstat else: diffstat = None body = getdescription(body, sender) msg = mail.mimeencode(ui, body, _charsets, opts.get('test')) msg['Subject'] = mail.headencode(ui, subj, _charsets, opts.get('test')) return (msg, subj, diffstat) def getbundlemsgs(bundle): subj = (opts.get('subject') or prompt(ui, 'Subject:', 'A bundle for your repository')) body = getdescription('', sender) msg = email.MIMEMultipart.MIMEMultipart() if body: msg.attach(mail.mimeencode(ui, body, _charsets, opts.get('test'))) datapart = email.MIMEBase.MIMEBase('application', 'x-mercurial-bundle') datapart.set_payload(bundle) bundlename = '%s.hg' % opts.get('bundlename', 'bundle') datapart.add_header('Content-Disposition', 'attachment', filename=bundlename) email.Encoders.encode_base64(datapart) msg.attach(datapart) msg['Subject'] = mail.headencode(ui, subj, _charsets, opts.get('test')) return [(msg, subj, None)] sender = (opts.get('from') or ui.config('email', 'from') or ui.config('patchbomb', 'from') or prompt(ui, 'From', ui.username())) if patches: msgs = getpatchmsgs(patches, opts.get('patchnames')) elif bundle: msgs = getbundlemsgs(getbundle(dest)) else: msgs = getpatchmsgs(list(getpatches(revs))) showaddrs = [] def getaddrs(header, ask=False, default=None): configkey = header.lower() opt = header.replace('-', '_').lower() addrs = opts.get(opt) if addrs: showaddrs.append('%s: %s' % (header, ', '.join(addrs))) return mail.addrlistencode(ui, addrs, _charsets, opts.get('test')) # not on the command line: fallback to config and then maybe ask addr = (ui.config('email', configkey) or ui.config('patchbomb', configkey) or '') if not addr and ask: addr = prompt(ui, header, default=default) if addr: showaddrs.append('%s: %s' % (header, addr)) return mail.addrlistencode(ui, [addr], _charsets, opts.get('test')) else: return default to = getaddrs('To', ask=True) if not to: # we can get here in non-interactive mode raise util.Abort(_('no recipient addresses provided')) cc = getaddrs('Cc', ask=True, default='') or [] bcc = getaddrs('Bcc') or [] replyto = getaddrs('Reply-To') if opts.get('diffstat') or opts.get('confirm'): ui.write(_('\nFinal summary:\n\n')) ui.write('From: %s\n' % sender) for addr in showaddrs: ui.write('%s\n' % addr) for m, subj, ds in msgs: ui.write('Subject: %s\n' % subj) if ds: ui.write(ds) ui.write('\n') if ui.promptchoice(_('are you sure you want to send (yn)?'), (_('&Yes'), _('&No'))): raise util.Abort(_('patchbomb canceled')) ui.write('\n') parent = opts.get('in_reply_to') or None # angle brackets may be omitted, they're not semantically part of the msg-id if parent is not None: if not parent.startswith('<'): parent = '<' + parent if not parent.endswith('>'): parent += '>' first = True sender_addr = email.Utils.parseaddr(sender)[1] sender = mail.addressencode(ui, sender, _charsets, opts.get('test')) sendmail = None for i, (m, subj, ds) in enumerate(msgs): try: m['Message-Id'] = genmsgid(m['X-Mercurial-Node']) except TypeError: m['Message-Id'] = genmsgid('patchbomb') if parent: m['In-Reply-To'] = parent m['References'] = parent if first: parent = m['Message-Id'] first = False m['User-Agent'] = 'Mercurial-patchbomb/%s' % util.version() m['Date'] = email.Utils.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('test'): ui.status(_('Displaying '), subj, ' ...\n') ui.flush() if 'PAGER' in os.environ and not ui.plain(): fp = util.popen(os.environ['PAGER'], 'w') else: fp = ui generator = email.Generator.Generator(fp, mangle_from_=False) try: generator.flatten(m, 0) fp.write('\n') except IOError, inst: if inst.errno != errno.EPIPE: raise if fp is not ui: fp.close() else: if not sendmail: sendmail = mail.connect(ui, mbox=mbox) ui.status(_('Sending '), subj, ' ...\n') ui.progress(_('sending'), i, item=subj, total=len(msgs)) if not mbox: # Exim does not remove the Bcc field del m['Bcc'] fp = cStringIO.StringIO() generator = email.Generator.Generator(fp, mangle_from_=False) generator.flatten(m, 0) sendmail(sender_addr, to + bcc + cc, fp.getvalue())
def template_pushdate(repo, ctx, templ, cache, **args): """:pushdate: Date information. When this changeset was pushed.""" pushid, who, when, nodes = _getpushinfo(repo, ctx, cache) return util.makedate(when) if when else None
def patchbomb(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. Next, (optionally) if the diffstat program is installed and -d/--diffstat is used, the result of running diffstat on the patch. Finally, the patch itself, as generated by "hg export". 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. 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. 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 # 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 Before using this command, you will need to enable email in your hgrc. See the [email] section in hgrc(5) for details. ''' _charsets = mail._charsets(ui) def outgoing(dest, revs): '''Return the revisions present locally but not in dest''' dest = ui.expandpath(dest or 'default-push', dest or 'default') dest, branches = hg.parseurl(dest) revs, checkout = hg.addbranchrevs(repo, repo, branches, revs) if revs: revs = [repo.lookup(rev) for rev in revs] other = hg.repository(cmdutil.remoteui(repo, opts), 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)[0] return [str(repo.changelog.rev(r)) for r in o] def getpatches(revs): for r in cmdutil.revrange(repo, revs): output = cStringIO.StringIO() patch.export(repo, [r], fp=output, opts=patch.diffopts(ui, opts)) yield output.getvalue().split('\n') def getbundle(dest): tmpdir = tempfile.mkdtemp(prefix='hg-email-bundle-') tmpfn = os.path.join(tmpdir, 'bundle') try: commands.bundle(ui, repo, tmpfn, dest, **opts) return open(tmpfn, 'rb').read() finally: try: os.unlink(tmpfn) except: pass os.rmdir(tmpdir) if not (opts.get('test') or opts.get('mbox')): # really sending mail.validateconfig(ui) if not (revs or opts.get('rev') or opts.get('outgoing') or opts.get('bundle') or opts.get('patches')): raise util.Abort(_('specify at least one changeset with -r or -o')) if opts.get('outgoing') and opts.get('bundle'): raise util.Abort(_("--outgoing mode always on with --bundle;" " do not re-specify --outgoing")) if opts.get('outgoing') or opts.get('bundle'): 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')) if opts.get('bundle'): opts['revs'] = revs # start if opts.get('date'): start_time = util.parsedate(opts.get('date')) else: start_time = util.makedate() def genmsgid(id): return '<%s.%s@%s>' % (id[:20], int(start_time[0]), socket.getfqdn()) def getdescription(body, sender): if opts.get('desc'): body = open(opts.get('desc')).read() else: ui.write(_('\nWrite the introductory message for the ' 'patch series.\n\n')) body = ui.edit(body, sender) return body def getpatchmsgs(patches, patchnames=None): jumbo = [] msgs = [] ui.write(_('This patch series consists of %d patches.\n\n') % len(patches)) name = None for i, p in enumerate(patches): jumbo.extend(p) if patchnames: name = patchnames[i] msg = makepatch(ui, repo, p, opts, _charsets, i + 1, len(patches), name) msgs.append(msg) if len(patches) > 1 or opts.get('intro'): tlen = len(str(len(patches))) flag = ' '.join(opts.get('flag')) if flag: subj = '[PATCH %0*d of %d %s]' % (tlen, 0, len(patches), flag) else: subj = '[PATCH %0*d of %d]' % (tlen, 0, len(patches)) subj += ' ' + (opts.get('subject') or prompt(ui, 'Subject: ', rest=subj)) body = '' if opts.get('diffstat'): d = cdiffstat(ui, _('Final summary:\n'), jumbo) if d: body = '\n' + d body = getdescription(body, sender) msg = mail.mimeencode(ui, body, _charsets, opts.get('test')) msg['Subject'] = mail.headencode(ui, subj, _charsets, opts.get('test')) msgs.insert(0, (msg, subj)) return msgs def getbundlemsgs(bundle): subj = (opts.get('subject') or prompt(ui, 'Subject:', 'A bundle for your repository')) body = getdescription('', sender) msg = email.MIMEMultipart.MIMEMultipart() if body: msg.attach(mail.mimeencode(ui, body, _charsets, opts.get('test'))) datapart = email.MIMEBase.MIMEBase('application', 'x-mercurial-bundle') datapart.set_payload(bundle) bundlename = '%s.hg' % opts.get('bundlename', 'bundle') datapart.add_header('Content-Disposition', 'attachment', filename=bundlename) email.Encoders.encode_base64(datapart) msg.attach(datapart) msg['Subject'] = mail.headencode(ui, subj, _charsets, opts.get('test')) return [(msg, subj)] sender = (opts.get('from') or ui.config('email', 'from') or ui.config('patchbomb', 'from') or prompt(ui, 'From', ui.username())) # internal option used by pbranches patches = opts.get('patches') if patches: msgs = getpatchmsgs(patches, opts.get('patchnames')) elif opts.get('bundle'): msgs = getbundlemsgs(getbundle(dest)) else: msgs = getpatchmsgs(list(getpatches(revs))) def getaddrs(opt, prpt=None, default=None): if opts.get(opt): return mail.addrlistencode(ui, opts.get(opt), _charsets, opts.get('test')) addrs = (ui.config('email', opt) or ui.config('patchbomb', opt) or '') if not addrs and prpt: addrs = prompt(ui, prpt, default) return mail.addrlistencode(ui, [addrs], _charsets, opts.get('test')) to = getaddrs('to', 'To') cc = getaddrs('cc', 'Cc', '') bcc = getaddrs('bcc') ui.write('\n') parent = opts.get('in_reply_to') or None # angle brackets may be omitted, they're not semantically part of the msg-id if parent is not None: if not parent.startswith('<'): parent = '<' + parent if not parent.endswith('>'): parent += '>' first = True sender_addr = email.Utils.parseaddr(sender)[1] sender = mail.addressencode(ui, sender, _charsets, opts.get('test')) sendmail = None for m, subj in msgs: try: m['Message-Id'] = genmsgid(m['X-Mercurial-Node']) except TypeError: m['Message-Id'] = genmsgid('patchbomb') if parent: m['In-Reply-To'] = parent m['References'] = parent if first: parent = m['Message-Id'] first = False m['User-Agent'] = 'Mercurial-patchbomb/%s' % util.version() m['Date'] = email.Utils.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 opts.get('test'): ui.status(_('Displaying '), subj, ' ...\n') ui.flush() if 'PAGER' in os.environ: fp = util.popen(os.environ['PAGER'], 'w') else: fp = ui generator = email.Generator.Generator(fp, mangle_from_=False) try: generator.flatten(m, 0) fp.write('\n') except IOError, inst: if inst.errno != errno.EPIPE: raise if fp is not ui: fp.close() elif opts.get('mbox'): ui.status(_('Writing '), subj, ' ...\n') fp = open(opts.get('mbox'), 'In-Reply-To' in m and 'ab+' or 'wb+') generator = email.Generator.Generator(fp, mangle_from_=True) # Should be time.asctime(), but Windows prints 2-characters day # of month instead of one. Make them print the same thing. date = time.strftime('%a %b %d %H:%M:%S %Y', time.localtime(start_time[0])) fp.write('From %s %s\n' % (sender_addr, date)) generator.flatten(m, 0) fp.write('\n\n') fp.close()
def entries(sortcolumn="", descending=False, subdir="", **map): def sessionvars(**map): fields = [] if 'style' in req.form: style = req.form['style'][0] if style != get('web', 'style', ''): fields.append(('style', style)) separator = url[-1] == '?' and ';' or '?' for name, value in fields: yield dict(name=name, value=value, separator=separator) separator = ';' rows = [] parity = paritygen(self.stripecount) for name, path in self.repos: if not name.startswith(subdir): continue name = name[len(subdir):] u = ui.ui(parentui=self.parentui) try: u.readconfig(os.path.join(path, '.hg', 'hgrc')) except Exception, e: u.warn(_('error reading %s/.hg/hgrc: %s\n' % (path, e))) continue def get(section, name, default=None): return u.config(section, name, default, untrusted=True) if u.configbool("web", "hidden", untrusted=True): continue parts = [name] if 'PATH_INFO' in req.env: parts.insert(0, req.env['PATH_INFO'].rstrip('/')) if req.env['SCRIPT_NAME']: parts.insert(0, req.env['SCRIPT_NAME']) url = ('/'.join(parts).replace("//", "/")) + '/' # update time with local timezone try: d = (get_mtime(path), util.makedate()[1]) except OSError: continue contact = get_contact(get) description = get("web", "description", "") name = get("web", "name", name) row = dict(contact=contact or "unknown", contact_sort=contact.upper() or "unknown", name=name, name_sort=name, url=url, description=description or "unknown", description_sort=description.upper() or "unknown", lastchange=d, lastchange_sort=d[1]-d[0], sessionvars=sessionvars, archives=archivelist(u, "tip", url)) if (not sortcolumn or (sortcolumn, descending) == self.repos_sorted): # fast path for unsorted output row['parity'] = parity.next() yield row else: rows.append((row["%s_sort" % sortcolumn], row))
def patchbomb(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 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". With --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 --bundle, changesets are selected as for --outgoing, but a single email containing a binary Mercurial bundle as an attachment will be sent. 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 # 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 Before using this command, you will need to enable email in your hgrc. See the [email] section in hgrc(5) for details. ''' def prompt(prompt, default = None, rest = ': ', empty_ok = False): if not ui.interactive: return default if default: prompt += ' [%s]' % default prompt += rest while True: r = ui.prompt(prompt, default=default) if r: return r if default is not None: return default if empty_ok: return r ui.warn(_('Please enter a valid value.\n')) def confirm(s, denial): if not prompt(s, default = 'y', rest = '? ').lower().startswith('y'): raise util.Abort(denial) def cdiffstat(summary, patchlines): s = patch.diffstat(patchlines) if s: if summary: ui.write(summary, '\n') ui.write(s, '\n') confirm(_('Does the diffstat above look okay'), _('diffstat rejected')) elif s is None: ui.warn(_('No diffstat information available.\n')) s = '' return s def makepatch(patch, idx, total): desc = [] node = None body = '' for line in patch: if line.startswith('#'): if line.startswith('# Node ID'): node = line.split()[-1] continue if line.startswith('diff -r') or line.startswith('diff --git'): break desc.append(line) if not node: raise ValueError if opts['attach']: body = ('\n'.join(desc[1:]).strip() or 'Patch subject is complete summary.') body += '\n\n\n' if opts.get('plain'): while patch and patch[0].startswith('# '): patch.pop(0) if patch: patch.pop(0) while patch and not patch[0].strip(): patch.pop(0) if opts.get('diffstat'): body += cdiffstat('\n'.join(desc), patch) + '\n\n' if opts.get('attach') or opts.get('inline'): msg = email.MIMEMultipart.MIMEMultipart() if body: msg.attach(email.MIMEText.MIMEText(body, 'plain')) p = email.MIMEText.MIMEText('\n'.join(patch), 'x-patch') binnode = bin(node) # if node is mq patch, it will have patch file name as tag patchname = [t for t in repo.nodetags(binnode) if t.endswith('.patch') or t.endswith('.diff')] if patchname: patchname = patchname[0] elif total > 1: patchname = cmdutil.make_filename(repo, '%b-%n.patch', binnode, idx, total) else: patchname = cmdutil.make_filename(repo, '%b.patch', binnode) disposition = 'inline' if opts['attach']: disposition = 'attachment' p['Content-Disposition'] = disposition + '; filename=' + patchname msg.attach(p) else: body += '\n'.join(patch) msg = email.MIMEText.MIMEText(body) subj = desc[0].strip().rstrip('. ') if total == 1: subj = '[PATCH] ' + (opts.get('subject') or subj) else: tlen = len(str(total)) subj = '[PATCH %0*d of %d] %s' % (tlen, idx, total, subj) msg['Subject'] = subj 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] def getbundle(dest): tmpdir = tempfile.mkdtemp(prefix='hg-email-bundle-') tmpfn = os.path.join(tmpdir, 'bundle') try: commands.bundle(ui, repo, tmpfn, dest, **opts) return open(tmpfn, 'rb').read() finally: try: os.unlink(tmpfn) except: pass os.rmdir(tmpdir) if not (opts.get('test') or opts.get('mbox')): # really sending mail.validateconfig(ui) if not (revs or opts.get('rev') or opts.get('outgoing') or opts.get('bundle')): raise util.Abort(_('specify at least one changeset with -r or -o')) cmdutil.setremoteconfig(ui, opts) if opts.get('outgoing') and opts.get('bundle'): raise util.Abort(_("--outgoing mode always on with --bundle;" " do not re-specify --outgoing")) if opts.get('outgoing') or opts.get('bundle'): 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')) if opts.get('bundle'): opts['revs'] = revs # start if opts.get('date'): start_time = util.parsedate(opts.get('date')) else: start_time = util.makedate() def genmsgid(id): return '<%s.%s@%s>' % (id[:20], int(start_time[0]), socket.getfqdn()) def getdescription(body, sender): if opts.get('desc'): body = open(opts.get('desc')).read() else: ui.write(_('\nWrite the introductory message for the ' 'patch series.\n\n')) body = ui.edit(body, sender) return body def getexportmsgs(): patches = [] class exportee: def __init__(self, container): self.lines = [] self.container = container self.name = 'email' def write(self, data): self.lines.append(data) def close(self): self.container.append(''.join(self.lines).split('\n')) self.lines = [] commands.export(ui, repo, *revs, **{'output': exportee(patches), 'switch_parent': False, 'text': None, 'git': opts.get('git')}) jumbo = [] msgs = [] ui.write(_('This patch series consists of %d patches.\n\n') % len(patches)) for p, i in zip(patches, xrange(len(patches))): jumbo.extend(p) msgs.append(makepatch(p, i + 1, len(patches))) if len(patches) > 1: tlen = len(str(len(patches))) subj = '[PATCH %0*d of %d] %s' % ( tlen, 0, len(patches), opts.get('subject') or prompt('Subject:', rest=' [PATCH %0*d of %d] ' % (tlen, 0, len(patches)))) body = '' if opts.get('diffstat'): d = cdiffstat(_('Final summary:\n'), jumbo) if d: body = '\n' + d body = getdescription(body, sender) msg = email.MIMEText.MIMEText(body) msg['Subject'] = subj msgs.insert(0, msg) return msgs def getbundlemsgs(bundle): subj = (opts.get('subject') or prompt('Subject:', default='A bundle for your repository')) body = getdescription('', sender) msg = email.MIMEMultipart.MIMEMultipart() if body: msg.attach(email.MIMEText.MIMEText(body, 'plain')) datapart = email.MIMEBase.MIMEBase('application', 'x-mercurial-bundle') datapart.set_payload(bundle) datapart.add_header('Content-Disposition', 'attachment', filename='bundle.hg') email.Encoders.encode_base64(datapart) msg.attach(datapart) msg['Subject'] = subj return [msg] sender = (opts.get('from') or ui.config('email', 'from') or ui.config('patchbomb', 'from') or prompt('From', ui.username())) if opts.get('bundle'): msgs = getbundlemsgs(getbundle(dest)) else: msgs = getexportmsgs() def getaddrs(opt, prpt, default = None): addrs = opts.get(opt) or (ui.config('email', opt) or ui.config('patchbomb', opt) or prompt(prpt, default = default)).split(',') return [a.strip() for a in addrs if a.strip()] to = getaddrs('to', 'To') cc = getaddrs('cc', 'Cc', '') bcc = opts.get('bcc') or (ui.config('email', 'bcc') or ui.config('patchbomb', 'bcc') or '').split(',') bcc = [a.strip() for a in bcc if a.strip()] ui.write('\n') parent = None sender_addr = email.Utils.parseaddr(sender)[1] sendmail = None for m in msgs: try: m['Message-Id'] = genmsgid(m['X-Mercurial-Node']) except TypeError: m['Message-Id'] = genmsgid('patchbomb') if parent: m['In-Reply-To'] = parent else: parent = m['Message-Id'] m['Date'] = util.datestr(start_time, "%a, %d %b %Y %H:%M:%S %1%2") 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 opts.get('test'): ui.status('Displaying ', m['Subject'], ' ...\n') ui.flush() if 'PAGER' in os.environ: fp = os.popen(os.environ['PAGER'], 'w') else: fp = ui try: fp.write(m.as_string(0)) fp.write('\n') except IOError, inst: if inst.errno != errno.EPIPE: raise if fp is not ui: fp.close() elif opts.get('mbox'): ui.status('Writing ', m['Subject'], ' ...\n') fp = open(opts.get('mbox'), 'In-Reply-To' in m and 'ab+' or 'wb+') date = util.datestr(start_time, '%a %b %d %H:%M:%S %Y') fp.write('From %s %s\n' % (sender_addr, date)) fp.write(m.as_string(0)) fp.write('\n\n') fp.close()
def rawentries(subdir="", **map): descend = self.ui.configbool('web', 'descend', True) collapse = self.ui.configbool('web', 'collapse', False) seenrepos = set() seendirs = set() for name, path in self.repos: if not name.startswith(subdir): continue name = name[len(subdir):] directory = False if '/' in name: if not descend: continue nameparts = name.split('/') rootname = nameparts[0] if not collapse: pass elif rootname in seendirs: continue elif rootname in seenrepos: pass else: directory = True name = rootname # redefine the path to refer to the directory discarded = '/'.join(nameparts[1:]) # remove name parts plus accompanying slash path = path[:-len(discarded) - 1] try: r = hg.repository(self.ui, path) directory = False except (IOError, error.RepoError): pass parts = [name] if 'PATH_INFO' in req.env: parts.insert(0, req.env['PATH_INFO'].rstrip('/')) if req.env['SCRIPT_NAME']: parts.insert(0, req.env['SCRIPT_NAME']) url = re.sub(r'/+', '/', '/'.join(parts) + '/') # show either a directory entry or a repository if directory: # get the directory's time information try: d = (get_mtime(path), util.makedate()[1]) except OSError: continue # add '/' to the name to make it obvious that # the entry is a directory, not a regular repository row = {'contact': "", 'contact_sort': "", 'name': name + '/', 'name_sort': name, 'url': url, 'description': "", 'description_sort': "", 'lastchange': d, 'lastchange_sort': d[1]-d[0], 'archives': [], 'isdirectory': True} seendirs.add(name) yield row continue u = self.ui.copy() try: u.readconfig(os.path.join(path, '.hg', 'hgrc')) except Exception as e: u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e)) continue def get(section, name, default=None): return u.config(section, name, default, untrusted=True) if u.configbool("web", "hidden", untrusted=True): continue if not self.read_allowed(u, req): continue # update time with local timezone try: r = hg.repository(self.ui, path) except IOError: u.warn(_('error accessing repository at %s\n') % path) continue except error.RepoError: u.warn(_('error accessing repository at %s\n') % path) continue try: d = (get_mtime(r.spath), util.makedate()[1]) except OSError: continue contact = get_contact(get) description = get("web", "description", "") seenrepos.add(name) name = get("web", "name", name) row = {'contact': contact or "unknown", 'contact_sort': contact.upper() or "unknown", 'name': name, 'name_sort': name, 'url': url, 'description': description or "unknown", 'description_sort': description.upper() or "unknown", 'lastchange': d, 'lastchange_sort': d[1]-d[0], 'archives': archivelist(u, "tip", url), 'isdirectory': None, } yield row
def patchbomb(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 -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. 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. If the PAGER environment variable is set, your pager will be fired up once for each patchbomb message, so you can verify everything is alright. 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 # 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. ''' _charsets = mail._charsets(ui) bundle = opts.get('bundle') date = opts.get('date') mbox = opts.get('mbox') outgoing = opts.get('outgoing') rev = opts.get('rev') # internal option used by pbranches patches = opts.get('patches') if not (opts.get('test') or mbox): # really sending mail.validateconfig(ui) if not (revs or rev or outgoing or bundle or patches): raise util.Abort(_('specify at least one changeset with -r or -o')) if outgoing and bundle: raise util.Abort( _("--outgoing mode always on with --bundle;" " do not re-specify --outgoing")) if outgoing or bundle: if len(revs) > 1: raise util.Abort(_("too many destinations")) if revs: dest = revs[0] else: dest = None revs = [] if rev: if revs: raise util.Abort(_('use only one form to specify the revision')) revs = rev if outgoing: revs = _getoutgoing(repo, dest, rev) if bundle: opts['revs'] = revs # start if date: start_time = util.parsedate(date) else: start_time = util.makedate() def genmsgid(id): return '<%s.%s@%s>' % (id[:20], int(start_time[0]), socket.getfqdn()) sender = (opts.get('from') or ui.config('email', 'from') or ui.config('patchbomb', 'from') or prompt(ui, 'From', ui.username())) if patches: msgs = _getpatchmsgs(repo, sender, patches, opts.get('patchnames'), **opts) elif bundle: bundledata = _getbundle(repo, dest, **opts) bundleopts = opts.copy() bundleopts.pop('bundle', None) # already processed msgs = _getbundlemsgs(repo, sender, bundledata, **bundleopts) else: _patches = list(_getpatches(repo, revs, **opts)) msgs = _getpatchmsgs(repo, sender, _patches, **opts) showaddrs = [] def getaddrs(header, ask=False, default=None): configkey = header.lower() opt = header.replace('-', '_').lower() addrs = opts.get(opt) if addrs: showaddrs.append('%s: %s' % (header, ', '.join(addrs))) return mail.addrlistencode(ui, addrs, _charsets, opts.get('test')) # not on the command line: fallback to config and then maybe ask addr = (ui.config('email', configkey) or ui.config('patchbomb', configkey) or '') if not addr and ask: addr = prompt(ui, header, default=default) if addr: showaddrs.append('%s: %s' % (header, addr)) return mail.addrlistencode(ui, [addr], _charsets, opts.get('test')) else: return default to = getaddrs('To', ask=True) if not to: # we can get here in non-interactive mode raise util.Abort(_('no recipient addresses provided')) cc = getaddrs('Cc', ask=True, default='') or [] bcc = getaddrs('Bcc') or [] replyto = getaddrs('Reply-To') confirm = ui.configbool('patchbomb', 'confirm') confirm |= bool(opts.get('diffstat') or opts.get('confirm')) if confirm: ui.write(_('\nFinal summary:\n\n'), label='patchbomb.finalsummary') ui.write(('From: %s\n' % sender), label='patchbomb.from') for addr in showaddrs: ui.write('%s\n' % addr, label='patchbomb.to') for m, subj, ds in msgs: ui.write(('Subject: %s\n' % subj), label='patchbomb.subject') if ds: ui.write(ds, label='patchbomb.diffstats') ui.write('\n') if ui.promptchoice( _('are you sure you want to send (yn)?' '$$ &Yes $$ &No')): raise util.Abort(_('patchbomb canceled')) ui.write('\n') parent = opts.get('in_reply_to') or None # angle brackets may be omitted, they're not semantically part of the msg-id if parent is not None: if not parent.startswith('<'): parent = '<' + parent if not parent.endswith('>'): parent += '>' sender_addr = email.Utils.parseaddr(sender)[1] sender = mail.addressencode(ui, sender, _charsets, opts.get('test')) sendmail = None firstpatch = None 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() m['Date'] = email.Utils.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('test'): ui.status(_('displaying '), subj, ' ...\n') ui.flush() if 'PAGER' in os.environ and not ui.plain(): fp = util.popen(os.environ['PAGER'], 'w') else: fp = ui generator = email.Generator.Generator(fp, mangle_from_=False) try: generator.flatten(m, 0) fp.write('\n') except IOError, inst: if inst.errno != errno.EPIPE: raise if fp is not ui: fp.close() else: if not sendmail: verifycert = ui.config('smtp', 'verifycert') if opts.get('insecure'): ui.setconfig('smtp', 'verifycert', 'loose', 'patchbomb') try: sendmail = mail.connect(ui, mbox=mbox) finally: ui.setconfig('smtp', 'verifycert', verifycert, 'patchbomb') ui.status(_('sending '), subj, ' ...\n') ui.progress(_('sending'), i, item=subj, total=len(msgs)) if not mbox: # Exim does not remove the Bcc field del m['Bcc'] fp = cStringIO.StringIO() generator = email.Generator.Generator(fp, mangle_from_=False) generator.flatten(m, 0) sendmail(sender_addr, to + bcc + cc, fp.getvalue())
def cmddebugconvertobsolete(ui, repo): """import markers from an .hg/obsolete-relations file""" cnt = 0 err = 0 l = repo.lock() some = False try: unlink = [] tr = repo.transaction('convert-obsolete') try: repo._importoldobsolete = True store = repo.obsstore ### very first format try: f = repo.opener('obsolete-relations') try: some = True for line in f: subhex, objhex = line.split() suc = bin(subhex) prec = bin(objhex) sucs = (suc==nullid) and [] or [suc] meta = { 'date': '%i %i' % util.makedate(), 'user': ui.username(), } try: store.create(tr, prec, sucs, 0, meta) cnt += 1 except ValueError: repo.ui.write_err("invalid old marker line: %s" % (line)) err += 1 finally: f.close() unlink.append(repo.join('obsolete-relations')) except IOError: pass ### second (json) format data = repo.sopener.tryread('obsoletemarkers') if data: some = True for oldmark in json.loads(data): del oldmark['id'] # dropped for now del oldmark['reason'] # unused until then oldobject = str(oldmark.pop('object')) oldsubjects = [str(s) for s in oldmark.pop('subjects', [])] LOOKUP_ERRORS = (error.RepoLookupError, error.LookupError) if len(oldobject) != 40: try: oldobject = repo[oldobject].node() except LOOKUP_ERRORS: pass if any(len(s) != 40 for s in oldsubjects): try: oldsubjects = [repo[s].node() for s in oldsubjects] except LOOKUP_ERRORS: pass oldmark['date'] = '%i %i' % tuple(oldmark['date']) meta = dict((k.encode('utf-8'), v.encode('utf-8')) for k, v in oldmark.iteritems()) try: succs = [bin(n) for n in oldsubjects] succs = [n for n in succs if n != nullid] store.create(tr, bin(oldobject), succs, 0, meta) cnt += 1 except ValueError: repo.ui.write_err("invalid marker %s -> %s\n" % (oldobject, oldsubjects)) err += 1 unlink.append(repo.sjoin('obsoletemarkers')) tr.close() for path in unlink: util.unlink(path) finally: tr.release() finally: del repo._importoldobsolete l.release() if not some: ui.warn('nothing to do\n') ui.status('%i obsolete marker converted\n' % cnt) if err: ui.write_err('%i conversion failed. check you graph!\n' % err)
def patchbomb(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. Next, (optionally) if the diffstat program is installed and -d/--diffstat is used, the result of running diffstat on the patch. Finally, the patch itself, as generated by "hg export". 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. 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. 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 # 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 Before using this command, you will need to enable email in your hgrc. See the [email] section in hgrc(5) for details. ''' _charsets = mail._charsets(ui) def outgoing(dest, revs): '''Return the revisions present locally but not in dest''' dest = ui.expandpath(dest or 'default-push', dest or 'default') dest, branches = hg.parseurl(dest) revs, checkout = hg.addbranchrevs(repo, repo, branches, revs) if revs: revs = [repo.lookup(rev) for rev in revs] other = hg.repository(cmdutil.remoteui(repo, opts), 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)[0] return [str(repo.changelog.rev(r)) for r in o] def getpatches(revs): for r in cmdutil.revrange(repo, revs): output = cStringIO.StringIO() patch.export(repo, [r], fp=output, opts=patch.diffopts(ui, opts)) yield output.getvalue().split('\n') def getbundle(dest): tmpdir = tempfile.mkdtemp(prefix='hg-email-bundle-') tmpfn = os.path.join(tmpdir, 'bundle') try: commands.bundle(ui, repo, tmpfn, dest, **opts) return open(tmpfn, 'rb').read() finally: try: os.unlink(tmpfn) except: pass os.rmdir(tmpdir) if not (opts.get('test') or opts.get('mbox')): # really sending mail.validateconfig(ui) if not (revs or opts.get('rev') or opts.get('outgoing') or opts.get('bundle') or opts.get('patches')): raise util.Abort(_('specify at least one changeset with -r or -o')) if opts.get('outgoing') and opts.get('bundle'): raise util.Abort( _("--outgoing mode always on with --bundle;" " do not re-specify --outgoing")) if opts.get('outgoing') or opts.get('bundle'): 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')) if opts.get('bundle'): opts['revs'] = revs # start if opts.get('date'): start_time = util.parsedate(opts.get('date')) else: start_time = util.makedate() def genmsgid(id): return '<%s.%s@%s>' % (id[:20], int(start_time[0]), socket.getfqdn()) def getdescription(body, sender): if opts.get('desc'): body = open(opts.get('desc')).read() else: ui.write( _('\nWrite the introductory message for the ' 'patch series.\n\n')) body = ui.edit(body, sender) return body def getpatchmsgs(patches, patchnames=None): jumbo = [] msgs = [] ui.write( _('This patch series consists of %d patches.\n\n') % len(patches)) name = None for i, p in enumerate(patches): jumbo.extend(p) if patchnames: name = patchnames[i] msg = makepatch(ui, repo, p, opts, _charsets, i + 1, len(patches), name) msgs.append(msg) if len(patches) > 1 or opts.get('intro'): tlen = len(str(len(patches))) flag = ' '.join(opts.get('flag')) if flag: subj = '[PATCH %0*d of %d %s]' % (tlen, 0, len(patches), flag) else: subj = '[PATCH %0*d of %d]' % (tlen, 0, len(patches)) subj += ' ' + (opts.get('subject') or prompt(ui, 'Subject: ', rest=subj)) body = '' if opts.get('diffstat'): d = cdiffstat(ui, _('Final summary:\n'), jumbo) if d: body = '\n' + d body = getdescription(body, sender) msg = mail.mimeencode(ui, body, _charsets, opts.get('test')) msg['Subject'] = mail.headencode(ui, subj, _charsets, opts.get('test')) msgs.insert(0, (msg, subj)) return msgs def getbundlemsgs(bundle): subj = (opts.get('subject') or prompt(ui, 'Subject:', 'A bundle for your repository')) body = getdescription('', sender) msg = email.MIMEMultipart.MIMEMultipart() if body: msg.attach(mail.mimeencode(ui, body, _charsets, opts.get('test'))) datapart = email.MIMEBase.MIMEBase('application', 'x-mercurial-bundle') datapart.set_payload(bundle) bundlename = '%s.hg' % opts.get('bundlename', 'bundle') datapart.add_header('Content-Disposition', 'attachment', filename=bundlename) email.Encoders.encode_base64(datapart) msg.attach(datapart) msg['Subject'] = mail.headencode(ui, subj, _charsets, opts.get('test')) return [(msg, subj)] sender = (opts.get('from') or ui.config('email', 'from') or ui.config('patchbomb', 'from') or prompt(ui, 'From', ui.username())) # internal option used by pbranches patches = opts.get('patches') if patches: msgs = getpatchmsgs(patches, opts.get('patchnames')) elif opts.get('bundle'): msgs = getbundlemsgs(getbundle(dest)) else: msgs = getpatchmsgs(list(getpatches(revs))) def getaddrs(opt, prpt=None, default=None): if opts.get(opt): return mail.addrlistencode(ui, opts.get(opt), _charsets, opts.get('test')) addrs = (ui.config('email', opt) or ui.config('patchbomb', opt) or '') if not addrs and prpt: addrs = prompt(ui, prpt, default) return mail.addrlistencode(ui, [addrs], _charsets, opts.get('test')) to = getaddrs('to', 'To') cc = getaddrs('cc', 'Cc', '') bcc = getaddrs('bcc') ui.write('\n') parent = opts.get('in_reply_to') or None # angle brackets may be omitted, they're not semantically part of the msg-id if parent is not None: if not parent.startswith('<'): parent = '<' + parent if not parent.endswith('>'): parent += '>' first = True sender_addr = email.Utils.parseaddr(sender)[1] sender = mail.addressencode(ui, sender, _charsets, opts.get('test')) sendmail = None for m, subj in msgs: try: m['Message-Id'] = genmsgid(m['X-Mercurial-Node']) except TypeError: m['Message-Id'] = genmsgid('patchbomb') if parent: m['In-Reply-To'] = parent m['References'] = parent if first: parent = m['Message-Id'] first = False m['User-Agent'] = 'Mercurial-patchbomb/%s' % util.version() m['Date'] = email.Utils.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 opts.get('test'): ui.status(_('Displaying '), subj, ' ...\n') ui.flush() if 'PAGER' in os.environ: fp = util.popen(os.environ['PAGER'], 'w') else: fp = ui generator = email.Generator.Generator(fp, mangle_from_=False) try: generator.flatten(m, 0) fp.write('\n') except IOError, inst: if inst.errno != errno.EPIPE: raise if fp is not ui: fp.close() elif opts.get('mbox'): ui.status(_('Writing '), subj, ' ...\n') fp = open(opts.get('mbox'), 'In-Reply-To' in m and 'ab+' or 'wb+') generator = email.Generator.Generator(fp, mangle_from_=True) # Should be time.asctime(), but Windows prints 2-characters day # of month instead of one. Make them print the same thing. date = time.strftime('%a %b %d %H:%M:%S %Y', time.localtime(start_time[0])) fp.write('From %s %s\n' % (sender_addr, date)) generator.flatten(m, 0) fp.write('\n\n') fp.close()
def synthesize(ui, repo, descpath, **opts): '''synthesize commits based on a model of an existing repository The model must have been generated by :hg:`analyze`. Commits will be generated randomly according to the probabilities described in the model. If --initfiles is set, the repository will be seeded with the given number files following the modeled repository's directory structure. When synthesizing new content, commit descriptions, and user names, words will be chosen randomly from a dictionary that is presumed to contain one word per line. Use --dict to specify the path to an alternate dictionary to use. ''' try: fp = hg.openpath(ui, descpath) except Exception as err: raise error.Abort('%s: %s' % (descpath, err[0].strerror)) desc = json.load(fp) fp.close() def cdf(l): if not l: return [], [] vals, probs = zip(*sorted(l, key=lambda x: x[1], reverse=True)) t = float(sum(probs, 0)) s, cdfs = 0, [] for v in probs: s += v cdfs.append(s / t) return vals, cdfs lineschanged = cdf(desc['lineschanged']) fileschanged = cdf(desc['fileschanged']) filesadded = cdf(desc['filesadded']) dirsadded = cdf(desc['dirsadded']) filesremoved = cdf(desc['filesremoved']) linelengths = cdf(desc['linelengths']) parents = cdf(desc['parents']) p1distance = cdf(desc['p1distance']) p2distance = cdf(desc['p2distance']) interarrival = cdf(desc['interarrival']) linesinfilesadded = cdf(desc['linesinfilesadded']) tzoffset = cdf(desc['tzoffset']) dictfile = opts.get('dict') or '/usr/share/dict/words' try: fp = open(dictfile, 'rU') except IOError as err: raise error.Abort('%s: %s' % (dictfile, err.strerror)) words = fp.read().splitlines() fp.close() initdirs = {} if desc['initdirs']: for k, v in desc['initdirs']: initdirs[k.encode('utf-8').replace('.hg', '_hg')] = v initdirs = renamedirs(initdirs, words) initdirscdf = cdf(initdirs) def pick(cdf): return cdf[0][bisect.bisect_left(cdf[1], random.random())] def pickpath(): return os.path.join(pick(initdirscdf), random.choice(words)) def makeline(minimum=0): total = max(minimum, pick(linelengths)) c, l = 0, [] while c < total: w = random.choice(words) c += len(w) + 1 l.append(w) return ' '.join(l) wlock = repo.wlock() lock = repo.lock() nevertouch = {'.hgsub', '.hgignore', '.hgtags'} progress = ui.progress _synthesizing = _('synthesizing') _files = _('initial files') _changesets = _('changesets') # Synthesize a single initial revision adding files to the repo according # to the modeled directory structure. initcount = int(opts['initfiles']) if initcount and initdirs: pctx = repo[None].parents()[0] dirs = set(pctx.dirs()) files = {} def validpath(path): # Don't pick filenames which are already directory names. if path in dirs: return False # Don't pick directories which were used as file names. while path: if path in files: return False path = os.path.dirname(path) return True for i in xrange(0, initcount): ui.progress(_synthesizing, i, unit=_files, total=initcount) path = pickpath() while not validpath(path): path = pickpath() data = '%s contents\n' % path files[path] = context.memfilectx(repo, path, data) dir = os.path.dirname(path) while dir and dir not in dirs: dirs.add(dir) dir = os.path.dirname(dir) def filectxfn(repo, memctx, path): return files[path] ui.progress(_synthesizing, None) message = 'synthesized wide repo with %d files' % (len(files), ) mc = context.memctx(repo, [pctx.node(), nullid], message, files.iterkeys(), filectxfn, ui.username(), '%d %d' % util.makedate()) initnode = mc.commit() if ui.debugflag: hexfn = hex else: hexfn = short ui.status( _('added commit %s with %d files\n') % (hexfn(initnode), len(files))) # Synthesize incremental revisions to the repository, adding repo depth. count = int(opts['count']) heads = set(map(repo.changelog.rev, repo.heads())) for i in xrange(count): progress(_synthesizing, i, unit=_changesets, total=count) node = repo.changelog.node revs = len(repo) def pickhead(heads, distance): if heads: lheads = sorted(heads) rev = revs - min(pick(distance), revs) if rev < lheads[-1]: rev = lheads[bisect.bisect_left(lheads, rev)] else: rev = lheads[-1] return rev, node(rev) return nullrev, nullid r1 = revs - min(pick(p1distance), revs) p1 = node(r1) # the number of heads will grow without bound if we use a pure # model, so artificially constrain their proliferation toomanyheads = len(heads) > random.randint(1, 20) if p2distance[0] and (pick(parents) == 2 or toomanyheads): r2, p2 = pickhead(heads.difference([r1]), p2distance) else: r2, p2 = nullrev, nullid pl = [p1, p2] pctx = repo[r1] mf = pctx.manifest() mfk = mf.keys() changes = {} if mfk: for __ in xrange(pick(fileschanged)): for __ in xrange(10): fctx = pctx.filectx(random.choice(mfk)) path = fctx.path() if not (path in nevertouch or fctx.isbinary() or 'l' in fctx.flags()): break lines = fctx.data().splitlines() add, remove = pick(lineschanged) for __ in xrange(remove): if not lines: break del lines[random.randrange(0, len(lines))] for __ in xrange(add): lines.insert(random.randint(0, len(lines)), makeline()) path = fctx.path() changes[path] = context.memfilectx(repo, path, '\n'.join(lines) + '\n') for __ in xrange(pick(filesremoved)): path = random.choice(mfk) for __ in xrange(10): path = random.choice(mfk) if path not in changes: changes[path] = None break if filesadded: dirs = list(pctx.dirs()) dirs.insert(0, '') for __ in xrange(pick(filesadded)): pathstr = '' while pathstr in dirs: path = [random.choice(dirs)] if pick(dirsadded): path.append(random.choice(words)) path.append(random.choice(words)) pathstr = '/'.join(filter(None, path)) data = '\n'.join(makeline() for __ in xrange(pick(linesinfilesadded))) + '\n' changes[pathstr] = context.memfilectx(repo, pathstr, data) def filectxfn(repo, memctx, path): return changes[path] if not changes: continue if revs: date = repo['tip'].date()[0] + pick(interarrival) else: date = time.time() - (86400 * count) # dates in mercurial must be positive, fit in 32-bit signed integers. date = min(0x7fffffff, max(0, date)) user = random.choice(words) + '@' + random.choice(words) mc = context.memctx(repo, pl, makeline(minimum=2), sorted(changes.iterkeys()), filectxfn, user, '%d %d' % (date, pick(tzoffset))) newnode = mc.commit() heads.add(repo.changelog.rev(newnode)) heads.discard(r1) heads.discard(r2) lock.release() wlock.release()
path = pickpath() data = '%s contents\n' % path files[path] = context.memfilectx(repo, path, data) dir = os.path.dirname(path) while dir and dir not in dirs: dirs.add(dir) dir = os.path.dirname(dir) def filectxfn(repo, memctx, path): return files[path] ui.progress(_synthesizing, None) message = 'synthesized wide repo with %d files' % (len(files), ) mc = context.memctx(repo, [pctx.node(), nullid], message, files.iterkeys(), filectxfn, ui.username(), '%d %d' % util.makedate()) initnode = mc.commit() if ui.debugflag: hexfn = hex else: hexfn = short ui.status( _('added commit %s with %d files\n') % (hexfn(initnode), len(files))) # Synthesize incremental revisions to the repository, adding repo depth. count = int(opts['count']) heads = set(map(repo.changelog.rev, repo.heads())) for i in xrange(count): progress(_synthesizing, i, unit=_changesets, total=count)
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. ''' _charsets = mail._charsets(ui) bundle = opts.get('bundle') date = opts.get('date') mbox = opts.get('mbox') outgoing = opts.get('outgoing') rev = opts.get('rev') bookmark = opts.get('bookmark') if not (opts.get('test') or mbox): # really sending mail.validateconfig(ui) if not (revs or rev or outgoing or bundle or bookmark): raise error.Abort( _('specify at least one changeset with -B, -r or -o')) if outgoing and bundle: raise error.Abort( _("--outgoing mode always on with --bundle;" " do not re-specify --outgoing")) if rev and bookmark: raise error.Abort(_("-r and -B are mutually exclusive")) if outgoing or bundle: if len(revs) > 1: raise error.Abort(_("too many destinations")) if revs: dest = revs[0] else: dest = None revs = [] if rev: if revs: raise error.Abort(_('use only one form to specify the revision')) revs = rev elif bookmark: if bookmark not in repo._bookmarks: raise error.Abort(_("bookmark '%s' not found") % bookmark) revs = repair.stripbmrevset(repo, bookmark) revs = scmutil.revrange(repo, revs) if outgoing: revs = _getoutgoing(repo, dest, revs) if bundle: opts['revs'] = [str(r) for r in revs] # check if revision exist on the public destination publicurl = repo.ui.config('patchbomb', 'publicurl') if publicurl: repo.ui.debug('checking that revision exist in the public repo') try: publicpeer = hg.peer(repo, {}, publicurl) except error.RepoError: repo.ui.write_err( _('unable to access public repo: %s\n') % publicurl) raise if not publicpeer.capable('known'): repo.ui.debug('skipping existence checks: public repo too old') 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 1 < len(missing): msg = _('public "%s" is missing %s and %i others') msg %= (publicurl, missing[0], len(missing) - 1) else: msg = _('public url %s is missing %s') msg %= (publicurl, missing[0]) revhint = ' '.join('-r %s' % h for h in repo.set('heads(%ld)', missing)) hint = _("use 'hg push %s %s'") % (publicurl, revhint) raise error.Abort(msg, hint=hint) # start if date: start_time = util.parsedate(date) else: start_time = util.makedate() def genmsgid(id): return '<%s.%s@%s>' % (id[:20], int(start_time[0]), socket.getfqdn()) # deprecated config: patchbomb.from sender = (opts.get('from') or ui.config('email', 'from') or ui.config('patchbomb', 'from') or prompt(ui, 'From', ui.username())) if bundle: bundledata = _getbundle(repo, dest, **opts) bundleopts = opts.copy() bundleopts.pop('bundle', None) # already processed msgs = _getbundlemsgs(repo, sender, bundledata, **bundleopts) else: msgs = _getpatchmsgs(repo, sender, revs, **opts) showaddrs = [] def getaddrs(header, ask=False, default=None): configkey = header.lower() opt = header.replace('-', '_').lower() addrs = opts.get(opt) if addrs: showaddrs.append('%s: %s' % (header, ', '.join(addrs))) return mail.addrlistencode(ui, addrs, _charsets, opts.get('test')) # not on the command line: fallback to config and then maybe ask addr = (ui.config('email', configkey) or ui.config('patchbomb', configkey)) if not addr: specified = (ui.hasconfig('email', configkey) or ui.hasconfig('patchbomb', configkey)) if not specified and ask: addr = prompt(ui, header, default=default) if addr: showaddrs.append('%s: %s' % (header, addr)) return mail.addrlistencode(ui, [addr], _charsets, opts.get('test')) elif default: return mail.addrlistencode(ui, [default], _charsets, opts.get('test')) return [] to = getaddrs('To', ask=True) if not to: # we can get here in non-interactive mode raise error.Abort(_('no recipient addresses provided')) cc = getaddrs('Cc', ask=True, default='') bcc = getaddrs('Bcc') replyto = getaddrs('Reply-To') confirm = ui.configbool('patchbomb', 'confirm') confirm |= bool(opts.get('diffstat') or opts.get('confirm')) if confirm: ui.write(_('\nFinal summary:\n\n'), label='patchbomb.finalsummary') ui.write(('From: %s\n' % sender), label='patchbomb.from') for addr in showaddrs: ui.write('%s\n' % addr, label='patchbomb.to') for m, subj, ds in msgs: ui.write(('Subject: %s\n' % subj), label='patchbomb.subject') if ds: ui.write(ds, label='patchbomb.diffstats') ui.write('\n') if ui.promptchoice( _('are you sure you want to send (yn)?' '$$ &Yes $$ &No')): raise error.Abort(_('patchbomb canceled')) ui.write('\n') parent = opts.get('in_reply_to') or None # angle brackets may be omitted, they're not semantically part of the msg-id if parent is not None: if not parent.startswith('<'): parent = '<' + parent if not parent.endswith('>'): parent += '>' sender_addr = emailmod.Utils.parseaddr(sender)[1] sender = mail.addressencode(ui, sender, _charsets, opts.get('test')) sendmail = None firstpatch = None 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() m['Date'] = emailmod.Utils.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('test'): ui.status(_('displaying '), subj, ' ...\n') ui.pager('email') generator = emailmod.Generator.Generator(ui, mangle_from_=False) try: generator.flatten(m, 0) ui.write('\n') except IOError as inst: if inst.errno != errno.EPIPE: raise else: if not sendmail: sendmail = mail.connect(ui, mbox=mbox) ui.status(_('sending '), subj, ' ...\n') ui.progress(_('sending'), i, item=subj, total=len(msgs), unit=_('emails')) if not mbox: # Exim does not remove the Bcc field del m['Bcc'] fp = stringio() generator = emailmod.Generator.Generator(fp, mangle_from_=False) generator.flatten(m, 0) sendmail(sender_addr, to + bcc + cc, fp.getvalue()) ui.progress(_('writing'), None) ui.progress(_('sending'), None)
def patchbomb(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 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". With --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 --bundle, changesets are selected as for --outgoing, but a single email containing a binary Mercurial bundle as an attachment will be sent. 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 # 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 Before using this command, you will need to enable email in your hgrc. See the [email] section in hgrc(5) for details. ''' def prompt(prompt, default=None, rest=': ', empty_ok=False): if not ui.interactive: return default if default: prompt += ' [%s]' % default prompt += rest while True: r = ui.prompt(prompt, default=default) if r: return r if default is not None: return default if empty_ok: return r ui.warn(_('Please enter a valid value.\n')) def confirm(s, denial): if not prompt(s, default='y', rest='? ').lower().startswith('y'): raise util.Abort(denial) def cdiffstat(summary, patchlines): s = patch.diffstat(patchlines) if s: if summary: ui.write(summary, '\n') ui.write(s, '\n') confirm(_('Does the diffstat above look okay'), _('diffstat rejected')) elif s is None: ui.warn(_('No diffstat information available.\n')) s = '' return s def makepatch(patch, idx, total): desc = [] node = None body = '' for line in patch: if line.startswith('#'): if line.startswith('# Node ID'): node = line.split()[-1] continue if line.startswith('diff -r') or line.startswith('diff --git'): break desc.append(line) if not node: raise ValueError if opts['attach']: body = ('\n'.join(desc[1:]).strip() or 'Patch subject is complete summary.') body += '\n\n\n' if opts.get('plain'): while patch and patch[0].startswith('# '): patch.pop(0) if patch: patch.pop(0) while patch and not patch[0].strip(): patch.pop(0) if opts.get('diffstat'): body += cdiffstat('\n'.join(desc), patch) + '\n\n' if opts.get('attach') or opts.get('inline'): msg = email.MIMEMultipart.MIMEMultipart() if body: msg.attach(email.MIMEText.MIMEText(body, 'plain')) p = email.MIMEText.MIMEText('\n'.join(patch), 'x-patch') binnode = bin(node) # if node is mq patch, it will have patch file name as tag patchname = [ t for t in repo.nodetags(binnode) if t.endswith('.patch') or t.endswith('.diff') ] if patchname: patchname = patchname[0] elif total > 1: patchname = cmdutil.make_filename(repo, '%b-%n.patch', binnode, idx, total) else: patchname = cmdutil.make_filename(repo, '%b.patch', binnode) disposition = 'inline' if opts['attach']: disposition = 'attachment' p['Content-Disposition'] = disposition + '; filename=' + patchname msg.attach(p) else: body += '\n'.join(patch) msg = email.MIMEText.MIMEText(body) subj = desc[0].strip().rstrip('. ') if total == 1: subj = '[PATCH] ' + (opts.get('subject') or subj) else: tlen = len(str(total)) subj = '[PATCH %0*d of %d] %s' % (tlen, idx, total, subj) msg['Subject'] = subj 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] def getbundle(dest): tmpdir = tempfile.mkdtemp(prefix='hg-email-bundle-') tmpfn = os.path.join(tmpdir, 'bundle') try: commands.bundle(ui, repo, tmpfn, dest, **opts) return open(tmpfn, 'rb').read() finally: try: os.unlink(tmpfn) except: pass os.rmdir(tmpdir) if not (opts.get('test') or opts.get('mbox')): # really sending mail.validateconfig(ui) if not (revs or opts.get('rev') or opts.get('outgoing') or opts.get('bundle')): raise util.Abort(_('specify at least one changeset with -r or -o')) cmdutil.setremoteconfig(ui, opts) if opts.get('outgoing') and opts.get('bundle'): raise util.Abort( _("--outgoing mode always on with --bundle;" " do not re-specify --outgoing")) if opts.get('outgoing') or opts.get('bundle'): 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')) if opts.get('bundle'): opts['revs'] = revs # start if opts.get('date'): start_time = util.parsedate(opts.get('date')) else: start_time = util.makedate() def genmsgid(id): return '<%s.%s@%s>' % (id[:20], int(start_time[0]), socket.getfqdn()) def getdescription(body, sender): if opts.get('desc'): body = open(opts.get('desc')).read() else: ui.write( _('\nWrite the introductory message for the ' 'patch series.\n\n')) body = ui.edit(body, sender) return body def getexportmsgs(): patches = [] class exportee: def __init__(self, container): self.lines = [] self.container = container self.name = 'email' def write(self, data): self.lines.append(data) def close(self): self.container.append(''.join(self.lines).split('\n')) self.lines = [] commands.export( ui, repo, *revs, **{ 'output': exportee(patches), 'switch_parent': False, 'text': None, 'git': opts.get('git') }) jumbo = [] msgs = [] ui.write( _('This patch series consists of %d patches.\n\n') % len(patches)) for p, i in zip(patches, xrange(len(patches))): jumbo.extend(p) msgs.append(makepatch(p, i + 1, len(patches))) if len(patches) > 1: tlen = len(str(len(patches))) subj = '[PATCH %0*d of %d] %s' % (tlen, 0, len(patches), opts.get('subject') or prompt( 'Subject:', rest=' [PATCH %0*d of %d] ' % (tlen, 0, len(patches)))) body = '' if opts.get('diffstat'): d = cdiffstat(_('Final summary:\n'), jumbo) if d: body = '\n' + d body = getdescription(body, sender) msg = email.MIMEText.MIMEText(body) msg['Subject'] = subj msgs.insert(0, msg) return msgs def getbundlemsgs(bundle): subj = (opts.get('subject') or prompt('Subject:', default='A bundle for your repository')) body = getdescription('', sender) msg = email.MIMEMultipart.MIMEMultipart() if body: msg.attach(email.MIMEText.MIMEText(body, 'plain')) datapart = email.MIMEBase.MIMEBase('application', 'x-mercurial-bundle') datapart.set_payload(bundle) datapart.add_header('Content-Disposition', 'attachment', filename='bundle.hg') email.Encoders.encode_base64(datapart) msg.attach(datapart) msg['Subject'] = subj return [msg] sender = (opts.get('from') or ui.config('email', 'from') or ui.config('patchbomb', 'from') or prompt('From', ui.username())) if opts.get('bundle'): msgs = getbundlemsgs(getbundle(dest)) else: msgs = getexportmsgs() def getaddrs(opt, prpt, default=None): addrs = opts.get(opt) or (ui.config('email', opt) or ui.config( 'patchbomb', opt) or prompt(prpt, default=default)).split(',') return [a.strip() for a in addrs if a.strip()] to = getaddrs('to', 'To') cc = getaddrs('cc', 'Cc', '') bcc = opts.get('bcc') or (ui.config('email', 'bcc') or ui.config( 'patchbomb', 'bcc') or '').split(',') bcc = [a.strip() for a in bcc if a.strip()] ui.write('\n') parent = None sender_addr = email.Utils.parseaddr(sender)[1] sendmail = None for m in msgs: try: m['Message-Id'] = genmsgid(m['X-Mercurial-Node']) except TypeError: m['Message-Id'] = genmsgid('patchbomb') if parent: m['In-Reply-To'] = parent else: parent = m['Message-Id'] m['Date'] = util.datestr(start_time, "%a, %d %b %Y %H:%M:%S %1%2") 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 opts.get('test'): ui.status('Displaying ', m['Subject'], ' ...\n') ui.flush() if 'PAGER' in os.environ: fp = os.popen(os.environ['PAGER'], 'w') else: fp = ui try: fp.write(m.as_string(0)) fp.write('\n') except IOError, inst: if inst.errno != errno.EPIPE: raise if fp is not ui: fp.close() elif opts.get('mbox'): ui.status('Writing ', m['Subject'], ' ...\n') fp = open(opts.get('mbox'), 'In-Reply-To' in m and 'ab+' or 'wb+') date = util.datestr(start_time, '%a %b %d %H:%M:%S %Y') fp.write('From %s %s\n' % (sender_addr, date)) fp.write(m.as_string(0)) fp.write('\n\n') fp.close()
path = pickpath() data = '%s contents\n' % path files[path] = context.memfilectx(repo, path, data) dir = os.path.dirname(path) while dir and dir not in dirs: dirs.add(dir) dir = os.path.dirname(dir) def filectxfn(repo, memctx, path): return files[path] ui.progress(_synthesizing, None) message = 'synthesized wide repo with %d files' % (len(files),) mc = context.memctx(repo, [pctx.node(), nullid], message, files.iterkeys(), filectxfn, ui.username(), '%d %d' % util.makedate()) initnode = mc.commit() if ui.debugflag: hexfn = hex else: hexfn = short ui.status(_('added commit %s with %d files\n') % (hexfn(initnode), len(files))) # Synthesize incremental revisions to the repository, adding repo depth. count = int(opts['count']) heads = set(map(repo.changelog.rev, repo.heads())) for i in xrange(count): progress(_synthesizing, i, unit=_changesets, total=count) node = repo.changelog.node
def patchbomb(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 -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. 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. If the PAGER environment variable is set, your pager will be fired up once for each patchbomb message, so you can verify everything is alright. 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 # 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. ''' _charsets = mail._charsets(ui) bundle = opts.get('bundle') date = opts.get('date') mbox = opts.get('mbox') outgoing = opts.get('outgoing') rev = opts.get('rev') # internal option used by pbranches patches = opts.get('patches') if not (opts.get('test') or mbox): # really sending mail.validateconfig(ui) if not (revs or rev or outgoing or bundle or patches): raise util.Abort(_('specify at least one changeset with -r or -o')) if outgoing and bundle: raise util.Abort(_("--outgoing mode always on with --bundle;" " do not re-specify --outgoing")) if outgoing or bundle: if len(revs) > 1: raise util.Abort(_("too many destinations")) if revs: dest = revs[0] else: dest = None revs = [] if rev: if revs: raise util.Abort(_('use only one form to specify the revision')) revs = rev revs = scmutil.revrange(repo, revs) if outgoing: revs = _getoutgoing(repo, dest, revs) if bundle: opts['revs'] = [str(r) for r in revs] # start if date: start_time = util.parsedate(date) else: start_time = util.makedate() def genmsgid(id): return '<%s.%s@%s>' % (id[:20], int(start_time[0]), socket.getfqdn()) sender = (opts.get('from') or ui.config('email', 'from') or ui.config('patchbomb', 'from') or prompt(ui, 'From', ui.username())) if patches: msgs = _getpatchmsgs(repo, sender, patches, opts.get('patchnames'), **opts) elif bundle: bundledata = _getbundle(repo, dest, **opts) bundleopts = opts.copy() bundleopts.pop('bundle', None) # already processed msgs = _getbundlemsgs(repo, sender, bundledata, **bundleopts) else: _patches = list(_getpatches(repo, revs, **opts)) msgs = _getpatchmsgs(repo, sender, _patches, **opts) showaddrs = [] def getaddrs(header, ask=False, default=None): configkey = header.lower() opt = header.replace('-', '_').lower() addrs = opts.get(opt) if addrs: showaddrs.append('%s: %s' % (header, ', '.join(addrs))) return mail.addrlistencode(ui, addrs, _charsets, opts.get('test')) # not on the command line: fallback to config and then maybe ask addr = (ui.config('email', configkey) or ui.config('patchbomb', configkey) or '') if not addr and ask: addr = prompt(ui, header, default=default) if addr: showaddrs.append('%s: %s' % (header, addr)) return mail.addrlistencode(ui, [addr], _charsets, opts.get('test')) else: return default to = getaddrs('To', ask=True) if not to: # we can get here in non-interactive mode raise util.Abort(_('no recipient addresses provided')) cc = getaddrs('Cc', ask=True, default='') or [] bcc = getaddrs('Bcc') or [] replyto = getaddrs('Reply-To') confirm = ui.configbool('patchbomb', 'confirm') confirm |= bool(opts.get('diffstat') or opts.get('confirm')) if confirm: ui.write(_('\nFinal summary:\n\n'), label='patchbomb.finalsummary') ui.write(('From: %s\n' % sender), label='patchbomb.from') for addr in showaddrs: ui.write('%s\n' % addr, label='patchbomb.to') for m, subj, ds in msgs: ui.write(('Subject: %s\n' % subj), label='patchbomb.subject') if ds: ui.write(ds, label='patchbomb.diffstats') ui.write('\n') if ui.promptchoice(_('are you sure you want to send (yn)?' '$$ &Yes $$ &No')): raise util.Abort(_('patchbomb canceled')) ui.write('\n') parent = opts.get('in_reply_to') or None # angle brackets may be omitted, they're not semantically part of the msg-id if parent is not None: if not parent.startswith('<'): parent = '<' + parent if not parent.endswith('>'): parent += '>' sender_addr = email.Utils.parseaddr(sender)[1] sender = mail.addressencode(ui, sender, _charsets, opts.get('test')) sendmail = None firstpatch = None 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() m['Date'] = email.Utils.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('test'): ui.status(_('displaying '), subj, ' ...\n') ui.flush() if 'PAGER' in os.environ and not ui.plain(): fp = util.popen(os.environ['PAGER'], 'w') else: fp = ui generator = email.Generator.Generator(fp, mangle_from_=False) try: generator.flatten(m, 0) fp.write('\n') except IOError, inst: if inst.errno != errno.EPIPE: raise if fp is not ui: fp.close() else: if not sendmail: verifycert = ui.config('smtp', 'verifycert') if opts.get('insecure'): ui.setconfig('smtp', 'verifycert', 'loose', 'patchbomb') try: sendmail = mail.connect(ui, mbox=mbox) finally: ui.setconfig('smtp', 'verifycert', verifycert, 'patchbomb') ui.status(_('sending '), subj, ' ...\n') ui.progress(_('sending'), i, item=subj, total=len(msgs)) if not mbox: # Exim does not remove the Bcc field del m['Bcc'] fp = cStringIO.StringIO() generator = email.Generator.Generator(fp, mangle_from_=False) generator.flatten(m, 0) sendmail(sender_addr, to + bcc + cc, fp.getvalue())
def cmddebugconvertobsolete(ui, repo): """import markers from an .hg/obsolete-relations file""" cnt = 0 err = 0 l = repo.lock() some = False try: unlink = [] tr = repo.transaction('convert-obsolete') try: repo._importoldobsolete = True store = repo.obsstore ### very first format try: f = repo.opener('obsolete-relations') try: some = True for line in f: subhex, objhex = line.split() suc = bin(subhex) prec = bin(objhex) sucs = (suc==nullid) and [] or [suc] meta = { 'date': '%i %i' % util.makedate(), 'user': ui.username(), } try: store.create(tr, prec, sucs, 0, metadata=meta) cnt += 1 except ValueError: repo.ui.write_err("invalid old marker line: %s" % (line)) err += 1 finally: f.close() unlink.append(repo.join('obsolete-relations')) except IOError: pass ### second (json) format data = repo.svfs.tryread('obsoletemarkers') if data: some = True for oldmark in json.loads(data): del oldmark['id'] # dropped for now del oldmark['reason'] # unused until then oldobject = str(oldmark.pop('object')) oldsubjects = [str(s) for s in oldmark.pop('subjects', [])] LOOKUP_ERRORS = (error.RepoLookupError, error.LookupError) if len(oldobject) != 40: try: oldobject = repo[oldobject].node() except LOOKUP_ERRORS: pass if any(len(s) != 40 for s in oldsubjects): try: oldsubjects = [repo[s].node() for s in oldsubjects] except LOOKUP_ERRORS: pass oldmark['date'] = '%i %i' % tuple(oldmark['date']) meta = dict((k.encode('utf-8'), v.encode('utf-8')) for k, v in oldmark.iteritems()) try: succs = [bin(n) for n in oldsubjects] succs = [n for n in succs if n != nullid] store.create(tr, bin(oldobject), succs, 0, metadata=meta) cnt += 1 except ValueError: repo.ui.write_err("invalid marker %s -> %s\n" % (oldobject, oldsubjects)) err += 1 unlink.append(repo.sjoin('obsoletemarkers')) tr.close() for path in unlink: util.unlink(path) finally: tr.release() finally: del repo._importoldobsolete l.release() if not some: ui.warn(_('nothing to do\n')) ui.status('%i obsolete marker converted\n' % cnt) if err: ui.write_err('%i conversion failed. check you graph!\n' % err)
def patchbomb(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 -c/--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. 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. 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. If the PAGER environment variable is set, your pager will be fired up once for each patchbomb message, so you can verify everything is alright. In case email sending fails, you will find a backup of your series introductory message in ``.hg/last-email.txt``. 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 # 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. ''' _charsets = mail._charsets(ui) bundle = opts.get('bundle') date = opts.get('date') mbox = opts.get('mbox') outgoing = opts.get('outgoing') rev = opts.get('rev') # internal option used by pbranches patches = opts.get('patches') def getoutgoing(dest, revs): '''Return the revisions present locally but not in dest''' dest = ui.expandpath(dest or 'default-push', dest or 'default') dest, branches = hg.parseurl(dest) revs, checkout = hg.addbranchrevs(repo, repo, branches, revs) other = hg.peer(repo, opts, dest) ui.status(_('comparing with %s\n') % util.hidepassword(dest)) common, _anyinc, _heads = discovery.findcommonincoming(repo, other) nodes = revs and map(repo.lookup, revs) or revs o = repo.changelog.findmissing(common, heads=nodes) if not o: ui.status(_("no changes found\n")) return [] return [str(repo.changelog.rev(r)) for r in o] def getpatches(revs): for r in scmutil.revrange(repo, revs): output = cStringIO.StringIO() cmdutil.export(repo, [r], fp=output, opts=patch.diffopts(ui, opts)) yield output.getvalue().split('\n') def getbundle(dest): tmpdir = tempfile.mkdtemp(prefix='hg-email-bundle-') tmpfn = os.path.join(tmpdir, 'bundle') try: commands.bundle(ui, repo, tmpfn, dest, **opts) fp = open(tmpfn, 'rb') data = fp.read() fp.close() return data finally: try: os.unlink(tmpfn) except: pass os.rmdir(tmpdir) if not (opts.get('test') or mbox): # really sending mail.validateconfig(ui) if not (revs or rev or outgoing or bundle or patches): raise util.Abort(_('specify at least one changeset with -r or -o')) if outgoing and bundle: raise util.Abort( _("--outgoing mode always on with --bundle;" " do not re-specify --outgoing")) if outgoing or bundle: if len(revs) > 1: raise util.Abort(_("too many destinations")) dest = revs and revs[0] or None revs = [] if rev: if revs: raise util.Abort(_('use only one form to specify the revision')) revs = rev if outgoing: revs = getoutgoing(dest, rev) if bundle: opts['revs'] = revs # start if date: start_time = util.parsedate(date) else: start_time = util.makedate() def genmsgid(id): return '<%s.%s@%s>' % (id[:20], int(start_time[0]), socket.getfqdn()) def getdescription(body, sender): if opts.get('desc'): body = open(opts.get('desc')).read() else: ui.write( _('\nWrite the introductory message for the ' 'patch series.\n\n')) body = ui.edit(body, sender) # Save series description in case sendmail fails msgfile = repo.opener('last-email.txt', 'wb') msgfile.write(body) msgfile.close() return body def getpatchmsgs(patches, patchnames=None): msgs = [] ui.write( _('This patch series consists of %d patches.\n\n') % len(patches)) # build the intro message, or skip it if the user declines if introwanted(opts, len(patches)): msg = makeintro(patches) if msg: msgs.append(msg) # are we going to send more than one message? numbered = len(msgs) + len(patches) > 1 # now generate the actual patch messages name = None for i, p in enumerate(patches): if patchnames: name = patchnames[i] msg = makepatch(ui, repo, p, opts, _charsets, i + 1, len(patches), numbered, name) msgs.append(msg) return msgs def makeintro(patches): tlen = len(str(len(patches))) flag = opts.get('flag') or '' if flag: flag = ' ' + ' '.join(flag) prefix = '[PATCH %0*d of %d%s]' % (tlen, 0, len(patches), flag) subj = (opts.get('subject') or prompt(ui, 'Subject: ', rest=prefix, default='')) if not subj: return None # skip intro if the user doesn't bother subj = prefix + ' ' + subj body = '' if opts.get('diffstat'): # generate a cumulative diffstat of the whole patch series diffstat = patch.diffstat(sum(patches, [])) body = '\n' + diffstat else: diffstat = None body = getdescription(body, sender) msg = mail.mimeencode(ui, body, _charsets, opts.get('test')) msg['Subject'] = mail.headencode(ui, subj, _charsets, opts.get('test')) return (msg, subj, diffstat) def getbundlemsgs(bundle): subj = (opts.get('subject') or prompt(ui, 'Subject:', 'A bundle for your repository')) body = getdescription('', sender) msg = email.MIMEMultipart.MIMEMultipart() if body: msg.attach(mail.mimeencode(ui, body, _charsets, opts.get('test'))) datapart = email.MIMEBase.MIMEBase('application', 'x-mercurial-bundle') datapart.set_payload(bundle) bundlename = '%s.hg' % opts.get('bundlename', 'bundle') datapart.add_header('Content-Disposition', 'attachment', filename=bundlename) email.Encoders.encode_base64(datapart) msg.attach(datapart) msg['Subject'] = mail.headencode(ui, subj, _charsets, opts.get('test')) return [(msg, subj, None)] sender = (opts.get('from') or ui.config('email', 'from') or ui.config('patchbomb', 'from') or prompt(ui, 'From', ui.username())) if patches: msgs = getpatchmsgs(patches, opts.get('patchnames')) elif bundle: msgs = getbundlemsgs(getbundle(dest)) else: msgs = getpatchmsgs(list(getpatches(revs))) showaddrs = [] def getaddrs(header, ask=False, default=None): configkey = header.lower() opt = header.replace('-', '_').lower() addrs = opts.get(opt) if addrs: showaddrs.append('%s: %s' % (header, ', '.join(addrs))) return mail.addrlistencode(ui, addrs, _charsets, opts.get('test')) # not on the command line: fallback to config and then maybe ask addr = (ui.config('email', configkey) or ui.config('patchbomb', configkey) or '') if not addr and ask: addr = prompt(ui, header, default=default) if addr: showaddrs.append('%s: %s' % (header, addr)) return mail.addrlistencode(ui, [addr], _charsets, opts.get('test')) else: return default to = getaddrs('To', ask=True) if not to: # we can get here in non-interactive mode raise util.Abort(_('no recipient addresses provided')) cc = getaddrs('Cc', ask=True, default='') or [] bcc = getaddrs('Bcc') or [] replyto = getaddrs('Reply-To') if opts.get('diffstat') or opts.get('confirm'): ui.write(_('\nFinal summary:\n\n')) ui.write('From: %s\n' % sender) for addr in showaddrs: ui.write('%s\n' % addr) for m, subj, ds in msgs: ui.write('Subject: %s\n' % subj) if ds: ui.write(ds) ui.write('\n') if ui.promptchoice(_('are you sure you want to send (yn)?'), (_('&Yes'), _('&No'))): raise util.Abort(_('patchbomb canceled')) ui.write('\n') parent = opts.get('in_reply_to') or None # angle brackets may be omitted, they're not semantically part of the msg-id if parent is not None: if not parent.startswith('<'): parent = '<' + parent if not parent.endswith('>'): parent += '>' first = True sender_addr = email.Utils.parseaddr(sender)[1] sender = mail.addressencode(ui, sender, _charsets, opts.get('test')) sendmail = None for i, (m, subj, ds) in enumerate(msgs): try: m['Message-Id'] = genmsgid(m['X-Mercurial-Node']) except TypeError: m['Message-Id'] = genmsgid('patchbomb') if parent: m['In-Reply-To'] = parent m['References'] = parent if first: parent = m['Message-Id'] first = False m['User-Agent'] = 'Mercurial-patchbomb/%s' % util.version() m['Date'] = email.Utils.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('test'): ui.status(_('Displaying '), subj, ' ...\n') ui.flush() if 'PAGER' in os.environ and not ui.plain(): fp = util.popen(os.environ['PAGER'], 'w') else: fp = ui generator = email.Generator.Generator(fp, mangle_from_=False) try: generator.flatten(m, 0) fp.write('\n') except IOError, inst: if inst.errno != errno.EPIPE: raise if fp is not ui: fp.close() else: if not sendmail: sendmail = mail.connect(ui, mbox=mbox) ui.status(_('Sending '), subj, ' ...\n') ui.progress(_('sending'), i, item=subj, total=len(msgs)) if not mbox: # Exim does not remove the Bcc field del m['Bcc'] fp = cStringIO.StringIO() generator = email.Generator.Generator(fp, mangle_from_=False) generator.flatten(m, 0) sendmail(sender_addr, to + bcc + cc, fp.getvalue())
def synthesize(ui, repo, descpath, **opts): '''synthesize commits based on a model of an existing repository The model must have been generated by :hg:`analyze`. Commits will be generated randomly according to the probabilities described in the model. If --initfiles is set, the repository will be seeded with the given number files following the modeled repository's directory structure. When synthesizing new content, commit descriptions, and user names, words will be chosen randomly from a dictionary that is presumed to contain one word per line. Use --dict to specify the path to an alternate dictionary to use. ''' try: fp = hg.openpath(ui, descpath) except Exception as err: raise util.Abort('%s: %s' % (descpath, err[0].strerror)) desc = json.load(fp) fp.close() def cdf(l): if not l: return [], [] vals, probs = zip(*sorted(l, key=lambda x: x[1], reverse=True)) t = float(sum(probs, 0)) s, cdfs = 0, [] for v in probs: s += v cdfs.append(s / t) return vals, cdfs lineschanged = cdf(desc['lineschanged']) fileschanged = cdf(desc['fileschanged']) filesadded = cdf(desc['filesadded']) dirsadded = cdf(desc['dirsadded']) filesremoved = cdf(desc['filesremoved']) linelengths = cdf(desc['linelengths']) parents = cdf(desc['parents']) p1distance = cdf(desc['p1distance']) p2distance = cdf(desc['p2distance']) interarrival = cdf(desc['interarrival']) linesinfilesadded = cdf(desc['linesinfilesadded']) tzoffset = cdf(desc['tzoffset']) dictfile = opts.get('dict') or '/usr/share/dict/words' try: fp = open(dictfile, 'rU') except IOError as err: raise util.Abort('%s: %s' % (dictfile, err.strerror)) words = fp.read().splitlines() fp.close() initdirs = {} if desc['initdirs']: for k, v in desc['initdirs']: initdirs[k.encode('utf-8').replace('.hg', '_hg')] = v initdirs = renamedirs(initdirs, words) initdirscdf = cdf(initdirs) def pick(cdf): return cdf[0][bisect.bisect_left(cdf[1], random.random())] def pickpath(): return os.path.join(pick(initdirscdf), random.choice(words)) def makeline(minimum=0): total = max(minimum, pick(linelengths)) c, l = 0, [] while c < total: w = random.choice(words) c += len(w) + 1 l.append(w) return ' '.join(l) wlock = repo.wlock() lock = repo.lock() nevertouch = set(('.hgsub', '.hgignore', '.hgtags')) progress = ui.progress _synthesizing = _('synthesizing') _files = _('initial files') _changesets = _('changesets') # Synthesize a single initial revision adding files to the repo according # to the modeled directory structure. initcount = int(opts['initfiles']) if initcount and initdirs: pctx = repo[None].parents()[0] dirs = set(pctx.dirs()) files = {} def validpath(path): # Don't pick filenames which are already directory names. if path in dirs: return False # Don't pick directories which were used as file names. while path: if path in files: return False path = os.path.dirname(path) return True for i in xrange(0, initcount): ui.progress(_synthesizing, i, unit=_files, total=initcount) path = pickpath() while not validpath(path): path = pickpath() data = '%s contents\n' % path files[path] = context.memfilectx(repo, path, data) dir = os.path.dirname(path) while dir and dir not in dirs: dirs.add(dir) dir = os.path.dirname(dir) def filectxfn(repo, memctx, path): return files[path] ui.progress(_synthesizing, None) message = 'synthesized wide repo with %d files' % (len(files),) mc = context.memctx(repo, [pctx.node(), nullid], message, files.iterkeys(), filectxfn, ui.username(), '%d %d' % util.makedate()) initnode = mc.commit() if ui.debugflag: hexfn = hex else: hexfn = short ui.status(_('added commit %s with %d files\n') % (hexfn(initnode), len(files))) # Synthesize incremental revisions to the repository, adding repo depth. count = int(opts['count']) heads = set(map(repo.changelog.rev, repo.heads())) for i in xrange(count): progress(_synthesizing, i, unit=_changesets, total=count) node = repo.changelog.node revs = len(repo) def pickhead(heads, distance): if heads: lheads = sorted(heads) rev = revs - min(pick(distance), revs) if rev < lheads[-1]: rev = lheads[bisect.bisect_left(lheads, rev)] else: rev = lheads[-1] return rev, node(rev) return nullrev, nullid r1 = revs - min(pick(p1distance), revs) p1 = node(r1) # the number of heads will grow without bound if we use a pure # model, so artificially constrain their proliferation toomanyheads = len(heads) > random.randint(1, 20) if p2distance[0] and (pick(parents) == 2 or toomanyheads): r2, p2 = pickhead(heads.difference([r1]), p2distance) else: r2, p2 = nullrev, nullid pl = [p1, p2] pctx = repo[r1] mf = pctx.manifest() mfk = mf.keys() changes = {} if mfk: for __ in xrange(pick(fileschanged)): for __ in xrange(10): fctx = pctx.filectx(random.choice(mfk)) path = fctx.path() if not (path in nevertouch or fctx.isbinary() or 'l' in fctx.flags()): break lines = fctx.data().splitlines() add, remove = pick(lineschanged) for __ in xrange(remove): if not lines: break del lines[random.randrange(0, len(lines))] for __ in xrange(add): lines.insert(random.randint(0, len(lines)), makeline()) path = fctx.path() changes[path] = context.memfilectx(repo, path, '\n'.join(lines) + '\n') for __ in xrange(pick(filesremoved)): path = random.choice(mfk) for __ in xrange(10): path = random.choice(mfk) if path not in changes: changes[path] = None break if filesadded: dirs = list(pctx.dirs()) dirs.insert(0, '') for __ in xrange(pick(filesadded)): pathstr = '' while pathstr in dirs: path = [random.choice(dirs)] if pick(dirsadded): path.append(random.choice(words)) path.append(random.choice(words)) pathstr = '/'.join(filter(None, path)) data = '\n'.join(makeline() for __ in xrange(pick(linesinfilesadded))) + '\n' changes[pathstr] = context.memfilectx(repo, pathstr, data) def filectxfn(repo, memctx, path): return changes[path] if not changes: continue if revs: date = repo['tip'].date()[0] + pick(interarrival) else: date = time.time() - (86400 * count) # dates in mercurial must be positive, fit in 32-bit signed integers. date = min(0x7fffffff, max(0, date)) user = random.choice(words) + '@' + random.choice(words) mc = context.memctx(repo, pl, makeline(minimum=2), sorted(changes.iterkeys()), filectxfn, user, '%d %d' % (date, pick(tzoffset))) newnode = mc.commit() heads.add(repo.changelog.rev(newnode)) heads.discard(r1) heads.discard(r2) lock.release() wlock.release()