diff --git a/contrib/helpers.py b/contrib/helpers.py new file mode 100644 index 0000000..b6db648 --- /dev/null +++ b/contrib/helpers.py @@ -0,0 +1,316 @@ +""" + +Put into Public Domain, by Nicolas Sebrecht. + +Helpers for maintenance scripts. + +""" + +from os import chdir, makedirs, system, getcwd +from os.path import expanduser +import shlex +from subprocess import check_output, check_call, CalledProcessError + +import yaml + + +FS_ENCODING = 'UTF-8' +ENCODING = 'UTF-8' + +MAILING_LIST = 'offlineimap-project@lists.alioth.debian.org' +CACHEDIR = '.git/offlineimap-release' +EDITOR = 'vim' +MAILALIASES_FILE = expanduser('~/.mutt/mail_aliases') +TESTERS_FILE = "{}/testers.yml".format(CACHEDIR) +ME = "Nicolas Sebrecht " + + +def run(cmd): + return check_output(cmd, timeout=5).rstrip() + +def goTo(path): + try: + chdir(path) + return True + except FileNotFoundError: + print("Could not find the '{}' directory in '{}'...".format( + path, getcwd()) + ) + return False + + +class Author(object): + def __init__(self, name, count, email): + self.name = name + self.count = count + self.email = email + + def getName(self): + return self.name + + def getCount(self): + return self.count + + def getEmail(self): + return self.email + + +class Git(object): + @staticmethod + def getShortlog(ref): + shortlog = "" + + cmd = shlex.split("git shortlog --no-merges -n v{}..".format(ref)) + output = run(cmd).decode(ENCODING) + + for line in output.split("\n"): + if len(line) > 0: + if line[0] != " ": + line = " {}\n".format(line) + else: + line = " {}\n".format(line.lstrip()) + else: + line = "\n" + + shortlog += line + + return shortlog + + @staticmethod + def add(files): + cmd = shlex.split("git add -- {}".format(files)) + return run(cmd).decode(ENCODING) + + @staticmethod + def commit(msg): + cmd = shlex.split("git commit -s -m 'v{}'".format(msg)) + return run(cmd).decode(ENCODING) + + @staticmethod + def tag(version): + cmd = shlex.split("git tag -a 'v{}' -m 'v{}'".format(version, version)) + return run(cmd).decode(ENCODING) + + @staticmethod + def stash(msg): + cmd = shlex.split("git stash create '{}'".format(msg)) + return run(cmd).decode(ENCODING) + + @staticmethod + def mergeFF(ref): + cmd = shlex.split("git merge --ff '{}'".format(ref)) + return run(cmd).decode(ENCODING) + + @staticmethod + def getDiffstat(ref): + cmd = shlex.split("git diff --stat v{}..".format(ref)) + return run(cmd).decode(ENCODING) + + @staticmethod + def isClean(): + try: + check_call(shlex.split("git diff --quiet")) + check_call(shlex.split("git diff --cached --quiet")) + except CalledProcessError: + return False + return True + + @staticmethod + def buildMessageId(): + cmd = shlex.split( + "git log HEAD~1.. --oneline --pretty='%H.%t.upcoming.%ce'") + return run(cmd).decode(ENCODING) + + @staticmethod + def resetKeep(ref): + return run(shlex.split("git reset --keep {}".format(ref))) + + @staticmethod + def getRef(ref): + return run(shlex.split("git rev-parse {}".format(ref))).rstrip() + + @staticmethod + def rmTag(tag): + return run(shlex.split("git tag -d {}".format(tag))) + + @staticmethod + def checkout(ref, create=False): + if create: + create = "-b" + else: + create = "" + + cmd = shlex.split("git checkout {} {}".format(create, ref)) + run(cmd) + head = shlex.split("git rev-parse HEAD") + revparseRef = shlex.split("git rev-parse {}".format(ref)) + if run(head) != run(revparseRef): + raise Exception("checkout to '{}' did not work".format(ref)) + + @staticmethod + def makeCacheDir(): + try: + makedirs(CACHEDIR) + except FileExistsError: + pass + + @staticmethod + def getLocalUser(): + cmd = shlex.split("git config --get user.name") + name = run(cmd).decode(ENCODING) + cmd = shlex.split("git config --get user.email") + email = run(cmd).decode(ENCODING) + return name, email + + @staticmethod + def buildDate(): + cmd = shlex.split("git log HEAD~1.. --oneline --pretty='%cD'") + return run(cmd).decode(ENCODING) + + @staticmethod + def getAuthorsList(sinceRef): + authors = [] + + cmd = shlex.split("git shortlog --no-merges -sne v{}..".format(sinceRef)) + output = run(cmd).decode(ENCODING) + + for line in output.split("\n"): + count, full = line.strip().split("\t") + full = full.split(' ') + name = ' '.join(full[:-1]) + email = full[-1] + + authors.append(Author(name, count, email)) + + return authors + + @staticmethod + def getCommitsList(sinceRef): + cmd = shlex.split( + "git log --no-merges --format='- %h %s. [%aN]' v{}..".format(sinceRef) + ) + return run(cmd).decode(ENCODING) + + @staticmethod + def chdirToRepositoryTopLevel(): + cmd = shlex.split("git rev-parse --show-toplevel") + topLevel = run(cmd) + + chdir(topLevel) + + +class OfflineimapInfo(object): + def getVersion(self): + cmd = shlex.split("./offlineimap.py --version") + return run(cmd).rstrip().decode(FS_ENCODING) + + def editInit(self): + return system("{} ./offlineimap/__init__.py".format(EDITOR)) + + + +class User(object): + """Interact with the user.""" + + @staticmethod + def request(msg, prompt='--> '): + print(msg) + return input(prompt) + + @staticmethod + def pause(msg=False): + return User.request(msg, prompt="Press Enter to continue..") + + @staticmethod + def yesNo(msg, defaultToYes=False, prompt='--> '): + endMsg = " [y/N]: No" + if defaultToYes: + endMsg = " [Y/n]: Yes" + msg += endMsg + answer = User.request(msg, prompt).lower() + if answer in ['y', 'yes']: + return True + if defaultToYes and answer not in ['n', 'no']: + return True + return False + + +class Tester(object): + def __init__(self, name, email, feedback): + self.name = name + self.email = email + self.feedback = feedback + + def __str__(self): + return "{} {}".format(self.name, self.email) + + def getName(self): + return self.name + + def getEmail(self): + return self.email + + def getFeedback(self): + return self.feedback + + def setFeedback(self, feedback): + assert feedback in [True, False, None] + self.feedback = feedback + + def switchFeedback(self): + self.feedback = not self.feedback + + +class Testers(object): + def __init__(self): + self.testers = [] + self._read() + + def _read(self): + with open(TESTERS_FILE, 'r') as fd: + testers = yaml.load(fd) + for tester in testers: + name = tester['name'] + email = tester['email'] + feedback = tester['feedback'] + self.testers.append(Tester(name, email, feedback)) + + @staticmethod + def listTestersInTeam(): + """Returns a list of emails extracted from my mailaliases file.""" + + cmd = shlex.split("grep offlineimap-testers {}".format(MAILALIASES_FILE)) + output = run(cmd).decode(ENCODING) + emails = output.lstrip("alias offlineimap-testers ").split(', ') + return emails + + def add(self, name, email, feedback=None): + self.testers.append(Tester(name, email, feedback)) + + def remove(self, tester): + self.testers.remove(tester) + + def get(self): + return self.testers + + def getList(self): + testersList = "" + for tester in self.testers: + testersList += "- {}\n".format(tester.getName()) + return testersList + + def reset(self): + for tester in self.testers: + tester.setFeedback(None) + + def write(self): + testers = [] + for tester in self.testers: + testers.append({ + 'name': tester.getName(), + 'email': tester.getEmail(), + 'feedback': tester.getFeedback(), + }) + with open(TESTERS_FILE, 'w') as fd: + fd.write(yaml.dump(testers)) + diff --git a/contrib/release.py b/contrib/release.py new file mode 100755 index 0000000..752df21 --- /dev/null +++ b/contrib/release.py @@ -0,0 +1,447 @@ +#!/usr/bin/python3 + +""" + +Put into Public Domain, by Nicolas Sebrecht. + +Make a new release. + +""" + +#TODO: announce: cc list on announce includes all testers +#TODO: announce: remove empty sections +#TODO: websitedoc up +#TODO: website branch not including all changes! + + +from os import system, path, rename +from datetime import datetime +from subprocess import check_call +import shlex +import time +from email import utils + +from helpers import ( + MAILING_LIST, CACHEDIR, EDITOR, Git, OfflineimapInfo, Testers, User, run, goTo +) + + +__VERSION__ = "0.1" + +SPHINXBUILD = 'sphinx-build' +DOCSDIR = 'docs' +CHANGELOG_MAGIC = '{:toc}' +WEBSITE_LATEST = "website/_data/latest.yml" + +CHANGELOG_EXCERPT = "{}/changelog.excerpt.md".format(CACHEDIR) +CHANGELOG_EXCERPT_OLD = "{}.old".format(CHANGELOG_EXCERPT) +CHANGELOG = "Changelog.md" +ANNOUNCE_FILE = "{}/announce.txt".format(CACHEDIR) + +WEBSITE_LATEST_SKEL = """# DO NOT EDIT MANUALLY: it is generated by the release script. +stable: v{stable} +""" + +CHANGELOG_SKEL = """ +### OfflineIMAP v{version} ({date}) + +#### Notes + + +This release was tested by: + +{testersList} + +#### Authors + +{authorsList} + +#### Features + + +#### Fixes + + +#### Changes + + + +{commitsList} + +""" + +END_MESSAGE = """ +Release is ready! +Make your checks and push the changes for both offlineimap and the website. +Announce template stands in '{announce}'. +Command samples to do manually: + +- git push master next {new_version} +- python setup.py sdist && twine upload dist/* && rm -rf dist MANIFEST +- cd website +- git checkout master +- git merge {website_branch} +- git push master +- cd .. +- git send-email {announce} + +...and write a Twitter message. +Have fun! ,-) +""" + + +class State(object): + def __init__(self): + self.master = None + self.next = None + self.website = None + self.tag = None + + def setTag(self, tag): + self.tag = tag + + def save(self): + self.master = Git.getRef('master') + self.next = Git.getRef('next') + + def saveWebsite(self): + Git.chdirToRepositoryTopLevel() + goTo('website') + self.website = Git.getRef('master') + goTo('..') + + def restore(self): + Git.chdirToRepositoryTopLevel() + Git.checkout('-f') + Git.checkout('master') + Git.resetKeep(self.master) + Git.checkout('next') + Git.resetKeep(self.next) + + if self.tag is not None: + Git.rmTag(self.tag) + + if self.website is not None: + if goTo('website'): + Git.checkout(self.website) + goTo('..') + + +class Changelog(object): + def __init__(self): + self.shouldUsePrevious = False + + def edit(self): + return system("{} {}".format(EDITOR, CHANGELOG_EXCERPT)) + + def update(self): + # Insert excerpt to CHANGELOG. + system("sed -i -e '/{}/ r {}' '{}'".format( + CHANGELOG_MAGIC, CHANGELOG_EXCERPT, CHANGELOG + ) + ) + # Remove trailing whitespaces. + system("sed -i -r -e 's, +$,,' '{}'".format(CHANGELOG)) + + def savePrevious(self): + rename(CHANGELOG_EXCERPT, CHANGELOG_EXCERPT_OLD) + + def isPrevious(self): + if path.isfile(CHANGELOG_EXCERPT_OLD): + return True + return False + + def showPrevious(self): + return run(shlex.split("tail {}".format(CHANGELOG_EXCERPT_OLD))) + + def usePrevious(self): + rename(CHANGELOG_EXCERPT_OLD, CHANGELOG_EXCERPT) + self.shouldUsePrevious = True + + def usingPrevious(self): + return self.shouldUsePrevious + + def writeExcerpt(self, version, date, + testersList, authorsList, commitsList): + + with open(CHANGELOG_EXCERPT, 'w+') as fd: + fd.write(CHANGELOG_SKEL.format( + version=version, + date=date, + testersList=testersList, + authorsList=authorsList, + commitsList=commitsList, + )) + + def getSectionsContent(self): + dict_Content = {} + + with open(CHANGELOG_EXCERPT, 'r') as fd: + currentSection = None + for line in fd: + line = line.rstrip() + if line == "#### Notes": + currentSection = 'Notes' + dict_Content['Notes'] = "" + continue # Don't keep this title. + elif line == "#### Authors": + currentSection = 'Authors' + dict_Content['Authors'] = "" + continue # Don't keep this title. + elif line == "#### Features": + currentSection = 'Features' + dict_Content['Features'] = "" + continue # Don't keep this title. + elif line == "#### Fixes": + currentSection = 'Fixes' + dict_Content['Fixes'] = "" + continue # Don't keep this title. + elif line == "#### Changes": + currentSection = 'Changes' + dict_Content['Changes'] = "" + continue # Don't keep this title. + elif line == "-- ": + break # Stop extraction. + + if currentSection is not None: + dict_Content[currentSection] += "{}\n".format(line) + + #TODO: cleanup empty sections. + return dict_Content + + +class Announce(object): + def __init__(self, version): + self.fd = open(ANNOUNCE_FILE, 'w') + self.version = version + + def setHeaders(self, messageId, date): + self.fd.write("Message-Id: <{}>\n".format(messageId)) + self.fd.write("Date: {}\n".format(date)) + self.fd.write("From: Nicolas Sebrecht \n") + self.fd.write("To: {}\n".format(MAILING_LIST)) + self.fd.write( + "Subject: [ANNOUNCE] OfflineIMAP v{} released\n".format(self.version)) + self.fd.write("\n") + + self.fd.write(""" +OfflineIMAP v{version} is out. + +Downloads: + http://github.com/OfflineIMAP/offlineimap/archive/v{version}.tar.gz + http://github.com/OfflineIMAP/offlineimap/archive/v{version}.zip + +Pip: + wget "https://raw.githubusercontent.com/OfflineIMAP/offlineimap/v{version}/requirements.txt" -O requirements.txt + pip install -r ./requirements.txt --user git+https://github.com/OfflineIMAP/offlineimap.git@v{version} + +""".format(version=self.version) + ) + + def setContent(self, dict_Content): + self.fd.write("\n") + for section in ['Notes', 'Authors', 'Features', 'Fixes', 'Changes']: + if section in dict_Content: + if section != "Notes": + self.fd.write("# {}\n".format(section)) + self.fd.write(dict_Content[section]) + self.fd.write("\n") + # Signature. + self.fd.write("-- \n") + self.fd.write("Nicolas Sebrecht\n") + + def close(self): + self.fd.close() + + +class Website(object): + def updateAPI(self): + req = "update API of the website? (requires {}) [Y/n]".format(SPHINXBUILD) + if not User.yesNo(req, defaultToYes=True): + return False + + if check_call(shlex.split("{} --version".format(SPHINXBUILD))) != 0: + print(""" +Oops! you don't have {} installed?" +Cannot update the webite documentation..." +You should install it and manually run:" + $ cd {}" + $ make websitedoc" +Then, commit and push changes of the website.""".format(SPHINXBUILD, DOCSDIR)) + User.pause() + return False + + Git.chdirToRepositoryTopLevel() + if not goTo('website'): + User.pause() + return False + if not Git.isClean: + print("There is WIP in the website repository: stashing") + Git.stash('WIP during offlineimap API import') + + goTo('..') + return True + + def buildLatest(self, version): + Git.chdirToRepositoryTopLevel() + with open(WEBSITE_LATEST, 'w') as fd: + fd.write(WEBSITE_LATEST_SKEL.format(stable=version)) + + def exportDocs(self, version): + branchName = "import-v{}".format(version) + + if not goTo(DOCSDIR): + User.pause() + return + + if check_call(shlex.split("make websitedoc")) != 0: + print("error while calling 'make websitedoc'") + exit(3) + + Git.chdirToRepositoryTopLevel() + if not goTo("website"): + User.pause() + return + + Git.checkout(branchName, create=True) + Git.add('_doc/versions') + Git.commit("update for offlineimap v{}".format(version)) + + User.pause( + "website: branch '{}' is ready for a merge in master!".format( + branchName + ) + ) + goTo('..') + return branchName + + +class Release(object): + def __init__(self): + self.state = State() + self.offlineimapInfo = OfflineimapInfo() + self.testers = Testers() + self.changelog = Changelog() + self.websiteBranch = "NO_BRANCH_NAME_ERROR" + + + def getVersion(self): + return self.offlineimapInfo.getVersion() + + def prepare(self): + if not Git.isClean(): + print("The git repository is not clean; aborting") + exit(1) + Git.makeCacheDir() + Git.checkout('next') + + def requestVersion(self, currentVersion): + User.request("going to make a new release after {}".format(currentVersion)) + + def updateVersion(self): + self.offlineimapInfo.editInit() + + def checkVersions(self, current, new): + if new == current: + print("version was not changed; stopping.") + exit(1) + + def updateChangelog(self): + if self.changelog.isPrevious(): + self.changelog.showPrevious() + if User.yesNo("A previous Changelog excerpt was found. Use it?"): + self.changelog.usePrevious() + + if not self.changelog.usingPrevious(): + date = datetime.now().strftime('%Y-%m-%d') + testersList = self.testers.getList() + authorsList = "" + authors = Git.getAuthorsList(currentVersion) + for author in authors: + authorsList += "- {} ({})\n".format( + author.getName(), author.getCount() + ) + commitsList = Git.getCommitsList(currentVersion) + date = datetime.now().strftime('%Y-%m-%d') + self.changelog.writeExcerpt( + newVersion, date, testersList, authorsList, commitsList + ) + + self.changelog.edit() + self.changelog.update() + + def writeAnnounce(self): + announce = Announce(newVersion) + + messageId = utils.make_msgid('release.py', 'laposte.net') + nowtuple = datetime.now().timetuple() + nowtimestamp = time.mktime(nowtuple) + date = utils.formatdate(nowtimestamp) + + announce.setHeaders(messageId, date) + announce.setContent(self.changelog.getSectionsContent()) + announce.close() + + def make(self): + Git.add('offlineimap/__init__.py') + Git.add('Changelog.md') + commitMsg = "{}\n".format(newVersion) + for tester in self.testers.get(): + commitMsg = "{}\nTested-by: {} {}".format( + commitMsg, tester.getName(), tester.getEmail() + ) + Git.commit(commitMsg) + self.state.setTag(newVersion) + Git.tag(newVersion) + Git.checkout('master') + Git.mergeFF('next') + Git.checkout('next') + + def updateWebsite(self, newVersion): + self.state.saveWebsite() + website = Website() + website.buildLatest(newVersion) + if website.updateAPI(): + self.websiteBranch = website.exportDocs(newVersion) + + def getWebsiteBranch(self): + return self.websiteBranch + + def after(self): + for protectedRun in [self.testers.reset, self.changelog.savePrevious]: + try: + protectedRun() + except Exception as e: + print(e) + + def restore(self): + self.state.restore() + + +if __name__ == '__main__': + release = Release() + Git.chdirToRepositoryTopLevel() + + try: + release.prepare() + currentVersion = release.getVersion() + + release.requestVersion(currentVersion) + release.updateVersion() + newVersion = release.getVersion() + + release.checkVersions(currentVersion, newVersion) + release.updateChangelog() + + release.writeAnnounce() + User.pause() + + release.make() + release.updateWebsite(newVersion) + release.after() + + websiteBranch = release.getWebsiteBranch() + print(END_MESSAGE.format(ANNOUNCE_FILE, newVersion, websiteBranch)) + except Exception as e: + release.restore() + raise diff --git a/contrib/tested-by.py b/contrib/tested-by.py new file mode 100755 index 0000000..60f8362 --- /dev/null +++ b/contrib/tested-by.py @@ -0,0 +1,143 @@ +#!/usr/bin/python3 + +""" + +Put into Public Domain, by Nicolas Sebrecht. + +Manage the feedbacks of the testers for the release notes. + +""" + +from os import system +import argparse + +from helpers import CACHEDIR, EDITOR, Testers, User, Git + + +class App(object): + def __init__(self): + self.args = None + self.testers = Testers() + self.feedbacks = None + + + def _getTestersByFeedback(self): + if self.feedbacks is not None: + return self.feedbacks + + feedbackOk = [] + feedbackNo = [] + + for tester in self.testers.get(): + if tester.getFeedback() is True: + feedbackOk.append(tester) + else: + feedbackNo.append(tester) + + for array in [feedbackOk, feedbackNo]: + array.sort(key=lambda t: t.getName()) + + self.feedbacks = feedbackOk + feedbackNo + + def parseArgs(self): + parser = argparse.ArgumentParser(description='Manage the feedbacks.') + + parser.add_argument('--add', '-a', dest='add_tester', + help='Add tester') + parser.add_argument('--delete', '-d', dest='delete_tester', + type=int, + help='Delete tester NUMBER') + parser.add_argument('--list', '-l', dest='list_all_testers', + action='store_true', + help='List the testers') + parser.add_argument('--switchFeedback', '-s', dest='switch_feedback', + action='store_true', + help='Switch the feedback of a tester') + + self.args = parser.parse_args() + + def run(self): + if self.args.list_all_testers is True: + self.listTesters() + if self.args.switch_feedback is True: + self.switchFeedback() + elif self.args.add_tester: + self.addTester(self.args.add_tester) + elif type(self.args.delete_tester) == int: + self.deleteTester(self.args.delete_tester) + + def addTester(self, strTester): + try: + splitted = strTester.split('<') + name = splitted[0].strip() + email = "<{}".format(splitted[1]).strip() + except Exception as e: + print(e) + print("expected format is: 'Firstname Lastname '") + exit(2) + self.testers.add(name, email) + self.testers.write() + + def deleteTester(self, number): + self.listTesters() + removed = self.feedbacks.pop(number) + self.testers.remove(removed) + + print("New list:") + self.feedbacks = None + self.listTesters() + print("Removed: {}".format(removed)) + ans = User.request("Save on disk? (s/Q)").lower() + if ans in ['s']: + self.testers.write() + + + def listTesters(self): + self._getTestersByFeedback() + + count = 0 + for tester in self.feedbacks: + feedback = "ok" + if tester.getFeedback() is not True: + feedback = "no" + print("{:02d} - {} {}: {}".format( + count, tester.getName(), tester.getEmail(), feedback + ) + ) + count += 1 + + def switchFeedback(self): + self._getTestersByFeedback() + msg = "Switch tester: [/s/q]" + + self.listTesters() + number = User.request(msg) + while number.lower() not in ['s', 'save', 'q', 'quit']: + if number == '': + continue + try: + number = int(number) + self.feedbacks[number].switchFeedback() + except (ValueError, IndexError) as e: + print(e) + exit(1) + finally: + self.listTesters() + number = User.request(msg) + if number in ['s', 'save']: + self.testers.write() + self.listTesters() + + def reset(self): + self.testers.reset() + self.testers.write() + + #def updateMailaliases(self): + +if __name__ == '__main__': + Git.chdirToRepositoryTopLevel() + ccList = Testers.listTestersInTeam() + + app = App() + app.parseArgs() + app.run() diff --git a/contrib/upcoming.py b/contrib/upcoming.py index 2f5423d..20a29c6 100755 --- a/contrib/upcoming.py +++ b/contrib/upcoming.py @@ -8,22 +8,15 @@ Produce the "upcoming release" notes. """ -from os import chdir, system -from os.path import expanduser -import shlex -from subprocess import check_output +from os import system + +from helpers import ( + MAILING_LIST, CACHEDIR, EDITOR, getTesters, Git, OfflineimapInfo, User +) -__VERSION__ = "0.1" -FS_ENCODING = 'UTF-8' -ENCODING = 'UTF-8' - -MAILING_LIST = 'offlineimap-project@lists.alioth.debian.org' -CACHEDIR = '.git/offlineimap-release' UPCOMING_FILE = "{}/upcoming.txt".format(CACHEDIR) -EDITOR = 'vim' -MAILALIASES_FILE = expanduser('~/.mutt/mail_aliases') UPCOMING_HEADER = """ Message-Id: <{messageId}> @@ -40,140 +33,22 @@ I think it's time for a new release. I aim to make the new release in one week, approximately. If you'd like more time, please let me know. ,-) +Please, send me a mail to confirm it works for you. This will be written in the +release notes and the git logs. + # Authors """ -def run(cmd): - return check_output(cmd, timeout=5).rstrip() - - -def getTesters(): - """Returns a list of emails extracted from my mailaliases file.""" - - cmd = shlex.split("grep offlineimap-testers {}".format(MAILALIASES_FILE)) - output = run(cmd).decode(ENCODING) - emails = output.lstrip("alias offlineimap-testers ").split(', ') - return emails - - -class Author(object): - def __init__(self, name, count, email): - self.name = name - self.count = count - self.email = email - - def getName(self): - return self.name - - def getCount(self): - return self.count - - def getEmail(self): - return self.email - - -class Git(object): - @staticmethod - def getShortlog(ref): - shortlog = "" - - cmd = shlex.split("git shortlog --no-merges -n v{}..".format(ref)) - output = run(cmd).decode(ENCODING) - - for line in output.split("\n"): - if len(line) > 0: - if line[0] != " ": - line = " {}\n".format(line) - else: - line = " {}\n".format(line.lstrip()) - else: - line = "\n" - - shortlog += line - - return shortlog - - @staticmethod - def getDiffstat(ref): - cmd = shlex.split("git diff --stat v{}..".format(ref)) - return run(cmd).decode(ENCODING) - - @staticmethod - def buildMessageId(): - cmd = shlex.split( - "git log HEAD~1.. --oneline --pretty='%H.%t.upcoming.%ce'") - return run(cmd).decode(ENCODING) - - @staticmethod - def getLocalUser(): - cmd = shlex.split("git config --get user.name") - name = run(cmd).decode(ENCODING) - cmd = shlex.split("git config --get user.email") - email = run(cmd).decode(ENCODING) - return name, email - - @staticmethod - def buildDate(): - cmd = shlex.split("git log HEAD~1.. --oneline --pretty='%cD'") - return run(cmd).decode(ENCODING) - - @staticmethod - def getAuthors(ref): - authors = [] - - cmd = shlex.split("git shortlog --no-merges -sne v{}..".format(ref)) - output = run(cmd).decode(ENCODING) - - for line in output.split("\n"): - count, full = line.strip().split("\t") - full = full.split(' ') - name = ' '.join(full[:-1]) - email = full[-1] - - authors.append(Author(name, count, email)) - - return authors - - @staticmethod - def chdirToRepositoryTopLevel(): - cmd = shlex.split("git rev-parse --show-toplevel") - topLevel = run(cmd) - - chdir(topLevel) - - -class OfflineimapInfo(object): - def __init__(self): - self.version = None - - def getCurrentVersion(self): - if self.version is None: - cmd = shlex.split("./offlineimap.py --version") - self.version = run(cmd).rstrip().decode(FS_ENCODING) - return self.version - - -class User(object): - """Interact with the user.""" - - prompt = '-> ' - - @staticmethod - def request(msg): - print(msg) - return input(User.prompt) - - if __name__ == '__main__': offlineimapInfo = OfflineimapInfo() Git.chdirToRepositoryTopLevel() - oVersion = offlineimapInfo.getCurrentVersion() + oVersion = offlineimapInfo.getVersion() ccList = getTesters() - authors = Git.getAuthors(oVersion) + authors = Git.getAuthorsList(oVersion) for author in authors: email = author.getEmail() if email not in ccList: