#!/usr/bin/python # -*- coding: utf-8 -*- # # darcs_send_changes.py # --------------------- # # Copyright (C) 2007 Jeremie Dimino # Copyright (C) 2007 Nicolas Dandrimont # # This file is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This file is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Street #330, Boston, MA 02111-1307, USA. """ Envoie un mail détaillant le dernier patch appliqué à un dépot. """ import commands, os, sys, email from interactive import * try: from lxml import etree except: sys.stderr.write("darcs_send_changes requiert le paquet python-lxml.\n") sys.stderr.flush() sys.exit(1) sys.path.append("/usr/scripts/gestion") from affich_tools import cprint, encoding from unicode2ascii import unicode2ascii from email.MIMEMultipart import MIMEMultipart from email.MIMEText import MIMEText from email.Utils import formatdate from email import Encoders CONF_PATH = "_darcs/third-party/darcs-send-changes" SEEN_FILE = CONF_PATH + "/seen" ID_FILE = CONF_PATH + "/id" def to_utf8(str): """ Decode un str ou un unicode vers un str en UTF-8. """ if isinstance(str, unicode): return str.encode("UTF-8") else: try: # Si c'est une chaine brute, on commend par essayer # de la décoder comme une chaine en UTF-8 str.decode("UTF-8") return str except: try: return str.decode(encoding).encode("UTF-8") except: return str def darcs(args): """ Invoque darcs et renvoie sa sortie. """ return to_utf8(commands.getstatusoutput("env DARCS_DONT_ESCAPE_8BIT=1 darcs " + args)) def darcs_raw(args): """ Invoque darcs et renvoie sa sortie. """ return commands.getstatusoutput("env DARCS_DONT_ESCAPE_8BIT=0 darcs " + args) def get_patch_properties(hash): """ Récupère les informations a propos d'un certain patch. """ if hash: match_cmd = "--match='hash %s'" % hash else: match_cmd = "--last 1" (status, changelog) = darcs("changes %s --xml-output" % match_cmd) changelog = to_utf8(changelog) if status != 0 or changelog == "": return None prop = etree.XML(changelog) if len(prop) == 0: return None prop = prop[0] diff = darcs("diff %s --unified" % match_cmd)[1] diff = diff[diff.find('\ndiff ')+1:] cwd = os.getcwd() hostname = commands.getoutput('hostname -s') shortrepo = os.path.basename(cwd) if os.path.exists(ID_FILE): id = file(ID_FILE).read().strip() else: id = shortrepo return { 'author': prop.attrib['author'], 'hostname': hostname, 'id': id, 'repo': "%s:%s" % (hostname, cwd), 'shortrepo': shortrepo, 'date': prop.attrib['local_date'], 'hash': prop.attrib['hash'], 'name': prop.findtext('name'), 'comment': prop.findtext('comment'), 'diff': diff, 'changes': darcs("changes %s --summary" % match_cmd)[1] } def get_patches_properties(from_hash): """ Construit la liste des informations sur les patches à partir du patch from_hash. """ changelog = etree.XML(darcs("changes --from-match='hash %s' --reverse --xml-output" % from_hash)) props = [] for change in changelog[1:]: # Ca peut parraitre inutile de refaire un darcs changes, mais c'est pour palier aux problemes # d'encodages props.append(get_patch_properties(change.attrib['hash'])) return props def send_changes(smtp, recipient, patch_props): """ Formate et envoie un mail avec les modifications sur le dernier patch appliqué au dépot. recipient est une liste des destinataires du mail. Les différents templates sont des chaînes de format python qui peuvent contenir les variables suivantes: * author, date, hash, name, comment * repo: le dépot (hote + chemin) * shortrepo: basename(chemin du dépot) * changes: la sortie de darcs changes --summary * diff: la sortie de darcs diff --unified a partir du premier diff * recipient: les destinataires du mail """ from_template = "%(author)s" subject_template = "Darcs record (%(id)s): %(name)s" message_template = "%(changes)s" diff_template = "%(diff)s" # On met toutes valeurs en string, en UTF-8 for key, val in patch_props.items(): patch_props[key] = to_utf8(val) mail = MIMEMultipart() # On met le titre en ascii sinon c'est atroce pour le filtrage # automatique subject = subject_template % patch_props if subject: subject = unicode2ascii(subject.decode("UTF-8")) mail['Subject'] = subject_template % patch_props mail['From'] = from_template % patch_props mail['To'] = ", ".join(recipient) mail['Date'] = formatdate(localtime=True) mail['Mail-Followup-To'] = ", ".join(recipient) mail['Mail-Reply-To'] = from_template % patch_props mail['X-CVSinfo'] = "CRANS" mail['X-DarcsInfo'] = "CRANS-%(id)s" % patch_props texte = MIMEText(message_template % patch_props, "UTF-8") texte.set_charset("UTF-8") mail.attach(texte) patch = MIMEText(diff_template % patch_props, "UTF-8") patch.set_type('text/x-patch') patch.set_charset("UTF-8") patch.add_header('Content-Disposition', 'inline', filename='%(hash)s.diff' % patch_props) mail.attach(patch) if not mail['Content-Transfer-Encoding']: mail['Content-Transfer-Encoding'] = '8bit' mail.set_charset("UTF-8") for to in recipient: smtp.sendmail(patch_props['author'], to, mail.as_string()) def __usage(err=None): if err: cprint("%s\n" % err) cprint("""Usage: %(name)s [OPTIONS] destinataires pour en savoir plus faites « %(name)s --help » """ % { 'name': os.path.basename(sys.argv[0]) }) sys.exit(2) def __help(): cprint("""Usage: %(name)s [options] destinataires Les options disponibles sont: -h, --help affiche cette aide -s, --smtp spécifie le serveur smtp à utiliser -r, --repo spécifie l'emplacement du dépôt -f, --from hash du premier patch de la série a envoyer -i, --interactive sélectionne les patches en mode interactif Si aucun destinataire n'est donné, roots@crans.org est utilisé. """ % { 'name': os.path.basename(sys.argv[0]) }) sys.exit(0) def getnew(): '''Renvoie les possibles nouveaux patches''' if os.access(SEEN_FILE, os.R_OK): seen=set(map(lambda x: x[0:-1], file(SEEN_FILE).readlines())) else: seen=set([]) return set(filter(lambda x: x.endswith(".gz"), os.listdir("_darcs/patches"))) - seen def addseen(patches): '''Ajoute des patches aux patches déjà vus''' if not os.path.exists(CONF_PATH): p = "." for comp in CONF_PATH.split('/'): p = "%s/%s" % (p, comp) if not os.path.exists(p): os.mkdir(p) open(SEEN_FILE, "a+").writelines([patch + "\n" for patch in patches]) def select(patches): '''Sélection interactive de patches''' decided = [] ignore = [] while patches: (status, changelog) = darcs("changes --match='hash %s'" % patches[-1]) if status == 0: print print changelog c = ask("Envoyer ce patch ? (%d/%d)" % (len(decided)+1, len(patches) + len(decided)), [("YO", "envoyer ce patch"), ("n", "ne pas envoyer ce patch"), ("p", "revenir au patch précédent"), ("e", "envoyer tout les patches suivant"), ("i", "n'envoyer aucun des patches suivant"), ("v", "voir le patch"), Exit]) if c == "y": decided.append((True, patches.pop())) elif c == "n": decided.append((False, patches.pop())) elif c == "p": if decided: patches.append(decided.pop()[1]) else: cprint("Déjà au début de la liste!", "rouge") elif c == "e": while patches: decided.append((True, patches.pop())) elif c == "i": while patches: decided.append((False, patches.pop())) else: ignore.append(patches.pop()) ask("%d patches à envoyer, accepter ?" % [x[0] for x in decided].count(True), [("YO", "oui"), ("nq", "non", None, lambda: sys.exit(0))]) for d in decided: if d[0]: patches.append(d[1]) else: ignore.append(d[1]) return patches, ignore if __name__ == "__main__": import smtplib, getopt smtp = 'localhost' repo = None from_hash = None interactive = False try: options, arg = getopt.getopt(sys.argv[1:], 'hs:r:f:i', [ 'help', 'smtp=', 'repo=', 'from=', 'interactive']) except getopt.error, msg: __usage(unicode(msg)) for opt, val in options: if opt in [ '-h', '--help' ]: __help() elif opt in [ '-s', '--smtp' ]: smtp = val elif opt in [ '-r', '--repo' ]: repo = val elif opt in [ '-f', '--from' ]: from_hash = val elif opt in [ '-i', '--interactive' ]: interactive = True else: __usage("option inconnue « %s »'" % opt) recipient = arg if len(recipient) == 0: recipient = [ 'roots@crans.org' ] if repo: os.chdir(repo) else: while not os.path.exists('_darcs') and os.getcwd() != '/': os.chdir('..') if not os.path.exists('_darcs'): cprint("Pas de dépôt darcs trouvé") sys.exit(1) if not os.path.exists(SEEN_FILE): # Sélection du patch façon darcs cprint(u"C'est la première fois que vous lancez %s dans ce dépôt." % os.path.basename(sys.argv[0])) c = ask("Que voulez-vous faire ?", [("S", "sélectionner les patchs à envoyer/ignorer en mode interactif", "select"), ("e", "tout envoyer", "all"), ("i", "tout ignorer", "none"), Exit]) if c == "none": addseen(getnew()) sys.exit(0) elif c == "all": patches = getnew() elif c == "select": # On récupère la liste des patches triés patches = [x.attrib['hash'] for x in etree.XML(darcs_raw("changes --xml-output")[1])] patches, ignore = select(patches) addseen(ignore) else: patches = [] ignore = [] # Tri des patches for patch in getnew(): (status, changelog) = darcs_raw("changes --xml-output --match='hash %s'" % patch) try: prop = etree.XML(changelog)[0] patches.append((int(prop.attrib['date']), prop.attrib['hash'])) except: ignore.append(patch) addseen(ignore) patches.sort() patches = [x[1] for x in patches] if interactive: patches, _ = select(patches) if len(patches) == 0: sys.exit(0) for patch in patches: props = get_patch_properties(patch) if props: cprint("Envoi du patch %s a %s." % (props['hash'], ", ".join(recipient))) send_changes(smtplib.SMTP(smtp), recipient, props) addseen([patch])