Permalink
Browse files

Initial commit

  • Loading branch information...
0 parents commit 53a8c4837cd79993a6aa176a629a50ea689b7ce3 Josh Braegger committed Mar 23, 2012
Showing with 373 additions and 0 deletions.
  1. +3 −0 .gitignore
  2. +3 −0 AUTHORS
  3. +1 −0 INSTALL
  4. +27 −0 LICENSE
  5. +8 −0 MANIFEST.in
  6. +1 −0 README
  7. +59 −0 bin/git-crucible
  8. +1 −0 crucible/__init__.py
  9. +108 −0 crucible/commands.py
  10. +131 −0 crucible/crucible.py
  11. +31 −0 setup.py
@@ -0,0 +1,3 @@
+*.pyc
+dist
+build
@@ -0,0 +1,3 @@
+Only one contributor so far:
+
+Josh Braegger <rckclmbr@gmail.com>
@@ -0,0 +1 @@
+todo
27 LICENSE
@@ -0,0 +1,27 @@
+Copyright (c) git-crucible developers.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+ 1. Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+ 3. Neither the name of Scrapy nor the names of its contributors may be used
+ to endorse or promote products derived from this software without
+ specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,8 @@
+include README
+include AUTHORS
+include INSTALL
+include LICENSE
+include MANIFEST.in
+recursive-include crucible *
+recursive-include docs *
+recursive-include bin *
1 README
@@ -0,0 +1 @@
+This is git-crucible, a command-line tool for submitting code review patches to Crucible.
@@ -0,0 +1,59 @@
+#!/usr/bin/env python
+
+from crucible.crucible import API
+from crucible import commands
+import argparse
+import ConfigParser
+import os
+import sys
+
+def parse_args(commands):
+ defaults = dict()
+ config_file = os.path.expanduser("~/.git_crucible.conf")
+ if os.path.isfile(config_file):
+ config = ConfigParser.SafeConfigParser()
+ config.read([config_file])
+ defaults.update(config.items("crucible"))
+
+ parser = argparse.ArgumentParser(description="""Submit code reviews to Crucible.""",
+ formatter_class=argparse.RawTextHelpFormatter,
+ epilog="""
+
+Config file:
+ All config options can also be specified in the file ~/.git_crucible.conf.
+ For example:
+
+ [crucible]
+ host=http://crucible.backcountry.com/crucible
+ username=jbraegger
+ password=test
+ reviewers=nbrunson,aglemann
+ repository=atg-backcountry-ca
+ project=CR
+
+ If you have the above in your config, the rest is easy.
+
+Examples:
+ See specific command help for examples (e.g. git-crucible create-review -h)
+ """)
+
+ parser.set_defaults(**defaults)
+ parser.add_argument("--host", help="base url of Crucible")
+ parser.add_argument("--username", help="Crucible username")
+ parser.add_argument("--password", help="Crucible password")
+ parser.add_argument("--verbose", action="store_true", default=False, help="Print full server requests and responses")
+ parser.add_argument("--debug", action="store_true", default=False, help="Don't actually send any requests")
+
+ subparsers = parser.add_subparsers()
+ for command in commands:
+ subparser = subparsers.add_parser(command.command, help=command.help, epilog=command.epilog,
+ formatter_class=argparse.RawTextHelpFormatter)
+ command.config_parser(subparser)
+ subparser.set_defaults(cls=command)
+
+ return vars(parser.parse_args())
+
+if __name__ == "__main__":
+ args = parse_args([commands.CreateReview, commands.AddPatch]);
+ api = API(args["host"], args["username"], args["password"], verbose=args["verbose"], debug=args["debug"])
+ args.get("cls").run(api, args)
@@ -0,0 +1 @@
+__version__ = "0.1"
@@ -0,0 +1,108 @@
+import argparse
+
+
+def get_config_values(opts):
+ def _prompt(description, default):
+ res = raw_input(description)
+ if not res:
+ return default
+ def _get_config_value(opts, key, description, default=None, allowEmpty=False, echo=True):
+ if not opts[key]:
+ if not opts["INFILE"].name == "<stdin>":
+ while not opts[key]:
+ opts[key] = _prompt(description, default)
+ if opts[key] or allowEmpty:
+ break
+ elif allowEmpty:
+ return default
+ else:
+ raise Exception("'%s' is required" % key)
+
+ return opts[key]
+ opts['host'] = _get_config_value(opts, 'host', 'Crucible base url:')
+ if not opts['host'].endswith('/'):
+ opts['host'] += '/'
+ opts['username'] = _get_config_value(opts, 'username', 'username: ')
+ opts['password'] = _get_config_value(opts, 'password', 'password: ', allowEmpty=True, echo=False)
+ opts['reviewers'] = _get_config_value(opts, 'reviewers', 'reviewers (comma separated, e.g. "fred,joe,matt"): ', allowEmpty=True)
+ opts['project'] = _get_config_value(opts, 'project', 'project (default is "CR"): ', default='CR')
+ opts['repository'] = _get_config_value(opts, 'repository', 'Git Repository: ', allowEmpty=True)
+ opts['title'] = _get_config_value(opts, 'title', 'Title (default is ""): ')
+ opts['jira_issue'] = _get_config_value(opts, 'jira_issue', 'JIRA Issue: ', allowEmpty=True)
+ opts['description'] = _get_config_value(opts, 'description', 'Description (default is ""): ', allowEmpty=True, default="")
+ return opts
+
+def get_patch_text(infile):
+ """ Validates patch exists and returns string """
+ return "".join(infile.readlines())
+
+
+class CreateReview(object):
+
+ command = "create-review"
+ help = "Creates a review. See create-review -h for more info"
+ epilog = """
+Examples:
+ git diff origin/deploy.. | git-crucible create-review \\
+ --jira_issue "FSD-1125" \\
+ --title "Automate XML Import -"
+
+ git-crucible create-review --patch_file test.diff \\
+ --jira_issue "FSD-1125" \\
+ --title "Automate XML Import" \\
+ --description "Some long description here \\
+ test.diff
+
+ cat test.diff | git-crucible create-review \\
+ --jira_issue "FSD-1125" \\
+ --title "Automate XML Import -"
+ """
+
+ @staticmethod
+ def config_parser(parser):
+ parser.add_argument('INFILE', type=argparse.FileType('r'),
+ help="A patch-file to add. Note when using stdin, you must specify all parameters"\
+ " on the command-line")
+ parser.add_argument("--title", help="Title of the post")
+ parser.add_argument("--project", help="Crucible project")
+ parser.add_argument("--repository", help="Crucible git repository the commit is in")
+ parser.add_argument("--reviewers", help="Reviewers (comma-separated, e.g. \"fred,joe,matt\")")
+ parser.add_argument("--jira_issue", help="The JIRA issue (e.g. JIRA-1234) (optional)")
+ parser.add_argument("--moderator", help="Moderator of the review (optional)")
+ parser.add_argument("--description", help="Description or statement of this review", default="")
+
+ @staticmethod
+ def run(api, args):
+ args = get_config_values(args)
+ args["patch_text"] = get_patch_text(args["INFILE"])
+
+ permaid = api.create_review(*[args[i] for i in ("title", "description", "project", "jira_issue", "moderator")])
+ print "Created", api.review_url(permaid)
+ if args["patch_text"]:
+ print "Adding patch to review"
+ api.add_patch(permaid, args["patch_text"], args["repository"])
+ if args["reviewers"]:
+ print "Adding reviewers to review"
+ api.add_reviewers(permaid, args["reviewers"])
+
+class AddPatch(object):
+
+ command = "add-patch"
+ help = "Adds a patch to a review. See add-patch -h for more info"
+ epilog = """
+ """
+
+ @staticmethod
+ def config_parser(parser):
+ parser.add_argument("CODEREVIEW", help="Which code review to add the patch to (e.g. CR-123)")
+ parser.add_argument('INFILE', type=argparse.FileType('r'),
+ help="A patch-file to add. Note when using stdin, you must specify all parameters"\
+ " on the command-line")
+ parser.add_argument("--repository", help="Crucible git repository the commit is in")
+
+ @staticmethod
+ def run(api, args):
+ args = get_config_values(args)
+ patch_text = get_patch_text(args["INFILE"])
+ api.add_patch(args["permaid"], patch_text, args["repository"])
+ print "Added patch to %s" % args["permaid"]
@@ -0,0 +1,131 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+'''
+Submits diffs to Crucible.
+'''
+
+
+import os
+import sys
+import tempfile
+from urlparse import urljoin
+import xml.etree.ElementTree as ElementTree
+from xml.sax.saxutils import escape
+import cStringIO
+from restkit import request, BasicAuth
+
+CREATE_REVIEW_URL = 'rest-service/reviews-v1'
+ADD_PATCH_URL = 'rest-service/reviews-v1/%s/patch'
+ADD_REVIEWERS_URL = 'rest-service/reviews-v1/%s/reviewers'
+APPROVE_URL = 'rest-service/reviews-v1/%s/transition?action=action:approveReview'
+
+CREATE_REVIEW_XML_TEMPLATE = \
+"""<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<createReview>
+ <reviewData>
+ <allowReviewersToJoin>true</allowReviewersToJoin>
+ <author>
+ <userName>%s</userName>
+ </author>
+ <creator>
+ <userName>%s</userName>
+ </creator>
+ <description>%s</description>
+ %s
+ <moderator>
+ <userName>%s</userName>
+ </moderator>
+ <name>%s</name>
+ <projectKey>%s</projectKey>
+ <state>Review</state>
+ <type>REVIEW</type>
+ </reviewData>
+</createReview>
+"""
+
+ADD_PATCH_XML_TEMPLATE = \
+'''<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<addPatch>
+ <patch>%s</patch>
+ <anchor>
+ <anchorPath></anchorPath>
+ <anchorRepository>%s</anchorRepository>
+ <stripCount>1</stripCount>
+ </anchor>
+</addPatch>
+'''
+
+def _successful(response):
+ return (response.status_int // 100) is 2
+
+
+class API(object):
+
+ def __init__(self, host, username, password, verbose=False, debug=False):
+ self.username = username
+ self.password = password
+ self.host = host
+ self.verbose = verbose
+ self.debug = debug
+
+ def _post(self, url, body, error_message):
+ url = urljoin(self.host, url)
+
+ if self.verbose:
+ print "URL: %s" % url
+ print "Request Body:\n%s" % body
+ if self.debug:
+ return
+
+ resp = request(url,
+ method='POST',
+ body=body,
+ filters=[BasicAuth(self.username, self.password)],
+ headers={'Content-Type': 'application/xml',
+ 'Accept': 'application/xml'},
+ follow_redirect=True)
+ if not _successful(resp):
+ xml = resp.body_string()
+ if self.verbose:
+ print xml
+ message = ElementTree.fromstring(xml).findtext('.//message')
+ raise Exception(error_message+": "+message)
+ return resp
+
+ def add_patch(self, permaid, patch, repository):
+ """Add one changeset to the review as a patch."""
+ body = ADD_PATCH_XML_TEMPLATE % (escape(patch), escape(repository))
+ self._post(ADD_PATCH_URL % permaid, body, "Unable to add patch")
+
+ def add_reviewers(self, permaid, reviewers):
+ """Add reviewers to the review."""
+ self._post(ADD_REVIEWERS_URL % permaid, reviewers,
+ "Unable to add reviewers '%s' to review" % reviewers)
+
+ def create_review(self, title, description, project, jira_issue=None, moderator=None):
+ """
+ Creates a new review
+ :return the new review's permaId string.
+ """
+ moderator = moderator if moderator else self.username
+ jira_issue = "" if not jira_issue else "<jiraIssueKey>%s</jiraIssueKey>" % escape(jira_issue),
+
+ body = CREATE_REVIEW_XML_TEMPLATE % \
+ (escape(self.username), escape(self.username), escape(description), jira_issue,
+ escape(moderator), escape(title[:255]), escape(project))
+ resp = self._post(CREATE_REVIEW_URL, body, "Unable to create new review")
+ if self.debug:
+ return "CR-123"
+ xml = resp.body_string()
+ return ElementTree.XML(xml).findtext('.//permaId/id')
+
+ def open_review(permaid):
+ '''Takes the specified review from Draft to Under Review state. Not yet 100%.'''
+ raise NotImplementedError()
+ self._post(APPROVE_URL % permaid)
+
+ def review_url(self, permaid):
+ """Returns the crucible url of the review"""
+ return urljoin(self.host, "cru/"+permaid)
+
@@ -0,0 +1,31 @@
+from distutils.core import setup
+
+args = {
+ 'name': 'git-crucible',
+ 'version': __import__("crucible").__version__,
+ 'url' : "http://github.com/rckclmbr/git-crucible",
+ 'description': "Git extension that creates reviews in Crucible straight from the command line.",
+ 'long_description': """Git extension that creates reviews in Crucible straight from the command line.""",
+ 'author': 'Josh Braegger',
+ 'author_email': 'rckclmbr@gmail.com',
+ 'maintainer': 'Josh Braegger',
+ 'maintainer_email': 'rckclmbr@gmail.com',
+ 'license': 'BSD',
+ 'packages': ["crucible"],
+ 'scripts': ["bin/git-crucible"],
+ 'install_requires': ['restkit'],
+ 'classifiers': [
+ 'Programming Language :: Python',
+ 'Programming Language :: Python :: 2.6',
+ 'Programming Language :: Python :: 2.7',
+ 'License :: OSI Approved :: BSD License',
+ 'Operating System :: OS Independent',
+ 'Development Status :: 3 - Alpha',
+ 'Intended Audience :: Developers',
+ 'Environment :: Console',
+ 'Topic :: Software Development :: Libraries :: Application Frameworks',
+ 'Topic :: Software Development :: Libraries :: Python Modules',
+ ]
+}
+
+setup(**args)

0 comments on commit 53a8c48

Please sign in to comment.