Skip to content

Instantly share code, notes, and snippets.

@keyan
Last active January 4, 2018 18:24
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save keyan/a340e336029d51baba4ca3c24b6a516c to your computer and use it in GitHub Desktop.
Save keyan/a340e336029d51baba4ca3c24b6a516c to your computer and use it in GitHub Desktop.
A script you can run in your repo to scan for TODO messages and email the authors asking them to fix it! Can be easily adapted for other search strings.
#!/usr/bin/python
"""
This script searches the git repo looking for TODO messages, determines the
author of the line, then emails that author asking them to repair the issue.
It is intended to be run at a regular interval on a CI server or cron-job
server. By changing the `SEARCH_STRING` the script can be easily adapted to
trigger on any undesired code.
Usage:
./email_todo_authors
"""
import email.message
import smtplib
import subprocess
SENDER_EMAIL = "email@gmail.com"
SENDER_PASSWORD = "password"
SEARCH_STRING = "TODO"
SEARCH_PATH = "*"
REPO_ADDR = "https://github.com/<org>/<repo>"
MESSAGE = "Hey there! Please take a look at your TODO and make any neccessary changes to cleanup your code. Thanks!"
def find_offending_lines():
"""
Grep for the `SEARCH_STRING` in the repo.
Returns a list of raw strings returned from grep.
"""
command = "git grep -n '{}' {}".format(SEARCH_STRING, SEARCH_PATH)
res = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE)
return [line for line in res.stdout]
def build_email_line_details(lines):
"""
Processes a list of raw search results from grep.
Returns a list of lists, where each element contains details regarding a
single offense. In the form:
[email_address, filename, line_num, commit_sha]
"""
output = []
for line in lines:
line_details = line.split(':')
filename = line_details[0]
line_num = line_details[1]
commit_sha, email = get_author_email_and_commit(line_num, filename)
if email:
output.append([email, filename, line_num, commit_sha])
return output
def get_author_email_and_commit(line_num, filename):
"""
Given the filename and line number of an offense, return author/commit info.
"""
command = "git blame -L {},{} {} -e".format(line_num, line_num, filename)
res = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE)
try:
detail_str = res.stdout.next()
except StopIteration:
print "Could not find blame details for filename: {}, line: {}".format(filename, line_num)
return None
return detail_str.split(' ')[0], detail_str[detail_str.find('<') + 1:detail_str.find('>')]
def start_smtp_connection():
"""
Initialize the SMTP server connection.
"""
try:
server = smtplib.SMTP("smtp.gmail.com", 587)
server.ehlo()
server.starttls()
server.ehlo()
server.login(SENDER_EMAIL, SENDER_PASSWORD)
return server
except Exception as e:
print "Was not able to connect to email server"
raise e
def build_email(email_address, filename, line_num, commit_sha):
mail = email.message.Message()
mail["Subject"] = "ATTENTION NEEDED: TODO message in API"
mail["From"] = SENDER_EMAIL
mail.add_header("Content-Type", "text/html")
mail["To"] = email_address
msg_body = (
"<html>" +
"{}<br><br>".format(MESSAGE) +
"Offending line: {}/blob/master/{}#L{}<br>".format(REPO_ADDR, filename, line_num) +
"Commit link: {}/commit/{}<br>".format(REPO_ADDR, commit_sha) +
"</html>"
)
mail.set_payload(msg_body)
return mail
def send_emails(email_line_details):
"""
Given a list of offense details, email each author.
Note that there is no attempt to merge offenses by the same author at this
time. Exceptions during SMTP server connection will end the attempt,
however, individual exceptions during email processing will be ignored and
the email will be skipped.
"""
server = start_smtp_connection()
for line_detail in email_line_details:
email_address, filename, line_num, commit_sha = line_detail
mail = build_email(email_address, filename, line_num, commit_sha)
try:
server.sendmail(mail["From"], mail["To"], mail.as_string())
print "Successfully emailed: {}".format(email_address)
except Exception as e:
print "Failed to email {}".format(email_address)
print "Caught exception, {}".format(e)
print "Closing email server connection"
server.quit()
print "Done sending emails"
if __name__ == "__main__":
lines = find_offending_lines()
if not lines:
exit("No TODO messages found")
email_line_details = build_email_line_details(lines)
if not email_line_details:
exit("Could not determine author details for all: {} lines".format(len(lines)))
send_emails(email_line_details)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment