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

Show tags in history sidebar #1272

Merged
merged 9 commits into from Oct 10, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
15 changes: 12 additions & 3 deletions jupyterlab_git/git.py
Expand Up @@ -1736,16 +1736,25 @@ async def version(self):
return None

async def tags(self, path):
"""List all tags of the git repository.
"""List all tags of the git repository, including the commit each tag points to.

path: str
Git path repository
"""
command = ["git", "tag", "--list"]
formats = ["refname:short", "objectname"]
command = [
"git",
"for-each-ref",
"--format=" + "%09".join("%({})".format(f) for f in formats),
"refs/tags",
]
code, output, error = await self.__execute(command, cwd=path)
if code != 0:
return {"code": code, "command": " ".join(command), "message": error}
tags = [tag for tag in output.split("\n") if len(tag) > 0]
tags = []
for tag_name, commit_id in (line.split("\t") for line in output.splitlines()):
tag = {"name": tag_name, "baseCommitId": commit_id}
tags.append(tag)
return {"code": code, "tags": tags}

async def tag_checkout(self, path, tag):
Expand Down
2 changes: 1 addition & 1 deletion jupyterlab_git/handlers.py
Expand Up @@ -861,7 +861,7 @@ async def get(self):

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

@tornado.web.authenticated
Expand Down
28 changes: 24 additions & 4 deletions jupyterlab_git/tests/test_tag.py
Expand Up @@ -10,16 +10,22 @@
@pytest.mark.asyncio
async def test_git_tag_success():
with patch("jupyterlab_git.git.execute") as mock_execute:
tag = "1.0.0"
output_tags = "v1.0.0\t6db57bf4987d387d439acd16ddfe8d54d46e8f4\nv2.0.1\t2aeae86b6010dd1f05b820d8753cff8349c181a6"

# Given
mock_execute.return_value = maybe_future((0, tag, ""))
mock_execute.return_value = maybe_future((0, output_tags, ""))

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

# Then
mock_execute.assert_called_once_with(
["git", "tag", "--list"],
[
"git",
"for-each-ref",
"--format=%(refname:short)%09%(objectname)",
"refs/tags",
],
cwd="test_curr_path",
timeout=20,
env=None,
Expand All @@ -28,7 +34,21 @@ async def test_git_tag_success():
is_binary=False,
)

assert {"code": 0, "tags": [tag]} == actual_response
expected_response = {
"code": 0,
"tags": [
{
"name": "v1.0.0",
"baseCommitId": "6db57bf4987d387d439acd16ddfe8d54d46e8f4",
},
{
"name": "v2.0.1",
"baseCommitId": "2aeae86b6010dd1f05b820d8753cff8349c181a6",
},
],
}

assert expected_response == actual_response


@pytest.mark.asyncio
Expand Down
1 change: 1 addition & 0 deletions src/__tests__/test-components/HistorySideBar.spec.tsx
Expand Up @@ -27,6 +27,7 @@ describe('HistorySideBar', () => {
}
],
branches: [],
tagsList: [],
model: {
selectedHistoryFile: null
} as GitExtension,
Expand Down
30 changes: 30 additions & 0 deletions src/__tests__/test-components/PastCommitNode.spec.tsx
Expand Up @@ -48,6 +48,27 @@ describe('PastCommitNode', () => {
}
];
const branches: Git.IBranch[] = notMatchingBranches.concat(matchingBranches);
const matchingTags: Git.ITag[] = [
{
name: '1.0.0',
baseCommitId: '2414721b194453f058079d897d13c4e377f92dc6'
},
{
name: 'feature-1',
baseCommitId: '2414721b194453f058079d897d13c4e377f92dc6'
}
];
const notMatchingTags: Git.ITag[] = [
{
name: 'feature-2',
baseCommitId: '798438398'
},
{
name: 'patch-007',
baseCommitId: '238848848'
}
];
const tags: Git.ITag[] = notMatchingTags.concat(matchingTags);
const toggleCommitExpansion = jest.fn();
const props: IPastCommitNodeProps = {
model: null,
Expand All @@ -59,6 +80,7 @@ describe('PastCommitNode', () => {
pre_commits: ['pre_commit']
},
branches: branches,
tagsList: tags,
commands: null,
trans,
onCompareWithSelected: null,
Expand All @@ -84,6 +106,14 @@ describe('PastCommitNode', () => {
expect(node.text()).not.toMatch('name2');
});

test('Includes only relevant tag info', () => {
const node = shallow(<PastCommitNode {...props} />);
expect(node.text()).toMatch('1.0.0');
expect(node.text()).toMatch('feature-1');
expect(node.text()).not.toMatch('feature-2');
expect(node.text()).not.toMatch('patch-007');
});

test('Toggle show details', () => {
// simulates SinglePastCommitInfo child
const node = shallow(
Expand Down
14 changes: 9 additions & 5 deletions src/__tests__/test-components/TagMenu.spec.tsx
Expand Up @@ -20,16 +20,20 @@ jest.mock('@jupyterlab/apputils');

const TAGS = [
{
name: '1.0.0'
name: '1.0.0',
baseCommitId: '4738782743'
},
{
name: 'feature-1'
name: 'feature-1',
baseCommitId: '7432743264'
},
{
name: 'feature-2'
name: 'feature-2',
baseCommitId: '798438398'
},
{
name: 'patch-007'
name: 'patch-007',
baseCommitId: '238848848'
}
];

Expand Down Expand Up @@ -78,7 +82,7 @@ describe('TagMenu', () => {
pastCommits: [],
logger: new Logger(),
model: model as IGitExtension,
tagsList: TAGS.map(tag => tag.name),
tagsList: TAGS,
trans: trans,
...props
};
Expand Down
3 changes: 2 additions & 1 deletion src/components/GitPanel.tsx
Expand Up @@ -93,7 +93,7 @@ export interface IGitPanelState {
/**
* List of tags.
*/
tagsList: string[];
tagsList: Git.ITag[];

/**
* List of changed files.
Expand Down Expand Up @@ -598,6 +598,7 @@ export class GitPanel extends React.Component<IGitPanelProps, IGitPanelState> {
<React.Fragment>
<HistorySideBar
branches={this.state.branches}
tagsList={this.state.tagsList}
commits={this.state.pastCommits}
model={this.props.model}
commands={this.props.commands}
Expand Down
6 changes: 6 additions & 0 deletions src/components/HistorySideBar.tsx
Expand Up @@ -32,6 +32,11 @@ export interface IHistorySideBarProps {
*/
branches: Git.IBranch[];

/**
* List of tags.
*/
tagsList: Git.ITag[];

/**
* Git extension data model.
*/
Expand Down Expand Up @@ -175,6 +180,7 @@ export const HistorySideBar: React.FunctionComponent<IHistorySideBarProps> = (
const commonProps = {
commit,
branches: props.branches,
tagsList: props.tagsList,
model: props.model,
commands: props.commands,
trans: props.trans
Expand Down
4 changes: 2 additions & 2 deletions src/components/NewTagDialog.tsx
Expand Up @@ -369,14 +369,14 @@ export const NewTagDialogBox: React.FunctionComponent<INewTagDialogProps> = (
*/
const createTag = async (): Promise<void> => {
const tagName = nameState;
const commitId = baseCommitIdState;
const baseCommitId = baseCommitIdState;

props.logger.log({
level: Level.RUNNING,
message: props.trans.__('Creating tag…')
});
try {
await props.model.setTag(tagName, commitId);
await props.model.setTag(tagName, baseCommitId);
} catch (err) {
setErrorState(err.message.replace(/^fatal:/, ''));
props.logger.log({
Expand Down
39 changes: 39 additions & 0 deletions src/components/PastCommitNode.tsx
Expand Up @@ -42,6 +42,11 @@ export interface IPastCommitNodeProps {
*/
branches: Git.IBranch[];

/**
* List of tags.
*/
tagsList: Git.ITag[];

/**
* Extension data model.
*/
Expand Down Expand Up @@ -199,6 +204,7 @@ export class PastCommitNode extends React.Component<
)}
</div>
<div className={branchWrapperClass}>{this._renderBranches()}</div>
<div className={branchWrapperClass}>{this._renderTags()}</div>
<div className={commitBodyClass}>
{this.props.commit.commit_msg}
{this.props.expanded && this.props.children}
Expand Down Expand Up @@ -250,6 +256,39 @@ export class PastCommitNode extends React.Component<
);
}

/**
* Renders tags information.
*
* @returns array of React elements
*/
private _renderTags(): React.ReactElement[] {
const curr = this.props.commit.commit;
const tags: Git.ITag[] = [];
for (let i = 0; i < this.props.tagsList.length; i++) {
const tag = this.props.tagsList[i];
if (tag.baseCommitId && tag.baseCommitId === curr) {
tags.push(tag);
}
}
return tags.map(this._renderTag, this);
}

/**
* Renders individual tag data.
*
* @param tag - tag data
* @returns React element
*/
private _renderTag(tag: Git.ITag): React.ReactElement {
return (
<React.Fragment key={tag.name}>
<span className={classes(branchClass, localBranchClass)}>
{tag.name}
</span>
</React.Fragment>
);
}

/**
* Callback invoked upon clicking on an individual commit.
*
Expand Down
14 changes: 7 additions & 7 deletions src/components/TagMenu.tsx
Expand Up @@ -91,7 +91,7 @@ export interface ITagMenuProps {
/**
* Current list of tags.
*/
tagsList: string[];
tagsList: Git.ITag[];

/**
* Boolean indicating whether branching is disabled.
Expand Down Expand Up @@ -215,7 +215,7 @@ export class TagMenu extends React.Component<ITagMenuProps, ITagMenuState> {
// Perform a "simple" filter... (TODO: consider implementing fuzzy filtering)
const filter = this.state.filter;
const tags = this.props.tagsList.filter(
tag => !filter || tag.includes(filter)
tag => !filter || tag.name.includes(filter)
);
return (
<FixedSizeList
Expand All @@ -225,7 +225,7 @@ export class TagMenu extends React.Component<ITagMenuProps, ITagMenuState> {
)}
itemCount={tags.length}
itemData={tags}
itemKey={(index, data) => data[index]}
itemKey={(index, data) => data[index].name}
itemSize={ITEM_HEIGHT}
style={{ overflowX: 'hidden', paddingTop: 0, paddingBottom: 0 }}
width={'auto'}
Expand All @@ -243,18 +243,18 @@ export class TagMenu extends React.Component<ITagMenuProps, ITagMenuState> {
*/
private _renderItem = (props: ListChildComponentProps): JSX.Element => {
const { data, index, style } = props;
const tag = data[index] as string;
const tag = data[index] as Git.ITag;

return (
<ListItem
button
title={this.props.trans.__('Checkout to tag: %1', tag)}
title={this.props.trans.__('Checkout to tag: %1', tag.name)}
className={listItemClass}
onClick={this._onTagClickFactory(tag)}
onClick={this._onTagClickFactory(tag.name)}
style={style}
>
<tagIcon.react className={listItemIconClass} tag="span" />
<span className={nameClass}>{tag}</span>
<span className={nameClass}>{tag.name}</span>
</ListItem>
);
};
Expand Down
2 changes: 1 addition & 1 deletion src/components/Toolbar.tsx
Expand Up @@ -48,7 +48,7 @@ export interface IToolbarProps {
/**
* Current list of tags.
*/
tagsList: string[];
tagsList: Git.ITag[];

/**
* Boolean indicating whether branching is disabled.
Expand Down
8 changes: 4 additions & 4 deletions src/model.ts
Expand Up @@ -90,7 +90,7 @@ export class GitExtension implements IGitExtension {
/**
* Tags list for the current repository.
*/
get tagsList(): string[] {
get tagsList(): Git.ITag[] {
return this._tagsList;
}

Expand Down Expand Up @@ -1784,7 +1784,7 @@ export class GitExtension implements IGitExtension {
}

/**
* Retrieve the list of tags in the repository.
* Retrieve the list of tags in the repository, with the respective commits they point to.
*
* @returns promise which resolves upon retrieving the tag list
*
Expand Down Expand Up @@ -1845,7 +1845,7 @@ export class GitExtension implements IGitExtension {
async setTag(tag: string, commitId: string): Promise<void> {
const path = await this._getPathRepository();
await this._taskHandler.execute<void>('git:tag:create', async () => {
return await requestAPI<void>(URLExt.join(path, 'new_tag'), 'POST', {
return await requestAPI<void>(URLExt.join(path, 'tag'), 'POST', {
tag_id: tag,
commit_id: commitId
});
Expand Down Expand Up @@ -2195,7 +2195,7 @@ export class GitExtension implements IGitExtension {
private _stash: Git.IStash;
private _pathRepository: string | null = null;
private _branches: Git.IBranch[] = [];
private _tagsList: string[] = [];
private _tagsList: Git.ITag[] = [];
private _currentBranch: Git.IBranch | null = null;
private _docmanager: IDocumentManager | null;
private _docRegistry: DocumentRegistry | null;
Expand Down