Skip to content

Commit

Permalink
Merge pull request #718 from fcollonval/auto-backport-of-pr-705-on-0.…
Browse files Browse the repository at this point in the history
…11.x

Backport PR #705: Git ignore UI
  • Loading branch information
fcollonval committed Aug 8, 2020
2 parents 48fd5d6 + 4139a71 commit 9a00d0b
Show file tree
Hide file tree
Showing 6 changed files with 232 additions and 3 deletions.
41 changes: 41 additions & 0 deletions jupyterlab_git/git.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import subprocess
from urllib.parse import unquote

import pathlib
import pexpect
import tornado
import tornado.locks
Expand Down Expand Up @@ -1110,6 +1111,46 @@ def remote_add(self, top_repo_path, url, name=DEFAULT_REMOTE_NAME):
"message": my_error.decode("utf-8").strip()
}

async def ensure_gitignore(self, top_repo_path):
"""Handle call to ensure .gitignore file exists and the
next append will be on a new line (this means an empty file
or a file ending with \n).
top_repo_path: str
Top Git repository path
"""
try:
gitignore = pathlib.Path(top_repo_path) / ".gitignore"
if not gitignore.exists():
gitignore.touch()
elif gitignore.stat().st_size > 0:
content = gitignore.read_text()
if (content[-1] != "\n"):
with gitignore.open("a") as f:
f.write('\n')
except BaseException as error:
return {"code": -1, "message": str(error)}
return {"code": 0}

async def ignore(self, top_repo_path, file_path):
"""Handle call to add an entry in .gitignore.
top_repo_path: str
Top Git repository path
file_path: str
The path of the file in .gitignore
"""
try:
res = await self.ensure_gitignore(top_repo_path)
if res["code"] != 0:
return res
gitignore = pathlib.Path(top_repo_path) / ".gitignore"
with gitignore.open("a") as f:
f.write(file_path + "\n")
except BaseException as error:
return {"code": -1, "message": str(error)}
return {"code": 0}

async def version(self):
"""Return the Git command version.
Expand Down
29 changes: 29 additions & 0 deletions jupyterlab_git/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,34 @@ async def post(self):
self.finish(json.dumps(response))


class GitIgnoreHandler(GitHandler):
"""
Handler to manage .gitignore
"""

@web.authenticated
async def post(self):
"""
POST add entry in .gitignore
"""
data = self.get_json_body()
top_repo_path = data["top_repo_path"]
file_path = data.get("file_path", None)
use_extension = data.get("use_extension", False)
if file_path:
if use_extension:
suffixes = Path(file_path).suffixes
if len(suffixes) > 0:
file_path = "**/*" + ".".join(suffixes)
body = await self.git.ignore(top_repo_path, file_path)
else:
body = await self.git.ensure_gitignore(top_repo_path)

if body["code"] != 0:
self.set_status(500)
self.finish(json.dumps(body))


class GitSettingsHandler(GitHandler):
@web.authenticated
async def get(self):
Expand Down Expand Up @@ -626,6 +654,7 @@ def setup_handlers(web_app):
("/git/show_top_level", GitShowTopLevelHandler),
("/git/status", GitStatusHandler),
("/git/upstream", GitUpstreamHandler),
("/git/ignore", GitIgnoreHandler),
("/git/tags", GitTagHandler),
("/git/tag_checkout", GitTagCheckoutHandler)
]
Expand Down
21 changes: 21 additions & 0 deletions src/commandsAndMenu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { ITerminal } from '@jupyterlab/terminal';
import { CommandRegistry } from '@phosphor/commands';
import { Menu } from '@phosphor/widgets';
import { IGitExtension } from './tokens';
import { GitExtension } from './model';
import { GitCredentialsForm } from './widgets/CredentialsBox';
import { doGitClone } from './widgets/gitClone';
import { GitPullPushDialog, Operation } from './widgets/gitPushPull';
Expand Down Expand Up @@ -39,6 +40,7 @@ export namespace CommandIDs {
export const gitToggleDoubleClickDiff = 'git:toggle-double-click-diff';
export const gitAddRemote = 'git:add-remote';
export const gitClone = 'git:clone';
export const gitOpenGitignore = 'git:open-gitignore';
export const gitPush = 'git:push';
export const gitPull = 'git:pull';
}
Expand Down Expand Up @@ -190,6 +192,21 @@ export function addCommands(
}
});

/** Add git open gitignore command */
commands.addCommand(CommandIDs.gitOpenGitignore, {
label: 'Open .gitignore',
caption: 'Open .gitignore',
isEnabled: () => model.pathRepository !== null,
execute: async () => {
await model.ensureGitignore();
const gitModel = model as GitExtension;
await gitModel.commands.execute('docmanager:reload');
await gitModel.commands.execute('docmanager:open', {
path: model.getRelativeFilePath('.gitignore')
});
}
});

/** Add git push command */
commands.addCommand(CommandIDs.gitPush, {
label: 'Push to Remote',
Expand Down Expand Up @@ -255,6 +272,10 @@ export function createGitMenu(commands: CommandRegistry): Menu {

menu.addItem({ type: 'separator' });

menu.addItem({ command: CommandIDs.gitOpenGitignore });

menu.addItem({ type: 'separator' });

const tutorial = new Menu({ commands });
tutorial.title.label = ' Help ';
RESOURCES.map(args => {
Expand Down
73 changes: 70 additions & 3 deletions src/components/FileList.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from 'react';
import { Dialog, showDialog, showErrorMessage } from '@jupyterlab/apputils';
import { ISettingRegistry } from '@jupyterlab/coreutils';
import { ISettingRegistry, PathExt } from '@jupyterlab/coreutils';
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
import { Menu } from '@phosphor/widgets';
import { GitExtension } from '../model';
Expand All @@ -23,6 +23,8 @@ export namespace CommandIDs {
export const gitFileDiscard = 'git:context-discard';
export const gitFileDiffWorking = 'git:context-diffWorking';
export const gitFileDiffIndex = 'git:context-diffIndex';
export const gitIgnore = 'git:context-ignore';
export const gitIgnoreExtension = 'git:context-ignoreExtension';
}

export interface IFileListState {
Expand All @@ -44,6 +46,7 @@ export class FileList extends React.Component<IFileListProps, IFileListState> {
this._contextMenuStaged = new Menu({ commands });
this._contextMenuUnstaged = new Menu({ commands });
this._contextMenuUntracked = new Menu({ commands });
this._contextMenuUntrackedMin = new Menu({ commands });
this._contextMenuSimpleUntracked = new Menu({ commands });
this._contextMenuSimpleTracked = new Menu({ commands });

Expand Down Expand Up @@ -141,6 +144,51 @@ export class FileList extends React.Component<IFileListProps, IFileListState> {
});
}

if (!commands.hasCommand(CommandIDs.gitIgnore)) {
commands.addCommand(CommandIDs.gitIgnore, {
label: () => 'Ignore this file (add to .gitignore)',
caption: () => 'Ignore this file (add to .gitignore)',
execute: async () => {
if (this.state.selectedFile) {
await this.props.model.ignore(this.state.selectedFile.to, false);
await this.props.model.commands.execute('docmanager:reload');
await this.props.model.commands.execute('docmanager:open', {
path: this.props.model.getRelativeFilePath('.gitignore')
});
}
}
});
}

if (!commands.hasCommand(CommandIDs.gitIgnoreExtension)) {
commands.addCommand(CommandIDs.gitIgnoreExtension, {
label: 'Ignore this file extension (add to .gitignore)',
caption: 'Ignore this file extension (add to .gitignore)',
execute: async () => {
if (this.state.selectedFile) {
const extension = PathExt.extname(this.state.selectedFile.to);
if (extension.length > 0) {
const result = await showDialog({
title: 'Ignore file extension',
body: `Are you sure you want to ignore all ${extension} files within this git repository?`,
buttons: [
Dialog.cancelButton(),
Dialog.okButton({ label: 'Ignore' })
]
});
if (result.button.label === 'Ignore') {
await this.props.model.ignore(this.state.selectedFile.to, true);
await this.props.model.commands.execute('docmanager:reload');
await this.props.model.commands.execute('docmanager:open', {
path: this.props.model.getRelativeFilePath('.gitignore')
});
}
}
}
}
});
}

[
CommandIDs.gitFileOpen,
CommandIDs.gitFileUnstage,
Expand All @@ -158,10 +206,23 @@ export class FileList extends React.Component<IFileListProps, IFileListState> {
this._contextMenuUnstaged.addItem({ command });
});

[CommandIDs.gitFileOpen, CommandIDs.gitFileTrack].forEach(command => {
[
CommandIDs.gitFileOpen,
CommandIDs.gitFileTrack,
CommandIDs.gitIgnore,
CommandIDs.gitIgnoreExtension
].forEach(command => {
this._contextMenuUntracked.addItem({ command });
});

[
CommandIDs.gitFileOpen,
CommandIDs.gitFileTrack,
CommandIDs.gitIgnore
].forEach(command => {
this._contextMenuUntrackedMin.addItem({ command });
});

[
CommandIDs.gitFileOpen,
CommandIDs.gitFileDiscard,
Expand Down Expand Up @@ -190,7 +251,12 @@ export class FileList extends React.Component<IFileListProps, IFileListState> {
/** Handle right-click on an untracked file */
contextMenuUntracked = (event: React.MouseEvent) => {
event.preventDefault();
this._contextMenuUntracked.open(event.clientX, event.clientY);
const extension = PathExt.extname(this.state.selectedFile.to);
if (extension.length > 0) {
this._contextMenuUntracked.open(event.clientX, event.clientY);
} else {
this._contextMenuUntrackedMin.open(event.clientX, event.clientY);
}
};

/** Handle right-click on an untracked file in Simple mode*/
Expand Down Expand Up @@ -744,6 +810,7 @@ export class FileList extends React.Component<IFileListProps, IFileListState> {
private _contextMenuStaged: Menu;
private _contextMenuUnstaged: Menu;
private _contextMenuUntracked: Menu;
private _contextMenuUntrackedMin: Menu;
private _contextMenuSimpleTracked: Menu;
private _contextMenuSimpleUntracked: Menu;
}
58 changes: 58 additions & 0 deletions src/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1172,6 +1172,64 @@ export class GitExtension implements IGitExtension {
}

/**
* Make request to ensure gitignore.
*
*/
async ensureGitignore(): Promise<Response> {
await this.ready;
const repositoryPath = this.pathRepository;

if (repositoryPath === null) {
return Promise.resolve(
new Response(
JSON.stringify({
code: -1,
message: 'Not in a git repository.'
})
)
);
}

const response = await httpGitRequest('/git/ignore', 'POST', {
top_repo_path: repositoryPath
});

this.refreshStatus();
return Promise.resolve(response);
}

/**
* Make request to ignore one file.
*
* @param filename Optional name of the files to add
*/
async ignore(filePath: string, useExtension: boolean): Promise<Response> {
await this.ready;
const repositoryPath = this.pathRepository;

if (repositoryPath === null) {
return Promise.resolve(
new Response(
JSON.stringify({
code: -1,
message: 'Not in a git repository.'
})
)
);
}

const response = await httpGitRequest('/git/ignore', 'POST', {
top_repo_path: repositoryPath,
file_path: filePath,
use_extension: useExtension
});

this.refreshStatus();
return Promise.resolve(response);
}

/**
* Make request for a list of all git branches in the repository
* Retrieve a list of repository branches.
*
* @returns promise which resolves upon fetching repository branches
Expand Down
13 changes: 13 additions & 0 deletions src/tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,19 @@ export interface IGitExtension extends IDisposable {
showTopLevel(path: string): Promise<Git.IShowTopLevelResult>;

/**
* Ensure a .gitignore file exists
*/
ensureGitignore(): Promise<Response>;

/**
* Add an entry in .gitignore file
*
* @param filename The name of the entry to ignore
* @param useExtension Ignore all files having the same extension as filename
*/
ignore(filename: string, useExtension: boolean): Promise<Response>;

/*
* Make request to list all the tags present in the remote repo
*
* @returns list of tags
Expand Down

0 comments on commit 9a00d0b

Please sign in to comment.