@jupyterlab/git
Version:
A JupyterLab extension for version control using git
1,062 lines • 74.6 kB
JavaScript
import { Dialog, InputDialog, MainAreaWidget, Notification, ReactWidget, showDialog, showErrorMessage } from '@jupyterlab/apputils';
import { CodeEditor, CodeEditorWrapper } from '@jupyterlab/codeeditor';
import { PathExt, URLExt } from '@jupyterlab/coreutils';
import { Toolbar, ToolbarButton, closeIcon, saveIcon } from '@jupyterlab/ui-components';
import { ArrayExt, find } from '@lumino/algorithm';
import { PromiseDelegate } from '@lumino/coreutils';
import { 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 { PreviewMainAreaWidget } from './components/diff/PreviewMainAreaWidget';
import { DiffModel } from './components/diff/model';
import { AUTH_ERROR_MESSAGES, requestAPI } from './git';
import { getDiffProvider } from './model';
import { showDetails, showError } from './notifications';
import { addIcon, diffIcon, discardIcon, gitIcon, historyIcon, openIcon, removeIcon, tagIcon } from './style/icons';
import { CommandIDs, ContextCommandIDs, Git } from './tokens';
import { AdvancedPushForm } from './widgets/AdvancedPushForm';
import { GitCredentialsForm } from './widgets/CredentialsBox';
import { CheckboxForm } from './widgets/GitResetToRemoteForm';
import { discardAllChanges } from './widgets/discardAllChanges';
/**
* Git operations requiring authentication
*/
export var Operation;
(function (Operation) {
Operation["Clone"] = "Clone";
Operation["Pull"] = "Pull";
Operation["Push"] = "Push";
Operation["ForcePush"] = "ForcePush";
Operation["Fetch"] = "Fetch";
})(Operation || (Operation = {}));
function pluralizedContextLabel(singular, plural) {
return (args) => {
const { files } = args;
if (files.length > 1) {
return plural;
}
else {
return singular;
}
};
}
/**
* Add the commands for the git extension.
*/
export function addCommands(app, gitModel, editorFactory, fileBrowserModel, settings, translator) {
const { commands, shell, serviceManager } = app;
const { serverSettings } = serviceManager;
const trans = translator.load('jupyterlab_git');
/**
* Commit using a keystroke combination when in CommitBox.
*
* This command is not accessible from the user interface (not visible),
* as it is handled by a signal listener in the CommitBox component instead.
* The label and caption are given to ensure that the command will
* show up in the shortcut editor UI with a nice description.
*/
commands.addCommand(CommandIDs.gitSubmitCommand, {
label: trans.__('Commit from the Commit Box'),
caption: trans.__('Submit the commit using the summary and description from commit box'),
execute: () => void 0,
isVisible: () => false
});
/**
* Add open terminal in the Git repository
*/
commands.addCommand(CommandIDs.gitTerminalCommand, {
label: trans.__('Open Git Repository in Terminal'),
caption: trans.__('Open a New Terminal to the Git Repository'),
execute: async (args) => {
const cwd = gitModel.pathRepository;
const main = (await commands.execute('terminal:create-new', {
...args,
cwd
}));
return main;
},
isEnabled: () => gitModel.pathRepository !== null &&
app.serviceManager.terminals.isAvailable()
});
/** Add open/go to git interface command */
commands.addCommand(CommandIDs.gitUI, {
label: trans.__('Git Interface'),
caption: trans.__('Go to Git user interface'),
execute: () => {
try {
shell.activateById('jp-git-sessions');
}
catch (_err) {
console.error('Fail to open Git tab.');
}
}
});
/** Add git init command */
commands.addCommand(CommandIDs.gitInit, {
label: trans.__('Initialize a Repository'),
caption: trans.__('Create an empty Git repository or reinitialize an existing one'),
execute: async () => {
const currentPath = app.serviceManager.contents.localPath(fileBrowserModel.path);
const result = await showDialog({
title: trans.__('Initialize a Repository'),
body: trans.__('Do you really want to make this directory a Git Repo?'),
buttons: [
Dialog.cancelButton({ label: trans.__('Cancel') }),
Dialog.warnButton({ label: trans.__('Yes') })
]
});
if (result.button.accept) {
const id = Notification.emit(trans.__('Initializing…'), 'in-progress', {
autoClose: false
});
try {
await gitModel.init(currentPath);
gitModel.pathRepository = currentPath;
Notification.update({
id,
message: trans.__('Git repository initialized.'),
type: 'success',
autoClose: 5000
});
}
catch (error) {
console.error(trans.__('Encountered an error when initializing the repository. Error: '), error);
Notification.update({
id,
message: trans.__('Failed to initialize the Git repository'),
type: 'error',
...showError(error, trans)
});
}
}
},
isEnabled: () => gitModel.pathRepository === null
});
/** Open URL externally */
commands.addCommand(CommandIDs.gitOpenUrl, {
label: args => trans.__(args['text']),
execute: args => {
const url = args['url'];
window.open(url);
}
});
/** add toggle for simple staging */
commands.addCommand(CommandIDs.gitToggleSimpleStaging, {
label: trans.__('Simple staging'),
isToggled: () => !!settings.composite['simpleStaging'],
execute: args => {
settings.set('simpleStaging', !settings.composite['simpleStaging']);
}
});
/** Command to add a remote Git repository */
commands.addCommand(CommandIDs.gitManageRemote, {
label: trans.__('Manage Remote Repositories'),
caption: trans.__('Manage Remote Repositories'),
isEnabled: () => gitModel.pathRepository !== null,
execute: () => {
if (gitModel.pathRepository === null) {
console.warn(trans.__('Not in a Git repository. Unable to add a remote.'));
return;
}
const widgetId = 'git-dialog-ManageRemote';
let anchor = document.querySelector(`#${widgetId}`);
if (!anchor) {
anchor = document.createElement('div');
anchor.id = widgetId;
document.body.appendChild(anchor);
}
const dialog = ReactWidget.create(React.createElement(ManageRemoteDialogue, { trans: trans, model: gitModel, onClose: () => dialog.dispose() }));
Widget.attach(dialog, anchor);
}
});
async function showGitignore(error) {
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, hidePrompt) {
if (hidePrompt) {
return showGitignore(error);
}
const result = await showDialog({
title: trans.__('Warning: The .gitignore file is a hidden file.'),
body: (React.createElement("div", null,
trans.__('Hidden files by default cannot be accessed with the regular code editor. In order to open the .gitignore file you must:'),
React.createElement("ol", null,
React.createElement("li", null,
trans.__('Print the command below to create a jupyter_server_config.py file with defaults commented out. If you already have the file located in .jupyter, skip this step.'),
React.createElement("div", { style: { padding: '0.5rem' } }, 'jupyter server --generate-config')),
React.createElement("li", null,
trans.__('Open jupyter_server_config.py, uncomment out the following line and set it to True:'),
React.createElement("div", { style: { padding: '0.5rem' } }, 'c.ContentsManager.allow_hidden = False'))))),
buttons: [
Dialog.cancelButton({ label: trans.__('Cancel') }),
Dialog.okButton({ label: trans.__('Show .gitignore file anyways') })
],
checkbox: {
label: trans.__('Do not show this warning again'),
checked: false
}
});
if (result.button.accept) {
settings.set('hideHiddenFileWarning', result.isChecked);
showGitignore(error);
}
}
/** Add git open gitignore command */
commands.addCommand(CommandIDs.gitOpenGitignore, {
label: trans.__('Open .gitignore'),
caption: trans.__('Open .gitignore'),
isEnabled: () => gitModel.pathRepository !== null,
execute: async () => {
try {
await gitModel.ensureGitignore();
}
catch (error) {
if ((error === null || error === void 0 ? void 0 : error.name) === 'hiddenFile') {
await showGitignoreHiddenFile(error, settings.composite['hideHiddenFileWarning']);
}
}
}
});
/** Add git push command */
commands.addCommand(CommandIDs.gitPush, {
label: args => args['advanced']
? trans.__('Push to Remote (Advanced)')
: trans.__('Push to Remote'),
caption: trans.__('Push code to remote repository'),
isEnabled: () => gitModel.pathRepository !== null,
execute: async (args) => {
let id = null;
try {
let remote;
let force;
if (args['advanced']) {
const result = await showDialog({
title: trans.__('Please select push options.'),
body: new AdvancedPushForm(trans, gitModel),
buttons: [
Dialog.cancelButton({ label: trans.__('Cancel') }),
Dialog.okButton({ label: trans.__('Proceed') })
]
});
if (result.button.accept && result.value) {
remote = result.value.remoteName;
force = result.value.force;
}
else {
return;
}
}
id = Notification.emit(trans.__('Pushing…'), 'in-progress', {
autoClose: false
});
const details = await showGitOperationDialog(gitModel, force ? Operation.ForcePush : Operation.Push, trans, (args = { remote }));
Notification.update({
id,
message: trans.__('Successfully pushed'),
type: 'success',
...showDetails(details, trans)
});
}
catch (error) {
if (error.name !== 'CancelledError') {
console.error(trans.__('Encountered an error when pushing changes. Error: '), error);
const message = trans.__('Failed to push');
const options = showError(error, trans);
if (id) {
Notification.update({
id,
message,
type: 'error',
...options
});
}
else {
Notification.error(message, options);
}
}
else {
if (id) {
Notification.dismiss(id);
}
}
}
}
});
/** Add git pull command */
commands.addCommand(CommandIDs.gitPull, {
label: args => args.force
? trans.__('Pull from Remote (Force)')
: trans.__('Pull from Remote'),
caption: args => args.force
? trans.__('Discard all current changes and pull from remote repository')
: trans.__('Pull latest code from remote repository'),
isEnabled: () => gitModel.pathRepository !== null,
execute: async (args) => {
let id = null;
try {
if (args.force) {
await discardAllChanges(gitModel, trans, args.fallback);
}
id = Notification.emit(trans.__('Pulling…'), 'in-progress', {
autoClose: false
});
const details = await showGitOperationDialog(gitModel, Operation.Pull, trans);
Notification.update({
id,
message: trans.__('Successfully pulled'),
type: 'success',
...showDetails(details, trans)
});
}
catch (error) {
if (error.name !== 'CancelledError') {
console.error('Encountered an error when pulling changes. Error: ', error);
const errorMsg = typeof error === 'string' ? error : error.message;
// Discard changes then retry pull
if (errorMsg
.toLowerCase()
.includes('your local changes to the following files would be overwritten by merge')) {
await commands.execute(CommandIDs.gitPull, {
force: true,
fallback: true
});
}
else {
if (error.cancelled) {
if (id) {
Notification.dismiss(id);
}
}
else {
const message = trans.__('Failed to pull');
const options = showError(error, trans);
if (id) {
Notification.update({
id,
message,
...options
});
}
else {
Notification.error(message, options);
}
}
}
}
else {
if (id) {
Notification.dismiss(id);
}
}
}
}
});
/** Add git reset --hard <remote-tracking-branch> command */
commands.addCommand(CommandIDs.gitResetToRemote, {
label: trans.__('Reset to Remote'),
caption: trans.__('Reset Current Branch to Remote State'),
isEnabled: () => gitModel.pathRepository !== null,
execute: async () => {
var _a, _b;
const result = await showDialog({
title: trans.__('Reset to Remote'),
body: new CheckboxForm(trans.__('To bring the current branch to the state of its corresponding remote tracking branch, \
a hard reset will be performed, which may result in some files being permanently deleted \
and some changes being permanently discarded. Are you sure you want to proceed? \
This action cannot be undone.'), trans.__('Close all opened files to avoid conflicts')),
buttons: [
Dialog.cancelButton({ label: trans.__('Cancel') }),
Dialog.warnButton({ label: trans.__('Proceed') })
]
});
if (result.button.accept) {
let id = null;
try {
if ((_a = result.value) === null || _a === void 0 ? void 0 : _a.checked) {
id = Notification.emit(trans.__('Closing all opened files...'), 'in-progress');
await fileBrowserModel.manager.closeAll();
}
const message = trans.__('Resetting...');
if (id) {
Notification.update({ id, message });
}
else {
id = Notification.emit(message, 'in-progress', {
autoClose: false
});
}
await gitModel.resetToCommit((_b = gitModel.status.remote) !== null && _b !== void 0 ? _b : undefined);
Notification.update({
id,
message: trans.__('Successfully reset'),
type: 'success',
...showDetails(trans.__('Successfully reset the current branch to its remote state'), trans)
});
}
catch (error) {
console.error('Encountered an error when resetting the current branch to its remote state. Error: ', error);
const message = trans.__('Reset failed');
const options = showError(error, trans);
if (id) {
Notification.update({
id,
type: 'error',
message,
...options
});
}
else {
Notification.error(message, options);
}
}
}
}
});
/**
* Git display diff command - internal command
*
* @params model: The diff model to display
* @params isText: Optional, whether the content is a plain text
* @params isMerge: Optional, whether the diff is a merge conflict
* @returns the main area widget or null
*/
commands.addCommand(CommandIDs.gitShowDiff, {
label: trans.__('Show Diff'),
caption: trans.__('Display a file diff.'),
execute: async (args) => {
var _a;
const { model, isText, isPreview } = args;
const fullPath = PathExt.join((_a = model.repositoryPath) !== null && _a !== void 0 ? _a : '/', model.filename);
const buildDiffWidget = getDiffProvider(fullPath, isText);
if (buildDiffWidget) {
const id = `git-diff-${fullPath}-${model.reference.label}-${model.challenger.label}`;
const mainAreaItems = shell.widgets('main');
let mainAreaItem = null;
for (const item of mainAreaItems) {
if (item.id === id) {
shell.activateById(id);
mainAreaItem = item;
break;
}
}
if (!mainAreaItem) {
const content = new Panel();
const modelIsLoading = new PromiseDelegate();
const diffWidget = (mainAreaItem = new PreviewMainAreaWidget({
content,
reveal: modelIsLoading.promise,
isPreview
}));
diffWidget.id = id;
diffWidget.title.label = PathExt.basename(model.filename);
diffWidget.title.caption = fullPath;
diffWidget.title.icon = diffIcon;
diffWidget.title.closable = true;
diffWidget.title.className = 'jp-git-diff-title';
diffWidget.addClass('jp-git-diff-parent-widget');
shell.add(diffWidget, 'main');
shell.activateById(diffWidget.id);
// Search for the tab
const dockPanel = app.shell._dockPanel;
// Get the index of the most recent tab opened
let tabPosition = -1;
const tabBar = Array.from(dockPanel.tabBars()).find(bar => {
tabPosition = bar.titles.indexOf(diffWidget.title);
return tabPosition !== -1;
});
// Pin the preview screen if applicable
if (tabBar) {
PreviewMainAreaWidget.pinWidget(tabPosition, tabBar, diffWidget);
}
// Create the diff widget
try {
const widget = await buildDiffWidget({
model,
toolbar: diffWidget.toolbar,
translator,
serverSettings
});
diffWidget.toolbar.addItem('spacer', Toolbar.createSpacerItem());
// Do not allow the user to refresh during merge conflicts
if (model.hasConflict) {
const resolveButton = new ToolbarButton({
label: trans.__('Mark as resolved'),
onClick: async () => {
var _a;
if (!widget.isFileResolved) {
const result = await showDialog({
title: trans.__('Resolve with conflicts'),
body: trans.__('Are you sure you want to mark this file as resolved with merge conflicts?')
});
// Bail early if the user wants to finish resolving conflicts
if (!result.button.accept) {
return;
}
}
try {
await serviceManager.contents.save(fullPath, await widget.getResolvedFile());
await gitModel.add(model.filename);
await gitModel.refresh();
}
catch (reason) {
Notification.error((_a = reason.message) !== null && _a !== void 0 ? _a : reason);
}
finally {
diffWidget.dispose();
}
},
tooltip: trans.__('Mark file as resolved'),
className: 'jp-git-diff-resolve'
});
diffWidget.toolbar.addItem('resolve', resolveButton);
}
else {
const refreshButton = new ToolbarButton({
label: trans.__('Refresh'),
onClick: async () => {
await widget.refresh();
refreshButton.hide();
},
tooltip: trans.__('Refresh diff widget'),
className: 'jp-git-diff-refresh'
});
refreshButton.hide();
diffWidget.toolbar.addItem('refresh', refreshButton);
const refresh = () => {
refreshButton.show();
};
model.changed.connect(refresh);
widget.disposed.connect(() => model.changed.disconnect(refresh));
}
// Load the diff widget
modelIsLoading.resolve();
content.addWidget(widget);
}
catch (reason) {
console.error(reason);
const msg = `Load Diff Model Error (${reason.message || reason})`;
modelIsLoading.reject(msg);
}
if (model.challenger.source === Git.Diff.SpecialRef.INDEX ||
model.challenger.source === Git.Diff.SpecialRef.WORKING ||
model.reference.source === Git.Diff.SpecialRef.INDEX ||
model.reference.source === Git.Diff.SpecialRef.WORKING) {
const maybeClose = (_, status) => {
const targetFile = status.files.find(fileStatus => model.filename === fileStatus.from);
if (!targetFile || targetFile.status === 'unmodified') {
gitModel.statusChanged.disconnect(maybeClose);
mainAreaItem.dispose();
}
};
gitModel.statusChanged.connect(maybeClose);
}
}
return mainAreaItem;
}
else {
await showErrorMessage(trans.__('Diff Not Supported'), trans.__('Diff is not supported for %1 files.', PathExt.extname(model.filename).toLocaleLowerCase()));
return null;
}
},
icon: diffIcon.bindprops({ stylesheet: 'menuItem' })
});
commands.addCommand(CommandIDs.gitMerge, {
label: trans.__('Merge Branch…'),
caption: trans.__('Merge selected branch in the current branch'),
execute: async (args) => {
var _a, _b, _c, _d, _e;
let { branch } = args !== null && args !== void 0 ? args : {};
if (!branch) {
// Prompts user to pick a branch
const localBranches = gitModel.branches.filter(branch => !branch.is_current_branch && !branch.is_remote_branch);
const widgetId = 'git-dialog-MergeBranch';
let anchor = document.querySelector(`#${widgetId}`);
if (!anchor) {
anchor = document.createElement('div');
anchor.id = widgetId;
document.body.appendChild(anchor);
}
const waitForDialog = new PromiseDelegate();
const dialog = ReactWidget.create(React.createElement(BranchPicker, { action: "merge", currentBranch: (_b = (_a = gitModel.currentBranch) === null || _a === void 0 ? void 0 : _a.name) !== null && _b !== void 0 ? _b : '', branches: localBranches, onClose: (branch) => {
dialog.dispose();
waitForDialog.resolve(branch !== null && branch !== void 0 ? branch : null);
}, trans: trans }));
Widget.attach(dialog, anchor);
branch = (_c = (await waitForDialog.promise)) !== null && _c !== void 0 ? _c : undefined;
}
if (branch) {
const id = Notification.emit(trans.__("Merging branch '%1'…", branch), 'in-progress');
try {
await gitModel.merge(branch);
}
catch (err) {
Notification.update({
id,
type: 'error',
message: trans.__("Failed to merge branch '%1' into '%2'.", branch, (_d = gitModel.currentBranch) === null || _d === void 0 ? void 0 : _d.name),
...showError(err, trans)
});
return;
}
Notification.update({
id,
type: 'success',
message: trans.__("Branch '%1' merged into '%2'.", branch, (_e = gitModel.currentBranch) === null || _e === void 0 ? void 0 : _e.name)
});
}
},
isEnabled: () => gitModel.branches.some(branch => !branch.is_current_branch && !branch.is_remote_branch)
});
commands.addCommand(CommandIDs.gitRebase, {
label: trans.__('Rebase branch…'),
caption: trans.__('Rebase current branch onto the selected branch'),
execute: async (args) => {
var _a, _b, _c, _d, _e;
let { branch } = args !== null && args !== void 0 ? args : {};
if (!branch) {
// Prompts user to pick a branch
const localBranches = gitModel.branches.filter(branch => !branch.is_current_branch && !branch.is_remote_branch);
const widgetId = 'git-dialog-MergeBranch';
let anchor = document.querySelector(`#${widgetId}`);
if (!anchor) {
anchor = document.createElement('div');
anchor.id = widgetId;
document.body.appendChild(anchor);
}
const waitForDialog = new PromiseDelegate();
const dialog = ReactWidget.create(React.createElement(BranchPicker, { action: "rebase", currentBranch: (_b = (_a = gitModel.currentBranch) === null || _a === void 0 ? void 0 : _a.name) !== null && _b !== void 0 ? _b : '', branches: localBranches, onClose: (branch) => {
dialog.dispose();
waitForDialog.resolve(branch !== null && branch !== void 0 ? branch : null);
}, trans: trans }));
Widget.attach(dialog, anchor);
branch = (_c = (await waitForDialog.promise)) !== null && _c !== void 0 ? _c : undefined;
}
if (branch) {
const id = Notification.emit(trans.__("Rebasing current branch onto '%1'…", branch), 'in-progress');
try {
await gitModel.rebase(branch);
}
catch (err) {
Notification.update({
id,
type: 'error',
message: trans.__("Failed to rebase branch '%1' onto '%2'.", (_d = gitModel.currentBranch) === null || _d === void 0 ? void 0 : _d.name, branch),
...showError(err, trans)
});
return;
}
Notification.update({
id,
type: 'success',
message: trans.__("Branch '%1' rebase onto '%2'.", (_e = gitModel.currentBranch) === null || _e === void 0 ? void 0 : _e.name, branch)
});
}
},
isEnabled: () => gitModel.branches.some(branch => !branch.is_current_branch && !branch.is_remote_branch)
});
commands.addCommand(CommandIDs.gitResolveRebase, {
label: (args = {}) => {
switch (args.action) {
case 'continue':
return trans.__('Continue rebase');
case 'skip':
return trans.__('Skip current commit');
case 'abort':
return trans.__('Abort rebase');
default:
return trans.__('Resolve rebase');
}
},
caption: (args = {}) => {
switch (args.action) {
case 'continue':
return trans.__('Continue the rebase by committing the current state.');
case 'skip':
return trans.__('Skip current commit and continue the rebase.');
case 'abort':
return trans.__('Abort the rebase');
default:
return trans.__('Resolve rebase');
}
},
execute: async (args = {}) => {
var _a, _b, _c;
const { action } = args;
if (['continue', 'abort', 'skip'].includes(action !== null && action !== void 0 ? action : '')) {
const message = (_a = (action => {
switch (action) {
case 'continue':
return trans.__('Continue the rebase…');
case 'skip':
return trans.__('Skip current commit…');
case 'abort':
return trans.__('Abort the rebase…');
}
})(action)) !== null && _a !== void 0 ? _a : '';
const id = Notification.emit(message, 'in-progress', {
autoClose: false
});
try {
await gitModel.resolveRebase(action);
}
catch (err) {
const message = (_b = (action => {
switch (action) {
case 'continue':
return trans.__('Fail to continue rebasing.');
case 'skip':
return trans.__('Fail to skip current commit when rebasing.');
case 'abort':
return trans.__('Fail to abort the rebase.');
}
})(action)) !== null && _b !== void 0 ? _b : '';
Notification.update({
id,
type: 'error',
message,
...showError(err, trans)
});
return;
}
const message_ = (_c = (action => {
switch (action) {
case 'continue':
return trans.__('Commit submitted continuing rebase.');
case 'skip':
return trans.__('Current commit skipped.');
case 'abort':
return trans.__('Rebase aborted.');
}
})(action)) !== null && _c !== void 0 ? _c : '';
Notification.update({
id,
type: 'success',
message: message_,
autoClose: 5000
});
}
},
isEnabled: () => gitModel.status.state === Git.State.REBASING
});
commands.addCommand(CommandIDs.gitStash, {
label: trans.__('Stash Changes'),
caption: trans.__('Stash all current changes'),
isEnabled: () => gitModel.pathRepository !== null,
execute: async (args) => {
var _a;
const stashDialog = await InputDialog.getText({
// Default stash message is the last commit hash and message
title: trans.__('Do you want to stash your changes? '),
placeholder: trans.__('Stash message (optional)'),
okLabel: trans.__('Stash')
});
const stashMsg = (_a = stashDialog.value) !== null && _a !== void 0 ? _a : '';
if (stashDialog.button.accept) {
const id = Notification.emit(trans.__('Stashing changes'), 'in-progress', { autoClose: false });
try {
await gitModel.stashChanges(stashMsg);
// Success
Notification.update({
id,
message: trans.__('Successfully stashed'),
type: 'success',
autoClose: 5000
});
}
catch (error) {
console.error('Encountered an error when pulling changes. Error: ', error);
Notification.update({
id,
message: trans.__('Failed to stash'),
type: 'error',
...showError(error, trans)
});
}
}
}
});
/**
* Calls refreshStash
*
*/
commands.addCommand(CommandIDs.gitStashList, {
label: trans.__('Stash List'),
caption: trans.__('Get all the stashed changes'),
// Check if we are in a git repository
isEnabled: () => gitModel.pathRepository !== null,
execute: async (args) => {
try {
await gitModel.refreshStash();
Notification.info(trans.__('Got the stash list'));
}
catch (err) {
Notification.error(trans.__('Failed to get the stash'), showError(err, trans));
}
}
});
/* Context menu commands */
commands.addCommand(ContextCommandIDs.gitFileOpen, {
label: trans.__('Open'),
caption: pluralizedContextLabel(trans.__('Open selected file'), trans.__('Open selected files')),
execute: async (args) => {
const { files } = args;
for (const file of files) {
const { x, y, to } = file;
if (x === 'D' || y === 'D') {
await showErrorMessage(trans.__('Open File Failed'), trans.__('This file has been deleted!'));
return;
}
try {
if (to[to.length - 1] !== '/') {
commands.execute('docmanager:open', {
path: gitModel.getRelativeFilePath(to)
});
}
else {
console.log('Cannot open a folder here');
}
}
catch (_err) {
console.error(`Fail to open ${to}.`);
}
}
},
icon: openIcon.bindprops({ stylesheet: 'menuItem' })
});
commands.addCommand(ContextCommandIDs.openFileFromDiff, {
label: trans.__('Open File'),
caption: trans.__('Open file from its diff view'),
execute: async (_) => {
const domNode = app.contextMenuHitTest((node) => {
const nodeId = node.dataset.id;
return (nodeId === null || nodeId === void 0 ? void 0 : nodeId.substring(0, 8)) === 'git-diff';
});
if (!domNode) {
return;
}
const matches = Array.from(shell.widgets('main')).filter(widget => widget.id === domNode.dataset.id);
if (matches.length === 0) {
return;
}
const diffModel = matches[0].content
.widgets[0].model;
const filename = diffModel.filename;
if (diffModel.reference.source === Git.Diff.SpecialRef.INDEX ||
diffModel.reference.source === Git.Diff.SpecialRef.WORKING ||
diffModel.challenger.source === Git.Diff.SpecialRef.INDEX ||
diffModel.challenger.source === Git.Diff.SpecialRef.WORKING) {
const file = gitModel.status.files.find(fileStatus => fileStatus.from === filename);
if (file) {
commands.execute(ContextCommandIDs.gitFileOpen, {
files: [file]
});
}
}
else {
commands.execute('docmanager:open', {
path: gitModel.getRelativeFilePath(filename)
});
}
}
});
commands.addCommand(ContextCommandIDs.gitFileDiff, {
label: trans.__('Diff'),
caption: pluralizedContextLabel(trans.__('Diff selected file'), trans.__('Diff selected files')),
execute: async (args) => {
const { files } = args;
if (gitModel.pathRepository === null) {
return;
}
for (const file of files) {
const { context, filePath, previousFilePath, isText, status, isPreview } = file;
// nothing to compare to for untracked files
if (status === 'untracked') {
continue;
}
const repositoryPath = gitModel.pathRepository;
const filename = filePath;
const fullPath = PathExt.join(repositoryPath, filename);
const diffContext = {
currentRef: '',
previousRef: 'HEAD',
...context
};
if (status === 'unmerged') {
diffContext.baseRef = Git.Diff.SpecialRef.BASE;
diffContext.currentRef =
gitModel.status.state !== Git.State.MERGING
? gitModel.status.state === Git.State.REBASING
? 'REBASE_HEAD'
: 'CHERRY_PICK_HEAD'
: 'MERGE_HEAD';
}
else if (!diffContext.currentRef) {
diffContext.currentRef =
status === 'staged'
? Git.Diff.SpecialRef.INDEX
: Git.Diff.SpecialRef.WORKING;
}
const challengerRef = Git.Diff.SpecialRef[diffContext.currentRef]
? { special: Git.Diff.SpecialRef[diffContext.currentRef] }
: { git: diffContext.currentRef };
// Base props used for Diff Model
const props = {
challenger: {
content: async () => {
return requestAPI(URLExt.join(repositoryPath, 'content'), 'POST', {
filename,
// @ts-expect-error this is serializable
reference: challengerRef
}, 'git', serverSettings).then(data => data.content);
},
label: Git.Diff.SpecialRef[diffContext.currentRef] ||
diffContext.currentRef,
source: diffContext.currentRef,
updateAt: Date.now()
},
filename,
reference: {
content: async () => {
return requestAPI(URLExt.join(repositoryPath, 'content'), 'POST', {
filename: previousFilePath !== null && previousFilePath !== void 0 ? previousFilePath : filename,
reference: { git: diffContext.previousRef }
}, 'git', serverSettings).then(data => data.content);
},
label: Git.Diff.SpecialRef[diffContext.previousRef] ||
diffContext.previousRef,
source: diffContext.previousRef,
updateAt: Date.now()
},
repositoryPath
};
// Case when file is relocated
if (previousFilePath) {
props.reference.label = `${previousFilePath} (${props.reference.label.slice(0, 7)})`;
props.challenger.label = `${filePath} (${props.challenger.label.slice(0, 7)})`;
}
if (diffContext.baseRef) {
props.reference.label = trans.__('Current');
props.challenger.label = trans.__('Incoming');
// Only add base when diff-ing merge conflicts
props.base = {
content: async () => {
return requestAPI(URLExt.join(repositoryPath, 'content'), 'POST', {
filename,
reference: {
special: Git.Diff.SpecialRef[diffContext.baseRef]
}
}, 'git', serverSettings).then(data => data.content);
},
label: trans.__('Result'),
source: diffContext.baseRef,
updateAt: Date.now()
};
}
// Create the diff widget
const model = new DiffModel(props);
const widget = await commands.execute(CommandIDs.gitShowDiff, {
model,
isText,
isPreview
});
if (widget) {
// Trigger diff model update
if (diffContext.previousRef === 'HEAD') {
const updateHead = () => {
model.reference = {
...model.reference,
updateAt: Date.now()
};
};
gitModel.headChanged.connect(updateHead);
widget.disposed.connect(() => {
gitModel.headChanged.disconnect(updateHead);
});
}
// If the diff is on the current file and it is updated => diff model changed
if (diffContext.currentRef === Git.Diff.SpecialRef.WORKING) {
const updateCurrent = (m, change) => {
var _a, _b, _c, _d;
const updateAt = new Date((_b = (_a = change.newValue) === null || _a === void 0 ? void 0 : _a.last_modified) !== null && _b !== void 0 ? _b : 0).valueOf();
if (app.serviceManager.contents.localPath((_d = (_c = change.newValue) === null || _c === void 0 ? void 0 : _c.path) !== null && _d !== void 0 ? _d : '') === fullPath &&
model.challenger.updateAt !== updateAt) {
model.challenger = {
...model.challenger,
updateAt
};
}
};
// More robust than fileBrowser.model.fileChanged
app.serviceManager.contents.fileChanged.connect(updateCurrent);
widget.disposed.connect(() => {
app.serviceManager.contents.fileChanged.disconnect(updateCurrent);
});
}
}
}
},
icon: diffIcon.bindprops({ stylesheet: 'menuItem' })
});
commands.addCommand(ContextCommandIDs.gitFileAdd, {
label: trans.__('Add'),
caption: pluralizedContextLabel(trans.__('Stage or track the changes to selected file'), trans.__('Stage or track the changes of selected files')),
execute: async (args) => {
const { files } = args;
for (const