diff --git a/CHANGELOG.md b/CHANGELOG.md
index 037ecf6..028cfee 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
- Adds editor link highlighting to the _File Changes_ annotations for easier discovery of the added or changed lines
- Adds a `line` option to `gitlens.changes.locations` setting to specify whether to add a line highlight to the _File Changes_ annotations
- Adds "vanilla" [Gerrit](https://www.gerritcodereview.com/) remote provider support — closes [#1953](https://github.com/gitkraken/vscode-gitlens/issues/1953) thanks to [PR #1954](https://github.com/gitkraken/vscode-gitlens/pull/1954) by Felipe Santos ([@felipecrs](https://github.com/felipecrs))
+- Adds "Oldest first" toggle to Interactive Rebase — closes [#1190](https://github.com/gitkraken/vscode-gitlens/issues/1190)
+ - Adds a `gitlens.rebaseEditor.ordering` setting to specify how Git commits are displayed in the _Interactive Rebase Editor_
## Changed
diff --git a/package.json b/package.json
index 20605c6..840252a 100644
--- a/package.json
+++ b/package.json
@@ -2065,6 +2065,28 @@
}
},
{
+ "id": "rebase-editor",
+ "title": "Interactive Rebase Editor",
+ "order": 105,
+ "properties": {
+ "gitlens.rebaseEditor.ordering": {
+ "type": "string",
+ "default": "desc",
+ "enum": [
+ "asc",
+ "desc"
+ ],
+ "enumDescriptions": [
+ "Shows oldest commit first",
+ "Shows newest commit first"
+ ],
+ "markdownDescription": "Specifies how Git commits are displayed in the _Interactive Rebase Editor_",
+ "scope": "window",
+ "order": 10
+ }
+ }
+ },
+ {
"id": "git-command-palette",
"title": "Git Command Palette",
"order": 110,
diff --git a/src/config.ts b/src/config.ts
index 389f82c..25672ee 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -121,6 +121,9 @@ export interface Config {
url: string | null;
strictSSL: boolean;
} | null;
+ rebaseEditor: {
+ ordering: 'asc' | 'desc';
+ };
remotes: RemotesConfig[] | null;
showWelcomeOnInstall: boolean;
showWhatsNewAfterUpgrades: boolean;
diff --git a/src/webviews/apps/rebase/rebase.html b/src/webviews/apps/rebase/rebase.html
index fddd89f..83607e8 100644
--- a/src/webviews/apps/rebase/rebase.html
+++ b/src/webviews/apps/rebase/rebase.html
@@ -9,6 +9,21 @@
diff --git a/src/webviews/apps/rebase/rebase.scss b/src/webviews/apps/rebase/rebase.scss
index d4a6d6a..af27397 100644
--- a/src/webviews/apps/rebase/rebase.scss
+++ b/src/webviews/apps/rebase/rebase.scss
@@ -208,8 +208,6 @@ $entry-padding: 5px;
&.entry--base,
&.entry--done {
- margin-top: 5px;
-
& > .entry-action {
opacity: 0.8;
}
@@ -249,8 +247,17 @@ $entry-padding: 5px;
}
}
+ &.entry--done {
+ margin-top: 5px;
+ }
+
&.entry--base {
- margin-top: 10px;
+ .entries--ascending & {
+ margin-bottom: 10px;
+ }
+ .entries:not(.entries--ascending) & {
+ margin-top: 10px;
+ }
.vscode-dark & {
background: rgba(255, 255, 255, 0.1);
@@ -263,8 +270,10 @@ $entry-padding: 5px;
}
}
- .entries--base &:nth-last-of-type(2),
- :not(.entries--base) &:nth-last-of-type(1) {
+ .entries--base:not(.entries--ascending) &:nth-last-of-type(2),
+ .entries:not(.entries--base):not(.entries--ascending) &:nth-last-of-type(1),
+ .entries--base.entries--ascending &:nth-of-type(2),
+ .entries--ascending:not(.entries--base) &:nth-of-type(1) {
& select {
& > option[value='squash'],
& > option[value='fixup'] {
@@ -399,4 +408,63 @@ $entry-padding: 5px;
}
}
+.toggle {
+ display: inline-flex;
+ flex-direction: row-reverse;
+ align-items: center;
+ gap: 0.5em;
+
+ &__input,
+ &__indicator {
+ width: 2.4em;
+ height: 1.4em;
+ }
+
+ &__input {
+ position: absolute;
+ appearance: none;
+ opacity: 0;
+ border-radius: 1em;
+ padding: 0;
+
+ &:focus {
+ border-radius: 1em;
+ }
+ }
+
+ &__indicator {
+ position: relative;
+ pointer-events: none;
+ display: block;
+ flex: none;
+ border-radius: 1em;
+ background-color: var(--color-background--lighten-075);
+ border: 1px solid var(--color-background--lighten-075);
+
+ &::before {
+ content: '';
+ top: 0.1em;
+ left: 0.1em;
+ position: absolute;
+ width: 1.2em;
+ height: 1.2em;
+ border-radius: 100%;
+ background-color: var(--color-button-foreground);
+
+ :checked ~ & {
+ transform: translateX(1em);
+ }
+ }
+
+ :checked ~ & {
+ background-color: var(--color-highlight);
+ }
+
+ :focus ~ & {
+ border-color: var(--color-background);
+ box-shadow: 0 0 0 1px var(--color-focus-border);
+ }
+ }
+}
+
@import '../shared/codicons';
diff --git a/src/webviews/apps/rebase/rebase.ts b/src/webviews/apps/rebase/rebase.ts
index a7a49e0..a4f14cc 100644
--- a/src/webviews/apps/rebase/rebase.ts
+++ b/src/webviews/apps/rebase/rebase.ts
@@ -10,6 +10,7 @@ import {
MoveEntryCommandType,
RebaseEntry,
RebaseEntryAction,
+ ReorderCommandType,
StartCommandType,
State,
SwitchCommandType,
@@ -61,7 +62,10 @@ class RebaseEditor extends App {
let squashing = false;
let squashToHere = false;
- const $entries = document.querySelectorAll('li[data-ref]');
+ const $entries = [...document.querySelectorAll('li[data-ref]')];
+ if (this.state.ascending) {
+ $entries.reverse();
+ }
for (const $entry of $entries) {
squashToHere = false;
if ($entry.classList.contains('entry--squash') || $entry.classList.contains('entry--fixup')) {
@@ -86,7 +90,11 @@ class RebaseEditor extends App {
const ref = e.item.dataset.ref;
if (ref != null) {
- this.moveEntry(ref, e.newIndex, false);
+ let indexTarget = e.newIndex;
+ if (this.state.ascending && e.oldIndex) {
+ indexTarget = this.getEntryIndex(ref) + (indexTarget - e.oldIndex) * -1;
+ }
+ this.moveEntry(ref, indexTarget, false);
document.querySelectorAll(`li[data-ref="${ref}"]`)[0]?.focus();
}
@@ -142,12 +150,17 @@ class RebaseEditor extends App {
}
} else if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
if (!e.metaKey && !e.ctrlKey && !e.shiftKey) {
+ const advance =
+ (e.key === 'ArrowDown' && !this.state.ascending) ||
+ (e.key === 'ArrowUp' && this.state.ascending)
+ ? 1
+ : -1;
if (e.altKey) {
const ref = target.dataset.ref;
if (ref) {
e.stopPropagation();
- this.moveEntry(ref, e.key === 'ArrowDown' ? 1 : -1, true);
+ this.moveEntry(ref, advance, true);
}
} else {
if (this.state == null) return;
@@ -157,7 +170,7 @@ class RebaseEditor extends App {
e.preventDefault();
- let index = this.getEntryIndex(ref) + (e.key === 'ArrowDown' ? 1 : -1);
+ let index = this.getEntryIndex(ref) + advance;
if (index < 0) {
index = this.state.entries.length - 1;
} else if (index === this.state.entries.length) {
@@ -210,6 +223,9 @@ class RebaseEditor extends App {
}
}),
DOM.on('select[data-ref]', 'input', (e, target: HTMLSelectElement) => this.onSelectChanged(target)),
+ DOM.on('input[data-action="reorder"]', 'input', (e, target: HTMLInputElement) =>
+ this.onOrderChanged(target),
+ ),
);
return disposables;
@@ -269,6 +285,14 @@ class RebaseEditor extends App {
this.sendCommand(SwitchCommandType, undefined);
}
+ private onOrderChanged($el: HTMLInputElement) {
+ const isChecked = $el.checked;
+
+ this.sendCommand(ReorderCommandType, {
+ ascending: isChecked,
+ });
+ }
+
protected override onMessageReceived(e: MessageEvent) {
const msg = e.data;
@@ -342,20 +366,36 @@ class RebaseEditor extends App {
let squashToHere = false;
let tabIndex = 0;
- for (const entry of state.entries) {
- squashToHere = false;
- if (entry.action === 'squash' || entry.action === 'fixup') {
- squashing = true;
- } else if (squashing) {
- if (entry.action !== 'drop') {
- squashToHere = true;
- squashing = false;
+ const $entries = document.createDocumentFragment();
+ const appendEntries = () => {
+ const appendEntry = (entry: RebaseEntry) => {
+ squashToHere = false;
+ if (entry.action === 'squash' || entry.action === 'fixup') {
+ squashing = true;
+ } else if (squashing) {
+ if (entry.action !== 'drop') {
+ squashToHere = true;
+ squashing = false;
+ }
}
+
+ 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));
}
+ };
- let $el: HTMLLIElement;
- [$el, tabIndex] = this.createEntry(entry, state, ++tabIndex, squashToHere);
- $container.appendChild($el);
+ if (!state.ascending) {
+ $container.classList.remove('entries--ascending');
+ appendEntries();
}
if (state.onto) {
@@ -372,11 +412,23 @@ class RebaseEditor extends App {
++tabIndex,
false,
);
- $container.appendChild($el);
+ $entries.appendChild($el);
$container.classList.add('entries--base');
}
}
+ if (state.ascending) {
+ $container.classList.add('entries--ascending');
+ appendEntries();
+ }
+
+ const $checkbox = document.getElementById('ordering');
+ if ($checkbox != null) {
+ ($checkbox as HTMLInputElement).checked = state.ascending;
+ }
+
+ $container.appendChild($entries);
+
document
.querySelectorAll(
`${focusSelect ? 'select' : 'li'}[data-ref="${focusRef ?? state.entries[0].ref}"]`,
@@ -398,7 +450,7 @@ class RebaseEditor extends App {
$entry.dataset.ref = entry.ref;
if (entry.action != null) {
- $entry.tabIndex = tabIndex++;
+ $entry.tabIndex = 0;
const $dragHandle = document.createElement('span');
$dragHandle.classList.add('entry-handle');
@@ -411,8 +463,8 @@ class RebaseEditor extends App {
const $select = document.createElement('select');
$select.dataset.ref = entry.ref;
$select.name = 'action';
- $select.tabIndex = tabIndex++;
+ const $options = document.createDocumentFragment();
for (const action of rebaseActions) {
const $option = document.createElement('option');
$option.value = action;
@@ -422,8 +474,9 @@ class RebaseEditor extends App {
$option.selected = true;
}
- $select.appendChild($option);
+ $options.appendChild($option);
}
+ $select.appendChild($options);
$selectContainer.appendChild($select);
}
@@ -465,7 +518,6 @@ class RebaseEditor extends App {
// $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);
- $ref.tabIndex = tabIndex++;
$entry.appendChild($ref);
return [$entry, tabIndex];
diff --git a/src/webviews/rebase/protocol.ts b/src/webviews/rebase/protocol.ts
index 2fcb57c..4e2f4e2 100644
--- a/src/webviews/rebase/protocol.ts
+++ b/src/webviews/rebase/protocol.ts
@@ -10,6 +10,8 @@ export interface State {
commands: {
commit: string;
};
+
+ ascending: boolean;
}
export interface RebaseEntry {
@@ -48,6 +50,11 @@ export const StartCommandType = new IpcCommandType('rebase/start');
export const SwitchCommandType = new IpcCommandType('rebase/switch');
+export interface ReorderParams {
+ ascending: boolean;
+}
+export const ReorderCommandType = new IpcCommandType('rebase/reorder');
+
export interface ChangeEntryParams {
ref: string;
action: RebaseEntryAction;
diff --git a/src/webviews/rebase/rebaseEditor.ts b/src/webviews/rebase/rebaseEditor.ts
index 4b7a5ba..125bf68 100644
--- a/src/webviews/rebase/rebaseEditor.ts
+++ b/src/webviews/rebase/rebaseEditor.ts
@@ -36,6 +36,8 @@ import {
MoveEntryCommandType,
RebaseEntry,
RebaseEntryAction,
+ ReorderCommandType,
+ ReorderParams,
StartCommandType,
State,
SwitchCommandType,
@@ -98,6 +100,7 @@ interface RebaseEditorContext {
export class RebaseEditorProvider implements CustomTextEditorProvider, Disposable {
private readonly _disposable: Disposable;
+ private ascending = false;
constructor(private readonly container: Container) {
this._disposable = Disposable.from(
@@ -108,6 +111,7 @@ export class RebaseEditorProvider implements CustomTextEditorProvider, Disposabl
},
}),
);
+ this.ascending = configuration.get('rebaseEditor.ordering') === 'asc';
}
dispose() {
@@ -245,7 +249,13 @@ export class RebaseEditorProvider implements CustomTextEditorProvider, Disposabl
private async parseState(context: RebaseEditorContext): Promise {
const branch = await this.container.git.getBranch(context.repoPath);
- const state = await parseRebaseTodo(this.container, context.document.getText(), context.repoPath, branch?.name);
+ const state = await parseRebaseTodo(
+ this.container,
+ context.document.getText(),
+ context.repoPath,
+ branch?.name,
+ this.ascending,
+ );
return state;
}
@@ -288,6 +298,12 @@ export class RebaseEditorProvider implements CustomTextEditorProvider, Disposabl
onIpc(SwitchCommandType, e, () => this.switch(context));
break;
+ case ReorderCommandType.method:
+ onIpc(ReorderCommandType, e, params => {
+ this.reorder(params, context);
+ });
+ break;
+
case ChangeEntryCommandType.method:
onIpc(ChangeEntryCommandType, e, async params => {
const entries = parseRebaseTodoEntries(context.document);
@@ -469,6 +485,12 @@ 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 {
const webRootUri = Uri.joinPath(this.container.context.extensionUri, 'dist', 'webviews');
const uri = Uri.joinPath(webRootUri, 'rebase.html');
@@ -516,6 +538,7 @@ async function parseRebaseTodo(
contents: string | { entries: RebaseEntry[]; onto: string },
repoPath: string,
branch: string | undefined,
+ ascending: boolean,
): Promise> {
let onto: string;
let entries;
@@ -598,6 +621,7 @@ async function parseRebaseTodo(
commands: {
commit: ShowQuickCommitCommand.getMarkdownCommandArgs(`\${commit}`, repoPath),
},
+ ascending: ascending,
};
}