Skip to content

Commit

Permalink
Git tag feature (#713)
Browse files Browse the repository at this point in the history
* Git Tag list feature changes

* Improve checkout tag PR
Use SVG icon
Reduce code foot print
Use new Alert component

* Correct unit test

* Explicitly dependent on `requests_unixsocket` for test

* Correct toolbar unit test

Co-authored-by: Chris John <chris.john@aexp.com>
Co-authored-by: Frederic Collonval <fcollonval@gmail.com>
  • Loading branch information
3 people committed Aug 1, 2020
1 parent e95e007 commit c1ffae2
Show file tree
Hide file tree
Showing 11 changed files with 428 additions and 22 deletions.
38 changes: 34 additions & 4 deletions jupyterlab_git/git.py
Original file line number Diff line number Diff line change
Expand Up @@ -950,17 +950,15 @@ async def get_upstream_branch(self, current_path, branch_name):
command, cwd=os.path.join(self.root_dir, current_path)
)
if code != 0:
return {"code": code, "command": " ".join(cmd), "message": error}
return {"code": code, "command": " ".join(command), "message": error}

remote_name = output.strip()
remote_branch = rev_parse_output.strip().lstrip(remote_name+"/")
return {"code": code, "remote_short_name": remote_name, "remote_branch": remote_branch}



async def _get_tag(self, current_path, commit_sha):
"""Execute 'git describe commit_sha' to get
nearest tag associated with lastest commit in branch.
nearest tag associated with latest commit in branch.
Reference : https://git-scm.com/docs/git-describe#git-describe-ltcommit-ishgt82308203
"""
command = ["git", "describe", "--tags", commit_sha]
Expand Down Expand Up @@ -1125,3 +1123,35 @@ async def version(self):
return version.group('version')

return None

async def tags(self, current_path):
"""List all tags of the git repository.
current_path: str
Git path repository
"""
command = ["git", "tag", "--list"]
code, output, error = await execute(command, cwd=os.path.join(self.root_dir, current_path))
if code != 0:
return {"code": code, "command": " ".join(command), "message": error}
tags = [tag for tag in output.split("\n") if len(tag) > 0]
return {"code": code, "tags": tags}

async def tag_checkout(self, current_path, tag):
"""Checkout the git repository at a given tag.
current_path: str
Git path repository
tag : str
Tag to checkout
"""
command = ["git", "checkout", "tags/" + tag]
code, _, error = await execute(command, cwd=os.path.join(self.root_dir, current_path))
if code == 0:
return {"code": code, "message": "Tag {} checked out".format(tag)}
else:
return {
"code": code,
"command": " ".join(command),
"message": error,
}
34 changes: 34 additions & 0 deletions jupyterlab_git/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,38 @@ async def get(self):
)


class GitTagHandler(GitHandler):
"""
Handler for 'git tag '. Fetches list of all tags in current repository
"""

@web.authenticated
async def post(self):
"""
POST request handler, fetches all tags in current repository.
"""
current_path = self.get_json_body()["current_path"]
result = await self.git.tags(current_path)
self.finish(json.dumps(result))


class GitTagCheckoutHandler(GitHandler):
"""
Handler for 'git tag checkout '. Checkout the tag version of repo
"""

@web.authenticated
async def post(self):
"""
POST request handler, checkout the tag version to a branch.
"""
data = self.get_json_body()
current_path = data["current_path"]
tag = data["tag_id"]
result = await self.git.tag_checkout(current_path, tag)
self.finish(json.dumps(result))


def setup_handlers(web_app):
"""
Setups all of the git command handlers.
Expand Down Expand Up @@ -594,6 +626,8 @@ def setup_handlers(web_app):
("/git/show_top_level", GitShowTopLevelHandler),
("/git/status", GitStatusHandler),
("/git/upstream", GitUpstreamHandler),
("/git/tags", GitTagHandler),
("/git/tag_checkout", GitTagCheckoutHandler)
]

# add the baseurl to our paths
Expand Down
46 changes: 46 additions & 0 deletions jupyterlab_git/tests/test_tag.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import os
from unittest.mock import Mock, call, patch

import pytest
import tornado

from jupyterlab_git.git import Git

from .testutils import FakeContentManager, ServerTest, maybe_future

@pytest.mark.asyncio
async def test_git_tag_success():
with patch("jupyterlab_git.git.execute") as mock_execute:
tag = "1.0.0"
# Given
mock_execute.return_value = maybe_future((0, tag, ""))

# When
actual_response = await Git(FakeContentManager("/bin")).tags("test_curr_path")

# Then
mock_execute.assert_called_once_with(
["git", "tag", "--list"],
cwd=os.path.join("/bin", "test_curr_path"),
)

assert {"code": 0, "tags": [tag]} == actual_response

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

# When
actual_response = await Git(FakeContentManager("/bin")).tag_checkout("test_curr_path", "mock_tag")

# Then
mock_execute.assert_called_once_with(
["git", "checkout", "tags/{}".format(tag)],
cwd=os.path.join("/bin", "test_curr_path"),
)

assert {"code": 0, "message": "Tag {} checked out".format(tag)} == actual_response
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ def runPackLabextension():
],
extras_require = {
'test': [
'requests_unixsocket',
'pytest',
'pytest-asyncio',
'jupyterlab~=2.0',
Expand Down
62 changes: 60 additions & 2 deletions src/components/Toolbar.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { showDialog } from '@jupyterlab/apputils';
import { PathExt } from '@jupyterlab/coreutils';
import {
caretDownIcon,
Expand All @@ -7,7 +8,13 @@ import {
import * as React from 'react';
import { classes } from 'typestyle';
import { CommandIDs } from '../commandsAndMenu';
import { branchIcon, desktopIcon, pullIcon, pushIcon } from '../style/icons';
import {
branchIcon,
desktopIcon,
pullIcon,
pushIcon,
tagIcon
} from '../style/icons';
import {
spacer,
toolbarButtonClass,
Expand All @@ -21,8 +28,9 @@ import {
toolbarMenuWrapperClass,
toolbarNavClass
} from '../style/Toolbar';
import { IGitExtension, ILogMessage } from '../tokens';
import { IGitExtension, ILogMessage, Git } from '../tokens';
import { sleep } from '../utils';
import { GitTagDialog } from '../widgets/TagList';
import { ActionButton } from './ActionButton';
import { Alert } from './Alert';
import { BranchMenu } from './BranchMenu';
Expand Down Expand Up @@ -175,6 +183,12 @@ export class Toolbar extends React.Component<IToolbarProps, IToolbarState> {
onClick={this._onPushClick}
title={'Push committed changes'}
/>
<ActionButton
className={toolbarButtonClass}
icon={tagIcon}
onClick={this._onTagClick}
title={'Checkout a tag'}
/>
<ActionButton
className={toolbarButtonClass}
icon={refreshIcon}
Expand Down Expand Up @@ -425,4 +439,48 @@ export class Toolbar extends React.Component<IToolbarProps, IToolbarState> {
alert: false
});
};

/**
* Callback invoked upon clicking a button to view tags.
*
* @param event - event object
*/
private _onTagClick = async (): Promise<void> => {
const result = await showDialog({
title: 'Checkout tag',
body: new GitTagDialog(this.props.model)
});
if (result.button.accept) {
this._log({
severity: 'info',
message: `Switching to ${result.value}...`
});
this._suspend(true);

let response: Git.ICheckoutResult;
try {
response = await this.props.model.checkoutTag(result.value);
} catch (error) {
response = {
code: -1,
message: error.message || error
};
} finally {
this._suspend(false);
}

if (response.code !== 0) {
console.error(response.message);
this._log({
severity: 'error',
message: `Fail to checkout tag ${result.value}`
});
} else {
this._log({
severity: 'success',
message: `Switched to ${result.value}`
});
}
}
};
}
75 changes: 75 additions & 0 deletions src/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1019,6 +1019,81 @@ export class GitExtension implements IGitExtension {
return data;
}

/**
* Retrieve the list of tags in the repository.
*
* @returns promise which resolves upon retrieving the tag list
*/
async tags(): Promise<Git.ITagResult> {
let response;

await this.ready;

const path = this.pathRepository;
if (path === null) {
response = {
code: -1,
message: 'Not in a Git repository.'
};
return Promise.resolve(response);
}

const tid = this._addTask('git:tag:list');
try {
response = await httpGitRequest('/git/tags', 'POST', {
current_path: path
});
} catch (err) {
throw new ServerConnection.NetworkError(err);
} finally {
this._removeTask(tid);
}

const data = await response.json();
if (!response.ok) {
throw new ServerConnection.ResponseError(response, data.message);
}
return data;
}

/**
* Checkout the specified tag version
*
* @param tag - selected tag version
* @returns promise which resolves upon checking out the tag version of the repository
*/
async checkoutTag(tag: string): Promise<Git.ICheckoutResult> {
let response;

await this.ready;

const path = this.pathRepository;
if (path === null) {
response = {
code: -1,
message: 'Not in a Git repository.'
};
return Promise.resolve(response);
}

const tid = this._addTask('git:tag:checkout');
try {
response = await httpGitRequest('/git/tag_checkout', 'POST', {
current_path: path,
tag_id: tag
});
} catch (err) {
throw new ServerConnection.NetworkError(err);
} finally {
this._removeTask(tid);
}
const data = await response.json();
if (!response.ok) {
throw new ServerConnection.ResponseError(response, data.message);
}
return data;
}

/**
* Add a file to the current marker object.
*
Expand Down
21 changes: 13 additions & 8 deletions src/style/icons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,9 @@ import pullSvg from '../../style/icons/pull.svg';
import pushSvg from '../../style/icons/push.svg';
import removeSvg from '../../style/icons/remove.svg';
import rewindSvg from '../../style/icons/rewind.svg';
import tagSvg from '../../style/icons/tag.svg';

export const gitIcon = new LabIcon({ name: 'git', svgstr: gitSvg });
export const deletionsMadeIcon = new LabIcon({
name: 'git:deletions',
svgstr: deletionsMadeSvg
});
export const insertionsMadeIcon = new LabIcon({
name: 'git:insertions',
svgstr: insertionsMadeSvg
});
export const addIcon = new LabIcon({
name: 'git:add',
svgstr: addSvg
Expand All @@ -37,6 +30,10 @@ export const cloneIcon = new LabIcon({
name: 'git:clone',
svgstr: cloneSvg
});
export const deletionsMadeIcon = new LabIcon({
name: 'git:deletions',
svgstr: deletionsMadeSvg
});
export const desktopIcon = new LabIcon({
name: 'git:desktop',
svgstr: desktopSvg
Expand All @@ -49,6 +46,10 @@ export const discardIcon = new LabIcon({
name: 'git:discard',
svgstr: discardSvg
});
export const insertionsMadeIcon = new LabIcon({
name: 'git:insertions',
svgstr: insertionsMadeSvg
});
export const openIcon = new LabIcon({
name: 'git:open-file',
svgstr: openSvg
Expand All @@ -69,3 +70,7 @@ export const rewindIcon = new LabIcon({
name: 'git:rewind',
svgstr: rewindSvg
});
export const tagIcon = new LabIcon({
name: 'git:tag',
svgstr: tagSvg
});

0 comments on commit c1ffae2

Please sign in to comment.