def download_gist(gist): """Download the gist specified by owner/name string. :return: Whether the gist has been successfully downloaded """ logger.debug("downloading gist %s ...", gist) owner, gist_name = gist.split('/', 1) for gist_json in iter_gists(owner): # GitHub names gists after their first files in alphabetical order # TODO(xion): warn the user when this could create problems, # i.e. when a single owner has two separate gists named the same way filename = list(sorted(gist_json['files'].keys()))[0] if filename != gist_name: continue # the gist should be placed inside a directory named after its ID clone_needed = True gist_dir = GISTS_DIR / str(gist_json['id']) if gist_dir.exists(): # this is an inconsistent state, as it means the binary # for a gist is missing, while the repository is not; # no real harm in that, but we should report it anyway logger.warning("gist %s already downloaded") clone_needed = False # clone it if necessary (which is usually the case) if clone_needed: logger.debug("gist %s found, cloning its repository...", gist) ensure_path(gist_dir) git_clone_run = run('git clone %s %s' % ( gist_json['git_pull_url'], gist_dir)) if git_clone_run.status_code != 0: logger.error( "cloning repository for gist %s failed (exitcode %s)", gist, git_clone_run.status_code) join(git_clone_run) logger.debug("gist %s successfully cloned", gist) # make sure the gist executable is, in fact, executable # TODO(xion): fix the hashbang while we're at it gist_exec = gist_dir / filename gist_exec.chmod(GIST_EXEC_PERMISSIONS) logger.debug("adjusted permissions for gist file %s", gist_exec) # create symlink from BIN_DIR/<owner>/<gist_name> # to the gist's executable file gist_owner_bin_dir = BIN_DIR / owner ensure_path(gist_owner_bin_dir) gist_link = gist_owner_bin_dir / filename if not gist_link.exists(): gist_link.symlink_to(path_vector(from_=gist_link, to=gist_exec)) logger.debug("symlinked gist 'binary' %s to executable %s", gist_link, gist_exec) if clone_needed: logger.info("gist %s downloaded sucessfully", gist) return True return False
def run_gist_url(gist, args=(), local=False): """Run the gist specified by an URL. If successful, this function does not return. :param gist: Gist as :class:`Gist` object :param args: Arguments to pass to the gist :param local: Whether to only run gists that are available locally """ try: gist_info = get_gist_info(gist.id) except requests.exceptions.HTTPError: error("couldn't retrieve GitHub gist %s/%s", gist.owner, gist.id) # warn if the actual gist owner is different than the one in the URL; # TODO(xion): consider asking for confirmation; # there may be some phishing scenarios possible here owner = gist_info.get('owner', {}).get('login') if gist.owner != owner: logger.warning("gist %s is owned by %s, not %s", gist.id, owner, gist.owner) gist_name = list(sorted(gist_info['files'].keys()))[0] actual_gist_ref = '/'.join((owner, gist_name)) ensure_gist(actual_gist_ref, local=local) return run_named_gist(actual_gist_ref, args)
def update_gist(gist): """Pull the latest version of the gist specified by owner/name string. :return: Whether the gist has been successfully updated """ logger.debug("updating gist %s ...", gist) gist_id = get_gist_id(gist) gist_dir = GISTS_DIR / gist_id git_pull_run = run('git pull', cwd=str(gist_dir)) if git_pull_run.status_code != 0: # TODO(xion): detect conflicts and do `git reset --merge` automatically logger.warning("pulling changes to gist %s failed (exitcode %s)", gist, git_pull_run.status_code) join(git_pull_run) logger.info("gist %s successfully updated", gist) return True
def run_named_gist(gist, args=()): """Run the gist specified by owner/name string. This function does not return, because the whole process is replaced by the gist's executable. :param gist: Gist as :class:`Gist` object or <owner>/<name> string :param args: Arguments to pass to the gist """ if isinstance(gist, Gist): gist = gist.ref logger.info("running gist %s ...", gist) executable = bytes(BIN_DIR / gist) try: os.execv(executable, [executable] + list(args)) except OSError as e: if e.errno != 8: # Exec format error raise logger.warning( "couldn't run gist %s directly -- " "does it have a proper hashbang?", gist) # try to figure out the interpreter to use based on file extension # contained within the gist name extension = Path(gist).suffix if not extension: # TODO(xion): use MIME type from GitHub as additional hint # as to the choice of interpreter error( "can't deduce interpreter for gist %s " "without file extension", gist) interpreter = COMMON_INTERPRETERS.get(extension) if not interpreter: error("no interpreter found for extension '%s'", extension) # format an interpreter-specific command line # and execute it within current process (hence the argv shenanigans) cmd = interpreter % dict(script=str(BIN_DIR / gist), args=' '.join(map(shell_quote, args))) cmd_argv = shell_split(cmd) os.execvp(cmd_argv[0], cmd_argv)
def _on_cache_rescue(self, path, content): if flags.command == GistCommand.INFO: logger.warning("could not communicate with GitHub -- " "gist information may be out of date")