Skip to content

Commit d152a60

Browse files
Load gitignore file (#1273) (#1298)
* Load gitignore file (#1273) * 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) * Fix tests --------- Co-authored-by: Kentaro Lim <[email protected]>
1 parent e63091d commit d152a60

File tree

13 files changed

+280
-26
lines changed

13 files changed

+280
-26
lines changed

jupyterlab_git/git.py

+37
Original file line numberDiff line numberDiff line change
@@ -1688,6 +1688,20 @@ async def remote_remove(self, path, name):
16881688

16891689
return response
16901690

1691+
def read_file(self, path):
1692+
"""
1693+
Reads file content located at path and returns it as a string
1694+
1695+
path: str
1696+
The path of the file
1697+
"""
1698+
try:
1699+
file = pathlib.Path(path)
1700+
content = file.read_text()
1701+
return {"code": 0, "content": content}
1702+
except BaseException as error:
1703+
return {"code": -1, "content": ""}
1704+
16911705
async def ensure_gitignore(self, path):
16921706
"""Handle call to ensure .gitignore file exists and the
16931707
next append will be on a new line (this means an empty file
@@ -1728,6 +1742,29 @@ async def ignore(self, path, file_path):
17281742
return {"code": -1, "message": str(error)}
17291743
return {"code": 0}
17301744

1745+
async def write_gitignore(self, path, content):
1746+
"""
1747+
Handle call to overwrite .gitignore.
1748+
Takes the .gitignore file and clears its previous contents
1749+
Writes the new content onto the file
1750+
1751+
path: str
1752+
Top Git repository path
1753+
content: str
1754+
New file contents
1755+
"""
1756+
try:
1757+
res = await self.ensure_gitignore(path)
1758+
if res["code"] != 0:
1759+
return res
1760+
gitignore = pathlib.Path(path) / ".gitignore"
1761+
if content and content[-1] != "\n":
1762+
content += "\n"
1763+
gitignore.write_text(content)
1764+
except BaseException as error:
1765+
return {"code": -1, "message": str(error)}
1766+
return {"code": 0}
1767+
17311768
async def version(self):
17321769
"""Return the Git command version.
17331770

jupyterlab_git/handlers.py

+15-2
Original file line numberDiff line numberDiff line change
@@ -810,6 +810,17 @@ class GitIgnoreHandler(GitHandler):
810810
Handler to manage .gitignore
811811
"""
812812

813+
@tornado.web.authenticated
814+
async def get(self, path: str = ""):
815+
"""
816+
GET read content in .gitignore
817+
"""
818+
local_path = self.url2localpath(path)
819+
body = self.git.read_file(local_path + "/.gitignore")
820+
if body["code"] != 0:
821+
self.set_status(500)
822+
self.finish(json.dumps(body))
823+
813824
@tornado.web.authenticated
814825
async def post(self, path: str = ""):
815826
"""
@@ -818,16 +829,18 @@ async def post(self, path: str = ""):
818829
local_path = self.url2localpath(path)
819830
data = self.get_json_body()
820831
file_path = data.get("file_path", None)
832+
content = data.get("content", None)
821833
use_extension = data.get("use_extension", False)
822-
if file_path:
834+
if content:
835+
body = await self.git.write_gitignore(local_path, content)
836+
elif file_path:
823837
if use_extension:
824838
suffixes = Path(file_path).suffixes
825839
if len(suffixes) > 0:
826840
file_path = "**/*" + ".".join(suffixes)
827841
body = await self.git.ignore(local_path, file_path)
828842
else:
829843
body = await self.git.ensure_gitignore(local_path)
830-
831844
if body["code"] != 0:
832845
self.set_status(500)
833846
self.finish(json.dumps(body))

schema/plugin.json

+6
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,12 @@
7676
"title": "Open files behind warning",
7777
"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.",
7878
"default": true
79+
},
80+
"hideHiddenFileWarning": {
81+
"type": "boolean",
82+
"title": "Hide hidden file warning",
83+
"description": "If true, the warning popup when opening the .gitignore file without hidden files will not be displayed.",
84+
"default": false
7985
}
8086
},
8187
"jupyter.lab.shortcuts": [

src/__tests__/commands.spec.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ describe('git-commands', () => {
6060
addCommands(
6161
app as JupyterFrontEnd,
6262
model,
63-
new CodeMirrorEditorFactory().newDocumentEditor,
63+
new CodeMirrorEditorFactory(),
6464
new EditorLanguageRegistry(),
6565
mockedFileBrowserModel,
6666
null as any,

src/commandsAndMenu.tsx

+145-15
Original file line numberDiff line numberDiff line change
@@ -8,35 +8,42 @@ import {
88
showDialog,
99
showErrorMessage
1010
} from '@jupyterlab/apputils';
11-
import { CodeEditor } from '@jupyterlab/codeeditor';
11+
import {
12+
CodeEditor,
13+
CodeEditorWrapper,
14+
IEditorFactoryService
15+
} from '@jupyterlab/codeeditor';
16+
import { IEditorLanguageRegistry } from '@jupyterlab/codemirror';
1217
import { PathExt, URLExt } from '@jupyterlab/coreutils';
1318
import { FileBrowser, FileBrowserModel } from '@jupyterlab/filebrowser';
1419
import { Contents } from '@jupyterlab/services';
1520
import { ISettingRegistry } from '@jupyterlab/settingregistry';
1621
import { ITerminal } from '@jupyterlab/terminal';
1722
import { ITranslator, TranslationBundle } from '@jupyterlab/translation';
1823
import {
19-
closeIcon,
2024
ContextMenuSvg,
2125
Toolbar,
22-
ToolbarButton
26+
ToolbarButton,
27+
closeIcon,
28+
saveIcon
2329
} from '@jupyterlab/ui-components';
24-
import { ArrayExt } from '@lumino/algorithm';
30+
import { ArrayExt, find } from '@lumino/algorithm';
2531
import { CommandRegistry } from '@lumino/commands';
2632
import { PromiseDelegate } from '@lumino/coreutils';
2733
import { Message } from '@lumino/messaging';
2834
import { ContextMenu, DockPanel, Menu, Panel, Widget } from '@lumino/widgets';
2935
import * as React from 'react';
3036
import { CancelledError } from './cancelledError';
3137
import { BranchPicker } from './components/BranchPicker';
38+
import { CONTEXT_COMMANDS } from './components/FileList';
39+
import { ManageRemoteDialogue } from './components/ManageRemoteDialogue';
3240
import { NewTagDialogBox } from './components/NewTagDialog';
33-
import { DiffModel } from './components/diff/model';
3441
import { createPlainTextDiff } from './components/diff/PlainTextDiff';
3542
import { PreviewMainAreaWidget } from './components/diff/PreviewMainAreaWidget';
36-
import { CONTEXT_COMMANDS } from './components/FileList';
37-
import { ManageRemoteDialogue } from './components/ManageRemoteDialogue';
43+
import { DiffModel } from './components/diff/model';
3844
import { AUTH_ERROR_MESSAGES, requestAPI } from './git';
39-
import { getDiffProvider, GitExtension } from './model';
45+
import { GitExtension, getDiffProvider } from './model';
46+
import { showDetails, showError } from './notifications';
4047
import {
4148
addIcon,
4249
diffIcon,
@@ -50,10 +57,8 @@ import {
5057
import { CommandIDs, ContextCommandIDs, Git, IGitExtension } from './tokens';
5158
import { AdvancedPushForm } from './widgets/AdvancedPushForm';
5259
import { GitCredentialsForm } from './widgets/CredentialsBox';
53-
import { discardAllChanges } from './widgets/discardAllChanges';
5460
import { CheckboxForm } from './widgets/GitResetToRemoteForm';
55-
import { IEditorLanguageRegistry } from '@jupyterlab/codemirror';
56-
import { showDetails, showError } from './notifications';
61+
import { discardAllChanges } from './widgets/discardAllChanges';
5762

5863
export interface IGitCloneArgs {
5964
/**
@@ -126,7 +131,7 @@ function pluralizedContextLabel(singular: string, plural: string) {
126131
export function addCommands(
127132
app: JupyterFrontEnd,
128133
gitModel: GitExtension,
129-
editorFactory: CodeEditor.Factory,
134+
editorFactory: IEditorFactoryService,
130135
languageRegistry: IEditorLanguageRegistry,
131136
fileBrowserModel: FileBrowserModel,
132137
settings: ISettingRegistry.ISettings,
@@ -314,13 +319,129 @@ export function addCommands(
314319
}
315320
});
316321

322+
async function showGitignore(error: any) {
323+
const model = new CodeEditor.Model({});
324+
const repoPath = gitModel.getRelativeFilePath();
325+
const id = repoPath + '/.git-ignore';
326+
const contentData = await gitModel.readGitIgnore();
327+
328+
const gitIgnoreWidget = find(
329+
shell.widgets(),
330+
shellWidget => shellWidget.id === id
331+
);
332+
if (gitIgnoreWidget) {
333+
shell.activateById(id);
334+
return;
335+
}
336+
model.sharedModel.setSource(contentData ? contentData : '');
337+
const editor = new CodeEditorWrapper({
338+
factory: editorFactory.newDocumentEditor.bind(editorFactory),
339+
model: model
340+
});
341+
const modelChangedSignal = model.sharedModel.changed;
342+
editor.disposed.connect(() => {
343+
model.dispose();
344+
});
345+
const preview = new MainAreaWidget({
346+
content: editor
347+
});
348+
349+
preview.title.label = '.gitignore';
350+
preview.id = id;
351+
preview.title.icon = gitIcon;
352+
preview.title.closable = true;
353+
preview.title.caption = repoPath + '/.gitignore';
354+
const saveButton = new ToolbarButton({
355+
icon: saveIcon,
356+
onClick: async () => {
357+
if (saved) {
358+
return;
359+
}
360+
const newContent = model.sharedModel.getSource();
361+
try {
362+
await gitModel.writeGitIgnore(newContent);
363+
preview.title.className = '';
364+
saved = true;
365+
} catch (error) {
366+
console.log('Could not save .gitignore');
367+
}
368+
},
369+
tooltip: trans.__('Saves .gitignore')
370+
});
371+
let saved = true;
372+
preview.toolbar.addItem('save', saveButton);
373+
shell.add(preview);
374+
modelChangedSignal.connect(() => {
375+
if (saved) {
376+
saved = false;
377+
preview.title.className = 'not-saved';
378+
}
379+
});
380+
}
381+
382+
/* Helper: Show gitignore hidden file */
383+
async function showGitignoreHiddenFile(error: any, hidePrompt: boolean) {
384+
if (hidePrompt) {
385+
return showGitignore(error);
386+
}
387+
const result = await showDialog({
388+
title: trans.__('Warning: The .gitignore file is a hidden file.'),
389+
body: (
390+
<div>
391+
{trans.__(
392+
'Hidden files by default cannot be accessed with the regular code editor. In order to open the .gitignore file you must:'
393+
)}
394+
<ol>
395+
<li>
396+
{trans.__(
397+
'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.'
398+
)}
399+
<div style={{ padding: '0.5rem' }}>
400+
{'jupyter server --generate-config'}
401+
</div>
402+
</li>
403+
<li>
404+
{trans.__(
405+
'Open jupyter_server_config.py, uncomment out the following line and set it to True:'
406+
)}
407+
<div style={{ padding: '0.5rem' }}>
408+
{'c.ContentsManager.allow_hidden = False'}
409+
</div>
410+
</li>
411+
</ol>
412+
</div>
413+
),
414+
buttons: [
415+
Dialog.cancelButton({ label: trans.__('Cancel') }),
416+
Dialog.okButton({ label: trans.__('Show .gitignore file anyways') })
417+
],
418+
checkbox: {
419+
label: trans.__('Do not show this warning again'),
420+
checked: false
421+
}
422+
});
423+
if (result.button.accept) {
424+
settings.set('hideHiddenFileWarning', result.isChecked);
425+
showGitignore(error);
426+
}
427+
}
428+
317429
/** Add git open gitignore command */
318430
commands.addCommand(CommandIDs.gitOpenGitignore, {
319431
label: trans.__('Open .gitignore'),
320432
caption: trans.__('Open .gitignore'),
321433
isEnabled: () => gitModel.pathRepository !== null,
322434
execute: async () => {
323-
await gitModel.ensureGitignore();
435+
try {
436+
await gitModel.ensureGitignore();
437+
} catch (error: any) {
438+
if (error?.name === 'hiddenFile') {
439+
await showGitignoreHiddenFile(
440+
error,
441+
settings.composite['hideHiddenFileWarning'] as boolean
442+
);
443+
}
444+
}
324445
}
325446
});
326447

@@ -586,7 +707,7 @@ export function addCommands(
586707
(options =>
587708
createPlainTextDiff({
588709
...options,
589-
editorFactory,
710+
editorFactory: editorFactory.newInlineEditor.bind(editorFactory),
590711
languageRegistry
591712
})));
592713

@@ -1523,7 +1644,16 @@ export function addCommands(
15231644
const { files } = args as any as CommandArguments.IGitContextAction;
15241645
for (const file of files) {
15251646
if (file) {
1526-
await gitModel.ignore(file.to, false);
1647+
try {
1648+
await gitModel.ignore(file.to, false);
1649+
} catch (error: any) {
1650+
if (error?.name === 'hiddenFile') {
1651+
await showGitignoreHiddenFile(
1652+
error,
1653+
settings.composite['hideHiddenFileWarning'] as boolean
1654+
);
1655+
}
1656+
}
15271657
}
15281658
}
15291659
}

src/components/diff/PreviewMainAreaWidget.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { MainAreaWidget } from '@jupyterlab/apputils/lib/mainareawidget';
1+
import { MainAreaWidget } from '@jupyterlab/apputils';
22
import { Message } from '@lumino/messaging';
33
import { Panel, TabBar, Widget } from '@lumino/widgets';
44

src/index.ts

+1-3
Original file line numberDiff line numberDiff line change
@@ -187,9 +187,7 @@ async function activate(
187187
addCommands(
188188
app,
189189
gitExtension,
190-
editorServices.factoryService.newInlineEditor.bind(
191-
editorServices.factoryService
192-
),
190+
editorServices.factoryService,
193191
languageRegistry,
194192
fileBrowser.model,
195193
settings,

0 commit comments

Comments
 (0)