pax_global_header00006660000000000000000000000064121507453530014517gustar00rootroot0000000000000052 comment=ae9fc592d7e35a6fc7e77ed3d84f32ebca5939c2 cligh-0.2/000077500000000000000000000000001215074535300124465ustar00rootroot00000000000000cligh-0.2/.gitignore000066400000000000000000000000061215074535300144320ustar00rootroot00000000000000*.pyc cligh-0.2/AUTHORS000066400000000000000000000001421215074535300135130ustar00rootroot00000000000000The following individuals have contributed to this project: * Christopher Brannon * William Hubbs cligh-0.2/LICENSE000066400000000000000000000027631215074535300134630ustar00rootroot00000000000000Copyright (c) 2010 Christopher M. Brannon 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 cligh 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. cligh-0.2/README.md000066400000000000000000000040711215074535300137270ustar00rootroot00000000000000# cligh - Command-line Interface to GitHub This is a simple command-line interface to the facilities of GitHub. It is written by Christopher Brannon . The current version is 0.2. This program is still in the early stage of development. It is by no means feature-complete. A friend and I consider it useful, but others may not. ## Obtaining Obtain the current stable version using this link: . You can verify the integrity of the file using the [gpg signature](http://the-brannons.com/software/cligh-0.2.tar.gz.sig). ## Installation If you are using a version of Python prior to 2.7, you first need to install the argparse module. Get it from [here](http://argparse.googlecode.com/). Python version 2.7 includes argparse in its standard library. cligh also requires the PyGithub package. The homepage for PyGithub is [https://github.com/jacquev6/PyGithub](https://github.com/jacquev6/PyGithub). The final dependency is PyXDG. Get it from [http://freedesktop.org/Software/pyxdg](http://freedesktop.org/Software/pyxdg). Once the dependencies are installed, type `./setup.py` in the cligh source directory, in order to install the script. ## Setup Run the following command to configure cligh: cligh configure The program will prompt you for a username and password. It then creates an authorization using the Github API. cligh never stores your password. Instead, it stores the token associated with the authorization that it created. It uses this token to authenticate to Github. ## Usage Usage is straightforward, and the program provides informative help messages. Simply type `cligh -h` at a shell prompt, to see the introductory help message. ## Development The source for this project is managed by git. If you wish to contribute, or if you simply wish to use the unpublished "development" sources, clone it using a command such as the following: git clone git://github.com/CMB/cligh.git Patches are most appreciated. Please send them to [chris@the-brannons.com](mailto:chris@the-brannons.com). cligh-0.2/bin/000077500000000000000000000000001215074535300132165ustar00rootroot00000000000000cligh-0.2/bin/cligh000077500000000000000000000015651215074535300142410ustar00rootroot00000000000000#!/usr/bin/python """Simple command-line interface to github.""" # See the file LICENSE for copyright and license info. import argparse from github import Github from cligh import config from cligh.collaborators import make_collab_parser from cligh.issues import make_issue_parser from cligh.repos import make_repo_parser # Option parsing. def make_argument_parser(): parser = argparse.ArgumentParser() subparsers = parser.add_subparsers(title='Subcommands') config.make_configcmd_parser(subparsers) make_collab_parser(subparsers) make_issue_parser(subparsers) make_repo_parser(subparsers) return parser def main(): parser = make_argument_parser() args = parser.parse_args() if args.func == config.do_configcmd: config.do_configcmd() else: config.read_config_file() client = Github(config.get_token()) args.func(client, args) if __name__ == '__main__': main() cligh-0.2/cligh/000077500000000000000000000000001215074535300135345ustar00rootroot00000000000000cligh-0.2/cligh/__init__.py000066400000000000000000000000001215074535300156330ustar00rootroot00000000000000cligh-0.2/cligh/collaborators.py000066400000000000000000000036461215074535300167650ustar00rootroot00000000000000#!/usr/bin/python # Commands for managing collaborators. from . import utils def add(client, args): """Add a collaborator to a repo.""" repository = utils.get_working_repo(client, args.repository) collaborator = utils.get_named_user(client, args.user) repository.add_to_collaborators(collaborator) print 'Collaborator added.' def remove(client, args): """Remove a collaborator from a repo.""" repository = utils.get_working_repo(client, args.repository) collaborator = utils.get_named_user(client, args.user) repository.remove_from_collaborators(collaborator) print 'Collaborator removed.' def do_list(client, args): """List a repository's collaborators.""" repository = utils.get_working_repo(client, args.repository) collaborators = repository.get_collaborators() if not collaborators: print 'There are no collaborators for %s.' % repository.full_name else: print 'The following people are collaborating on %s:' % \ repository.full_name for collaborator in collaborators: print collaborator.login def make_collab_parser(subparsers): collab = subparsers.add_parser('collab', help='Manage collaborators.') subparsers = collab.add_subparsers(title='Collaborator-related Subcommands') collab_list = subparsers.add_parser('list', help='List collaborators for a given repository.') collab_list.set_defaults(func=do_list) collab_list.add_argument('--repository', help='Name of the repository.') collab_add = subparsers.add_parser('add', help='Add a collaborator to a repository.') collab_add.set_defaults(func=add) collab_add.add_argument('user', help='Name of the user.') collab_add.add_argument('--repository', help='Name of the repository.') collab_remove = subparsers.add_parser('remove', help='Remove a collaborator from a repository.') collab_remove.set_defaults(func=remove) collab_remove.add_argument('user', help='Name of the user.') collab_remove.add_argument('--repository', help='Name of the repository.') cligh-0.2/cligh/config.py000066400000000000000000000052571215074535300153640ustar00rootroot00000000000000import ConfigParser import getpass import os from github import Github from xdg import BaseDirectory from . import utils USERNAME = None TOKEN = None def get_username(): global USERNAME return USERNAME def get_token(): global TOKEN return TOKEN def get_config_dir(): """Return the name of the directory containing the application's config file.""" config_dir = BaseDirectory.load_first_config('cligh') if config_dir is None: config_dir = BaseDirectory.save_config_path('cligh') return config_dir def get_config_filename(): """Get the absolute pathname of the config file.""" config_dir = get_config_dir() return os.path.join(config_dir, 'cligh.conf') def read_config_file(): global USERNAME, TOKEN config_parser = ConfigParser.ConfigParser() config_filename = get_config_filename() try: with open(config_filename, 'r') as f: config_parser.readfp(f) except ConfigParser.Error as e: utils.die("""The following error was encountered while attempting to parse the configuration file. %s This may indicate a mal-formed configuration file. Recreate the file by invoking cligh configure """ % str(e)) except IOError as e: utils.die("""The following error occurred while trying to open the configuration file. %s. If you have not already done so, create the configuration file using cligh configure at your shell prompt. """ % str(e)) try: USERNAME = config_parser.get('credentials', 'username') TOKEN = config_parser.get('credentials', 'token') except ConfigParser.Error as e: utils.die("""The config file is missing one or more expected options. You should probably recreate it using these two commands: rm %s cligh configure """ % config_filename) def do_configcmd(): """Create an oauth token. Write the username and token to the config file. Should be called the first time the application is executed.""" dummy_validator = lambda x : True username = utils.read_user_input('Username', dummy_validator) password = getpass.getpass('Password:') client = Github(username, password) user = client.get_user() authorization = user.create_authorization(scopes=['user', 'repo', 'gist', 'delete_repo'], note='cligh', note_url='https://github.com/CMB/cligh') config_parser = ConfigParser.ConfigParser() config_parser.add_section('credentials') config_parser.set('credentials', 'username', username) config_parser.set('credentials', 'token', authorization.token) os.umask(077) # Want permissions of 0600. with open(get_config_filename(), 'w') as f: config_parser.write(f) print 'cligh configured and authorized for use with github.' def make_configcmd_parser(subparsers): configcmd = subparsers.add_parser('configure', help='Configure cligh.') configcmd.set_defaults(func=do_configcmd) cligh-0.2/cligh/issues.py000066400000000000000000000140511215074535300154220ustar00rootroot00000000000000#!/usr/bin/python """Commands for managing and querying issues.""" from github import GithubException from cligh.utils import text_from_editor, get_working_repo, die # Helper functions: def get_working_issue(client, args): """Get an object corresponding to the issue that the user wishes to manipulate. Issues are identified by a repository name and issue number.""" issue = None repository = get_working_repo(client, args.repository) try: issue_number = int(args.number) except ValueError: die("""%s is not a valid issue number.""") try: issue = repository.get_issue(issue_number) except GithubException as e: die('Unable to fetch issue number %d for this repository: %s' % (args.number, e.data['message'])) return issue def print_enclosed_text(text): """Print some text, enclosed by horizontal lines.""" print '-' * 80 print text print '-' * 80 print def print_comment(comment): print 'Comment by %s on %s at %s' % (comment.user.login, comment.created_at.date(), comment.created_at.strftime('%H:%M:%S')) print_enclosed_text(comment.body) def do_open(client, args): """Create a new issue.""" repository = get_working_repo(client, args.repository) print 'Please enter the long description for this issue.' print 'Starting your text editor:' desc_text = text_from_editor() repository.create_issue(args.title, body=desc_text) def close(client, args): """Close an existing open issue.""" issue = get_working_issue(client, args) issue.edit(state='closed') def do_list(client, args): """Command to list the issues for a given repository.""" repository = get_working_repo(client, args.repository) status = args.status or 'open' issues = list(repository.get_issues(state=status)) if not issues: print '%s has no %s issues' % (repository.full_name, status) else: print '%s has the following %s issues' % (repository.full_name, status) print 'Issue# - Title' for issue in issues: print '%s - %s' % (issue.number, issue.title) def get(client, args): issue = get_working_issue(client, args) comments = issue.get_comments() print 'Issue #%d: %s' % (issue.number, issue.title) print 'Opened by %s on %s at %s' % (issue.user.login, issue.created_at.date(), issue.created_at.strftime('%H:%M:%S')) print 'Last updated on %s at %s' % (issue.updated_at.date(), issue.updated_at.strftime('%H:%M:%S')) if issue.closed_by and issue.closed_at: print "Closed by %s on %s at %s" % (issue.closed_by.login, issue.closed_at.date(), issue.closed_at.strftime('%H:%M:%S')) if issue.labels: print 'Labels:' for label in issue.labels: print '* %s' % label.name print 'Long description:' print_enclosed_text(issue.body) print 'Comments:' print for comment in comments: print_comment(comment) def comment(client, args): issue = get_working_issue(client, args) print 'Starting your text editor, so that you can compose your comment:' comment_text = text_from_editor() issue.create_comment(comment_text) def addlabel(client, args): issue = get_working_issue(client, args) repository = get_working_repo(client, args.repository) try: label = repository.get_label(args.label) except GithubException as e: if e.status == 404: die('''The label %s has not yet been added to this repository. First, add it using: cligh repo addlabel %s ''' % (args.label, args.label)) else: die('''Unable to find the label %s in this repository. Error message: %s ''' % (args.label, e.data['message'])) issue.add_to_labels(label) def remlabel(client, args): issue = get_working_issue(client, args) repository = get_working_repo(client, args.repository) try: label = repository.get_label(args.label) except GithubException as e: die('''Unable to find the label %s in this repository. It cannot be removed from the issue at this time. Error message: %s ''' % (args.label, e.data['message'])) issue.remove_from_labels(label) def make_issue_parser(subparsers): issue = subparsers.add_parser('issue', help='Manage and query issues.') subparsers = issue.add_subparsers(title='Issue-related subcommands.') issue_list = subparsers.add_parser('list', help='List issues for a given repository.') issue_list.set_defaults(func=do_list) issue_list.add_argument('--status', help='List issues having this status; default is "open"') issue_list.add_argument('--repository', help='Name of the repository.') issue_get = subparsers.add_parser('get', help='View an issue.') issue_get.set_defaults(func=get) issue_get.add_argument('number', help='Number of the issue to retrieve.') issue_get.add_argument('--repository', help='Name of the repository.') issue_close = subparsers.add_parser('close', help='Close an issue.') issue_close.set_defaults(func=close) issue_close.add_argument('number', help='Number of the issue to close.') issue_close.add_argument('--repository', help='Name of the repository.') issue_open = subparsers.add_parser('open', help='Open a new issue.') issue_open.set_defaults(func=do_open) issue_open.add_argument('title', help='Title of the issue.') issue_open.add_argument('--repository', help='Name of the repository.') issue_comment = subparsers.add_parser('comment', help='Comment on an existing issue.') issue_comment.set_defaults(func=comment) issue_comment.add_argument('number', help='Number of the issue on which you wish to comment.') issue_comment.add_argument('--repository', help='Name of the repository.') issue_addlabel = subparsers.add_parser('add_label', help='Add a label to an issue.') issue_addlabel.set_defaults(func=addlabel) issue_addlabel.add_argument('number', help='Number of the issue on which you wish to add a label.') issue_addlabel.add_argument('label', help='Label to add.') issue_addlabel.add_argument('--repository', help='Name of the repository.') issue_remlabel = subparsers.add_parser('remove_label', help='Remove a label from an issue.') issue_remlabel.set_defaults(func=remlabel) issue_remlabel.add_argument('number', help='Number of the issue from which you wish to remove a label.') issue_remlabel.add_argument('label', help='Label to remove.') issue_remlabel.add_argument('--repository', help='Name of the repository.') cligh-0.2/cligh/repos.py000066400000000000000000000067661215074535300152550ustar00rootroot00000000000000#!/usr/bin/python # Repository-related commands. from cligh.utils import get_working_repo, read_user_input def create(client, args): """Create a new repository.""" def validate_description(text): if len(text) == 0: print 'Description may not be empty. Try again.' return False return True def validate_name(text): if len(text) == 0: print 'Name may not be empty. Try again.' return False if any(char for char in text if char.isspace()): print 'Name may not contain spaces. Try again.' return False # What other characters don't belong in the name? return True def validate_homepage(text): # This is a lame excuse for validation. if len(text) == 0: print 'Home page may not be empty. Try again.' return False return True name = read_user_input('Repository name', validate_name) homepage = read_user_input('Homepage', validate_homepage) description = read_user_input('Description', validate_description) user = client.get_user() print client.get_user().create_repo(name=name, description=description, homepage=homepage) def fork(client, args): """Fork a repository.""" repo_to_fork = get_working_repo(client, args.repository) client.get_user().create_fork(repo_to_fork) print 'Repository forked.' def do_list(client, args): """Command to list the repos for a given user.""" user = client.get_user(args.user) repos = user.get_repos() print '%s has the following repositories:' % args.user print 'Name - Description' for repo in repos: print '%s - %s' % (repo.name, repo.description) def addlabel(client, args): # xxx Make this configurable by the user. White is a sane # default, for now. color = 'ffffff' repository = get_working_repo(client, args.repository) try: repository.create_label(args.label, color) except GithubException as e: die('''Unable to create label %s. The complete error response was: %s ''' % (args.label, e.data)) print 'Label added.' def remlabel(client, args): repository = get_working_repo(client, args.repository) try: label = repository.get_label(args.label) label.delete() except GithubException as e: die('''Unable to delete label %s from this repository. Error message: %s ''' % (args.label, e.data['message'])) print 'Label removed.' def make_repo_parser(subparsers): repo = subparsers.add_parser('repo', help='Manage and query repositories.') subparsers = repo.add_subparsers(title='Repository-related Subcommands') repo_list = subparsers.add_parser('list', help='List repositories belonging to a given user.') repo_list.set_defaults(func=do_list) repo_list.add_argument('user') repo_create = subparsers.add_parser('create', help='Create a new repository.') repo_create.set_defaults(func=create) repo_fork = subparsers.add_parser('fork', help='Fork an existing repository.') repo_fork.set_defaults(func=fork) repo_fork.add_argument('repository', help='Name of the repository, in the form USERNAME/REPONAME') repo_addlabel = subparsers.add_parser('add_label', help='Add a label to a repository.') repo_addlabel.set_defaults(func=addlabel) repo_addlabel.add_argument('--repository', help='Name of the repository, in the form USERNAME/REPONAME') repo_addlabel.add_argument('label', help='Name of the label to add') repo_remlabel = subparsers.add_parser('remove_label', help='Remove a label from a repository.') repo_remlabel.set_defaults(func=remlabel) repo_remlabel.add_argument('--repository', help='Name of the repository, in the form USERNAME/REPONAME') repo_remlabel.add_argument('label', help='Name of the label to remove') cligh-0.2/cligh/utils.py000066400000000000000000000076371215074535300152630ustar00rootroot00000000000000#!/usr/bin/python import os import os.path import re import subprocess import sys import tempfile # Helper functions. def print_error(message): """Display an error message.""" sys.stderr.write(message) def die(message): """Terminate, displaying an error message.""" print_error(message) sys.exit(1) def read_git_config(key): """Read a value from git's configuration files.""" cmd = ['git', 'config'] cmd.append(key) output = subprocess.Popen(cmd, stdout=subprocess.PIPE).communicate() return output[0].strip() def read_user_input(prompt, validator_func): """Read and validate user input.""" user_text = '' valid_input = False while not valid_input: sys.stdout.write('%s: ' % prompt) sys.stdout.flush() user_text = sys.stdin.readline() if not user_text: die('Could not read input from the user; unable to proceed.') user_text = user_text[0:-1] # Snip off the newline. if validator_func(user_text): valid_input = True return user_text def get_repository_name(name): """Get the name of the repository to work on. Either we return the "name" argument (if it is a non-empty string), or we try to detect the repository's name by looking at remote.origin.url.""" if not name: name = read_git_config('remote.origin.url') match = re.match(r'git@github.com:(.*)\.git$', name) if not match: die( """This command expects a repository name, but the name is unknown. There are two ways to indicate a repository. First, you can supply the --repository argument on the command line. If no --repository argument is specified, and the current directory is within a clone of a project hosted on github, then the name of the repository is detected automatically. In order for this auto-detection to work, the remote named "origin" must point to github. """) name = match.group(1) return name def split_repo_name(repository): """Take a string of the form user/repo, and return the tuple (user, repo). If the string does not contain the username, then just return None for the user.""" nameparts = repository.split('/', 1) if len(nameparts) == 1: return (None, nameparts[0]) else: return (nameparts[0], nameparts[1]) def get_working_repo(client, full_reponame): full_reponame = get_repository_name(full_reponame) username, reponame = split_repo_name(full_reponame) if username: user = client.get_user(username) else: user = client.get_user() repository = user.get_repo(reponame) return repository def get_named_user(client, username): try: user = client.get_user(username) except GithubException as e: die('''Failed to retrieve the record for user %s: Encountered the following exception: %s ''' % (username, str(e))) return user def find_executable(executable): path = os.environ['PATH'].split(os.pathsep) for subdirectory in path: tryname = os.path.join(subdirectory, executable) if os.access(tryname, os.X_OK): return tryname return None def choose_editor(): if os.environ.has_key('EDITOR'): editor = os.environ['EDITOR'] else: print_error('$EDITOR not set, assuming default of vi.') editor = find_executable('vi') if not editor: die( """Error: cannot continue with the current command, because the text editor could not be found. Please set your EDITOR environment variable to the pathname of your preferred editor. """) return editor def text_from_editor(original_text=''): """Allow a user to compose a text using his editor of choice.""" text = '' editor_cmd = choose_editor() my_tempfile = None try: my_tempfile = tempfile.NamedTemporaryFile(delete=False) if original_text: my_tempfile.write(original_text) my_tempfile.flush() my_tempfile.seek(0) # And go back to the beginning. editor_status = subprocess.call([editor_cmd, my_tempfile.name]) if editor_status != 0: die( """Error: the text editor did not complete successfully. Unable to continue. """) text = my_tempfile.read() finally: if my_tempfile: my_tempfile.close() os.unlink(my_tempfile.name) return text cligh-0.2/setup.py000077500000000000000000000004251215074535300141640ustar00rootroot00000000000000#!/usr/bin/env python from distutils.core import setup setup( name='cligh', version='0.1', description='Command-line interface to GitHub', author='Christopher M. Brannon', author_email='cmbrannon79@gmail.com', license='BSD', packages=['cligh'], scripts=['bin/cligh'])