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

Git tag feature #713

Merged
merged 8 commits into from
Aug 1, 2020
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
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 @@ -181,6 +189,12 @@ export class Toolbar extends React.Component<IToolbarProps, IToolbarState> {
onClick={this._onRefreshClick}
title={'Refresh the repository to detect local and remote changes'}
/>
<ActionButton
className={toolbarButtonClass}
icon={tagIcon}
onClick={this._onTagClick}
title={'Checkout a tag'}
/>
</div>
);
}
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
});