diff --git a/jupyterlab_git/git.py b/jupyterlab_git/git.py index 63d9f70c3..91c2b2a12 100644 --- a/jupyterlab_git/git.py +++ b/jupyterlab_git/git.py @@ -1684,6 +1684,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 @@ -1724,6 +1738,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. diff --git a/jupyterlab_git/handlers.py b/jupyterlab_git/handlers.py index 35098bfec..f68238378 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 = ""): """ @@ -818,8 +829,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.write_gitignore(local_path, content) + elif file_path: if use_extension: suffixes = Path(file_path).suffixes if len(suffixes) > 0: @@ -827,7 +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 body["code"] != 0: self.set_status(500) self.finish(json.dumps(body)) diff --git a/schema/plugin.json b/schema/plugin.json index 3b40fda7b..bae4df511 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/commandsAndMenu.tsx b/src/commandsAndMenu.tsx index a7ef3e778..0591a81c2 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'; @@ -54,6 +54,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 { editorServices } from '@jupyterlab/codemirror'; export interface IGitCloneArgs { /** @@ -308,13 +311,130 @@ 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 => { + if (shellWidget.id === id) { + return true; + } + }); + if (gitIgnoreWidget) { + shell.activateById(id); + return; + } + model.sharedModel.setSource(contentData ? contentData : ''); + const editor = new CodeEditorWrapper({ + factory: editorServices.factoryService.newDocumentEditor, + 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: ( +