quadre-git
Version:
Integration of Git into Quadre
340 lines (294 loc) • 13 kB
text/typescript
import { _, DefaultDialogs, Dialogs, Mustache, StringUtils } from "./brackets-modules";
import * as ErrorHandler from "./ErrorHandler";
import * as Events from "./Events";
import EventEmitter from "./EventEmitter";
import * as Git from "./git/GitCli";
import * as Git2 from "./git/Git";
import * as Preferences from "./Preferences";
import * as ProgressDialog from "./dialogs/Progress";
import * as Promise from "bluebird";
import * as PullDialog from "./dialogs/Pull";
import * as PushDialog from "./dialogs/Push";
import * as Strings from "strings";
import * as Utils from "./Utils";
import gitRemotesPickerTemplate = require("text!templates/git-remotes-picker.html");
let $selectedRemote = null;
let $remotesDropdown = null;
let $gitPanel = null;
let $gitPush = null;
function initVariables() {
$gitPanel = $("#git-panel");
$selectedRemote = $gitPanel.find(".git-selected-remote");
$remotesDropdown = $gitPanel.find(".git-remotes-dropdown");
$gitPush = $gitPanel.find(".git-push");
}
function getDefaultRemote(allRemotes) {
const defaultRemotes = Preferences.get("defaultRemotes") || {};
let candidate = defaultRemotes[Preferences.get("currentGitRoot")];
const exists = _.find(allRemotes, (remote) => remote.name === candidate);
if (!exists) {
candidate = null;
if (allRemotes.length > 0) {
candidate = _.first(allRemotes).name;
}
}
return candidate;
}
function setDefaultRemote(remoteName) {
const defaultRemotes = Preferences.get("defaultRemotes") || {};
defaultRemotes[Preferences.get("currentGitRoot")] = remoteName;
Preferences.persist("defaultRemotes", defaultRemotes);
}
function clearRemotePicker() {
$selectedRemote
.html("—")
.data("remote", null);
}
function selectRemote(remoteName, type) {
if (!remoteName) {
return clearRemotePicker();
}
// Set as default remote only if is a normal git remote
if (type === "git") { setDefaultRemote(remoteName); }
// Disable pull if it is not a normal git remote
$gitPanel.find(".git-pull").prop("disabled", type !== "git");
// Enable push and set selected-remote-type to Git push button by type of remote
$gitPush
.prop("disabled", false)
.attr("x-selected-remote-type", type);
// Update remote name of $selectedRemote
$selectedRemote
.text(remoteName)
.attr("data-type", type) // use attr to apply CSS styles
.data("remote", remoteName);
}
function refreshRemotesPicker() {
Git.getRemotes().then((remotes) => {
// Set default remote name and cache the remotes dropdown menu
const defaultRemoteName = getDefaultRemote(remotes);
// Disable Git-push and Git-pull if there are not remotes defined
$gitPanel
.find(".git-pull, .git-push, .git-fetch")
.prop("disabled", remotes.length === 0);
// Add options to change remote
remotes.forEach((remote) => remote.deletable = remote.name !== "origin");
// Pass to Mustache the needed data
const compiledTemplate = Mustache.render(gitRemotesPickerTemplate, { Strings, remotes });
// Inject the rendered template inside the $remotesDropdown
$remotesDropdown.html(compiledTemplate);
// Notify others that they may add more stuff to this dropdown
EventEmitter.emit(Events.REMOTES_REFRESH_PICKER);
// TODO: is it possible to wait for listeners to finish?
// TODO: if there're no remotes but there are some ftp remotes
// we need to adjust that something other may be put as default
// low priority
if (remotes.length > 0) {
selectRemote(defaultRemoteName, "git");
} else {
clearRemotePicker();
}
}).catch((err) => ErrorHandler.showError(err, "Getting remotes failed!"));
}
function handleRemoteCreation() {
return Utils.askQuestion(Strings.CREATE_NEW_REMOTE, Strings.ENTER_REMOTE_NAME)
.then((name) => {
return Utils.askQuestion(Strings.CREATE_NEW_REMOTE, Strings.ENTER_REMOTE_URL).then((url) => {
return [name, url];
});
})
.spread((name, url) => {
return Git.createRemote(name, url).then(() => {
return refreshRemotesPicker();
});
})
.catch((err) => {
if (!ErrorHandler.equals(err, Strings.USER_ABORTED)) {
ErrorHandler.showError(err, "Remote creation failed");
}
});
}
function deleteRemote(remoteName) {
return Utils.askQuestion(
Strings.DELETE_REMOTE, StringUtils.format(Strings.DELETE_REMOTE_NAME, remoteName), { booleanResponse: true }
)
.then((response) => {
if (response === true) {
return Git.deleteRemote(remoteName).then(() => refreshRemotesPicker());
}
return null;
})
.catch((err) => ErrorHandler.logError(err));
}
function showPushResult(result) {
if (typeof result.remoteUrl === "string") {
result.remoteUrl = Utils.encodeSensitiveInformation(result.remoteUrl);
}
const template = [
"<h3>{{flagDescription}}</h3>",
"Info:",
"Remote url - {{remoteUrl}}",
"Local branch - {{from}}",
"Remote branch - {{to}}",
"Summary - {{summary}}",
"<h4>Status - {{status}}</h4>"
].join("<br>");
Dialogs.showModalDialog(
DefaultDialogs.DIALOG_ID_INFO,
Strings.GIT_PUSH_RESPONSE, // title
Mustache.render(template, result) // message
);
}
function pushToRemote(remote) {
if (!remote) {
const msg = "No remote has been selected for push!";
return ErrorHandler.showError(new Error(msg), msg);
}
return PushDialog.show({ remote })
.then((pushConfig) => {
let q: Promise<any> = Promise.resolve();
const additionalArgs = [];
if (pushConfig.tags) {
additionalArgs.push("--tags");
}
// set a new tracking branch if desired
if (pushConfig.branch && pushConfig.setBranchAsTracking) {
q = q.then(() => Git.setUpstreamBranch(pushConfig.remote, pushConfig.branch));
}
// put username and password into remote url
if (pushConfig.remoteUrlNew) {
q = q.then(() => Git2.setRemoteUrl(pushConfig.remote, pushConfig.remoteUrlNew));
}
// do the pull itself (we are not using pull command)
q = q.then(() => {
let op;
if (pushConfig.pushToNew) {
op = Git2.pushToNewUpstream(pushConfig.remote, pushConfig.branch);
} else if (pushConfig.strategy === "DEFAULT") {
op = Git.push(pushConfig.remote, pushConfig.branch, additionalArgs);
} else if (pushConfig.strategy === "FORCED") {
op = Git2.pushForced(pushConfig.remote, pushConfig.branch);
} else if (pushConfig.strategy === "DELETE_BRANCH") {
op = Git2.deleteRemoteBranch(pushConfig.remote, pushConfig.branch);
}
return ProgressDialog.show(op)
.then((result) => {
return ProgressDialog.waitForClose().then(() => {
showPushResult(result);
});
})
.catch((err) => ErrorHandler.showError(err, "Pushing to remote failed"));
});
// restore original url if desired
if (pushConfig.remoteUrlRestore) {
q = q.finally(() => Git2.setRemoteUrl(pushConfig.remote, pushConfig.remoteUrlRestore));
}
return q.finally(() => EventEmitter.emit(Events.REFRESH_ALL));
})
.catch((err) => {
// when dialog is cancelled, there's no error
if (err) { ErrorHandler.showError(err, "Pushing operation failed"); }
});
}
function pullFromRemote(remote) {
if (!remote) {
const msg = "No remote has been selected for pull!";
return ErrorHandler.showError(new Error(msg), msg);
}
return PullDialog.show({ remote })
.then((pullConfig) => {
let q: Promise<any> = Promise.resolve();
// set a new tracking branch if desired
if (pullConfig.branch && pullConfig.setBranchAsTracking) {
q = q.then(() => Git.setUpstreamBranch(pullConfig.remote, pullConfig.branch));
}
// put username and password into remote url
if (pullConfig.remoteUrlNew) {
q = q.then(() => Git2.setRemoteUrl(pullConfig.remote, pullConfig.remoteUrlNew));
}
// do the pull itself (we are not using pull command)
q = q.then(() => {
// fetch the remote first
return ProgressDialog.show(Git.fetchRemote(pullConfig.remote))
.then(() => {
if (pullConfig.strategy === "DEFAULT") {
return Git.mergeRemote(pullConfig.remote, pullConfig.branch);
}
if (pullConfig.strategy === "AVOID_MERGING") {
return Git.mergeRemote(pullConfig.remote, pullConfig.branch, true);
}
if (pullConfig.strategy === "MERGE_NOCOMMIT") {
return Git.mergeRemote(pullConfig.remote, pullConfig.branch, false, true);
}
if (pullConfig.strategy === "REBASE") {
return Git.rebaseRemote(pullConfig.remote, pullConfig.branch);
}
if (pullConfig.strategy === "RESET") {
return Git.resetRemote(pullConfig.remote, pullConfig.branch);
}
throw new Error(`Unexpected pullConfig.strategy: ${pullConfig.strategy}`);
})
.then((result) => {
return ProgressDialog.waitForClose().then(() => {
return Utils.showOutput(result, Strings.GIT_PULL_RESPONSE);
});
})
.catch((err) => ErrorHandler.showError(err, "Pulling from remote failed"));
});
// restore original url if desired
if (pullConfig.remoteUrlRestore) {
q = q.finally(() => Git2.setRemoteUrl(pullConfig.remote, pullConfig.remoteUrlRestore));
}
return q.finally(() => EventEmitter.emit(Events.REFRESH_ALL));
})
.catch((err) => {
// when dialog is cancelled, there's no error
if (err) { ErrorHandler.showError(err, "Pulling operation failed"); }
});
}
function handleFetch(silent = false) {
// Tell the rest of the plugin that the fetch has started
EventEmitter.emit(Events.FETCH_STARTED);
let q;
if (!silent) {
// If it's not a silent fetch show a progress window
q = ProgressDialog.show(Git.fetchAllRemotes())
.catch((err) => ErrorHandler.showError(err))
.then(ProgressDialog.waitForClose);
} else {
// Else fetch in the background
q = Git.fetchAllRemotes()
.catch((err) => ErrorHandler.logError(err));
}
// Tell the rest of the plugin that the fetch has completed
return q.finally(() => EventEmitter.emit(Events.FETCH_COMPLETE));
}
// Event subscriptions
EventEmitter.on(Events.GIT_ENABLED, () => {
initVariables();
refreshRemotesPicker();
});
EventEmitter.on(Events.HANDLE_REMOTE_PICK, (event) => {
const $remote = $(event.target).closest(".remote-name");
const remoteName = $remote.data("remote-name");
const type = $remote.data("type");
selectRemote(remoteName, type);
EventEmitter.emit(Events.REFRESH_COUNTERS);
});
EventEmitter.on(Events.HANDLE_REMOTE_CREATE, () => {
handleRemoteCreation();
});
EventEmitter.on(Events.HANDLE_REMOTE_DELETE, (event) => {
const remoteName = $(event.target).closest(".remote-name").data("remote-name");
deleteRemote(remoteName);
});
EventEmitter.on(Events.HANDLE_PULL, () => {
const remoteName = $selectedRemote.data("remote");
pullFromRemote(remoteName);
});
EventEmitter.on(Events.HANDLE_PUSH, () => {
const remoteName = $selectedRemote.data("remote");
pushToRemote(remoteName);
});
EventEmitter.on(Events.HANDLE_FETCH, () => {
handleFetch();
});