Switch to side-by-side view

--- a/Allura/push_re.py
+++ b/Allura/push_re.py
@@ -1,104 +1,144 @@
 import os
 import re
+import readline # changes raw_input to allow line editing (sorry, pyflakes)
 import shlex
 import string
 import subprocess
+import sys
 from collections import defaultdict
 from ConfigParser import ConfigParser
 from datetime import date
 from urlparse import urljoin
-
-from allura.config import middleware
+from optparse import OptionParser
+
 from allura.lib import rest_api
 
-DEBUG=1
+VERBOSE = 2
+DRY_RUN = False
 CP = ConfigParser()
 
 re_ticket_ref = re.compile(r'\[#(\d+)\]')
 re_allura_ref = re.compile(r'\nreference: ')
 re_git_dir = re.compile(r'.*\.git/?\Z')
+re_ws = re.compile(r'\s')
 
 CRED={}
 
 def main():
+    global DRY_RUN, VERBOSE
+    op = OptionParser()
+    op.add_option('--dry-run', action='store_true', dest='dry_run', default=False)
+    (options, args) = op.parse_args(sys.argv[1:])
+    if options.dry_run:
+        DRY_RUN = True
+        print("This is a dry-run: tags will be created locally but nothing will be pushed.")
+
     CP.read(os.path.join(os.environ['HOME'], '.forgepushrc'))
     engineer = option('re', 'engineer', 'Name of engineer pushing: ')
+    sf_identity = option('re', 'sf_identity', 'Your SourceForge.net user-name: ')
     api_key = option('re', 'api_key', 'Forge API Key:')
     secret_key = option('re', 'secret_key', 'Forge Secret Key:')
     classic_path = option('re', 'classic_path', 'The path to your forge-classic repo:')
     theme_path = option('re', 'theme_path', 'The path to your sftheme repo:')
     if not re_git_dir.match(classic_path):
         classic_path += '/.git/'
+    if not re_git_dir.match(theme_path):
+        theme_path += '/.git/'
     CRED['api_key'] = api_key
     CRED['secret_key'] = secret_key
-    text, tag = make_ticket_text(engineer, classic_path, theme_path)
-    raw_input("Verify that there are no new dependencies, or RPM's are built for all deps...")
-    raw_input("Verify that a new sandbox builds starts without engr help...")
-    print '*** Create a ticket on SourceForge (https://sourceforge.net/p/allura/tickets/new/) with the following contents:'
-    print '*** Summary: Production Push (R:%s, D:%s) - allura' % (
-        tag, date.today().strftime('%Y%m%d'))
-    print '---BEGIN---'
-    print text
-    print '---END---'
+
+    if ask_yes_or_no('Confirm each git command?', 'y'):
+        VERBOSE = 2
+    elif ask_yes_or_no('Echo each git command?', 'y'):
+        VERBOSE = 1
+    else:
+        VERBOSE = 0
+
+    if VERBOSE:
+        print("Making sure our existing tags are up-to-date...")
+    git('fetch origin --tags')
+    git('fetch origin --tags', git_dir=classic_path)
+    git('fetch origin --tags', git_dir=theme_path)
+    text, new_tag = make_ticket_text(engineer, classic_path, theme_path)
+    raw_input("Make sure you merged dev up into master for forge, forge_classic, and sftheme.")
+    raw_input("Make sure there are no new dependencies, or RPM's are built for all dependencies.")
+    raw_input("Make sure a new sandbox builds and starts without engr help.")
+    print('*** Create a ticket on SourceForge (https://sourceforge.net/p/allura/tickets/new/) with the following contents:')
+    print('*** Summary: Production Push (R:%s, D:%s) - allura' % (
+        new_tag, date.today().strftime('%Y%m%d')))
+    print('---BEGIN---')
+    print(text)
+    print('---END---')
     newforge_num = raw_input('What is the newforge ticket number? ')
-    print '*** Create a SOG Trac ticket (https://control.sog.geek.net/sog/trac/newticket?keywords=LIAISON) with the same summary...'
-    print '---BEGIN---'
+    print('*** Create a SOG Trac ticket (https://control.sog.geek.net/sog/trac/newticket?keywords=LIAISON) with the same summary...')
+    print('---BEGIN---')
     sog_text = re_ticket_ref.sub('FO:\g<1>', text)
-    print re_allura_ref.sub('\nreference: https://sourceforge.net/p/allura/tickets/%s/' % newforge_num, sog_text)
-    print '---END---'
-    raw_input('Now link the two tickets...')
-    print "Let's tag the forge repo:"
-    command('git', 'tag', '-a', '-m', '[#%s] - Push to RE' % newforge_num, tag, 'master')
-    command('git', 'push', 'origin', 'master', '--tags')
-    command('git', 'push', 'live', 'master', '--tags')
-
-    print "Let's make a matching tag in the forge-classic repo:"
-    command('git', '--git-dir=%s' % classic_path, 'tag', '-a', '-m', '[#%s] - Push to RE' % newforge_num, tag, 'master')
-    command('git', '--git-dir=%s' % classic_path, 'push', 'origin', 'master', '--tags')
-    command('git', '--git-dir=%s' % classic_path, 'push', 'live', 'master', '--tags')
-
-    print "...and in the sftheme repo:"
-    command('git', '--git-dir=%s' % theme_path, 'tag', '-a', '-m', '[#%s] - Push to RE' % newforge_num, tag, 'master')
-    command('git', '--git-dir=%s' % theme_path, 'push', 'origin', 'master', '--tags')
-    command('git', '--git-dir=%s' % theme_path, 'push', 'live', 'master', '--tags')
-
-    raw_input('Now go to the sog-engr channel and let them know that %s is ready'
-              ' for pushing (include the JIRA ticket #' % tag)
-    raw_input('Make sure SOG restarted reactors and web services.')
+    print(re_allura_ref.sub('\nreference: https://sourceforge.net/p/allura/tickets/%s/' % newforge_num, sog_text))
+    print('---END---')
+    sog_num = raw_input('What is the SOG ticket number? ')
+    raw_input('Now link the two tickets.')
+    if VERBOSE:
+        print("Ask for approval (copy/paste the following text into Jabber)")
+    raw_input('Allura %s push for your approval (https://control.sog.geek.net/sog/trac/ticket/%s).' % (new_tag, sog_num))
+
+    if VERBOSE:
+        print("Tag and push the Allura repo for release...")
+    tag_message = '[#%s] - Push to RE' % newforge_num
+    git('tag', '-a', '-m', tag_message, new_tag, 'master')
+    git('push', 'origin', 'master', new_tag)
+    git('push', 'control.sog.geek.net:allura-live', 'master', new_tag)
+    if ask_yes_or_no('Do you want to push to the public repo, too?', 'n'):
+        git('push', 'ssh://%s@git.code.sf.net/p/allura/git.git' % sf_identity, 'master', new_tag, fail_ok=True)
+
+    if VERBOSE:
+        print("Tag and push the forge-classic repo for release...")
+    git('tag', '-a', '-m', tag_message, new_tag, 'master', git_dir=classic_path)
+    git('push', 'origin', 'master', new_tag, git_dir=classic_path)
+    git('push', 'control.sog.geek.net:forge-classic-live', 'master', new_tag, git_dir=classic_path)
+
+    if VERBOSE:
+        print("Tag and push the sftheme repo for release...")
+    git('tag', '-a', '-m', tag_message, new_tag, 'master', git_dir=theme_path)
+    git('push', 'origin', 'master', new_tag, git_dir=theme_path)
+    git('push', 'control.sog.geek.net:sftheme-live', 'master', new_tag, git_dir=theme_path)
+
+    if VERBOSE:
+        print("Tell SOG we're ready (copy/paste the following text into Jabber)")
+    print('Allura %s is ready for pushing (https://control.sog.geek.net/sog/trac/ticket/%s).' % (new_tag, sog_num))
     CP.write(open(os.path.join(os.environ['HOME'], '.forgepushrc'), 'w'))
-    print "You're done!"
+    if VERBOSE:
+        print("That's all, folks!")
 
 def make_ticket_text(engineer, classic_path, theme_path):
     tag_prefix = date.today().strftime('release_%Y%m%d')
     # get release tag
-    existing_tags_today = command('git tag -l %s*' % tag_prefix)
+    existing_tags_today = git('tag -l %s*' % tag_prefix)
     if existing_tags_today:
-        tag = '%s.%.2d' % (tag_prefix, len(existing_tags_today))
-    else:
-        tag = tag_prefix
-    last_release = command('git tag -l release_*')
-    if last_release: last_release = last_release[-1]
-    else: last_release = ''
-    changes = command(
-            'git', 'log', "--format=* %h %s", last_release.strip() + '..')
-    changes += command(
-            'git', '--git-dir=%s' % classic_path, 'log', "--format=* %h %s", last_release.strip() + '..')
-    changes += command(
-            'git', '--git-dir=%s' % theme_path, 'log', "--format=* %h %s", last_release.strip() + '..')
+        new_tag = '%s.%.2d' % (tag_prefix, len(existing_tags_today))
+    else:
+        new_tag = tag_prefix
+    last_release = get_last_release_tag()
+    since_last_release = last_release + '..master'
+    format = '--format=* %h %s'
+    if VERBOSE:
+        print("Examining commits to build the list of fixed tickets...")
+    changes = git('log', format, since_last_release, strip_eol=False)
+    changes += git('log', format, since_last_release, git_dir=classic_path, strip_eol=False)
+    changes += git('log', format, since_last_release, git_dir=theme_path, strip_eol=False)
     if not changes:
-        print 'There were no commits found; maybe you forgot to merge dev->master? (Ctrl-C to abort)'
+        print('There were no commits found; maybe you forgot to merge dev->master? (Ctrl-C to abort)')
     changelog = ''.join(changes or [])
     changes = ''.join(format_changes(changes))
-    print 'Changelog:\n%s' % changelog
-    print 'Tickets:\n%s' % changes
+    print('Changelog:\n%s' % changelog)
+    print('Tickets:\n%s' % changes)
     prelaunch = []
     postlaunch = []
-    needs_flyway = raw_input('Does this release require a migration? [y]')
-    needs_ensure_index = raw_input('Does this release require ensure_index? [y]')
-    if needs_flyway[:1].lower() in ('', 'y', '1'):
+    needs_flyway = ask_yes_or_no('Does this release require a migration?', 'y')
+    needs_ensure_index = ask_yes_or_no('Does this release require ensure_index?', 'y')
+    if needs_flyway:
         prelaunch.append('* dump the database in case we need to roll back')
         postlaunch.append('* allurapaste flyway --url mongo://sfn-mongo:27017/')
-    if needs_ensure_index[:1].lower() in ('', 'y', '1'):
+    if needs_ensure_index:
         postlaunch.append('* allurapaste ensure_index /var/local/config/production.ini')
     if postlaunch:
         postlaunch = [ 'From sfu-scmprocess-1 do the following:\n' ] + postlaunch
@@ -110,7 +150,7 @@
         prelaunch = '\n'.join(prelaunch)
     else:
         prelaunch = '-none-'
-    return TICKET_TEMPLATE.substitute(locals()), tag
+    return TICKET_TEMPLATE.substitute(locals()), new_tag
 
 def format_changes(changes):
     if not changes:
@@ -133,25 +173,58 @@
                 'closed': 'Fix' }.get(ticket['status'], 'Address')
             yield ' * %s %s: %s\n' % (verb, ref, ticket['summary'])
     except:
-        print '*** ERROR CONTACTING FORGE FOR TICKET SUMMARIES ***'
+        print('*** ERROR CONTACTING FORGE FOR TICKET SUMMARIES ***')
         raise
         for ci in changes:
             yield ci
 
-def command(*args):
-    if len(args) == 1 and isinstance(args[0], basestring):
+def ask_yes_or_no(prompt, default):
+    result = raw_input('%s [%s] ' % (prompt, default)) or default
+    return result[:1].lower() in ('y', '1')
+
+def assemble_command(*args):
+    quoted = [ "'%s'" % arg if re_ws.search(arg) else arg for arg in args ]
+    return ' '.join(quoted)
+
+def git(*args, **kwargs):
+    if len(args)==1 and isinstance(args[0], basestring):
         argv = shlex.split(args[0])
     else:
         argv = list(args)
-    if DEBUG:
-        print ' '.join(argv)
-        raw_input('Press enter to run this command...')
+    if argv[0] != 'git':
+        argv.insert(0, 'git')
+    if DRY_RUN and argv[1]=='push':
+        argv.insert(2, '--dry-run')
+    if 'git_dir' in kwargs:
+        argv.insert(1, '--git-dir=%s' % kwargs['git_dir'])
+    full_command = assemble_command(*argv)
+    if VERBOSE==2:
+        raw_input(full_command)
+    elif VERBOSE==1:
+        print(full_command)
     p = subprocess.Popen(argv, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
     rc = p.wait()
-    if rc != 0:
-        print 'Error running %s' % ' '.join(argv)
-        import pdb; pdb.set_trace()
-    return p.stdout.readlines()
+    output = p.stdout.readlines()
+    if kwargs.get('strip_eol', True):
+        output = [ line[:-1] for line in output ]
+    if rc:
+        print('Error: %s' % full_command)
+        for line in output: print(line.rstrip())
+        if not kwargs.get('fail_ok', False):
+            import pdb; pdb.set_trace()
+    return output
+
+def get_last_release_tag():
+    has_clear_history = getattr(readline, 'clear_history')
+    if has_clear_history:
+        readline.clear_history()
+    for tag in git('tag -l release_*'):
+        readline.add_history(tag)
+    default = tag or ''
+    result = raw_input('Last successful push? [%s] ' % default) or default
+    if has_clear_history:
+        readline.clear_history()
+    return result
 
 def option(section, key, prompt=None):
     if not CP.has_section(section):
@@ -169,7 +242,7 @@
 (engr) Name of Engineer pushing: $engineer
 (engr) Which code tree(s): allura, forge-classic, sftheme
 (engr) Is configtree to be pushed?: no
-(engr) Which release/revision is going to be synced?: $tag
+(engr) Which release/revision is going to be synced?: $new_tag
 (engr) Itemized list of changes to be launched with sync:
 
 $changes