Skip to content

Commit

Permalink
Load gitignore file (jupyterlab#1273)
Browse files Browse the repository at this point in the history
* Throw 403 if file is hidden

* Add Helpful GUI message for hidden files

* Fetch file contents if hidden file

* Show hidden file in widget

* Format gitignore file

* Show hidden file message when adding file to .gitignore

* Add ability to save .gitignore

* Fix bug where you can open two .gitignore files

* Add option to hide hidden file warning

* Fix PR requests for gitignore bug

* Fix prettier styles for gitignore

* Improve translation

* Improve gitignore model and add hiddenFile option to schema

* Fix eslint

* Fix .gitignore content sending

---------

Co-authored-by: Frédéric Collonval <[email protected]>
(cherry picked from commit 23fa764)
  • Loading branch information
kentarolim10 authored and fcollonval committed Nov 16, 2023
1 parent 9872008 commit bc60971
Show file tree
Hide file tree
Showing 12 changed files with 279 additions and 25 deletions.
37 changes: 37 additions & 0 deletions jupyterlab_git/git.py
Original file line number Diff line number Diff line change
Expand Up @@ -1688,6 +1688,20 @@ async def remote_remove(self, path, name):

return response

def read_file(self, path):
"""
Reads file content located at path and returns it as a string
path: str
The path of the file
"""
try:
file = pathlib.Path(path)
content = file.read_text()
return {"code": 0, "content": content}
except BaseException as error:
return {"code": -1, "content": ""}

async def ensure_gitignore(self, path):
"""Handle call to ensure .gitignore file exists and the
next append will be on a new line (this means an empty file
Expand Down Expand Up @@ -1728,6 +1742,29 @@ async def ignore(self, path, file_path):
return {"code": -1, "message": str(error)}
return {"code": 0}

async def write_gitignore(self, path, content):
"""
Handle call to overwrite .gitignore.
Takes the .gitignore file and clears its previous contents
Writes the new content onto the file
path: str
Top Git repository path
content: str
New file contents
"""
try:
res = await self.ensure_gitignore(path)
if res["code"] != 0:
return res
gitignore = pathlib.Path(path) / ".gitignore"
if content and content[-1] != "\n":
content += "\n"
gitignore.write_text(content)
except BaseException as error:
return {"code": -1, "message": str(error)}
return {"code": 0}

async def version(self):
"""Return the Git command version.
Expand Down
17 changes: 15 additions & 2 deletions jupyterlab_git/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -810,6 +810,17 @@ class GitIgnoreHandler(GitHandler):
Handler to manage .gitignore
"""

@tornado.web.authenticated
async def get(self, path: str = ""):
"""
GET read content in .gitignore
"""
local_path = self.url2localpath(path)
body = self.git.read_file(local_path + "/.gitignore")
if body["code"] != 0:
self.set_status(500)
self.finish(json.dumps(body))

@tornado.web.authenticated
async def post(self, path: str = ""):
"""
Expand All @@ -818,16 +829,18 @@ async def post(self, path: str = ""):
local_path = self.url2localpath(path)
data = self.get_json_body()
file_path = data.get("file_path", None)
content = data.get("content", None)
use_extension = data.get("use_extension", False)
if file_path:
if content:
body = await self.git.write_gitignore(local_path, content)
elif file_path:
if use_extension:
suffixes = Path(file_path).suffixes
if len(suffixes) > 0:
file_path = "**/*" + ".".join(suffixes)
body = await self.git.ignore(local_path, file_path)
else:
body = await self.git.ensure_gitignore(local_path)

if body["code"] != 0:
self.set_status(500)
self.finish(json.dumps(body))
Expand Down
6 changes: 6 additions & 0 deletions schema/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,12 @@
"title": "Open files behind warning",
"description": "If true, a popup dialog will be displayed if a user opens a file that is behind its remote branch version, or if an opened file has updates on the remote branch.",
"default": true
},
"hideHiddenFileWarning": {
"type": "boolean",
"title": "Hide hidden file warning",
"description": "If true, the warning popup when opening the .gitignore file without hidden files will not be displayed.",
"default": false
}
},
"jupyter.lab.shortcuts": [
Expand Down
160 changes: 145 additions & 15 deletions src/commandsAndMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,35 +8,42 @@ import {
showDialog,
showErrorMessage
} from '@jupyterlab/apputils';
import { CodeEditor } from '@jupyterlab/codeeditor';
import {
CodeEditor,
CodeEditorWrapper,
IEditorFactoryService
} from '@jupyterlab/codeeditor';
import { IEditorLanguageRegistry } from '@jupyterlab/codemirror';
import { PathExt, URLExt } from '@jupyterlab/coreutils';
import { FileBrowser, FileBrowserModel } from '@jupyterlab/filebrowser';
import { Contents } from '@jupyterlab/services';
import { ISettingRegistry } from '@jupyterlab/settingregistry';
import { ITerminal } from '@jupyterlab/terminal';
import { ITranslator, TranslationBundle } from '@jupyterlab/translation';
import {
closeIcon,
ContextMenuSvg,
Toolbar,
ToolbarButton
ToolbarButton,
closeIcon,
saveIcon
} from '@jupyterlab/ui-components';
import { ArrayExt } from '@lumino/algorithm';
import { ArrayExt, find } from '@lumino/algorithm';
import { CommandRegistry } from '@lumino/commands';
import { PromiseDelegate } from '@lumino/coreutils';
import { Message } from '@lumino/messaging';
import { ContextMenu, DockPanel, Menu, Panel, Widget } from '@lumino/widgets';
import * as React from 'react';
import { CancelledError } from './cancelledError';
import { BranchPicker } from './components/BranchPicker';
import { CONTEXT_COMMANDS } from './components/FileList';
import { ManageRemoteDialogue } from './components/ManageRemoteDialogue';
import { NewTagDialogBox } from './components/NewTagDialog';
import { DiffModel } from './components/diff/model';
import { createPlainTextDiff } from './components/diff/PlainTextDiff';
import { PreviewMainAreaWidget } from './components/diff/PreviewMainAreaWidget';
import { CONTEXT_COMMANDS } from './components/FileList';
import { ManageRemoteDialogue } from './components/ManageRemoteDialogue';
import { DiffModel } from './components/diff/model';
import { AUTH_ERROR_MESSAGES, requestAPI } from './git';
import { getDiffProvider, GitExtension } from './model';
import { GitExtension, getDiffProvider } from './model';
import { showDetails, showError } from './notifications';
import {
addIcon,
diffIcon,
Expand All @@ -50,10 +57,8 @@ import {
import { CommandIDs, ContextCommandIDs, Git, IGitExtension } from './tokens';
import { AdvancedPushForm } from './widgets/AdvancedPushForm';
import { GitCredentialsForm } from './widgets/CredentialsBox';
import { discardAllChanges } from './widgets/discardAllChanges';
import { CheckboxForm } from './widgets/GitResetToRemoteForm';
import { IEditorLanguageRegistry } from '@jupyterlab/codemirror';
import { showDetails, showError } from './notifications';
import { discardAllChanges } from './widgets/discardAllChanges';

export interface IGitCloneArgs {
/**
Expand Down Expand Up @@ -126,7 +131,7 @@ function pluralizedContextLabel(singular: string, plural: string) {
export function addCommands(
app: JupyterFrontEnd,
gitModel: GitExtension,
editorFactory: CodeEditor.Factory,
editorFactory: IEditorFactoryService,
languageRegistry: IEditorLanguageRegistry,
fileBrowserModel: FileBrowserModel,
settings: ISettingRegistry.ISettings,
Expand Down Expand Up @@ -314,13 +319,129 @@ export function addCommands(
}
});

async function showGitignore(error: any) {
const model = new CodeEditor.Model({});
const repoPath = gitModel.getRelativeFilePath();
const id = repoPath + '/.git-ignore';
const contentData = await gitModel.readGitIgnore();

const gitIgnoreWidget = find(
shell.widgets(),
shellWidget => shellWidget.id === id
);
if (gitIgnoreWidget) {
shell.activateById(id);
return;
}
model.sharedModel.setSource(contentData ? contentData : '');
const editor = new CodeEditorWrapper({
factory: editorFactory.newDocumentEditor.bind(editorFactory),
model: model
});
const modelChangedSignal = model.sharedModel.changed;
editor.disposed.connect(() => {
model.dispose();
});
const preview = new MainAreaWidget({
content: editor
});

preview.title.label = '.gitignore';
preview.id = id;
preview.title.icon = gitIcon;
preview.title.closable = true;
preview.title.caption = repoPath + '/.gitignore';
const saveButton = new ToolbarButton({
icon: saveIcon,
onClick: async () => {
if (saved) {
return;
}
const newContent = model.sharedModel.getSource();
try {
await gitModel.writeGitIgnore(newContent);
preview.title.className = '';
saved = true;
} catch (error) {
console.log('Could not save .gitignore');
}
},
tooltip: trans.__('Saves .gitignore')
});
let saved = true;
preview.toolbar.addItem('save', saveButton);
shell.add(preview);
modelChangedSignal.connect(() => {
if (saved) {
saved = false;
preview.title.className = 'not-saved';
}
});
}

/* Helper: Show gitignore hidden file */
async function showGitignoreHiddenFile(error: any, hidePrompt: boolean) {
if (hidePrompt) {
return showGitignore(error);
}
const result = await showDialog({
title: trans.__('Warning: The .gitignore file is a hidden file.'),
body: (
<div>
{trans.__(
'Hidden files by default cannot be accessed with the regular code editor. In order to open the .gitignore file you must:'
)}
<ol>
<li>
{trans.__(
'Print the command below to create a jupyter_server_config.py file with defaults commented out. If you already have the file located in .jupyter, skip this step.'
)}
<div style={{ padding: '0.5rem' }}>
{'jupyter server --generate-config'}
</div>
</li>
<li>
{trans.__(
'Open jupyter_server_config.py, uncomment out the following line and set it to True:'
)}
<div style={{ padding: '0.5rem' }}>
{'c.ContentsManager.allow_hidden = False'}
</div>
</li>
</ol>
</div>
),
buttons: [
Dialog.cancelButton({ label: trans.__('Cancel') }),
Dialog.okButton({ label: trans.__('Show .gitignore file anyways') })
],
checkbox: {
label: trans.__('Do not show this warning again'),
checked: false
}
});
if (result.button.accept) {
settings.set('hideHiddenFileWarning', result.isChecked);
showGitignore(error);
}
}

/** Add git open gitignore command */
commands.addCommand(CommandIDs.gitOpenGitignore, {
label: trans.__('Open .gitignore'),
caption: trans.__('Open .gitignore'),
isEnabled: () => gitModel.pathRepository !== null,
execute: async () => {
await gitModel.ensureGitignore();
try {
await gitModel.ensureGitignore();
} catch (error: any) {
if (error?.name === 'hiddenFile') {
await showGitignoreHiddenFile(
error,
settings.composite['hideHiddenFileWarning'] as boolean
);
}
}
}
});

Expand Down Expand Up @@ -586,7 +707,7 @@ export function addCommands(
(options =>
createPlainTextDiff({
...options,
editorFactory,
editorFactory: editorFactory.newInlineEditor.bind(editorFactory),
languageRegistry
})));

Expand Down Expand Up @@ -1523,7 +1644,16 @@ export function addCommands(
const { files } = args as any as CommandArguments.IGitContextAction;
for (const file of files) {
if (file) {
await gitModel.ignore(file.to, false);
try {
await gitModel.ignore(file.to, false);
} catch (error: any) {
if (error?.name === 'hiddenFile') {
await showGitignoreHiddenFile(
error,
settings.composite['hideHiddenFileWarning'] as boolean
);
}
}
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/components/diff/PreviewMainAreaWidget.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { MainAreaWidget } from '@jupyterlab/apputils/lib/mainareawidget';
import { MainAreaWidget } from '@jupyterlab/apputils';
import { Message } from '@lumino/messaging';
import { Panel, TabBar, Widget } from '@lumino/widgets';

Expand Down
4 changes: 1 addition & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,9 +187,7 @@ async function activate(
addCommands(
app,
gitExtension,
editorServices.factoryService.newInlineEditor.bind(
editorServices.factoryService
),
editorServices.factoryService,
languageRegistry,
fileBrowser.model,
settings,
Expand Down
Loading

0 comments on commit bc60971

Please sign in to comment.