diff --git a/editor/js/ui/notifications.js b/editor/js/ui/notifications.js index 5c4dfd26c..3be2ffe6c 100644 --- a/editor/js/ui/notifications.js +++ b/editor/js/ui/notifications.js @@ -17,6 +17,21 @@ RED.notify = (function() { var currentNotifications = []; var c = 0; return function(msg,type,fixed,timeout) { + var options = {}; + if (typeof type === 'object') { + options = type; + fixed = options.fixed; + timeout = options.timeout; + type = options.type; + } + + if (options.modal) { + $("#header-shade").show(); + $("#editor-shade").show(); + $("#palette-shade").show(); + $(".sidebar-shade").show(); + } + if (currentNotifications.length > 4) { var ll = currentNotifications.length; for (var i = 0;ll > 4 && i'+ + '
Authentication required for repository:
'+ + '
'+url+'
'+ + '
'+ + '
'+ + '
'+ + '
'+ + ''+ + ''+ + '
'+ + ''); + $(message.find('button')[0]).click(function(evt) { + evt.preventDefault(); + notification.close(); + }) + $(message.find('button')[1]).click(function(evt) { + evt.preventDefault(); + var username = $('#projects-user-auth-username').val(); + var password = $('#projects-user-auth-password').val(); + body = body || {}; + var done = function(err) { + if (err) { + console.log("Failed to update auth"); + console.log(err); + } else { + sendRequest(options,body); + notification.close(); + } + + } + sendRequest({ + url: "projects/"+activeProject.name, + type: "PUT", + responses: { + 0: function(error) { + done(error,null); + }, + 200: function(data) { + done(null,data); + }, + 400: { + 'unexpected_error': function(error) { + done(error,null); + } + }, + } + },{ + remote: { + origin: { + username: username, + password: password + } + } + }); + + }) + var notification = RED.notify(message,{ + type:"error", + fixed: true, + modal: true + }); + return; } else if (responses[xhr.responseJSON.error]) { resultCallback = responses[xhr.responseJSON.error]; resultCallbackArgs = xhr.responseJSON; diff --git a/editor/js/ui/tab-versionControl.js b/editor/js/ui/tab-versionControl.js index 25f9c1275..c506ba9a4 100644 --- a/editor/js/ui/tab-versionControl.js +++ b/editor/js/ui/tab-versionControl.js @@ -216,7 +216,21 @@ RED.sidebar.versionControl = (function() { utils = _utils; RED.actions.add("core:show-version-control-tab",show); + RED.events.on("deploy", function() { + var activeProject = RED.projects.getActiveProject(); + if (activeProject) { + // TODO: this is a full refresh of the files - should be able to + // just do an incremental refresh + allChanges = {}; + unstagedChangesList.editableList('empty'); + stagedChangesList.editableList('empty'); + unmergedChangesList.editableList('empty'); + $.getJSON("/projects/"+activeProject.name+"/status",function(result) { + refreshFiles(result); + }); + } + }) sidebarContent = $('
', {class:"sidebar-version-control"}); var stackContainer = $("
",{class:"sidebar-version-control-stack"}).appendTo(sidebarContent); sections = RED.stack.create({ @@ -414,8 +428,7 @@ RED.sidebar.versionControl = (function() { 200: function(data) { spinner.remove(); cancelCommitButton.click(); - refreshFiles(data); - refreshLocalCommits(); + refresh(true); }, 400: { 'unexpected_error': function(error) { @@ -442,7 +455,7 @@ RED.sidebar.versionControl = (function() { .appendTo(bg) .click(function(evt) { evt.preventDefault(); - refreshLocalCommits(); + refresh(true); }) var localBranchToolbar = $('').appendTo(localHistory.content); @@ -466,7 +479,15 @@ RED.sidebar.versionControl = (function() { },100); } }) - var repoStatusButton = $('') + var repoStatusButton = $('') .appendTo(localBranchToolbar) .click(function(evt) { evt.preventDefault(); @@ -485,7 +506,7 @@ RED.sidebar.versionControl = (function() { }); localCommitList = $("
    ",{style:"position: absolute; top: 30px; bottom: 0px; right:0; left:0;"}).appendTo(localHistory.content); - localCommitListShade = $('
    ').css('top',"30px").hide().appendTo(localHistory.content); + localCommitListShade = $('
    ').css('top',"30px").hide().appendTo(localHistory.content); localCommitList.editableList({ addButton: false, scrollOnAdd: false, @@ -636,6 +657,39 @@ RED.sidebar.versionControl = (function() { $('').appendTo(remoteBox); + + var errorMessage = $('').hide().appendTo(remoteBox); + $('
    Unable to access remote repository
    ').appendTo(errorMessage) + var buttonRow = $('
    ').appendTo(errorMessage); + $('') + .appendTo(buttonRow) + .click(function(e) { + e.preventDefault(); + var activeProject = RED.projects.getActiveProject(); + var spinner = utils.addSpinnerOverlay(remoteBox).addClass("projects-dialog-spinner-contain"); + utils.sendRequest({ + url: "/projects/"+activeProject.name+"/branches/remote", + type: "GET", + responses: { + 0: function(error) { + console.log(error); + // done(error,null); + }, + 200: function(data) { + refresh(true); + }, + 400: { + 'unexpected_error': function(error) { + console.log(error); + // done(error,null); + } + } + } + }).always(function() { + spinner.remove(); + }); + }) + $('').appendTo(remoteBox); var remoteBranchSubRow = $('
    ').hide().appendTo(remoteBranchRow); @@ -891,14 +945,6 @@ RED.sidebar.versionControl = (function() { 'unexpected_error': function(error) { console.log(error); // done(error,null); - }, - 'git_auth_failed': function(error) { - RED.notify('Authentication failed against remote repository','error'); - $('#sidebar-version-control-repo-toolbar-message').text("Failed to fetch remote repository status"); - $("#sidebar-version-control-repo-pull").attr('disabled',true); - $("#sidebar-version-control-repo-push").attr('disabled',true); - - spinner.remove(); } } } @@ -1071,7 +1117,6 @@ RED.sidebar.versionControl = (function() { var activeProject = RED.projects.getActiveProject(); if (activeProject) { $.getJSON("/projects/"+activeProject.name+"/status",function(result) { - refreshFiles(result); $('#sidebar-version-control-local-branch').text(result.branches.local); @@ -1079,19 +1124,36 @@ RED.sidebar.versionControl = (function() { var commitsAhead = result.commits.ahead || 0; var commitsBehind = result.commits.behind || 0; - console.log(commitsBehind,commitsAhead); if (activeProject.hasOwnProperty('remotes')) { - $("#sidebar-version-control-repo-status-button").show(); - if (result.branches.hasOwnProperty('remote')) { - updateRemoteStatus(commitsAhead, commitsBehind); - } else { - $('#sidebar-version-control-commits-ahead').text(""); - $('#sidebar-version-control-commits-behind').text(""); - - $('#sidebar-version-control-repo-toolbar-message').text("Your local branch is not currently tracking a remote branch."); + if (result.branches.hasOwnProperty("remoteError")) { + $("#sidebar-version-control-repo-status-auth-issue").show(); + $("#sidebar-version-control-repo-status-stats").hide(); + $('#sidebar-version-control-repo-branch').attr('disabled',true); $("#sidebar-version-control-repo-pull").attr('disabled',true); $("#sidebar-version-control-repo-push").attr('disabled',true); + $('#sidebar-version-control-repo-toolbar-message').hide(); + $('#sidebar-version-control-repo-toolbar-error-message').show(); + } else { + $('#sidebar-version-control-repo-toolbar-message').show(); + $('#sidebar-version-control-repo-toolbar-error-message').hide(); + + $("#sidebar-version-control-repo-status-auth-issue").hide(); + $("#sidebar-version-control-repo-status-stats").show(); + + $('#sidebar-version-control-repo-branch').attr('disabled',false); + + $("#sidebar-version-control-repo-status-button").show(); + if (result.branches.hasOwnProperty('remote')) { + updateRemoteStatus(commitsAhead, commitsBehind); + } else { + $('#sidebar-version-control-commits-ahead').text(""); + $('#sidebar-version-control-commits-behind').text(""); + + $('#sidebar-version-control-repo-toolbar-message').text("Your local branch is not currently tracking a remote branch."); + $("#sidebar-version-control-repo-pull").attr('disabled',true); + $("#sidebar-version-control-repo-push").attr('disabled',true); + } } } else { $("#sidebar-version-control-repo-status-button").hide(); diff --git a/editor/sass/projects.scss b/editor/sass/projects.scss index d6cd4e6a7..0ea0f0a20 100644 --- a/editor/sass/projects.scss +++ b/editor/sass/projects.scss @@ -330,7 +330,7 @@ box-sizing: border-box; overflow: hidden; &.sidebar-version-control-slide-box-top { - z-index: 10; + z-index: 4; top: 0px; left: auto; width: 100%; diff --git a/red/api/editor/projects/index.js b/red/api/editor/projects/index.js index 179f60ed6..5b0c06709 100644 --- a/red/api/editor/projects/index.js +++ b/red/api/editor/projects/index.js @@ -28,8 +28,8 @@ module.exports = { // Projects + // List all projects app.get("/", function(req,res) { - // List projects runtime.storage.projects.listProjects().then(function(list) { var active = runtime.storage.projects.getActiveProject(); var response = { @@ -47,8 +47,8 @@ module.exports = { }) }); + // Create project app.post("/", function(req,res) { - // Create project runtime.storage.projects.createProject(req.body).then(function(data) { res.json(data); }).catch(function(err) { @@ -61,8 +61,8 @@ module.exports = { }) }); + // Update a project app.put("/:id", function(req,res) { - // Update a project //TODO: validate the payload properly if (req.body.active) { var currentProject = runtime.storage.projects.getActiveProject(); @@ -83,7 +83,8 @@ module.exports = { req.body.hasOwnProperty('description') || req.body.hasOwnProperty('dependencies')|| req.body.hasOwnProperty('summary') || - req.body.hasOwnProperty('files')) { + req.body.hasOwnProperty('files') || + req.body.hasOwnProperty('remote')) { runtime.storage.projects.updateProject(req.params.id, req.body).then(function() { res.redirect(303,req.baseUrl + '/'+ req.params.id); }).catch(function(err) { @@ -96,11 +97,10 @@ module.exports = { } else { res.status(400).json({error:"unexpected_error", message:"invalid_request"}); } - }); + // Get project metadata app.get("/:id", function(req,res) { - // Get project metadata runtime.storage.projects.getProject(req.params.id).then(function(data) { if (data) { res.json(data); @@ -113,13 +113,13 @@ module.exports = { }) }); - app.delete("/:id",function(req,res) { - // Delete project + // Delete project - tbd + app.delete("/:id", function(req,res) { }); + // Get project status - files, commit counts, branch info app.get("/:id/status", function(req,res) { - // Get project metadata runtime.storage.projects.getStatus(req.params.id).then(function(data) { if (data) { res.json(data); @@ -133,8 +133,7 @@ module.exports = { }); - // Project Files - + // Project file listing app.get("/:id/files", function(req,res) { runtime.storage.projects.getFiles(req.params.id).then(function(data) { console.log("TODO: REMOVE /:id/files as /:id/status is better!") @@ -146,7 +145,8 @@ module.exports = { }) }); - // /:project/files/:treeish/file-path + + // Get file content in a given tree (index/stage) app.get("/:id/files/:treeish/*", function(req,res) { var projectId = req.params.id; var treeish = req.params.treeish; @@ -161,6 +161,7 @@ module.exports = { }) }); + // Stage a file app.post("/:id/stage/*", function(req,res) { var projectName = req.params.id; var file = req.params[0]; @@ -173,6 +174,8 @@ module.exports = { res.status(400).json({error:"unexpected_error", message:err.toString()}); }) }); + + // Stage multiple files app.post("/:id/stage", function(req,res) { var projectName = req.params.id; var files = req.body.files; @@ -186,6 +189,7 @@ module.exports = { }) }); + // Commit changes app.post("/:id/commit", function(req,res) { var projectName = req.params.id; @@ -198,6 +202,7 @@ module.exports = { }) }); + // Unstage a file app.delete("/:id/stage/*", function(req,res) { var projectName = req.params.id; var file = req.params[0]; @@ -210,6 +215,8 @@ module.exports = { res.status(400).json({error:"unexpected_error", message:err.toString()}); }) }); + + // Unstage multiple files app.delete("/:id/stage", function(req, res) { var projectName = req.params.id; runtime.storage.projects.unstageFile(projectName).then(function(data) { @@ -221,6 +228,7 @@ module.exports = { }) }); + // Get a file diff app.get("/:id/diff/:type/*", function(req,res) { var projectName = req.params.id; var type = req.params.type; @@ -236,6 +244,7 @@ module.exports = { }) }); + // Get a list of commits app.get("/:id/commits", function(req, res) { var projectName = req.params.id; var options = { @@ -255,6 +264,7 @@ module.exports = { }) }); + // Get an individual commit details app.get("/:id/commits/:sha", function(req, res) { var projectName = req.params.id; var sha = req.params.sha; @@ -268,6 +278,7 @@ module.exports = { }) }); + // Push local commits to remote app.post("/:id/push/?*", function(req,res) { var projectName = req.params.id; var remoteBranchName = req.params[0] @@ -284,6 +295,8 @@ module.exports = { } }) }); + + // Pull remote commits app.get("/:id/pull/?*", function(req,res) { var projectName = req.params.id; var remoteBranchName = req.params[0]; @@ -301,6 +314,7 @@ module.exports = { }) }); + // Abort an ongoing merge app.delete("/:id/merge", function(req, res) { var projectName = req.params.id; runtime.storage.projects.abortMerge(projectName).then(function(data) { @@ -316,6 +330,7 @@ module.exports = { }) }); + // Resolve a merge app.post("/:id/resolve/*", function(req, res) { var projectName = req.params.id; var file = req.params[0]; @@ -333,6 +348,7 @@ module.exports = { }) }); + // Get a list of local branches app.get("/:id/branches", function(req, res) { var projectName = req.params.id; runtime.storage.projects.getBranches(projectName,false).then(function(data) { @@ -347,6 +363,8 @@ module.exports = { } }) }); + + // Get a list of remote branches app.get("/:id/branches/remote", function(req, res) { var projectName = req.params.id; runtime.storage.projects.getBranches(projectName,true).then(function(data) { @@ -362,6 +380,7 @@ module.exports = { }) }); + // Get branch status - commit counts/ahead/behind app.get("/:id/branches/remote/*/status", function(req, res) { var projectName = req.params.id; var branch = req.params[0]; @@ -378,7 +397,7 @@ module.exports = { }) }); - + // Set the active local branch app.post("/:id/branches", function(req, res) { var projectName = req.params.id; var branchName = req.body.name; diff --git a/red/runtime/storage/localfilesystem/projects/Project.js b/red/runtime/storage/localfilesystem/projects/Project.js index 272a03b41..b67db60f7 100644 --- a/red/runtime/storage/localfilesystem/projects/Project.js +++ b/red/runtime/storage/localfilesystem/projects/Project.js @@ -29,10 +29,19 @@ var log; var projectsDir; +var authCache = {}; + +function getAuth(project,remote) { + if (authCache.hasOwnProperty(project) && authCache[project].hasOwnProperty(remote)) { + return authCache[project][remote]; + } + return null; +} function Project(name) { this.name = name; this.path = fspath.join(projectsDir,name); this.paths = {}; + this.auth = {origin:{}}; this.missingFiles = []; @@ -169,7 +178,14 @@ Project.prototype.update = function (data) { savePackage = true; this.package.description = data.summary; } - + if (data.hasOwnProperty('remote')) { + authCache[project.name] = authCache[project.name]||{}; + for (var remote in data.remote) { + if (data.remote.hasOwnProperty(remote)) { + authCache[project.name][remote] = data.remote[remote]; + } + } + } if (data.hasOwnProperty('files')) { this.package['node-red'] = this.package['node-red'] || { settings: {}}; if (data.files.hasOwnProperty('flow') && this.package['node-red'].settings.flowFile !== data.files.flow) { @@ -221,24 +237,7 @@ Project.prototype.getFileDiff = function(file,type) { return gitTools.getFileDiff(this.path,file,type); } Project.prototype.getCommits = function(options) { - var self = this; - var fetchPromise; - if (this.remote) { - options.hasRemote = true; - fetchPromise = gitTools.fetch(this.path) - } else { - fetchPromise = when.resolve(); - } - return fetchPromise.then(function() { - return gitTools.getCommits(self.path,options); - }).otherwise(function(e) { - if (e.code === 'git_auth_failed') { - throw e; - } - console.log("Fetch failed"); - console.log(e); - return gitTools.getCommits(self.path,options); - }); + return gitTools.getCommits(this.path,options); } Project.prototype.getCommit = function(sha) { return gitTools.getCommit(this.path,sha); @@ -253,32 +252,56 @@ Project.prototype.getFile = function (filePath,treeish) { Project.prototype.status = function() { var self = this; - var promises = [ - gitTools.getStatus(this.path), - fs.exists(fspath.join(this.path,".git","MERGE_HEAD")) - ] - return when.all(promises).then(function(results) { - var result = results[0]; - if (results[1]) { - result.merging = true; + + var fetchPromise; + if (this.remote) { + fetchPromise = gitTools.fetch(this.path,getAuth(this.name,'origin')); + } else { + fetchPromise = when.resolve(); + } + + var completeStatus = function(fetchError) { + var promises = [ + gitTools.getStatus(self.path), + fs.exists(fspath.join(self.path,".git","MERGE_HEAD")) + ]; + return when.all(promises).then(function(results) { + var result = results[0]; + if (results[1]) { + result.merging = true; + } + self.branches.local = result.branches.local; + self.branches.remote = result.branches.remote; + if (fetchError) { + result.branches.remoteError = { + code: fetchError.code + } + } + return result; + }); + } + return fetchPromise.then(completeStatus).catch(function(e) { + if (e.code === 'git_auth_failed') { + console.log("Fetch auth failed"); + } else { + console.log("Fetch failed"); + console.log(e); } - self.branches.local = result.branches.local; - self.branches.remote = result.branches.remote; - return result; + return completeStatus(e); }) }; Project.prototype.push = function (remoteBranchName,setRemote) { - return gitTools.push(this.path, remoteBranchName, setRemote); + return gitTools.push(this.path, remoteBranchName, setRemote, getAuth(this.name,'origin')); }; Project.prototype.pull = function (remoteBranchName,setRemote) { if (setRemote) { return gitTools.setUpstream(this.path, remoteBranchName).then(function() { - return gitTools.pull(this.path); + return gitTools.pull(this.path, null, getAuth(this.name,'origin')); }) } else { - return gitTools.pull(this.path, remoteBranchName); + return gitTools.pull(this.path, remoteBranchName, getAuth(this.name,'origin')); } }; @@ -328,7 +351,16 @@ Project.prototype.abortMerge = function () { }; Project.prototype.getBranches = function (remote) { - return gitTools.getBranches(this.path,remote); + var self = this; + var fetchPromise; + if (remote) { + fetchPromise = gitTools.fetch(this.path,getAuth(this.name,'origin')) + } else { + fetchPromise = when.resolve(); + } + return fetchPromise.then(function() { + return gitTools.getBranches(self.path,remote); + }); }; Project.prototype.setBranch = function (branchName, isCreate) { var self = this; @@ -347,6 +379,7 @@ Project.prototype.getFlowFile = function() { return null; } } + Project.prototype.getFlowFileBackup = function() { return getBackupFilename(this.getFlowFile()); } @@ -499,8 +532,18 @@ function createProject(metadata) { return settings.set('projects',projects); }).then(function() { if (metadata.remote) { - console.log(metadata.remote); - return gitTools.clone(metadata.remote,projectPath).then(function(result) { + var auth; + if (metadata.remote.hasOwnProperty("username") && metadata.remote.hasOwnProperty("password")) { + authCache[project] = { + origin: { // TODO: hardcoded remote name + username: metadata.remote.username, + password: metadata.remote.password + } + } + auth = authCache[project].origin; + } + + return gitTools.clone(metadata.remote,auth,projectPath).then(function(result) { // Check this is a valid project // If it is empty // - if 'populate' flag is set, call populateProject diff --git a/red/runtime/storage/localfilesystem/projects/git/authServer.js b/red/runtime/storage/localfilesystem/projects/git/authServer.js index 856f4ba49..0a2ae60e4 100644 --- a/red/runtime/storage/localfilesystem/projects/git/authServer.js +++ b/red/runtime/storage/localfilesystem/projects/git/authServer.js @@ -28,7 +28,7 @@ function getListenPath() { } else { listenPath = path.join(process.env['XDG_RUNTIME_DIR'] || os.tmpdir(), fn); } - console.log(listenPath); + // console.log(listenPath); return listenPath; } @@ -68,7 +68,7 @@ var ResponseServer = function(auth) { resolve({path:listenPath,close:function() { server.close(); }}); }); server.on('close', function() { - console.log("Closing response server"); + // console.log("Closing response server"); fs.removeSync(listenPath); }); server.on('error',function(err) { diff --git a/red/runtime/storage/localfilesystem/projects/git/index.js b/red/runtime/storage/localfilesystem/projects/git/index.js index 52a9a79a5..c695fa4c0 100644 --- a/red/runtime/storage/localfilesystem/projects/git/index.js +++ b/red/runtime/storage/localfilesystem/projects/git/index.js @@ -263,26 +263,6 @@ function parseLog(log) { return commits; } -// function getCommitCounts(cwd, options) { -// var commands = [ -// runGitCommand(['rev-list', 'HEAD', '--count'],cwd), // #commits on master -// ]; -// if (options.hasRemote) { -// commands.push(runGitCommand(['rev-list', 'master','^origin/master', '--count'],cwd)); // #commits master ahead -// commands.push(runGitCommand(['rev-list', '^master','origin/master', '--count'],cwd)); // #commits master behind -// } -// return when.all(commands).then(function(results) { -// var result = { -// total: parseInt(results[0]) -// } -// if (options.hasRemote) { -// result.ahead = parseInt(results[1]); -// result.behind = parseInt(results[2]); -// } -// return result; -// }); -// } - function getRemotes(cwd) { return runGitCommand(['remote','-v'],cwd).then(function(output) { var result; @@ -362,7 +342,7 @@ module.exports = { throw err; }); }, - push: function(cwd,remoteBranch,setUpstream) { + push: function(cwd,remoteBranch,setUpstream, auth) { var args = ["push"]; var m = /^(.*?)\/(.*)$/.exec(remoteBranch); if (m) { @@ -375,7 +355,13 @@ module.exports = { args.push("origin"); } args.push("--porcelain"); - return runGitCommand(args,cwd).otherwise(function(err) { + var promise; + if (auth) { + promise = runGitCommandWithAuth(args,cwd,auth); + } else { + promise = runGitCommand(args,cwd) + } + return promise.catch(function(err) { if (err.code === 'git_error') { if (/^!.*non-fast-forward/m.test(err.stdout)) { err.code = 'git_push_failed'; @@ -384,7 +370,7 @@ module.exports = { } }); }, - clone: function(remote, cwd) { + clone: function(remote, auth, cwd) { var args = ["clone",remote.url]; if (remote.name) { args.push("-o"); @@ -395,8 +381,8 @@ module.exports = { args.push(remote.branch); } args.push("."); - if (remote.hasOwnProperty("username") && remote.hasOwnProperty("password")) { - return runGitCommandWithAuth(args,cwd,remote); + if (auth) { + return runGitCommandWithAuth(args,cwd,auth); } else { return runGitCommand(args,cwd); } @@ -441,8 +427,13 @@ module.exports = { args.push(file); return runGitCommand(args,cwd); }, - fetch: function(cwd) { - return runGitCommand(["fetch"],cwd); + fetch: function(cwd,auth) { + var args = ["fetch"]; + if (auth) { + return runGitCommandWithAuth(args,cwd,auth); + } else { + return runGitCommand(args,cwd); + } }, getCommits: function(cwd,options) { var args = ["log", "--format=sha: %H%nparents: %p%nrefs: %D%nauthor: %an%ndate: %ct%nsubject: %s%n-----"];