Name: js-handler/node_modules/restify/node_modules/bunyan/tools/cutarelease.py 
1:
#!/usr/bin/env python
2:
# -*- coding: utf-8 -*-
3:
# Copyright (c) 2009-2012 Trent Mick
4:
 
5:
"""cutarelease -- Cut a release of your project.
6:
 
7:
A script that will help cut a release for a git-based project that follows
8:
a few conventions. It'll update your changelog (CHANGES.md), add a git
9:
tag, push those changes, update your version to the next patch level release
10:
and create a new changelog section for that new version.
11:
 
12:
Conventions:
13:
- XXX
14:
"""
15:
 
16:
__version_info__ = (1, 0, 7)
17:
__version__ = '.'.join(map(str, __version_info__))
18:
 
19:
import sys
20:
import os
21:
from os.path import join, dirname, normpath, abspath, exists, basename, splitext
22:
from glob import glob
23:
from pprint import pprint
24:
import re
25:
import codecs
26:
import logging
27:
import optparse
28:
import json
29:
 
30:
 
31:
 
32:
#---- globals and config
33:
 
34:
log = logging.getLogger("cutarelease")
35:
 
36:
class Error(Exception):
37:
    pass
38:
 
39:
 
40:
 
41:
#---- main functionality
42:
 
43:
def cutarelease(project_name, version_files, dry_run=False):
44:
    """Cut a release.
45:
 
46:
    @param project_name {str}
47:
    @param version_files {list} List of paths to files holding the version
48:
        info for this project.
49:
 
50:
        If none are given it attempts to guess the version file:
51:
        package.json or VERSION.txt or VERSION or $project_name.py
52:
        or lib/$project_name.py or $project_name.js or lib/$project_name.js.
53:
 
54:
        The version file can be in one of the following forms:
55:
 
56:
        - A .py file, in which case the file is expect to have a top-level
57:
          global called "__version_info__" as follows. [1]
58:
 
59:
            __version_info__ = (0, 7, 6)
60:
 
61:
          Note that I typically follow that with the following to get a
62:
          string version attribute on my modules:
63:
 
64:
            __version__ = '.'.join(map(str, __version_info__))
65:
 
66:
        - A .js file, in which case the file is expected to have a top-level
67:
          global called "VERSION" as follows:
68:
 
69:
            ver VERSION = "1.2.3";
70:
 
71:
        - A "package.json" file, typical of a node.js npm-using project.
72:
          The package.json file must have a "version" field.
73:
 
74:
        - TODO: A simple version file whose only content is a "1.2.3"-style version
75:
          string.
76:
 
77:
    [1]: This is a convention I tend to follow in my projects.
78:
        Granted it might not be your cup of tea. I should add support for
79:
        just `__version__ = "1.2.3"`. I'm open to other suggestions too.
80:
    """
81:
    dry_run_str = dry_run and " (dry-run)" or ""
82:
 
83:
    if not version_files:
84:
        log.info("guessing version file")
85:
        candidates = [
86:
            "package.json",
87:
            "VERSION.txt",
88:
            "VERSION",
89:
            "%s.py" % project_name,
90:
            "lib/%s.py" % project_name,
91:
            "%s.js" % project_name,
92:
            "lib/%s.js" % project_name,
93:
        ]
94:
        for candidate in candidates:
95:
            if exists(candidate):
96:
                version_files = [candidate]
97:
                break
98:
        else:
99:
            raise Error("could not find a version file: specify its path or "
100:
                "add one of the following to your project: '%s'"
101:
                % "', '".join(candidates))
102:
        log.info("using '%s' as version file", version_files[0])
103:
 
104:
    parsed_version_files = [_parse_version_file(f) for f in version_files]
105:
    version_file_type, version_info = parsed_version_files[0]
106:
    version = _version_from_version_info(version_info)
107:
 
108:
    # Confirm
109:
    if not dry_run:
110:
        answer = query_yes_no("* * *\n"
111:
            "Are you sure you want cut a %s release?\n"
112:
            "This will involved commits and a push." % version,
113:
            default="no")
114:
        print "* * *"
115:
        if answer != "yes":
116:
            log.info("user abort")
117:
            return
118:
    log.info("cutting a %s release%s", version, dry_run_str)
119:
 
120:
    # Checks: Ensure there is a section in changes for this version.
121:
 
122:
 
123:
 
124:
    changes_path = "CHANGES.md"
125:
    changes_txt, changes, nyr = parse_changelog(changes_path)
126:
    #pprint(changes)
127:
    top_ver = changes[0]["version"]
128:
    if top_ver != version:
129:
        raise Error("changelog '%s' top section says "
130:
            "version %r, expected version %r: aborting"
131:
            % (changes_path, top_ver, version))
132:
    top_verline = changes[0]["verline"]
133:
    if not top_verline.endswith(nyr):
134:
        answer = query_yes_no("\n* * *\n"
135:
            "The changelog '%s' top section doesn't have the expected\n"
136:
            "'%s' marker. Has this been released already?"
137:
            % (changes_path, nyr), default="yes")
138:
        print "* * *"
139:
        if answer != "no":
140:
            log.info("abort")
141:
            return
142:
    top_body = changes[0]["body"]
143:
    if top_body.strip() == "(nothing yet)":
144:
        raise Error("top section body is `(nothing yet)': it looks like "
145:
            "nothing has been added to this release")
146:
 
147:
    # Commits to prepare release.
148:
    changes_txt_before = changes_txt
149:
    changes_txt = changes_txt.replace(" (not yet released)", "", 1)
150:
    if not dry_run and changes_txt != changes_txt_before:
151:
        log.info("prepare `%s' for release", changes_path)
152:
        f = codecs.open(changes_path, 'w', 'utf-8')
153:
        f.write(changes_txt)
154:
        f.close()
155:
        run('git commit %s -m "prepare for %s release"'
156:
            % (changes_path, version))
157:
 
158:
    # Tag version and push.
159:
    curr_tags = set(t for t in _capture_stdout(["git", "tag", "-l"]).split('\n') if t)
160:
    if not dry_run and version not in curr_tags:
161:
        log.info("tag the release")
162:
        run('git tag -a "%s" -m "version %s"' % (version, version))
163:
        run('git push --tags')
164:
 
165:
    # Optionally release.
166:
    if exists("package.json"):
167:
        answer = query_yes_no("\n* * *\nPublish to npm?", default="yes")
168:
        print "* * *"
169:
        if answer == "yes":
170:
            if dry_run:
171:
                log.info("skipping npm publish (dry-run)")
172:
            else:
173:
                run('npm publish')
174:
    elif exists("setup.py"):
175:
        answer = query_yes_no("\n* * *\nPublish to pypi?", default="yes")
176:
        print "* * *"
177:
        if answer == "yes":
178:
            if dry_run:
179:
                log.info("skipping pypi publish (dry-run)")
180:
            else:
181:
                run("%spython setup.py sdist --formats zip upload"
182:
                    % _setup_command_prefix())
183:
 
184:
    # Commits to prepare for future dev and push.
185:
    # - update changelog file
186:
    next_version_info = _get_next_version_info(version_info)
187:
    next_version = _version_from_version_info(next_version_info)
188:
    log.info("prepare for future dev (version %s)", next_version)
189:
    marker = "## " + changes[0]["verline"]
190:
    if marker.endswith(nyr):
191:
        marker = marker[0:-len(nyr)]
192:
    if marker not in changes_txt:
193:
        raise Error("couldn't find `%s' marker in `%s' "
194:
            "content: can't prep for subsequent dev" % (marker, changes_path))
195:
    next_verline = "%s %s%s" % (marker.rsplit(None, 1)[0], next_version, nyr)
196:
    changes_txt = changes_txt.replace(marker + '\n',
197:
        "%s\n\n(nothing yet)\n\n\n%s\n" % (next_verline, marker))
198:
    if not dry_run:
199:
        f = codecs.open(changes_path, 'w', 'utf-8')
200:
        f.write(changes_txt)
201:
        f.close()
202:
 
203:
    # - update version file
204:
    next_version_tuple = _tuple_from_version(next_version)
205:
    for i, ver_file in enumerate(version_files):
206:
        ver_content = codecs.open(ver_file, 'r', 'utf-8').read()
207:
        ver_file_type, ver_info = parsed_version_files[i]
208:
        if ver_file_type == "json":
209:
            marker = '"version": "%s"' % version
210:
            if marker not in ver_content:
211:
                raise Error("couldn't find `%s' version marker in `%s' "
212:
                    "content: can't prep for subsequent dev" % (marker, ver_file))
213:
            ver_content = ver_content.replace(marker,
214:
                '"version": "%s"' % next_version)
215:
        elif ver_file_type == "javascript":
216:
            candidates = [
217:
                ("single", "var VERSION = '%s';" % version),
218:
                ("double", 'var VERSION = "%s";' % version),
219:
            ]
220:
            for quote_type, marker in candidates:
221:
                if marker in ver_content:
222:
                    break
223:
            else:
224:
                raise Error("couldn't find any candidate version marker in "
225:
                    "`%s' content: can't prep for subsequent dev: %r"
226:
                    % (ver_file, candidates))
227:
            if quote_type == "single":
228:
                ver_content = ver_content.replace(marker,
229:
                    "var VERSION = '%s';" % next_version)
230:
            else:
231:
                ver_content = ver_content.replace(marker,
232:
                    'var VERSION = "%s";' % next_version)
233:
        elif ver_file_type == "python":
234:
            marker = "__version_info__ = %r" % (version_info,)
235:
            if marker not in ver_content:
236:
                raise Error("couldn't find `%s' version marker in `%s' "
237:
                    "content: can't prep for subsequent dev" % (marker, ver_file))
238:
            ver_content = ver_content.replace(marker,
239:
                "__version_info__ = %r" % (next_version_tuple,))
240:
        elif ver_file_type == "version":
241:
            ver_content = next_version
242:
        else:
243:
            raise Error("unknown ver_file_type: %r" % ver_file_type)
244:
        if not dry_run:
245:
            log.info("update version to '%s' in '%s'", next_version, ver_file)
246:
            f = codecs.open(ver_file, 'w', 'utf-8')
247:
            f.write(ver_content)
248:
            f.close()
249:
 
250:
    if not dry_run:
251:
        run('git commit %s %s -m "prep for future dev"' % (
252:
            changes_path, ' '.join(version_files)))
253:
        run('git push')
254:
 
255:
 
256:
 
257:
#---- internal support routines
258:
 
259:
def _indent(s, indent='    '):
260:
    return indent + indent.join(s.splitlines(True))
261:
 
262:
def _tuple_from_version(version):
263:
    def _intify(s):
264:
        try:
265:
            return int(s)
266:
        except ValueError:
267:
            return s
268:
    return tuple(_intify(b) for b in version.split('.'))
269:
 
270:
def _get_next_version_info(version_info):
271:
    next = list(version_info[:])
272:
    next[-1] += 1
273:
    return tuple(next)
274:
 
275:
def _version_from_version_info(version_info):
276:
    v = str(version_info[0])
277:
    state_dot_join = True
278:
    for i in version_info[1:]:
279:
        if state_dot_join:
280:
            try:
281:
                int(i)
282:
            except ValueError:
283:
                state_dot_join = False
284:
            else:
285:
                pass
286:
        if state_dot_join:
287:
            v += "." + str(i)
288:
        else:
289:
            v += str(i)
290:
    return v
291:
 
292:
_version_re = re.compile(r"^(\d+)\.(\d+)(?:\.(\d+)([abc](\d+)?)?)?$")
293:
def _version_info_from_version(version):
294:
    m = _version_re.match(version)
295:
    if not m:
296:
        raise Error("could not convert '%s' version to version info" % version)
297:
    version_info = []
298:
    for g in m.groups():
299:
        if g is None:
300:
            break
301:
        try:
302:
            version_info.append(int(g))
303:
        except ValueError:
304:
            version_info.append(g)
305:
    return tuple(version_info)
306:
 
307:
def _parse_version_file(version_file):
308:
    """Get version info from the given file. It can be any of:
309:
 
310:
    Supported version file types (i.e. types of files from which we know
311:
    how to parse the version string/number -- often by some convention):
312:
    - json: use the "version" key
313:
    - javascript: look for a `var VERSION = "1.2.3";` or
314:
      `var VERSION = '1.2.3';`
315:
    - python: Python script/module with `__version_info__ = (1, 2, 3)`
316:
    - version: a VERSION.txt or VERSION file where the whole contents are
317:
      the version string
318:
 
319:
    @param version_file {str} Can be a path or "type:path", where "type"
320:
        is one of the supported types.
321:
    """
322:
    # Get version file *type*.
323:
    version_file_type = None
324:
    match = re.compile("^([a-z]+):(.*)$").search(version_file)
325:
    if match:
326:
        version_file = match.group(2)
327:
        version_file_type = match.group(1)
328:
        aliases = {
329:
            "js": "javascript"
330:
        }
331:
        if version_file_type in aliases:
332:
            version_file_type = aliases[version_file_type]
333:
 
334:
    f = codecs.open(version_file, 'r', 'utf-8')
335:
    content = f.read()
336:
    f.close()
337:
 
338:
    if not version_file_type:
339:
        # Guess the type.
340:
        base = basename(version_file)
341:
        ext = splitext(base)[1]
342:
        if ext == ".json":
343:
            version_file_type = "json"
344:
        elif ext == ".py":
345:
            version_file_type = "python"
346:
        elif ext == ".js":
347:
            version_file_type = "javascript"
348:
        elif content.startswith("#!"):
349:
            shebang = content.splitlines(False)[0]
350:
            shebang_bits = re.split(r'[/ \t]', shebang)
351:
            for name, typ in {"python": "python", "node": "javascript"}.items():
352:
                if name in shebang_bits:
353:
                    version_file_type = typ
354:
                    break
355:
        elif base in ("VERSION", "VERSION.txt"):
356:
            version_file_type = "version"
357:
    if not version_file_type:
358:
        raise RuntimeError("can't extract version from '%s': no idea "
359:
            "what type of file it it" % version_file)
360:
 
361:
    if version_file_type == "json":
362:
        obj = json.loads(content)
363:
        version_info = _version_info_from_version(obj["version"])
364:
    elif version_file_type == "python":
365:
        m = re.search(r'^__version_info__ = (.*?)$', content, re.M)
366:
        version_info = eval(m.group(1))
367:
    elif version_file_type == "javascript":
368:
        m = re.search(r'^var VERSION = (\'|")(.*?)\1;$', content, re.M)
369:
        version_info = _version_info_from_version(m.group(2))
370:
    elif version_file_type == "version":
371:
        version_info = _version_info_from_version(content.strip())
372:
    else:
373:
        raise RuntimeError("unexpected version_file_type: %r"
374:
            % version_file_type)
375:
    return version_file_type, version_info
376:
 
377:
 
378:
def parse_changelog(changes_path):
379:
    """Parse the given changelog path and return `(content, parsed, nyr)`
380:
    where `nyr` is the ' (not yet released)' marker and `parsed` looks like:
381:
 
382:
        [{'body': u'\n(nothing yet)\n\n',
383:
          'verline': u'restify 1.0.1 (not yet released)',
384:
          'version': u'1.0.1'},    # version is parsed out for top section only
385:
         {'body': u'...',
386:
          'verline': u'1.0.0'},
387:
         {'body': u'...',
388:
          'verline': u'1.0.0-rc2'},
389:
         {'body': u'...',
390:
          'verline': u'1.0.0-rc1'}]
391:
 
392:
    A changelog (CHANGES.md) is expected to look like this:
393:
 
394:
        # $project Changelog
395:
 
396:
        ## $next_version (not yet released)
397:
 
398:
        ...
399:
 
400:
        ## $version1
401:
 
402:
        ...
403:
 
404:
        ## $version2
405:
 
406:
        ... and so on
407:
 
408:
    The version lines are enforced as follows:
409:
 
410:
    - The top entry should have a " (not yet released)" suffix. "Should"
411:
      because recovery from half-cutarelease failures is supported.
412:
    - A version string must be extractable from there, but it tries to
413:
      be loose (though strict "X.Y.Z" versioning is preferred). Allowed
414:
 
415:
            ## 1.0.0
416:
            ## my project 1.0.1
417:
            ## foo 1.2.3-rc2
418:
 
419:
      Basically, (a) the " (not yet released)" is stripped, (b) the
420:
      last token is the version, and (c) that version must start with
421:
      a digit (sanity check).
422:
    """
423:
    if not exists(changes_path):
424:
        raise Error("changelog file '%s' not found" % changes_path)
425:
    content = codecs.open(changes_path, 'r', 'utf-8').read()
426:
 
427:
    parser = re.compile(
428:
        r'^##\s*(?P<verline>[^\n]*?)\s*$(?P<body>.*?)(?=^##|\Z)',
429:
        re.M | re.S)
430:
    sections = parser.findall(content)
431:
 
432:
    # Sanity checks on changelog format.
433:
    if not sections:
434:
        template = "## 1.0.0 (not yet released)\n\n(nothing yet)\n"
435:
        raise Error("changelog '%s' must have at least one section, "
436:
            "suggestion:\n\n%s" % (changes_path, _indent(template)))
437:
    first_section_verline = sections[0][0]
438:
    nyr = ' (not yet released)'
439:
    #if not first_section_verline.endswith(nyr):
440:
    #    eg = "## %s%s" % (first_section_verline, nyr)
441:
    #    raise Error("changelog '%s' top section must end with %r, "
442:
    #        "naive e.g.: '%s'" % (changes_path, nyr, eg))
443:
 
444:
    items = []
445:
    for i, section in enumerate(sections):
446:
        item = {
447:
            "verline": section[0],
448:
            "body": section[1]
449:
        }
450:
        if i == 0:
451:
            # We only bother to pull out 'version' for the top section.
452:
            verline = section[0]
453:
            if verline.endswith(nyr):
454:
                verline = verline[0:-len(nyr)]
455:
            version = verline.split()[-1]
456:
            try:
457:
                int(version[0])
458:
            except ValueError:
459:
                msg = ''
460:
                if version.endswith(')'):
461:
                    msg = " (cutarelease is picky about the trailing %r " \
462:
                        "on the top version line. Perhaps you misspelled " \
463:
                        "that?)" % nyr
464:
                raise Error("changelog '%s' top section version '%s' is "
465:
                    "invalid: first char isn't a number%s"
466:
                    % (changes_path, version, msg))
467:
            item["version"] = version
468:
        items.append(item)
469:
 
470:
    return content, items, nyr
471:
 
472:
## {{{ http://code.activestate.com/recipes/577058/ (r2)
473:
def query_yes_no(question, default="yes"):
474:
    """Ask a yes/no question via raw_input() and return their answer.
475:
 
476:
    "question" is a string that is presented to the user.
477:
    "default" is the presumed answer if the user just hits <Enter>.
478:
        It must be "yes" (the default), "no" or None (meaning
479:
        an answer is required of the user).
480:
 
481:
    The "answer" return value is one of "yes" or "no".
482:
    """
483:
    valid = {"yes":"yes",   "y":"yes",  "ye":"yes",
484:
             "no":"no",     "n":"no"}
485:
    if default == None:
486:
        prompt = " [y/n] "
487:
    elif default == "yes":
488:
        prompt = " [Y/n] "
489:
    elif default == "no":
490:
        prompt = " [y/N] "
491:
    else:
492:
        raise ValueError("invalid default answer: '%s'" % default)
493:
 
494:
    while 1:
495:
        sys.stdout.write(question + prompt)
496:
        choice = raw_input().lower()
497:
        if default is not None and choice == '':
498:
            return default
499:
        elif choice in valid.keys():
500:
            return valid[choice]
501:
        else:
502:
            sys.stdout.write("Please respond with 'yes' or 'no' "\
503:
                             "(or 'y' or 'n').\n")
504:
## end of http://code.activestate.com/recipes/577058/ }}}
505:
 
506:
def _capture_stdout(argv):
507:
    import subprocess
508:
    p = subprocess.Popen(argv, stdout=subprocess.PIPE)
509:
    return p.communicate()[0]
510:
 
511:
class _NoReflowFormatter(optparse.IndentedHelpFormatter):
512:
    """An optparse formatter that does NOT reflow the description."""
513:
    def format_description(self, description):
514:
        return description or ""
515:
 
516:
def run(cmd):
517:
    """Run the given command.
518:
 
519:
    Raises OSError is the command returns a non-zero exit status.
520:
    """
521:
    log.debug("running '%s'", cmd)
522:
    fixed_cmd = cmd
523:
    if sys.platform == "win32" and cmd.count('"') > 2:
524:
        fixed_cmd = '"' + cmd + '"'
525:
    retval = os.system(fixed_cmd)
526:
    if hasattr(os, "WEXITSTATUS"):
527:
        status = os.WEXITSTATUS(retval)
528:
    else:
529:
        status = retval
530:
    if status:
531:
        raise OSError(status, "error running '%s'" % cmd)
532:
 
533:
def _setup_command_prefix():
534:
    prefix = ""
535:
    if sys.platform == "darwin":
536:
        # http://forums.macosxhints.com/archive/index.php/t-43243.html
537:
        # This is an Apple customization to `tar` to avoid creating
538:
        # '._foo' files for extended-attributes for archived files.
539:
        prefix = "COPY_EXTENDED_ATTRIBUTES_DISABLE=1 "
540:
    return prefix
541:
 
542:
 
543:
#---- mainline
544:
 
545:
def main(argv):
546:
    logging.basicConfig(format="%(name)s: %(levelname)s: %(message)s")
547:
    log.setLevel(logging.INFO)
548:
 
549:
    # Parse options.
550:
    parser = optparse.OptionParser(prog="cutarelease", usage='',
551:
        version="%prog " + __version__, description=__doc__,
552:
        formatter=_NoReflowFormatter())
553:
    parser.add_option("-v", "--verbose", dest="log_level",
554:
        action="store_const", const=logging.DEBUG,
555:
        help="more verbose output")
556:
    parser.add_option("-q", "--quiet", dest="log_level",
557:
        action="store_const", const=logging.WARNING,
558:
        help="quieter output (just warnings and errors)")
559:
    parser.set_default("log_level", logging.INFO)
560:
    parser.add_option("--test", action="store_true",
561:
        help="run self-test and exit (use 'eol.py -v --test' for verbose test output)")
562:
    parser.add_option("-p", "--project-name", metavar="NAME",
563:
        help='the name of this project (default is the base dir name)',
564:
        default=basename(os.getcwd()))
565:
    parser.add_option("-f", "--version-file", metavar="[TYPE:]PATH",
566:
        action='append', dest="version_files",
567:
        help='The path to the project file holding the version info. Can be '
568:
             'specified multiple times if more than one file should be updated '
569:
             'with new version info. If excluded, it will be guessed.')
570:
    parser.add_option("-n", "--dry-run", action="store_true",
571:
        help='Do a dry-run', default=False)
572:
    opts, args = parser.parse_args()
573:
    log.setLevel(opts.log_level)
574:
 
575:
    cutarelease(opts.project_name, opts.version_files, dry_run=opts.dry_run)
576:
 
577:
 
578:
## {{{ http://code.activestate.com/recipes/577258/ (r5+)
579:
if __name__ == "__main__":
580:
    try:
581:
        retval = main(sys.argv)
582:
    except KeyboardInterrupt:
583:
        sys.exit(1)
584:
    except SystemExit:
585:
        raise
586:
    except:
587:
        import traceback, logging
588:
        if not log.handlers and not logging.root.handlers:
589:
            logging.basicConfig()
590:
        skip_it = False
591:
        exc_info = sys.exc_info()
592:
        if hasattr(exc_info[0], "__name__"):
593:
            exc_class, exc, tb = exc_info
594:
            if isinstance(exc, IOError) and exc.args[0] == 32:
595:
                # Skip 'IOError: [Errno 32] Broken pipe': often a cancelling of `less`.
596:
                skip_it = True
597:
            if not skip_it:
598:
                tb_path, tb_lineno, tb_func = traceback.extract_tb(tb)[-1][:3]
599:
                log.error("%s (%s:%s in %s)", exc_info[1], tb_path,
600:
                    tb_lineno, tb_func)
601:
        else:  # string exception
602:
            log.error(exc_info[0])
603:
        if not skip_it:
604:
            if log.isEnabledFor(logging.DEBUG):
605:
                traceback.print_exception(*exc_info)
606:
            sys.exit(1)
607:
    else:
608:
        sys.exit(retval)
609:
## end of http://code.activestate.com/recipes/577258/ }}}