def determine_whether_to_auto_fetch( cli: CLIUX, opts: ValidateOptions, active_branch_tracked_ref: RemoteReference): user_response = "" if opts.is_auto_fetch_enabled() == True: cli.debug( 'User provided --auto-fetch CLI argument. Proceeding as instructed' ) user_response = 'y' elif opts.is_auto_fetch_enabled() == False: cli.debug( 'User provided --no-auto-fetch CLI argument. Halting as instructed' ) raise UserBypassException( f"""user decided not to download new commits from { active_branch_tracked_ref.name}""") while user_response not in ['y', 'Y']: user_response = input( "Would you like to download these new commits? [y/n]") if user_response in ['N', 'n']: cli.debug( f"When asked whether we can download new commits, user response was {user_response}" ) raise UserBypassException( f"""user decided not to download new commits from { active_branch_tracked_ref.name}""") if user_response not in ['y', 'Y']: cli.warning( f"Invalid response detected: '{user_response}'. Please answer 'Y' or 'N'." )
async def validate_branches_and_merge_bases(cli: CLIUX, repo: Repo, opts: ValidateOptions): active_branch = get_branch_information( repo, await opts.get_current_branch_name(repo)) # current_branch cli.debug( f"active branch: {active_branch.name} @ {active_branch.commit.hexsha}") default_branch = get_branch_information( repo, await get_default_git_branch(repo)) # default branch cli.debug( f"default branch: {default_branch.name} @ {default_branch.commit.hexsha}" ) if active_branch == default_branch: raise NonApplicableSituationException( f"You are on the default branch ({default_branch.name})", """git_guardrails is intended to catch potential problems when pushing review branches, and will not take any action when on a git repo's default branch""" ) cli.debug('validating merge base between review branch and default branch') validate_merge_bases_with_default_branch( cli=cli, repo=repo, active_branch_name=active_branch, default_branch_name=default_branch) cli.debug('merge base validation complete') return (active_branch, default_branch)
async def validate(verbose: bool, cwd: str, enabled: bool, current_branch: str, color: bool, tty: bool, auto_fetch: bool, commit_count_soft_fail_threshold: bool, commit_count_hard_fail_threshold: bool, commit_count_auto_bypass_soft_fail: bool): """Examine the current Git workspace and perform some sanity-checking""" cliOptions = ValidateCLIOptions( verbose=verbose, cwd=cwd, current_branch=current_branch, color=color, tty=tty, auto_fetch=auto_fetch, commit_count_soft_fail_threshold=commit_count_soft_fail_threshold, commit_count_hard_fail_threshold=commit_count_hard_fail_threshold, commit_count_auto_bypass_soft_fail=commit_count_auto_bypass_soft_fail) opts = ValidateOptions(cliOptions) log_level = DEBUG if opts.is_verbose() else INFO cli = CLIUX(log_level=log_level, supports_color=opts.is_terminal_color_supported(), supports_tty=opts.is_terminal_tty_supported()) if (enabled == False): print( "skipping validation, due to '--no-enabled' CLI arg, or GIT_GUARDRAILS_ENABLED=False env variable" ) return await do_validate(cli, opts)
def fake_cliux( supports_color: bool = False, supports_tty: bool = False, log_level: int = INFO ) -> Iterator[Tuple[CLIUX, Callable[[], List[str]]]]: with fake_logger(log_level) as (my_logger, get_lines): cli = CLIUX(supports_color=supports_color, supports_tty=supports_tty, log_level=log_level, logger=my_logger) yield cli, get_lines
def analyze_review_branch_tracking_situation(cli: CLIUX, repo: Repo, active_branch: Head): active_branch_tracked_ref = active_branch.tracking_branch() if (active_branch_tracked_ref is None): raise NonApplicableSituationException( f"Branch {active_branch.name} does not track a remote branch", "The currently active branch does not track a remote branch yet, perhaps you have not pushed it yet?" ) cli.info( f"""determined that local branch {format_branch_name(active_branch.name)} tracks upstream branch { format_branch_name(active_branch_tracked_ref.remote_head) } on remote {format_remote_name(active_branch_tracked_ref.remote_name)}""" ) with yaspin().white.bold as sp: sp.text = f"searching for new upstream commits on {active_branch_tracked_ref.name}" sp.start() latest_remote_sha = git_ls_remote(repo=repo, ref=active_branch_tracked_ref, ref_types=["heads"]) latest_local_sha = active_branch_tracked_ref.commit.hexsha sp.stop() cli.debug( f"latest commit for local ref {active_branch_tracked_ref.name}: {latest_local_sha}" ) cli.debug( f"""latest commit for tracked branch {active_branch_tracked_ref.remote_head } on remote {active_branch_tracked_ref.remote_name}: {latest_remote_sha}""" ) return (latest_remote_sha, active_branch_tracked_ref)
def offer_to_fetch_from_upstream(cli: CLIUX, repo: Repo, opts: ValidateOptions, active_branch: Head, active_branch_tracked_ref: RemoteReference): cli.warning(f"""New commits on {active_branch_tracked_ref } were detected, which have not yet been pulled down to {active_branch.name}""" ) determine_whether_to_auto_fetch(cli, opts, active_branch_tracked_ref) origin: Remote = repo.remotes['origin'] refspec = f"{active_branch.name}:{active_branch_tracked_ref.name}" cli.info( f"Fetching new commits for branch {active_branch_tracked_ref.name}") cli.debug( f"running 'git fetch' from remote '{origin.name}' with refspec '{refspec}'" ) origin.fetch() cli.info(f"Fetch from {origin.name} complete")
def validate_merge_bases_with_default_branch( cli: CLIUX, repo: Repo, active_branch_name: str, default_branch_name: str) -> Commit: merge_bases = repo.merge_base(default_branch_name, active_branch_name) merge_bases_csv = ", ".join(map(lambda c: c.hexsha[0:8], merge_bases)) cli.debug(f"merge_bases: {merge_bases_csv}") num_merge_bases = len(merge_bases) if (num_merge_bases == 0): raise UnhandledSituationException( 'No merge-base commit found', f"""No merge-base commits between branches {format_branch_name(default_branch_name) } and { format_branch_name(active_branch_name) } could be found, so validation could not be performed.""" ) if (num_merge_bases > 1): description = " ".join([ f"Multiple ({format_integer(num_merge_bases)}) merge-base commits between branches", format_branch_name(default_branch_name), "and", format_branch_name(active_branch_name), "were found:" ]) commits_description = reduce(lambda a, b: f"{a}\n- {b}", map(format_commit, merge_bases), "") raise UnhandledSituationException('Multiple merge-base commits found', description + commits_description) return merge_bases[0]
async def do_validate(cli: CLIUX, opts: ValidateOptions): try: cwd = opts.get_cwd() # working directory repo = Repo(cwd) # git repo validate_remotes(repo=repo) (active_branch, default_branch) = await validate_branches_and_merge_bases(cli=cli, repo=repo, opts=opts) (latest_remote_sha, active_branch_tracked_ref) = analyze_review_branch_tracking_situation( cli, repo, active_branch) has_latest_commits_from_upstream = git_does_commit_exist_locally( repo=repo, sha=latest_remote_sha) if (has_latest_commits_from_upstream == False): offer_to_fetch_from_upstream( cli=cli, repo=repo, opts=opts, active_branch=active_branch, active_branch_tracked_ref=active_branch_tracked_ref) cli.info( f"Comparing {active_branch.name} against {active_branch_tracked_ref.name}" ) merge_base = validate_merge_bases_with_default_branch( cli, repo, repo.active_branch.name, active_branch_tracked_ref.name) cli.debug(f"Identified common commit {merge_base.hexsha[0:8]}") cli.debug(f"Local sha: {active_branch.commit.hexsha}") cli.debug(f"Upstream sha: {active_branch_tracked_ref.commit.hexsha}") new_local_commits = get_truncated_log(repo, active_branch.commit, merge_base.hexsha) new_upstream_commits = get_truncated_log( repo, active_branch_tracked_ref.commit, merge_base.hexsha) cli.debug(f"new local commits: {new_local_commits}") cli.debug(f"new upstream commits: {new_upstream_commits}") if (len(new_local_commits) > opts.get_commit_count_hard_fail_threshold()): raise LikelyUserErrorException( "Very large number of review branch commits", f"""An very large {len(new_local_commits)} number of commits were detected on review branch { active_branch.name }, which were not found on tracked branch {active_branch_tracked_ref.name }. {format_highlight("This may be an indication of an improper rebase!")} This warning is presented whenever more than {format_integer(opts.get_commit_count_hard_fail_threshold()) } new commits that have not yet been pushed are found on a review branch. Please take a close look at your review branch, and ensure you don't see any duplicate commits that are already on { default_branch.name}""") elif (len(new_local_commits) > opts.get_commit_count_soft_fail_threshold()): raise UserBypassableWarning( "Large number of review branch commits", f"""An unusually large {format_integer(len(new_local_commits))} number of commits were detected on review branch { active_branch.name }, which were not found on tracked branch {active_branch_tracked_ref.name }. {format_highlight("This may be an indication of an improper rebase!")} This warning is presented whenever more than {opts.get_commit_count_soft_fail_threshold() } new commits, that have not yet been pushed, are found on a review branch. Please take a close look at your review branch, and ensure you don't see any duplicate commits that are already on { default_branch.name}""") except UserBypassException as ex: cli.handle_user_bypass_exception(ex) except UserBypassableWarning as ex: cli.handle_user_bypassable_warning( ex, bypass_response=("continue" if opts.should_auto_bypass_commit_count_soft_fail() else None)) except NonApplicableSituationException as ex: cli.handle_non_applicable_situation_exception(ex) except UnhandledSituationException as ex: cli.handle_unhandled_situation_exception(ex) except InvalidGitRepositoryError: cli.error( f"""git_guardrails is only intended for use within a git repository directory {cwd} does not seem to be a git repository""")
def test_cliux_creation(): cli = CLIUX(supports_color=False, log_level=DEBUG, supports_tty=False) assert cli is not None