From ed1c9d28b607d127a6c2925b43e297039e94d224 Mon Sep 17 00:00:00 2001 From: Kentaro Lim Date: Fri, 6 Oct 2023 12:42:21 -0700 Subject: [PATCH 01/15] Throw 403 if file is hidden --- jupyterlab_git/git.py | 2 ++ jupyterlab_git/handlers.py | 4 +++- src/git.ts | 3 +++ src/model.ts | 1 + src/tokens.ts | 6 ++++++ 5 files changed, 15 insertions(+), 1 deletion(-) diff --git a/jupyterlab_git/git.py b/jupyterlab_git/git.py index 27a4ea352..9dc29df52 100644 --- a/jupyterlab_git/git.py +++ b/jupyterlab_git/git.py @@ -1698,6 +1698,8 @@ async def ensure_gitignore(self, path): if content[-1] != "\n": with gitignore.open("a") as f: f.write("\n") + else: # .gitignore exists, but the file is hidden + return {"code": -2, "message": ".gitignore exists but is hidden"} except BaseException as error: return {"code": -1, "message": str(error)} return {"code": 0} diff --git a/jupyterlab_git/handlers.py b/jupyterlab_git/handlers.py index 1a9edb908..651070f62 100644 --- a/jupyterlab_git/handlers.py +++ b/jupyterlab_git/handlers.py @@ -828,7 +828,9 @@ async def post(self, path: str = ""): else: body = await self.git.ensure_gitignore(local_path) - if body["code"] != 0: + if body["code"] == -2: + self.set_status(403, body["message"]) + elif body["code"] != 0: self.set_status(500) self.finish(json.dumps(body)) diff --git a/src/git.ts b/src/git.ts index f54fac670..2fbb8d892 100644 --- a/src/git.ts +++ b/src/git.ts @@ -65,6 +65,9 @@ export async function requestAPI( if (!response.ok) { if (isJSON) { const { message, traceback, ...json } = data; + if (response.status === 403) { + throw new Git.HiddenFile(); + } throw new Git.GitResponseError( response, message || diff --git a/src/model.ts b/src/model.ts index 9620bd486..6143716a8 100644 --- a/src/model.ts +++ b/src/model.ts @@ -860,6 +860,7 @@ export class GitExtension implements IGitExtension { * @throws {Git.NotInRepository} If the current path is not a Git repository * @throws {Git.GitResponseError} If the server response is not ok * @throws {ServerConnection.NetworkError} If the request cannot be made + * @throws {Git.HiddenFile} If the file is hidden */ async ensureGitignore(): Promise { const path = await this._getPathRepository(); diff --git a/src/tokens.ts b/src/tokens.ts index 551ab4bf4..010dd5aac 100644 --- a/src/tokens.ts +++ b/src/tokens.ts @@ -1300,6 +1300,12 @@ export namespace Git { } } + export class HiddenFile extends Error { + constructor() { + super('File is hidden'); + } + } + /** * Interface for dialog with one checkbox. */ From 972e6dc7ee560deb9d1aeb714de4cd4268190de2 Mon Sep 17 00:00:00 2001 From: Kentaro Lim Date: Wed, 11 Oct 2023 07:28:07 -0700 Subject: [PATCH 02/15] Add Helpful GUI message for hidden files --- jupyterlab_git/git.py | 2 -- jupyterlab_git/handlers.py | 2 ++ src/commandsAndMenu.tsx | 35 ++++++++++++++++++++++++++++++++++- src/tokens.ts | 2 ++ 4 files changed, 38 insertions(+), 3 deletions(-) diff --git a/jupyterlab_git/git.py b/jupyterlab_git/git.py index 9dc29df52..27a4ea352 100644 --- a/jupyterlab_git/git.py +++ b/jupyterlab_git/git.py @@ -1698,8 +1698,6 @@ async def ensure_gitignore(self, path): if content[-1] != "\n": with gitignore.open("a") as f: f.write("\n") - else: # .gitignore exists, but the file is hidden - return {"code": -2, "message": ".gitignore exists but is hidden"} except BaseException as error: return {"code": -1, "message": str(error)} return {"code": 0} diff --git a/jupyterlab_git/handlers.py b/jupyterlab_git/handlers.py index 651070f62..57f21f821 100644 --- a/jupyterlab_git/handlers.py +++ b/jupyterlab_git/handlers.py @@ -826,6 +826,8 @@ async def post(self, path: str = ""): file_path = "**/*" + ".".join(suffixes) body = await self.git.ignore(local_path, file_path) else: + if not self.serverapp.contents_manager.allow_hidden: + self.set_status(403, "hidden files cannot be accessed") body = await self.git.ensure_gitignore(local_path) if body["code"] == -2: diff --git a/src/commandsAndMenu.tsx b/src/commandsAndMenu.tsx index e2920d18d..8237ca812 100644 --- a/src/commandsAndMenu.tsx +++ b/src/commandsAndMenu.tsx @@ -309,7 +309,40 @@ export function addCommands( 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 showDialog({ + title: trans.__('The .gitignore file cannot be accessed'), + body: ( +
+ {trans.__( + 'Hidden files by default cannot be accessed. In order to open the .gitignore file you must:' + )} +
    +
  1. + {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.' + )} +
    + {trans.__('jupyter server --generate-config')} +
    +
  2. +
  3. + {trans.__( + 'Open jupyter_server_config.py, uncomment out the following line and set it to True:' + )} +
    + {trans.__('c.ContentsManager.allow_hidden = False')} +
    +
  4. +
+
+ ) + }); + } + } } }); diff --git a/src/tokens.ts b/src/tokens.ts index 010dd5aac..325626545 100644 --- a/src/tokens.ts +++ b/src/tokens.ts @@ -1303,6 +1303,8 @@ export namespace Git { export class HiddenFile extends Error { constructor() { super('File is hidden'); + this.name = 'hiddenFile'; + this.message = 'File is hidden and cannot be accessed.'; } } From 939e9cbb598f8994b344229659b9f8d4c640cbff Mon Sep 17 00:00:00 2001 From: Kentaro Lim Date: Wed, 18 Oct 2023 07:28:40 -0700 Subject: [PATCH 03/15] Fetch file contents if hidden file --- jupyterlab_git/git.py | 11 +++++++++++ jupyterlab_git/handlers.py | 7 +++---- src/git.ts | 2 +- src/tokens.ts | 4 +++- 4 files changed, 18 insertions(+), 6 deletions(-) diff --git a/jupyterlab_git/git.py b/jupyterlab_git/git.py index 27a4ea352..497632e55 100644 --- a/jupyterlab_git/git.py +++ b/jupyterlab_git/git.py @@ -1681,6 +1681,17 @@ async def remote_remove(self, path, name): return response + async def read_file(self, path): + try: + file = pathlib.Path(path) + if file.stat().st_size > 0: + content = file.read_text() + print(content) + return content + return "" + except BaseException as error: + return "" + 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 diff --git a/jupyterlab_git/handlers.py b/jupyterlab_git/handlers.py index 57f21f821..594462b11 100644 --- a/jupyterlab_git/handlers.py +++ b/jupyterlab_git/handlers.py @@ -826,13 +826,12 @@ async def post(self, path: str = ""): file_path = "**/*" + ".".join(suffixes) body = await self.git.ignore(local_path, file_path) else: + body = await self.git.ensure_gitignore(local_path) if not self.serverapp.contents_manager.allow_hidden: self.set_status(403, "hidden files cannot be accessed") - body = await self.git.ensure_gitignore(local_path) + body["content"] = await self.git.read_file(local_path + "/.gitignore") - if body["code"] == -2: - self.set_status(403, body["message"]) - elif body["code"] != 0: + if body["code"] != 0: self.set_status(500) self.finish(json.dumps(body)) diff --git a/src/git.ts b/src/git.ts index 2fbb8d892..f18093af9 100644 --- a/src/git.ts +++ b/src/git.ts @@ -66,7 +66,7 @@ export async function requestAPI( if (isJSON) { const { message, traceback, ...json } = data; if (response.status === 403) { - throw new Git.HiddenFile(); + throw new Git.HiddenFile(data.content); } throw new Git.GitResponseError( response, diff --git a/src/tokens.ts b/src/tokens.ts index 325626545..73a5918d4 100644 --- a/src/tokens.ts +++ b/src/tokens.ts @@ -1301,10 +1301,12 @@ export namespace Git { } export class HiddenFile extends Error { - constructor() { + content: string; + constructor(content: string) { super('File is hidden'); this.name = 'hiddenFile'; this.message = 'File is hidden and cannot be accessed.'; + this.content = content; } } From 37e6ee32c121df6a7a7b21ed5fa5dd576d32e917 Mon Sep 17 00:00:00 2001 From: Kentaro Lim Date: Wed, 18 Oct 2023 07:29:44 -0700 Subject: [PATCH 04/15] Show hidden file in widget --- src/commandsAndMenu.tsx | 39 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/src/commandsAndMenu.tsx b/src/commandsAndMenu.tsx index 8237ca812..271ea3a04 100644 --- a/src/commandsAndMenu.tsx +++ b/src/commandsAndMenu.tsx @@ -52,6 +52,9 @@ import { AdvancedPushForm } from './widgets/AdvancedPushForm'; import { GitCredentialsForm } from './widgets/CredentialsBox'; import { discardAllChanges } from './widgets/discardAllChanges'; import { CheckboxForm } from './widgets/GitResetToRemoteForm'; +import { CodeEditor } from '@jupyterlab/codeeditor/lib/editor'; +import { CodeEditorWrapper } from '@jupyterlab/codeeditor/lib/widget'; +import { CodeMirrorEditorFactory } from '@jupyterlab/codemirror/lib/factory'; export interface IGitCloneArgs { /** @@ -313,7 +316,7 @@ export function addCommands( await gitModel.ensureGitignore(); } catch (error: any) { if (error?.name === 'hiddenFile') { - await showDialog({ + const result = await showDialog({ title: trans.__('The .gitignore file cannot be accessed'), body: (
@@ -339,8 +342,40 @@ export function addCommands(
- ) + ), + buttons: [ + Dialog.cancelButton({ label: trans.__('Cancel') }), + Dialog.okButton({ label: trans.__('Show .gitignore file') }) + ] }); + if (result.button.accept) { + const model = new CodeEditor.Model({}); + // const host = document.createElement('div'); + const previewWidget = new PreviewMainAreaWidget({ + content: new Panel() + }); + const id = 'git-ignore'; + // document.body.appendChild(host); + model.sharedModel.setSource(error.content ? error.content : ''); + const widget = new CodeEditorWrapper({ + factory: () => + new CodeMirrorEditorFactory().newDocumentEditor({ + model: model, + host: previewWidget.node, + config: { readOnly: true } + }), + model: model + }); + widget.disposed.connect(() => { + model.dispose(); + }); + // widget.id = id; + // shell.add(widget); + // shell.activateById(widget.id); + + previewWidget.id = id; + shell.add(previewWidget); + } } } } From 335a43a420e0e2dc0c469bf7631ea1998de7eeec Mon Sep 17 00:00:00 2001 From: Kentaro Lim Date: Wed, 18 Oct 2023 09:30:09 -0700 Subject: [PATCH 05/15] Format gitignore file --- src/commandsAndMenu.tsx | 35 ++++++++++++++--------------------- 1 file changed, 14 insertions(+), 21 deletions(-) diff --git a/src/commandsAndMenu.tsx b/src/commandsAndMenu.tsx index 271ea3a04..337aac886 100644 --- a/src/commandsAndMenu.tsx +++ b/src/commandsAndMenu.tsx @@ -54,7 +54,7 @@ import { discardAllChanges } from './widgets/discardAllChanges'; import { CheckboxForm } from './widgets/GitResetToRemoteForm'; import { CodeEditor } from '@jupyterlab/codeeditor/lib/editor'; import { CodeEditorWrapper } from '@jupyterlab/codeeditor/lib/widget'; -import { CodeMirrorEditorFactory } from '@jupyterlab/codemirror/lib/factory'; +import { editorServices } from '@jupyterlab/codemirror'; export interface IGitCloneArgs { /** @@ -350,31 +350,24 @@ export function addCommands( }); if (result.button.accept) { const model = new CodeEditor.Model({}); - // const host = document.createElement('div'); - const previewWidget = new PreviewMainAreaWidget({ - content: new Panel() - }); const id = 'git-ignore'; - // document.body.appendChild(host); model.sharedModel.setSource(error.content ? error.content : ''); - const widget = new CodeEditorWrapper({ - factory: () => - new CodeMirrorEditorFactory().newDocumentEditor({ - model: model, - host: previewWidget.node, - config: { readOnly: true } - }), - model: model + const editor = new CodeEditorWrapper({ + factory: editorServices.factoryService.newDocumentEditor, + model: model, + config: { readOnly: true } }); - widget.disposed.connect(() => { + editor.disposed.connect(() => { model.dispose(); }); - // widget.id = id; - // shell.add(widget); - // shell.activateById(widget.id); - - previewWidget.id = id; - shell.add(previewWidget); + const preview = new PreviewMainAreaWidget({ + content: editor + }); + preview.title.label = '.gitignore'; + preview.id = id; + preview.title.icon = gitIcon; + preview.title.closable = true; + shell.add(preview); } } } From adc74aaa7994026936d1e66287f500fe8feaa108 Mon Sep 17 00:00:00 2001 From: Kentaro Lim Date: Wed, 18 Oct 2023 10:33:39 -0700 Subject: [PATCH 06/15] Show hidden file message when adding file to .gitignore --- jupyterlab_git/handlers.py | 6 +- src/commandsAndMenu.tsx | 120 ++++++++++++++++++++----------------- src/model.ts | 1 + 3 files changed, 70 insertions(+), 57 deletions(-) diff --git a/jupyterlab_git/handlers.py b/jupyterlab_git/handlers.py index 594462b11..3fb390473 100644 --- a/jupyterlab_git/handlers.py +++ b/jupyterlab_git/handlers.py @@ -827,9 +827,9 @@ async def post(self, path: str = ""): body = await self.git.ignore(local_path, file_path) else: body = await self.git.ensure_gitignore(local_path) - if not self.serverapp.contents_manager.allow_hidden: - self.set_status(403, "hidden files cannot be accessed") - body["content"] = await self.git.read_file(local_path + "/.gitignore") + if not self.serverapp.contents_manager.allow_hidden: + self.set_status(403, "hidden files cannot be accessed") + body["content"] = await self.git.read_file(local_path + "/.gitignore") if body["code"] != 0: self.set_status(500) diff --git a/src/commandsAndMenu.tsx b/src/commandsAndMenu.tsx index 337aac886..25c1e33ab 100644 --- a/src/commandsAndMenu.tsx +++ b/src/commandsAndMenu.tsx @@ -306,6 +306,63 @@ export function addCommands( } }); + /* Helper: Show gitignore hidden file */ + async function showGitignoreHiddenFile(error: any) { + const result = await showDialog({ + title: trans.__('The .gitignore file cannot be accessed'), + body: ( +
+ {trans.__( + 'Hidden files by default cannot be accessed. In order to open the .gitignore file you must:' + )} +
    +
  1. + {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.' + )} +
    + {trans.__('jupyter server --generate-config')} +
    +
  2. +
  3. + {trans.__( + 'Open jupyter_server_config.py, uncomment out the following line and set it to True:' + )} +
    + {trans.__('c.ContentsManager.allow_hidden = False')} +
    +
  4. +
+
+ ), + buttons: [ + Dialog.cancelButton({ label: trans.__('Cancel') }), + Dialog.okButton({ label: trans.__('Show .gitignore file') }) + ] + }); + if (result.button.accept) { + const model = new CodeEditor.Model({}); + const id = 'git-ignore'; + model.sharedModel.setSource(error.content ? error.content : ''); + const editor = new CodeEditorWrapper({ + factory: editorServices.factoryService.newDocumentEditor, + model: model, + config: { readOnly: true } + }); + editor.disposed.connect(() => { + model.dispose(); + }); + const preview = new PreviewMainAreaWidget({ + content: editor + }); + preview.title.label = '.gitignore'; + preview.id = id; + preview.title.icon = gitIcon; + preview.title.closable = true; + shell.add(preview); + } + } + /** Add git open gitignore command */ commands.addCommand(CommandIDs.gitOpenGitignore, { label: trans.__('Open .gitignore'), @@ -316,59 +373,7 @@ export function addCommands( await gitModel.ensureGitignore(); } catch (error: any) { if (error?.name === 'hiddenFile') { - const result = await showDialog({ - title: trans.__('The .gitignore file cannot be accessed'), - body: ( -
- {trans.__( - 'Hidden files by default cannot be accessed. In order to open the .gitignore file you must:' - )} -
    -
  1. - {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.' - )} -
    - {trans.__('jupyter server --generate-config')} -
    -
  2. -
  3. - {trans.__( - 'Open jupyter_server_config.py, uncomment out the following line and set it to True:' - )} -
    - {trans.__('c.ContentsManager.allow_hidden = False')} -
    -
  4. -
-
- ), - buttons: [ - Dialog.cancelButton({ label: trans.__('Cancel') }), - Dialog.okButton({ label: trans.__('Show .gitignore file') }) - ] - }); - if (result.button.accept) { - const model = new CodeEditor.Model({}); - const id = 'git-ignore'; - model.sharedModel.setSource(error.content ? error.content : ''); - const editor = new CodeEditorWrapper({ - factory: editorServices.factoryService.newDocumentEditor, - model: model, - config: { readOnly: true } - }); - editor.disposed.connect(() => { - model.dispose(); - }); - const preview = new PreviewMainAreaWidget({ - content: editor - }); - preview.title.label = '.gitignore'; - preview.id = id; - preview.title.icon = gitIcon; - preview.title.closable = true; - shell.add(preview); - } + await showGitignoreHiddenFile(error); } } } @@ -1517,7 +1522,14 @@ 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) { + console.log(error); + if (error?.name === 'hiddenFile') { + await showGitignoreHiddenFile(error); + } + } } } } diff --git a/src/model.ts b/src/model.ts index 6143716a8..573e8c80d 100644 --- a/src/model.ts +++ b/src/model.ts @@ -924,6 +924,7 @@ export class GitExtension implements IGitExtension { * @throws {Git.NotInRepository} If the current path is not a Git repository * @throws {Git.GitResponseError} If the server response is not ok * @throws {ServerConnection.NetworkError} If the request cannot be made + * @throws {Git.HiddenFile} If hidden files are not enabled */ async ignore(filePath: string, useExtension: boolean): Promise { const path = await this._getPathRepository(); From eb6abf2c741930e7a089c558bf5b37bc791c515f Mon Sep 17 00:00:00 2001 From: Kentaro Lim Date: Mon, 23 Oct 2023 10:20:41 -0700 Subject: [PATCH 07/15] Add ability to save .gitignore --- jupyterlab_git/git.py | 33 ++++++++++++++++++++++++++++++++- jupyterlab_git/handlers.py | 7 +++++-- src/commandsAndMenu.tsx | 32 ++++++++++++++++++++++++++++---- src/model.ts | 14 ++++++++++++++ style/diff-common.css | 14 ++++++++++++++ 5 files changed, 93 insertions(+), 7 deletions(-) diff --git a/jupyterlab_git/git.py b/jupyterlab_git/git.py index 497632e55..04a721d6e 100644 --- a/jupyterlab_git/git.py +++ b/jupyterlab_git/git.py @@ -1682,11 +1682,16 @@ async def remote_remove(self, path, name): return response async 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) if file.stat().st_size > 0: content = file.read_text() - print(content) return content return "" except BaseException as error: @@ -1732,6 +1737,32 @@ async def ignore(self, path, file_path): return {"code": -1, "message": str(error)} return {"code": 0} + async def writeGitignore(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" + with gitignore.open("a") as f: + f.truncate(0) + f.seek(0) + f.write(content) + if content[-1] != "\n": + f.write("\n") + except BaseException as error: + return {"code": -1, "message": str(error)} + return {"code": 0} + async def version(self): """Return the Git command version. diff --git a/jupyterlab_git/handlers.py b/jupyterlab_git/handlers.py index 3fb390473..9e4f5cc28 100644 --- a/jupyterlab_git/handlers.py +++ b/jupyterlab_git/handlers.py @@ -818,8 +818,11 @@ 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.writeGitignore(local_path, content) + elif file_path: if use_extension: suffixes = Path(file_path).suffixes if len(suffixes) > 0: @@ -827,7 +830,7 @@ async def post(self, path: str = ""): body = await self.git.ignore(local_path, file_path) else: body = await self.git.ensure_gitignore(local_path) - if not self.serverapp.contents_manager.allow_hidden: + if not self.serverapp.contents_manager.allow_hidden and not content: self.set_status(403, "hidden files cannot be accessed") body["content"] = await self.git.read_file(local_path + "/.gitignore") diff --git a/src/commandsAndMenu.tsx b/src/commandsAndMenu.tsx index 25c1e33ab..73094a9d7 100644 --- a/src/commandsAndMenu.tsx +++ b/src/commandsAndMenu.tsx @@ -15,7 +15,7 @@ import { Contents, ContentsManager } from '@jupyterlab/services'; import { ISettingRegistry } from '@jupyterlab/settingregistry'; import { ITerminal } from '@jupyterlab/terminal'; import { ITranslator, TranslationBundle } from '@jupyterlab/translation'; -import { closeIcon, ContextMenuSvg } from '@jupyterlab/ui-components'; +import { closeIcon, ContextMenuSvg, saveIcon } from '@jupyterlab/ui-components'; import { ArrayExt, find, toArray } from '@lumino/algorithm'; import { CommandRegistry } from '@lumino/commands'; import { PromiseDelegate } from '@lumino/coreutils'; @@ -346,9 +346,9 @@ export function addCommands( model.sharedModel.setSource(error.content ? error.content : ''); const editor = new CodeEditorWrapper({ factory: editorServices.factoryService.newDocumentEditor, - model: model, - config: { readOnly: true } + model: model }); + const modelChangedSignal = model.sharedModel.changed; editor.disposed.connect(() => { model.dispose(); }); @@ -359,7 +359,32 @@ export function addCommands( preview.id = id; preview.title.icon = gitIcon; preview.title.closable = true; + 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'; + } + }); } } @@ -1525,7 +1550,6 @@ export function addCommands( try { await gitModel.ignore(file.to, false); } catch (error: any) { - console.log(error); if (error?.name === 'hiddenFile') { await showGitignoreHiddenFile(error); } diff --git a/src/model.ts b/src/model.ts index 573e8c80d..1eed96b46 100644 --- a/src/model.ts +++ b/src/model.ts @@ -870,6 +870,20 @@ export class GitExtension implements IGitExtension { await this.refreshStatus(); } + /** + * Overwrites content onto .gitignore file + * + * @throws {Git.NotInRepository} If the current path is not a Git repository + * @throws {Git.GitResponseError} If the server response is not ok + * @throws {ServerConnection.NetworkError} If the request cannot be made + */ + async writeGitIgnore(content: string): Promise { + const path = await this._getPathRepository(); + + await requestAPI(URLExt.join(path, 'ignore'), 'POST', { content: content }); + await this.refreshStatus(); + } + /** * Fetch to get ahead/behind status * diff --git a/style/diff-common.css b/style/diff-common.css index 57fd4d16e..1c4fb77d1 100644 --- a/style/diff-common.css +++ b/style/diff-common.css @@ -180,3 +180,17 @@ button.jp-git-diff-resolve .jp-ToolbarButtonComponent-label { var(--jp-border-color0) 12px ); } + +.not-saved + > .lm-TabBar-tabCloseIcon + > :not(:hover) + > .jp-icon-busy[fill] { + fill: var(--jp-inverse-layout-color3); +} + +.not-saved + > .lm-TabBar-tabCloseIcon + > :not(:hover) + > .jp-icon3[fill] { + fill: none; +} \ No newline at end of file From 8eb7021b9fa031fa42c6c9a3e6cc5a0dfbd98b73 Mon Sep 17 00:00:00 2001 From: Kentaro Lim Date: Mon, 23 Oct 2023 10:44:01 -0700 Subject: [PATCH 08/15] Fix bug where you can open two .gitignore files --- src/commandsAndMenu.tsx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/commandsAndMenu.tsx b/src/commandsAndMenu.tsx index 73094a9d7..22c1982f9 100644 --- a/src/commandsAndMenu.tsx +++ b/src/commandsAndMenu.tsx @@ -343,6 +343,16 @@ export function addCommands( if (result.button.accept) { const model = new CodeEditor.Model({}); const id = 'git-ignore'; + // + const gitIgnoreWidget = find(shell.widgets(), shellWidget => { + if (shellWidget.id === id) { + return true; + } + }); + if (gitIgnoreWidget) { + shell.activateById(id); + return; + } model.sharedModel.setSource(error.content ? error.content : ''); const editor = new CodeEditorWrapper({ factory: editorServices.factoryService.newDocumentEditor, From 6eac31d4920f9f2c3c88fa0b25ffc9c39b51e8ee Mon Sep 17 00:00:00 2001 From: Kentaro Lim Date: Tue, 24 Oct 2023 16:32:36 -0700 Subject: [PATCH 09/15] Add option to hide hidden file warning --- src/commandsAndMenu.tsx | 131 ++++++++++++++++++++++------------------ 1 file changed, 72 insertions(+), 59 deletions(-) diff --git a/src/commandsAndMenu.tsx b/src/commandsAndMenu.tsx index 22c1982f9..9aac1dd25 100644 --- a/src/commandsAndMenu.tsx +++ b/src/commandsAndMenu.tsx @@ -306,14 +306,71 @@ export function addCommands( } }); + async function showGitignore(error: any) { + const model = new CodeEditor.Model({}); + const id = 'git-ignore'; + // + const gitIgnoreWidget = find(shell.widgets(), shellWidget => { + if (shellWidget.id === id) { + return true; + } + }); + if (gitIgnoreWidget) { + shell.activateById(id); + return; + } + model.sharedModel.setSource(error.content ? error.content : ''); + const editor = new CodeEditorWrapper({ + factory: editorServices.factoryService.newDocumentEditor, + model: model + }); + const modelChangedSignal = model.sharedModel.changed; + editor.disposed.connect(() => { + model.dispose(); + }); + const preview = new PreviewMainAreaWidget({ + content: editor + }); + preview.title.label = '.gitignore'; + preview.id = id; + preview.title.icon = gitIcon; + preview.title.closable = true; + 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) { const result = await showDialog({ - title: trans.__('The .gitignore file cannot be accessed'), + title: trans.__('Warning: The .gitignore file is a hidden file.'), body: (
{trans.__( - 'Hidden files by default cannot be accessed. In order to open the .gitignore file you must:' + 'Hidden files by default cannot be accessed with the regular code editor. In order to open the .gitignore file you must:' )}
  1. @@ -337,64 +394,16 @@ export function addCommands( ), buttons: [ Dialog.cancelButton({ label: trans.__('Cancel') }), - Dialog.okButton({ label: trans.__('Show .gitignore file') }) - ] + Dialog.okButton({ label: trans.__('Show .gitignore file anyways') }) + ], + checkbox: { + label: 'Do not show this warning again', + checked: false + } }); if (result.button.accept) { - const model = new CodeEditor.Model({}); - const id = 'git-ignore'; - // - const gitIgnoreWidget = find(shell.widgets(), shellWidget => { - if (shellWidget.id === id) { - return true; - } - }); - if (gitIgnoreWidget) { - shell.activateById(id); - return; - } - model.sharedModel.setSource(error.content ? error.content : ''); - const editor = new CodeEditorWrapper({ - factory: editorServices.factoryService.newDocumentEditor, - model: model - }); - const modelChangedSignal = model.sharedModel.changed; - editor.disposed.connect(() => { - model.dispose(); - }); - const preview = new PreviewMainAreaWidget({ - content: editor - }); - preview.title.label = '.gitignore'; - preview.id = id; - preview.title.icon = gitIcon; - preview.title.closable = true; - 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'; - } - }); + settings.set('hideHiddenFileWarning', result.isChecked); + showGitignore(error); } } @@ -408,7 +417,11 @@ export function addCommands( await gitModel.ensureGitignore(); } catch (error: any) { if (error?.name === 'hiddenFile') { - await showGitignoreHiddenFile(error); + if (settings.composite['hideHiddenFileWarning']) { + await showGitignore(error); + } else { + await showGitignoreHiddenFile(error); + } } } } From a09953de6d15ff9226ee55ec3b4d71874544902f Mon Sep 17 00:00:00 2001 From: Kentaro Lim Date: Mon, 30 Oct 2023 10:10:46 -0700 Subject: [PATCH 10/15] Fix PR requests for gitignore bug --- jupyterlab_git/git.py | 21 ++++++++------------ jupyterlab_git/handlers.py | 17 +++++++++++----- src/commandsAndMenu.tsx | 31 ++++++++++++++++++----------- src/git.ts | 3 --- src/model.ts | 40 +++++++++++++++++++++++++++++++++++++- src/tokens.ts | 4 +--- 6 files changed, 80 insertions(+), 36 deletions(-) diff --git a/jupyterlab_git/git.py b/jupyterlab_git/git.py index 04a721d6e..e3a7d14c0 100644 --- a/jupyterlab_git/git.py +++ b/jupyterlab_git/git.py @@ -1681,7 +1681,7 @@ async def remote_remove(self, path, name): return response - async def read_file(self, path): + def read_file(self, path): """ Reads file content located at path and returns it as a string @@ -1690,12 +1690,10 @@ async def read_file(self, path): """ try: file = pathlib.Path(path) - if file.stat().st_size > 0: - content = file.read_text() - return content - return "" + content = file.read_text() + return {"code": 0, "content": content} except BaseException as error: - return "" + return {"code": -1, "content": ""} async def ensure_gitignore(self, path): """Handle call to ensure .gitignore file exists and the @@ -1737,7 +1735,7 @@ async def ignore(self, path, file_path): return {"code": -1, "message": str(error)} return {"code": 0} - async def writeGitignore(self, path, content): + async def write_gitignore(self, path, content): """ Handle call to overwrite .gitignore. Takes the .gitignore file and clears its previous contents @@ -1753,12 +1751,9 @@ async def writeGitignore(self, path, content): if res["code"] != 0: return res gitignore = pathlib.Path(path) / ".gitignore" - with gitignore.open("a") as f: - f.truncate(0) - f.seek(0) - f.write(content) - if content[-1] != "\n": - f.write("\n") + 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} diff --git a/jupyterlab_git/handlers.py b/jupyterlab_git/handlers.py index 9e4f5cc28..fc0835b23 100644 --- a/jupyterlab_git/handlers.py +++ b/jupyterlab_git/handlers.py @@ -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 = ""): """ @@ -821,7 +832,7 @@ async def post(self, path: str = ""): content = data.get("content", None) use_extension = data.get("use_extension", False) if content: - body = await self.git.writeGitignore(local_path, content) + body = await self.git.write_gitignore(local_path, content) elif file_path: if use_extension: suffixes = Path(file_path).suffixes @@ -830,10 +841,6 @@ async def post(self, path: str = ""): body = await self.git.ignore(local_path, file_path) else: body = await self.git.ensure_gitignore(local_path) - if not self.serverapp.contents_manager.allow_hidden and not content: - self.set_status(403, "hidden files cannot be accessed") - body["content"] = await self.git.read_file(local_path + "/.gitignore") - if body["code"] != 0: self.set_status(500) self.finish(json.dumps(body)) diff --git a/src/commandsAndMenu.tsx b/src/commandsAndMenu.tsx index 9aac1dd25..49f6af4c7 100644 --- a/src/commandsAndMenu.tsx +++ b/src/commandsAndMenu.tsx @@ -308,8 +308,10 @@ export function addCommands( async function showGitignore(error: any) { const model = new CodeEditor.Model({}); - const id = 'git-ignore'; - // + const repoPath = gitModel.getRelativeFilePath(); + const id = repoPath + '/.git-ignore'; + const contentData = await gitModel.readGitIgnore(); + const gitIgnoreWidget = find(shell.widgets(), shellWidget => { if (shellWidget.id === id) { return true; @@ -319,7 +321,7 @@ export function addCommands( shell.activateById(id); return; } - model.sharedModel.setSource(error.content ? error.content : ''); + model.sharedModel.setSource(contentData.content ? contentData.content : ''); const editor = new CodeEditorWrapper({ factory: editorServices.factoryService.newDocumentEditor, model: model @@ -328,13 +330,15 @@ export function addCommands( editor.disposed.connect(() => { model.dispose(); }); - const preview = new PreviewMainAreaWidget({ + 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 () => { @@ -364,7 +368,10 @@ export function addCommands( } /* Helper: Show gitignore hidden file */ - async function showGitignoreHiddenFile(error: any) { + 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: ( @@ -417,11 +424,10 @@ export function addCommands( await gitModel.ensureGitignore(); } catch (error: any) { if (error?.name === 'hiddenFile') { - if (settings.composite['hideHiddenFileWarning']) { - await showGitignore(error); - } else { - await showGitignoreHiddenFile(error); - } + await showGitignoreHiddenFile( + error, + settings.composite['hideHiddenFileWarning'] as boolean + ); } } } @@ -1574,7 +1580,10 @@ export function addCommands( await gitModel.ignore(file.to, false); } catch (error: any) { if (error?.name === 'hiddenFile') { - await showGitignoreHiddenFile(error); + await showGitignoreHiddenFile( + error, + settings.composite['hideHiddenFileWarning'] as boolean + ); } } } diff --git a/src/git.ts b/src/git.ts index f18093af9..f54fac670 100644 --- a/src/git.ts +++ b/src/git.ts @@ -65,9 +65,6 @@ export async function requestAPI( if (!response.ok) { if (isJSON) { const { message, traceback, ...json } = data; - if (response.status === 403) { - throw new Git.HiddenFile(data.content); - } throw new Git.GitResponseError( response, message || diff --git a/src/model.ts b/src/model.ts index 1eed96b46..a25ce7b2a 100644 --- a/src/model.ts +++ b/src/model.ts @@ -9,6 +9,7 @@ import { AUTH_ERROR_MESSAGES, requestAPI } from './git'; import { TaskHandler } from './taskhandler'; import { Git, IGitExtension } from './tokens'; import { decodeStage } from './utils'; +import { ServerConnection } from '@jupyterlab/services'; // Default refresh interval (in milliseconds) for polling the current Git status (NOTE: this value should be the same value as in the plugin settings schema): const DEFAULT_REFRESH_INTERVAL = 3000; // ms @@ -866,10 +867,38 @@ export class GitExtension implements IGitExtension { const path = await this._getPathRepository(); await requestAPI(URLExt.join(path, 'ignore'), 'POST', {}); + try { + await this._docmanager.services.contents.get(`${path}/.gitignore`, { + content: false + }); + } catch (e) { + // If the previous request failed with a 404 error, it means hidden file cannot be accessed + if ((e as ServerConnection.ResponseError).response?.status === 404) { + throw new Git.HiddenFile(); + } + } this._openGitignore(); await this.refreshStatus(); } + /** + * Reads content of .gitignore file + * + * @throws {Git.NotInRepository} If the current path is not a Git repository + * @throws {Git.GitResponseError} If the server response is not ok + * @throws {ServerConnection.NetworkError} If the request cannot be made + */ + async readGitIgnore(): Promise<{ code: number; content: string }> { + const path = await this._getPathRepository(); + + const res = (await requestAPI(URLExt.join(path, 'ignore'), 'GET')) as { + code: number; + content: string; + }; + await this.refreshStatus(); + return res; + } + /** * Overwrites content onto .gitignore file * @@ -947,7 +976,16 @@ export class GitExtension implements IGitExtension { file_path: filePath, use_extension: useExtension }); - + try { + await this._docmanager.services.contents.get(`${path}/.gitignore`, { + content: false + }); + } catch (e) { + // If the previous request failed with a 404 error, it means hidden file cannot be accessed + if ((e as ServerConnection.ResponseError).response?.status === 404) { + throw new Git.HiddenFile(); + } + } this._openGitignore(); await this.refreshStatus(); } diff --git a/src/tokens.ts b/src/tokens.ts index 73a5918d4..325626545 100644 --- a/src/tokens.ts +++ b/src/tokens.ts @@ -1301,12 +1301,10 @@ export namespace Git { } export class HiddenFile extends Error { - content: string; - constructor(content: string) { + constructor() { super('File is hidden'); this.name = 'hiddenFile'; this.message = 'File is hidden and cannot be accessed.'; - this.content = content; } } From 8a51328330a9feba485f7a80c122237daeb5dd43 Mon Sep 17 00:00:00 2001 From: Kentaro Lim Date: Mon, 30 Oct 2023 10:35:43 -0700 Subject: [PATCH 11/15] Fix prettier styles for gitignore --- style/diff-common.css | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/style/diff-common.css b/style/diff-common.css index 1c4fb77d1..d983c7b94 100644 --- a/style/diff-common.css +++ b/style/diff-common.css @@ -181,16 +181,10 @@ button.jp-git-diff-resolve .jp-ToolbarButtonComponent-label { ); } -.not-saved - > .lm-TabBar-tabCloseIcon - > :not(:hover) - > .jp-icon-busy[fill] { +.not-saved > .lm-TabBar-tabCloseIcon > :not(:hover) > .jp-icon-busy[fill] { fill: var(--jp-inverse-layout-color3); } -.not-saved - > .lm-TabBar-tabCloseIcon - > :not(:hover) - > .jp-icon3[fill] { +.not-saved > .lm-TabBar-tabCloseIcon > :not(:hover) > .jp-icon3[fill] { fill: none; -} \ No newline at end of file +} From e6c1e9b774040ea153934926d60d70b21d6f5b44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Collonval?= Date: Wed, 8 Nov 2023 16:51:53 +0100 Subject: [PATCH 12/15] Improve translation --- src/commandsAndMenu.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/commandsAndMenu.tsx b/src/commandsAndMenu.tsx index 49f6af4c7..44cc1e6b2 100644 --- a/src/commandsAndMenu.tsx +++ b/src/commandsAndMenu.tsx @@ -385,7 +385,7 @@ export function addCommands( '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.' )}
    - {trans.__('jupyter server --generate-config')} + {'jupyter server --generate-config'}
  2. @@ -393,7 +393,7 @@ export function addCommands( 'Open jupyter_server_config.py, uncomment out the following line and set it to True:' )}
    - {trans.__('c.ContentsManager.allow_hidden = False')} + {'c.ContentsManager.allow_hidden = False'}
@@ -404,7 +404,7 @@ export function addCommands( Dialog.okButton({ label: trans.__('Show .gitignore file anyways') }) ], checkbox: { - label: 'Do not show this warning again', + label: trans.__('Do not show this warning again'), checked: false } }); From 7c73b8ccaf02db8c2b37129cd132e138e6d9542c Mon Sep 17 00:00:00 2001 From: Kentaro Lim Date: Thu, 9 Nov 2023 14:28:02 -0800 Subject: [PATCH 13/15] Improve gitignore model and add hiddenFile option to schema --- schema/plugin.json | 6 ++++++ src/model.ts | 14 +++++++------- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/schema/plugin.json b/schema/plugin.json index 3b40fda7b..0d3809af2 100644 --- a/schema/plugin.json +++ b/schema/plugin.json @@ -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": [ diff --git a/src/model.ts b/src/model.ts index a25ce7b2a..b66fb6e30 100644 --- a/src/model.ts +++ b/src/model.ts @@ -888,15 +888,15 @@ export class GitExtension implements IGitExtension { * @throws {Git.GitResponseError} If the server response is not ok * @throws {ServerConnection.NetworkError} If the request cannot be made */ - async readGitIgnore(): Promise<{ code: number; content: string }> { + async readGitIgnore(): Promise { const path = await this._getPathRepository(); - const res = (await requestAPI(URLExt.join(path, 'ignore'), 'GET')) as { - code: number; - content: string; - }; - await this.refreshStatus(); - return res; + return ( + (await requestAPI(URLExt.join(path, 'ignore'), 'GET')) as { + code: number; + content: string; + } + ).content; } /** From 14950b7d4968e0c500e71c68c32c80b98ccb442b Mon Sep 17 00:00:00 2001 From: Kentaro Lim Date: Thu, 9 Nov 2023 15:10:50 -0800 Subject: [PATCH 14/15] Fix eslint --- schema/plugin.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/schema/plugin.json b/schema/plugin.json index 0d3809af2..bae4df511 100644 --- a/schema/plugin.json +++ b/schema/plugin.json @@ -79,7 +79,7 @@ }, "hideHiddenFileWarning": { "type": "boolean", - "title":"Hide hidden file warning", + "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 } From 78fe0c5ff149f72aa385af13df0103b747cd7071 Mon Sep 17 00:00:00 2001 From: Kentaro Lim Date: Fri, 10 Nov 2023 10:23:12 -0800 Subject: [PATCH 15/15] Fix .gitignore content sending --- src/commandsAndMenu.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commandsAndMenu.tsx b/src/commandsAndMenu.tsx index 44cc1e6b2..493015d1b 100644 --- a/src/commandsAndMenu.tsx +++ b/src/commandsAndMenu.tsx @@ -321,7 +321,7 @@ export function addCommands( shell.activateById(id); return; } - model.sharedModel.setSource(contentData.content ? contentData.content : ''); + model.sharedModel.setSource(contentData ? contentData : ''); const editor = new CodeEditorWrapper({ factory: editorServices.factoryService.newDocumentEditor, model: model