Skip to content

Commit 0835e72

Browse files
Merge pull request #3087 from boegel/sync_pr_with_develop
add support for --sync-pr-with-develop
2 parents 301c21f + bd1ad33 commit 0835e72

File tree

5 files changed

+225
-33
lines changed

5 files changed

+225
-33
lines changed

easybuild/main.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@
5858
from easybuild.tools.docs import list_software
5959
from easybuild.tools.filetools import adjust_permissions, cleanup, write_file
6060
from easybuild.tools.github import check_github, find_easybuild_easyconfig, install_github_token
61-
from easybuild.tools.github import close_pr, list_prs, new_pr, merge_pr, update_pr
61+
from easybuild.tools.github import close_pr, list_prs, new_pr, merge_pr, sync_pr_with_develop, update_pr
6262
from easybuild.tools.hooks import START, END, load_hooks, run_hook
6363
from easybuild.tools.modules import modules_tool
6464
from easybuild.tools.options import set_up_configuration, use_color
@@ -293,8 +293,8 @@ def main(args=None, logfile=None, do_build=None, testing=False, modtool=None):
293293
categorized_paths = categorize_files_by_type(orig_paths)
294294

295295
# command line options that do not require any easyconfigs to be specified
296-
new_update_preview_pr = options.new_pr or options.update_pr or options.preview_pr
297-
no_ec_opts = [options.aggregate_regtest, options.regtest, search_query, new_update_preview_pr]
296+
pr_options = options.new_pr or options.preview_pr or options.sync_pr_with_develop or options.update_pr
297+
no_ec_opts = [options.aggregate_regtest, options.regtest, pr_options, search_query]
298298

299299
# determine paths to easyconfigs
300300
determined_paths = det_easyconfig_paths(categorized_paths['easyconfigs'])
@@ -355,7 +355,7 @@ def main(args=None, logfile=None, do_build=None, testing=False, modtool=None):
355355
dry_run_mode = options.dry_run or options.dry_run_short or options.missing_modules
356356

357357
# skip modules that are already installed unless forced, or unless an option is used that warrants not skipping
358-
if not (forced or dry_run_mode or options.extended_dry_run or new_update_preview_pr or options.inject_checksums):
358+
if not (forced or dry_run_mode or options.extended_dry_run or pr_options or options.inject_checksums):
359359
retained_ecs = skip_available(easyconfigs, modtool)
360360
if not testing:
361361
for skipped_ec in [ec for ec in easyconfigs if ec not in retained_ecs]:
@@ -366,26 +366,30 @@ def main(args=None, logfile=None, do_build=None, testing=False, modtool=None):
366366
if len(easyconfigs) > 0:
367367
# resolve dependencies if robot is enabled, except in dry run mode
368368
# one exception: deps *are* resolved with --new-pr or --update-pr when dry run mode is enabled
369-
if options.robot and (not dry_run_mode or new_update_preview_pr):
369+
if options.robot and (not dry_run_mode or pr_options):
370370
print_msg("resolving dependencies ...", log=_log, silent=testing)
371371
ordered_ecs = resolve_dependencies(easyconfigs, modtool)
372372
else:
373373
ordered_ecs = easyconfigs
374-
elif new_update_preview_pr:
374+
elif pr_options:
375375
ordered_ecs = None
376376
else:
377377
print_msg("No easyconfigs left to be built.", log=_log, silent=testing)
378378
ordered_ecs = []
379379

380380
# creating/updating PRs
381-
if new_update_preview_pr:
381+
if pr_options:
382382
if options.new_pr:
383383
new_pr(categorized_paths, ordered_ecs, title=options.pr_title, descr=options.pr_descr,
384384
commit_msg=options.pr_commit_msg)
385385
elif options.preview_pr:
386386
print(review_pr(paths=determined_paths, colored=use_color(options.color)))
387-
else:
387+
elif options.sync_pr_with_develop:
388+
sync_pr_with_develop(options.sync_pr_with_develop)
389+
elif options.update_pr:
388390
update_pr(options.update_pr, categorized_paths, ordered_ecs, commit_msg=options.pr_commit_msg)
391+
else:
392+
raise EasyBuildError("Unknown PR option!")
389393

390394
# dry_run: print all easyconfigs and dependencies, and whether they are already built
391395
elif dry_run_mode:

easybuild/tools/github.py

Lines changed: 122 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@
8585
GITHUB_DIR_TYPE = u'dir'
8686
GITHUB_EB_MAIN = 'easybuilders'
8787
GITHUB_EASYCONFIGS_REPO = 'easybuild-easyconfigs'
88+
GITHUB_DEVELOP_BRANCH = 'develop'
8889
GITHUB_FILE_TYPE = u'file'
8990
GITHUB_PR_STATE_OPEN = 'open'
9091
GITHUB_PR_STATES = [GITHUB_PR_STATE_OPEN, 'closed', 'all']
@@ -808,35 +809,73 @@ def _easyconfigs_pr_common(paths, ecs, start_branch=None, pr_branch=None, start_
808809
# commit
809810
git_repo.index.commit(commit_msg)
810811

811-
# push to GitHub
812-
github_url = '[email protected]:%s/%s.git' % (target_account, pr_target_repo)
812+
push_branch_to_github(git_repo, target_account, pr_target_repo, pr_branch)
813+
814+
return file_info, deleted_paths, git_repo, pr_branch, diff_stat
815+
816+
817+
def create_remote(git_repo, account, repo, https=False):
818+
"""
819+
Create remote in specified git working directory for specified account & repository.
820+
821+
:param git_repo: git.Repo instance to use (after init_repo & setup_repo)
822+
:param account: GitHub account name
823+
:param repo: repository name
824+
:param https: use https:// URL rather than git@
825+
"""
826+
827+
if https:
828+
github_url = 'https://github.com/%s/%s.git' % (account, repo)
829+
else:
830+
github_url = '[email protected]:%s/%s.git' % (account, repo)
831+
813832
salt = ''.join(random.choice(ascii_letters) for _ in range(5))
814-
remote_name = 'github_%s_%s' % (target_account, salt)
833+
remote_name = 'github_%s_%s' % (account, salt)
834+
835+
try:
836+
remote = git_repo.create_remote(remote_name, github_url)
837+
except GitCommandError as err:
838+
raise EasyBuildError("Failed to create remote %s for %s: %s", remote_name, github_url, err)
839+
840+
return remote
841+
842+
843+
def push_branch_to_github(git_repo, target_account, target_repo, branch):
844+
"""
845+
Push specified branch to GitHub from specified git repository.
846+
847+
:param git_repo: git.Repo instance to use (after init_repo & setup_repo)
848+
:param target_account: GitHub account name
849+
:param target_repo: repository name
850+
:param branch: name of branch to push
851+
"""
852+
853+
# push to GitHub
854+
remote = create_remote(git_repo, target_account, target_repo)
815855

816856
dry_run = build_option('dry_run') or build_option('extended_dry_run')
817857

818-
push_branch_msg = "pushing branch '%s' to remote '%s' (%s)" % (pr_branch, remote_name, github_url)
858+
github_url = '[email protected]:%s/%s.git' % (target_account, target_repo)
859+
860+
push_branch_msg = "pushing branch '%s' to remote '%s' (%s)" % (branch, remote.name, github_url)
819861
if dry_run:
820862
print_msg(push_branch_msg + ' [DRY RUN]', log=_log)
821863
else:
822864
print_msg(push_branch_msg, log=_log)
823865
try:
824-
my_remote = git_repo.create_remote(remote_name, github_url)
825-
res = my_remote.push(pr_branch)
866+
res = remote.push(branch)
826867
except GitCommandError as err:
827-
raise EasyBuildError("Failed to push branch '%s' to GitHub (%s): %s", pr_branch, github_url, err)
868+
raise EasyBuildError("Failed to push branch '%s' to GitHub (%s): %s", branch, github_url, err)
828869

829870
if res:
830871
if res[0].ERROR & res[0].flags:
831872
raise EasyBuildError("Pushing branch '%s' to remote %s (%s) failed: %s",
832-
pr_branch, my_remote, github_url, res[0].summary)
873+
branch, remote, github_url, res[0].summary)
833874
else:
834-
_log.debug("Pushed branch %s to remote %s (%s): %s", pr_branch, my_remote, github_url, res[0].summary)
875+
_log.debug("Pushed branch %s to remote %s (%s): %s", branch, remote, github_url, res[0].summary)
835876
else:
836877
raise EasyBuildError("Pushing branch '%s' to remote %s (%s) failed: empty result",
837-
pr_branch, my_remote, github_url)
838-
839-
return file_info, deleted_paths, git_repo, pr_branch, diff_stat
878+
branch, remote, github_url)
840879

841880

842881
def is_patch_for(patch_name, ec):
@@ -1359,41 +1398,55 @@ def new_pr(paths, ecs, title=None, descr=None, commit_msg=None):
13591398
_log.info("Failed to add labels to PR# %s: %s." % (pr, err))
13601399

13611400

1401+
def det_account_branch_for_pr(pr_id, github_user=None):
1402+
"""Determine account & branch corresponding to pull request with specified id."""
1403+
1404+
if github_user is None:
1405+
github_user = build_option('github_user')
1406+
1407+
if github_user is None:
1408+
raise EasyBuildError("GitHub username (--github-user) must be specified!")
1409+
1410+
pr_target_account = build_option('pr_target_account')
1411+
pr_target_repo = build_option('pr_target_repo')
1412+
1413+
pr_data, _ = fetch_pr_data(pr_id, pr_target_account, pr_target_repo, github_user)
1414+
1415+
# branch that corresponds with PR is supplied in form <account>:<branch_label>
1416+
account = pr_data['head']['label'].split(':')[0]
1417+
branch = ':'.join(pr_data['head']['label'].split(':')[1:])
1418+
github_target = '%s/%s' % (pr_target_account, pr_target_repo)
1419+
print_msg("Determined branch name corresponding to %s PR #%s: %s" % (github_target, pr_id, branch), log=_log)
1420+
1421+
return account, branch
1422+
1423+
13621424
@only_if_module_is_available('git', pkgname='GitPython')
1363-
def update_pr(pr, paths, ecs, commit_msg=None):
1425+
def update_pr(pr_id, paths, ecs, commit_msg=None):
13641426
"""
13651427
Update specified pull request using specified files
13661428
1367-
:param pr: ID of pull request to update
1429+
:param pr_id: ID of pull request to update
13681430
:param paths: paths to categorized lists of files (easyconfigs, files to delete, patches)
13691431
:param ecs: list of parsed easyconfigs, incl. for dependencies (if robot is enabled)
13701432
:param commit_msg: commit message to use
13711433
"""
1372-
github_user = build_option('github_user')
1373-
if github_user is None:
1374-
raise EasyBuildError("GitHub user must be specified to use --update-pr")
13751434

13761435
if commit_msg is None:
13771436
raise EasyBuildError("A meaningful commit message must be specified via --pr-commit-msg when using --update-pr")
13781437

13791438
pr_target_account = build_option('pr_target_account')
13801439
pr_target_repo = build_option('pr_target_repo')
13811440

1382-
pr_data, _ = fetch_pr_data(pr, pr_target_account, pr_target_repo, github_user)
1383-
1384-
# branch that corresponds with PR is supplied in form <account>:<branch_label>
1385-
account = pr_data['head']['label'].split(':')[0]
1386-
branch = ':'.join(pr_data['head']['label'].split(':')[1:])
1387-
github_target = '%s/%s' % (pr_target_account, pr_target_repo)
1388-
print_msg("Determined branch name corresponding to %s PR #%s: %s" % (github_target, pr, branch), log=_log)
1441+
account, branch = det_account_branch_for_pr(pr_id)
13891442

13901443
_, _, _, _, diff_stat = _easyconfigs_pr_common(paths, ecs, start_branch=branch, pr_branch=branch,
13911444
start_account=account, commit_msg=commit_msg)
13921445

13931446
print_msg("Overview of changes:\n%s\n" % diff_stat, log=_log, prefix=False)
13941447

13951448
full_repo = '%s/%s' % (pr_target_account, pr_target_repo)
1396-
msg = "Updated %s PR #%s by pushing to branch %s/%s" % (full_repo, pr, account, branch)
1449+
msg = "Updated %s PR #%s by pushing to branch %s/%s" % (full_repo, pr_id, account, branch)
13971450
if build_option('dry_run') or build_option('extended_dry_run'):
13981451
msg += " [DRY RUN]"
13991452
print_msg(msg, log=_log, prefix=False)
@@ -1777,3 +1830,47 @@ def reviews_url(gh):
17771830
pr_data['reviews'] = reviews_data
17781831

17791832
return pr_data, pr_url
1833+
1834+
1835+
def sync_pr_with_develop(pr_id):
1836+
"""Sync pull request with specified ID with current develop branch."""
1837+
github_user = build_option('github_user')
1838+
if github_user is None:
1839+
raise EasyBuildError("GitHub user must be specified to use --sync-pr-with-develop")
1840+
1841+
target_account = build_option('pr_target_account')
1842+
target_repo = build_option('pr_target_repo')
1843+
1844+
pr_account, pr_branch = det_account_branch_for_pr(pr_id)
1845+
1846+
# initialize repository
1847+
git_working_dir = tempfile.mkdtemp(prefix='git-working-dir')
1848+
git_repo = init_repo(git_working_dir, target_repo)
1849+
1850+
setup_repo(git_repo, pr_account, target_repo, pr_branch)
1851+
1852+
# pull in latest version of 'develop' branch from central repository
1853+
msg = "pulling latest version of '%s' branch from %s/%s..." % (target_account, target_repo, GITHUB_DEVELOP_BRANCH)
1854+
print_msg(msg, log=_log)
1855+
easybuilders_remote = create_remote(git_repo, target_account, target_repo, https=True)
1856+
pull_out = git_repo.git.pull(easybuilders_remote.name, GITHUB_DEVELOP_BRANCH)
1857+
_log.debug("Output of 'git pull %s %s': %s", easybuilders_remote.name, GITHUB_DEVELOP_BRANCH, pull_out)
1858+
1859+
# create 'develop' branch (with force if one already exists),
1860+
# and check it out to check git log
1861+
git_repo.create_head(GITHUB_DEVELOP_BRANCH, force=True).checkout()
1862+
git_log_develop = git_repo.git.log('-n 3')
1863+
_log.debug("Top of 'git log' for %s branch:\n%s", GITHUB_DEVELOP_BRANCH, git_log_develop)
1864+
1865+
# checkout PR branch, and merge develop branch in it (which will create a merge commit)
1866+
print_msg("merging '%s' branch into PR branch '%s'..." % (GITHUB_DEVELOP_BRANCH, pr_branch), log=_log)
1867+
git_repo.git.checkout(pr_branch)
1868+
merge_out = git_repo.git.merge(GITHUB_DEVELOP_BRANCH)
1869+
_log.debug("Output of 'git merge %s':\n%s", GITHUB_DEVELOP_BRANCH, merge_out)
1870+
1871+
# check git log, should show merge commit on top
1872+
post_merge_log = git_repo.git.log('-n 3')
1873+
_log.debug("Top of 'git log' after 'git merge %s':\n%s", GITHUB_DEVELOP_BRANCH, post_merge_log)
1874+
1875+
# push updated branch back to GitHub (unless we're doing a dry run)
1876+
return push_branch_to_github(git_repo, pr_account, target_repo, pr_branch)

easybuild/tools/options.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -605,6 +605,8 @@ def github_options(self):
605605
'pr-target-repo': ("Target repository for new/updating PRs", str, 'store', GITHUB_EASYCONFIGS_REPO),
606606
'pr-title': ("Title for new pull request created with --new-pr", str, 'store', None),
607607
'preview-pr': ("Preview a new pull request", None, 'store_true', False),
608+
'sync-pr-with-develop': ("Sync pull request with current 'develop' branch",
609+
int, 'store', None, {'metavar': 'PR#'}),
608610
'review-pr': ("Review specified pull request", int, 'store', None, {'metavar': 'PR#'}),
609611
'test-report-env-filter': ("Regex used to filter out variables in environment dump of test report",
610612
None, 'regex', None),

test/framework/github.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -636,6 +636,65 @@ def test_create_delete_gist(self):
636636
gist_id = gist_url.split('/')[-1]
637637
gh.delete_gist(gist_id, github_user=GITHUB_TEST_ACCOUNT, github_token=self.github_token)
638638

639+
def test_det_account_branch_for_pr(self):
640+
"""Test det_account_branch_for_pr."""
641+
if self.skip_github_tests:
642+
print("Skipping test_det_account_branch_for_pr, no GitHub token available?")
643+
return
644+
645+
init_config(build_options={
646+
'pr_target_account': 'easybuilders',
647+
'pr_target_repo': 'easybuild-easyconfigs',
648+
})
649+
650+
# see https://github.com/easybuilders/easybuild-easyconfigs/pull/9149
651+
self.mock_stdout(True)
652+
account, branch = gh.det_account_branch_for_pr(9149, github_user=GITHUB_TEST_ACCOUNT)
653+
self.mock_stdout(False)
654+
self.assertEqual(account, 'boegel')
655+
self.assertEqual(branch, '20191017070734_new_pr_EasyBuild401')
656+
657+
init_config(build_options={
658+
'pr_target_account': 'easybuilders',
659+
'pr_target_repo': 'easybuild-framework',
660+
})
661+
662+
# see https://github.com/easybuilders/easybuild-framework/pull/3069
663+
self.mock_stdout(True)
664+
account, branch = gh.det_account_branch_for_pr(3069, github_user=GITHUB_TEST_ACCOUNT)
665+
self.mock_stdout(False)
666+
self.assertEqual(account, 'migueldiascosta')
667+
self.assertEqual(branch, 'fix_inject_checksums')
668+
669+
def test_push_branch_to_github(self):
670+
"""Test push_branch_to_github."""
671+
672+
build_options = {'dry_run': True}
673+
init_config(build_options=build_options)
674+
675+
git_repo = gh.init_repo(self.test_prefix, GITHUB_REPO)
676+
branch = 'test123'
677+
678+
self.mock_stderr(True)
679+
self.mock_stdout(True)
680+
gh.setup_repo(git_repo, GITHUB_USER, GITHUB_REPO, 'master')
681+
git_repo.create_head(branch, force=True)
682+
gh.push_branch_to_github(git_repo, GITHUB_USER, GITHUB_REPO, branch)
683+
stderr = self.get_stderr()
684+
stdout = self.get_stdout()
685+
self.mock_stderr(True)
686+
self.mock_stdout(True)
687+
688+
self.assertEqual(stderr, '')
689+
690+
github_path = '%s/%s.git' % (GITHUB_USER, GITHUB_REPO)
691+
pattern = r'^' + '\n'.join([
692+
r"== fetching branch 'master' from https://github.com/%s\.\.\." % github_path,
693+
r"== pushing branch 'test123' to remote 'github_.*' \([email protected]:%s\) \[DRY RUN\]" % github_path,
694+
]) + r'$'
695+
regex = re.compile(pattern)
696+
self.assertTrue(regex.match(stdout.strip()), "Pattern '%s' doesn't match: %s" % (regex.pattern, stdout))
697+
639698

640699
def suite():
641700
""" returns all the testcases in this module """

test/framework/options.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3030,6 +3030,36 @@ def test_new_update_pr(self):
30303030
]
30313031
self._assert_regexs(regexs, txt, assert_true=False)
30323032

3033+
def test_sync_pr_with_develop(self):
3034+
"""Test use of --sync-pr-with-develop (dry run only)."""
3035+
if self.github_token is None:
3036+
print("Skipping test_sync_pr_with_develop, no GitHub token available?")
3037+
return
3038+
3039+
# use https://github.com/easybuilders/easybuild-easyconfigs/pull/9150,
3040+
# which is a PR from boegel:develop to easybuilders:develop
3041+
# (to sync 'develop' branch in boegel's fork with central develop branch);
3042+
# we need to test with a branch that is guaranteed to stay in place for the test to work,
3043+
# since it will actually be downloaded (only the final push to update the branch is skipped under --dry-run)
3044+
args = [
3045+
'--github-user=%s' % GITHUB_TEST_ACCOUNT,
3046+
'--sync-pr-with-develop=9150',
3047+
'--dry-run',
3048+
]
3049+
txt, _ = self._run_mock_eb(args, do_build=True, raise_error=True, testing=False)
3050+
3051+
github_path = r"boegel/easybuild-easyconfigs\.git"
3052+
pattern = '\n'.join([
3053+
r"== temporary log file in case of crash .*",
3054+
r"== Determined branch name corresponding to easybuilders/easybuild-easyconfigs PR #9150: develop",
3055+
r"== fetching branch 'develop' from https://github\.com/%s\.\.\." % github_path,
3056+
r"== pulling latest version of 'easybuilders' branch from easybuild-easyconfigs/develop\.\.\.",
3057+
r"== merging 'develop' branch into PR branch 'develop'\.\.\.",
3058+
r"== pushing branch 'develop' to remote '.*' \(git@github\.com:%s\) \[DRY RUN\]" % github_path,
3059+
])
3060+
regex = re.compile(pattern)
3061+
self.assertTrue(regex.match(txt), "Pattern '%s' doesn't match: %s" % (regex.pattern, txt))
3062+
30333063
def test_new_pr_python(self):
30343064
"""Check generated PR title for --new-pr on easyconfig that includes Python dependency."""
30353065
if self.github_token is None:

0 commit comments

Comments
 (0)