1
1
mirror of https://github.com/kahole/edamagit.git synced 2024-09-11 07:15:31 +03:00

implements support for interactive rebase status and finished implementation of basic interactive rebasing #10

This commit is contained in:
kahole 2020-03-23 21:53:20 +01:00
parent f03930f07d
commit 9e98b0aa1c
13 changed files with 166 additions and 107 deletions

View File

@ -511,7 +511,7 @@
{
"command": "magit.save-and-close-editor",
"key": "ctrl+c ctrl+c",
"when": "editorTextFocus && (editorLangId == git-commit || editorLangId == git-rebase)"
"when": "editorTextFocus && editorLangId == git-commit"
},
{
"command": "magit.clear-and-abort-editor",
@ -519,7 +519,12 @@
"when": "editorTextFocus && editorLangId == git-commit"
},
{
"command": "magit.abort-interactive-rebase",
"command": "magit.save-and-close-editor",
"key": "ctrl+c ctrl+c",
"when": "editorTextFocus && editorLangId == git-rebase"
},
{
"command": "magit.clear-and-abort-editor",
"key": "ctrl+c ctrl+k",
"when": "editorTextFocus && editorLangId == git-rebase"
}

View File

@ -52,9 +52,10 @@ interface CommitEditorOptions {
updatePostCommitTask?: boolean;
showStagedChanges?: boolean;
editor?: string;
propagateErrors?: boolean;
}
export async function runCommitLikeCommand(repository: MagitRepository, args: string[], { showStagedChanges, updatePostCommitTask, editor }: CommitEditorOptions = { showStagedChanges: true }) {
export async function runCommitLikeCommand(repository: MagitRepository, args: string[], { showStagedChanges, updatePostCommitTask, editor, propagateErrors }: CommitEditorOptions = { showStagedChanges: true }) {
let stagedEditorTask: Thenable<TextEditor> | undefined;
let instructionStatus;
@ -93,6 +94,9 @@ export async function runCommitLikeCommand(repository: MagitRepository, args: st
instructionStatus.dispose();
}
window.setStatusBarMessage(`Commit canceled.`, Constants.StatusMessageDisplayTimeout);
if (propagateErrors) {
throw e;
}
} finally {
const stagedEditor = await stagedEditorTask;

View File

@ -16,6 +16,7 @@ import FilePathUtils from '../utils/filePathUtils';
import { TagListingView } from '../views/tags/tagListingView';
import { BranchListingView } from '../views/branches/branchListingView';
import { RemoteBranchListingView } from '../views/remotes/remoteBranchListingView';
import * as Constants from '../common/constants';
export async function magitDiscardAtPoint(repository: MagitRepository, currentView: DocumentView): Promise<any> {
@ -138,7 +139,7 @@ export async function magitDiscardAtPoint(repository: MagitRepository, currentVi
return gitRun(repository, args);
}
} else {
window.setStatusBarMessage('There is no thing at point that could be deleted', 10000);
window.setStatusBarMessage('There is no thing at point that could be deleted', Constants.StatusMessageDisplayTimeout);
}
}

View File

@ -5,6 +5,7 @@ import FilePathUtils from '../utils/filePathUtils';
import * as fs from 'fs';
import { window } from 'vscode';
import { EOL } from 'os';
import * as Constants from '../common/constants';
const ignoringMenu = {
title: 'Ignoring',
@ -52,7 +53,7 @@ async function ignore(repository: MagitRepository, globally = false) {
reject(err);
return;
}
window.setStatusBarMessage(`Wrote file ${gitIgnoreFilePath}`, 10000);
window.setStatusBarMessage(`Wrote file ${gitIgnoreFilePath}`, Constants.StatusMessageDisplayTimeout);
resolve();
});
});

View File

@ -1,4 +1,4 @@
import { MenuState, MenuUtil } from '../menu/menu';
import { MenuState, MenuUtil, Switch } from '../menu/menu';
import { MagitRepository } from '../models/magitRepository';
import { gitRun } from '../utils/gitRawRunner';
import MagitUtils from '../utils/magitUtils';
@ -11,7 +11,7 @@ const whileRebasingMenu = {
commands: [
{ label: 'r', description: 'Continue', action: (state: MenuState) => rebaseControlCommand(state, '--continue') },
{ label: 's', description: 'Skip', action: (state: MenuState) => rebaseControlCommand(state, '--skip') },
// { label: 'e', description: 'Edit', action: (state: MenuState) => rebaseControlCommand(state, '--edit-todo') },
{ label: 'e', description: 'Edit', action: editTodo },
{ label: 'a', description: 'Abort', action: (state: MenuState) => rebaseControlCommand(state, '--abort') }
]
};
@ -22,16 +22,32 @@ export async function rebasing(repository: MagitRepository) {
return MenuUtil.showMenu(whileRebasingMenu, { repository });
} else {
const switches = [
{ shortName: '-k', longName: '--keep-empty', description: 'Keep empty commits' },
{ shortName: '-p', longName: '--preserve-merges', description: 'Preserve merges' },
{ shortName: '-c', longName: '--committer-date-is-author-date', description: 'Lie about committer date' },
{ shortName: '-a', longName: '--autosquash', description: 'Autosquash' },
{ shortName: '-A', longName: '--autostash', description: 'Autostash' },
{ shortName: '-i', longName: '--interactive', description: 'Interactive' },
{ shortName: '-h', longName: '--no-verify', description: 'Disable hooks' },
];
const HEAD = repository.magitState?.HEAD;
const commands = [];
if (HEAD?.pushRemote) {
commands.push({ label: 'p', description: `onto ${HEAD.pushRemote.remote}/${HEAD.pushRemote.name}`, action: rebase });
commands.push({
label: 'p', description: `onto ${HEAD.pushRemote.remote}/${HEAD.pushRemote.name}`,
action: ({ switches }: MenuState) => _rebase(repository, `${HEAD.pushRemote!.remote}/${HEAD.pushRemote!.name}`, switches)
});
}
if (HEAD?.upstreamRemote) {
commands.push({ label: 'u', description: `onto ${HEAD.upstreamRemote.remote}/${HEAD.upstreamRemote.name}`, action: rebase });
commands.push({
label: 'u', description: `onto ${HEAD.upstreamRemote.remote}/${HEAD.upstreamRemote.name}`,
action: ({ switches }: MenuState) => _rebase(repository, `${HEAD.upstreamRemote!.remote}/${HEAD.upstreamRemote!.name}`, switches)
});
}
commands.push(...[
@ -44,23 +60,28 @@ export async function rebasing(repository: MagitRepository) {
commands
};
return MenuUtil.showMenu(rebasingMenu, { repository });
return MenuUtil.showMenu(rebasingMenu, { repository, switches });
}
}
async function rebase({ repository }: MenuState) {
async function rebase({ repository, switches }: MenuState) {
const ref = await MagitUtils.chooseRef(repository, 'Rebase');
if (ref) {
return _rebase(repository, ref);
return _rebase(repository, ref, switches);
}
}
async function _rebase(repository: MagitRepository, ref: string) {
async function _rebase(repository: MagitRepository, ref: string, switches: Switch[] = []) {
const args = ['rebase', ref];
const args = ['rebase', ...MenuUtil.switchesToArgs(switches), ref];
try {
if (switches.find(s => s.activated && s.longName === '--interactive')) {
return CommitCommands.runCommitLikeCommand(repository, args, { editor: 'GIT_SEQUENCE_EDITOR' });
}
return await gitRun(repository, args);
}
catch (e) {
@ -73,17 +94,21 @@ async function rebaseControlCommand({ repository }: MenuState, command: string)
return gitRun(repository, args);
}
async function rebaseInteractively({ repository }: MenuState) {
const ref = await MagitUtils.chooseRef(repository, 'Rebase');
async function editTodo({ repository }: MenuState) {
if (ref) {
const args = ['rebase', '--interactive', ref];
const args = ['rebase', '--edit-todo'];
return CommitCommands.runCommitLikeCommand(repository, args, { editor: 'GIT_SEQUENCE_EDITOR', propagateErrors: true });
}
async function rebaseInteractively({ repository, switches }: MenuState) {
const commit = await MagitUtils.chooseCommit(repository, 'Rebase commit and all above it');
if (commit) {
const interactiveSwitches = (switches ?? []).map(s => ({ ...s, activated: s.activated || s.longName === '--interactive' }));
const args = ['rebase', ...MenuUtil.switchesToArgs(interactiveSwitches), commit];
return CommitCommands.runCommitLikeCommand(repository, args, { editor: 'GIT_SEQUENCE_EDITOR' });
}
}
export async function abortInteractiveRebase(repository: MagitRepository) {
await commands.executeCommand('magit.clear-and-abort-editor');
return rebaseControlCommand({ repository }, '--abort');
}

View File

@ -6,15 +6,14 @@ import GitTextUtils from '../utils/gitTextUtils';
import { MagitRepository } from '../models/magitRepository';
import MagitUtils from '../utils/magitUtils';
import MagitStatusView from '../views/magitStatusView';
import { Status, Commit, RefType } from '../typings/git';
import { Status, Commit, RefType, Repository } from '../typings/git';
import { MagitBranch } from '../models/magitBranch';
import { Section } from '../views/general/sectionHeader';
import { gitRun, LogLevel } from '../utils/gitRawRunner';
import * as Constants from '../common/constants';
import { getCommit } from '../utils/commitCache';
import { MagitRemote } from '../models/magitRemote';
import { MagitCherryPickingState } from '../models/magitCherryPickingState';
import { MagitMergingState } from '../models/magitMergingState';
import { MagitRebasingState } from '../models/magitRebasingState';
export async function magitRefresh() { }
@ -51,7 +50,7 @@ export async function internalMagitStatus(repository: MagitRepository): Promise<
const stashTask = repository._repository.getStashes();
const logTask = repository.state.HEAD?.commit ? repository.log({ maxEntries: 10 }) : [];
const logTask = repository.state.HEAD?.commit ? repository.log({ maxEntries: 100 }) : Promise.resolve([]);
if (repository.state.HEAD?.commit) {
interestingCommits.push(repository.state.HEAD?.commit);
@ -122,35 +121,6 @@ export async function internalMagitStatus(repository: MagitRepository): Promise<
const revertHeadPath = Uri.parse(dotGitPath + 'REVERT_HEAD');
const revertHeadFileTask = workspace.fs.readFile(revertHeadPath).then(f => f.toString().replace(Constants.FinalLineBreakRegex, ''), err => undefined);
const rebaseHeadNamePath = Uri.parse(dotGitPath + 'rebase-apply/head-name');
const rebaseOntoPath = Uri.parse(dotGitPath + 'rebase-apply/onto');
let rebaseHeadNameFileTask: Thenable<string>;
let rebaseOntoPathFileTask: Thenable<string>;
let rebaseCommitListTask: Thenable<Commit[]> | undefined = undefined;
let rebaseNextIndex: number = 0;
if (repository.state.rebaseCommit) {
rebaseHeadNameFileTask = workspace.fs.readFile(rebaseHeadNamePath).then(f => f.toString().replace(Constants.FinalLineBreakRegex, ''));
rebaseOntoPathFileTask = workspace.fs.readFile(rebaseOntoPath).then(f => f.toString().replace(Constants.FinalLineBreakRegex, ''));
const rebaseLastIndexTask = workspace.fs.readFile(Uri.parse(dotGitPath + 'rebase-apply/last')).then(f => f.toString().replace(Constants.FinalLineBreakRegex, '')).then(Number.parseInt);
rebaseNextIndex = await workspace.fs.readFile(Uri.parse(dotGitPath + 'rebase-apply/next')).then(f => f.toString().replace(Constants.FinalLineBreakRegex, '')).then(Number.parseInt);
const indices: number[] = [];
for (let i = await rebaseLastIndexTask; i > rebaseNextIndex; i--) {
indices.push(i);
}
rebaseCommitListTask =
Promise.all(
indices.map(
index => workspace.fs.readFile(Uri.parse(dotGitPath + 'rebase-apply/' + index.toString().padStart(4, '0'))).then(f => f.toString().replace(Constants.FinalLineBreakRegex, ''))
.then(GitTextUtils.commitDetailTextToCommit)
));
}
const commitTasks = Promise.all(
interestingCommits
.map(c => getCommit(repository, c)));
@ -181,8 +151,10 @@ export async function internalMagitStatus(repository: MagitRepository): Promise<
if (cherryPickHeadCommitHash) {
const sequencerTodoPathFileTask = workspace.fs.readFile(sequencerTodoPath).then(f => f.toString().replace(Constants.FinalLineBreakRegex, ''), err => undefined);
const sequencerHeadPathFileTask = workspace.fs.readFile(sequencerHeadPath).then(f => f.toString().replace(Constants.FinalLineBreakRegex, ''), err => undefined);
const sequencerTodoPathFileTask = workspace.fs.readFile(sequencerTodoPath)
.then(f => f.toString().replace(Constants.FinalLineBreakRegex, ''), err => undefined);
const sequencerHeadPathFileTask = workspace.fs.readFile(sequencerHeadPath)
.then(f => f.toString().replace(Constants.FinalLineBreakRegex, ''), err => undefined);
const todo = await sequencerTodoPathFileTask;
const head = await sequencerHeadPathFileTask;
@ -203,8 +175,10 @@ export async function internalMagitStatus(repository: MagitRepository): Promise<
const revertHeadCommitHash = await revertHeadFileTask;
if (revertHeadCommitHash) {
const sequencerTodoPathFileTask = workspace.fs.readFile(sequencerTodoPath).then(f => f.toString().replace(Constants.FinalLineBreakRegex, ''), err => undefined);
const sequencerHeadPathFileTask = workspace.fs.readFile(sequencerHeadPath).then(f => f.toString().replace(Constants.FinalLineBreakRegex, ''), err => undefined);
const sequencerTodoPathFileTask = workspace.fs.readFile(sequencerTodoPath)
.then(f => f.toString().replace(Constants.FinalLineBreakRegex, ''), err => undefined);
const sequencerHeadPathFileTask = workspace.fs.readFile(sequencerHeadPath)
.then(f => f.toString().replace(Constants.FinalLineBreakRegex, ''), err => undefined);
const todo = await sequencerTodoPathFileTask;
const head = await sequencerHeadPathFileTask;
@ -220,38 +194,13 @@ export async function internalMagitStatus(repository: MagitRepository): Promise<
}
} catch { }
let log: Commit[] = [];
try {
log = await logTask;
} catch { }
let rebasingState;
if (repository.state.rebaseCommit) {
const ontoCommit = await getCommit(repository, await rebaseOntoPathFileTask!);
const ontoBranch = repository.state.refs.find(ref => ref.commit === ontoCommit.hash && ref.type !== RefType.RemoteHead) as MagitBranch;
ontoBranch.commitDetails = ontoCommit;
const doneCommits: Commit[] = log.slice(0, rebaseNextIndex - 1);
const upcomingCommits: Commit[] = (await rebaseCommitListTask) ?? [];
rebasingState = {
currentCommit: repository.state.rebaseCommit,
origBranchName: (await rebaseHeadNameFileTask!).split('/')[2],
ontoBranch,
doneCommits,
upcomingCommits
};
}
const commitMap: { [id: string]: Commit; } = (await commitTasks).reduce((prev, commit) => ({ ...prev, [commit.hash]: commit }), {});
const HEAD = repository.state.HEAD as MagitBranch | undefined;
if (HEAD?.commit) {
HEAD.commitDetails = commitMap[HEAD.commit];
// Resolve tag at HEAD
HEAD.tag = repository.state.refs.find(r => HEAD?.commit === r.commit && r.type === RefType.Tag);
HEAD.commitsAhead = commitsAhead.map(hash => commitMap[hash]);
@ -290,12 +239,12 @@ export async function internalMagitStatus(repository: MagitRepository): Promise<
repository.magitState = {
HEAD,
stashes: await stashTask,
log,
log: await logTask,
workingTreeChanges: await workingTreeChangesTasks,
indexChanges: await indexChangesTasks,
mergeChanges: await mergeChangesTasks,
untrackedFiles,
rebasingState,
rebasingState: await rebasingState(repository, dotGitPath, logTask),
mergingState,
cherryPickingState,
revertingState,
@ -304,4 +253,77 @@ export async function internalMagitStatus(repository: MagitRepository): Promise<
tags: repository.state.refs.filter(ref => ref.type === RefType.Tag),
latestGitError: repository.magitState?.latestGitError
};
}
async function rebasingState(repository: Repository, dotGitPath: string, logTask: Promise<Commit[]>): Promise<MagitRebasingState | undefined> {
try {
if (repository.state.rebaseCommit) {
let activeRebasingDirectory: Uri;
let interactive = false;
const rebasingDirectory = Uri.parse(dotGitPath + 'rebase-apply/');
const interactiveRebasingDirectory = Uri.parse(dotGitPath + 'rebase-merge/');
if (await workspace.fs.readDirectory(rebasingDirectory).then(res => res.length, err => undefined)) {
activeRebasingDirectory = rebasingDirectory;
} else {
interactive = true;
activeRebasingDirectory = interactiveRebasingDirectory;
}
const rebaseHeadNamePath = Uri.parse(activeRebasingDirectory + 'head-name');
const rebaseOntoPath = Uri.parse(activeRebasingDirectory + 'onto');
const rebaseHeadNameFileTask = workspace.fs.readFile(rebaseHeadNamePath).then(f => f.toString().replace(Constants.FinalLineBreakRegex, ''));
const rebaseOntoPathFileTask = workspace.fs.readFile(rebaseOntoPath).then(f => f.toString().replace(Constants.FinalLineBreakRegex, ''));
let rebaseNextIndex: number;
let rebaseCommitListTask: Thenable<Commit[]>;
if (interactive) {
rebaseNextIndex = await workspace.fs.readFile(Uri.parse(dotGitPath + 'rebase-merge/msgnum'))
.then(f => f.toString().replace(Constants.FinalLineBreakRegex, '')).then(Number.parseInt);
rebaseCommitListTask = workspace.fs.readFile(Uri.parse(dotGitPath + 'rebase-merge/git-rebase-todo'))
.then(f => f.toString().replace(Constants.FinalLineBreakRegex, ''), err => undefined)
.then(GitTextUtils.parseSequencerTodo).then(commits => commits.reverse());
} else {
const rebaseLastIndexTask = workspace.fs.readFile(Uri.parse(dotGitPath + 'rebase-apply/last')).then(f => f.toString().replace(Constants.FinalLineBreakRegex, '')).then(Number.parseInt);
rebaseNextIndex = await workspace.fs.readFile(Uri.parse(dotGitPath + 'rebase-apply/next')).then(f => f.toString().replace(Constants.FinalLineBreakRegex, '')).then(Number.parseInt);
const indices: number[] = [];
for (let i = await rebaseLastIndexTask; i > rebaseNextIndex; i--) {
indices.push(i);
}
rebaseCommitListTask =
Promise.all(
indices.map(
index => workspace.fs.readFile(Uri.parse(dotGitPath + 'rebase-apply/' + index.toString().padStart(4, '0'))).then(f => f.toString().replace(Constants.FinalLineBreakRegex, ''))
.then(GitTextUtils.commitDetailTextToCommit)
));
}
const ontoCommit = await getCommit(repository, await rebaseOntoPathFileTask!);
const ontoBranch = repository.state.refs.find(ref => ref.commit === ontoCommit.hash && ref.type !== RefType.RemoteHead) as MagitBranch;
ontoBranch.commitDetails = ontoCommit;
const doneCommits: Commit[] = (await logTask).slice(0, rebaseNextIndex - 1);
const upcomingCommits: Commit[] = (await rebaseCommitListTask) ?? [];
return {
currentCommit: repository.state.rebaseCommit,
origBranchName: (await rebaseHeadNameFileTask!).split('/')[2],
ontoBranch,
doneCommits,
upcomingCommits
};
}
} catch { }
}

View File

@ -14,6 +14,7 @@ import { BranchListingView } from '../views/branches/branchListingView';
import { RemoteBranchListingView } from '../views/remotes/remoteBranchListingView';
import { TagListingView } from '../views/tags/tagListingView';
import { showStashDetail } from './diffingCommands';
import * as Constants from '../common/constants';
export async function magitVisitAtPoint(repository: MagitRepository, currentView: DocumentView) {
@ -46,7 +47,7 @@ export async function magitVisitAtPoint(repository: MagitRepository, currentView
const stash = (selectedView as StashItemView).stash;
showStashDetail(repository, stash);
} else {
window.setStatusBarMessage('There is no thing at point that could be visited', 10000);
window.setStatusBarMessage('There is no thing at point that could be visited', Constants.StatusMessageDisplayTimeout);
}
}

View File

@ -4,7 +4,7 @@ export const LineSplitterRegex: RegExp = /\r?\n/g;
export const FinalLineBreakRegex: RegExp = /\r?\n$/g;
export const StatusMessageDisplayTimeout: number = 5000;
export const StatusMessageDisplayTimeout: number = 10000;
export const MagitUriScheme: string = 'magit';

View File

@ -20,7 +20,7 @@ import { DocumentView } from './views/general/documentView';
import { magitApplyEntityAtPoint } from './commands/applyAtPointCommands';
import { magitDiscardAtPoint } from './commands/discardAtPointCommands';
import { merging } from './commands/mergingCommands';
import { rebasing, abortInteractiveRebase } from './commands/rebasingCommands';
import { rebasing } from './commands/rebasingCommands';
import { filePopup } from './commands/filePopupCommands';
import { DispatchView } from './views/dispatchView';
import MagitUtils from './utils/magitUtils';
@ -131,8 +131,6 @@ export function activate(context: ExtensionContext) {
context.subscriptions.push(commands.registerTextEditorCommand('magit.quit', quitMagitView));
context.subscriptions.push(commands.registerTextEditorCommand('magit.save-and-close-editor', saveClose));
context.subscriptions.push(commands.registerTextEditorCommand('magit.clear-and-abort-editor', clearSaveClose));
context.subscriptions.push(commands.registerTextEditorCommand('magit.abort-interactive-rebase', Command.primeRepo(abortInteractiveRebase)));
}
export function deactivate() {

View File

@ -107,6 +107,8 @@ export class MenuUtil {
if (menuState.switches) {
_quickPick.items = menuState.switches.map(s => ({ label: s.shortName, detail: s.longName, description: s.description, activated: s.activated }));
_quickPick.selectedItems = _quickPick.items.filter(s => (s as any).activated);
_quickPick.matchOnDescription = true;
_quickPick.matchOnDetail = true;
_quickPick.canSelectMany = true;
_quickPick.title = 'Switches (select with <space>)';
}

View File

@ -58,14 +58,16 @@ export default class GitTextUtils {
if (!todo) {
return [];
}
return todo.split(Constants.LineSplitterRegex).map(line => {
const [label, hash, ...messageParts] = line.split(' ');
return {
hash,
message: messageParts.join(' '),
parents: []
};
});
return todo.split(Constants.LineSplitterRegex)
.filter(line => line.charAt(0) !== '#')
.map(line => {
const [label, hash, ...messageParts] = line.split(' ');
return {
hash,
message: messageParts.join(' '),
parents: []
};
});
}
public static commitDetailTextToCommit(commitText: string): Commit {

View File

@ -114,15 +114,13 @@ export default class MagitUtils {
public static async chooseCommit(repository: MagitRepository, prompt: string): Promise<string> {
const log = await repository.log({ maxEntries: 100 });
const commitPicker = log.map(commit => ({
const commitPicker = repository.magitState?.log.map(commit => ({
label: GitTextUtils.shortHash(commit.hash),
description: commit.message.concat(' ').concat(commit.hash),
meta: commit.hash
}));
})) ?? [];
return QuickMenuUtil.showMenuWithFreeform(commitPicker);
return QuickMenuUtil.showMenuWithFreeform(commitPicker, prompt);
}
public static async chooseTag(repository: MagitRepository, prompt: string) {

View File

@ -81,7 +81,7 @@ export default class MagitStatusView extends DocumentView {
}
} else if (magitState.log.length > 0) {
this.addSubview(new CommitSectionView(Section.RecentCommits, magitState.log));
this.addSubview(new CommitSectionView(Section.RecentCommits, magitState.log.slice(0, 10)));
}
}