def test_git_invocation(tmp_path): """ Tests for the git/git_output functions. """ repo_path = tmp_path / 'repo' repo_path.mkdir() assert repo_path.is_dir() # Ensure the dir is there for us to work with. repo_dir = str(repo_path) git('init', git_dir=repo_dir) (repo_path / 'initial').write_text(u'initial') git('add', 'initial', git_dir=repo_dir) # Check that we can report an error on failure. with pytest.raises(GitError) as err: git('add', 'foo', git_dir=repo_dir) assert err.value.stderr.startswith('fatal') assert repr(err.value).startswith('GitError') # Check that errors can be ignored. git('add', 'foo', git_dir=repo_dir, ignore_error=True) output = git_output('commit', '-m', 'initial', git_dir=repo_dir) assert len(output) > 0 # Ensure that the output is stripped. output = git_output('rev-list', 'HEAD', git_dir=repo_dir) assert '\n' not in output output = git_output('rev-list', 'HEAD', git_dir=repo_dir, strip=False) assert '\n' in output # Ensure that commit exists works only for commit hashes. hash = output.strip() assert commit_exists(hash) assert not commit_exists('HEAD') assert not commit_exists(hash + 'abc') assert not commit_exists('000000') # Ensure that we can get the directory of the checkout even when the # working directory is a subdirectory. os.chdir(repo_dir) dir_a = get_current_checkout_directory() (repo_path / 'subdir').mkdir() cwd = os.getcwd() os.chdir(os.path.join(repo_dir, 'subdir')) dir_b = get_current_checkout_directory() os.chdir(cwd) assert dir_a == dir_b assert read_file_or_none('HEAD', 'initial') == 'initial' assert read_file_or_none(hash, 'initial') == 'initial' assert read_file_or_none('HEAD', 'path/does-not-exist') is None assert read_file_or_none('foo', 'initial') is None
def verify_branch_contents(branch_name: str, verbose): logging.basicConfig(level=logging.DEBUG if verbose else logging.WARNING, format='%(levelname)s: %(message)s [%(filename)s:%(lineno)d at %(asctime)s] ',) # Verify that we're in a git checkout. git_path = get_current_checkout_directory() if git_path is None: fatal('not a git repository') os.chdir(git_path) if branch_name.startswith('swift/'): verify_swift_branch(branch_name)
def pr(verbose): """ Tool for working with pull requests """ logging.basicConfig( level=logging.DEBUG if verbose else logging.WARNING, format= '%(levelname)s: %(message)s [%(filename)s:%(lineno)d at %(asctime)s] ', ) # Verify that we're in a git checkout. if not get_current_checkout_directory(): fatal('not a git repository') # Load the config file. config = load_pr_config() if not config: fatal('missing `git apple-llvm pr` configuration file') global pr_tool, ci_test_type ci_test_type = config.test_type if pr_tool: return pr_tool = config.create_tool()
def git_apple_llvm_push(refspec, dry_run, verbose, merge_strategy, push_limit): """ Push changes back to the split Git repositories. """ logging.basicConfig(level=logging.DEBUG if verbose else logging.WARNING, format='%(levelname)s: %(message)s') # Verify that we're in a git checkout. git_path = get_current_checkout_directory() if git_path is None: fatal('not a git repository') os.chdir(git_path) # Figure out the set of remote branches we care about. remote = 'origin' remote_monorepo_branches = [ x.strip() for x in git_output('branch', '-r', '-l').splitlines() ] remote_monorepo_branches = list( filter(lambda x: isKnownTrackingBranch(remote, x), remote_monorepo_branches)) log.info('Branches we care about %s', remote_monorepo_branches) refs = refspec.split(':') if len(refs) < 2: fatal(f'Git refspec "{refspec}" is invalid') source_ref = refs[0] dest_ref = refs[1] remote_dest_ref = f'{remote}/{dest_ref}' # Verify that the source ref is valid and get its commit hash. source_commit_hash = git_output('rev-parse', source_ref, ignore_error=True) if source_commit_hash is None: fatal(f'source Git refspec "{source_ref}" is invalid') # Ensure that the source ref is associated with a ref that can be fetched. git('branch', '-f', MONOREPO_SRC_REF_NAME, source_commit_hash) # Verify that the destination ref is valid and load its push config. dest_commit_hash = git_output('rev-parse', remote_dest_ref, ignore_error=True) if dest_commit_hash is None: fatal(f'destination Git refspec "{dest_ref}" is invalid') push_config = load_push_config(source_commit_hash, dest_ref) if push_config is None: fatal(f'destination Git refspec "{dest_ref}" cannot be pushed to.') # The rev-list command is used to compute the graph we would like to # commit. rev_list = git_output('rev-list', '--boundary', source_commit_hash, '--not', *remote_monorepo_branches, ignore_error=True) if rev_list is None: fatal('unable to determine the commit graph to push') commit_graph = compute_commit_graph(rev_list) if commit_graph is None: print('No commits to commit: everything up-to-date.') return # Prohibit pushing more than 50 commits by default in a bid to avoid # inadvertent mistakes. if push_limit != 0 and len(commit_graph.commits) >= push_limit: fatal( f'pushing {len(commit_graph.commits)} commits, are you really sure?' f'\nPass --push-limit={len(commit_graph.commits)+1} if yes.') click.echo( click.style( f'Preparing to push to {len(commit_graph.commits)} commits:', bold=True)) git('log', '--format=%h %s', '--graph', commit_graph.source_commit_hash, '--not', *commit_graph.roots) message_bodies = git_output('log', '--format=%b', commit_graph.source_commit_hash, '--not', *commit_graph.roots) if 'apple-llvm-split-commit:' in message_bodies: fatal('one or more commits is already present in the split repo') # Prepare the split remotes. split_repos_of_interest = commit_graph.compute_changed_split_repos() click.echo( f'Split repos that should be updated: {", ".join(map(split_dir_to_str, split_repos_of_interest))}\n' ) split_remotes = {} for split_dir in split_repos_of_interest: if not push_config.can_push_to_split_dir(split_dir): fatal( f'push configuration "{push_config.name}" prohibits pushing to "{split_dir}"' ) remote = SplitRemote( split_dir, push_config.repo_mapping[split_dir], push_config.get_split_repo_branch(split_dir, dest_ref)) click.echo( click.style( f'Fetching "{remote.destination_branch}" for {split_dir_to_str(split_dir)}...', bold=True)) try: remote.update_remote() except GitError: fatal( f'failed to fetch from the remote for {split_dir_to_str(split_dir)}.' ) click.echo( 'Fetching monorepo commits from monorepo to the split clone (takes time on first push)...\n' ) remote.update_mono_remote() split_remotes[split_dir] = remote # Regraft the commit history. for split_dir in split_repos_of_interest: click.echo( click.style( f'Regrafting the commits from monorepo to {split_dir_to_str(split_dir)}...', bold=True)) split_remotes[split_dir].begin() split_remotes[ split_dir].regrafted_graph = regraft_commit_graph_onto_split_repo( commit_graph, split_dir) # Merge/rebase the commit history. for split_dir in split_repos_of_interest: click.echo( click.style( f'\nRebasing/merging the {split_dir_to_str(split_dir)} commits...', bold=True)) remote = split_remotes[split_dir] remote.begin() try: remote.commit_hash = merge_commit_graph_with_top_of_branch( remote.regrafted_graph, split_dir, 'origin/' + remote.destination_branch, merge_strategy) except ImpossibleMergeError as err: fatal( f'unable to {err.operation} commits in {split_dir_to_str(split_dir)}.' f'Please rebase your monorepo commits first.') # Once everything is ready, push! for split_dir in split_repos_of_interest: split_remotes[split_dir].push(dry_run)