Przeglądaj źródła

Fixes #2141, #1732, #1652 improves rebase editor

- Improves performance and user experience
 - Adds always available header and footer
 - Shows commit details on selection
 - Adds full commit message
 - Adds  fixup shortcut key to UI
 - Fixes large rebases not showing commit details
main
Eric Amodio 2 lat temu
rodzic
commit
528e1e3a79
10 zmienionych plików z 527 dodań i 412 usunięć
  1. +9
    -0
      CHANGELOG.md
  2. +6
    -4
      src/env/node/git/git.ts
  3. +10
    -3
      src/env/node/git/localGitProvider.ts
  4. +8
    -1
      src/git/gitProviderService.ts
  5. +32
    -27
      src/webviews/apps/rebase/rebase.html
  6. +50
    -13
      src/webviews/apps/rebase/rebase.scss
  7. +85
    -87
      src/webviews/apps/rebase/rebase.ts
  8. +6
    -6
      src/webviews/apps/shared/dom.ts
  9. +13
    -7
      src/webviews/rebase/protocol.ts
  10. +308
    -264
      src/webviews/rebase/rebaseEditor.ts

+ 9
- 0
CHANGELOG.md Wyświetl plik

@ -8,10 +8,19 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
### Changed
- Improved Rebase editor — better performance and user experience
- Changes the header and footer to always be visible
- Shows the _Commit Details_ view on commit selection
- Adds full (multiline) commit message
- Adds the `f` fixup shortcut key to UI
- Ensures that large rebases show rich commit details
- Changes the _Home_ view to always be available
### Fixed
- Fixes [#2141](https://github.com/gitkraken/vscode-gitlens/issues/2141) - GitLens's rebase UI randomly fails loading interactive rebase when performed outside of VSC
- Fixes [#1732](https://github.com/gitkraken/vscode-gitlens/issues/1732) - Phantom rebase-merge directory (`rm -rf ".git/rebase-merge"`)
- Fixes [#1652](https://github.com/gitkraken/vscode-gitlens/issues/1652) - Closing interacteractive rebase editor after "git rebase --edit" aborts rebase-in-progress
- Fixes [#1549](https://github.com/gitkraken/vscode-gitlens/issues/1549) - Fetch does not work when local branch name differs from remote branch name
- Fixes [#2292](https://github.com/gitkraken/vscode-gitlens/issues/2292) - Push button in BranchTrackingStatusNode of non-current branch does not trigger "Push force"
- Fixes [#1488](https://github.com/gitkraken/vscode-gitlens/issues/1488) - Open Folder History not working with non-English language pack

+ 6
- 4
src/env/node/git/git.ts Wyświetl plik

@ -15,6 +15,7 @@ import { GitLogParser } from '../../../git/parsers/logParser';
import { GitReflogParser } from '../../../git/parsers/reflogParser';
import { GitTagParser } from '../../../git/parsers/tagParser';
import { Logger } from '../../../logger';
import { join } from '../../../system/iterable';
import { dirname, isAbsolute, isFolderGlob, joinPaths, normalizePath, splitPath } from '../../../system/path';
import { getDurationMilliseconds } from '../../../system/string';
import { compare, fromString } from '../../../system/version';
@ -1191,17 +1192,18 @@ export class Git {
limit?: number;
ordering?: 'date' | 'author-date' | 'topo' | null;
skip?: number;
useShow?: boolean;
shas?: Set<string>;
},
) {
if (options?.useShow) {
if (options?.shas != null) {
const stdin = join(options.shas, '\n');
return this.git<string>(
{ cwd: repoPath },
{ cwd: repoPath, stdin: stdin },
'show',
'--stdin',
'--name-status',
`--format=${GitLogParser.defaultFormat}`,
'--use-mailmap',
...search,
);
}

+ 10
- 3
src/env/node/git/localGitProvider.ts Wyświetl plik

@ -4231,7 +4231,14 @@ export class LocalGitProvider implements GitProvider, Disposable {
return cancelled ? ref : resolved ?? ref;
}
@log()
@log<LocalGitProvider['richSearchCommits']>({
args: {
1: s =>
`[${s.matchAll ? 'A' : ''}${s.matchCase ? 'C' : ''}${s.matchRegex ? 'R' : ''}]: ${
s.query.length > 500 ? `${s.query.substring(0, 500)}...` : s.query
}`,
},
})
async richSearchCommits(
repoPath: string,
search: SearchQuery,
@ -4252,11 +4259,11 @@ export class LocalGitProvider implements GitProvider, Disposable {
args.push(...files);
}
const data = await this.git.log__search(repoPath, args, {
const data = await this.git.log__search(repoPath, shas?.size ? undefined : args, {
ordering: configuration.get('advanced.commitOrdering'),
...options,
limit: limit,
useShow: Boolean(shas?.size),
shas: shas,
});
const log = GitLogParser.parse(
this.container,

+ 8
- 1
src/git/gitProviderService.ts Wyświetl plik

@ -2232,7 +2232,14 @@ export class GitProviderService implements Disposable {
return provider.resolveReference(path, ref, pathOrUri, options);
}
@log()
@log<GitProviderService['richSearchCommits']>({
args: {
1: s =>
`[${s.matchAll ? 'A' : ''}${s.matchCase ? 'C' : ''}${s.matchRegex ? 'R' : ''}]: ${
s.query.length > 500 ? `${s.query.substring(0, 500)}...` : s.query
}`,
},
})
async richSearchCommits(
repoPath: string | Uri,
search: SearchQuery,

+ 32
- 27
src/webviews/apps/rebase/rebase.html Wyświetl plik

@ -4,7 +4,7 @@
<meta charset="utf-8" />
</head>
<body class="preload">
<body class="scrollable preload">
<div class="container">
<header>
<h2>GitLens Interactive Rebase</h2>
@ -25,35 +25,40 @@
<!-- should have an <div aria-live="polite" /> to notify order change -->
</div>
</header>
<ul id="entries" class="entries"></ul>
<div class="shortcuts">
<span class="shortcut"><kbd>p</kbd><span>Pick</span></span>
<span class="shortcut"><kbd>r</kbd><span>Reword</span></span>
<span class="shortcut"><kbd>e</kbd><span>Edit</span></span>
<span class="shortcut"><kbd>s</kbd><span>Squash</span></span>
<span class="shortcut"><kbd>d</kbd><span>Drop</span></span>
<span class="shortcut"><kbd>alt ↑</kbd><span>Move Up</span></span>
<span class="shortcut"><kbd>alt ↓</kbd><span>Move Down</span></span>
<span class="shortcut"><kbd>/</kbd><span>Search</span></span>
<div class="entries-container scrollable">
<ul id="entries" class="entries scrollable"></ul>
</div>
<div class="actions">
<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>
<footer>
<div class="shortcuts">
<span class="shortcut"><kbd>p</kbd><span>Pick</span></span>
<span class="shortcut"><kbd>r</kbd><span>Reword</span></span>
<span class="shortcut"><kbd>e</kbd><span>Edit</span></span>
<span class="shortcut"><kbd>s</kbd><span>Squash</span></span>
<span class="shortcut"><kbd>f</kbd><span>Fixup</span></span>
<span class="shortcut"><kbd>d</kbd><span>Drop</span></span>
<span class="shortcut"><kbd>alt ↑</kbd><span>Move&nbsp;Up</span></span>
<span class="shortcut"><kbd>alt ↓</kbd><span>Move&nbsp;Down</span></span>
<span class="shortcut"><kbd>/</kbd><span>Search</span></span>
</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 class="actions">
<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>
<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>
</footer>
</div>
#{endOfBody}
<style nonce="#{cspNonce}">

+ 50
- 13
src/webviews/apps/rebase/rebase.scss Wyświetl plik

@ -3,25 +3,30 @@
@import '../shared/utils';
body {
height: unset;
overflow: overlay;
}
.container {
display: grid;
font-size: 1.3em;
grid-template-areas: 'header' 'entries' 'shortcuts' 'actions';
grid-template-columns: repeat(1, 1fr min-content);
grid-template-areas: 'header' 'entries' 'footer';
grid-template-columns: 1fr;
grid-template-rows: auto 1fr auto;
height: 100vh;
margin: 0 auto;
grid-gap: 0.5em 0;
max-width: 1200px;
min-width: 450px;
min-width: 495px;
}
header {
grid-area: header;
display: flex;
align-items: baseline;
flex-wrap: wrap;
margin: 0;
gap: 0 0.5em;
position: relative;
h2 {
flex: auto 0 1;
@ -57,8 +62,24 @@ h4 {
opacity: 0.8;
}
.entries {
h4#subhead {
padding-right: 12rem;
}
footer {
grid-area: footer;
display: grid;
grid-template-areas: 'shortcuts' 'actions';
border-top: 1px solid var(--vscode-sideBarSectionHeader-border);
}
.entries-container {
grid-area: entries;
overflow: auto;
}
.entries {
border-left: 2px solid;
margin-left: 10px;
padding-left: 4px;
@ -82,23 +103,37 @@ h4 {
.shortcuts {
grid-area: shortcuts;
margin: 0 auto;
display: flex;
justify-content: center;
text-align: center;
}
.actions {
grid-area: actions;
margin: 10px;
margin: 0.5rem 0 1rem 0;
display: flex;
flex-wrap: wrap;
}
.actions--left {
display: flex;
gap: 0 1rem;
}
.actions--right {
display: flex;
margin-left: auto;
gap: 0 1rem;
}
.button {
letter-spacing: 0.1rem;
margin: 0;
padding: 0.5rem 1rem;
.shortcut {
margin: 0.1rem 0 0 0;
}
}
$entry-padding: 5px;
@ -107,7 +142,7 @@ $entry-padding: 5px;
display: flex;
align-items: center;
justify-content: space-between;
margin: 0 10px;
margin: 0px 5px 0px 10px;
padding: $entry-padding 0;
border: 2px solid transparent;
border-radius: 3px;
@ -222,7 +257,7 @@ $entry-padding: 5px;
opacity: 0.3;
}
& > .entry-ref {
& > .entry-sha {
opacity: 0.4;
}
@ -349,7 +384,6 @@ $entry-padding: 5px;
.entry-message {
flex: 100% 1 1;
margin: 0 10px;
position: relative;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@ -377,7 +411,7 @@ $entry-padding: 5px;
.entry-author,
.entry-date,
.entry-ref {
.entry-sha {
flex: auto 0 0;
margin: 0 10px;
opacity: 0.5;
@ -394,7 +428,7 @@ $entry-padding: 5px;
}
}
.entry-ref {
.entry-sha {
opacity: 0.7;
}
@ -413,6 +447,9 @@ $entry-padding: 5px;
flex-direction: row-reverse;
align-items: center;
gap: 0.5em;
position: absolute;
top: 2rem;
right: 0;
&__input,
&__indicator {

+ 85
- 87
src/webviews/apps/rebase/rebase.ts Wyświetl plik

@ -13,6 +13,7 @@ import {
SearchCommandType,
StartCommandType,
SwitchCommandType,
UpdateSelectionCommandType,
} from '../../rebase/protocol';
import { App } from '../shared/appBase';
import { DOM } from '../shared/dom';
@ -61,7 +62,7 @@ class RebaseEditor extends App {
let squashing = false;
let squashToHere = false;
const $entries = [...document.querySelectorAll<HTMLLIElement>('li[data-ref]')];
const $entries = [...document.querySelectorAll<HTMLLIElement>('li[data-sha]')];
if (this.state.ascending) {
$entries.reverse();
}
@ -87,15 +88,15 @@ class RebaseEditor extends App {
return;
}
const ref = e.item.dataset.ref;
if (ref != null) {
const sha = e.item.dataset.sha;
if (sha != null) {
let indexTarget = e.newIndex;
if (this.state.ascending && e.oldIndex) {
indexTarget = this.getEntryIndex(ref) + (indexTarget - e.oldIndex) * -1;
indexTarget = this.getEntryIndex(sha) + (indexTarget - e.oldIndex) * -1;
}
this.moveEntry(ref, indexTarget, false);
this.moveEntry(sha, indexTarget, false);
document.querySelectorAll<HTMLLIElement>(`li[data-ref="${ref}"]`)[0]?.focus();
this.setSelectedEntry(sha);
}
},
onMove: e => !e.related.classList.contains('entry--base'),
@ -134,8 +135,8 @@ class RebaseEditor extends App {
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', (e, target: HTMLElement) => {
if (target.matches('select[data-ref]')) {
DOM.on('li[data-sha]', 'keydown', (e, target: HTMLLIElement) => {
if (e.target?.matches('select[data-sha]')) {
if (e.key === 'Escape') {
target.focus();
}
@ -144,11 +145,11 @@ class RebaseEditor extends App {
}
if (e.key === 'Enter' || e.key === ' ') {
if (e.key === 'Enter' && target.matches('a.entry-ref')) {
if (e.key === 'Enter' && e.target?.matches('a.entry-sha')) {
return;
}
const $select = target.querySelectorAll<HTMLSelectElement>('select[data-ref]')[0];
const $select = target.querySelectorAll<HTMLSelectElement>('select[data-sha]')[0];
if ($select != null) {
$select.focus();
}
@ -160,59 +161,59 @@ class RebaseEditor extends App {
? 1
: -1;
if (e.altKey) {
const ref = target.dataset.ref;
if (ref) {
const sha = target.dataset.sha;
if (sha) {
e.stopPropagation();
this.moveEntry(ref, advance, true);
this.moveEntry(sha, advance, true);
}
} else {
if (this.state == null) return;
let ref = target.dataset.ref;
if (ref == null) return;
let sha = target.dataset.sha;
if (sha == null) return;
e.preventDefault();
let index = this.getEntryIndex(ref) + advance;
let index = this.getEntryIndex(sha) + advance;
if (index < 0) {
index = this.state.entries.length - 1;
} else if (index === this.state.entries.length) {
index = 0;
}
ref = this.state.entries[index].ref;
document.querySelectorAll<HTMLLIElement>(`li[data-ref="${ref}"]`)[0]?.focus();
sha = this.state.entries[index].sha;
this.setSelectedEntry(sha);
}
}
} else if (e.key === 'j' || e.key === 'k') {
if (!e.metaKey && !e.ctrlKey && !e.shiftKey && !e.altKey) {
if (this.state == null) return;
let ref = target.dataset.ref;
if (ref == null) return;
let sha = target.dataset.sha;
if (sha == null) return;
e.preventDefault();
const shouldAdvance = this.state.ascending ? e.key === 'k' : e.key === 'j';
let index = this.getEntryIndex(ref) + (shouldAdvance ? 1 : -1);
let index = this.getEntryIndex(sha) + (shouldAdvance ? 1 : -1);
if (index < 0) {
index = this.state.entries.length - 1;
} else if (index === this.state.entries.length) {
index = 0;
}
ref = this.state.entries[index].ref;
document.querySelectorAll<HTMLLIElement>(`li[data-ref="${ref}"]`)[0]?.focus();
sha = this.state.entries[index].sha;
this.setSelectedEntry(sha);
}
} else if (e.key === 'J' || e.key === 'K') {
if (!e.metaKey && !e.ctrlKey && !e.altKey && e.shiftKey) {
const ref = target.dataset.ref;
if (ref) {
const sha = target.dataset.sha;
if (sha) {
e.stopPropagation();
const shouldAdvance = this.state.ascending ? e.key === 'K' : e.key === 'J';
this.moveEntry(ref, shouldAdvance ? 1 : -1, true);
this.moveEntry(sha, shouldAdvance ? 1 : -1, true);
}
}
} else if (!e.metaKey && !e.altKey && !e.ctrlKey) {
@ -220,7 +221,7 @@ class RebaseEditor extends App {
if (action !== undefined) {
e.stopPropagation();
const $select = target.querySelectorAll<HTMLSelectElement>('select[data-ref]')[0];
const $select = target.querySelectorAll<HTMLSelectElement>('select[data-sha]')[0];
if ($select != null && !$select.disabled) {
$select.value = action;
this.onSelectChanged($select);
@ -228,7 +229,8 @@ class RebaseEditor extends App {
}
}
}),
DOM.on('select[data-ref]', 'input', (e, target: HTMLSelectElement) => this.onSelectChanged(target)),
DOM.on('li[data-sha]', 'focus', (e, target: HTMLLIElement) => this.onSelectionChanged(target.dataset.sha)),
DOM.on('select[data-sha]', 'input', (e, target: HTMLSelectElement) => this.onSelectChanged(target)),
DOM.on('input[data-action="reorder"]', 'input', (e, target: HTMLInputElement) =>
this.onOrderChanged(target),
),
@ -237,32 +239,32 @@ class RebaseEditor extends App {
return disposables;
}
private getEntry(ref: string) {
return this.state?.entries.find(e => e.ref === ref);
private getEntry(sha: string) {
return this.state?.entries.find(e => e.sha === sha);
}
private getEntryIndex(ref: string) {
return this.state?.entries.findIndex(e => e.ref === ref) ?? -1;
private getEntryIndex(sha: string) {
return this.state?.entries.findIndex(e => e.sha === sha) ?? -1;
}
private moveEntry(ref: string, index: number, relative: boolean) {
const entry = this.getEntry(ref);
private moveEntry(sha: string, index: number, relative: boolean) {
const entry = this.getEntry(sha);
if (entry != null) {
this.sendCommand(MoveEntryCommandType, {
ref: entry.ref,
sha: entry.sha,
to: index,
relative: relative,
});
}
}
private setEntryAction(ref: string, action: RebaseEntryAction) {
const entry = this.getEntry(ref);
private setEntryAction(sha: string, action: RebaseEntryAction) {
const entry = this.getEntry(sha);
if (entry != null) {
if (entry.action === action) return;
this.sendCommand(ChangeEntryCommandType, {
ref: entry.ref,
sha: entry.sha,
action: action,
});
}
@ -281,9 +283,9 @@ class RebaseEditor extends App {
}
private onSelectChanged($el: HTMLSelectElement) {
const ref = $el.dataset.ref;
if (ref) {
this.setEntryAction(ref, $el.options[$el.selectedIndex].value as RebaseEntryAction);
const sha = $el.dataset.sha;
if (sha) {
this.setEntryAction(sha, $el.options[$el.selectedIndex].value as RebaseEntryAction);
}
}
@ -298,9 +300,17 @@ class RebaseEditor extends App {
private onOrderChanged($el: HTMLInputElement) {
const isChecked = $el.checked;
this.sendCommand(ReorderCommandType, {
ascending: isChecked,
});
this.sendCommand(ReorderCommandType, { ascending: isChecked });
}
private onSelectionChanged(sha: string | undefined) {
if (sha == null) return;
this.sendCommand(UpdateSelectionCommandType, { sha: sha });
}
private setSelectedEntry(sha: string, focusSelect: boolean = false) {
document.querySelectorAll<HTMLLIElement>(`${focusSelect ? 'select' : 'li'}[data-sha="${sha}"]`)[0]?.focus();
}
protected override onMessageReceived(e: MessageEvent) {
@ -311,7 +321,7 @@ class RebaseEditor extends App {
this.log(`${this.appName}.onMessageReceived(${msg.id}): name=${msg.method}`);
onIpc(DidChangeNotificationType, msg, params => {
this.setState({ ...this.state, ...params.state });
this.setState(params.state);
this.refresh(this.state);
});
break;
@ -322,9 +332,9 @@ class RebaseEditor extends App {
}
private refresh(state: State) {
const focusRef = document.activeElement?.closest<HTMLLIElement>('li[data-ref]')?.dataset.ref;
const focusRef = document.activeElement?.closest<HTMLLIElement>('li[data-sha]')?.dataset.sha;
let focusSelect = false;
if (document.activeElement?.matches('select[data-ref]')) {
if (document.activeElement?.matches('select[data-sha]')) {
focusSelect = true;
}
@ -343,9 +353,9 @@ class RebaseEditor extends App {
);
$subhead.appendChild($el);
if (state.onto) {
if (state.onto != null) {
$el = document.createElement('span');
$el.textContent = state.onto;
$el.textContent = state.onto.sha;
$el.classList.add('icon--commit');
$subhead.appendChild($el);
}
@ -377,8 +387,10 @@ class RebaseEditor extends App {
let tabIndex = 0;
const $entries = document.createDocumentFragment();
const appendEntries = () => {
const appendEntry = (entry: RebaseEntry) => {
function appendEntries(this: RebaseEditor) {
const entries = state.ascending ? state.entries.slice().reverse() : state.entries;
let $el: HTMLLIElement;
for (const entry of entries) {
squashToHere = false;
if (entry.action === 'squash' || entry.action === 'fixup') {
squashing = true;
@ -389,34 +401,26 @@ class RebaseEditor extends App {
}
}
let $el: HTMLLIElement;
[$el, tabIndex] = this.createEntry(entry, state, ++tabIndex, squashToHere);
return $el;
};
const entryList = state.entries.map(appendEntry);
if (state.ascending) {
entryList.reverse().forEach($el => $entries.appendChild($el));
} else {
entryList.forEach($el => $entries.appendChild($el));
$entries.appendChild($el);
}
};
}
if (!state.ascending) {
$container.classList.remove('entries--ascending');
appendEntries();
appendEntries.call(this);
}
if (state.onto) {
const commit = state.commits.find(c => c.ref.startsWith(state.onto));
if (state.onto != null) {
const commit = state.onto.commit;
if (commit != null) {
const [$el] = this.createEntry(
{
action: undefined!,
index: 0,
message: commit.message.split('\n')[0],
ref: state.onto,
message: commit.message,
sha: state.onto.sha,
},
state,
++tabIndex,
@ -429,7 +433,7 @@ class RebaseEditor extends App {
if (state.ascending) {
$container.classList.add('entries--ascending');
appendEntries();
appendEntries.call(this);
}
const $checkbox = document.getElementById('ordering');
@ -439,13 +443,7 @@ class RebaseEditor extends App {
$container.appendChild($entries);
document
.querySelectorAll<HTMLLIElement>(
`${focusSelect ? 'select' : 'li'}[data-ref="${focusRef ?? state.entries[0].ref}"]`,
)[0]
?.focus();
this.bind();
this.setSelectedEntry(focusRef ?? state.entries[0].sha, focusSelect);
}
private createEntry(
@ -457,7 +455,7 @@ class RebaseEditor extends App {
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;
$entry.dataset.sha = entry.sha;
if (entry.action != null) {
$entry.tabIndex = 0;
@ -471,7 +469,7 @@ class RebaseEditor extends App {
$entry.appendChild($selectContainer);
const $select = document.createElement('select');
$select.dataset.ref = entry.ref;
$select.dataset.sha = entry.sha;
$select.name = 'action';
const $options = document.createDocumentFragment();
@ -490,17 +488,18 @@ class RebaseEditor extends App {
$selectContainer.appendChild($select);
}
const commit = entry.commit;
const $message = document.createElement('span');
$message.classList.add('entry-message');
$message.textContent = entry.message ?? '';
const message = commit?.message.trim() ?? entry.message.trim();
$message.textContent = message.replace(/\n+(?:\s+\n+)?/g, ' | ');
$message.title = message;
$entry.appendChild($message);
const commit = state.commits.find(c => c.ref.startsWith(entry.ref));
if (commit != null) {
$message.title = commit.message ?? '';
if (commit.author) {
const author = state.authors.find(a => a.author === commit.author);
const author = state.authors[commit.author];
if (author?.avatarUrl.length) {
const $avatar = document.createElement('img');
$avatar.classList.add('entry-avatar');
@ -523,12 +522,11 @@ class RebaseEditor extends App {
}
}
const $ref = document.createElement('a');
$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.textContent = entry.ref.substr(0, 7);
$entry.appendChild($ref);
const $sha = document.createElement('a');
$sha.classList.add('entry-sha', 'icon--commit');
$sha.href = state.commands.commit.replace(this.commitTokenRegex, commit?.sha ?? entry.sha);
$sha.textContent = entry.sha.substr(0, 7);
$entry.appendChild($sha);
return [$entry, tabIndex];
}

+ 6
- 6
src/webviews/apps/shared/dom.ts Wyświetl plik

@ -20,19 +20,19 @@ export namespace DOM {
export function on<T extends HTMLElement, K extends keyof DocumentEventMap>(
element: T,
name: K,
listener: (e: DocumentEventMap[K], target: T) => void,
listener: (e: DocumentEventMap[K] & { target: HTMLElement | null }, target: T) => void,
options?: boolean | AddEventListenerOptions,
): Disposable;
export function on<T extends Element, K extends keyof DocumentEventMap>(
selector: string,
name: K,
listener: (e: DocumentEventMap[K], target: T) => void,
listener: (e: DocumentEventMap[K] & { target: HTMLElement | null }, target: T) => void,
options?: boolean | AddEventListenerOptions,
): Disposable;
export function on<T extends HTMLElement, K>(
selector: string,
name: string,
listener: (e: CustomEvent<K>, target: T) => void,
listener: (e: CustomEvent<K> & { target: HTMLElement | null }, target: T) => void,
options?: boolean | AddEventListenerOptions,
): Disposable;
export function on<K extends keyof (DocumentEventMap | WindowEventMap), T extends Document | Element | Window>(
@ -45,10 +45,10 @@ export namespace DOM {
if (typeof sourceOrSelector === 'string') {
const filteredListener = function (this: T, e: (DocumentEventMap | WindowEventMap)[K]) {
const target = e?.target as HTMLElement;
if (!target?.matches(sourceOrSelector)) return;
const target = (e?.target as HTMLElement)?.closest(sourceOrSelector) as unknown as T | null | undefined;
if (target == null) return;
listener(e, target as unknown as T);
listener(e, target);
};
document.addEventListener(name, filteredListener as EventListener, options ?? true);

+ 13
- 7
src/webviews/rebase/protocol.ts Wyświetl plik

@ -2,11 +2,10 @@ import { IpcCommandType, IpcNotificationType } from '../protocol';
export interface State {
branch: string;
onto: string;
onto: { sha: string; commit?: Commit } | undefined;
entries: RebaseEntry[];
authors: Author[];
commits: Commit[];
authors: Record<string, Author>;
commands: {
commit: string;
};
@ -16,9 +15,11 @@ export interface State {
export interface RebaseEntry {
readonly action: RebaseEntryAction;
readonly ref: string;
readonly sha: string;
readonly message: string;
readonly index: number;
commit?: Commit;
}
export type RebaseEntryAction = 'pick' | 'reword' | 'edit' | 'squash' | 'fixup' | 'break' | 'drop';
@ -30,7 +31,7 @@ export interface Author {
}
export interface Commit {
readonly ref: string;
readonly sha: string;
readonly author: string;
// readonly avatarUrl: string;
readonly date: string;
@ -54,18 +55,23 @@ export interface ReorderParams {
export const ReorderCommandType = new IpcCommandType<ReorderParams>('rebase/reorder');
export interface ChangeEntryParams {
ref: string;
sha: string;
action: RebaseEntryAction;
}
export const ChangeEntryCommandType = new IpcCommandType<ChangeEntryParams>('rebase/change/entry');
export interface MoveEntryParams {
ref: string;
sha: string;
to: number;
relative: boolean;
}
export const MoveEntryCommandType = new IpcCommandType<MoveEntryParams>('rebase/move/entry');
export interface UpdateSelectionParams {
sha: string;
}
export const UpdateSelectionCommandType = new IpcCommandType<UpdateSelectionParams>('rebase/selection/update');
// NOTIFICATIONS
export interface DidChangeParams {

+ 308
- 264
src/webviews/rebase/rebaseEditor.ts Wyświetl plik

@ -2,20 +2,32 @@ import type { CancellationToken, CustomTextEditorProvider, TextDocument, Webview
import { ConfigurationTarget, Disposable, Position, Range, Uri, window, workspace, WorkspaceEdit } from 'vscode';
import { getNonce } from '@env/crypto';
import { ShowQuickCommitCommand } from '../../commands';
import { GitActions } from '../../commands/gitCommands.actions';
import { configuration } from '../../configuration';
import { CoreCommands } from '../../constants';
import type { Container } from '../../container';
import type { GitCommit } from '../../git/models/commit';
import { RepositoryChange, RepositoryChangeComparisonMode } from '../../git/models/repository';
import { Logger } from '../../logger';
import { showRebaseSwitchToTextWarningMessage } from '../../messages';
import { executeCoreCommand } from '../../system/command';
import { gate } from '../../system/decorators/gate';
import { debug } from '../../system/decorators/log';
import { debug, log } from '../../system/decorators/log';
import type { Deferrable } from '../../system/function';
import { debounce } from '../../system/function';
import { join, map } from '../../system/iterable';
import { normalizePath } from '../../system/path';
import type { IpcMessage } from '../protocol';
import { onIpc } from '../protocol';
import type { Author, Commit, RebaseEntry, RebaseEntryAction, ReorderParams, State } from './protocol';
import type {
Author,
ChangeEntryParams,
MoveEntryParams,
RebaseEntry,
RebaseEntryAction,
ReorderParams,
State,
UpdateSelectionParams,
} from './protocol';
import {
AbortCommandType,
ChangeEntryCommandType,
@ -26,6 +38,7 @@ import {
SearchCommandType,
StartCommandType,
SwitchCommandType,
UpdateSelectionCommandType,
} from './protocol';
const maxSmallIntegerV8 = 2 ** 30; // Max number that can be stored in V8's smis (small integers)
@ -79,8 +92,13 @@ interface RebaseEditorContext {
readonly repoPath: string;
readonly subscriptions: Disposable[];
abortOnClose: boolean;
authors?: Map<string, Author>;
branchName?: string | null;
commits?: GitCommit[];
pendingChange?: boolean;
fireSelectionChangedDebounced?: Deferrable<RebaseEditorProvider['fireSelectionChanged']> | undefined;
notifyDidChangeStateDebounced?: Deferrable<RebaseEditorProvider['notifyDidChangeState']> | undefined;
}
export class RebaseEditorProvider implements CustomTextEditorProvider, Disposable {
@ -155,7 +173,7 @@ export class RebaseEditorProvider implements CustomTextEditorProvider, Disposabl
await configuration.updateAny('workbench.editorAssociations', associations, ConfigurationTarget.Global);
}
@debug({ args: false })
@debug<RebaseEditorProvider['resolveCustomTextEditor']>({ args: { 0: d => d.uri.toString(true) } })
async resolveCustomTextEditor(document: TextDocument, panel: WebviewPanel, _token: CancellationToken) {
const repoPath = normalizePath(Uri.joinPath(document.uri, '..', '..', '..').fsPath);
const repo = this.container.git.getRepository(repoPath);
@ -169,32 +187,27 @@ export class RebaseEditorProvider implements CustomTextEditorProvider, Disposabl
document: document,
panel: panel,
repoPath: repo?.path ?? repoPath,
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);
this.updateState(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;
void this.getStateAndNotify(context);
this.updateState(context, true);
}),
workspace.onDidSaveTextDocument(e => {
if (e.uri.toString() !== document.uri.toString()) return;
void this.getStateAndNotify(context);
this.updateState(context, true);
}),
);
@ -203,7 +216,7 @@ export class RebaseEditorProvider implements CustomTextEditorProvider, Disposabl
repo.onDidChange(e => {
if (!e.changed(RepositoryChange.Rebase, RepositoryChangeComparisonMode.Any)) return;
void this.getStateAndNotify(context);
this.updateState(context);
}),
);
}
@ -217,31 +230,12 @@ export class RebaseEditorProvider implements CustomTextEditorProvider, Disposabl
}
}
@gate((context: RebaseEditorContext) => `${context.id}`)
private async getStateAndNotify(context: RebaseEditorContext) {
if (!context.panel.visible) {
context.pendingChange = true;
return;
}
const state = await this.parseState(context);
void this.postMessage(context, {
id: nextIpcId(),
method: DidChangeNotificationType.method,
params: { state: state },
});
}
private async parseState(context: RebaseEditorContext): Promise<State> {
const branch = await this.container.git.getBranch(context.repoPath);
const state = await parseRebaseTodo(
this.container,
context.document.getText(),
context.repoPath,
branch?.name,
this.ascending,
);
if (context.branchName === undefined) {
const branch = await this.container.git.getBranch(context.repoPath);
context.branchName = branch?.name ?? null;
}
const state = await this.parseRebaseTodo(context);
return state;
}
@ -285,156 +279,209 @@ export class RebaseEditorProvider implements CustomTextEditorProvider, Disposabl
break;
case SwitchCommandType.method:
onIpc(SwitchCommandType, e, () => this.switch(context));
onIpc(SwitchCommandType, e, () => this.switchToText(context));
break;
case ReorderCommandType.method:
onIpc(ReorderCommandType, e, params => {
this.reorder(params, context);
});
onIpc(ReorderCommandType, e, params => this.swapOrdering(params, context));
break;
case ChangeEntryCommandType.method:
onIpc(ChangeEntryCommandType, e, async params => {
const entries = parseRebaseTodoEntries(context.document);
onIpc(ChangeEntryCommandType, e, params => this.onEntryChanged(context, params));
break;
const entry = entries.find(e => e.ref === params.ref);
if (entry == null) return;
case MoveEntryCommandType.method:
onIpc(MoveEntryCommandType, e, params => this.onEntryMoved(context, params));
break;
const start = context.document.positionAt(entry.index);
const range = context.document.validateRange(
new Range(new Position(start.line, 0), new Position(start.line, maxSmallIntegerV8)),
);
case UpdateSelectionCommandType.method:
onIpc(UpdateSelectionCommandType, e, params => this.onSelectionChanged(context, params));
}
}
let action = params.action;
const edit = new WorkspaceEdit();
private async onEntryChanged(context: RebaseEditorContext, params: ChangeEntryParams) {
const entries = parseRebaseTodoEntries(context.document);
// 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,
action: params.action,
});
const entry = entries.find(e => e.sha === params.sha);
if (entry == null) return;
let squashing = false;
for (const entry of newEntries) {
if (entry.action === 'squash' || entry.action === 'fixup') {
squashing = true;
} else if (squashing) {
if (entry.action !== 'drop') {
squashing = false;
}
}
}
// 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 = context.document.positionAt(lastEntry.index);
const range = context.document.validateRange(
new Range(new Position(start.line, 0), new Position(start.line, maxSmallIntegerV8)),
);
edit.replace(context.document.uri, range, `pick ${lastEntry.ref} ${lastEntry.message}`);
}
}
edit.replace(context.document.uri, range, `${action} ${entry.ref} ${entry.message}`);
await workspace.applyEdit(edit);
});
const start = context.document.positionAt(entry.index);
const range = context.document.validateRange(
new Range(new Position(start.line, 0), new Position(start.line, maxSmallIntegerV8)),
);
break;
let action = params.action;
const edit = new WorkspaceEdit();
case MoveEntryCommandType.method:
onIpc(MoveEntryCommandType, e, async params => {
const entries = parseRebaseTodoEntries(context.document);
const entry = entries.find(e => e.ref === params.ref);
if (entry == null) return;
const index = entries.findIndex(e => e.ref === params.ref);
let newIndex;
if (params.relative) {
if ((params.to === -1 && index === 0) || (params.to === 1 && index === entries.length - 1)) {
return;
}
newIndex = index + params.to;
} else {
if (index === params.to) return;
newIndex = params.to;
}
const newEntry = entries[newIndex];
let newLine = context.document.positionAt(newEntry.index).line;
if (newIndex < index) {
newLine++;
}
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)),
);
// Fake the new set of entries, so we can ensure that the last entry isn't a squash/fixup
const newEntries = [...entries];
newEntries.splice(index, 1);
newEntries.splice(newIndex, 0, entry);
let squashing = false;
for (const entry of newEntries) {
if (entry.action === 'squash' || entry.action === 'fixup') {
squashing = true;
} else if (squashing) {
if (entry.action !== 'drop') {
squashing = false;
}
}
}
const edit = new WorkspaceEdit();
let action = entry.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 = context.document.positionAt(lastEntry.index);
const range = context.document.validateRange(
new Range(new Position(start.line, 0), new Position(start.line, maxSmallIntegerV8)),
);
edit.replace(context.document.uri, range, `pick ${lastEntry.ref} ${lastEntry.message}`);
}
}
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);
});
// 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,
action: params.action,
});
break;
let squashing = false;
for (const entry of newEntries) {
if (entry.action === 'squash' || entry.action === 'fixup') {
squashing = true;
} else if (squashing) {
if (entry.action !== 'drop') {
squashing = false;
}
}
}
// Ensure that the last entry isn't a squash/fixup
if (squashing) {
const lastEntry = newEntries[newEntries.length - 1];
if (entry.sha === lastEntry.sha) {
action = 'pick';
} else {
const start = context.document.positionAt(lastEntry.index);
const range = context.document.validateRange(
new Range(new Position(start.line, 0), new Position(start.line, maxSmallIntegerV8)),
);
edit.replace(context.document.uri, range, `pick ${lastEntry.sha} ${lastEntry.message}`);
}
}
edit.replace(context.document.uri, range, `${action} ${entry.sha} ${entry.message}`);
await workspace.applyEdit(edit);
}
private async abort(context: RebaseEditorContext) {
context.abortOnClose = false;
private async onEntryMoved(context: RebaseEditorContext, params: MoveEntryParams) {
const entries = parseRebaseTodoEntries(context.document);
const entry = entries.find(e => e.sha === params.sha);
if (entry == null) return;
const index = entries.findIndex(e => e.sha === params.sha);
let newIndex;
if (params.relative) {
if ((params.to === -1 && index === 0) || (params.to === 1 && index === entries.length - 1)) {
return;
}
newIndex = index + params.to;
} else {
if (index === params.to) return;
newIndex = params.to;
}
const newEntry = entries[newIndex];
let newLine = context.document.positionAt(newEntry.index).line;
if (newIndex < index) {
newLine++;
}
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)),
);
// Fake the new set of entries, so we can ensure that the last entry isn't a squash/fixup
const newEntries = [...entries];
newEntries.splice(index, 1);
newEntries.splice(newIndex, 0, entry);
let squashing = false;
for (const entry of newEntries) {
if (entry.action === 'squash' || entry.action === 'fixup') {
squashing = true;
} else if (squashing) {
if (entry.action !== 'drop') {
squashing = false;
}
}
}
const edit = new WorkspaceEdit();
let action = entry.action;
// Ensure that the last entry isn't a squash/fixup
if (squashing) {
const lastEntry = newEntries[newEntries.length - 1];
if (entry.sha === lastEntry.sha) {
action = 'pick';
} else {
const start = context.document.positionAt(lastEntry.index);
const range = context.document.validateRange(
new Range(new Position(start.line, 0), new Position(start.line, maxSmallIntegerV8)),
);
edit.replace(context.document.uri, range, `pick ${lastEntry.sha} ${lastEntry.message}`);
}
}
edit.delete(context.document.uri, range);
edit.insert(context.document.uri, new Position(newLine, 0), `${action} ${entry.sha} ${entry.message}\n`);
await workspace.applyEdit(edit);
}
private onSelectionChanged(context: RebaseEditorContext, params: UpdateSelectionParams) {
if (context.fireSelectionChangedDebounced == null) {
context.fireSelectionChangedDebounced = debounce(this.fireSelectionChanged.bind(this), 250);
}
void context.fireSelectionChangedDebounced(context, params.sha);
}
private async fireSelectionChanged(context: RebaseEditorContext, sha: string | undefined) {
let commit: GitCommit | undefined;
if (sha != null) {
commit = await this.container.git.getCommit(context.repoPath, sha);
}
if (commit == null) return;
void GitActions.Commit.showDetailsView(commit, {
pin: true,
preserveFocus: true,
preserveVisibility: false,
});
}
@debug<RebaseEditorProvider['updateState']>({ args: { 0: c => `${c.id}:${c.document.uri.toString(true)}` } })
private updateState(context: RebaseEditorContext, immediate: boolean = false) {
if (immediate) {
context.notifyDidChangeStateDebounced?.cancel();
void this.notifyDidChangeState(context);
return;
}
if (context.notifyDidChangeStateDebounced == null) {
context.notifyDidChangeStateDebounced = debounce(this.notifyDidChangeState.bind(this), 250);
}
void context.notifyDidChangeStateDebounced(context);
}
@debug<RebaseEditorProvider['notifyDidChangeState']>({
args: { 0: c => `${c.id}:${c.document.uri.toString(true)}` },
})
private async notifyDidChangeState(context: RebaseEditorContext) {
if (!context.panel.visible) {
context.pendingChange = true;
return;
}
const state = await this.parseState(context);
void this.postMessage(context, {
id: nextIpcId(),
method: DidChangeNotificationType.method,
params: { state: state },
});
}
@log({ args: false })
private async abort(context: RebaseEditorContext) {
// Avoid triggering events by disposing them first
context.dispose();
@ -447,14 +494,14 @@ export class RebaseEditorProvider implements CustomTextEditorProvider, Disposabl
context.panel.dispose();
}
@log({ args: false })
private async disable(context: RebaseEditorContext) {
await this.abort(context);
await this.setEnabled(false);
}
@log({ args: false })
private async rebase(context: RebaseEditorContext) {
context.abortOnClose = false;
// Avoid triggering events by disposing them first
context.dispose();
@ -463,9 +510,15 @@ export class RebaseEditorProvider implements CustomTextEditorProvider, Disposabl
context.panel.dispose();
}
private switch(context: RebaseEditorContext) {
context.abortOnClose = false;
@log({ args: false })
private swapOrdering(params: ReorderParams, context: RebaseEditorContext) {
this.ascending = params.ascending ?? false;
void configuration.updateEffective('rebaseEditor.ordering', this.ascending ? 'asc' : 'desc');
this.updateState(context, true);
}
@log({ args: false })
private switchToText(context: RebaseEditorContext) {
void showRebaseSwitchToTextWarningMessage();
// Open the text version of the document
@ -475,12 +528,6 @@ export class RebaseEditorProvider implements CustomTextEditorProvider, Disposabl
});
}
private reorder(params: ReorderParams, context: RebaseEditorContext) {
this.ascending = params.ascending ?? false;
void configuration.updateEffective('rebaseEditor.ordering', this.ascending ? 'asc' : 'desc');
void this.getStateAndNotify(context);
}
private async getHtml(context: RebaseEditorContext): Promise<string> {
const webRootUri = Uri.joinPath(this.container.context.extensionUri, 'dist', 'webviews');
const uri = Uri.joinPath(webRootUri, 'rebase.html');
@ -519,98 +566,95 @@ export class RebaseEditorProvider implements CustomTextEditorProvider, Disposabl
return html;
}
}
async function parseRebaseTodo(
container: Container,
contents: string | { entries: RebaseEntry[]; onto: string },
repoPath: string,
branch: string | undefined,
ascending: boolean,
): Promise<Omit<State, 'rebasing'>> {
let onto: string;
let entries;
if (typeof contents === 'string') {
entries = parseRebaseTodoEntries(contents);
[, , , onto] = rebaseRegex.exec(contents) ?? ['', '', ''];
} else {
({ entries, onto } = contents);
}
@debug({ args: false })
private async parseRebaseTodo(context: RebaseEditorContext): Promise<Omit<State, 'rebasing'>> {
const contents = context.document.getText();
const entries = parseRebaseTodoEntries(contents);
let [, , , onto] = rebaseRegex.exec(contents) ?? ['', '', ''];
const authors = new Map<string, Author>();
const commits: Commit[] = [];
const log = await container.git.richSearchCommits(repoPath, {
query: `${onto ? `#:${onto} ` : ''}${join(
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) {
const { name, email } = ontoCommit.author;
if (!authors.has(name)) {
authors.set(name, {
author: name,
avatarUrl: (
await ontoCommit.getAvatarUri({ defaultStyle: configuration.get('defaultGravatarsStyle') })
).toString(true),
email: email,
});
if (context.authors == null || context.commits == null) {
await this.loadRichCommitData(context, onto, entries);
}
commits.push({
ref: ontoCommit.ref,
author: name,
date: ontoCommit.formatDate(configuration.get('defaultDateFormat')),
dateFromNow: ontoCommit.formatDateFromNow(),
message: ontoCommit.message || 'root',
});
}
const defaultDateFormat = configuration.get('defaultDateFormat');
const command = ShowQuickCommitCommand.getMarkdownCommandArgs(`\${commit}`, context.repoPath);
for (const entry of entries) {
const commit = foundCommits.find(c => c.ref.startsWith(entry.ref));
if (commit == null) continue;
const ontoCommit = onto ? context.commits?.find(c => c.sha.startsWith(onto)) : undefined;
// 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 = '';
}
let commit;
for (const entry of entries) {
commit = context.commits?.find(c => c.sha.startsWith(entry.sha));
if (commit == null) continue;
const { name, email } = commit.author;
if (!authors.has(name)) {
authors.set(name, {
author: name,
avatarUrl: (
await commit.getAvatarUri({ defaultStyle: configuration.get('defaultGravatarsStyle') })
).toString(true),
email: email,
});
// If the onto commit is contained in the list of commits, remove it and clear the 'onto' value — See #1201
if (commit.sha === ontoCommit?.sha) {
onto = '';
}
entry.commit = {
sha: commit.sha,
author: commit.author.name,
date: commit.formatDate(defaultDateFormat),
dateFromNow: commit.formatDateFromNow(),
message: commit.message ?? commit.summary,
};
}
commits.push({
ref: commit.ref,
author: name,
date: commit.formatDate(configuration.get('defaultDateFormat')),
dateFromNow: commit.formatDateFromNow(),
message: commit.message ?? commit.summary,
});
return {
branch: context.branchName ?? '',
onto: onto
? {
sha: onto,
commit:
ontoCommit != null
? {
sha: ontoCommit.sha,
author: ontoCommit.author.name,
date: ontoCommit.formatDate(defaultDateFormat),
dateFromNow: ontoCommit.formatDateFromNow(),
message: ontoCommit.message || 'root',
}
: undefined,
}
: undefined,
entries: entries,
authors: context.authors != null ? Object.fromEntries(context.authors) : {},
commands: { commit: command },
ascending: this.ascending,
};
}
return {
branch: branch ?? '',
onto: onto,
entries: entries,
authors: [...authors.values()],
commits: commits,
commands: {
commit: ShowQuickCommitCommand.getMarkdownCommandArgs(`\${commit}`, repoPath),
},
ascending: ascending,
};
@debug({ args: false })
private async loadRichCommitData(context: RebaseEditorContext, onto: string, entries: RebaseEntry[]) {
context.commits = [];
context.authors = new Map<string, Author>();
const log = await this.container.git.richSearchCommits(
context.repoPath,
{
query: `${onto ? `#:${onto} ` : ''}${join(
map(entries, e => `#:${e.sha}`),
' ',
)}`,
},
{ limit: 0 },
);
if (log != null) {
for (const c of log.commits.values()) {
context.commits.push(c);
if (!context.authors.has(c.author.name)) {
context.authors.set(c.author.name, {
author: c.author.name,
avatarUrl: (await c.getAvatarUri()).toString(true),
email: c.author.email,
});
}
}
}
}
}
function parseRebaseTodoEntries(contents: string): RebaseEntry[];
@ -622,20 +666,20 @@ function parseRebaseTodoEntries(contentsOrDocument: string | TextDocument): Reba
let match;
let action;
let ref;
let sha;
let message;
do {
match = rebaseCommandsRegex.exec(contents);
if (match == null) break;
[, action, ref, message] = match;
[, action, sha, 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),
sha: ` ${sha}`.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),
});

Ładowanie…
Anuluj
Zapisz