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
42 changes: 42 additions & 0 deletions jupyterlab_git/git.py
Original file line number Diff line number Diff line change
Expand Up @@ -1112,6 +1112,48 @@ 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 = os.path.join(top_repo_path, ".gitignore")
if not os.path.exists(gitignore):
open(gitignore, "w").close()
elif os.path.getsize(gitignore) != 0:
f = open(gitignore, "r")
if (f.read()[-1] != "\n"):
f = open(gitignore, "a")
f.write('\n')
f.close()
return {"code": 0}
except:
return {"code": -1}
echarles marked this conversation as resolved.
Show resolved Hide resolved

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 = os.path.join(top_repo_path, '.gitignore')
f = open(gitignore, "a")
f.write(file_path + "\n")
f.close()
return {"code": 0}
except:
return {"code": -1}
echarles marked this conversation as resolved.
Show resolved Hide resolved

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:
parts = os.path.splitext(file_path)
if len(parts) == 2:
file_path = "**/*" + parts[1]
echarles marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -594,6 +622,7 @@ def setup_handlers(web_app):
("/git/show_top_level", GitShowTopLevelHandler),
("/git/status", GitStatusHandler),
("/git/upstream", GitUpstreamHandler),
("/git/ignore", GitIgnoreHandler),
]

# add the baseurl to our paths
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 () => {
model.ensureGitignore();
echarles marked this conversation as resolved.
Show resolved Hide resolved
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
55 changes: 53 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,34 @@ 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 () => {
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 () => {
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 +197,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 +242,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 && extension !== 'ipynb' && extension !== 'py') {
this._contextMenuUntracked.open(event.clientX, event.clientY);
} else {
this._contextMenuUntrackedMin.open(event.clientX, event.clientY);
}
echarles marked this conversation as resolved.
Show resolved Hide resolved
};

/** Handle right-click on an untracked file in Simple mode*/
Expand Down Expand Up @@ -751,6 +801,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 @@ -1094,6 +1094,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 @@ -278,6 +279,19 @@ export interface IGitExtension extends IDisposable {
* @param path Path from which the top Git repository needs to be found
*/
showTopLevel(path: string): Promise<Git.IShowTopLevelResult>;

/**
* Ensure a.gitignore file
echarles marked this conversation as resolved.
Show resolved Hide resolved
*
*/
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
*/
ignore(filename: string, useExtension: boolean): Promise<Response>;
}

export namespace Git {
Expand Down