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 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 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)