def test_push_branch_with_retry(self, mock_sleep, mock_git): mock_repo = mock.Mock() mock_repo.active_branch = mock.Mock() mock_repo.active_branch.name = 'womp' mock_repo.git = mock.Mock() mock_repo.git.checkout = mock.Mock() mock_git.Repo = mock.Mock(return_value=mock_repo) mock_ret_f = mock.Mock() mock_ret_f.flags = 1 mock_ret_f.ERROR = 0 mock_ret_f.summary = "mock failure summary" mock_ret_ok = mock.Mock() mock_ret_ok.flags = 0 mock_ret_ok.ERROR = 1 mock_repo.remotes = mock.Mock() mock_repo.remotes.origin = mock.Mock() mock_repo.remotes.origin.push = mock.Mock( side_effect=[[mock_ret_f], [mock_ret_f], [mock_ret_ok]]) ghc = GitHubContext('REPO') ghc.push_branch_with_retry('womp', attempts=3, cooloff=2) self.assertEqual(mock_repo.remotes.origin.push.call_count, 3) mock_repo.remotes.origin.push = mock.Mock( side_effect=GitCommandError('B', 128)) self.assertRaises(RuntimeError, ghc.push_branch_with_retry, 'womp', attempts=3, cooloff=2)
def test_push_branch_with_retry(self, mock_sleep, mock_git): mock_repo = mock.Mock() mock_repo.active_branch = mock.Mock() mock_repo.active_branch.name = 'womp' mock_repo.git = mock.Mock() mock_repo.git.checkout = mock.Mock() mock_git.Repo = mock.Mock(return_value=mock_repo) mock_ret_f = mock.Mock() mock_ret_f.flags = 1 mock_ret_f.ERROR = 0 mock_ret_f.summary = "mock failure summary" mock_ret_ok = mock.Mock() mock_ret_ok.flags = 0 mock_ret_ok.ERROR = 1 mock_repo.remotes = mock.Mock() mock_repo.remotes.origin = mock.Mock() mock_repo.remotes.origin.push = mock.Mock( side_effect=[[mock_ret_f], [mock_ret_f], [mock_ret_ok]] ) ghc = GitHubContext('REPO') ghc.push_branch_with_retry('womp', attempts=3, cooloff=2) self.assertEqual(mock_repo.remotes.origin.push.call_count, 3) mock_repo.remotes.origin.push = mock.Mock(side_effect=GitCommandError('B', 128)) self.assertRaises(RuntimeError, ghc.push_branch_with_retry, 'womp', attempts=3, cooloff=2)
def test_tag_release(self, mock_git): """test tag_release call""" mock_repo = mock.Mock() mock_repo.active_branch = mock.Mock() mock_repo.active_branch.name = 'master' mock_repo.git = mock.Mock() mock_repo.git.checkout = mock.Mock() mock_tag1 = mock.Mock() mock_tag1.name = '0.0.0' mock_tag2 = mock.Mock() mock_tag2.name = '0.0.1' mock_repo.tags = [ mock_tag1, mock_tag2 ] mock_repo.create_tag = mock.Mock() mock_git.Repo = mock.Mock(return_value=mock_repo) mock_repo.remotes = mock.Mock() mock_repo.remotes.origin = mock.Mock() mock_repo.remotes.origin.push = mock.Mock() ghc = GitHubContext('REPO') ghc.tag_release('0.0.2', 'master', push=True, attempts=2, cooloff=0) self.failUnless(mock_repo.create_tag.called) self.failUnless(mock_repo.remotes.origin.push.called)
def test_merge_branch(self, mock_git): """test merge branch""" mock_repo = mock.Mock() mock_repo.git = mock.Mock() mock_repo.git.merge = mock.Mock() mock_git.Repo = mock.Mock(return_value=mock_repo) ghc = GitHubContext('REPO') ghc.merge_branch('develop') self.assertTrue(mock_repo.git.merge.called)
def test_tag_release_retry(self, mock_time, mock_git): """test repeated tries to push tags""" mock_repo = mock.Mock() mock_repo.active_branch = mock.Mock() mock_repo.active_branch.name = 'master' mock_repo.git = mock.Mock() mock_repo.git.checkout = mock.Mock() mock_tag1 = mock.Mock() mock_tag1.name = '0.0.0' mock_tag2 = mock.Mock() mock_tag2.name = '0.0.1' mock_repo.tags = [mock_tag1, mock_tag2] mock_repo.create_tag = mock.Mock() mock_git.Repo = mock.Mock(return_value=mock_repo) mock_repo.remotes = mock.Mock() mock_repo.remotes.origin = mock.Mock() mock_repo.remotes.origin.push = mock.Mock( side_effect=RuntimeError("push it real good")) mock_time.sleep = mock.Mock() ghc = GitHubContext('REPO') self.assertRaises(RuntimeError, ghc.tag_release, '0.0.2', 'master', push=True, attempts=5, cooloff=0) self.assertEqual(mock_repo.remotes.origin.push.call_count, 5) self.assertEqual(mock_time.sleep.call_count, 5) self.failUnless(mock_repo.create_tag.called)
def plusone_pr(opts): """ _plusone_pr_ Set the +1 context status for the PR """ repo_dir = os.getcwd() with GitHubContext(repo_dir) as ghc: ghc.plus_one_pull_request(pr_id=opts.id, context=opts.plus_one_context)
def list_feature_branches(opts): """ list unmerged feature branches """ repo_dir = os.getcwd() print "unmerged feature branches:" with GitHubContext(repo_dir) as ghc: for x in ghc.iter_git_feature_branches(merged=False): print x
def list_feature_branches(opts): """ list unmerged feature branches """ repo_dir = repo_directory() print("unmerged feature branches:") with GitHubContext(repo_dir) as ghc: for x in ghc.iter_git_feature_branches(merged=False): print(x)
def review_pr(opts): """ _review_pr_ Add a review comment to a PR, and optionally set the plus one context status """ repo_dir = os.getcwd() with GitHubContext(repo_dir) as ghc: ghc.review_pull_request(opts.id, opts.comment, opts.plus_one, opts.plus_one_context)
def get_pr(opts): """ _get_pr_ Print the details of the pr specified by the CLI opts """ repo_dir = os.getcwd() with GitHubContext(repo_dir) as ghc: pr_data = ghc.pull_request_details(opts.id) pprint.pprint(pr_data, indent=2)
def list_prs(opts): """ _list_prs_ Print a summary of all open PRs for this repo optionally filtered by user """ repo_dir = os.getcwd() with GitHubContext(repo_dir) as ghc: print " ID, User, Title" for pr in ghc.pull_requests(user=opts.user): print pr['number'], pr['user']['login'], pr['title']
def test_push_branch(self, mock_git): mock_repo = mock.Mock() mock_repo.active_branch = mock.Mock() mock_repo.active_branch.name = 'womp' mock_repo.head = "HEAD" mock_repo.git = mock.Mock() mock_repo.git.checkout = mock.Mock() mock_git.Repo = mock.Mock(return_value=mock_repo) mock_repo.remotes = mock.Mock() mock_ret = mock.Mock() mock_ret.flags = 1000 mock_ret.ERROR = 10000 mock_ret.summary = "SUMMARY" mock_repo.remotes.origin = mock.Mock() mock_repo.remotes.origin.push = mock.Mock(return_value=[mock_ret]) ghc = GitHubContext('REPO') ghc.push_branch('womp') self.failUnless(mock_repo.remotes.origin.push.called) mock_repo.remotes.origin.push.assert_has_calls([mock.call('HEAD')]) mock_repo.remotes.origin.push = mock.Mock(side_effect=GitCommandError('A', 128)) self.assertRaises(RuntimeError, ghc.push_branch, 'womp2')
def test_tag_release(self, mock_git): """test tag_release call""" mock_repo = mock.Mock() mock_repo.active_branch = mock.Mock() mock_repo.active_branch.name = 'master' mock_repo.git = mock.Mock() mock_repo.git.checkout = mock.Mock() mock_tag1 = mock.Mock() mock_tag1.name = '0.0.0' mock_tag2 = mock.Mock() mock_tag2.name = '0.0.1' mock_repo.tags = [mock_tag1, mock_tag2] mock_repo.create_tag = mock.Mock() mock_git.Repo = mock.Mock(return_value=mock_repo) mock_repo.remotes = mock.Mock() mock_repo.remotes.origin = mock.Mock() mock_repo.remotes.origin.push = mock.Mock() ghc = GitHubContext('REPO') ghc.tag_release('0.0.2', 'master', push=True, attempts=2, cooloff=0) self.failUnless(mock_repo.create_tag.called) self.failUnless(mock_repo.remotes.origin.push.called)
def test_push_branch(self, mock_git): mock_repo = mock.Mock() mock_repo.active_branch = mock.Mock() mock_repo.active_branch.name = 'womp' mock_repo.head = "HEAD" mock_repo.git = mock.Mock() mock_repo.git.checkout = mock.Mock() mock_git.Repo = mock.Mock(return_value=mock_repo) mock_repo.remotes = mock.Mock() mock_ret = mock.Mock() mock_ret.flags = 1000 mock_repo.remotes.origin = mock.Mock() mock_repo.remotes.origin.push = mock.Mock(return_value=[mock_ret]) ghc = GitHubContext('REPO') ghc.push_branch('womp') self.failUnless(mock_repo.remotes.origin.push.called) mock_repo.remotes.origin.push.assert_has_calls([mock.call('HEAD')]) mock_repo.remotes.origin.push = mock.Mock( side_effect=GitCommandError('A', 128)) self.assertRaises(RuntimeError, ghc.push_branch, 'womp2')
def test_merge_branch_conflict(self, mock_git): """test merge branch""" mock_repo = mock.Mock() mock_repo.git = mock.Mock() mock_repo.git.merge = mock.Mock( side_effect=GitCommandError(mock.Mock(), mock.Mock(), mock.Mock())) mock_repo.active_branch = mock.Mock() mock_repo.active_branch.name = "ACTIVE" mock_repo.index = mock.Mock() mock_repo.index.unmerged_blobs = mock.Mock(return_value={ 'file1': [(1, "BLOB1")], 'file2': [(2, "BLOB2")] }) mock_git.Repo = mock.Mock(return_value=mock_repo) ghc = GitHubContext('REPO') self.assertRaises(GitCommandError, ghc.merge_branch, 'develop')
def cleanup_release(opts): """ _cleanup_release_ Remove local and remote release branches if they exist """ config = load_configuration() repo_dir = os.getcwd() pfix = config.gitflow_release_prefix() branch_name = release_branch_name(config) if opts.version is not None: if not opts.version.startswith(pfix): branch_name = "{0}{1}".format(pfix, opts.version) else: branch_name = opts.version LOGGER.info("Cleaning release branches for {}".format(branch_name)) with GitHubContext(repo_dir) as ghc: ghc.delete_branch(branch_name, not opts.no_remote)
def merge_feature_branch(opts): """ merge current feature branch into develop """ config = load_configuration() main_branch = config.gitflow_branch_name() repo_dir = repo_directory() curr_branch = current_branch(repo_dir) LOGGER.info("Merging {} into {}".format(curr_branch, main_branch)) # make sure repo is clean if has_unstaged_changes(repo_dir): msg = ("Error: Unstaged changes are present on the feature branch {}" "Please commit them or clean up before proceeding" ).format(curr_branch) LOGGER.error(msg) raise RuntimeError(msg) checkout_and_pull(repo_dir, main_branch, pull=not opts.no_remote) with GitHubContext(repo_dir) as ghc: ghc.merge_branch(curr_branch) if not opts.no_remote: ghc.push_branch(main_branch) LOGGER.info("Branch {0} pushed to remote".format(main_branch))
def merge_release(opts): """ _merge_release_ Merge a release branch git flow style into master and develop branches (or those configured for this package) and tag master. """ config = load_configuration() rel_conf = release_config(config, opts) repo_dir = os.getcwd() tag = config.package_version() master = config.gitflow_master_name() develop = config.gitflow_branch_name() with GitHubContext(repo_dir) as ghc: release_branch = ghc.active_branch_name expected_branch = release_branch_name(config) if release_branch != expected_branch: msg = (u"Not on the expected release branch according " u"to cirrus.conf\n Expected:{0} but on {1}").format( expected_branch, release_branch) LOGGER.error(msg) raise RuntimeError(msg) # merge release branch into master LOGGER.info(u"Tagging and pushing {0}".format(tag)) if opts.skip_master: LOGGER.info(u'Skipping merging to {}'.format(master)) if opts.skip_develop: LOGGER.info(u'Skipping merging to {}'.format(develop)) if opts.log_status: ghc.log_branch_status(master) if not opts.skip_master: sha = ghc.repo.head.ref.commit.hexsha if rel_conf['wait_on_ci']: # # wait on release branch CI success # LOGGER.info( u"Waiting on CI build for {0}".format(release_branch)) ghc.wait_on_gh_status(sha, timeout=rel_conf['wait_on_ci_timeout'], interval=rel_conf['wait_on_ci_interval']) LOGGER.info(u"Merging {} into {}".format(release_branch, master)) ghc.pull_branch(master, remote=not opts.no_remote) ghc.merge_branch(release_branch) sha = ghc.repo.head.ref.commit.hexsha if rel_conf['wait_on_ci_master']: # # wait on release branch CI success # LOGGER.info(u"Waiting on CI build for {0}".format(master)) ghc.wait_on_gh_status(sha, timeout=rel_conf['wait_on_ci_timeout'], interval=rel_conf['wait_on_ci_interval']) if rel_conf['update_github_context']: for ctx in rel_conf['github_context_string']: LOGGER.info(u"Setting {} for {}".format(ctx, sha)) ghc.set_branch_state('success', ctx, branch=sha) if rel_conf['update_master_github_context']: for ctx in rel_conf['github_master_context_string']: LOGGER.info(u"Setting {} for {}".format(ctx, sha)) ghc.set_branch_state('success', ctx, branch=sha) if not opts.no_remote: ghc.push_branch_with_retry( attempts=rel_conf['push_retry_attempts'], cooloff=rel_conf['push_retry_cooloff']) LOGGER.info(u"Tagging {} as {}".format(master, tag)) ghc.tag_release(tag, master, push=not opts.no_remote, attempts=rel_conf['push_retry_attempts'], cooloff=rel_conf['push_retry_cooloff']) LOGGER.info(u"Merging {} into {}".format(release_branch, develop)) if opts.log_status: ghc.log_branch_status(develop) if not opts.skip_develop: ghc.pull_branch(develop, remote=not opts.no_remote) ghc.merge_branch(release_branch) sha = ghc.repo.head.ref.commit.hexsha if rel_conf['wait_on_ci_develop']: # # wait on release branch CI success # LOGGER.info(u"Waiting on CI build for {0}".format(develop)) ghc.wait_on_gh_status(sha, timeout=rel_conf['wait_on_ci_timeout'], interval=rel_conf['wait_on_ci_interval']) if rel_conf['update_github_context']: for ctx in rel_conf['github_context_string']: LOGGER.info(u"Setting {} for {}".format(ctx, sha)) ghc.set_branch_state('success', ctx, branch=sha) if rel_conf['update_develop_github_context']: for ctx in rel_conf['github_develop_context_string']: LOGGER.info(u"Setting {} for {}".format(ctx, sha)) ghc.set_branch_state('success', ctx, branch=sha) if not opts.no_remote: ghc.push_branch_with_retry( attempts=rel_conf['push_retry_attempts'], cooloff=rel_conf['push_retry_cooloff']) if opts.cleanup: ghc.delete_branch(release_branch, remote=not opts.no_remote)
def release_status(release): """ given a release branch name or tag, look at the status of that release based on git history and attempt to check wether it has been fully merged or tagged. returns True if the release looks to be successfully merged and tagged """ result = False with GitHubContext(repo_directory()) as ghc: LOGGER.info("Checking release status for {}".format(release)) # work out the branch and tag for the release rel_pfix = ghc.config.gitflow_release_prefix() develop_branch = ghc.config.gitflow_branch_name() master_branch = ghc.config.gitflow_master_name() origin_name = ghc.config.gitflow_origin_name() if rel_pfix in release: release_tag = release.split(rel_pfix)[1] release_branch = release else: release_branch = "{}{}".format(rel_pfix, release) release_tag = release LOGGER.info("Checking: branch={} tag={}".format( release_branch, release_tag)) branch_commit = ghc.find_release_commit(release_branch) tag_commit = ghc.find_release_commit(release_tag) LOGGER.info("Resolved Commits: branch={} tag={}".format( branch_commit, tag_commit)) # handles caser where tag or release is not present, eg typo if (not tag_commit) and (not branch_commit): msg = ("Unable to find any branch or tag commits " "for release \'{}\'\n" "Are you sure this is a valid release?").format(release) LOGGER.error(msg) return False # check the tag commit is present on master and remote master tag_present = False if tag_commit: branches = ghc.commit_on_branches(tag_commit) remote_master = "remotes/{}/{}".format(origin_name, master_branch) on_master = master_branch in branches on_origin = remote_master in branches if on_master: LOGGER.info("Tag is present on local master {}...".format( master_branch)) tag_present = True if on_origin: LOGGER.info("Tag is present on remote master {}...".format( remote_master)) tag_present = True # look for common merge base containing the release name for master # and develop develop_merge = ghc.merge_base(release_tag, develop_branch) master_merge = ghc.merge_base(release_tag, master_branch) merge_on_develop = False merge_on_master = False if develop_merge: merge_on_develop = release_branch in ghc.git_show_commit( develop_merge) if master_merge: merge_on_master = release_branch in ghc.git_show_commit( master_merge) if merge_on_develop: LOGGER.info("Merge of {} is on {}".format(release_branch, develop_branch)) else: LOGGER.info("Merge of {} not found on {}".format( release_branch, develop_branch)) if merge_on_master: LOGGER.info("Merge of {} is on {}".format(release_branch, master_branch)) else: LOGGER.info("Merge of {} not found on {}".format( release_branch, master_branch)) if not all([tag_present, merge_on_develop, merge_on_master]): msg = ( "\nRelease branch {} was not found either as a tag or merge " "commit on develop branch: {} or master branch: {} branches\n" "This may mean that the release is still running on a CI platform or " "has errored out. \n" " => Tag Present: {}\n" " => Merged to develop: {}\n" " => Merged to master: {}\n" "\nFor troubleshooting please see: " "https://github.com/evansde77/cirrus/wiki/Troubleshooting#release\n" ).format(release_branch, develop_branch, master_branch, tag_present, merge_on_develop, merge_on_master) LOGGER.error(msg) result = False else: msg = "Release {} looks to be successfully merged and tagged\n".format( release_branch) LOGGER.info(msg) result = True return result