diff --git a/editor/js/main.js b/editor/js/main.js index e9eea4b58..09750b85e 100644 --- a/editor/js/main.js +++ b/editor/js/main.js @@ -97,17 +97,50 @@ } if (msg.text) { var text = RED._(msg.text,{default:msg.text}); + var options = { + type: msg.type, + fixed: msg.timeout === undefined, + timeout: msg.timeout + } if (notificationId === "runtime-state") { if (msg.error === "credentials_load_failed") { - // TODO: NLS - text += '

'+'Setup credentials'+'

'; + options.buttons = [ + { + text: "Setup credentials", + click: function() { + RED.projects.showCredentialsPrompt(); + } + } + ] } else if (msg.error === "missing_flow_file") { - // TODO: NLS - text += '

'+'Setup project files'+'

'; + options.buttons = [ + { + text: "Setup project files", + click: function() { + persistentNotifications[notificationId].close(); + RED.projects.showFilesPrompt(); + } + } + ] + } else if (msg.error === "project_empty") { + options.buttons = [ + { + text: "No thanks", + click: function() { + persistentNotifications[notificationId].close(); + } + }, { + text: "Create default project files", + click: function() { + persistentNotifications[notificationId].close(); + RED.projects.createDefaultFileSet(); + } + } + ] } } if (!persistentNotifications.hasOwnProperty(notificationId)) { - persistentNotifications[notificationId] = RED.notify(text,msg.type,msg.timeout === undefined,msg.timeout); + persistentNotifications[notificationId] = RED.notify(text,options); } else { persistentNotifications[notificationId].update(text,msg.timeout); } diff --git a/editor/js/ui/notifications.js b/editor/js/ui/notifications.js index 995ce1ce5..1e2a50d53 100644 --- a/editor/js/ui/notifications.js +++ b/editor/js/ui/notifications.js @@ -69,6 +69,10 @@ RED.notify = (function() { n.close = (function() { var nn = n; return function() { + if (nn.closed) { + return; + } + nn.closed = true; currentNotifications.splice(currentNotifications.indexOf(nn),1); $(nn).slideUp(300, function() { nn.parentNode.removeChild(nn); diff --git a/editor/js/ui/projects.js b/editor/js/ui/projects.js index 04571fa55..a535507c1 100644 --- a/editor/js/ui/projects.js +++ b/editor/js/ui/projects.js @@ -83,7 +83,7 @@ RED.projects = (function() { migrateProjectHeader.appendTo(container); var body = $('
').appendTo(container); - $('

').text("Step 1 : Setup your version control client").appendTo(body); + $('

').text("Setup your version control client").appendTo(body); $('

').text("Node-RED uses the open source tool Git for version control. It tracks changes to your project files and lets you push them to remote repositories.").appendTo(body); $('

').text("When you commit a set of changes, Git records who made the changes with a username and email address. The Username can be anything you want - it does not need to be your real name.").appendTo(body); $('

').text("If your Git client is already configured, you can skip this step.").appendTo(body); @@ -139,25 +139,24 @@ RED.projects = (function() { migrateProjectHeader.appendTo(container); var body = $('

').appendTo(container); - $('

').text("Step 2 : Create your project").appendTo(body); - $('

').text("A project contains your flow files, a README file, a package.json file and a settings file. It makes it much easier to share your flows with others and to collaborate on them.").appendTo(body); + $('

').text("Create your project").appendTo(body); + $('

').text("A project is maintained as a Git repository. It makes it much easier to share your flows with others and to collaborate on them.").appendTo(body); $('

').text("You can create multiple projects and quickly switch between them from the editor.").appendTo(body); $('

').text("To begin, your project needs a name and an optional description.").appendTo(body); var validateForm = function() { var projectName = projectNameInput.val(); var valid = true; + var projectNameValid = /^[a-zA-Z0-9\-_]+$/.test(projectName); if (projectNameInputChanged) { projectNameStatus.empty(); - if (!/^[a-zA-Z0-9\-_]+$/.test(projectName)) { + if (!projectNameValid) { projectNameInput.addClass("input-error"); $('').appendTo(projectNameStatus); - projectNameValid = false; valid = false; } else { projectNameInput.removeClass("input-error"); $('').appendTo(projectNameStatus); - projectNameValid = true; } projectNameLastChecked = projectName; } @@ -206,6 +205,7 @@ RED.projects = (function() { setTimeout(function() { projectNameInput.focus(); + validateForm(); },50); return container; }, @@ -224,14 +224,112 @@ RED.projects = (function() { click: function() { createProjectOptions.name = projectNameInput.val(); createProjectOptions.summary = projectSummaryInput.val(); - createProjectOptions.files = { migrateFiles: true }; - show('encryption-config'); + show('default-files'); } } ] }; })(), + 'default-files': (function() { + var projectFlowFileInput; + var projectCredentialFileInput; + return { + content: function(options) { + var container = $('

'); + migrateProjectHeader.appendTo(container); + var body = $('
').appendTo(container); + $('

').text("Create your project files").appendTo(body); + $('

').text("A project contains your flow files, a README file, a package.json file and a settings file.").appendTo(body); + $('

').text("It can contain any other files you want to maintain in the Git repository.").appendTo(body); + if (!options.existingProject) { + $('

').text("Your existing flow and credential files will be copied into the project.").appendTo(body); + } + + var validateForm = function() { + var valid = true; + var flowFile = projectFlowFileInput.val(); + if (flowFile === "" || !/\.json$/.test(flowFile)) { + valid = false; + if (!projectFlowFileInput.hasClass("input-error")) { + projectFlowFileInput.addClass("input-error"); + projectFlowFileInput.next().empty().append(''); + } + projectCredentialFileInput.text(""); + if (!projectCredentialFileInput.hasClass("input-error")) { + projectCredentialFileInput.addClass("input-error"); + projectCredentialFileInput.next().empty().append(''); + } + } else { + if (projectFlowFileInput.hasClass("input-error")) { + projectFlowFileInput.removeClass("input-error"); + projectFlowFileInput.next().empty(); + } + if (projectCredentialFileInput.hasClass("input-error")) { + projectCredentialFileInput.removeClass("input-error"); + projectCredentialFileInput.next().empty(); + } + projectCredentialFileInput.text(flowFile.substring(0,flowFile.length-5)+"_cred.json"); + } + $("#projects-dialog-create-default-files").prop('disabled',!valid).toggleClass('disabled ui-button-disabled ui-state-disabled',!valid); + } + var row = $('

').appendTo(body); + $('').appendTo(row); + var subrow = $('
').appendTo(row); + var defaultFlowFile = (createProjectOptions.files &&createProjectOptions.files.flow) || RED.settings.files.flow||"flow.json"; + projectFlowFileInput = $('').val(defaultFlowFile) + .on("change keyup paste",validateForm) + .appendTo(subrow); + $('
').appendTo(subrow); + $('').appendTo(row); + + var defaultCredentialsFile = (createProjectOptions.files &&createProjectOptions.files.credentials) || RED.settings.files.credentials||"flow_cred.json"; + row = $('
').appendTo(body); + $('').appendTo(row); + subrow = $('
').appendTo(row); + projectCredentialFileInput = $('
').text(defaultCredentialsFile) + .appendTo(subrow); + $('
').appendTo(subrow); + + setTimeout(function() { + projectFlowFileInput.focus(); + validateForm(); + },50); + + return container; + }, + buttons: function(options) { + return [ + { + // id: "clipboard-dialog-cancel", + text: options.existingProject?"Cancel":"Back", + click: function() { + if (options.existingProject) { + $(this).dialog('close'); + } else { + show('project-details',options); + } + } + }, + { + id: "projects-dialog-create-default-files", + text: "Next", // TODO: nls + class: "primary", + click: function() { + createProjectOptions.files = { + flow: projectFlowFileInput.val(), + credentials: projectCredentialFileInput.text() + } + if (!options.existingProject) { + createProjectOptions.migrateFiles = true; + } + show('encryption-config',options); + } + } + ] + } + } + })(), 'encryption-config': (function() { var emptyProjectCredentialInput; return { @@ -241,20 +339,23 @@ RED.projects = (function() { migrateProjectHeader.appendTo(container); var body = $('
').appendTo(container); - $('

').text("Step 3 : Setup encryption of your credentials file").appendTo(body); - // $('

').text("Your flow credentials file can be encrypted to keep its contents secure.").appendTo(body); - - if (RED.settings.flowEncryptionType === 'disabled') { - $('

').text("Your flow credentials file is not currently encrypted.").appendTo(body); - $('

').text("That means its contents, such as passwords and access tokens, can be read by anyone with access to the file.").appendTo(body); + $('

').text("Setup encryption of your credentials file").appendTo(body); + if (options.existingProject) { + $('

').text("Your flow credentials file can be encrypted to keep its contents secure.").appendTo(body); $('

').text("If you want to store these credentials in a public Git repository, you must encrypt them by providing a secret key phrase.").appendTo(body); } else { - if (RED.settings.flowEncryptionType === 'user') { - $('

').text("Your flow credentials file is currently encrypted using the credentialSecret property from your settings file as the key.").appendTo(body); - } else if (RED.settings.flowEncryptionType === 'system') { - $('

').text("Your flow credentials file is currently encrypted using a system-generated secret as the key. You should provide a new secret key for this project.").appendTo(body); + if (RED.settings.flowEncryptionType === 'disabled') { + $('

').text("Your flow credentials file is not currently encrypted.").appendTo(body); + $('

').text("That means its contents, such as passwords and access tokens, can be read by anyone with access to the file.").appendTo(body); + $('

').text("If you want to store these credentials in a public Git repository, you must encrypt them by providing a secret key phrase.").appendTo(body); + } else { + if (RED.settings.flowEncryptionType === 'user') { + $('

').text("Your flow credentials file is currently encrypted using the credentialSecret property from your settings file as the key.").appendTo(body); + } else if (RED.settings.flowEncryptionType === 'system') { + $('

').text("Your flow credentials file is currently encrypted using a system-generated secret as the key. You should provide a new secret key for this project.").appendTo(body); + } + $('

').text("The secret will be copied into the settings for your new project. You can then manage the secret within the editor.").appendTo(body); } - $('

').text("The secret will be copied into the settings for your new project. You can then manage the secret within the editor.").appendTo(body); } // var row = $('

').appendTo(body); @@ -356,75 +457,89 @@ RED.projects = (function() { return container; }, - buttons: [ - { - // id: "clipboard-dialog-cancel", - text: "Back", - click: function() { - show('project-details'); - } - }, - { - id: "projects-dialog-create-encryption", - text: "Create project", // TODO: nls - class: "primary disabled", - disabled: true, - click: function() { - var encryptionState = $("input[name=projects-encryption-type]:checked").val(); - if (encryptionState === 'enabled') { - var encryptionKeyType = $("input[name=projects-encryption-key]:checked").val(); - if (encryptionKeyType === 'custom') { - createProjectOptions.credentialSecret = emptyProjectCredentialInput.val(); - } else { - // If 'use existing', leave createProjectOptions.credentialSecret blank - // - that will trigger it to use the existing key - // TODO: this option should be disabled if encryption is disabled - } - } else { - // Disabled encryption by explicitly setting credSec to false - createProjectOptions.credentialSecret = false; + buttons: function(options) { + return [ + { + // id: "clipboard-dialog-cancel", + text: "Back", + click: function() { + show('default-files',options); } + }, + { + id: "projects-dialog-create-encryption", + text: options.existingProject?"Create project files":"Create project", // TODO: nls + class: "primary disabled", + disabled: true, + click: function() { + var encryptionState = $("input[name=projects-encryption-type]:checked").val(); + if (encryptionState === 'enabled') { + var encryptionKeyType = $("input[name=projects-encryption-key]:checked").val(); + if (encryptionKeyType === 'custom') { + createProjectOptions.credentialSecret = emptyProjectCredentialInput.val(); + } else { + // If 'use existing', leave createProjectOptions.credentialSecret blank + // - that will trigger it to use the existing key + // TODO: this option should be disabled if encryption is disabled + } + } else { + // Disabled encryption by explicitly setting credSec to false + createProjectOptions.credentialSecret = false; + } + RED.deploy.setDeployInflight(true); + RED.projects.settings.switchProject(createProjectOptions.name); - RED.deploy.setDeployInflight(true); - RED.projects.settings.switchProject(createProjectOptions.name); + var method = "POST"; + var url = "projects"; - sendRequest({ - url: "projects", - type: "POST", - requireCleanWorkspace: true, - handleAuthFail: false, - responses: { - 200: function(data) { - createProjectOptions = {}; - show('create-success'); - }, - 400: { - 'project_exists': function(error) { - console.log("already exists"); + if (options.existingProject) { + createProjectOptions.initialise = true; + method = "PUT"; + url = "projects/"+activeProject.name; + } + var self = this; + sendRequest({ + url: url, + type: method, + requireCleanWorkspace: true, + handleAuthFail: false, + responses: { + 200: function(data) { + createProjectOptions = {}; + if (options.existingProject) { + $( self ).dialog( "close" ); + } else { + show('create-success'); + } }, - 'git_error': function(error) { - console.log("git error",error); - }, - 'git_connection_failed': function(error) { - projectRepoInput.addClass("input-error"); - }, - 'git_auth_failed': function(error) { - projectRepoUserInput.addClass("input-error"); - projectRepoPasswordInput.addClass("input-error"); - // getRepoAuthDetails(req); - console.log("git auth error",error); - }, - 'unexpected_error': function(error) { - console.log("unexpected_error",error) + 400: { + 'project_exists': function(error) { + console.log("already exists"); + }, + 'git_error': function(error) { + console.log("git error",error); + }, + 'git_connection_failed': function(error) { + projectRepoInput.addClass("input-error"); + }, + 'git_auth_failed': function(error) { + projectRepoUserInput.addClass("input-error"); + projectRepoPasswordInput.addClass("input-error"); + // getRepoAuthDetails(req); + console.log("git auth error",error); + }, + 'unexpected_error': function(error) { + console.log("unexpected_error",error) + } } } - } - },createProjectOptions).always(function() { - RED.deploy.setDeployInflight(false); - }) + },createProjectOptions).always(function() { + RED.deploy.setDeployInflight(false); + }) + } } - } - ] + ]; + } } })(), 'create-success': { @@ -437,10 +552,10 @@ RED.projects = (function() { $('

').text("You have successfully created your first project!").appendTo(body); $('

').text("You can now continue to use Node-RED just as you always have.").appendTo(body); $('

').text("The 'info' tab in the sidebar shows you what your current active project is. "+ - "The button next to the name can be used to access the project settings view.").appendTo(body); - $('

').text("The new 'history' tab in the sidebar can be used to view files that have changed "+ - "in your project and to commit them. It shows you a complete history of your commits and "+ - "allows you to push your changes to a remote repository.").appendTo(body); + "The button next to the name can be used to access the project settings view.").appendTo(body); + $('

').text("The 'history' tab in the sidebar can be used to view files that have changed "+ + "in your project and to commit them. It shows you a complete history of your commits and "+ + "allows you to push your changes to a remote repository.").appendTo(body); return container; }, @@ -537,18 +652,15 @@ RED.projects = (function() { } if (/^(?:ssh|[\d\w\.\-_]+@[\w\.]+):(?:\/\/)?/.test(repo)) { $(".projects-dialog-screen-create-row-creds").hide(); - $(".projects-dialog-screen-create-row-passphrase").show(); $(".projects-dialog-screen-create-row-sshkey").show(); // if ( !getSelectedSSHKey(projectRepoSSHKeySelect) ) { // valid = false; // } } else if (/^https?:\/\//.test(repo)) { $(".projects-dialog-screen-create-row-creds").show(); - $(".projects-dialog-screen-create-row-passphrase").hide(); $(".projects-dialog-screen-create-row-sshkey").hide(); } else { $(".projects-dialog-screen-create-row-creds").show(); - $(".projects-dialog-screen-create-row-passphrase").hide(); $(".projects-dialog-screen-create-row-sshkey").hide(); } @@ -757,9 +869,9 @@ RED.projects = (function() { projectRepoPasswordInput = $('').appendTo(subrow); row = $('

').hide().appendTo(container); - - $('').appendTo(row); - projectRepoSSHKeySelect = $("",{style:"width: 100%"}).appendTo(subrow); $.getJSON("settings/user/keys", function(data) { var count = 0; @@ -773,20 +885,9 @@ RED.projects = (function() { }); - row = $('
').hide().appendTo(container); - $('').appendTo(row); - projectRepoPassphrase = $('').appendTo(row); - - // row = $('
').hide().appendTo(container); - // $('').appendTo(row); - // projectRepoRemoteName = $('').val("origin").appendTo(row); - // - // row = $('
').hide().appendTo(container); - // $('').appendTo(row); - // projectRepoBranch = $('').val('master').appendTo(row); - - - + subrow = $('
').appendTo(row); + $('').appendTo(subrow); + projectRepoPassphrase = $('').appendTo(subrow); // // Secret - clone // row = $('
').appendTo(container); @@ -905,6 +1006,10 @@ RED.projects = (function() { projectRepoPassphrase.addClass("input-error"); console.log("git auth error",error); }, + 'project_empty': function(error) { + // This is handled via a runtime notification. + dialog.dialog("close"); + }, 'unexpected_error': function(error) { console.log("unexpected_error",error) } @@ -996,15 +1101,12 @@ RED.projects = (function() { class: "primary disabled", disabled: true, click: function() { + dialog.dialog( "close" ); switchProject(selectedProject.name,function(err,data) { if (err) { - if (err.error === 'credentials_load_failed') { - dialog.dialog( "close" ); - } else { + if (err.error !== 'credentials_load_failed') { console.log("unexpected_error",err) } - } else { - dialog.dialog( "close" ); } }) } @@ -1109,7 +1211,11 @@ RED.projects = (function() { var container = screen.content(options); dialogBody.empty(); - dialog.dialog('option','buttons',screen.buttons); + var buttons = screen.buttons; + if (typeof buttons === 'function') { + buttons = buttons(options); + } + dialog.dialog('option','buttons',buttons); dialogBody.append(container); dialog.dialog('option','title',screen.title||""); dialog.dialog("open"); @@ -1258,7 +1364,7 @@ RED.projects = (function() { count++; }); if (count === 0) { - // projectRepoSSHKeySelect + //TODO: handle no keys yet setup } }); row = $('
').appendTo(message); @@ -1520,6 +1626,38 @@ RED.projects = (function() { // initSidebar(); } + function createDefaultFileSet() { + if (!activeProject) { + throw new Error("Cannot create default file set without an active project"); + } else if (!activeProject.empty) { + throw new Error("Cannot create default file set on a non-empty project"); + } + createProjectOptions = {}; + show('default-files',{existingProject: true}); + // var payload = { + // + // } + // RED.deploy.setDeployInflight(true); + // utils.sendRequest({ + // url: "projects/"+activeProject.name, + // type: "PUT", + // responses: { + // 0: function(error) {}, + // 200: function(data) { + // activeProject = data; + // RED.sidebar.versionControl.refresh(true); + // }, + // 400: { + // 'unexpected_error': function(error) { + // console.log(error); + // } + // }, + // } + // },payload).always(function() { + // RED.deploy.setDeployInflight(false); + // }); + + } function refresh(done) { $.getJSON("projects",function(data) { @@ -1546,6 +1684,7 @@ RED.projects = (function() { return { init: init, + _show: show, showStartup: function() { show('welcome'); }, @@ -1568,6 +1707,7 @@ RED.projects = (function() { showFilesPrompt: function() { //TODO: rename this function RED.projects.settings.show('settings'); }, + createDefaultFileSet: createDefaultFileSet, // showSidebar: showSidebar, refresh: refresh, editProject: function() { diff --git a/editor/js/ui/tab-versionControl.js b/editor/js/ui/tab-versionControl.js index 8c03e0004..89c577677 100644 --- a/editor/js/ui/tab-versionControl.js +++ b/editor/js/ui/tab-versionControl.js @@ -1191,7 +1191,7 @@ RED.sidebar.versionControl = (function() { var commitsBehind = result.commits.behind || 0; if (activeProject.git.hasOwnProperty('remotes')) { - if (result.branches.hasOwnProperty("remoteError")) { + if (result.branches.hasOwnProperty("remoteError") && result.branches.remoteError.code !== 'git_remote_gone') { $("#sidebar-version-control-repo-status-auth-issue").show(); $("#sidebar-version-control-repo-status-stats").hide(); $('#sidebar-version-control-repo-branch').attr('disabled',true); diff --git a/editor/sass/editor.scss b/editor/sass/editor.scss index 734d8cb94..9d62de15a 100644 --- a/editor/sass/editor.scss +++ b/editor/sass/editor.scss @@ -160,7 +160,7 @@ } #full-shade { @include shade; - z-index: 101; + z-index: 15; } .dialog-form,#dialog-form, #node-config-dialog-edit-form { diff --git a/editor/sass/notifications.scss b/editor/sass/notifications.scss index d8c8f853b..7f1fe4f43 100644 --- a/editor/sass/notifications.scss +++ b/editor/sass/notifications.scss @@ -15,7 +15,7 @@ **/ #notifications { - z-index: 101; + z-index: 100; width: 500px; margin-left: -250px; left: 50%; diff --git a/red/api/editor/locales/en-US/editor.json b/red/api/editor/locales/en-US/editor.json index 249afb37f..187aba33c 100644 --- a/red/api/editor/locales/en-US/editor.json +++ b/red/api/editor/locales/en-US/editor.json @@ -86,7 +86,8 @@ "missing-types": "Flows stopped due to missing node types. Check logs for details.", "restartRequired": "Node-RED must be restarted to enable upgraded modules", "credentials_load_failed": "Flows stopped due to missing or invalid credentialSecret", - "missing_flow_file": "Could not find the project flow file" + "missing_flow_file": "Could not find the project flow file", + "project_empty": "

The project repository is empty.

Do you want to create a default set of project files?
Otherwise, you will have to manually add files to the project outside of the editor.

" }, "error": "Error: __message__", diff --git a/red/api/editor/projects/index.js b/red/api/editor/projects/index.js index 072abc8d5..4860ded3d 100644 --- a/red/api/editor/projects/index.js +++ b/red/api/editor/projects/index.js @@ -90,6 +90,17 @@ module.exports = { } else { res.redirect(303,req.baseUrl + '/'+ req.params.id); } + } else if (req.body.initialise) { + // Initialised set when creating default files for an empty repo + runtime.storage.projects.initialiseProject(req.user, req.params.id, req.body).then(function() { + res.redirect(303,req.baseUrl + '/'+ req.params.id); + }).catch(function(err) { + if (err.code) { + res.status(400).json({error:err.code, message: err.message}); + } else { + res.status(400).json({error:"unexpected_error", message:err.toString()}); + } + }) } else if (req.body.hasOwnProperty('credentialSecret') || req.body.hasOwnProperty('description') || req.body.hasOwnProperty('dependencies')|| @@ -145,8 +156,11 @@ module.exports = { res.status(404).end(); } }).catch(function(err) { - console.log(err.stack); - res.status(400).json({error:"unexpected_error", message:err.toString()}); + if (err.code) { + res.status(400).json({error:err.code, message: err.message}); + } else { + res.status(400).json({error:"unexpected_error", message:err.toString()}); + } }) }); @@ -154,12 +168,15 @@ module.exports = { // Project file listing app.get("/:id/files", needsPermission("projects.read"), function(req,res) { runtime.storage.projects.getFiles(req.user, req.params.id).then(function(data) { - console.log("TODO: REMOVE /:id/files as /:id/status is better!") + // console.log("TODO: REMOVE /:id/files as /:id/status is better!") res.json(data); }) .catch(function(err) { - console.log(err.stack); - res.status(400).json({error:"unexpected_error", message:err.toString()}); + if (err.code) { + res.status(400).json({error:err.code, message: err.message}); + } else { + res.status(400).json({error:"unexpected_error", message:err.toString()}); + } }) }); diff --git a/red/api/editor/settings.js b/red/api/editor/settings.js index 840534011..0668f3631 100644 --- a/red/api/editor/settings.js +++ b/red/api/editor/settings.js @@ -48,6 +48,16 @@ module.exports = { safeSettings.editorTheme.palette = safeSettings.editorTheme.palette || {}; safeSettings.editorTheme.palette.editable = false; } + if (runtime.storage.projects) { + var activeProject = runtime.storage.projects.getActiveProject(); + if (activeProject) { + safeSettings.project = activeProject; + } + safeSettings.files = { + flow: runtime.storage.projects.getFlowFilename(), + credentials: runtime.storage.projects.getCredentialsFilename() + } + } safeSettings.flowEncryptionType = runtime.nodes.getCredentialKeyType(); diff --git a/red/runtime/storage/localfilesystem/projects/Project.js b/red/runtime/storage/localfilesystem/projects/Project.js index 934b97ca5..65f2e8aec 100644 --- a/red/runtime/storage/localfilesystem/projects/Project.js +++ b/red/runtime/storage/localfilesystem/projects/Project.js @@ -36,6 +36,7 @@ function Project(name) { this.name = name; this.path = fspath.join(projectsDir,name); this.paths = {}; + this.files = {}; this.auth = {origin:{}}; this.missingFiles = []; @@ -111,6 +112,44 @@ Project.prototype.load = function () { }); }; +Project.prototype.initialise = function(user,data) { + var project = this; + if (!this.empty) { + throw new Error("Cannot initialise non-empty project"); + } + var files = Object.keys(defaultFileSet); + var promises = []; + + if (data.hasOwnProperty('credentialSecret')) { + var projects = settings.get('projects'); + projects.projects[project.name] = projects.projects[project.name] || {}; + projects.projects[project.name].credentialSecret = data.credentialSecret; + promises.push(settings.set('projects',projects)); + } + + project.files.flow = data.files.flow; + project.files.credentials = data.files.credentials; + var flowFilePath = fspath.join(project.path,project.files.flow); + var credsFilePath = getCredentialsFilename(flowFilePath); + promises.push(util.writeFile(flowFilePath,"[]")); + promises.push(util.writeFile(credsFilePath,"{}")); + files.push(project.files.flow); + files.push(project.files.credentials); + for (var file in defaultFileSet) { + if (defaultFileSet.hasOwnProperty(file)) { + promises.push(util.writeFile(fspath.join(project.path,file),defaultFileSet[file](project))); + } + } + + return when.all(promises).then(function() { + return gitTools.stageFile(project.path,files); + }).then(function() { + return gitTools.commit(project.path,"Create project files"); + }).then(function() { + return project.load() + }) +} + Project.prototype.loadRemotes = function() { var project = this; return gitTools.getRemotes(project.path).then(function(remotes) { @@ -154,8 +193,15 @@ Project.prototype.loadBranches = function() { var project = this; return gitTools.getBranchInfo(project.path).then(function(branches) { project.branches = branches; + project.empty = project.branches.empty; + delete project.branches.empty; }); } + +Project.prototype.isEmpty = function () { + return this.empty; +}; + Project.prototype.update = function (user, data) { var username; if (!user) { @@ -297,7 +343,12 @@ Project.prototype.update = function (user, data) { }; Project.prototype.getFiles = function () { - return gitTools.getFiles(this.path); + return gitTools.getFiles(this.path).catch(function(err) { + if (/ambiguous argument/.test(err.message)) { + return {}; + } + throw err; + }); }; Project.prototype.stageFile = function(file) { return gitTools.stageFile(this.path,file); @@ -319,7 +370,16 @@ Project.prototype.getFileDiff = function(file,type) { return gitTools.getFileDiff(this.path,file,type); } Project.prototype.getCommits = function(options) { - return gitTools.getCommits(this.path,options); + return gitTools.getCommits(this.path,options).catch(function(err) { + if (/ambiguous argument/.test(err.message) || /does not have any commits yet/.test(err.message)) { + return { + count:0, + commits:[], + total: 0 + } + } + throw err; + }) } Project.prototype.getCommit = function(sha) { return gitTools.getCommit(this.path,sha); @@ -373,20 +433,29 @@ Project.prototype.status = function(user) { } self.branches.local = result.branches.local; self.branches.remote = result.branches.remote; - if (fetchError) { + if (fetchError && !/ambiguous argument/.test(fetchError.message)) { result.branches.remoteError = { remote: fetchError.remote, code: fetchError.code } } return result; + }).catch(function(err) { + if (/ambiguous argument/.test(err.message)) { + return { + files:{}, + commits:{total:0}, + branches:{} + }; + } + throw err; }); } return fetchPromise.then(completeStatus).catch(function(e) { - if (e.code !== 'git_auth_failed') { - console.log("Fetch failed"); - console.log(e); - } + // if (e.code !== 'git_auth_failed') { + // console.log("Fetch failed"); + // console.log(e); + // } return completeStatus(e); }) }; @@ -600,6 +669,7 @@ Project.prototype.toJSON = function () { summary: this.package.description, description: this.description, dependencies: this.package.dependencies||{}, + empty: this.empty, settings: { credentialsEncrypted: (typeof this.credentialSecret === "string"), credentialSecretInvalid: this.credentialSecretInvalid @@ -663,15 +733,15 @@ function createDefaultProject(user, project) { var credsFilePath; if (project.files.migrateFiles) { - var baseFlowFileName = fspath.basename(project.files.flow); - var baseCredentialFileName = fspath.basename(project.files.credentials); + var baseFlowFileName = project.files.flow || fspath.basename(project.files.oldFlow); + var baseCredentialFileName = project.files.credentials || fspath.basename(project.files.oldCredentials); files.push(baseFlowFileName); files.push(baseCredentialFileName); flowFilePath = fspath.join(projectPath,baseFlowFileName); credsFilePath = fspath.join(projectPath,baseCredentialFileName); - log.trace("Migrating "+project.files.flow+" to "+flowFilePath); - log.trace("Migrating "+project.files.credentials+" to "+credsFilePath); - promises.push(fs.copy(project.files.flow,flowFilePath)); + log.trace("Migrating "+project.files.oldFlow+" to "+flowFilePath); + log.trace("Migrating "+project.files.oldCredentials+" to "+credsFilePath); + promises.push(fs.copy(project.files.oldFlow,flowFilePath)); runtime.nodes.setCredentialSecret(project.credentialSecret); promises.push(runtime.nodes.exportCredentials().then(function(creds) { var credentialData; @@ -769,7 +839,6 @@ function createProject(user, metadata) { if (metadata.git && metadata.git.remotes && metadata.git.remotes.origin) { var originRemote = metadata.git.remotes.origin; var auth; - console.log('originRemote:', originRemote); if (originRemote.hasOwnProperty("username") && originRemote.hasOwnProperty("password")) { authCache.set(project,originRemote.url,username,{ // TODO: hardcoded remote name username: originRemote.username, @@ -799,7 +868,18 @@ function createProject(user, metadata) { // console.log("checkProjectFiles"); // console.log(results); // }); - + // return gitTools.getFiles(projectPath).then(function() { + // // It wasn't an empty repository. + // // TODO: check for required files - checkProjectFiles + // + // }).catch(function(err) { + // if (/ambiguous argument/.test(err.message)) { + // // Empty repository + // err.code = "project_empty"; + // err.message = "Project is empty"; + // } + // throw err; + // }); resolve(getProject(project)); }).catch(function(error) { fs.remove(projectPath,function() { diff --git a/red/runtime/storage/localfilesystem/projects/git/index.js b/red/runtime/storage/localfilesystem/projects/git/index.js index 8bce2ce64..d710a0131 100644 --- a/red/runtime/storage/localfilesystem/projects/git/index.js +++ b/red/runtime/storage/localfilesystem/projects/git/index.js @@ -121,20 +121,17 @@ function getBranchInfo(localRepo) { return runGitCommand(["status","--porcelain","-b"],localRepo).then(function(output) { var lines = output.split("\n"); var unknownDirs = []; - var branchLineRE = /^## (.+?)($|\.\.\.(.+?)($| \[(ahead (\d+))?.*?(behind (\d+))?\]))/m; + var branchLineRE = /^## (No commits yet on )?(.+?)($|\.\.\.(.+?)($| \[(ahead (\d+))?.*?(behind (\d+))?\]))/m; var m = branchLineRE.exec(output); var result = {}; //commits:{}}; if (m) { - result.local = m[1]; - if (m[3]) { - result.remote = m[3]; + if (m[1]) { + result.empty = true; + } + result.local = m[2]; + if (m[4]) { + result.remote = m[4]; } - // if (m[6] !== undefined) { - // result.commits.ahead = parseInt(m[6]); - // } - // if (m[8] !== undefined) { - // result.commits.behind = parseInt(m[8]); - // } } return result; }); @@ -177,7 +174,7 @@ function getStatus(localRepo) { return runGitCommand(["status","--porcelain","-b"],localRepo).then(function(output) { var lines = output.split("\n"); var unknownDirs = []; - var branchLineRE = /^## (.+?)($|\.\.\.(.+?)($| \[(ahead (\d+))?.*?(behind (\d+))?\]))/; + var branchLineRE = /^## (.+?)(?:$|\.\.\.(.+?)(?:$| \[(?:(?:ahead (\d+)(?:,\s*)?)?(?:behind (\d+))?|(gone))\]))/; lines.forEach(function(line) { if (line==="") { return; @@ -186,16 +183,22 @@ function getStatus(localRepo) { var m = branchLineRE.exec(line); if (m) { result.branches.local = m[1]; - if (m[3]) { - result.branches.remote = m[3]; + if (m[2]) { + result.branches.remote = m[2]; result.commits.ahead = 0; result.commits.behind = 0; } - if (m[6] !== undefined) { - result.commits.ahead = parseInt(m[6]); + if (m[3] !== undefined) { + result.commits.ahead = parseInt(m[3]); } - if (m[8] !== undefined) { - result.commits.behind = parseInt(m[8]); + if (m[4] !== undefined) { + result.commits.behind = parseInt(m[4]); + } + if (m[5] !== undefined) { + result.commits.ahead = result.commits.total; + result.branches.remoteError = { + code: "git_remote_gone" + } } } return; diff --git a/red/runtime/storage/localfilesystem/projects/index.js b/red/runtime/storage/localfilesystem/projects/index.js index a6802eeb1..e124675b9 100644 --- a/red/runtime/storage/localfilesystem/projects/index.js +++ b/red/runtime/storage/localfilesystem/projects/index.js @@ -303,8 +303,15 @@ function createProject(user, metadata) { if (!metadata.hasOwnProperty('credentialSecret')) { metadata.credentialSecret = currentEncryptionKey; } - metadata.files.flow = flowsFullPath; - metadata.files.credentials = credentialsFile; + if (!metadata.files.flow) { + metadata.files.flow = fspath.basename(flowsFullPath); + } + if (!metadata.files.credentials) { + metadata.files.credentials = fspath.basename(credentialsFile); + } + + metadata.files.oldFlow = flowsFullPath; + metadata.files.oldCredentials = credentialsFile; metadata.files.credentialSecret = currentEncryptionKey; } return Projects.create(null,metadata).then(function(p) { @@ -327,6 +334,21 @@ function setActiveProject(user, projectName) { }) }); } + +function initialiseProject(user, project, data) { + if (!activeProject || activeProject.name !== project) { + // TODO standardise + throw new Error("Cannot initialise inactive project"); + } + return activeProject.initialise(user,data).then(function(result) { + flowsFullPath = activeProject.getFlowFile(); + flowsFileBackup = activeProject.getFlowFileBackup(); + credentialsFile = activeProject.getCredentialsFile(); + credentialsFileBackup = activeProject.getCredentialsFileBackup(); + runtime.nodes.setCredentialSecret(activeProject.credentialSecret); + return reloadActiveProject("updated"); + }); +} function updateProject(user, project, data) { if (!activeProject || activeProject.name !== project) { // TODO standardise @@ -414,9 +436,16 @@ function getFlows() { } } if (activeProject) { + var error; + if (activeProject.isEmpty()) { + log.warn("Project repository is empty"); + error = new Error("Project repository is empty"); + error.code = "project_empty"; + return when.reject(error); + } if (!activeProject.getFlowFile()) { - log.warn("NLS: project has no flow file"); - var error = new Error("NLS: project has no flow file"); + log.warn("Project has no flow file"); + error = new Error("Project has no flow file"); error.code = "missing_flow_file"; return when.reject(error); } @@ -466,6 +495,16 @@ function saveCredentials(credentials) { return util.writeFile(credentialsFile, credentialData); } +function getFlowFilename() { + if (flowsFullPath) { + return fspath.basename(flowsFullPath); + } +} +function getCredentialsFilename() { + if (flowsFullPath) { + return fspath.basename(credentialsFile); + } +} module.exports = { init: init, @@ -475,6 +514,7 @@ module.exports = { getProject: getProject, deleteProject: deleteProject, createProject: createProject, + initialiseProject: initialiseProject, updateProject: updateProject, getFiles: getFiles, getFile: getFile, @@ -498,6 +538,8 @@ module.exports = { addRemote: addRemote, removeRemote: removeRemote, updateRemote: updateRemote, + getFlowFilename: getFlowFilename, + getCredentialsFilename: getCredentialsFilename, getFlows: getFlows, saveFlows: saveFlows,