joyent/node-triton#131 update 'make cutarelease' and 'make versioncheck' to avoid commits for cutting a release
Reviewed by: Josh Wilsdon <josh@wilsdon.ca> Approved by: Josh Wilsdon <josh@wilsdon.ca>
This commit is contained in:
		
							parent
							
								
									21f2d29c86
								
							
						
					
					
						commit
						b8b753a94d
					
				@ -5,7 +5,10 @@ Known issues:
 | 
				
			|||||||
- `triton ssh ...` disables ssh ControlMaster to avoid issue #52.
 | 
					- `triton ssh ...` disables ssh ControlMaster to avoid issue #52.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## 4.14.0 (not yet released)
 | 
					## not yet released
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 4.14.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- [#130] Include disabled images when using an image cache (e.g. for filling in
 | 
					- [#130] Include disabled images when using an image cache (e.g. for filling in
 | 
				
			||||||
  image name and version details in `triton ls` output.
 | 
					  image name and version details in `triton ls` output.
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										20
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										20
									
								
								Makefile
									
									
									
									
									
								
							@ -50,12 +50,26 @@ check:: versioncheck
 | 
				
			|||||||
.PHONY: versioncheck
 | 
					.PHONY: versioncheck
 | 
				
			||||||
versioncheck:
 | 
					versioncheck:
 | 
				
			||||||
	@echo version is: $(shell cat package.json | json version)
 | 
						@echo version is: $(shell cat package.json | json version)
 | 
				
			||||||
	[[ `cat package.json | json version` == `grep '^## ' CHANGES.md | head -1 | awk '{print $$2}'` ]]
 | 
						[[ `cat package.json | json version` == `grep '^## ' CHANGES.md | head -2 | tail -1 | awk '{print $$2}'` ]]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.PHONY: cutarelease
 | 
					.PHONY: cutarelease
 | 
				
			||||||
cutarelease: versioncheck
 | 
					cutarelease: versioncheck
 | 
				
			||||||
	[[ `git status | tail -n1` == "nothing to commit, working directory clean" ]]
 | 
						[[ -z `git status --short` ]]  # If this fails, the working dir is dirty.
 | 
				
			||||||
	./tools/cutarelease.py -p triton -f package.json
 | 
						@ver=$(shell cat package.json | json version) && \
 | 
				
			||||||
 | 
						    publishedVer=$(shell npm view -j triton@$(shell json -f package.json version) version 2>/dev/null) && \
 | 
				
			||||||
 | 
						    if [[ -n "$$publishedVer" ]]; then \
 | 
				
			||||||
 | 
							echo "error: triton@$$ver is already published to npm"; \
 | 
				
			||||||
 | 
							exit 1; \
 | 
				
			||||||
 | 
						    fi; \
 | 
				
			||||||
 | 
						    echo "** Are you sure you want to tag and publish triton@$$ver to npm?"; \
 | 
				
			||||||
 | 
						    echo "** Enter to continue, Ctrl+C to abort."; \
 | 
				
			||||||
 | 
						    read
 | 
				
			||||||
 | 
						ver=$(shell cat package.json | json version) && \
 | 
				
			||||||
 | 
						    name=$(shell cat package.json | json name) && \
 | 
				
			||||||
 | 
						    date=$(shell date -u "+%Y-%m-%d") && \
 | 
				
			||||||
 | 
						    git tag -a "$$ver" -m "version $$ver ($$date)" && \
 | 
				
			||||||
 | 
						    git push --tags origin && \
 | 
				
			||||||
 | 
						    npm publish $$name
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.PHONY: git-hooks
 | 
					.PHONY: git-hooks
 | 
				
			||||||
git-hooks:
 | 
					git-hooks:
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										27
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										27
									
								
								README.md
									
									
									
									
									
								
							@ -351,6 +351,33 @@ where "coal" here refers to a development Triton (a.k.a SDC) ["Cloud On A
 | 
				
			|||||||
Laptop"](https://github.com/joyent/sdc#getting-started) standup.
 | 
					Laptop"](https://github.com/joyent/sdc#getting-started) standup.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Release process
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Here is how to cut a release:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					1. Make a commit to set the intended version in "package.json#version" and changing `## not yet released` at the top of "CHANGES.md" to:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ```
 | 
				
			||||||
 | 
					    ## not yet released
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ## $version
 | 
				
			||||||
 | 
					    ```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					2. Get that commit approved and merged via <https://cr.joyent.us>, as with all
 | 
				
			||||||
 | 
					   commits to this repo. See the discussion of contribution at the top of this
 | 
				
			||||||
 | 
					   readme.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					3. Once that is merged and you've updated your local copy, run:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ```
 | 
				
			||||||
 | 
					    make cutarelease
 | 
				
			||||||
 | 
					    ```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   This will run a couple checks (clean working copy, versions in package.json
 | 
				
			||||||
 | 
					   and CHANGES.md match), then will git tag and npm publish.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## License
 | 
					## License
 | 
				
			||||||
 | 
					
 | 
				
			||||||
MPL 2.0
 | 
					MPL 2.0
 | 
				
			||||||
 | 
				
			|||||||
@ -1,611 +0,0 @@
 | 
				
			|||||||
#!/usr/bin/env python
 | 
					 | 
				
			||||||
# -*- coding: utf-8 -*-
 | 
					 | 
				
			||||||
# Copyright (c) 2009-2012 Trent Mick
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
"""cutarelease -- Cut a release of your project.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
A script that will help cut a release for a git-based project that follows
 | 
					 | 
				
			||||||
a few conventions. It'll update your changelog (CHANGES.md), add a git
 | 
					 | 
				
			||||||
tag, push those changes, update your version to the next patch level release
 | 
					 | 
				
			||||||
and create a new changelog section for that new version.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Conventions:
 | 
					 | 
				
			||||||
- XXX
 | 
					 | 
				
			||||||
"""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
__version_info__ = (1, 0, 7)
 | 
					 | 
				
			||||||
__version__ = '.'.join(map(str, __version_info__))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import sys
 | 
					 | 
				
			||||||
import os
 | 
					 | 
				
			||||||
from os.path import join, dirname, normpath, abspath, exists, basename, splitext
 | 
					 | 
				
			||||||
from glob import glob
 | 
					 | 
				
			||||||
from pprint import pprint
 | 
					 | 
				
			||||||
import re
 | 
					 | 
				
			||||||
import codecs
 | 
					 | 
				
			||||||
import logging
 | 
					 | 
				
			||||||
import optparse
 | 
					 | 
				
			||||||
import json
 | 
					 | 
				
			||||||
import time
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#---- globals and config
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
log = logging.getLogger("cutarelease")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class Error(Exception):
 | 
					 | 
				
			||||||
    pass
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#---- main functionality
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def cutarelease(project_name, version_files, dry_run=False):
 | 
					 | 
				
			||||||
    """Cut a release.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @param project_name {str}
 | 
					 | 
				
			||||||
    @param version_files {list} List of paths to files holding the version
 | 
					 | 
				
			||||||
        info for this project.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        If none are given it attempts to guess the version file:
 | 
					 | 
				
			||||||
        package.json or VERSION.txt or VERSION or $project_name.py
 | 
					 | 
				
			||||||
        or lib/$project_name.py or $project_name.js or lib/$project_name.js.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        The version file can be in one of the following forms:
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        - A .py file, in which case the file is expect to have a top-level
 | 
					 | 
				
			||||||
          global called "__version_info__" as follows. [1]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            __version_info__ = (0, 7, 6)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          Note that I typically follow that with the following to get a
 | 
					 | 
				
			||||||
          string version attribute on my modules:
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            __version__ = '.'.join(map(str, __version_info__))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        - A .js file, in which case the file is expected to have a top-level
 | 
					 | 
				
			||||||
          global called "VERSION" as follows:
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            ver VERSION = "1.2.3";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        - A "package.json" file, typical of a node.js npm-using project.
 | 
					 | 
				
			||||||
          The package.json file must have a "version" field.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        - TODO: A simple version file whose only content is a "1.2.3"-style version
 | 
					 | 
				
			||||||
          string.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    [1]: This is a convention I tend to follow in my projects.
 | 
					 | 
				
			||||||
        Granted it might not be your cup of tea. I should add support for
 | 
					 | 
				
			||||||
        just `__version__ = "1.2.3"`. I'm open to other suggestions too.
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
    dry_run_str = dry_run and " (dry-run)" or ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if not version_files:
 | 
					 | 
				
			||||||
        log.info("guessing version file")
 | 
					 | 
				
			||||||
        candidates = [
 | 
					 | 
				
			||||||
            "package.json",
 | 
					 | 
				
			||||||
            "VERSION.txt",
 | 
					 | 
				
			||||||
            "VERSION",
 | 
					 | 
				
			||||||
            "%s.py" % project_name,
 | 
					 | 
				
			||||||
            "lib/%s.py" % project_name,
 | 
					 | 
				
			||||||
            "%s.js" % project_name,
 | 
					 | 
				
			||||||
            "lib/%s.js" % project_name,
 | 
					 | 
				
			||||||
        ]
 | 
					 | 
				
			||||||
        for candidate in candidates:
 | 
					 | 
				
			||||||
            if exists(candidate):
 | 
					 | 
				
			||||||
                version_files = [candidate]
 | 
					 | 
				
			||||||
                break
 | 
					 | 
				
			||||||
        else:
 | 
					 | 
				
			||||||
            raise Error("could not find a version file: specify its path or "
 | 
					 | 
				
			||||||
                "add one of the following to your project: '%s'"
 | 
					 | 
				
			||||||
                % "', '".join(candidates))
 | 
					 | 
				
			||||||
        log.info("using '%s' as version file", version_files[0])
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    parsed_version_files = [_parse_version_file(f) for f in version_files]
 | 
					 | 
				
			||||||
    version_file_type, version_info = parsed_version_files[0]
 | 
					 | 
				
			||||||
    version = _version_from_version_info(version_info)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # Confirm
 | 
					 | 
				
			||||||
    if not dry_run:
 | 
					 | 
				
			||||||
        answer = query_yes_no("* * *\n"
 | 
					 | 
				
			||||||
            "Are you sure you want cut a %s release?\n"
 | 
					 | 
				
			||||||
            "This will involved commits and a push." % version,
 | 
					 | 
				
			||||||
            default="no")
 | 
					 | 
				
			||||||
        print "* * *"
 | 
					 | 
				
			||||||
        if answer != "yes":
 | 
					 | 
				
			||||||
            log.info("user abort")
 | 
					 | 
				
			||||||
            return
 | 
					 | 
				
			||||||
    log.info("cutting a %s release%s", version, dry_run_str)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # Checks: Ensure there is a section in changes for this version.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    changes_path = "CHANGES.md"
 | 
					 | 
				
			||||||
    changes_txt, changes, nyr = parse_changelog(changes_path)
 | 
					 | 
				
			||||||
    #pprint(changes)
 | 
					 | 
				
			||||||
    top_ver = changes[0]["version"]
 | 
					 | 
				
			||||||
    if top_ver != version:
 | 
					 | 
				
			||||||
        raise Error("changelog '%s' top section says "
 | 
					 | 
				
			||||||
            "version %r, expected version %r: aborting"
 | 
					 | 
				
			||||||
            % (changes_path, top_ver, version))
 | 
					 | 
				
			||||||
    top_verline = changes[0]["verline"]
 | 
					 | 
				
			||||||
    if not top_verline.endswith(nyr):
 | 
					 | 
				
			||||||
        answer = query_yes_no("\n* * *\n"
 | 
					 | 
				
			||||||
            "The changelog '%s' top section doesn't have the expected\n"
 | 
					 | 
				
			||||||
            "'%s' marker. Has this been released already?"
 | 
					 | 
				
			||||||
            % (changes_path, nyr), default="yes")
 | 
					 | 
				
			||||||
        print "* * *"
 | 
					 | 
				
			||||||
        if answer != "no":
 | 
					 | 
				
			||||||
            log.info("abort")
 | 
					 | 
				
			||||||
            return
 | 
					 | 
				
			||||||
    top_body = changes[0]["body"]
 | 
					 | 
				
			||||||
    if top_body.strip() == "(nothing yet)":
 | 
					 | 
				
			||||||
        raise Error("top section body is `(nothing yet)': it looks like "
 | 
					 | 
				
			||||||
            "nothing has been added to this release")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # Commits to prepare release.
 | 
					 | 
				
			||||||
    changes_txt_before = changes_txt
 | 
					 | 
				
			||||||
    changes_txt = changes_txt.replace(" (not yet released)", "", 1)
 | 
					 | 
				
			||||||
    if not dry_run and changes_txt != changes_txt_before:
 | 
					 | 
				
			||||||
        log.info("prepare `%s' for release", changes_path)
 | 
					 | 
				
			||||||
        f = codecs.open(changes_path, 'w', 'utf-8')
 | 
					 | 
				
			||||||
        f.write(changes_txt)
 | 
					 | 
				
			||||||
        f.close()
 | 
					 | 
				
			||||||
        run('git commit %s -m "%s"'
 | 
					 | 
				
			||||||
            % (changes_path, version))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # Tag version and push.
 | 
					 | 
				
			||||||
    curr_tags = set(t for t in _capture_stdout(["git", "tag", "-l"]).split('\n') if t)
 | 
					 | 
				
			||||||
    if not dry_run and version not in curr_tags:
 | 
					 | 
				
			||||||
        log.info("tag the release")
 | 
					 | 
				
			||||||
        date = time.strftime("%Y-%m-%d")
 | 
					 | 
				
			||||||
        run('git tag -a "%s" -m "version %s (%s)"' % (version, version, date))
 | 
					 | 
				
			||||||
        run('git push --tags')
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # Optionally release.
 | 
					 | 
				
			||||||
    if exists("package.json"):
 | 
					 | 
				
			||||||
        answer = query_yes_no("\n* * *\nPublish to npm?", default="yes")
 | 
					 | 
				
			||||||
        print "* * *"
 | 
					 | 
				
			||||||
        if answer == "yes":
 | 
					 | 
				
			||||||
            if dry_run:
 | 
					 | 
				
			||||||
                log.info("skipping npm publish (dry-run)")
 | 
					 | 
				
			||||||
            else:
 | 
					 | 
				
			||||||
                run('npm publish')
 | 
					 | 
				
			||||||
    elif exists("setup.py"):
 | 
					 | 
				
			||||||
        answer = query_yes_no("\n* * *\nPublish to pypi?", default="yes")
 | 
					 | 
				
			||||||
        print "* * *"
 | 
					 | 
				
			||||||
        if answer == "yes":
 | 
					 | 
				
			||||||
            if dry_run:
 | 
					 | 
				
			||||||
                log.info("skipping pypi publish (dry-run)")
 | 
					 | 
				
			||||||
            else:
 | 
					 | 
				
			||||||
                run("%spython setup.py sdist --formats zip upload"
 | 
					 | 
				
			||||||
                    % _setup_command_prefix())
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # Commits to prepare for future dev and push.
 | 
					 | 
				
			||||||
    # - update changelog file
 | 
					 | 
				
			||||||
    next_version_info = _get_next_version_info(version_info)
 | 
					 | 
				
			||||||
    next_version = _version_from_version_info(next_version_info)
 | 
					 | 
				
			||||||
    log.info("prepare for future dev (version %s)", next_version)
 | 
					 | 
				
			||||||
    marker = "## " + changes[0]["verline"]
 | 
					 | 
				
			||||||
    if marker.endswith(nyr):
 | 
					 | 
				
			||||||
        marker = marker[0:-len(nyr)]
 | 
					 | 
				
			||||||
    if marker not in changes_txt:
 | 
					 | 
				
			||||||
        raise Error("couldn't find `%s' marker in `%s' "
 | 
					 | 
				
			||||||
            "content: can't prep for subsequent dev" % (marker, changes_path))
 | 
					 | 
				
			||||||
    next_verline = "%s %s%s" % (marker.rsplit(None, 1)[0], next_version, nyr)
 | 
					 | 
				
			||||||
    changes_txt = changes_txt.replace(marker + '\n',
 | 
					 | 
				
			||||||
        "%s\n\n(nothing yet)\n\n\n%s\n" % (next_verline, marker))
 | 
					 | 
				
			||||||
    if not dry_run:
 | 
					 | 
				
			||||||
        f = codecs.open(changes_path, 'w', 'utf-8')
 | 
					 | 
				
			||||||
        f.write(changes_txt)
 | 
					 | 
				
			||||||
        f.close()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # - update version file
 | 
					 | 
				
			||||||
    next_version_tuple = _tuple_from_version(next_version)
 | 
					 | 
				
			||||||
    for i, ver_file in enumerate(version_files):
 | 
					 | 
				
			||||||
        ver_content = codecs.open(ver_file, 'r', 'utf-8').read()
 | 
					 | 
				
			||||||
        ver_file_type, ver_info = parsed_version_files[i]
 | 
					 | 
				
			||||||
        if ver_file_type == "json":
 | 
					 | 
				
			||||||
            marker = '"version": "%s"' % version
 | 
					 | 
				
			||||||
            if marker not in ver_content:
 | 
					 | 
				
			||||||
                raise Error("couldn't find `%s' version marker in `%s' "
 | 
					 | 
				
			||||||
                    "content: can't prep for subsequent dev" % (marker, ver_file))
 | 
					 | 
				
			||||||
            ver_content = ver_content.replace(marker,
 | 
					 | 
				
			||||||
                '"version": "%s"' % next_version)
 | 
					 | 
				
			||||||
        elif ver_file_type == "javascript":
 | 
					 | 
				
			||||||
            candidates = [
 | 
					 | 
				
			||||||
                ("single", "var VERSION = '%s';" % version),
 | 
					 | 
				
			||||||
                ("double", 'var VERSION = "%s";' % version),
 | 
					 | 
				
			||||||
            ]
 | 
					 | 
				
			||||||
            for quote_type, marker in candidates:
 | 
					 | 
				
			||||||
                if marker in ver_content:
 | 
					 | 
				
			||||||
                    break
 | 
					 | 
				
			||||||
            else:
 | 
					 | 
				
			||||||
                raise Error("couldn't find any candidate version marker in "
 | 
					 | 
				
			||||||
                    "`%s' content: can't prep for subsequent dev: %r"
 | 
					 | 
				
			||||||
                    % (ver_file, candidates))
 | 
					 | 
				
			||||||
            if quote_type == "single":
 | 
					 | 
				
			||||||
                ver_content = ver_content.replace(marker,
 | 
					 | 
				
			||||||
                    "var VERSION = '%s';" % next_version)
 | 
					 | 
				
			||||||
            else:
 | 
					 | 
				
			||||||
                ver_content = ver_content.replace(marker,
 | 
					 | 
				
			||||||
                    'var VERSION = "%s";' % next_version)
 | 
					 | 
				
			||||||
        elif ver_file_type == "python":
 | 
					 | 
				
			||||||
            marker = "__version_info__ = %r" % (version_info,)
 | 
					 | 
				
			||||||
            if marker not in ver_content:
 | 
					 | 
				
			||||||
                raise Error("couldn't find `%s' version marker in `%s' "
 | 
					 | 
				
			||||||
                    "content: can't prep for subsequent dev" % (marker, ver_file))
 | 
					 | 
				
			||||||
            ver_content = ver_content.replace(marker,
 | 
					 | 
				
			||||||
                "__version_info__ = %r" % (next_version_tuple,))
 | 
					 | 
				
			||||||
        elif ver_file_type == "version":
 | 
					 | 
				
			||||||
            ver_content = next_version
 | 
					 | 
				
			||||||
        else:
 | 
					 | 
				
			||||||
            raise Error("unknown ver_file_type: %r" % ver_file_type)
 | 
					 | 
				
			||||||
        if not dry_run:
 | 
					 | 
				
			||||||
            log.info("update version to '%s' in '%s'", next_version, ver_file)
 | 
					 | 
				
			||||||
            f = codecs.open(ver_file, 'w', 'utf-8')
 | 
					 | 
				
			||||||
            f.write(ver_content)
 | 
					 | 
				
			||||||
            f.close()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if not dry_run:
 | 
					 | 
				
			||||||
        run('git commit %s %s -m "bumpver for subsequent work"' % (
 | 
					 | 
				
			||||||
            changes_path, ' '.join(version_files)))
 | 
					 | 
				
			||||||
        run('git push')
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#---- internal support routines
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def _indent(s, indent='    '):
 | 
					 | 
				
			||||||
    return indent + indent.join(s.splitlines(True))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def _tuple_from_version(version):
 | 
					 | 
				
			||||||
    def _intify(s):
 | 
					 | 
				
			||||||
        try:
 | 
					 | 
				
			||||||
            return int(s)
 | 
					 | 
				
			||||||
        except ValueError:
 | 
					 | 
				
			||||||
            return s
 | 
					 | 
				
			||||||
    return tuple(_intify(b) for b in version.split('.'))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def _get_next_version_info(version_info):
 | 
					 | 
				
			||||||
    next = list(version_info[:])
 | 
					 | 
				
			||||||
    next[-1] += 1
 | 
					 | 
				
			||||||
    return tuple(next)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def _version_from_version_info(version_info):
 | 
					 | 
				
			||||||
    v = str(version_info[0])
 | 
					 | 
				
			||||||
    state_dot_join = True
 | 
					 | 
				
			||||||
    for i in version_info[1:]:
 | 
					 | 
				
			||||||
        if state_dot_join:
 | 
					 | 
				
			||||||
            try:
 | 
					 | 
				
			||||||
                int(i)
 | 
					 | 
				
			||||||
            except ValueError:
 | 
					 | 
				
			||||||
                state_dot_join = False
 | 
					 | 
				
			||||||
            else:
 | 
					 | 
				
			||||||
                pass
 | 
					 | 
				
			||||||
        if state_dot_join:
 | 
					 | 
				
			||||||
            v += "." + str(i)
 | 
					 | 
				
			||||||
        else:
 | 
					 | 
				
			||||||
            v += str(i)
 | 
					 | 
				
			||||||
    return v
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
_version_re = re.compile(r"^(\d+)\.(\d+)(?:\.(\d+)([abc](\d+)?)?)?$")
 | 
					 | 
				
			||||||
def _version_info_from_version(version):
 | 
					 | 
				
			||||||
    m = _version_re.match(version)
 | 
					 | 
				
			||||||
    if not m:
 | 
					 | 
				
			||||||
        raise Error("could not convert '%s' version to version info" % version)
 | 
					 | 
				
			||||||
    version_info = []
 | 
					 | 
				
			||||||
    for g in m.groups():
 | 
					 | 
				
			||||||
        if g is None:
 | 
					 | 
				
			||||||
            break
 | 
					 | 
				
			||||||
        try:
 | 
					 | 
				
			||||||
            version_info.append(int(g))
 | 
					 | 
				
			||||||
        except ValueError:
 | 
					 | 
				
			||||||
            version_info.append(g)
 | 
					 | 
				
			||||||
    return tuple(version_info)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def _parse_version_file(version_file):
 | 
					 | 
				
			||||||
    """Get version info from the given file. It can be any of:
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    Supported version file types (i.e. types of files from which we know
 | 
					 | 
				
			||||||
    how to parse the version string/number -- often by some convention):
 | 
					 | 
				
			||||||
    - json: use the "version" key
 | 
					 | 
				
			||||||
    - javascript: look for a `var VERSION = "1.2.3";` or
 | 
					 | 
				
			||||||
      `var VERSION = '1.2.3';`
 | 
					 | 
				
			||||||
    - python: Python script/module with `__version_info__ = (1, 2, 3)`
 | 
					 | 
				
			||||||
    - version: a VERSION.txt or VERSION file where the whole contents are
 | 
					 | 
				
			||||||
      the version string
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @param version_file {str} Can be a path or "type:path", where "type"
 | 
					 | 
				
			||||||
        is one of the supported types.
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
    # Get version file *type*.
 | 
					 | 
				
			||||||
    version_file_type = None
 | 
					 | 
				
			||||||
    match = re.compile("^([a-z]+):(.*)$").search(version_file)
 | 
					 | 
				
			||||||
    if match:
 | 
					 | 
				
			||||||
        version_file = match.group(2)
 | 
					 | 
				
			||||||
        version_file_type = match.group(1)
 | 
					 | 
				
			||||||
        aliases = {
 | 
					 | 
				
			||||||
            "js": "javascript"
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        if version_file_type in aliases:
 | 
					 | 
				
			||||||
            version_file_type = aliases[version_file_type]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    f = codecs.open(version_file, 'r', 'utf-8')
 | 
					 | 
				
			||||||
    content = f.read()
 | 
					 | 
				
			||||||
    f.close()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if not version_file_type:
 | 
					 | 
				
			||||||
        # Guess the type.
 | 
					 | 
				
			||||||
        base = basename(version_file)
 | 
					 | 
				
			||||||
        ext = splitext(base)[1]
 | 
					 | 
				
			||||||
        if ext == ".json":
 | 
					 | 
				
			||||||
            version_file_type = "json"
 | 
					 | 
				
			||||||
        elif ext == ".py":
 | 
					 | 
				
			||||||
            version_file_type = "python"
 | 
					 | 
				
			||||||
        elif ext == ".js":
 | 
					 | 
				
			||||||
            version_file_type = "javascript"
 | 
					 | 
				
			||||||
        elif content.startswith("#!"):
 | 
					 | 
				
			||||||
            shebang = content.splitlines(False)[0]
 | 
					 | 
				
			||||||
            shebang_bits = re.split(r'[/ \t]', shebang)
 | 
					 | 
				
			||||||
            for name, typ in {"python": "python", "node": "javascript"}.items():
 | 
					 | 
				
			||||||
                if name in shebang_bits:
 | 
					 | 
				
			||||||
                    version_file_type = typ
 | 
					 | 
				
			||||||
                    break
 | 
					 | 
				
			||||||
        elif base in ("VERSION", "VERSION.txt"):
 | 
					 | 
				
			||||||
            version_file_type = "version"
 | 
					 | 
				
			||||||
    if not version_file_type:
 | 
					 | 
				
			||||||
        raise RuntimeError("can't extract version from '%s': no idea "
 | 
					 | 
				
			||||||
            "what type of file it it" % version_file)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if version_file_type == "json":
 | 
					 | 
				
			||||||
        obj = json.loads(content)
 | 
					 | 
				
			||||||
        version_info = _version_info_from_version(obj["version"])
 | 
					 | 
				
			||||||
    elif version_file_type == "python":
 | 
					 | 
				
			||||||
        m = re.search(r'^__version_info__ = (.*?)$', content, re.M)
 | 
					 | 
				
			||||||
        version_info = eval(m.group(1))
 | 
					 | 
				
			||||||
    elif version_file_type == "javascript":
 | 
					 | 
				
			||||||
        m = re.search(r'^var VERSION = (\'|")(.*?)\1;$', content, re.M)
 | 
					 | 
				
			||||||
        version_info = _version_info_from_version(m.group(2))
 | 
					 | 
				
			||||||
    elif version_file_type == "version":
 | 
					 | 
				
			||||||
        version_info = _version_info_from_version(content.strip())
 | 
					 | 
				
			||||||
    else:
 | 
					 | 
				
			||||||
        raise RuntimeError("unexpected version_file_type: %r"
 | 
					 | 
				
			||||||
            % version_file_type)
 | 
					 | 
				
			||||||
    return version_file_type, version_info
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def parse_changelog(changes_path):
 | 
					 | 
				
			||||||
    """Parse the given changelog path and return `(content, parsed, nyr)`
 | 
					 | 
				
			||||||
    where `nyr` is the ' (not yet released)' marker and `parsed` looks like:
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        [{'body': u'\n(nothing yet)\n\n',
 | 
					 | 
				
			||||||
          'verline': u'restify 1.0.1 (not yet released)',
 | 
					 | 
				
			||||||
          'version': u'1.0.1'},    # version is parsed out for top section only
 | 
					 | 
				
			||||||
         {'body': u'...',
 | 
					 | 
				
			||||||
          'verline': u'1.0.0'},
 | 
					 | 
				
			||||||
         {'body': u'...',
 | 
					 | 
				
			||||||
          'verline': u'1.0.0-rc2'},
 | 
					 | 
				
			||||||
         {'body': u'...',
 | 
					 | 
				
			||||||
          'verline': u'1.0.0-rc1'}]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    A changelog (CHANGES.md) is expected to look like this:
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # $project Changelog
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        ## $next_version (not yet released)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        ...
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        ## $version1
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        ...
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        ## $version2
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        ... and so on
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    The version lines are enforced as follows:
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    - The top entry should have a " (not yet released)" suffix. "Should"
 | 
					 | 
				
			||||||
      because recovery from half-cutarelease failures is supported.
 | 
					 | 
				
			||||||
    - A version string must be extractable from there, but it tries to
 | 
					 | 
				
			||||||
      be loose (though strict "X.Y.Z" versioning is preferred). Allowed
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            ## 1.0.0
 | 
					 | 
				
			||||||
            ## my project 1.0.1
 | 
					 | 
				
			||||||
            ## foo 1.2.3-rc2
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      Basically, (a) the " (not yet released)" is stripped, (b) the
 | 
					 | 
				
			||||||
      last token is the version, and (c) that version must start with
 | 
					 | 
				
			||||||
      a digit (sanity check).
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
    if not exists(changes_path):
 | 
					 | 
				
			||||||
        raise Error("changelog file '%s' not found" % changes_path)
 | 
					 | 
				
			||||||
    content = codecs.open(changes_path, 'r', 'utf-8').read()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    parser = re.compile(
 | 
					 | 
				
			||||||
        r'^##\s*(?P<verline>[^\n]*?)\s*$(?P<body>.*?)(?=^##|\Z)',
 | 
					 | 
				
			||||||
        re.M | re.S)
 | 
					 | 
				
			||||||
    sections = parser.findall(content)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # Sanity checks on changelog format.
 | 
					 | 
				
			||||||
    if not sections:
 | 
					 | 
				
			||||||
        template = "## 1.0.0 (not yet released)\n\n(nothing yet)\n"
 | 
					 | 
				
			||||||
        raise Error("changelog '%s' must have at least one section, "
 | 
					 | 
				
			||||||
            "suggestion:\n\n%s" % (changes_path, _indent(template)))
 | 
					 | 
				
			||||||
    first_section_verline = sections[0][0]
 | 
					 | 
				
			||||||
    nyr = ' (not yet released)'
 | 
					 | 
				
			||||||
    #if not first_section_verline.endswith(nyr):
 | 
					 | 
				
			||||||
    #    eg = "## %s%s" % (first_section_verline, nyr)
 | 
					 | 
				
			||||||
    #    raise Error("changelog '%s' top section must end with %r, "
 | 
					 | 
				
			||||||
    #        "naive e.g.: '%s'" % (changes_path, nyr, eg))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    items = []
 | 
					 | 
				
			||||||
    for i, section in enumerate(sections):
 | 
					 | 
				
			||||||
        item = {
 | 
					 | 
				
			||||||
            "verline": section[0],
 | 
					 | 
				
			||||||
            "body": section[1]
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        if i == 0:
 | 
					 | 
				
			||||||
            # We only bother to pull out 'version' for the top section.
 | 
					 | 
				
			||||||
            verline = section[0]
 | 
					 | 
				
			||||||
            if verline.endswith(nyr):
 | 
					 | 
				
			||||||
                verline = verline[0:-len(nyr)]
 | 
					 | 
				
			||||||
            version = verline.split()[-1]
 | 
					 | 
				
			||||||
            try:
 | 
					 | 
				
			||||||
                int(version[0])
 | 
					 | 
				
			||||||
            except ValueError:
 | 
					 | 
				
			||||||
                msg = ''
 | 
					 | 
				
			||||||
                if version.endswith(')'):
 | 
					 | 
				
			||||||
                    msg = " (cutarelease is picky about the trailing %r " \
 | 
					 | 
				
			||||||
                        "on the top version line. Perhaps you misspelled " \
 | 
					 | 
				
			||||||
                        "that?)" % nyr
 | 
					 | 
				
			||||||
                raise Error("changelog '%s' top section version '%s' is "
 | 
					 | 
				
			||||||
                    "invalid: first char isn't a number%s"
 | 
					 | 
				
			||||||
                    % (changes_path, version, msg))
 | 
					 | 
				
			||||||
            item["version"] = version
 | 
					 | 
				
			||||||
        items.append(item)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return content, items, nyr
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## {{{ http://code.activestate.com/recipes/577058/ (r2)
 | 
					 | 
				
			||||||
def query_yes_no(question, default="yes"):
 | 
					 | 
				
			||||||
    """Ask a yes/no question via raw_input() and return their answer.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    "question" is a string that is presented to the user.
 | 
					 | 
				
			||||||
    "default" is the presumed answer if the user just hits <Enter>.
 | 
					 | 
				
			||||||
        It must be "yes" (the default), "no" or None (meaning
 | 
					 | 
				
			||||||
        an answer is required of the user).
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    The "answer" return value is one of "yes" or "no".
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
    valid = {"yes":"yes",   "y":"yes",  "ye":"yes",
 | 
					 | 
				
			||||||
             "no":"no",     "n":"no"}
 | 
					 | 
				
			||||||
    if default == None:
 | 
					 | 
				
			||||||
        prompt = " [y/n] "
 | 
					 | 
				
			||||||
    elif default == "yes":
 | 
					 | 
				
			||||||
        prompt = " [Y/n] "
 | 
					 | 
				
			||||||
    elif default == "no":
 | 
					 | 
				
			||||||
        prompt = " [y/N] "
 | 
					 | 
				
			||||||
    else:
 | 
					 | 
				
			||||||
        raise ValueError("invalid default answer: '%s'" % default)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    while 1:
 | 
					 | 
				
			||||||
        sys.stdout.write(question + prompt)
 | 
					 | 
				
			||||||
        choice = raw_input().lower()
 | 
					 | 
				
			||||||
        if default is not None and choice == '':
 | 
					 | 
				
			||||||
            return default
 | 
					 | 
				
			||||||
        elif choice in valid.keys():
 | 
					 | 
				
			||||||
            return valid[choice]
 | 
					 | 
				
			||||||
        else:
 | 
					 | 
				
			||||||
            sys.stdout.write("Please respond with 'yes' or 'no' "\
 | 
					 | 
				
			||||||
                             "(or 'y' or 'n').\n")
 | 
					 | 
				
			||||||
## end of http://code.activestate.com/recipes/577058/ }}}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def _capture_stdout(argv):
 | 
					 | 
				
			||||||
    import subprocess
 | 
					 | 
				
			||||||
    p = subprocess.Popen(argv, stdout=subprocess.PIPE)
 | 
					 | 
				
			||||||
    return p.communicate()[0]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class _NoReflowFormatter(optparse.IndentedHelpFormatter):
 | 
					 | 
				
			||||||
    """An optparse formatter that does NOT reflow the description."""
 | 
					 | 
				
			||||||
    def format_description(self, description):
 | 
					 | 
				
			||||||
        return description or ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def run(cmd):
 | 
					 | 
				
			||||||
    """Run the given command.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    Raises OSError is the command returns a non-zero exit status.
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
    log.debug("running '%s'", cmd)
 | 
					 | 
				
			||||||
    fixed_cmd = cmd
 | 
					 | 
				
			||||||
    if sys.platform == "win32" and cmd.count('"') > 2:
 | 
					 | 
				
			||||||
        fixed_cmd = '"' + cmd + '"'
 | 
					 | 
				
			||||||
    retval = os.system(fixed_cmd)
 | 
					 | 
				
			||||||
    if hasattr(os, "WEXITSTATUS"):
 | 
					 | 
				
			||||||
        status = os.WEXITSTATUS(retval)
 | 
					 | 
				
			||||||
    else:
 | 
					 | 
				
			||||||
        status = retval
 | 
					 | 
				
			||||||
    if status:
 | 
					 | 
				
			||||||
        raise OSError(status, "error running '%s'" % cmd)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def _setup_command_prefix():
 | 
					 | 
				
			||||||
    prefix = ""
 | 
					 | 
				
			||||||
    if sys.platform == "darwin":
 | 
					 | 
				
			||||||
        # http://forums.macosxhints.com/archive/index.php/t-43243.html
 | 
					 | 
				
			||||||
        # This is an Apple customization to `tar` to avoid creating
 | 
					 | 
				
			||||||
        # '._foo' files for extended-attributes for archived files.
 | 
					 | 
				
			||||||
        prefix = "COPY_EXTENDED_ATTRIBUTES_DISABLE=1 "
 | 
					 | 
				
			||||||
    return prefix
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#---- mainline
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def main(argv):
 | 
					 | 
				
			||||||
    logging.basicConfig(format="%(name)s: %(levelname)s: %(message)s")
 | 
					 | 
				
			||||||
    log.setLevel(logging.INFO)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # Parse options.
 | 
					 | 
				
			||||||
    parser = optparse.OptionParser(prog="cutarelease", usage='',
 | 
					 | 
				
			||||||
        version="%prog " + __version__, description=__doc__,
 | 
					 | 
				
			||||||
        formatter=_NoReflowFormatter())
 | 
					 | 
				
			||||||
    parser.add_option("-v", "--verbose", dest="log_level",
 | 
					 | 
				
			||||||
        action="store_const", const=logging.DEBUG,
 | 
					 | 
				
			||||||
        help="more verbose output")
 | 
					 | 
				
			||||||
    parser.add_option("-q", "--quiet", dest="log_level",
 | 
					 | 
				
			||||||
        action="store_const", const=logging.WARNING,
 | 
					 | 
				
			||||||
        help="quieter output (just warnings and errors)")
 | 
					 | 
				
			||||||
    parser.set_default("log_level", logging.INFO)
 | 
					 | 
				
			||||||
    parser.add_option("--test", action="store_true",
 | 
					 | 
				
			||||||
        help="run self-test and exit (use 'eol.py -v --test' for verbose test output)")
 | 
					 | 
				
			||||||
    parser.add_option("-p", "--project-name", metavar="NAME",
 | 
					 | 
				
			||||||
        help='the name of this project (default is the base dir name)',
 | 
					 | 
				
			||||||
        default=basename(os.getcwd()))
 | 
					 | 
				
			||||||
    parser.add_option("-f", "--version-file", metavar="[TYPE:]PATH",
 | 
					 | 
				
			||||||
        action='append', dest="version_files",
 | 
					 | 
				
			||||||
        help='The path to the project file holding the version info. Can be '
 | 
					 | 
				
			||||||
             'specified multiple times if more than one file should be updated '
 | 
					 | 
				
			||||||
             'with new version info. If excluded, it will be guessed.')
 | 
					 | 
				
			||||||
    parser.add_option("-n", "--dry-run", action="store_true",
 | 
					 | 
				
			||||||
        help='Do a dry-run', default=False)
 | 
					 | 
				
			||||||
    opts, args = parser.parse_args()
 | 
					 | 
				
			||||||
    log.setLevel(opts.log_level)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    cutarelease(opts.project_name, opts.version_files, dry_run=opts.dry_run)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## {{{ http://code.activestate.com/recipes/577258/ (r5+)
 | 
					 | 
				
			||||||
if __name__ == "__main__":
 | 
					 | 
				
			||||||
    try:
 | 
					 | 
				
			||||||
        retval = main(sys.argv)
 | 
					 | 
				
			||||||
    except KeyboardInterrupt:
 | 
					 | 
				
			||||||
        sys.exit(1)
 | 
					 | 
				
			||||||
    except SystemExit:
 | 
					 | 
				
			||||||
        raise
 | 
					 | 
				
			||||||
    except:
 | 
					 | 
				
			||||||
        import traceback, logging
 | 
					 | 
				
			||||||
        if not log.handlers and not logging.root.handlers:
 | 
					 | 
				
			||||||
            logging.basicConfig()
 | 
					 | 
				
			||||||
        skip_it = False
 | 
					 | 
				
			||||||
        exc_info = sys.exc_info()
 | 
					 | 
				
			||||||
        if hasattr(exc_info[0], "__name__"):
 | 
					 | 
				
			||||||
            exc_class, exc, tb = exc_info
 | 
					 | 
				
			||||||
            if isinstance(exc, IOError) and exc.args[0] == 32:
 | 
					 | 
				
			||||||
                # Skip 'IOError: [Errno 32] Broken pipe': often a cancelling of `less`.
 | 
					 | 
				
			||||||
                skip_it = True
 | 
					 | 
				
			||||||
            if not skip_it:
 | 
					 | 
				
			||||||
                tb_path, tb_lineno, tb_func = traceback.extract_tb(tb)[-1][:3]
 | 
					 | 
				
			||||||
                log.error("%s (%s:%s in %s)", exc_info[1], tb_path,
 | 
					 | 
				
			||||||
                    tb_lineno, tb_func)
 | 
					 | 
				
			||||||
        else:  # string exception
 | 
					 | 
				
			||||||
            log.error(exc_info[0])
 | 
					 | 
				
			||||||
        if not skip_it:
 | 
					 | 
				
			||||||
            if log.isEnabledFor(logging.DEBUG):
 | 
					 | 
				
			||||||
                traceback.print_exception(*exc_info)
 | 
					 | 
				
			||||||
            sys.exit(1)
 | 
					 | 
				
			||||||
    else:
 | 
					 | 
				
			||||||
        sys.exit(retval)
 | 
					 | 
				
			||||||
## end of http://code.activestate.com/recipes/577258/ }}}
 | 
					 | 
				
			||||||
		Reference in New Issue
	
	Block a user