Skip to content

Commit

Permalink
Add new tag feature (#1264)
Browse files Browse the repository at this point in the history
* Add new tag feature

* remove debugging parts

* cosmetic changes

* Apply cosmetic suggestions from code review

Co-authored-by: Michał Krassowski <[email protected]>

* fix dialog box scroll behavior and add tests

* change to function components

* fix states

* remove GitGraph when filtering and fix endpoint for creating a new tag

* add refresh tags list functionality

* code improvements

* Apply suggestions from code review

Co-authored-by: Frédéric Collonval <[email protected]>

* Update src/components/NewTagDialog.tsx

Co-authored-by: Frédéric Collonval <[email protected]>

* tests improvements

* add useCallback hooks

* fix TagMenu test

* Apply suggestions from code review

Co-authored-by: Frédéric Collonval <[email protected]>

* fix TagMenu test

* lint files

* delete unnecessary code

* Apply suggestions from code review

Co-authored-by: Frédéric Collonval <[email protected]>

* fix switch tag test

* change all files to the new name for creating a new tag function

* fix test_tag.py

* Skip browser check for now

---------

Co-authored-by: Michał Krassowski <[email protected]>
Co-authored-by: Frédéric Collonval <[email protected]>
  • Loading branch information
3 people authored Sep 25, 2023
1 parent 44c26e7 commit 07ee168
Show file tree
Hide file tree
Showing 14 changed files with 1,091 additions and 34 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ jobs:
jupyter labextension list
jupyter labextension list 2>&1 | grep -ie "@jupyterlab/git.*OK"
python -m jupyterlab.browser_check
# python -m jupyterlab.browser_check
- name: Package the extension
run: |
Expand Down
26 changes: 26 additions & 0 deletions jupyterlab_git/git.py
Original file line number Diff line number Diff line change
Expand Up @@ -1767,6 +1767,32 @@ async def tag_checkout(self, path, tag):
"message": error,
}

async def set_tag(self, path, tag, commitId):
"""Set a git tag pointing to a specific commit.
path: str
Git path repository
tag : str
Name of new tag.
commitId:
Identifier of commit tag is pointing to.
"""
command = ["git", "tag", tag, commitId]
code, _, error = await self.__execute(command, cwd=path)
if code == 0:
return {
"code": code,
"message": "Tag {} created, pointing to commit {}".format(
tag, commitId
),
}
else:
return {
"code": code,
"command": " ".join(command),
"message": error,
}

async def check_credential_helper(self, path: str) -> Optional[bool]:
"""
Check if the credential helper exists, and whether we need to setup a Git credential cache daemon in case the credential helper is Git credential cache.
Expand Down
22 changes: 22 additions & 0 deletions jupyterlab_git/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -895,6 +895,27 @@ async def post(self, path: str = ""):
self.finish(json.dumps(result))


class GitNewTagHandler(GitHandler):
"""
Hadler for 'git tag <tag_name> <commit_id>. Create new tag pointing to a specific commit.
"""

@tornado.web.authenticated
async def post(self, path: str = ""):
"""
POST request handler, create a new tag pointing to a specific commit.
"""
data = self.get_json_body()
tag = data["tag_id"]
commit = data["commit_id"]
response = await self.git.set_tag(self.url2localpath(path), tag, commit)
if response["code"] == 0:
self.set_status(201)
else:
self.set_status(500)
self.finish(json.dumps(response))


class GitRebaseHandler(GitHandler):
"""
Handler for git rebase '<rebase_onto>'.
Expand Down Expand Up @@ -1069,6 +1090,7 @@ def setup_handlers(web_app):
("/ignore", GitIgnoreHandler),
("/tags", GitTagHandler),
("/tag_checkout", GitTagCheckoutHandler),
("/tag", GitNewTagHandler),
("/add", GitAddHandler),
("/rebase", GitRebaseHandler),
("/stash", GitStashHandler),
Expand Down
33 changes: 33 additions & 0 deletions jupyterlab_git/tests/test_tag.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,36 @@ async def test_git_tag_checkout_success():
"code": 0,
"message": "Tag {} checked out".format(tag),
} == actual_response


@pytest.mark.asyncio
async def test_set_tag_succes():
with patch("os.environ", {"TEST": "test"}):
with patch("jupyterlab_git.git.execute") as mock_execute:
tag = "mock_tag"
commitId = "mock_commit_id"
# Given
mock_execute.return_value = maybe_future((0, "", ""))

# When
actual_response = await Git().set_tag(
"test_curr_path", "mock_tag", "mock_commit_id"
)

# Then
mock_execute.assert_called_once_with(
["git", "tag", tag, commitId],
cwd="test_curr_path",
timeout=20,
env=None,
username=None,
password=None,
is_binary=False,
)

assert {
"code": 0,
"message": "Tag {} created, pointing to commit {}".format(
tag, commitId
),
} == actual_response
3 changes: 3 additions & 0 deletions src/__tests__/test-components/GitPanel.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,9 @@ describe('GitPanel', () => {
headChanged: {
connect: jest.fn()
},
tagsChanged: {
connect: jest.fn()
},
markChanged: {
connect: jest.fn()
},
Expand Down
224 changes: 224 additions & 0 deletions src/__tests__/test-components/TagMenu.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
import { mount, shallow } from 'enzyme';
import 'jest';
import * as React from 'react';
import { TagMenu, ITagMenuProps } from '../../components/TagMenu';
import * as git from '../../git';
import { Logger } from '../../logger';
import { GitExtension } from '../../model';
import { IGitExtension } from '../../tokens';
import { listItemClass } from '../../style/BranchMenu';
import {
mockedRequestAPI,
defaultMockedResponses,
DEFAULT_REPOSITORY_PATH
} from '../utils';
import ClearIcon from '@material-ui/icons/Clear';
import { nullTranslator } from '@jupyterlab/translation';

jest.mock('../../git');
jest.mock('@jupyterlab/apputils');

const TAGS = [
{
name: '1.0.0'
},
{
name: 'feature-1'
},
{
name: 'feature-2'
},
{
name: 'patch-007'
}
];

async function createModel() {
const model = new GitExtension();
model.pathRepository = DEFAULT_REPOSITORY_PATH;

await model.ready;
return model;
}

describe('TagMenu', () => {
let model: GitExtension;
const trans = nullTranslator.load('jupyterlab_git');

beforeEach(async () => {
jest.restoreAllMocks();

const mock = git as jest.Mocked<typeof git>;
mock.requestAPI.mockImplementation(
mockedRequestAPI({
responses: {
...defaultMockedResponses,
'tags/delete': {
body: () => {
return { code: 0 };
}
},
checkout: {
body: () => {
return {
code: 0
};
}
}
}
})
);

model = await createModel();
});

function createProps(props?: Partial<ITagMenuProps>): ITagMenuProps {
return {
branching: false,
pastCommits: [],
logger: new Logger(),
model: model as IGitExtension,
tagsList: TAGS.map(tag => tag.name),
trans: trans,
...props
};
}

describe('constructor', () => {
it('should return a new instance', () => {
const menu = shallow(<TagMenu {...createProps()} />);
expect(menu.instance()).toBeInstanceOf(TagMenu);
});

it('should set the default menu filter to an empty string', () => {
const menu = shallow(<TagMenu {...createProps()} />);
expect(menu.state('filter')).toEqual('');
});

it('should set the default flag indicating whether to show a dialog to create a new tag to `false`', () => {
const menu = shallow(<TagMenu {...createProps()} />);
expect(menu.state('tagDialog')).toEqual(false);
});
});

describe('render', () => {
it('should display placeholder text for the menu filter', () => {
const component = shallow(<TagMenu {...createProps()} />);
const node = component.find('input[type="text"]').first();
expect(node.prop('placeholder')).toEqual('Filter');
});

it('should set a `title` attribute on the input element to filter a tag menu', () => {
const component = shallow(<TagMenu {...createProps()} />);
const node = component.find('input[type="text"]').first();
expect(node.prop('title').length > 0).toEqual(true);
});

it('should display a button to clear the menu filter once a filter is provided', () => {
const component = shallow(<TagMenu {...createProps()} />);
component.setState({
filter: 'foo'
});
const nodes = component.find(ClearIcon);
expect(nodes.length).toEqual(1);
});

it('should set a `title` on the button to clear the menu filter', () => {
const component = shallow(<TagMenu {...createProps()} />);
component.setState({
filter: 'foo'
});
const html = component.find(ClearIcon).first().html();
expect(html.includes('<title>')).toEqual(true);
});

it('should display a button to create a new tag', () => {
const component = shallow(<TagMenu {...createProps()} />);
const node = component.find('input[type="button"]').first();
expect(node.prop('value')).toEqual('New Tag');
});

it('should set a `title` attribute on the button to create a new tag', () => {
const component = shallow(<TagMenu {...createProps()} />);
const node = component.find('input[type="button"]').first();
expect(node.prop('title').length > 0).toEqual(true);
});

it('should not, by default, show a dialog to create a new tag', () => {
const component = shallow(<TagMenu {...createProps()} />);
const node = component.find('NewTagDialogBox').first();
expect(node.prop('open')).toEqual(false);
});

it('should show a dialog to create a new tag when the flag indicating whether to show the dialog is `true`', () => {
const component = shallow(<TagMenu {...createProps()} />);
component.setState({
tagDialog: true
});
const node = component.find('NewTagDialogBox').first();
expect(node.prop('open')).toEqual(true);
});
});

describe('switch tag', () => {
it('should not switch to a specified tag upon clicking its corresponding element when branching is disabled', () => {
const spy = jest.spyOn(GitExtension.prototype, 'checkoutTag');

const component = mount(<TagMenu {...createProps()} />);
const nodes = component.find(
`.${listItemClass}[title*="${TAGS[1].name}"]`
);
nodes.at(0).simulate('click');

expect(spy).toHaveBeenCalledTimes(0);
spy.mockRestore();
});

it('should switch to a specified tag upon clicking its corresponding element when branching is enabled', () => {
const spy = jest.spyOn(GitExtension.prototype, 'checkoutTag');

const component = mount(
<TagMenu {...createProps({ branching: true })} />
);
const nodes = component.find(
`.${listItemClass}[title*="${TAGS[1].name}"]`
);
nodes.at(0).simulate('click');

expect(spy).toHaveBeenCalledTimes(1);
expect(spy).toHaveBeenCalledWith(TAGS[1].name);

spy.mockRestore();
});
});

describe('create tag', () => {
it('should not allow creating a new tag when branching is disabled', () => {
const spy = jest.spyOn(GitExtension.prototype, 'setTag');

const component = shallow(<TagMenu {...createProps()} />);

const node = component.find('input[type="button"]').first();
node.simulate('click');

expect(component.state('tagDialog')).toEqual(false);
expect(spy).toHaveBeenCalledTimes(0);
spy.mockRestore();
});

it('should display a dialog to create a new tag when branching is enabled and the new tag button is clicked', () => {
const spy = jest.spyOn(GitExtension.prototype, 'setTag');

const component = shallow(
<TagMenu {...createProps({ branching: true })} />
);

const node = component.find('input[type="button"]').first();
node.simulate('click');

expect(component.state('tagDialog')).toEqual(true);
expect(spy).toHaveBeenCalledTimes(0);
spy.mockRestore();
});
});
});
2 changes: 2 additions & 0 deletions src/__tests__/test-components/Toolbar.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ describe('Toolbar', () => {
tag: ''
}
],
tagsList: model.tagsList,
pastCommits: [],
repository: model.pathRepository,
model: model,
branching: false,
Expand Down
Loading

0 comments on commit 07ee168

Please sign in to comment.