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 ignore UI #705

Merged
merged 12 commits into from
Aug 5, 2020
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 '@lumino/commands';
import { Menu } from '@lumino/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
72 changes: 70 additions & 2 deletions src/components/FileList.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Dialog, showDialog, showErrorMessage } from '@jupyterlab/apputils';
import { PathExt } from '@jupyterlab/coreutils';
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
import { ISettingRegistry } from '@jupyterlab/settingregistry';
import { Menu } from '@lumino/widgets';
Expand Down Expand Up @@ -30,6 +31,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 @@ -51,6 +54,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 @@ -148,6 +152,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 @@ -165,10 +214,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 @@ -197,7 +259,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 @@ -751,6 +818,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;
}
59 changes: 59 additions & 0 deletions src/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1169,6 +1169,65 @@ export class GitExtension implements IGitExtension {
}

/**
* Make request to ensure gitignore.
*
* @param filename Optional name of the files to add
echarles marked this conversation as resolved.
Show resolved Hide resolved
*/
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
echarles marked this conversation as resolved.
Show resolved Hide resolved
*/
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
14 changes: 14 additions & 0 deletions src/tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export interface IGitExtension extends IDisposable {
/**
* The current branch
*/

echarles marked this conversation as resolved.
Show resolved Hide resolved
currentBranch: Git.IBranch;

/**
Expand Down Expand Up @@ -280,6 +281,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
echarles marked this conversation as resolved.
Show resolved Hide resolved
* @param useExtension Ignore all file with the same extension than filename
echarles marked this conversation as resolved.
Show resolved Hide resolved
*/
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