Browse Source

Adds switch to text option to rebase editor

main
Eric Amodio 4 years ago
parent
commit
ae03319ab1
17 changed files with 650 additions and 329 deletions
  1. +2
    -0
      CHANGELOG.md
  2. +6
    -1
      package.json
  3. +1
    -1
      src/commands/git/search.ts
  4. +1
    -0
      src/config.ts
  5. +1
    -0
      src/constants.ts
  6. +12
    -7
      src/git/gitService.ts
  7. +5
    -3
      src/git/models/rebase.ts
  8. +38
    -2
      src/git/models/repository.ts
  9. +9
    -0
      src/messages.ts
  10. +3
    -3
      src/views/nodes/mergeConflictIncomingChangesNode.ts
  11. +33
    -17
      src/views/nodes/rebaseStatusNode.ts
  12. +26
    -17
      src/webviews/apps/rebase/rebase.html
  13. +54
    -33
      src/webviews/apps/rebase/rebase.ts
  14. +39
    -0
      src/webviews/apps/scss/codicons.scss
  15. +117
    -72
      src/webviews/apps/scss/rebase.scss
  16. +6
    -4
      src/webviews/protocol.ts
  17. +297
    -169
      src/webviews/rebaseEditor.ts

+ 2
- 0
CHANGELOG.md View File

@ -14,9 +14,11 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
- Adds _Repository from Before Here_ and _Repository from Before Here in New Window_ to the _Browse_ submenu of commits in the views
- Adds a new _Copy Current Branch Name_ (`gitlens.copyCurrentBranch`) command to copy the current branch name to the clipboard — closes [#1306](https://github.com/eamodio/vscode-gitlens/issues/1306) — thanks to [PR #1307](https://github.com/eamodio/vscode-gitlens/pull/1307) by Ken Hom ([@kh0m](https://github.com/kh0m))
- Adds a `gitlens.advanced.abbreviateShaOnCopy` setting to specify to whether to copy full or abbreviated commit SHAs to the clipboard. Abbreviates to the length of `gitlens.advanced.abbreviatedShaLength` — closes [#1062](https://github.com/eamodio/vscode-gitlens/issues/1062) — thanks to [PR #1316](https://github.com/eamodio/vscode-gitlens/pull/1316) by Brendon Smith ([@br3ndonland](https://github.com/br3ndonland))
- Adds a _Switch to Text_ button on the _Interactive Rebase Editor_ to open the text rebase todo file — note that closing either document will start the rebase
### Changed
- Changes the _Interactive Rebase Editor_ to abort the rebase if you just close it without choosing an action
- Changes _Push to Commit..._ on the HEAD commit to be _Push_ instead as there is no need for a commit specific push in that case
- Renames _Browse from Here_ command to _Browse Repository from Here_ in the command palette and quick pick menus
- Renames _Browse from Here in New Window_ command to _Browse Repository from Here in New Window_ in the command palette and quick pick menus

+ 6
- 1
package.json View File

@ -2414,7 +2414,8 @@
"suppressGitDisabledWarning": false,
"suppressGitVersionWarning": false,
"suppressLineUncommittedWarning": false,
"suppressNoRepositoryWarning": false
"suppressNoRepositoryWarning": false,
"suppressRebaseSwitchToTextWarning": false
},
"properties": {
"suppressCommitHasNoPreviousCommitWarning": {
@ -2444,6 +2445,10 @@
"suppressNoRepositoryWarning": {
"type": "boolean",
"default": false
},
"suppressRebaseSwitchToTextWarning": {
"type": "boolean",
"default": false
}
},
"markdownDescription": "Specifies which messages should be suppressed",

+ 1
- 1
src/commands/git/search.ts View File

@ -160,7 +160,7 @@ export class SearchGitCommand extends QuickCommand {
const searchKey = SearchPattern.toKey(search);
if (context.resultsPromise == null || context.resultsKey !== searchKey) {
context.resultsPromise = Container.git.getLogForSearch(state.repo.path, search);
context.resultsPromise = state.repo.searchForCommits(search);
context.resultsKey = searchKey;
}

+ 1
- 0
src/config.ts View File

@ -299,6 +299,7 @@ export interface AdvancedConfig {
suppressGitVersionWarning: boolean;
suppressLineUncommittedWarning: boolean;
suppressNoRepositoryWarning: boolean;
suppressRebaseSwitchToTextWarning: boolean;
};
quickPick: {
closeOnFocusOut: boolean;

+ 1
- 0
src/constants.ts View File

@ -17,6 +17,7 @@ export enum BuiltInCommands {
Open = 'vscode.open',
OpenFolder = 'vscode.openFolder',
OpenInTerminal = 'openInTerminal',
OpenWith = 'vscode.openWith',
NextEditor = 'workbench.action.nextEditor',
PreviewHtml = 'vscode.previewHtml',
RevealLine = 'revealLine',

+ 12
- 7
src/git/gitService.ts View File

@ -2450,7 +2450,7 @@ export class GitService implements Disposable {
const rebase = await Git.rev_parse__verify(repoPath, 'REBASE_HEAD');
if (rebase != null) {
// eslint-disable-next-line prefer-const
let [mergeBase, branch, onto, step, stepMessage, steps] = await Promise.all([
let [mergeBase, branch, onto, stepsNumber, stepsMessage, stepsTotal] = await Promise.all([
this.getMergeBase(repoPath, 'REBASE_HEAD', 'HEAD'),
Git.readDotGitFile(repoPath, ['rebase-merge', 'head-name']),
Git.readDotGitFile(repoPath, ['rebase-merge', 'onto']),
@ -2482,6 +2482,7 @@ export class GitService implements Disposable {
repoPath: repoPath,
mergeBase: mergeBase,
HEAD: GitReference.create(rebase, repoPath, { refType: 'revision' }),
onto: GitReference.create(onto, repoPath, { refType: 'revision' }),
current:
possibleSourceBranch != null
? GitReference.create(possibleSourceBranch, repoPath, {
@ -2496,12 +2497,16 @@ export class GitService implements Disposable {
name: branch,
remote: false,
}),
step: step,
stepCurrent: GitReference.create(rebase, repoPath, {
refType: 'revision',
message: stepMessage,
}),
steps: steps,
steps: {
current: {
number: stepsNumber ?? 0,
commit: GitReference.create(rebase, repoPath, {
refType: 'revision',
message: stepsMessage,
}),
},
total: stepsTotal ?? 0,
},
};
}

+ 5
- 3
src/git/models/rebase.ts View File

@ -5,11 +5,13 @@ export interface GitRebaseStatus {
type: 'rebase';
repoPath: string;
HEAD: GitRevisionReference;
onto: GitRevisionReference;
mergeBase: string | undefined;
current: GitBranchReference | undefined;
incoming: GitBranchReference;
step: number | undefined;
stepCurrent: GitRevisionReference;
steps: number | undefined;
steps: {
current: { number: number; commit: GitRevisionReference };
total: number;
};
}

+ 38
- 2
src/git/models/repository.ts View File

@ -16,12 +16,29 @@ import {
import { BranchSorting, configuration, TagSorting } from '../../configuration';
import { Starred, WorkspaceState } from '../../constants';
import { Container } from '../../container';
import { GitBranch, GitContributor, GitDiffShortStat, GitRemote, GitStash, GitStatus, GitTag } from '../git';
import {
GitBranch,
GitContributor,
GitDiffShortStat,
GitRemote,
GitStash,
GitStatus,
GitTag,
SearchPattern,
} from '../git';
import { GitService } from '../gitService';
import { GitUri } from '../gitUri';
import { Logger } from '../../logger';
import { Messages } from '../../messages';
import { GitBranchReference, GitReference, GitTagReference } from './models';
import {
GitBranchReference,
GitLog,
GitLogCommit,
GitMergeStatus,
GitRebaseStatus,
GitReference,
GitTagReference,
} from './models';
import { RemoteProviderFactory, RemoteProviders, RichRemoteProvider } from '../remotes/factory';
import { Arrays, Dates, debug, Functions, gate, Iterables, log, logName } from '../../system';
import { runGitCommandInTerminal } from '../../terminal';
@ -479,6 +496,10 @@ export class Repository implements Disposable {
return Container.git.getChangedFilesCount(this.path, sha);
}
getCommit(ref: string): Promise<GitLogCommit | undefined> {
return Container.git.getCommit(this.path, ref);
}
getContributors(): Promise<GitContributor[]> {
return Container.git.getContributors(this.path);
}
@ -504,6 +525,14 @@ export class Repository implements Disposable {
return this._lastFetched ?? 0;
}
getMergeStatus(): Promise<GitMergeStatus | undefined> {
return Container.git.getMergeStatus(this.path);
}
getRebaseStatus(): Promise<GitRebaseStatus | undefined> {
return Container.git.getRebaseStatus(this.path);
}
getRemotes(_options: { sort?: boolean } = {}): Promise<GitRemote[]> {
if (this._remotes == null || !this.supportsChangeEvents) {
if (this._providers == null) {
@ -718,6 +747,13 @@ export class Repository implements Disposable {
this.runTerminalCommand('revert', ...args);
}
searchForCommits(
search: SearchPattern,
options: { limit?: number; skip?: number } = {},
): Promise<GitLog | undefined> {
return Container.git.getLogForSearch(this.path, search, options);
}
get starred() {
const starred = Container.context.workspaceState.get<Starred>(WorkspaceState.StarredRepositories);
return starred != null && starred[this.id] === true;

+ 9
- 0
src/messages.ts View File

@ -12,6 +12,7 @@ export enum SuppressedMessages {
GitVersionWarning = 'suppressGitVersionWarning',
LineUncommittedWarning = 'suppressLineUncommittedWarning',
NoRepositoryWarning = 'suppressNoRepositoryWarning',
RebaseSwitchToTextWarning = 'suppressRebaseSwitchToTextWarning',
}
export class Messages {
@ -102,6 +103,14 @@ export class Messages {
);
}
static showRebaseSwitchToTextWarningMessage(): Promise<MessageItem | undefined> {
return Messages.showMessage(
'warn',
'Closing either the git-rebase-todo file or the Rebase Editor will start the rebase.',
SuppressedMessages.RebaseSwitchToTextWarning,
);
}
static async showWhatsNewMessage(version: string) {
const actions: MessageItem[] = [{ title: "What's New" }, { title: '❤ Sponsor' }];

+ 3
- 3
src/views/nodes/mergeConflictIncomingChangesNode.ts View File

@ -27,14 +27,14 @@ export class MergeConflictIncomingChangesNode extends ViewNode
async getTreeItem(): Promise<TreeItem> {
const commit = await Container.git.getCommit(
this.status.repoPath,
this.status.type === 'rebase' ? this.status.stepCurrent.ref : this.status.HEAD.ref,
this.status.type === 'rebase' ? this.status.steps.current.commit.ref : this.status.HEAD.ref,
);
const item = new TreeItem('Incoming changes', TreeItemCollapsibleState.None);
item.contextValue = ContextValues.MergeConflictIncomingChanges;
item.description = `${GitReference.toString(this.status.incoming, { expand: false, icon: false })}${
this.status.type === 'rebase'
? ` (${GitReference.toString(this.status.stepCurrent, { expand: false, icon: false })})`
? ` (${GitReference.toString(this.status.steps.current.commit, { expand: false, icon: false })})`
: ` (${GitReference.toString(this.status.HEAD, { expand: false, icon: false })})`
}`;
item.iconPath = this.view.config.avatars
@ -58,7 +58,7 @@ export class MergeConflictIncomingChangesNode extends ViewNode
},
)}`
: this.status.type === 'rebase'
? `\n\n${GitReference.toString(this.status.stepCurrent, {
? `\n\n${GitReference.toString(this.status.steps.current.commit, {
capitalize: true,
label: false,
})}`

+ 33
- 17
src/views/nodes/rebaseStatusNode.ts View File

@ -1,8 +1,21 @@
'use strict';
import * as paths from 'path';
import { Command, MarkdownString, ThemeColor, ThemeIcon, TreeItem, TreeItemCollapsibleState } from 'vscode';
import {
Command,
commands,
MarkdownString,
ThemeColor,
ThemeIcon,
TreeItem,
TreeItemCollapsibleState,
Uri,
} from 'vscode';
import { BranchNode } from './branchNode';
import { Commands, DiffWithPreviousCommandArgs } from '../../commands';
import { CommitFileNode } from './commitFileNode';
import { ViewFilesLayout } from '../../configuration';
import { BuiltInCommands, GlyphChars } from '../../constants';
import { Container } from '../../container';
import { FileNode, FolderNode } from './folderNode';
import {
CommitFormatter,
@ -18,10 +31,6 @@ import { MergeConflictFileNode } from './mergeConflictFileNode';
import { Arrays, Strings } from '../../system';
import { ViewsWithCommits } from '../viewBase';
import { ContextValues, ViewNode, ViewRefNode } from './viewNode';
import { Container } from '../../container';
import { GlyphChars } from '../../constants';
import { CommitFileNode } from './commitFileNode';
import { Commands, DiffWithPreviousCommandArgs } from '../../commands';
export class RebaseStatusNode extends ViewNode<ViewsWithCommits> {
static key = ':rebase';
@ -69,7 +78,10 @@ export class RebaseStatusNode extends ViewNode {
);
}
const commit = await Container.git.getCommit(this.rebaseStatus.repoPath, this.rebaseStatus.stepCurrent.ref);
const commit = await Container.git.getCommit(
this.rebaseStatus.repoPath,
this.rebaseStatus.steps.current.commit.ref,
);
if (commit != null) {
children.splice(0, 0, new RebaseCommitNode(this.view, this, commit) as any);
}
@ -83,11 +95,7 @@ export class RebaseStatusNode extends ViewNode {
this.rebaseStatus.incoming != null
? `${GitReference.toString(this.rebaseStatus.incoming, { expand: false, icon: false })}`
: ''
}${
this.rebaseStatus.step != null && this.rebaseStatus.steps != null
? ` (${this.rebaseStatus.step}/${this.rebaseStatus.steps})`
: ''
}`,
} (${this.rebaseStatus.steps.current.number}/${this.rebaseStatus.steps.total})`,
TreeItemCollapsibleState.Expanded,
);
item.id = this.id;
@ -101,11 +109,12 @@ export class RebaseStatusNode extends ViewNode {
item.tooltip = new MarkdownString(
`${`Rebasing ${
this.rebaseStatus.incoming != null ? GitReference.toString(this.rebaseStatus.incoming) : ''
}onto ${GitReference.toString(this.rebaseStatus.current)}`}${
this.rebaseStatus.step != null && this.rebaseStatus.steps != null
? `\n\nStep ${this.rebaseStatus.step} of ${this.rebaseStatus.steps}\\\n`
: '\n\n'
}Stopped at ${GitReference.toString(this.rebaseStatus.stepCurrent, { icon: true })}${
}onto ${GitReference.toString(this.rebaseStatus.current)}`}\n\nStep ${
this.rebaseStatus.steps.current.number
} of ${this.rebaseStatus.steps.total}\\\nPaused at ${GitReference.toString(
this.rebaseStatus.steps.current.commit,
{ icon: true },
)}${
this.status?.hasConflicts
? `\n\n${Strings.pluralize('conflicted file', this.status.conflicts.length)}`
: ''
@ -115,6 +124,13 @@ export class RebaseStatusNode extends ViewNode {
return item;
}
async openEditor() {
const rebaseTodoUri = Uri.joinPath(this.uri, '.git', 'rebase-merge', 'git-rebase-todo');
await commands.executeCommand(BuiltInCommands.OpenWith, rebaseTodoUri, 'gitlens.rebase', {
preview: false,
});
}
}
export class RebaseCommitNode extends ViewRefNode<ViewsWithCommits, GitRevisionReference> {
@ -180,7 +196,7 @@ export class RebaseCommitNode extends ViewRefNode
}
getTreeItem(): TreeItem {
const item = new TreeItem(`Stopped at commit ${this.commit.shortSha}`, TreeItemCollapsibleState.Collapsed);
const item = new TreeItem(`Paused at commit ${this.commit.shortSha}`, TreeItemCollapsibleState.Collapsed);
// item.contextValue = ContextValues.RebaseCommit;

+ 26
- 17
src/webviews/apps/rebase/rebase.html View File

@ -29,23 +29,32 @@
<span class="shortcut"><kbd>alt ↓</kbd><span>Move Down</span></span>
</div>
<div class="actions">
<button name="disable" class="button button--flat-subtle" data-action="disable" tabindex="-1">
Disable Rebase Editor<span class="shortcut">Will Abort Rebase</span>
</button>
<!-- <div class="snow__trigger-container">
<img
class="snow__trigger snow__trigger--centered"
title="Let it snow — Happy Holidays!"
alt="Let it snow — Happy Holidays!"
src="#{root}/images/snowman.png"
/>
</div> -->
<button name="start" class="button button--flat-primary button--right" data-action="start">
Start Rebase <span class="shortcut">Ctrl+Enter</span>
</button>
<button name="abort" class="button button--flat-secondary" data-action="abort">
Abort <span class="shortcut">Ctrl+A</span>
</button>
<div class="actions--left">
<button name="disable" class="button button--flat-subtle" data-action="disable" tabindex="-1">
Disable Rebase Editor<span class="shortcut">Will Abort Rebase</span>
</button>
<button name="switch" class="button button--flat-subtle" data-action="switch" tabindex="-1">
Switch to Text
</button>
<!--
<div class="snow__trigger-container">
<img
class="snow__trigger snow__trigger--centered"
title="Let it snow — Happy Holidays!"
alt="Let it snow — Happy Holidays!"
src="#{root}/images/snowman.png"
/>
</div>
-->
</div>
<div class="actions--right">
<button name="start" class="button button--flat-primary" data-action="start">
Start Rebase <span class="shortcut">Ctrl+Enter</span>
</button>
<button name="abort" class="button button--flat-secondary" data-action="abort">
Abort <span class="shortcut">Ctrl+A</span>
</button>
</div>
</div>
</div>
#{endOfBody}

+ 54
- 33
src/webviews/apps/rebase/rebase.ts View File

@ -10,6 +10,7 @@ import {
RebaseDidDisableCommandType,
RebaseDidMoveEntryCommandType,
RebaseDidStartCommandType,
RebaseDidSwitchCommandType,
RebaseEntry,
RebaseEntryAction,
RebaseState,
@ -127,6 +128,7 @@ class RebaseEditor extends App {
DOM.on('[data-action="start"]', 'click', () => this.onStartClicked()),
DOM.on('[data-action="abort"]', 'click', () => this.onAbortClicked()),
DOM.on('[data-action="disable"]', 'click', () => this.onDisableClicked()),
DOM.on('[data-action="switch"]', 'click', () => this.onSwitchClicked()),
DOM.on('li[data-ref]', 'keydown', function (this: Element, e: KeyboardEvent) {
if ((e.target as HTMLElement).matches('select[data-ref]')) {
if (e.key === 'Escape') {
@ -181,7 +183,7 @@ class RebaseEditor extends App {
const $select = (this as HTMLLIElement).querySelectorAll<HTMLSelectElement>(
'select[data-ref]',
)[0];
if ($select != null) {
if ($select != null && !$select.disabled) {
$select.value = action;
me.onSelectChanged($select);
}
@ -206,7 +208,7 @@ class RebaseEditor extends App {
private moveEntry(ref: string, index: number, relative: boolean) {
const entry = this.getEntry(ref);
if (entry !== undefined) {
if (entry != null) {
this.sendCommand(RebaseDidMoveEntryCommandType, {
ref: entry.ref,
to: index,
@ -217,7 +219,7 @@ class RebaseEditor extends App {
private setEntryAction(ref: string, action: RebaseEntryAction) {
const entry = this.getEntry(ref);
if (entry !== undefined) {
if (entry != null) {
if (entry.action === action) return;
this.sendCommand(RebaseDidChangeEntryCommandType, {
@ -246,13 +248,17 @@ class RebaseEditor extends App {
this.sendCommand(RebaseDidStartCommandType, {});
}
private onSwitchClicked() {
this.sendCommand(RebaseDidSwitchCommandType, {});
}
protected onMessageReceived(e: MessageEvent) {
const msg = e.data;
switch (msg.method) {
case RebaseDidChangeNotificationType.method:
onIpcNotification(RebaseDidChangeNotificationType, msg, params => {
this.setState({ ...this.state, ...params });
this.setState({ ...this.state, ...params.state });
this.refresh(this.state);
});
break;
@ -263,22 +269,37 @@ class RebaseEditor extends App {
}
private refresh(state: RebaseState) {
const $subhead = document.getElementById('subhead')! as HTMLHeadingElement;
$subhead.innerHTML = `<span class="branch ml-1 mr-1">${state.branch}</span><span>Rebasing ${
state.entries.length
} commit${state.entries.length !== 1 ? 's' : ''}${
state.onto ? ` onto <span class="commit">${state.onto}</span>` : ''
}</span>`;
const $container = document.getElementById('entries')!;
const focusRef = document.activeElement?.closest<HTMLLIElement>('li[data-ref]')?.dataset.ref;
let focusSelect = false;
if (document.activeElement?.matches('select[data-ref]')) {
focusSelect = true;
}
const $subhead = document.getElementById('subhead')! as HTMLHeadingElement;
$subhead.innerHTML = '';
let $el: HTMLElement | Text = document.createElement('span');
$el.textContent = state.branch;
$el.classList.add('icon--branch', 'mr-1');
$subhead.appendChild($el);
$el = document.createTextNode(
`Rebasing ${state.entries.length} commit${state.entries.length !== 1 ? 's' : ''}${
state.onto ? ' onto' : ''
}`,
);
$subhead.appendChild($el);
if (state.onto) {
$el = document.createElement('span');
$el.textContent = state.onto;
$el.classList.add('icon--commit');
$subhead.appendChild($el);
}
const $container = document.getElementById('entries')!;
$container.innerHTML = '';
if (state.entries.length === 0) {
$container.classList.add('entries--empty');
@ -290,7 +311,7 @@ class RebaseEditor extends App {
const $entry = document.createElement('li');
const $el = document.createElement('h3');
$el.innerText = 'No commits to rebase';
$el.textContent = 'No commits to rebase';
$entry.appendChild($el);
$container.appendChild($entry);
@ -314,12 +335,7 @@ class RebaseEditor extends App {
}
let $el: HTMLLIElement;
[$el, tabIndex] = this.createEntry(entry, state, ++tabIndex);
if (squashToHere) {
$el.classList.add('entry--squash-to');
}
[$el, tabIndex] = this.createEntry(entry, state, ++tabIndex, squashToHere);
$container.appendChild($el);
}
@ -335,6 +351,7 @@ class RebaseEditor extends App {
},
state,
++tabIndex,
false,
);
$container.appendChild($el);
$container.classList.add('entries--base');
@ -350,9 +367,15 @@ class RebaseEditor extends App {
this.bind();
}
private createEntry(entry: RebaseEntry, state: RebaseState, tabIndex: number): [HTMLLIElement, number] {
private createEntry(
entry: RebaseEntry,
state: RebaseState,
tabIndex: number,
squashToHere: boolean,
): [HTMLLIElement, number] {
const $entry = document.createElement('li');
$entry.classList.add('entry', `entry--${entry.action ?? 'base'}`);
$entry.classList.toggle('entry--squash-to', squashToHere);
$entry.dataset.ref = entry.ref;
if (entry.action != null) {
@ -372,24 +395,22 @@ class RebaseEditor extends App {
$select.tabIndex = tabIndex++;
for (const action of rebaseActions) {
const option = document.createElement('option');
option.value = action;
option.text = action;
const $option = document.createElement('option');
$option.value = action;
$option.text = action;
if (entry.action === action) {
option.selected = true;
$option.selected = true;
}
$select.appendChild(option);
$select.appendChild($option);
}
$selectContainer.appendChild($select);
} else {
$entry.tabIndex = -1;
}
const $message = document.createElement('span');
$message.classList.add('entry-message');
$message.innerText = entry.message ?? '';
$message.textContent = entry.message ?? '';
$entry.appendChild($message);
const commit = state.commits.find(c => c.ref.startsWith(entry.ref));
@ -407,7 +428,7 @@ class RebaseEditor extends App {
const $author = document.createElement('span');
$author.classList.add('entry-author');
$author.innerText = commit.author;
$author.textContent = commit.author;
$entry.appendChild($author);
}
@ -415,16 +436,16 @@ class RebaseEditor extends App {
const $date = document.createElement('span');
$date.title = commit.date ?? '';
$date.classList.add('entry-date');
$date.innerText = commit.dateFromNow;
$date.textContent = commit.dateFromNow;
$entry.appendChild($date);
}
}
const $ref = document.createElement('a');
$ref.classList.add('entry-ref');
$ref.classList.add('entry-ref', 'icon--commit');
// $ref.dataset.prev = prev ? `${prev} \u2190 ` : '';
$ref.href = commit?.ref ? state.commands.commit.replace(this.commitTokenRegex, commit.ref) : '#';
$ref.innerText = entry.ref;
$ref.textContent = entry.ref.substr(0, 7);
$ref.tabIndex = tabIndex++;
$entry.appendChild($ref);

+ 39
- 0
src/webviews/apps/scss/codicons.scss View File

@ -0,0 +1,39 @@
.icon--branch {
&::before {
content: '\ea68';
font-family: codicon;
position: relative;
top: 2px;
margin: 0 3px;
}
}
.icon--commit {
&::before {
content: '\eafc';
font-family: codicon;
position: relative;
top: 2px;
margin: 0 1px 0 3px;
}
}
.icon--paused {
&::before {
content: '\ead1';
font-family: codicon;
position: relative;
top: 2px;
margin: 0 3px;
}
}
.icon--warning {
&::before {
content: '\ea6c';
font-family: codicon;
position: relative;
top: 2px;
margin: 0 3px;
}
}

+ 117
- 72
src/webviews/apps/scss/rebase.scss View File

@ -20,16 +20,35 @@ body {
header {
display: flex;
align-items: baseline;
flex-wrap: wrap;
margin: 0;
h2 {
flex: auto 1 1;
flex: auto 0 1;
margin-top: 0.5em;
margin-right: 1em;
font-size: 2.3rem;
}
h4 {
flex: auto 1 1;
margin-top: 0;
}
h3,
h4#subhead-status {
flex: 100% 1 1;
margin: 0.25em 0 0.5em 0;
}
& > *[class^='icon--'],
& > *[class*=' icon--'] {
&::before {
opacity: 0.7;
font-size: 1.4em;
top: 5px;
line-height: 0.6em;
}
}
}
@ -40,7 +59,6 @@ h4 {
.entries {
grid-area: entries;
// border-left: 2px solid var(--color-highlight);
border-left: 2px solid;
margin-left: 10px;
padding-left: 4px;
@ -71,6 +89,16 @@ h4 {
grid-area: actions;
margin: 10px;
display: flex;
flex-wrap: wrap;
}
.actions--left {
display: flex;
}
.actions--right {
display: flex;
margin-left: auto;
}
$entry-padding: 5px;
@ -83,13 +111,13 @@ $entry-padding: 5px;
padding: $entry-padding 0;
border: 2px solid transparent;
border-radius: 3px;
position: relative;
&::after {
display: inline-block;
content: ' ';
background-color: var(--color-background);
// border: 2px solid var(--color-highlight);
border: 2px solid var(--color-foreground--50);
border: 2px solid var(--color-foreground--75);
border-radius: 50%;
height: 12px;
width: 12px;
@ -104,31 +132,10 @@ $entry-padding: 5px;
border-radius: 3px;
}
&.entry--base {
margin-left: 10px;
margin-top: 5px;
.vscode-dark & {
background: rgba(255, 255, 255, 0.1);
}
.vscode-light & {
background: rgba(0, 0, 0, 0.1);
}
// &::after {
// background-color: var(--color-highlight);
// }
&:focus,
&:focus-within {
outline: none !important;
}
}
&.entry--squash-to {
&.entry--edit,
&.entry--reword {
&::after {
border: 2px solid rgba(212, 153, 0, 1);
border: 2px solid rgba(0, 153, 0, 1) !important;
z-index: 3;
}
@ -136,38 +143,34 @@ $entry-padding: 5px;
display: inline-block;
content: ' ';
background-color: var(--color-background);
border-left: 2px solid rgba(212, 153, 0, 1);
height: 32px;
width: 2px;
border-right: 2px solid rgba(0, 153, 0, 1);
height: #{28px + ($entry-padding * 2)};
margin-left: -18px;
margin-top: -10px;
position: absolute;
z-index: 1;
}
}
&.entry--edit,
&.entry--reword {
&.entry--squash,
&.entry--fixup {
&::after {
border: 2px solid rgba(0, 153, 0, 1);
z-index: 3;
display: none;
}
&::before {
display: inline-block;
content: ' ';
background-color: var(--color-background);
border-left: 2px solid rgba(0, 153, 0, 1);
height: #{28px + ($entry-padding * 2)};
width: 2px;
border-right: 2px solid rgba(212, 153, 0, 1);
height: #{31px + ($entry-padding * 2)};
margin-left: -18px;
margin-top: 6px;
position: absolute;
z-index: 1;
}
}
&.entry--squash,
&.entry--fixup {
&.entry--drop {
&::after {
display: none;
}
@ -176,34 +179,90 @@ $entry-padding: 5px;
display: inline-block;
content: ' ';
background-color: var(--color-background);
border-left: 2px solid rgba(212, 153, 0, 1);
height: #{44px + ($entry-padding * 2)};
width: 2px;
border-right: 2px solid rgba(153, 0, 0, 1);
height: #{28px + ($entry-padding * 2)};
margin-left: -18px;
margin-top: 6px;
position: absolute;
z-index: 1;
}
}
&.entry--drop {
&.entry--squash-to {
&::after {
display: none;
border: 2px solid rgba(212, 153, 0, 1) !important;
z-index: 3;
}
&::before {
display: inline-block;
content: ' ';
background-color: var(--color-background);
border-left: 2px solid rgba(153, 0, 0, 1);
height: #{28px + ($entry-padding * 2)};
width: 2px;
border-right: 2px solid rgba(212, 153, 0, 1);
height: 40px;
margin-left: -18px;
margin-top: -5px;
position: absolute;
z-index: 1;
}
}
&.entry--base,
&.entry--done {
margin-top: 5px;
& > .entry-action {
opacity: 0.8;
}
& > .entry-message,
& > .entry-avatar {
opacity: 0.4;
}
& > .entry-author,
& > .entry-date {
opacity: 0.3;
}
& > .entry-ref {
opacity: 0.4;
}
&::after {
border: 2px solid var(--color-foreground--50);
}
&:focus,
&:focus-within {
border-color: transparent !important;
outline: none !important;
}
& ~ .entry--done {
margin-top: 0;
}
& > .entry-action {
&::after {
opacity: 0.7;
}
}
}
&.entry--base {
margin-top: 10px;
.vscode-dark & {
background: rgba(255, 255, 255, 0.1);
box-shadow: 0px -1px 0px 0px rgba(255, 255, 255, 0.2);
}
.vscode-light & {
background: rgba(0, 0, 0, 0.1);
box-shadow: 0px -1px 0px 0px rgba(0, 0, 0, 0.2);
}
}
.entries--base &:nth-last-of-type(2),
:not(.entries--base) &:nth-last-of-type(1) {
& select {
@ -286,6 +345,7 @@ $entry-padding: 5px;
text-overflow: ellipsis;
white-space: nowrap;
.entry--fixup &,
.entry--drop & {
text-decoration: line-through;
opacity: 0.25;
@ -301,7 +361,7 @@ $entry-padding: 5px;
.entry--squash &,
.entry--fixup &,
.entry--drop & {
opacity: 0.25;
opacity: 0.25 !important;
}
}
@ -320,10 +380,14 @@ $entry-padding: 5px;
.entry--fixup &,
.entry--drop & {
text-decoration: line-through;
opacity: 0.25;
opacity: 0.25 !important;
}
}
.entry-ref {
opacity: 0.7;
}
.shortcut {
display: inline-block;
margin: 5px 10px 5px 0;
@ -334,24 +398,5 @@ $entry-padding: 5px;
}
}
.branch {
&::before {
content: '\ea68';
font-family: codicon;
position: relative;
top: 2px;
margin: 0 3px;
}
}
.commit {
&::before {
content: '\eafc';
font-family: codicon;
position: relative;
top: 2px;
margin: 0 1px 0 -1px;
}
}
@import 'codicons';
// @import 'snow';

+ 6
- 4
src/webviews/protocol.ts View File

@ -124,18 +124,20 @@ export interface RebaseEntry {
}
export interface RebaseDidChangeNotificationParams {
entries: RebaseEntry[];
state: RebaseState;
}
export const RebaseDidChangeNotificationType = new IpcNotificationType<RebaseDidChangeNotificationParams>(
'rebase/change',
);
export const RebaseDidStartCommandType = new IpcCommandType('rebase/start');
export const RebaseDidAbortCommandType = new IpcCommandType('rebase/abort');
export const RebaseDidDisableCommandType = new IpcCommandType('rebase/disable');
export const RebaseDidStartCommandType = new IpcCommandType('rebase/start');
export const RebaseDidSwitchCommandType = new IpcCommandType('rebase/switch');
export interface RebaseDidChangeEntryCommandParams {
ref: string;
action: RebaseEntryAction;
@ -151,7 +153,7 @@ export interface RebaseDidMoveEntryCommandParams {
}
export const RebaseDidMoveEntryCommandType = new IpcCommandType<RebaseDidMoveEntryCommandParams>('rebase/move/entry');
export interface RebaseState extends RebaseDidChangeNotificationParams {
export interface RebaseState {
branch: string;
onto: string;

+ 297
- 169
src/webviews/rebaseEditor.ts View File

@ -2,6 +2,7 @@
import { TextDecoder } from 'util';
import {
CancellationToken,
commands,
ConfigurationTarget,
CustomTextEditorProvider,
Disposable,
@ -9,7 +10,6 @@ import {
Range,
TextDocument,
Uri,
Webview,
WebviewPanel,
window,
workspace,
@ -17,9 +17,12 @@ import {
} from 'vscode';
import { ShowQuickCommitCommand } from '../commands';
import { configuration } from '../configuration';
import { BuiltInCommands } from '../constants';
import { Container } from '../container';
import { Repository, RepositoryChange } from '../git/git';
import { Logger } from '../logger';
import { debug } from '../system';
import { Messages } from '../messages';
import { debug, gate, Iterables } from '../system';
import {
Author,
Commit,
@ -31,6 +34,7 @@ import {
RebaseDidDisableCommandType,
RebaseDidMoveEntryCommandType,
RebaseDidStartCommandType,
RebaseDidSwitchCommandType,
RebaseEntry,
RebaseEntryAction,
RebaseState,
@ -47,6 +51,17 @@ function nextIpcId() {
return `host:${ipcSequence}`;
}
let webviewId = 0;
function nextWebviewId() {
if (webviewId === Number.MAX_SAFE_INTEGER) {
webviewId = 1;
} else {
webviewId++;
}
return webviewId;
}
const rebaseRegex = /^\s?#\s?Rebase\s([0-9a-f]+)(?:..([0-9a-f]+))?\sonto\s([0-9a-f]+)\s.*$/im;
const rebaseCommandsRegex = /^\s?(p|pick|r|reword|e|edit|s|squash|f|fixup|d|drop)\s([0-9a-f]+?)\s(.*)$/gm;
@ -65,14 +80,29 @@ const rebaseActionsMap = new Map([
['drop', 'drop'],
]);
interface RebaseEditorContext {
dispose(): void;
readonly id: number;
readonly document: TextDocument;
readonly panel: WebviewPanel;
readonly repo: Repository;
readonly subscriptions: Disposable[];
abortOnClose: boolean;
pendingChange?: boolean;
}
export class RebaseEditorProvider implements CustomTextEditorProvider, Disposable {
private readonly _disposable: Disposable;
constructor() {
this._disposable = Disposable.from(
window.registerCustomEditorProvider('gitlens.rebase', this, {
supportsMultipleEditorsPerDocument: false,
webviewOptions: {
enableFindWidget: true,
retainContextWhenHidden: true,
},
}),
);
@ -142,20 +172,62 @@ export class RebaseEditorProvider implements CustomTextEditorProvider, Disposabl
@debug<RebaseEditorProvider['resolveCustomTextEditor']>({ args: false })
async resolveCustomTextEditor(document: TextDocument, panel: WebviewPanel, _token: CancellationToken) {
const disposable = Disposable.from(
panel.onDidDispose(() => disposable.dispose()),
panel.webview.onDidReceiveMessage(e =>
this.onMessageReceived({ document: document, panel: panel, disposable: disposable }, e),
),
const repo = await this.getRepository(document);
const subscriptions: Disposable[] = [];
const context: RebaseEditorContext = {
dispose: () => Disposable.from(...subscriptions).dispose(),
id: nextWebviewId(),
subscriptions: subscriptions,
document: document,
panel: panel,
repo: repo,
abortOnClose: true,
};
subscriptions.push(
panel.onDidDispose(() => {
// If the user closed this without taking an action, consider it an abort
if (context.abortOnClose) {
void this.abort(context);
}
Disposable.from(...subscriptions).dispose();
}),
panel.onDidChangeViewState(() => {
if (!context.pendingChange) return;
void this.getStateAndNotify(context);
}),
panel.webview.onDidReceiveMessage(e => this.onMessageReceived(context, e)),
workspace.onDidChangeTextDocument(e => {
if (e.contentChanges.length === 0 || e.document.uri.toString() !== document.uri.toString()) return;
this.parseEntriesAndSendChange(panel, document);
void this.getStateAndNotify(context);
}),
workspace.onDidSaveTextDocument(e => {
if (e.uri.toString() !== document.uri.toString()) return;
void this.getStateAndNotify(context);
}),
repo.onDidChange(e => {
if (
e.changed(RepositoryChange.Closed, true) ||
e.changed(RepositoryChange.Ignores, true) ||
e.changed(RepositoryChange.Remotes, true) ||
e.changed(RepositoryChange.Starred, true) ||
e.changed(RepositoryChange.Stash, true) ||
e.changed(RepositoryChange.Tags, true)
) {
return;
}
void this.getStateAndNotify(context);
}),
);
panel.webview.options = { enableCommandUris: true, enableScripts: true };
panel.webview.html = await this.getHtml(panel.webview, document);
panel.webview.html = await this.getHtml(context);
if (this._disableAfterNextUse) {
this._disableAfterNextUse = false;
@ -163,135 +235,42 @@ export class RebaseEditorProvider implements CustomTextEditorProvider, Disposabl
}
}
private parseEntries(contents: string): RebaseEntry[];
private parseEntries(document: TextDocument): RebaseEntry[];
@debug<RebaseEditorProvider['parseEntries']>({ args: false })
private parseEntries(contentsOrDocument: string | TextDocument): RebaseEntry[] {
const contents = typeof contentsOrDocument === 'string' ? contentsOrDocument : contentsOrDocument.getText();
const entries: RebaseEntry[] = [];
@gate((context: RebaseEditorContext) => `${context.id}`)
private async getStateAndNotify(context: RebaseEditorContext) {
if (!context.panel.visible) {
context.pendingChange = true;
let match;
let action;
let ref;
let message;
do {
match = rebaseCommandsRegex.exec(contents);
if (match == null) break;
[, action, ref, message] = match;
entries.push({
index: match.index,
action: rebaseActionsMap.get(action) ?? 'pick',
// Stops excessive memory usage -- https://bugs.chromium.org/p/v8/issues/detail?id=2869
ref: ` ${ref}`.substr(1),
// Stops excessive memory usage -- https://bugs.chromium.org/p/v8/issues/detail?id=2869
message: message == null || message.length === 0 ? '' : ` ${message}`.substr(1),
});
} while (true);
return entries.reverse();
}
return;
}
private parseEntriesAndSendChange(panel: WebviewPanel, document: TextDocument) {
const entries = this.parseEntries(document);
void this.postMessage(panel, {
const state = await this.parseState(context);
void this.postMessage(context, {
id: nextIpcId(),
method: RebaseDidChangeNotificationType.method,
params: { entries: entries },
params: { state: state },
});
}
private async parseState(document: TextDocument): Promise<RebaseState> {
const repoPath = await Container.git.getRepoPath(Uri.joinPath(document.uri, '../../..'));
const branch = await Container.git.getBranch(repoPath);
const contents = document.getText();
const entries = this.parseEntries(contents);
let [, , , onto] = rebaseRegex.exec(contents) ?? ['', '', ''];
const authors = new Map<string, Author>();
const commits: Commit[] = [];
const ontoCommit = await Container.git.getCommit(repoPath!, onto);
if (ontoCommit != null) {
if (!authors.has(ontoCommit.author)) {
authors.set(ontoCommit.author, {
author: ontoCommit.author,
avatarUrl: (
await ontoCommit.getAvatarUri({ defaultStyle: Container.config.defaultGravatarsStyle })
).toString(true),
email: ontoCommit.email,
});
}
commits.push({
ref: ontoCommit.ref,
author: ontoCommit.author,
date: ontoCommit.formatDate(Container.config.defaultDateFormat),
dateFromNow: ontoCommit.formatDateFromNow(),
message: ontoCommit.message || 'root',
});
}
for (const entry of entries) {
const commit = await Container.git.getCommit(repoPath!, entry.ref);
if (commit == null) continue;
// If the onto commit is contained in the list of commits, remove it and clear the 'onto' value — See #1201
if (commit.ref === ontoCommit?.ref) {
commits.splice(0, 1);
onto = '';
}
if (!authors.has(commit.author)) {
authors.set(commit.author, {
author: commit.author,
avatarUrl: (
await commit.getAvatarUri({ defaultStyle: Container.config.defaultGravatarsStyle })
).toString(true),
email: commit.email,
});
}
commits.push({
ref: commit.ref,
author: commit.author,
date: commit.formatDate(Container.config.defaultDateFormat),
dateFromNow: commit.formatDateFromNow(),
message: commit.message,
});
}
return {
branch: branch?.name ?? '',
onto: onto,
entries: entries,
authors: [...authors.values()],
commits: commits,
commands: {
// eslint-disable-next-line no-template-curly-in-string
commit: ShowQuickCommitCommand.getMarkdownCommandArgs('${commit}', repoPath),
},
};
private async parseState(context: RebaseEditorContext): Promise<RebaseState> {
const branch = await context.repo.getBranch();
const state = await parseRebaseTodo(context.document.getText(), context.repo, branch?.name);
return state;
}
private async postMessage(panel: WebviewPanel, message: IpcMessage) {
private async postMessage(context: RebaseEditorContext, message: IpcMessage) {
try {
const success = await panel.webview.postMessage(message);
const success = await context.panel.webview.postMessage(message);
context.pendingChange = !success;
return success;
} catch (ex) {
Logger.error(ex);
context.pendingChange = true;
return false;
}
}
private onMessageReceived(
{ document, panel, disposable }: { document: TextDocument; panel: WebviewPanel; disposable: Disposable },
e: IpcMessage,
) {
private onMessageReceived(context: RebaseEditorContext, e: IpcMessage) {
switch (e.method) {
// case ReadyCommandType.method:
// onIpcCommand(ReadyCommandType, e, params => {
@ -300,41 +279,39 @@ export class RebaseEditorProvider implements CustomTextEditorProvider, Disposabl
// break;
case RebaseDidDisableCommandType.method:
onIpcCommand(RebaseDidDisableCommandType, e, async () => {
await this.abort(document, panel, disposable);
await this.setEnabled(false);
});
case RebaseDidAbortCommandType.method:
onIpcCommand(RebaseDidAbortCommandType, e, () => this.abort(context));
break;
case RebaseDidStartCommandType.method:
onIpcCommand(RebaseDidStartCommandType, e, async () => {
await this.rebase(document, panel, disposable);
});
case RebaseDidDisableCommandType.method:
onIpcCommand(RebaseDidDisableCommandType, e, () => this.disable(context));
break;
case RebaseDidAbortCommandType.method:
onIpcCommand(RebaseDidAbortCommandType, e, async () => {
await this.abort(document, panel, disposable);
});
case RebaseDidStartCommandType.method:
onIpcCommand(RebaseDidStartCommandType, e, () => this.rebase(context));
break;
case RebaseDidSwitchCommandType.method:
onIpcCommand(RebaseDidSwitchCommandType, e, () => this.switch(context));
break;
case RebaseDidChangeEntryCommandType.method:
onIpcCommand(RebaseDidChangeEntryCommandType, e, async params => {
const entries = this.parseEntries(document);
const entries = parseRebaseTodoEntries(context.document);
const entry = entries.find(e => e.ref === params.ref);
if (entry == null) return;
const start = document.positionAt(entry.index);
const range = document.validateRange(
const start = context.document.positionAt(entry.index);
const range = context.document.validateRange(
new Range(new Position(start.line, 0), new Position(start.line, Number.MAX_SAFE_INTEGER)),
);
// Fake the new set of entries, so we can ensure that the last entry isn't a squash/fixup
let action = params.action;
const edit = new WorkspaceEdit();
// Fake the new set of entries, to check if last entry is a squash/fixup
const newEntries = [...entries];
newEntries.splice(entries.indexOf(entry), 1, {
...entry,
@ -353,29 +330,25 @@ export class RebaseEditorProvider implements CustomTextEditorProvider, Disposabl
}
}
const edit = new WorkspaceEdit();
let action = params.action;
// Ensure that the last entry isn't a squash/fixup
if (squashing) {
const lastEntry = newEntries[newEntries.length - 1];
if (entry.ref === lastEntry.ref) {
action = 'pick';
} else {
const start = document.positionAt(lastEntry.index);
const range = document.validateRange(
const start = context.document.positionAt(lastEntry.index);
const range = context.document.validateRange(
new Range(
new Position(start.line, 0),
new Position(start.line, Number.MAX_SAFE_INTEGER),
),
);
edit.replace(document.uri, range, `pick ${lastEntry.ref} ${lastEntry.message}`);
edit.replace(context.document.uri, range, `pick ${lastEntry.ref} ${lastEntry.message}`);
}
}
edit.replace(document.uri, range, `${action} ${entry.ref} ${entry.message}`);
edit.replace(context.document.uri, range, `${action} ${entry.ref} ${entry.message}`);
await workspace.applyEdit(edit);
});
@ -383,7 +356,7 @@ export class RebaseEditorProvider implements CustomTextEditorProvider, Disposabl
case RebaseDidMoveEntryCommandType.method:
onIpcCommand(RebaseDidMoveEntryCommandType, e, async params => {
const entries = this.parseEntries(document);
const entries = parseRebaseTodoEntries(context.document);
const entry = entries.find(e => e.ref === params.ref);
if (entry == null) return;
@ -404,13 +377,13 @@ export class RebaseEditorProvider implements CustomTextEditorProvider, Disposabl
}
const newEntry = entries[newIndex];
let newLine = document.positionAt(newEntry.index).line;
let newLine = context.document.positionAt(newEntry.index).line;
if (newIndex < index) {
newLine++;
}
const start = document.positionAt(entry.index);
const range = document.validateRange(
const start = context.document.positionAt(entry.index);
const range = context.document.validateRange(
new Range(new Position(start.line, 0), new Position(start.line + 1, 0)),
);
@ -441,20 +414,24 @@ export class RebaseEditorProvider implements CustomTextEditorProvider, Disposabl
if (entry.ref === lastEntry.ref) {
action = 'pick';
} else {
const start = document.positionAt(lastEntry.index);
const range = document.validateRange(
const start = context.document.positionAt(lastEntry.index);
const range = context.document.validateRange(
new Range(
new Position(start.line, 0),
new Position(start.line, Number.MAX_SAFE_INTEGER),
),
);
edit.replace(document.uri, range, `pick ${lastEntry.ref} ${lastEntry.message}`);
edit.replace(context.document.uri, range, `pick ${lastEntry.ref} ${lastEntry.message}`);
}
}
edit.delete(document.uri, range);
edit.insert(document.uri, new Position(newLine, 0), `${action} ${entry.ref} ${entry.message}\n`);
edit.delete(context.document.uri, range);
edit.insert(
context.document.uri,
new Position(newLine, 0),
`${action} ${entry.ref} ${entry.message}\n`,
);
await workspace.applyEdit(edit);
});
@ -463,35 +440,58 @@ export class RebaseEditorProvider implements CustomTextEditorProvider, Disposabl
}
}
private async abort(document: TextDocument, panel: WebviewPanel, disposable: Disposable) {
private async abort(context: RebaseEditorContext) {
context.abortOnClose = false;
// Avoid triggering events by disposing them first
disposable.dispose();
context.dispose();
// Delete the contents to abort the rebase
const edit = new WorkspaceEdit();
edit.replace(document.uri, new Range(0, 0, document.lineCount, 0), '');
edit.replace(context.document.uri, new Range(0, 0, context.document.lineCount, 0), '');
await workspace.applyEdit(edit);
await document.save();
panel.dispose();
await context.document.save();
context.panel.dispose();
}
private async disable(context: RebaseEditorContext) {
await this.abort(context);
await this.setEnabled(false);
}
private async rebase(document: TextDocument, panel: WebviewPanel, disposable: Disposable) {
private async rebase(context: RebaseEditorContext) {
context.abortOnClose = false;
// Avoid triggering events by disposing them first
disposable.dispose();
context.dispose();
await context.document.save();
context.panel.dispose();
}
private switch(context: RebaseEditorContext) {
context.abortOnClose = false;
void Messages.showRebaseSwitchToTextWarningMessage();
await document.save();
panel.dispose();
// Open the text version of the document
void commands.executeCommand(BuiltInCommands.Open, context.document.uri, {
override: false,
preview: false,
});
}
private async getHtml(webview: Webview, document: TextDocument): Promise<string> {
private async getHtml(context: RebaseEditorContext): Promise<string> {
const uri = Uri.joinPath(Container.context.extensionUri, 'dist', 'webviews', 'rebase.html');
const content = new TextDecoder('utf8').decode(await workspace.fs.readFile(uri));
let html = content
.replace(/#{cspSource}/g, webview.cspSource)
.replace(/#{root}/g, webview.asWebviewUri(Container.context.extensionUri).toString());
.replace(/#{cspSource}/g, context.panel.webview.cspSource)
.replace(/#{root}/g, context.panel.webview.asWebviewUri(Container.context.extensionUri).toString());
const bootstrap = await this.parseState(document);
const bootstrap = await this.parseState(context);
html = html.replace(
/#{endOfBody}/i,
@ -502,4 +502,132 @@ export class RebaseEditorProvider implements CustomTextEditorProvider, Disposabl
return html;
}
private async getRepository(document: TextDocument): Promise<Repository> {
const repo = await Container.git.getRepository(Uri.joinPath(document.uri, '..', '..', '..'));
if (repo == null) {
// eslint-disable-next-line no-debugger
debugger;
}
return repo!;
}
}
async function parseRebaseTodo(
contents: string | { entries: RebaseEntry[]; onto: string },
repo: Repository,
branch: string | undefined,
): Promise<Omit<RebaseState, 'rebasing'>> {
let onto: string;
let entries;
if (typeof contents === 'string') {
entries = parseRebaseTodoEntries(contents);
[, , , onto] = rebaseRegex.exec(contents) ?? ['', '', ''];
} else {
({ entries, onto } = contents);
}
const authors = new Map<string, Author>();
const commits: Commit[] = [];
const log = await repo.searchForCommits({
pattern: `${onto ? `#:${onto} ` : ''}${Iterables.join(
Iterables.map(entries, e => `#:${e.ref}`),
' ',
)}`,
});
const foundCommits = log != null ? [...log.commits.values()] : [];
const ontoCommit = onto ? foundCommits.find(c => c.ref.startsWith(onto)) : undefined;
if (ontoCommit != null) {
if (!authors.has(ontoCommit.author)) {
authors.set(ontoCommit.author, {
author: ontoCommit.author,
avatarUrl: (
await ontoCommit.getAvatarUri({ defaultStyle: Container.config.defaultGravatarsStyle })
).toString(true),
email: ontoCommit.email,
});
}
commits.push({
ref: ontoCommit.ref,
author: ontoCommit.author,
date: ontoCommit.formatDate(Container.config.defaultDateFormat),
dateFromNow: ontoCommit.formatDateFromNow(),
message: ontoCommit.message || 'root',
});
}
for (const entry of entries) {
const commit = foundCommits.find(c => c.ref.startsWith(entry.ref));
if (commit == null) continue;
// If the onto commit is contained in the list of commits, remove it and clear the 'onto' value — See #1201
if (commit.ref === ontoCommit?.ref) {
commits.splice(0, 1);
onto = '';
}
if (!authors.has(commit.author)) {
authors.set(commit.author, {
author: commit.author,
avatarUrl: (
await commit.getAvatarUri({ defaultStyle: Container.config.defaultGravatarsStyle })
).toString(true),
email: commit.email,
});
}
commits.push({
ref: commit.ref,
author: commit.author,
date: commit.formatDate(Container.config.defaultDateFormat),
dateFromNow: commit.formatDateFromNow(),
message: commit.message,
});
}
return {
branch: branch ?? '',
onto: onto,
entries: entries,
authors: [...authors.values()],
commits: commits,
commands: {
// eslint-disable-next-line no-template-curly-in-string
commit: ShowQuickCommitCommand.getMarkdownCommandArgs('${commit}', repo.path),
},
};
}
function parseRebaseTodoEntries(contents: string): RebaseEntry[];
function parseRebaseTodoEntries(document: TextDocument): RebaseEntry[];
function parseRebaseTodoEntries(contentsOrDocument: string | TextDocument): RebaseEntry[] {
const contents = typeof contentsOrDocument === 'string' ? contentsOrDocument : contentsOrDocument.getText();
const entries: RebaseEntry[] = [];
let match;
let action;
let ref;
let message;
do {
match = rebaseCommandsRegex.exec(contents);
if (match == null) break;
[, action, ref, message] = match;
entries.push({
index: match.index,
action: rebaseActionsMap.get(action) ?? 'pick',
// Stops excessive memory usage -- https://bugs.chromium.org/p/v8/issues/detail?id=2869
ref: ` ${ref}`.substr(1),
// Stops excessive memory usage -- https://bugs.chromium.org/p/v8/issues/detail?id=2869
message: message == null || message.length === 0 ? '' : ` ${message}`.substr(1),
});
} while (true);
return entries.reverse();
}

Loading…
Cancel
Save