diff --git a/jupyterlab_git/git.py b/jupyterlab_git/git.py index 78636a40..0b1283df 100644 --- a/jupyterlab_git/git.py +++ b/jupyterlab_git/git.py @@ -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 @@ -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. diff --git a/jupyterlab_git/handlers.py b/jupyterlab_git/handlers.py index afac6aa5..ef316070 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 da4b654a..c0dba936 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 dbf0c58a..4e83a469 100644 --- a/src/commandsAndMenu.tsx +++ b/src/commandsAndMenu.tsx @@ -8,7 +8,12 @@ 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'; @@ -16,12 +21,13 @@ 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'; @@ -29,14 +35,15 @@ 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, @@ -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 { /** @@ -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, @@ -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: ( +