339 lines
12 KiB
Python
Executable file
339 lines
12 KiB
Python
Executable file
#!/usr/bin/python
|
||
# -*- coding: utf-8 -*-
|
||
#
|
||
# darcs_send_changes.py
|
||
# ---------------------
|
||
#
|
||
# Copyright (C) 2007 Jeremie Dimino <jeremie@dimino.org>
|
||
# Copyright (C) 2007 Nicolas Dandrimont <Nicolas.Dandrimont@crans.org>
|
||
#
|
||
# 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)
|
||
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) }
|
||
|
||
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 <serveur> spécifie le serveur smtp à utiliser
|
||
-r, --repo <chemin> spécifie l'emplacement du dépôt
|
||
-f, --from <hash> 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])
|