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") logger.warning( "Submitting %s commit%s %s:", len(commits), "" if len(commits) == 1 else "s", "as Work In Progress" if args.wip else "for review", ) 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) # Validate commit stack is suitable for review. show_commit_stack(commits, wip=args.wip, validate=True, ignore_reviewers=args.wip) try: with wait_message("Checking commits.."): repo.check_commits_for_submit( commits, validate_reviewers=not args.wip, 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) # 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) has_commit_reviewers = bool(commit["reviewers"]["granted"] + commit["reviewers"]["request"]) # 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"], commit["title-preview"]) repo.checkout(commit["node"]) # WIP submissions shouldn't set reviewers on phabricator. if args.wip: reviewers = "" else: reviewers = ", ".join(commit["reviewers"]["granted"] + commit["reviewers"]["request"]) # Create arc-annotated commit description. template_vars = dict( title=commit["title-preview"], 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) if is_update: with wait_message("Updating revision..."): rev = conduit.update_revision( commit, has_commit_reviewers, existing_reviewers, diff_phid=diff.phid, wip=args.wip, comment=args.message, check_in_needed=check_in_needed, ) else: with wait_message("Creating a new revision..."): rev = conduit.create_revision( commit, commit["title-preview"], summary, diff.phid, has_commit_reviewers, wip=args.wip, 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 args.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 args.wip and has_commit_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. 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()
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.")
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"])
def submit(repo, args): telemetry().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() 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") if args.command == "uplift": # Perform uplift logic during submission. avoid_local_changes = local_uplift_if_possible(args, repo, commits) else: avoid_local_changes = False 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().submission.preparation_time.stop() telemetry().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().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) # Create a diff if needed with wait_message("Creating local diff..."): diff = repo.get_diff(commit) if diff: telemetry().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"]) # 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"]) if not avoid_local_changes: with wait_message("Updating commit.."): repo.amend_commit(commit, commits) # Diff property has to be set after potential SHA1 change. if 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().submission.process_time.stop()
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 = ( not diff_commits or 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") if commit["wip"]: logger.warning( ' 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"])
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"] if not args.raw: 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) elif args.raw: logger.info(raw) else: diff_commits = diff["attachments"]["commits"]["commits"] author = "%s <%s>" % ( diff_commits[0]["author"]["name"], diff_commits[0]["author"]["email"], ) try: with wait_message("Applying D%s.." % rev["id"]): repo.apply_patch(raw, body, author, diff["fields"]["dateCreated"]) except subprocess.CalledProcessError: raise Error("Patch failed to apply") if not args.raw and rev["id"] != revs[-1]["id"]: logger.info("D%s applied", rev["id"]) if not args.raw: logger.warning("D%s applied", rev_id)
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"])