Beispiel #1
0
def submit(repo, args):
    if environment.DEBUG:
        arcanist.ARC.append("--trace")

    telemetry.metrics.mozphab.submission.preparation_time.start()
    with wait_message("Checking connection to Phabricator."):
        # Check if raw Conduit API can be used
        if not conduit.check():
            raise Error("Failed to use Conduit API")

        # Check if local and remote VCS matches
        repo.check_vcs()

        # Check if arc is configured
        if not args.no_arc and not repo.check_arc():
            raise Error("Failed to run %s." % arcanist.ARC_COMMAND)

    repo.before_submit()

    # Find and preview commits to submits.
    with wait_message("Looking for commits.."):
        commits = repo.commit_stack(single=args.single)
    if not commits:
        raise Error("Failed to find any commits to submit")

    with wait_message("Loading commits.."):
        # Pre-process to load metadata.
        morph_blocking_reviewers(commits)
        augment_commits_from_body(commits)
        update_commits_from_args(commits, args)

    # Display a one-line summary of commit and WIP count.
    commit_count = len(commits)
    wip_commit_count = sum(1 for commit in commits if commit["wip"])

    if wip_commit_count == commit_count:
        status = "as Work In Progress"
    elif wip_commit_count:
        status = f"{wip_commit_count} as Work In Progress"
    else:
        status = "for review"

    logger.warning(
        f"Submitting {commit_count} commit{'s'[:commit_count^1]} {status}")

    # Validate commit stack is suitable for review.
    show_commit_stack(commits, validate=True)
    try:
        with wait_message("Checking commits.."):
            repo.check_commits_for_submit(commits, require_bug=not args.no_bug)
    except Error as e:
        if not args.force:
            raise Error("Unable to submit commits:\n\n%s" % e)
        logger.error("Ignoring issues found with commits:\n\n%s", e)

    if not any(commit["submit"] for commit in commits):
        logger.warning("No changes to submit.")
        return

    # Show a warning if there are untracked files.
    if config.warn_untracked:
        untracked = repo.untracked()
        if untracked:
            logger.warning(
                "Warning: found %s untracked file%s (will not be submitted):",
                len(untracked),
                "" if len(untracked) == 1 else "s",
            )
            if len(untracked) <= 5:
                for filename in untracked:
                    logger.info("  %s", filename)

    # Show a warning if -m is used and there are new commits.
    if args.message and any([c for c in commits if not c["rev-id"]]):
        logger.warning(
            "Warning: --message works with updates only, and will not\n"
            "be result in a comment on new revisions.")

    telemetry.metrics.mozphab.submission.preparation_time.stop()
    telemetry.metrics.mozphab.submission.commits_count.add(len(commits))

    # Confirmation prompt.
    if args.yes:
        pass
    elif config.auto_submit and not args.interactive:
        logger.info(
            "Automatically submitting (as per submit.auto_submit in %s)",
            config.name)
    else:
        res = prompt(
            "Submit to %s" %
            PHABRICATOR_URLS.get(repo.phab_url, repo.phab_url),
            ["Yes", "No", "Always"],
        )
        if res == "No":
            return
        if res == "Always":
            config.auto_submit = True
            config.write()

    # Process.
    telemetry.metrics.mozphab.submission.process_time.start()
    previous_commit = None
    # Collect all existing revisions to get reviewers info.
    rev_ids_to_update = [int(c["rev-id"]) for c in commits if c.get("rev-id")]
    revisions_to_update = None
    if rev_ids_to_update:
        with wait_message("Loading revision data..."):
            list_to_update = conduit.get_revisions(ids=rev_ids_to_update)

        revisions_to_update = {str(r["id"]): r for r in list_to_update}

    last_node = commits[-1]["orig-node"]
    for commit in commits:
        diff = None

        check_in_needed = args.check_in_needed and commit[
            "orig-node"] == last_node
        # Only revisions being updated have an ID.  Newly created ones don't.
        if not commit["submit"]:
            previous_commit = commit
            continue

        is_update = bool(commit["rev-id"])
        revision_to_update = (revisions_to_update[commit["rev-id"]]
                              if is_update else None)
        existing_reviewers = (
            revision_to_update["attachments"]["reviewers"]["reviewers"]
            if revision_to_update else None)

        # Let the user know something's happening.
        if is_update:
            logger.info("\nUpdating revision D%s:", commit["rev-id"])
        else:
            logger.info("\nCreating new revision:")

        logger.info("%s %s", commit["name"],
                    revision_title_from_commit(commit))
        repo.checkout(commit["node"])

        # WIP submissions shouldn't set reviewers on phabricator.
        if commit["wip"]:
            reviewers = ""
        else:
            reviewers = ", ".join(commit["reviewers"]["granted"] +
                                  commit["reviewers"]["request"])

        # Create arc-annotated commit description.
        template_vars = dict(
            title=revision_title_from_commit(commit),
            body=commit["body"],
            reviewers=reviewers,
            bug_id=commit["bug-id"],
        )
        summary = commit["body"]
        if previous_commit and not args.no_stack:
            template_vars[
                "depends_on"] = "Depends on D%s" % previous_commit["rev-id"]
            summary = "%s\n\n%s" % (summary, template_vars["depends_on"])

        message = arc_message(template_vars)

        if args.no_arc:
            # Create a diff if needed
            with wait_message("Creating local diff..."):
                diff = repo.get_diff(commit)

            if diff:
                telemetry.metrics.mozphab.submission.files_count.add(
                    len(diff.changes))
                with wait_message("Uploading binary file(s)..."):
                    diff.upload_files()

                with wait_message("Submitting the diff..."):
                    diff.submit(commit, message)

            if is_update:
                with wait_message("Updating revision..."):
                    rev = conduit.update_revision(
                        commit,
                        existing_reviewers,
                        diff_phid=diff.phid,
                        comment=args.message,
                        check_in_needed=check_in_needed,
                    )
            else:
                with wait_message("Creating a new revision..."):
                    rev = conduit.create_revision(
                        commit,
                        summary,
                        diff.phid,
                        check_in_needed=check_in_needed,
                    )

            revision_url = "%s/D%s" % (repo.phab_url, rev["object"]["id"])

        else:
            # Run arc.
            with temporary_file(message) as message_file:
                arc_args = (["diff"] + ["--base", "arc:this"] +
                            ["--allow-untracked", "--no-amend", "--no-ansi"] +
                            ["--message-file", message_file])
                if args.nolint:
                    arc_args.append("--nolint")
                if commit["wip"]:
                    arc_args.append("--plan-changes")
                if args.lesscontext:
                    arc_args.append("--less-context")
                if is_update:
                    message = args.message if args.message else DEFAULT_UPDATE_MESSAGE
                    arc_args.extend(["--message", message] +
                                    ["--update", commit["rev-id"]])
                else:
                    arc_args.append("--create")

                revision_url = None
                for line in check_call_by_line(arcanist.ARC + arc_args,
                                               cwd=repo.path,
                                               never_log=True):
                    print(line)
                    revision_url = extract_revision_url(line) or revision_url

            if not revision_url:
                raise Error("Failed to find 'Revision URL' in arc output")

            if is_update:
                current_status = revision_to_update["fields"]["status"][
                    "value"]
                with wait_message("Updating D%s.." % commit["rev-id"]):
                    transactions = []
                    revision = conduit.get_revisions(
                        ids=[int(commit["rev-id"])])[0]

                    update_revision_description(transactions, commit, revision)
                    update_revision_bug_id(transactions, commit, revision)

                    # Add reviewers only if revision lacks them
                    if (not commit["wip"] and commit["has-reviewers"]
                            and not existing_reviewers):
                        conduit.update_revision_reviewers(transactions, commit)
                        if current_status != "needs-review":
                            transactions.append(dict(type="request-review"))

                    if transactions:
                        arcanist.call_conduit(
                            "differential.revision.edit",
                            {
                                "objectIdentifier": "D%s" % commit["rev-id"],
                                "transactions": transactions,
                            },
                            repo.path,
                        )

        # Append/replace div rev url to/in commit description.
        body = amend_revision_url(commit["body"], revision_url)

        # Amend the commit if required.
        # As commit rewriting can be expensive we avoid it in some circumstances, such
        # as pre-pending "WIP: " to commits submitted as WIP to Phabricator.
        if commit["title-preview"] != commit["title"] or body != commit["body"]:
            commit["title"] = commit["title-preview"]
            commit["body"] = body
            commit["rev-id"] = parse_arc_diff_rev(commit["body"])
            with wait_message("Updating commit.."):
                repo.amend_commit(commit, commits)

        # Diff property has to be set after potential SHA1 change.
        if args.no_arc and diff:
            with wait_message("Setting diff metadata..."):
                diff.set_property(commit, message)

        previous_commit = commit

    # Cleanup (eg. strip nodes) and refresh to ensure the stack is right for the
    # final showing.
    with wait_message("Cleaning up.."):
        repo.finalize(commits)
        repo.after_submit()
        repo.cleanup()
        repo.refresh_commit_stack(commits)

    logger.warning("\nCompleted")
    show_commit_stack(commits,
                      validate=False,
                      show_rev_urls=True,
                      show_updated_only=True)
    telemetry.metrics.mozphab.submission.process_time.stop()
Beispiel #2
0
def show_commit_stack(
    commits,
    validate=True,
    show_rev_urls=False,
    show_updated_only=False,
):
    """Log the commit stack in a human readable form."""

    # keep output in columns by sizing the action column to the longest rev + 1 ("D")
    max_len = (max(len(c.get("rev-id", "") or "")
                   for c in commits) + 1 if commits else "")
    action_template = "(%" + str(max_len) + "s)"

    if validate:
        # preload all revisions
        ids = [int(c["rev-id"]) for c in commits if c.get("rev-id")]
        if ids:
            with wait_message("Loading existing revisions..."):
                revisions = conduit.get_revisions(ids=ids)

            # preload diffs
            with wait_message("Loading diffs..."):
                diffs = conduit.get_diffs(
                    [r["fields"]["diffPHID"] for r in revisions])

    for commit in reversed(commits):
        if show_updated_only and not commit["submit"]:
            continue

        revision_is_closed = False
        revision_is_wip = False
        bug_id_changed = False
        is_author = True
        revision = None

        if commit.get("rev-id"):
            action = action_template % ("D" + commit["rev-id"])

            if validate:
                revisions = conduit.get_revisions(ids=[int(commit["rev-id"])])
                if revisions:
                    revision = revisions[0]
                    fields = revision["fields"]

                    # WIP if either in changes-planned state, or if the revision has the
                    # `draft` flag set.
                    revision_is_wip = (fields["status"]["value"]
                                       == "changes-planned"
                                       or fields["isDraft"])

                    # Check if target bug ID is the same as in the Phabricator revision
                    bug_id_changed = fields.get("bugzilla.bug-id") and (
                        commit["bug-id"] != fields["bugzilla.bug-id"])

                    # Check if revision is closed
                    revision_is_closed = fields["status"]["closed"]

                    # Check if comandeering is required
                    with wait_message("Figuring out who you are..."):
                        whoami = conduit.whoami()
                    if "authorPHID" in fields and (fields["authorPHID"] !=
                                                   whoami["phid"]):
                        is_author = False

                    # Any reviewers added to a revision without them?
                    reviewers_added = bool(
                        not revision["attachments"]["reviewers"]["reviewers"]
                        and commit["reviewers"]["granted"])

                    # if SHA1 hasn't changed
                    # and we're not changing the WIP or draft status
                    # and we're not adding reviewers to a revision without reviewers
                    # and we're not changing the bug-id
                    diff_phid = fields["diffPHID"]
                    diff_commits = diffs[diff_phid]["attachments"]["commits"][
                        "commits"]
                    sha1_changed = commit["node"] != diff_commits[0][
                        "identifier"]
                    if (not sha1_changed and commit["wip"] == revision_is_wip
                            and not reviewers_added and not bug_id_changed
                            and not revision_is_closed):
                        commit["submit"] = False

        else:
            action = action_template % "New"

        logger.info("%s %s %s", action, commit["name"],
                    commit["title-preview"])
        if validate:
            if not commit["submit"]:
                logger.info(
                    " * This revision has not changed and will not be submitted."
                )
                continue

            if revision:
                if not commit["wip"] and revision_is_wip:
                    logger.warning(
                        '!! "Changes Planned" status will change to "Request Review"'
                    )
                if commit["wip"] and not revision_is_wip:
                    logger.warning(
                        '!! "Request Review" status will change to "Changes Planned"'
                    )

            if bug_id_changed:
                logger.warning(
                    "!! Bug ID in Phabricator revision will change from %s to %s",
                    revision["fields"]["bugzilla.bug-id"],
                    commit["bug-id"],
                )

            if not is_author:
                logger.warning(
                    "!! You don't own this revision. Normally, you should only\n"
                    '   update revisions you own. You can "Commandeer" this\n'
                    "   revision from the web interface if you want to become\n"
                    "   the owner.")

            if revision_is_closed:
                logger.warning(
                    "!! This revision is closed!\n"
                    "   It will be reopened if submission proceeds.\n"
                    "   You can stop now and refine the stack range.")

            if not commit["bug-id"]:
                logger.warning("!! Missing Bug ID")

            if commit["bug-id-orig"] and commit["bug-id"] != commit[
                    "bug-id-orig"]:
                logger.warning(
                    "!! Bug ID changed from %s to %s",
                    commit["bug-id-orig"],
                    commit["bug-id"],
                )

            if not commit["has-reviewers"]:
                logger.warning(
                    "!! Missing reviewers\n"
                    '   It will be submitted as "Changes Planned".\n'
                    "   Run submit again with --no-wip to prevent this.")

        if show_rev_urls and commit["rev-id"]:
            logger.warning("-> %s/D%s", conduit.repo.phab_url,
                           commit["rev-id"])
Beispiel #3
0
def show_commit_stack(
    commits,
    wip=None,
    validate=True,
    ignore_reviewers=False,
    show_rev_urls=False,
    show_updated_only=False,
):
    """Log the commit stack in a human readable form."""

    # keep output in columns by sizing the action column to the longest rev + 1 ("D")
    max_len = (max(len(c.get("rev-id", "") or "")
                   for c in commits) + 1 if commits else "")
    action_template = "(%" + str(max_len) + "s)"

    if validate:
        # preload all revisions
        ids = [int(c["rev-id"]) for c in commits if c.get("rev-id")]
        if ids:
            with wait_message("Loading existing revisions..."):
                revisions = conduit.get_revisions(ids=ids)

            # preload diffs
            with wait_message("Loading diffs..."):
                diffs = conduit.get_diffs(
                    [r["fields"]["diffPHID"] for r in revisions])

    for commit in reversed(commits):
        if show_updated_only and not commit["submit"]:
            continue

        closed = False
        change_bug_id = False
        is_author = True
        revision = None
        wip_removed = False
        wip_set = False
        reviewers_added = False

        if commit.get("rev-id"):
            action = action_template % ("D" + commit["rev-id"])
            if validate:
                revisions = conduit.get_revisions(ids=[int(commit["rev-id"])])
                if len(revisions) > 0:
                    revision = revisions[0]

                    # Check if target bug ID is the same as in the Phabricator revision
                    change_bug_id = ("bugzilla.bug-id" in revision["fields"]
                                     and revision["fields"]["bugzilla.bug-id"]
                                     and
                                     (commit["bug-id"] !=
                                      revision["fields"]["bugzilla.bug-id"]))

                    # Check if revision is closed
                    closed = revision["fields"]["status"]["closed"]

                    # Check if comandeering is required
                    whoami = conduit.whoami()
                    if "authorPHID" in revision["fields"] and (
                            revision["fields"]["authorPHID"] !=
                            whoami["phid"]):
                        is_author = False

                    # Do we remove "Changes Planned" status?
                    wip_removed = (not wip
                                   and revision["fields"]["status"]["value"]
                                   == "changes-planned")

                    # Do we set "Changes Planned" status?
                    wip_set = (wip and revision["fields"]["status"]["value"] !=
                               "changes-planned")

                    # Any reviewers added to a revision without them?
                    reviewers_added = bool(
                        not revision["attachments"]["reviewers"]["reviewers"]
                        and commit["reviewers"]["granted"])

                    # if SHA1 hasn't changed
                    # and we're not changing the WIP status
                    # and we're not adding reviewers to a revision without reviewers
                    # and we're not changing the bug-id
                    sha1_changed = (
                        commit["node"] != diffs[revision["fields"]["diffPHID"]]
                        ["attachments"]["commits"]["commits"][0]["identifier"])
                    if (not sha1_changed and not wip_removed and not wip_set
                            and not reviewers_added and not change_bug_id
                            and not closed):
                        commit["submit"] = False

        else:
            action = action_template % "New"

        logger.info("%s %s %s", action, commit["name"],
                    commit["title-preview"])
        if validate:
            if not commit["submit"]:
                logger.info(
                    " * This revision is not changed and will not be submitted."
                )

            else:
                if wip_removed:
                    logger.warning(
                        '!! "Changes Planned" status will change to "Request Review"'
                    )

                if change_bug_id:
                    logger.warning(
                        "!! Bug ID in Phabricator revision will change from %s to %s",
                        revision["fields"]["bugzilla.bug-id"],
                        commit["bug-id"],
                    )

                if not is_author:
                    logger.warning(
                        "!! You don't own this revision. Normally, you should only\n"
                        '   update revisions you own. You can "Commandeer" this\n'
                        "   revision from the web interface if you want to become\n"
                        "   the owner.")

                if closed:
                    logger.warning(
                        "!! This revision is closed!\n"
                        "   It will be reopened if submission proceeds.\n"
                        "   You can stop now and refine the stack range.")

                if not commit["bug-id"]:
                    logger.warning("!! Missing Bug ID")

                if commit["bug-id-orig"] and commit["bug-id"] != commit[
                        "bug-id-orig"]:
                    logger.warning(
                        "!! Bug ID changed from %s to %s",
                        commit["bug-id-orig"],
                        commit["bug-id"],
                    )

                if (not ignore_reviewers
                        and not commit["reviewers"]["granted"] +
                        commit["reviewers"]["request"]):
                    logger.warning("!! Missing reviewers")

        if show_rev_urls and commit["rev-id"]:
            logger.warning("-> %s/D%s", conduit.repo.phab_url,
                           commit["rev-id"])
Beispiel #4
0
def show_commit_stack(commits,
                      validate=True,
                      ignore_reviewers=False,
                      show_rev_urls=False):
    """Log the commit stack in a human readable form."""

    # keep output in columns by sizing the action column to the longest rev + 1 ("D")
    max_len = (max(len(c.get("rev-id", "") or "")
                   for c in commits) + 1 if commits else "")
    action_template = "(%" + str(max_len) + "s)"

    if validate:
        # preload all revisions
        ids = [int(c["rev-id"]) for c in commits if c.get("rev-id")]
        if ids:
            with wait_message("Loading existing revisions..."):
                conduit.get_revisions(ids=ids)

    for commit in reversed(commits):
        closed = False
        change_bug_id = False
        is_author = True
        revision = None

        if commit.get("rev-id"):
            action = action_template % ("D" + commit["rev-id"])
            if validate:
                revisions = conduit.get_revisions(ids=[int(commit["rev-id"])])
                if len(revisions) > 0:
                    revision = revisions[0]

                    # Check if target bug ID is the same as in the Phabricator revision
                    change_bug_id = ("bugzilla.bug-id" in revision["fields"]
                                     and revision["fields"]["bugzilla.bug-id"]
                                     and
                                     (commit["bug-id"] !=
                                      revision["fields"]["bugzilla.bug-id"]))

                    # Check if revision is closed
                    closed = revision["fields"]["status"]["closed"]

                    # Check if comandeering is required
                    whoami = conduit.whoami()
                    if "authorPHID" in revision["fields"] and (
                            revision["fields"]["authorPHID"] !=
                            whoami["phid"]):
                        is_author = False
        else:
            action = action_template % "New"

        logger.info("%s %s %s", action, commit["name"],
                    commit["title-preview"])
        if validate:
            if change_bug_id:
                logger.warning(
                    "!! Bug ID in Phabricator revision will be changed from %s to %s",
                    revision["fields"]["bugzilla.bug-id"],
                    commit["bug-id"],
                )

            if not is_author:
                logger.warning(
                    "!! You don't own this revision. Normally, you should only\n"
                    '   update revisions you own. You can "Commandeer" this\n'
                    "   revision from the web interface if you want to become\n"
                    "   the owner.")

            if closed:
                logger.warning(
                    "!! This revision is closed!\n"
                    "   It will be reopened if submission proceeds.\n"
                    "   You can stop now and refine the stack range.")

            if not commit["bug-id"]:
                logger.warning("!! Missing Bug ID")

            if commit["bug-id-orig"] and commit["bug-id"] != commit[
                    "bug-id-orig"]:
                logger.warning(
                    "!! Bug ID changed from %s to %s",
                    commit["bug-id-orig"],
                    commit["bug-id"],
                )

            if (not ignore_reviewers and not commit["reviewers"]["granted"] +
                    commit["reviewers"]["request"]):
                logger.warning("!! Missing reviewers")

        if show_rev_urls and commit["rev-id"]:
            logger.warning("-> %s/D%s", conduit.repo.phab_url,
                           commit["rev-id"])
Beispiel #5
0
def reorganise(repo, args):
    telemetry.metrics.mozphab.submission.preparation_time.start()

    with wait_message("Checking connection to Phabricator."):
        # Check if raw Conduit API can be used
        if not conduit.check():
            raise Error("Failed to use Conduit API")

    # Find and preview commits to submits.
    with wait_message("Looking for commits.."):
        commits = repo.commit_stack()

    if not commits:
        raise Error("Failed to find any commits to reorganise.")

    with wait_message("Loading commits.."):
        augment_commits_from_body(commits)

    localstack_ids = [c["rev-id"] for c in commits]
    if None in localstack_ids:
        names = [c["name"] for c in commits if c["rev-id"] is None]
        plural = len(names) > 1
        raise Error(
            "Found new commit{plural} in the local stack: {names}.\n"
            "Please submit {them} separately and call the `reorg` again.".
            format(
                plural="s" if plural else "",
                them="them" if plural else "it",
                names=", ".join(names),
            ))

    logger.warning("Reorganisation based on {} commit{}:".format(
        len(commits),
        "" if len(commits) == 1 else "s",
    ))

    # Get PhabricatorStack
    # Errors will be raised later in the `walk_llist` method
    with wait_message("Detecting the remote stack..."):
        try:
            phabstack = conduit.get_stack(localstack_ids)
        except Error:
            logger.error("Remote stack is not linear.")
            raise

    # Preload the phabricator stack
    with wait_message("Preloading Phabricator stack revisions..."):
        conduit.get_revisions(phids=list(phabstack.keys()))

    if phabstack:
        try:
            phabstack_phids = walk_llist(phabstack)
        except Error:
            logger.error("Remote stack is not linear.\n"
                         "Detected stack:\n{}".format(" <- ".join(
                             conduit.phids_to_ids(list(phabstack.keys())))))
            raise
    else:
        phabstack_phids = []

    localstack_phids = conduit.ids_to_phids(localstack_ids)
    try:
        transactions = stack_transactions(phabstack_phids, localstack_phids)
    except Error:
        logger.error("Unable to prepare stack transactions.")
        raise

    if not transactions:
        raise Error("Reorganisation is not needed.")

    logger.warning("Stack will be reorganised:")
    for phid, rev_transactions in transactions.items():
        node_id = conduit.phid_to_id(phid)
        if "abandon" in [t["type"] for t in rev_transactions]:
            logger.info(" * {} will be abandoned".format(node_id))
        else:
            for t in rev_transactions:
                if t["type"] == "children.set":
                    logger.info(" * {child} will depend on {parent}".format(
                        child=conduit.phid_to_id(t["value"][0]),
                        parent=node_id,
                    ))
                if t["type"] == "children.remove":
                    logger.info(
                        " * {child} will no longer depend on {parent}".format(
                            child=conduit.phid_to_id(t["value"][0]),
                            parent=node_id,
                        ))

    telemetry.metrics.mozphab.submission.preparation_time.stop()

    if args.yes:
        pass
    else:
        res = prompt("Perform reorganisation", ["Yes", "No"])
        if res == "No":
            sys.exit(1)

    telemetry.metrics.mozphab.submission.process_time.start()

    with wait_message("Applying transactions..."):
        for phid, rev_transactions in transactions.items():
            conduit.edit_revision(rev_id=phid, transactions=rev_transactions)

    telemetry.metrics.mozphab.submission.process_time.stop()
    logger.info("Stack has been reorganised.")
Beispiel #6
0
def patch(repo, args):
    """Patch repository from Phabricator's revisions.

    By default:
    * perform sanity checks
    * find the base commit
    * create a new branch/bookmark
    * apply the patches and commit the changes

    args.no_commit is True - no commit will be created after applying diffs
    args.apply_to - <head|tip|branch> (default: branch)
        branch - find base commit and apply on top of it
        head/tip - apply changes to current commit
    args.raw is True - only print out the diffs (--force doesn't change anything)

    Raises:
    * Error if uncommitted changes are present in the working tree
    * Error if Phabricator revision is not found
    * Error if `--apply-to base` and no base commit found in the first diff
    * Error if base commit not found in repository
    """
    # Check if raw Conduit API can be used
    with wait_message("Checking connection to Phabricator."):
        # Check if raw Conduit API can be used
        if not conduit.check():
            raise Error("Failed to use Conduit API")

    if not args.raw:
        # Check if local and remote VCS matches
        with wait_message("Checking VCS"):
            repo.check_vcs()

        # Look for any uncommitted changes
        with wait_message("Checking repository.."):
            clean = repo.is_worktree_clean()

        if not clean:
            raise Error(
                "Uncommitted changes present. Please %s them or commit before patching."
                % ("shelve" if isinstance(repo, Mercurial) else "stash"))

    # Get the target revision
    with wait_message("Fetching D%s.." % args.revision_id):
        revs = conduit.get_revisions(ids=[args.revision_id])

    if not revs:
        raise Error("Revision not found")

    revision = revs[0]

    if not args.skip_dependencies:
        with wait_message("Fetching D%s children.." % args.revision_id):
            try:
                children = conduit.get_successor_phids(
                    revision["phid"], include_abandoned=args.include_abandoned)
                non_linear = False
            except NonLinearException:
                children = []
                non_linear = True

        patch_children = True
        if children:
            if args.yes or config.always_full_stack:
                patch_children = True

            else:
                children_msg = ("a child commit"
                                if len(children) == 1 else "child commits")
                res = prompt(
                    "Revision D%s has %s.  Would you like to patch the "
                    "full stack?." % (args.revision_id, children_msg),
                    ["Yes", "No", "Always"],
                )
                if res == "Always":
                    config.always_full_stack = True
                    config.write()

                patch_children = res == "Yes" or res == "Always"

            if patch_children:
                if non_linear and not args.yes:
                    logger.warning(
                        "Revision D%s has a non-linear successor graph.\n"
                        "Unable to apply the full stack.",
                        args.revision_id,
                    )
                    res = prompt("Continue with only part of the stack?",
                                 ["Yes", "No"])
                    if res == "No":
                        return

        # Get list of PHIDs in the stack
        try:
            with wait_message("Fetching D%s parents.." % args.revision_id):
                phids = conduit.get_ancestor_phids(revision["phid"])
        except NonLinearException:
            raise Error(
                "Non linear dependency detected. Unable to patch the stack.")

        # Pull revisions data
        if phids:
            with wait_message("Fetching related revisions.."):
                revs.extend(conduit.get_revisions(phids=phids))
            revs.reverse()

        if children and patch_children:
            with wait_message("Fetching related revisions.."):
                revs.extend(conduit.get_revisions(phids=children))

    # Set the target id
    rev_id = revs[-1]["id"]

    logger.info(
        "Patching revision%s: %s",
        "s" if len(revs) > 1 else "",
        " ".join(["D%s" % r["id"] for r in revs]),
    )

    # Pull diffs
    with wait_message("Downloading patch information.."):
        diffs = conduit.get_diffs([r["fields"]["diffPHID"] for r in revs])

    if not args.no_commit and not args.raw:
        for rev in revs:
            diff = diffs[rev["fields"]["diffPHID"]]
            if not diff["attachments"]["commits"]["commits"]:
                raise Error(
                    "A diff without commit information detected in revision D%s.\n"
                    "Use `--no-commit` to patch the working tree." % rev["id"])

    base_node = None
    if not args.raw:
        args.apply_to = args.apply_to or config.apply_patch_to

        if args.apply_to == "base":
            base_node = get_base_ref(diffs[revs[0]["fields"]["diffPHID"]])

            if not base_node:
                raise Error("Base commit not found in diff. "
                            "Use `--apply-to here` to patch current commit.")
        elif args.apply_to != "here":
            base_node = args.apply_to

        if args.apply_to != "here":
            try:
                with wait_message("Checking %s.." % short_node(base_node)):
                    base_node = repo.check_node(base_node)
            except NotFoundError as e:
                msg = "Unknown revision: %s" % short_node(base_node)
                if str(e):
                    msg += "\n%s" % str(e)

                if args.apply_to == "base":
                    msg += "\nUse --apply-to to set the base commit."

                raise Error(msg)

        branch_name = None if args.no_commit else "phab-D%s" % rev_id
        repo.before_patch(base_node, branch_name)

    parent = None
    for rev in revs:
        # Prepare the body using just the data from Phabricator
        body = prepare_body(
            rev["fields"]["title"],
            rev["fields"]["summary"],
            rev["id"],
            repo.phab_url,
            depends_on=parent,
        )
        parent = rev["id"]
        diff = diffs[rev["fields"]["diffPHID"]]
        with wait_message("Downloading D%s.." % rev["id"]):
            raw = conduit.call("differential.getrawdiff",
                               {"diffID": diff["id"]})

        if args.no_commit:
            with wait_message("Applying D%s.." % rev["id"]):
                apply_patch(raw, repo.path)
        else:
            try:
                diff_commits = diff["attachments"]["commits"]["commits"]
                author = "%s <%s>" % (
                    diff_commits[0]["author"]["name"],
                    diff_commits[0]["author"]["email"],
                )
            except (IndexError, KeyError):
                author = None
            try:
                date_created = diff["fields"]["dateCreated"]
            except KeyError:
                date_created = None

            if args.raw:
                # print rather than use logger.info; there's no need for this
                # to be in our logs.
                print(repo.format_patch(raw, body, author, date_created))

            else:
                try:
                    with wait_message("Applying D%s.." % rev["id"]):
                        repo.apply_patch(raw, body, author, date_created)
                except subprocess.CalledProcessError:
                    raise Error("Patch failed to apply")

        if rev["id"] != revs[-1]["id"]:
            logger.info("D%s applied", rev["id"])

    logger.warning("D%s applied", rev_id)