Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add commands to manage Git refs #296

Merged
merged 29 commits into from
Feb 27, 2019
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
3f6ded6
add commands to manage Git refs
vbossica Dec 27, 2018
2527b35
resolve pylint warnings
vbossica Dec 28, 2018
b97c891
resolve flake8 warnings
vbossica Dec 28, 2018
dae81ea
remove transcient debugging code
vbossica Jan 2, 2019
dd9537b
add unit tests for the refs methods
vbossica Jan 3, 2019
3c765d1
adding content to _init_.py file
vbossica Jan 3, 2019
1aca2f3
delete test/utilities/helper.py as python 2.7.15 doesn't seem to like it
vbossica Jan 3, 2019
e2db6da
missing .self
vbossica Jan 3, 2019
4ba5533
add ref commands to utCoverage.json
vbossica Jan 3, 2019
65aaa4c
add vcs test
vbossica Jan 4, 2019
0ee4ae3
Merge remote-tracking branch 'upstream/master' into azext/manage_refs
vbossica Jan 26, 2019
9f855ab
fix documentation issues
vbossica Jan 26, 2019
c325561
address review comments
vbossica Jan 27, 2019
ba0904f
fix Not None typo
vbossica Jan 27, 2019
5e037f3
allow large response
vbossica Jan 29, 2019
6b24368
fix uppercase in format.py
vbossica Jan 28, 2019
ff94274
Only add column is value exists
vbossica Jan 28, 2019
bdb6283
add recording
vbossica Jan 31, 2019
7eb9162
merge master
vbossica Jan 31, 2019
e1ea6bc
adapt to the new way to define organization
vbossica Jan 31, 2019
43e8d12
merge upstream/master
vbossica Feb 25, 2019
518a112
add lock/unlock method and update integration tests
vbossica Feb 25, 2019
f2d5c05
remove unnecessary stop method
vbossica Feb 25, 2019
4d4f42a
try to adapt the tests
vbossica Feb 26, 2019
88eb6c7
Update test_ref.py
atbagga Feb 27, 2019
49eb061
Update commands.py
atbagga Feb 27, 2019
01da440
Update test_reposRefTest.py
atbagga Feb 27, 2019
5169eb9
Update test_ref_createListUpdateDelete.yaml
atbagga Feb 27, 2019
742acf5
Update test_ref.py
atbagga Feb 27, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file.
22 changes: 22 additions & 0 deletions azure-devops/azext_devops/dev/common/git.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,27 @@ def get_git_remotes():
return _git_remotes


def resolve_git_refs(ref):
"""Prepends 'refs/' prefix to ref str if not already there.
:param str ref: The text to validate.
:rtype: str
"""
if ref is not None and not ref.startswith(REFS_PREFIX):
ref = REFS_PREFIX + ref
return ref


def get_ref_name_from_ref(ref):
"""Removes 'refs/' prefix from ref str if there.
:param ref: The text to validate.
:type ref: str
:rtype: str
"""
if ref is not None and ref.startswith(REFS_PREFIX):
ref = ref[len(REFS_PREFIX):]
return ref


def resolve_git_ref_heads(ref):
"""Prepends 'refs/heads/' prefix to ref str if not already there.
:param ref: The text to validate.
Expand Down Expand Up @@ -168,6 +189,7 @@ def _get_alias_value(command):

_git_remotes = {}
_ORIGIN_PUSH_KEY = 'origin(push)'
REFS_PREFIX = 'refs/'
REF_HEADS_PREFIX = 'refs/heads/'
GIT_CREDENTIALS_USERNAME_KEY = 'username'
GIT_CREDENTIALS_PASSWORD_KEY = 'password'
33 changes: 33 additions & 0 deletions azure-devops/azext_devops/dev/repos/_format.py
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,39 @@ def _convert_policy_status(status):
return status.capitalize()


def transform_refs_table_output(result):
table_output = []
for item in sorted(result, key=_get_repo_key):
table_output.append(_transform_ref_row(item))
return table_output


def transform_ref_table_output(result):
table_output = [_transform_ref_row(result)]
return table_output


def _transform_ref_row(row):
from azext_devops.dev.common.git import get_ref_name_from_ref
table_row = OrderedDict()
if 'objectId' in row:
table_row['Object ID'] = row['objectId']
if ('oldObjectId' in row) and ('newObjectId' in row):
old_id = row['oldObjectId']
new_id = row['newObjectId']
if old_id == '0000000000000000000000000000000000000000':
table_row['Object ID'] = new_id
elif new_id == '0000000000000000000000000000000000000000':
table_row['Object ID'] = old_id
else:
table_row['Old Object ID'] = old_id
table_row['New Object ID'] = new_id
table_row['Name'] = get_ref_name_from_ref(row['name'])
table_row['Success'] = row['success'] if 'success' in row else None
table_row['Update Status'] = row['updateStatus'] if 'updateStatus' in row else None
return table_row


def transform_repos_table_output(result):
table_output = []
for item in sorted(result, key=_get_repo_key):
Expand Down
6 changes: 6 additions & 0 deletions azure-devops/azext_devops/dev/repos/_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,9 @@ def load_repos_help():
short-summary: Manage Git repositories import
long-summary:
"""

helps['repos ref'] = """
type: group
short-summary: Manage Git references.
long-summary:
"""
4 changes: 4 additions & 0 deletions azure-devops/azext_devops/dev/repos/arguments.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,7 @@ def load_code_arguments(self, _):

with self.argument_context('repos import create') as context:
context.argument('git_source_url', options_list=('--git-source-url', '--git-url'))

with self.argument_context('repos ref') as context:
context.argument('repository', options_list=('--repository', '-r'))
vbossica marked this conversation as resolved.
Show resolved Hide resolved
context.argument('object_id', options_list=('--object-id'))
13 changes: 13 additions & 0 deletions azure-devops/azext_devops/dev/repos/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
transform_pull_requests_table_output,
transform_repo_table_output,
transform_repos_table_output,
transform_ref_table_output,
transform_refs_table_output,
transform_reviewers_table_output,
transform_reviewer_table_output,
transform_policies_table_output,
Expand All @@ -22,6 +24,10 @@
operations_tmpl='azext_devops.dev.repos.pull_request#{}'
)

reposRefOps = CliCommandType(
operations_tmpl='azext_devops.dev.repos.ref#{}'
)

reposRepositoryOps = CliCommandType(
operations_tmpl='azext_devops.dev.repos.repository#{}'
)
Expand Down Expand Up @@ -87,3 +93,10 @@ def load_code_commands(self, _):
with self.command_group('repos import', command_type=reposImportOps) as g:
# import request
g.command('create', 'create_import_request', table_transformer=transform_repo_import_table_output)

with self.command_group('repos ref', command_type=reposRefOps) as g:
# refs commands
g.command('create', 'create_ref', table_transformer=transform_ref_table_output)
g.command('delete', 'delete_ref', table_transformer=transform_ref_table_output)
g.command('list', 'list_refs', table_transformer=transform_refs_table_output)
g.command('update', 'update_ref', table_transformer=transform_ref_table_output)
116 changes: 116 additions & 0 deletions azure-devops/azext_devops/dev/repos/ref.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

from knack.util import CLIError
from vsts.git.v4_0.models.git_ref_update import GitRefUpdate
from vsts.exceptions import VstsServiceError
from azext_devops.dev.common.git import resolve_git_refs
from azext_devops.dev.common.services import (get_git_client,
resolve_instance_project_and_repo)


# pylint: disable=redefined-builtin
def list_refs(filter=None, repository=None, organization=None, project=None, detect=None):
"""List the references.
:param str filter: A filter to apply to the refs (starts with). Example: head or heads/ for the branches.
:param str repository: Name or ID of the repository.
:param str organization: Azure Devops organization URL. Example: https://dev.azure.com/MyOrganizationName/
:param str project: Name or ID of the project.
:param str detect: Automatically detect organization and project. Default is "on".
"""
try:
organization, project, repository = resolve_instance_project_and_repo(
detect=detect,
organization=organization,
project=project,
repo=repository)
client = get_git_client(organization)
return client.get_refs(repository_id=repository,
project=project,
filter=filter)
except VstsServiceError as ex:
raise CLIError(ex)


def create_ref(name, object_id, locked=False, repository=None, organization=None, project=None, detect=None):
"""Create a reference.
vbossica marked this conversation as resolved.
Show resolved Hide resolved
:param str name: Name of the reference to create (example: heads/my_branch or tags/my_tag).
:param str object_id: Id of the object to create the reference from.
:param bool locked: If the reference is locked (default False)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried the command with --locked and still got

{
"customMessage": null,
"isLocked": false,
"name": "refs/heads/NewBranchCreationLocked1",
"newObjectId": "ffb155f085e36281514672fe8d31983e8ec6d640",
"oldObjectId": "0000000000000000000000000000000000000000",
"rejectedBy": null,
"repositoryId": "89c5139b-34e7-4c45-8d40-d23353e22014",
"success": true,
"updateStatus": "succeeded"
}

Will need to check this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right. I think the problem is in the serialization/deserialization of boolean values (which is strange if accurate.)
When retrieving the list of all refs, the isLocked value is null:

"isLocked": null,
"isLockedBy": null,
"name": "refs/tags/tag_4",
"objectId": "4f20eea9a98c52b75e8264634ca63b662d6fa55a",

when a boolean value should be true/false.

Suggestion is to remove the --locked parameter to unblock this PR and open an issue for further investigation.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Go ahead with the suggestion.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the issue lies somewhere between the vsts-python-api and the rest endpoint: microsoft/azure-devops-python-api#174

:param str repository: Name or ID of the repository.
:param str organization: Azure Devops organization URL. Example: https://dev.azure.com/MyOrganizationName/
:param str project: Name or ID of the project.
:param str detect: Automatically detect organization and project. Default is "on".
"""
try:
organization, project, repository = resolve_instance_project_and_repo(
detect=detect,
organization=organization,
project=project,
repo=repository)
client = get_git_client(organization)
ref_update = GitRefUpdate(is_locked=locked,
name=resolve_git_refs(name),
new_object_id=object_id,
old_object_id='0000000000000000000000000000000000000000')
return client.update_refs(ref_updates=[ref_update],
repository_id=repository,
project=project)[0]
except VstsServiceError as ex:
raise CLIError(ex)


def delete_ref(name, object_id, repository=None, organization=None, project=None, detect=None):
"""Delete a reference.
:param str name: Name of the reference to delete (example: heads/my_branch).
:param str object_id: Id of the reference to delete.
:param str repository: Name or ID of the repository.
:param str organization: Azure Devops organization URL. Example: https://dev.azure.com/MyOrganizationName/
:param str project: Name or ID of the project.
:param str detect: Automatically detect organization and project. Default is "on".
"""
try:
organization, project, repository = resolve_instance_project_and_repo(
detect=detect,
organization=organization,
project=project,
repo=repository)
client = get_git_client(organization)
ref_update = GitRefUpdate(name=resolve_git_refs(name),
new_object_id='0000000000000000000000000000000000000000',
old_object_id=object_id)
return client.update_refs(ref_updates=[ref_update],
repository_id=repository,
project=project)[0]
except VstsServiceError as ex:
raise CLIError(ex)


def update_ref(name, old_object_id, new_object_id, repository=None, organization=None,
project=None, detect=None):
"""Update a reference.
:param str name: Name of the reference to update (example: heads/my_branch).
:param str old_object_id: Id of the old reference.
:param str new_object_id: Id of the new reference.
:param str repository: Name or ID of the repository.
:param str organization: Azure Devops organization URL. Example: https://dev.azure.com/MyOrganizationName/
:param str project: Name or ID of the project.
:param str detect: Automatically detect organization and project. Default is "on".
"""
try:
organization, project, repository = resolve_instance_project_and_repo(
detect=detect,
organization=organization,
project=project,
repo=repository)
client = get_git_client(organization)
ref_update = GitRefUpdate(name=resolve_git_refs(name),
new_object_id=new_object_id,
old_object_id=old_object_id)
return client.update_refs(ref_updates=[ref_update],
repository_id=repository,
project=project)[0]
except VstsServiceError as ex:
raise CLIError(ex)
2 changes: 1 addition & 1 deletion azure-devops/azext_devops/test/common/_init_.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------
import pkg_resources
pkg_resources.declare_namespace(__name__)
pkg_resources.declare_namespace(__name__)
82 changes: 82 additions & 0 deletions azure-devops/azext_devops/test/repos/test_ref.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

import unittest

try:
# Attempt to load mock (works on Python 3.3 and above)
from unittest.mock import patch, ANY
except ImportError:
# Attempt to load mock (works on Python version below 3.3)
from mock import patch, ANY

from azext_devops.dev.common.services import clear_connection_cache
from azext_devops.dev.repos.ref import (list_refs, create_ref, delete_ref, update_ref)


class TestRefMethods(unittest.TestCase):

_TEST_DEVOPS_ORGANIZATION = 'https://AzureDevOpsCliTest.visualstudio.com'
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Put an invalid url here like.. https://some-invalid-org.visualstudio.com.

This will fail the test since the "client = get_git_client(organization)" call in each of the commands makes a network call. You will need to mock it, in your case to return a GitClient object.

We discovered this recently and I am fixing other tests.

This one is implemented correctly.
https://github.com/Microsoft/azure-devops-cli-extension/blob/master/azure-devops/azext_devops/test/pipelines/test_pipelines_build.py

check - self.get_client = patch('vsts.vss_connection.VssConnection.get_client')


def setUp(self):
self.list_refs_patcher = patch('vsts.git.v4_0.git_client.GitClient.get_refs')
self.update_ref_patcher = patch('vsts.git.v4_0.git_client.GitClient.update_refs')

# start the patchers
self.mock_list_refs = self.list_refs_patcher.start()
self.mock_update_ref = self.update_ref_patcher.start()

# clear connection cache before running each test
clear_connection_cache()

def tearDown(self):
self.mock_list_refs.stop()
self.mock_update_ref.stop()

def test_list_refs(self):
response = list_refs(organization=self._TEST_DEVOPS_ORGANIZATION,
project='sample_project',
detect='off')
# assert
self.mock_list_refs.assert_called_once_with(filter=None,
project='sample_project',
repository_id=None)

def test_create_ref(self):
response = create_ref(name='sample_ref',
object_id='1234567890',
organization=self._TEST_DEVOPS_ORGANIZATION,
project='sample_project',
detect='off')
# assert
self.mock_update_ref.assert_called_once_with(project='sample_project',
ref_updates=ANY,
repository_id=None)

def test_update_ref(self):
response = update_ref(name='sample_ref',
old_object_id='1234567890',
new_object_id='0987654321',
organization=self._TEST_DEVOPS_ORGANIZATION,
project='sample_project',
detect='off')
# assert
self.mock_update_ref.assert_called_once_with(project='sample_project',
ref_updates=ANY,
repository_id=None)

def test_delete_ref(self):
response = delete_ref(name='sample_ref',
object_id='1234567890',
organization=self._TEST_DEVOPS_ORGANIZATION,
project='sample_project',
detect='off')
# assert
self.mock_update_ref.assert_called_once_with(project='sample_project',
ref_updates=ANY,
repository_id=None)

if __name__ == '__main__':
unittest.main()
2 changes: 1 addition & 1 deletion azure-devops/azext_devops/test/team/_init_.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------
import pkg_resources
pkg_resources.declare_namespace(__name__)
pkg_resources.declare_namespace(__name__)
29 changes: 28 additions & 1 deletion doc/authoring_tests.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,31 @@
# Authoring Live Tests
vbossica marked this conversation as resolved.
Show resolved Hide resolved
# Authoring Tests

## Authoring Unit Tests

1. install pytest

1. run all the tests:

```bash
cd azure-devops
pytest
```

1. run an individual test:

```bash
cd azure-devops
pytest test/artifacts/test_universal.py
```

## Authoring Live Tests

1. install `azure-cli-testsdk` and `azure-devtools`:

```bash
pip install --user 'git+https://github.com/Azure/azure-cli@master#egg=azure-cli-testsdk&subdirectory=src/azure-cli-testsdk' -q
pip install --user azure-devtools
```

## Clear cache while recording a new test

Expand Down
16 changes: 16 additions & 0 deletions scripts/utCoverage.json
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,22 @@
"name": "repos import create",
"status": "None"
},
{
"name": "repos ref create",
"status": "None"
},
{
"name": "repos ref delete",
"status": "None"
},
{
"name": "repos ref list",
"status": "None"
},
{
"name": "repos ref update",
"status": "None"
},
{
"name": "devops login",
"status": "None"
Expand Down
Loading