mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-25 09:35:37 -05:00
Merge from vscode 718331d6f3ebd1b571530ab499edb266ddd493d5
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
|
||||
import { Model } from '../model';
|
||||
import { Repository as BaseRepository, Resource } from '../repository';
|
||||
import { InputBox, Git, API, Repository, Remote, RepositoryState, Branch, Ref, Submodule, Commit, Change, RepositoryUIState, Status, LogOptions, APIState } from './git';
|
||||
import { InputBox, Git, API, Repository, Remote, RepositoryState, Branch, Ref, Submodule, Commit, Change, RepositoryUIState, Status, LogOptions, APIState, CommitOptions } from './git';
|
||||
import { Event, SourceControlInputBox, Uri, SourceControl } from 'vscode';
|
||||
import { mapEvent } from '../util';
|
||||
import { toGitUri } from '../uri';
|
||||
@@ -202,6 +202,10 @@ export class ApiRepository implements Repository {
|
||||
log(options?: LogOptions): Promise<Commit[]> {
|
||||
return this._repository.log(options);
|
||||
}
|
||||
|
||||
commit(message: string, opts?: CommitOptions): Promise<void> {
|
||||
return this._repository.commit(message, opts);
|
||||
}
|
||||
}
|
||||
|
||||
export class ApiGit implements Git {
|
||||
|
||||
14
extensions/git/src/api/git.d.ts
vendored
14
extensions/git/src/api/git.d.ts
vendored
@@ -41,7 +41,9 @@ export interface Commit {
|
||||
readonly hash: string;
|
||||
readonly message: string;
|
||||
readonly parents: string[];
|
||||
readonly authorEmail?: string | undefined;
|
||||
readonly authorDate?: Date;
|
||||
readonly authorName?: string;
|
||||
readonly authorEmail?: string;
|
||||
}
|
||||
|
||||
export interface Submodule {
|
||||
@@ -119,6 +121,14 @@ export interface LogOptions {
|
||||
readonly maxEntries?: number;
|
||||
}
|
||||
|
||||
export interface CommitOptions {
|
||||
all?: boolean | 'tracked';
|
||||
amend?: boolean;
|
||||
signoff?: boolean;
|
||||
signCommit?: boolean;
|
||||
empty?: boolean;
|
||||
}
|
||||
|
||||
export interface Repository {
|
||||
|
||||
readonly rootUri: Uri;
|
||||
@@ -174,6 +184,8 @@ export interface Repository {
|
||||
|
||||
blame(path: string): Promise<string>;
|
||||
log(options?: LogOptions): Promise<Commit[]>;
|
||||
|
||||
commit(message: string, opts?: CommitOptions): Promise<void>;
|
||||
}
|
||||
|
||||
export type APIState = 'uninitialized' | 'initialized';
|
||||
|
||||
@@ -9,13 +9,14 @@ import * as path from 'path';
|
||||
import { commands, Disposable, LineChange, MessageOptions, OutputChannel, Position, ProgressLocation, QuickPickItem, Range, SourceControlResourceState, TextDocumentShowOptions, TextEditor, Uri, ViewColumn, window, workspace, WorkspaceEdit, WorkspaceFolder } from 'vscode';
|
||||
import TelemetryReporter from 'vscode-extension-telemetry';
|
||||
import * as nls from 'vscode-nls';
|
||||
import { Branch, GitErrorCodes, Ref, RefType, Status } from './api/git';
|
||||
import { CommitOptions, ForcePushMode, Git, Stash } from './git';
|
||||
import { Branch, GitErrorCodes, Ref, RefType, Status, CommitOptions } from './api/git';
|
||||
import { ForcePushMode, Git, Stash } from './git';
|
||||
import { Model } from './model';
|
||||
import { Repository, Resource, ResourceGroupType } from './repository';
|
||||
import { applyLineChanges, getModifiedRange, intersectDiffWithRange, invertLineChange, toLineRanges } from './staging';
|
||||
import { fromGitUri, toGitUri, isGitUri } from './uri';
|
||||
import { grep, isDescendant, pathEquals } from './util';
|
||||
import { Log, LogLevel } from './log';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
@@ -252,6 +253,36 @@ export class CommandCenter {
|
||||
});
|
||||
}
|
||||
|
||||
@command('git.setLogLevel')
|
||||
async setLogLevel(): Promise<void> {
|
||||
const createItem = (logLevel: LogLevel) => ({
|
||||
label: LogLevel[logLevel],
|
||||
logLevel,
|
||||
description: Log.logLevel === logLevel ? localize('current', "Current") : undefined
|
||||
});
|
||||
|
||||
const items = [
|
||||
createItem(LogLevel.Trace),
|
||||
createItem(LogLevel.Debug),
|
||||
createItem(LogLevel.Info),
|
||||
createItem(LogLevel.Warning),
|
||||
createItem(LogLevel.Error),
|
||||
createItem(LogLevel.Critical),
|
||||
createItem(LogLevel.Off)
|
||||
];
|
||||
|
||||
const choice = await window.showQuickPick(items, {
|
||||
placeHolder: localize('select log level', "Select log level")
|
||||
});
|
||||
|
||||
if (!choice) {
|
||||
return;
|
||||
}
|
||||
|
||||
Log.logLevel = choice.logLevel;
|
||||
this.outputChannel.appendLine(localize('changed', "Log level changed to: {0}", LogLevel[Log.logLevel]));
|
||||
}
|
||||
|
||||
@command('git.refresh', { repository: true })
|
||||
async refresh(repository: Repository): Promise<void> {
|
||||
await repository.status();
|
||||
@@ -1292,6 +1323,9 @@ export class CommandCenter {
|
||||
}
|
||||
|
||||
const enableSmartCommit = config.get<boolean>('enableSmartCommit') === true;
|
||||
const enableCommitSigning = config.get<boolean>('enableCommitSigning') === true;
|
||||
const noStagedChanges = repository.indexGroup.resourceStates.length === 0;
|
||||
const noUnstagedChanges = repository.workingTreeGroup.resourceStates.length === 0;
|
||||
|
||||
if (promptToSaveFilesBeforeCommit !== 'never') {
|
||||
let documents = workspace.textDocuments
|
||||
@@ -1312,17 +1346,13 @@ export class CommandCenter {
|
||||
|
||||
if (pick === saveAndCommit) {
|
||||
await Promise.all(documents.map(d => d.save()));
|
||||
await repository.add([]);
|
||||
await repository.add(documents.map(d => d.uri));
|
||||
} else if (pick !== commit) {
|
||||
return false; // do not commit on cancel
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const enableCommitSigning = config.get<boolean>('enableCommitSigning') === true;
|
||||
const noStagedChanges = repository.indexGroup.resourceStates.length === 0;
|
||||
const noUnstagedChanges = repository.workingTreeGroup.resourceStates.length === 0;
|
||||
|
||||
// no changes, and the user has not configured to commit all in this case
|
||||
if (!noUnstagedChanges && noStagedChanges && !enableSmartCommit) {
|
||||
const suggestSmartCommit = config.get<boolean>('suggestSmartCommit') === true;
|
||||
@@ -1590,7 +1620,7 @@ export class CommandCenter {
|
||||
|
||||
const rawBranchName = defaultName || await window.showInputBox({
|
||||
placeHolder: localize('branch name', "Branch name"),
|
||||
prompt: localize('provide branch name', "Please provide a branch name"),
|
||||
prompt: localize('provide branch name', "Please provide a new branch name"),
|
||||
value: initialValue,
|
||||
ignoreFocusOut: true,
|
||||
validateInput: (name: string) => {
|
||||
@@ -2301,6 +2331,23 @@ export class CommandCenter {
|
||||
return result && result.stash;
|
||||
}
|
||||
|
||||
@command('git.openDiff', { repository: false })
|
||||
async openDiff(uri: Uri, lhs: string, rhs: string) {
|
||||
const basename = path.basename(uri.fsPath);
|
||||
|
||||
let title;
|
||||
if ((lhs === 'HEAD' || lhs === '~') && rhs === '') {
|
||||
title = `${basename} (Working Tree)`;
|
||||
}
|
||||
else if (lhs === 'HEAD' && rhs === '~') {
|
||||
title = `${basename} (Index)`;
|
||||
} else {
|
||||
title = `${basename} (${lhs.endsWith('^') ? `${lhs.substr(0, 8)}^` : lhs.substr(0, 8)}) \u27f7 ${basename} (${rhs.endsWith('^') ? `${rhs.substr(0, 8)}^` : rhs.substr(0, 8)})`;
|
||||
}
|
||||
|
||||
return commands.executeCommand('vscode.diff', toGitUri(uri, lhs), rhs === '' ? uri : toGitUri(uri, rhs), title);
|
||||
}
|
||||
|
||||
private createCommand(id: string, key: string, method: Function, options: CommandOptions): (...args: any[]) => any {
|
||||
const result = (...args: any[]) => {
|
||||
let result: Promise<any>;
|
||||
|
||||
@@ -12,10 +12,10 @@ import { EventEmitter } from 'events';
|
||||
import iconv = require('iconv-lite');
|
||||
import * as filetype from 'file-type';
|
||||
import { assign, groupBy, IDisposable, toDisposable, dispose, mkdirp, readBytes, detectUnicodeEncoding, Encoding, onceEvent, splitInChunks, Limiter } from './util';
|
||||
import { CancellationToken, Progress } from 'vscode';
|
||||
import { CancellationToken, Progress, Uri } from 'vscode';
|
||||
import { URI } from 'vscode-uri';
|
||||
import { detectEncoding } from './encoding';
|
||||
import { Ref, RefType, Branch, Remote, GitErrorCodes, LogOptions, Change, Status } from './api/git';
|
||||
import { Ref, RefType, Branch, Remote, GitErrorCodes, LogOptions, Change, Status, CommitOptions } from './api/git';
|
||||
import * as byline from 'byline';
|
||||
import { StringDecoder } from 'string_decoder';
|
||||
|
||||
@@ -45,6 +45,15 @@ interface MutableRemote extends Remote {
|
||||
isReadOnly: boolean;
|
||||
}
|
||||
|
||||
// TODO[ECA]: Move to git.d.ts once we are good with the api
|
||||
/**
|
||||
* Log file options.
|
||||
*/
|
||||
export interface LogFileOptions {
|
||||
/** Max number of log entries to retrieve. If not specified, the default is 32. */
|
||||
readonly maxEntries?: number;
|
||||
}
|
||||
|
||||
function parseVersion(raw: string): string {
|
||||
return raw.replace(/^git version /, '');
|
||||
}
|
||||
@@ -318,7 +327,13 @@ function getGitErrorCode(stderr: string): string | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const COMMIT_FORMAT = '%H\n%ae\n%P\n%B';
|
||||
// https://github.com/microsoft/vscode/issues/89373
|
||||
// https://github.com/git-for-windows/git/issues/2478
|
||||
function sanitizePath(path: string): string {
|
||||
return path.replace(/^([a-z]):\\/i, (_, letter) => `${letter.toUpperCase()}:\\`);
|
||||
}
|
||||
|
||||
const COMMIT_FORMAT = '%H\n%aN\n%aE\n%at\n%P\n%B';
|
||||
|
||||
export class Git {
|
||||
|
||||
@@ -487,6 +502,10 @@ export class Git {
|
||||
LANG: 'en_US.UTF-8'
|
||||
});
|
||||
|
||||
if (options.cwd) {
|
||||
options.cwd = sanitizePath(options.cwd);
|
||||
}
|
||||
|
||||
if (options.log !== false) {
|
||||
this.log(`> git ${args.join(' ')}\n`);
|
||||
}
|
||||
@@ -503,7 +522,9 @@ export interface Commit {
|
||||
hash: string;
|
||||
message: string;
|
||||
parents: string[];
|
||||
authorEmail?: string | undefined;
|
||||
authorDate?: Date;
|
||||
authorName?: string;
|
||||
authorEmail?: string;
|
||||
}
|
||||
|
||||
export class GitStatusParser {
|
||||
@@ -634,14 +655,43 @@ export function parseGitmodules(raw: string): Submodule[] {
|
||||
return result;
|
||||
}
|
||||
|
||||
export function parseGitCommit(raw: string): Commit | null {
|
||||
const match = /^([0-9a-f]{40})\n(.*)\n(.*)(\n([^]*))?$/m.exec(raw.trim());
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
const commitRegex = /([0-9a-f]{40})\n(.*)\n(.*)\n(.*)\n(.*)(?:\n([^]*?))?(?:\x00)/gm;
|
||||
|
||||
const parents = match[3] ? match[3].split(' ') : [];
|
||||
return { hash: match[1], message: match[5], parents, authorEmail: match[2] };
|
||||
export function parseGitCommits(data: string): Commit[] {
|
||||
let commits: Commit[] = [];
|
||||
|
||||
let ref;
|
||||
let name;
|
||||
let email;
|
||||
let date;
|
||||
let parents;
|
||||
let message;
|
||||
let match;
|
||||
|
||||
do {
|
||||
match = commitRegex.exec(data);
|
||||
if (match === null) {
|
||||
break;
|
||||
}
|
||||
|
||||
[, ref, name, email, date, parents, message] = match;
|
||||
|
||||
if (message[message.length - 1] === '\n') {
|
||||
message = message.substr(0, message.length - 1);
|
||||
}
|
||||
|
||||
// Stop excessive memory usage by using substr -- https://bugs.chromium.org/p/v8/issues/detail?id=2869
|
||||
commits.push({
|
||||
hash: ` ${ref}`.substr(1),
|
||||
message: ` ${message}`.substr(1),
|
||||
parents: parents ? parents.split(' ') : [],
|
||||
authorDate: new Date(Number(date) * 1000),
|
||||
authorName: ` ${name}`.substr(1),
|
||||
authorEmail: ` ${email}`.substr(1)
|
||||
});
|
||||
} while (true);
|
||||
|
||||
return commits;
|
||||
}
|
||||
|
||||
interface LsTreeElement {
|
||||
@@ -675,14 +725,6 @@ export function parseLsFiles(raw: string): LsFilesElement[] {
|
||||
.map(([, mode, object, stage, file]) => ({ mode, object, stage, file }));
|
||||
}
|
||||
|
||||
export interface CommitOptions {
|
||||
all?: boolean | 'tracked';
|
||||
amend?: boolean;
|
||||
signoff?: boolean;
|
||||
signCommit?: boolean;
|
||||
empty?: boolean;
|
||||
}
|
||||
|
||||
export interface PullOptions {
|
||||
unshallow?: boolean;
|
||||
tags?: boolean;
|
||||
@@ -760,38 +802,28 @@ export class Repository {
|
||||
|
||||
async log(options?: LogOptions): Promise<Commit[]> {
|
||||
const maxEntries = options && typeof options.maxEntries === 'number' && options.maxEntries > 0 ? options.maxEntries : 32;
|
||||
const args = ['log', '-' + maxEntries, `--pretty=format:${COMMIT_FORMAT}%x00%x00`];
|
||||
const args = ['log', '-' + maxEntries, `--format:${COMMIT_FORMAT}`, '-z'];
|
||||
|
||||
const gitResult = await this.run(args);
|
||||
if (gitResult.exitCode) {
|
||||
const result = await this.run(args);
|
||||
if (result.exitCode) {
|
||||
// An empty repo
|
||||
return [];
|
||||
}
|
||||
|
||||
const s = gitResult.stdout;
|
||||
const result: Commit[] = [];
|
||||
let index = 0;
|
||||
while (index < s.length) {
|
||||
let nextIndex = s.indexOf('\x00\x00', index);
|
||||
if (nextIndex === -1) {
|
||||
nextIndex = s.length;
|
||||
}
|
||||
return parseGitCommits(result.stdout);
|
||||
}
|
||||
|
||||
let entry = s.substr(index, nextIndex - index);
|
||||
if (entry.startsWith('\n')) {
|
||||
entry = entry.substring(1);
|
||||
}
|
||||
async logFile(uri: Uri, options?: LogFileOptions): Promise<Commit[]> {
|
||||
const maxEntries = options?.maxEntries ?? 32;
|
||||
const args = ['log', `-${maxEntries}`, `--format=${COMMIT_FORMAT}`, '-z', '--', uri.fsPath];
|
||||
|
||||
const commit = parseGitCommit(entry);
|
||||
if (!commit) {
|
||||
break;
|
||||
}
|
||||
|
||||
result.push(commit);
|
||||
index = nextIndex + 2;
|
||||
const result = await this.run(args);
|
||||
if (result.exitCode) {
|
||||
// No file history, e.g. a new file or untracked
|
||||
return [];
|
||||
}
|
||||
|
||||
return result;
|
||||
return parseGitCommits(result.stdout);
|
||||
}
|
||||
|
||||
async bufferString(object: string, encoding: string = 'utf8', autoGuessEncoding = false): Promise<string> {
|
||||
@@ -857,12 +889,12 @@ export class Repository {
|
||||
}
|
||||
|
||||
async lstree(treeish: string, path: string): Promise<LsTreeElement[]> {
|
||||
const { stdout } = await this.run(['ls-tree', '-l', treeish, '--', path]);
|
||||
const { stdout } = await this.run(['ls-tree', '-l', treeish, '--', sanitizePath(path)]);
|
||||
return parseLsTree(stdout);
|
||||
}
|
||||
|
||||
async lsfiles(path: string): Promise<LsFilesElement[]> {
|
||||
const { stdout } = await this.run(['ls-files', '--stage', '--', path]);
|
||||
const { stdout } = await this.run(['ls-files', '--stage', '--', sanitizePath(path)]);
|
||||
return parseLsFiles(stdout);
|
||||
}
|
||||
|
||||
@@ -956,7 +988,7 @@ export class Repository {
|
||||
return await this.diffFiles(false);
|
||||
}
|
||||
|
||||
const args = ['diff', '--', path];
|
||||
const args = ['diff', '--', sanitizePath(path)];
|
||||
const result = await this.run(args);
|
||||
return result.stdout;
|
||||
}
|
||||
@@ -969,7 +1001,7 @@ export class Repository {
|
||||
return await this.diffFiles(false, ref);
|
||||
}
|
||||
|
||||
const args = ['diff', ref, '--', path];
|
||||
const args = ['diff', ref, '--', sanitizePath(path)];
|
||||
const result = await this.run(args);
|
||||
return result.stdout;
|
||||
}
|
||||
@@ -982,7 +1014,7 @@ export class Repository {
|
||||
return await this.diffFiles(true);
|
||||
}
|
||||
|
||||
const args = ['diff', '--cached', '--', path];
|
||||
const args = ['diff', '--cached', '--', sanitizePath(path)];
|
||||
const result = await this.run(args);
|
||||
return result.stdout;
|
||||
}
|
||||
@@ -995,7 +1027,7 @@ export class Repository {
|
||||
return await this.diffFiles(true, ref);
|
||||
}
|
||||
|
||||
const args = ['diff', '--cached', ref, '--', path];
|
||||
const args = ['diff', '--cached', ref, '--', sanitizePath(path)];
|
||||
const result = await this.run(args);
|
||||
return result.stdout;
|
||||
}
|
||||
@@ -1015,7 +1047,7 @@ export class Repository {
|
||||
return await this.diffFiles(false, range);
|
||||
}
|
||||
|
||||
const args = ['diff', range, '--', path];
|
||||
const args = ['diff', range, '--', sanitizePath(path)];
|
||||
const result = await this.run(args);
|
||||
|
||||
return result.stdout.trim();
|
||||
@@ -1128,7 +1160,7 @@ export class Repository {
|
||||
args.push('--');
|
||||
|
||||
if (paths && paths.length) {
|
||||
args.push.apply(args, paths);
|
||||
args.push.apply(args, paths.map(sanitizePath));
|
||||
} else {
|
||||
args.push('.');
|
||||
}
|
||||
@@ -1143,13 +1175,13 @@ export class Repository {
|
||||
return;
|
||||
}
|
||||
|
||||
args.push(...paths);
|
||||
args.push(...paths.map(sanitizePath));
|
||||
|
||||
await this.run(args);
|
||||
}
|
||||
|
||||
async stage(path: string, data: string): Promise<void> {
|
||||
const child = this.stream(['hash-object', '--stdin', '-w', '--path', path], { stdio: [null, null, null] });
|
||||
const child = this.stream(['hash-object', '--stdin', '-w', '--path', sanitizePath(path)], { stdio: [null, null, null] });
|
||||
child.stdin!.end(data, 'utf8');
|
||||
|
||||
const { exitCode, stdout } = await exec(child);
|
||||
@@ -1194,7 +1226,7 @@ export class Repository {
|
||||
|
||||
try {
|
||||
if (paths && paths.length > 0) {
|
||||
for (const chunk of splitInChunks(paths, MAX_CLI_LENGTH)) {
|
||||
for (const chunk of splitInChunks(paths.map(sanitizePath), MAX_CLI_LENGTH)) {
|
||||
await this.run([...args, '--', ...chunk]);
|
||||
}
|
||||
} else {
|
||||
@@ -1333,7 +1365,7 @@ export class Repository {
|
||||
}
|
||||
|
||||
async clean(paths: string[]): Promise<void> {
|
||||
const pathsByGroup = groupBy(paths, p => path.dirname(p));
|
||||
const pathsByGroup = groupBy(paths.map(sanitizePath), p => path.dirname(p));
|
||||
const groups = Object.keys(pathsByGroup).map(k => pathsByGroup[k]);
|
||||
|
||||
const limiter = new Limiter(5);
|
||||
@@ -1379,7 +1411,7 @@ export class Repository {
|
||||
}
|
||||
|
||||
if (paths && paths.length) {
|
||||
args.push.apply(args, paths);
|
||||
args.push.apply(args, paths.map(sanitizePath));
|
||||
} else {
|
||||
args.push('.');
|
||||
}
|
||||
@@ -1530,11 +1562,8 @@ export class Repository {
|
||||
|
||||
async blame(path: string): Promise<string> {
|
||||
try {
|
||||
const args = ['blame'];
|
||||
args.push(path);
|
||||
|
||||
let result = await this.run(args);
|
||||
|
||||
const args = ['blame', sanitizePath(path)];
|
||||
const result = await this.run(args);
|
||||
return result.stdout.trim();
|
||||
} catch (err) {
|
||||
if (/^fatal: no such path/.test(err.stderr || '')) {
|
||||
@@ -1853,14 +1882,18 @@ export class Repository {
|
||||
}
|
||||
|
||||
async getCommit(ref: string): Promise<Commit> {
|
||||
const result = await this.run(['show', '-s', `--format=${COMMIT_FORMAT}`, ref]);
|
||||
return parseGitCommit(result.stdout) || Promise.reject<Commit>('bad commit format');
|
||||
const result = await this.run(['show', '-s', `--format=${COMMIT_FORMAT}`, '-z', ref]);
|
||||
const commits = parseGitCommits(result.stdout);
|
||||
if (commits.length === 0) {
|
||||
return Promise.reject<Commit>('bad commit format');
|
||||
}
|
||||
return commits[0];
|
||||
}
|
||||
|
||||
async updateSubmodules(paths: string[]): Promise<void> {
|
||||
const args = ['submodule', 'update', '--'];
|
||||
|
||||
for (const chunk of splitInChunks(paths, MAX_CLI_LENGTH)) {
|
||||
for (const chunk of splitInChunks(paths.map(sanitizePath), MAX_CLI_LENGTH)) {
|
||||
await this.run([...args, ...chunk]);
|
||||
}
|
||||
}
|
||||
|
||||
50
extensions/git/src/log.ts
Normal file
50
extensions/git/src/log.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Event, EventEmitter } from 'vscode';
|
||||
|
||||
/**
|
||||
* The severity level of a log message
|
||||
*/
|
||||
export enum LogLevel {
|
||||
Trace = 1,
|
||||
Debug = 2,
|
||||
Info = 3,
|
||||
Warning = 4,
|
||||
Error = 5,
|
||||
Critical = 6,
|
||||
Off = 7
|
||||
}
|
||||
|
||||
let _logLevel: LogLevel = LogLevel.Info;
|
||||
const _onDidChangeLogLevel = new EventEmitter<LogLevel>();
|
||||
|
||||
export const Log = {
|
||||
/**
|
||||
* Current logging level.
|
||||
*/
|
||||
get logLevel(): LogLevel {
|
||||
return _logLevel;
|
||||
},
|
||||
|
||||
/**
|
||||
* Current logging level.
|
||||
*/
|
||||
set logLevel(logLevel: LogLevel) {
|
||||
if (_logLevel === logLevel) {
|
||||
return;
|
||||
}
|
||||
|
||||
_logLevel = logLevel;
|
||||
_onDidChangeLogLevel.fire(logLevel);
|
||||
},
|
||||
|
||||
/**
|
||||
* An [event](#Event) that fires when the log level has changed.
|
||||
*/
|
||||
get onDidChangeLogLevel(): Event<LogLevel> {
|
||||
return _onDidChangeLogLevel.event;
|
||||
}
|
||||
};
|
||||
@@ -22,6 +22,7 @@ import { GitExtensionImpl } from './api/extension';
|
||||
// import * as path from 'path';
|
||||
// import * as fs from 'fs';
|
||||
import { createIPCServer, IIPCServer } from './ipc/ipcServer';
|
||||
import { GitTimelineProvider } from './timelineProvider';
|
||||
|
||||
const deactivateTasks: { (): Promise<any>; }[] = [];
|
||||
|
||||
@@ -82,7 +83,8 @@ async function createModel(context: ExtensionContext, outputChannel: OutputChann
|
||||
new GitContentProvider(model),
|
||||
new GitFileSystemProvider(model),
|
||||
new GitDecorations(model),
|
||||
new GitProtocolHandler()
|
||||
new GitProtocolHandler(),
|
||||
new GitTimelineProvider(model)
|
||||
);
|
||||
|
||||
await checkGitVersion(info);
|
||||
|
||||
@@ -5,16 +5,17 @@
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { CancellationToken, Command, Disposable, env, Event, EventEmitter, LogLevel, Memento, OutputChannel, ProgressLocation, ProgressOptions, scm, SourceControl, SourceControlInputBox, SourceControlInputBoxValidation, SourceControlInputBoxValidationType, SourceControlResourceDecorations, SourceControlResourceGroup, SourceControlResourceState, ThemeColor, Uri, window, workspace, WorkspaceEdit, Decoration } from 'vscode';
|
||||
import { CancellationToken, Command, Disposable, Event, EventEmitter, Memento, OutputChannel, ProgressLocation, ProgressOptions, scm, SourceControl, SourceControlInputBox, SourceControlInputBoxValidation, SourceControlInputBoxValidationType, SourceControlResourceDecorations, SourceControlResourceGroup, SourceControlResourceState, ThemeColor, Uri, window, workspace, WorkspaceEdit, Decoration } from 'vscode';
|
||||
import * as nls from 'vscode-nls';
|
||||
import { Branch, Change, GitErrorCodes, LogOptions, Ref, RefType, Remote, Status } from './api/git';
|
||||
import { Branch, Change, GitErrorCodes, LogOptions, Ref, RefType, Remote, Status, CommitOptions } from './api/git';
|
||||
import { AutoFetcher } from './autofetch';
|
||||
import { debounce, memoize, throttle } from './decorators';
|
||||
import { Commit, CommitOptions, ForcePushMode, GitError, Repository as BaseRepository, Stash, Submodule } from './git';
|
||||
import { Commit, ForcePushMode, GitError, Repository as BaseRepository, Stash, Submodule, LogFileOptions } from './git';
|
||||
import { StatusBarCommands } from './statusbar';
|
||||
import { toGitUri } from './uri';
|
||||
import { anyEvent, combinedDisposable, debounceEvent, dispose, EmptyDisposable, eventToPromise, filterEvent, find, IDisposable, isDescendant, onceEvent } from './util';
|
||||
import { IFileWatcher, watch } from './watch';
|
||||
import { Log, LogLevel } from './log';
|
||||
|
||||
const timeout = (millis: number) => new Promise(c => setTimeout(c, millis));
|
||||
|
||||
@@ -303,15 +304,22 @@ export const enum Operation {
|
||||
Apply = 'Apply',
|
||||
Blame = 'Blame',
|
||||
Log = 'Log',
|
||||
LogFile = 'LogFile',
|
||||
}
|
||||
|
||||
function isReadOnly(operation: Operation): boolean {
|
||||
switch (operation) {
|
||||
case Operation.Show:
|
||||
case Operation.GetCommitTemplate:
|
||||
case Operation.Blame:
|
||||
case Operation.CheckIgnore:
|
||||
case Operation.Diff:
|
||||
case Operation.FindTrackingBranches:
|
||||
case Operation.GetBranch:
|
||||
case Operation.GetCommitTemplate:
|
||||
case Operation.GetObjectDetails:
|
||||
case Operation.Log:
|
||||
case Operation.LogFile:
|
||||
case Operation.MergeBase:
|
||||
case Operation.Show:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
@@ -457,8 +465,8 @@ class FileEventLogger {
|
||||
private onDotGitFileChange: Event<Uri>,
|
||||
private outputChannel: OutputChannel
|
||||
) {
|
||||
this.logLevelDisposable = env.onDidChangeLogLevel(this.onDidChangeLogLevel, this);
|
||||
this.onDidChangeLogLevel(env.logLevel);
|
||||
this.logLevelDisposable = Log.onDidChangeLogLevel(this.onDidChangeLogLevel, this);
|
||||
this.onDidChangeLogLevel(Log.logLevel);
|
||||
}
|
||||
|
||||
private onDidChangeLogLevel(level: LogLevel): void {
|
||||
@@ -519,7 +527,7 @@ class DotGitWatcher implements IFileWatcher {
|
||||
this.transientDisposables.push(upstreamWatcher);
|
||||
upstreamWatcher.event(this.emitter.fire, this.emitter, this.transientDisposables);
|
||||
} catch (err) {
|
||||
if (env.logLevel <= LogLevel.Error) {
|
||||
if (Log.logLevel <= LogLevel.Error) {
|
||||
this.outputChannel.appendLine(`Failed to watch ref '${upstreamPath}', is most likely packed.\n${err.stack || err}`);
|
||||
}
|
||||
}
|
||||
@@ -682,7 +690,7 @@ export class Repository implements Disposable {
|
||||
onDotGitFileChange = dotGitFileWatcher.event;
|
||||
this.disposables.push(dotGitFileWatcher);
|
||||
} catch (err) {
|
||||
if (env.logLevel <= LogLevel.Error) {
|
||||
if (Log.logLevel <= LogLevel.Error) {
|
||||
outputChannel.appendLine(`Failed to watch '${this.dotGit}', reverting to legacy API file watched. Some events might be lost.\n${err.stack || err}`);
|
||||
}
|
||||
|
||||
@@ -867,6 +875,11 @@ export class Repository implements Disposable {
|
||||
return this.run(Operation.Log, () => this.repository.log(options));
|
||||
}
|
||||
|
||||
logFile(uri: Uri, options?: LogFileOptions): Promise<Commit[]> {
|
||||
// TODO: This probably needs per-uri granularity
|
||||
return this.run(Operation.LogFile, () => this.repository.logFile(uri, options));
|
||||
}
|
||||
|
||||
@throttle
|
||||
async status(): Promise<void> {
|
||||
await this.run(Operation.Status);
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'mocha';
|
||||
import { GitStatusParser, parseGitCommit, parseGitmodules, parseLsTree, parseLsFiles } from '../git';
|
||||
import { GitStatusParser, parseGitCommits, parseGitmodules, parseLsTree, parseLsFiles } from '../git';
|
||||
import * as assert from 'assert';
|
||||
import { splitInChunks } from '../util';
|
||||
|
||||
@@ -189,44 +189,56 @@ suite('git', () => {
|
||||
suite('parseGitCommit', () => {
|
||||
test('single parent commit', function () {
|
||||
const GIT_OUTPUT_SINGLE_PARENT = `52c293a05038d865604c2284aa8698bd087915a1
|
||||
John Doe
|
||||
john.doe@mail.com
|
||||
1580811030
|
||||
8e5a374372b8393906c7e380dbb09349c5385554
|
||||
This is a commit message.`;
|
||||
This is a commit message.\x00`;
|
||||
|
||||
assert.deepEqual(parseGitCommit(GIT_OUTPUT_SINGLE_PARENT), {
|
||||
assert.deepEqual(parseGitCommits(GIT_OUTPUT_SINGLE_PARENT), [{
|
||||
hash: '52c293a05038d865604c2284aa8698bd087915a1',
|
||||
message: 'This is a commit message.',
|
||||
parents: ['8e5a374372b8393906c7e380dbb09349c5385554'],
|
||||
authorDate: new Date(1580811030000),
|
||||
authorName: 'John Doe',
|
||||
authorEmail: 'john.doe@mail.com',
|
||||
});
|
||||
}]);
|
||||
});
|
||||
|
||||
test('multiple parent commits', function () {
|
||||
const GIT_OUTPUT_MULTIPLE_PARENTS = `52c293a05038d865604c2284aa8698bd087915a1
|
||||
John Doe
|
||||
john.doe@mail.com
|
||||
1580811030
|
||||
8e5a374372b8393906c7e380dbb09349c5385554 df27d8c75b129ab9b178b386077da2822101b217
|
||||
This is a commit message.`;
|
||||
This is a commit message.\x00`;
|
||||
|
||||
assert.deepEqual(parseGitCommit(GIT_OUTPUT_MULTIPLE_PARENTS), {
|
||||
assert.deepEqual(parseGitCommits(GIT_OUTPUT_MULTIPLE_PARENTS), [{
|
||||
hash: '52c293a05038d865604c2284aa8698bd087915a1',
|
||||
message: 'This is a commit message.',
|
||||
parents: ['8e5a374372b8393906c7e380dbb09349c5385554', 'df27d8c75b129ab9b178b386077da2822101b217'],
|
||||
authorDate: new Date(1580811030000),
|
||||
authorName: 'John Doe',
|
||||
authorEmail: 'john.doe@mail.com',
|
||||
});
|
||||
}]);
|
||||
});
|
||||
|
||||
test('no parent commits', function () {
|
||||
const GIT_OUTPUT_NO_PARENTS = `52c293a05038d865604c2284aa8698bd087915a1
|
||||
John Doe
|
||||
john.doe@mail.com
|
||||
1580811030
|
||||
|
||||
This is a commit message.`;
|
||||
This is a commit message.\x00`;
|
||||
|
||||
assert.deepEqual(parseGitCommit(GIT_OUTPUT_NO_PARENTS), {
|
||||
assert.deepEqual(parseGitCommits(GIT_OUTPUT_NO_PARENTS), [{
|
||||
hash: '52c293a05038d865604c2284aa8698bd087915a1',
|
||||
message: 'This is a commit message.',
|
||||
parents: [],
|
||||
authorDate: new Date(1580811030000),
|
||||
authorName: 'John Doe',
|
||||
authorEmail: 'john.doe@mail.com',
|
||||
});
|
||||
}]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
30
extensions/git/src/test/index.ts
Normal file
30
extensions/git/src/test/index.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
const path = require('path');
|
||||
const testRunner = require('vscode/lib/testrunner');
|
||||
|
||||
const suite = 'Integration Git Tests';
|
||||
|
||||
const options: any = {
|
||||
ui: 'tdd',
|
||||
useColors: (!process.env.BUILD_ARTIFACTSTAGINGDIRECTORY && process.platform !== 'win32'),
|
||||
timeout: 60000
|
||||
};
|
||||
|
||||
if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) {
|
||||
options.reporter = 'mocha-multi-reporters';
|
||||
options.reporterOptions = {
|
||||
reporterEnabled: 'spec, mocha-junit-reporter',
|
||||
mochaJunitReporterReporterOptions: {
|
||||
testsuitesTitle: `${suite} ${process.platform}`,
|
||||
mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
testRunner.configure(options);
|
||||
|
||||
export = testRunner;
|
||||
127
extensions/git/src/test/smoke.test.ts
Normal file
127
extensions/git/src/test/smoke.test.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'mocha';
|
||||
import * as assert from 'assert';
|
||||
import { workspace, commands, window, Uri, WorkspaceEdit, Range, TextDocument, extensions } from 'vscode';
|
||||
import * as cp from 'child_process';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { GitExtension, API, Repository, Status } from '../api/git';
|
||||
import { eventToPromise } from '../util';
|
||||
|
||||
suite('git smoke test', function () {
|
||||
const cwd = fs.realpathSync(workspace.workspaceFolders![0].uri.fsPath);
|
||||
|
||||
function file(relativePath: string) {
|
||||
return path.join(cwd, relativePath);
|
||||
}
|
||||
|
||||
function uri(relativePath: string) {
|
||||
return Uri.file(file(relativePath));
|
||||
}
|
||||
|
||||
async function open(relativePath: string) {
|
||||
const doc = await workspace.openTextDocument(uri(relativePath));
|
||||
await window.showTextDocument(doc);
|
||||
return doc;
|
||||
}
|
||||
|
||||
async function type(doc: TextDocument, text: string) {
|
||||
const edit = new WorkspaceEdit();
|
||||
const end = doc.lineAt(doc.lineCount - 1).range.end;
|
||||
edit.replace(doc.uri, new Range(end, end), text);
|
||||
await workspace.applyEdit(edit);
|
||||
}
|
||||
|
||||
let git: API;
|
||||
let repository: Repository;
|
||||
|
||||
suiteSetup(async function () {
|
||||
fs.writeFileSync(file('app.js'), 'hello', 'utf8');
|
||||
fs.writeFileSync(file('index.pug'), 'hello', 'utf8');
|
||||
cp.execSync('git init', { cwd });
|
||||
cp.execSync('git config user.name testuser', { cwd });
|
||||
cp.execSync('git config user.email monacotools@microsoft.com', { cwd });
|
||||
cp.execSync('git add .', { cwd });
|
||||
cp.execSync('git commit -m "initial commit"', { cwd });
|
||||
|
||||
// make sure git is activated
|
||||
const ext = extensions.getExtension<GitExtension>('vscode.git');
|
||||
await ext?.activate();
|
||||
git = ext!.exports.getAPI(1);
|
||||
|
||||
if (git.repositories.length === 0) {
|
||||
await eventToPromise(git.onDidOpenRepository);
|
||||
}
|
||||
|
||||
assert.equal(git.repositories.length, 1);
|
||||
assert.equal(fs.realpathSync(git.repositories[0].rootUri.fsPath), cwd);
|
||||
|
||||
repository = git.repositories[0];
|
||||
});
|
||||
|
||||
test('reflects working tree changes', async function () {
|
||||
await commands.executeCommand('workbench.view.scm');
|
||||
|
||||
const appjs = await open('app.js');
|
||||
await type(appjs, ' world');
|
||||
await appjs.save();
|
||||
await repository.status();
|
||||
assert.equal(repository.state.workingTreeChanges.length, 1);
|
||||
repository.state.workingTreeChanges.some(r => r.uri.path === appjs.uri.path && r.status === Status.MODIFIED);
|
||||
|
||||
fs.writeFileSync(file('newfile.txt'), '');
|
||||
const newfile = await open('newfile.txt');
|
||||
await type(newfile, 'hey there');
|
||||
await newfile.save();
|
||||
await repository.status();
|
||||
assert.equal(repository.state.workingTreeChanges.length, 2);
|
||||
repository.state.workingTreeChanges.some(r => r.uri.path === appjs.uri.path && r.status === Status.MODIFIED);
|
||||
repository.state.workingTreeChanges.some(r => r.uri.path === newfile.uri.path && r.status === Status.UNTRACKED);
|
||||
});
|
||||
|
||||
test('opens diff editor', async function () {
|
||||
const appjs = uri('app.js');
|
||||
await commands.executeCommand('git.openChange', appjs);
|
||||
|
||||
assert(window.activeTextEditor);
|
||||
assert.equal(window.activeTextEditor!.document.uri.path, appjs.path);
|
||||
|
||||
// TODO: how do we really know this is a diff editor?
|
||||
});
|
||||
|
||||
test('stages correctly', async function () {
|
||||
const appjs = uri('app.js');
|
||||
const newfile = uri('newfile.txt');
|
||||
|
||||
await commands.executeCommand('git.stage', appjs);
|
||||
assert.equal(repository.state.workingTreeChanges.length, 1);
|
||||
repository.state.workingTreeChanges.some(r => r.uri.path === newfile.path && r.status === Status.UNTRACKED);
|
||||
assert.equal(repository.state.indexChanges.length, 1);
|
||||
repository.state.indexChanges.some(r => r.uri.path === appjs.path && r.status === Status.INDEX_MODIFIED);
|
||||
|
||||
await commands.executeCommand('git.unstage', appjs);
|
||||
assert.equal(repository.state.workingTreeChanges.length, 2);
|
||||
repository.state.workingTreeChanges.some(r => r.uri.path === appjs.path && r.status === Status.MODIFIED);
|
||||
repository.state.workingTreeChanges.some(r => r.uri.path === newfile.path && r.status === Status.UNTRACKED);
|
||||
});
|
||||
|
||||
test('stages, commits changes and verifies outgoing change', async function () {
|
||||
const appjs = uri('app.js');
|
||||
const newfile = uri('newfile.txt');
|
||||
|
||||
await commands.executeCommand('git.stage', appjs);
|
||||
await repository.commit('second commit');
|
||||
assert.equal(repository.state.workingTreeChanges.length, 1);
|
||||
repository.state.workingTreeChanges.some(r => r.uri.path === newfile.path && r.status === Status.UNTRACKED);
|
||||
assert.equal(repository.state.indexChanges.length, 0);
|
||||
|
||||
await commands.executeCommand('git.stageAll', appjs);
|
||||
await repository.commit('third commit');
|
||||
assert.equal(repository.state.workingTreeChanges.length, 0);
|
||||
assert.equal(repository.state.indexChanges.length, 0);
|
||||
});
|
||||
});
|
||||
213
extensions/git/src/timelineProvider.ts
Normal file
213
extensions/git/src/timelineProvider.ts
Normal file
@@ -0,0 +1,213 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as dayjs from 'dayjs';
|
||||
import * as advancedFormat from 'dayjs/plugin/advancedFormat';
|
||||
import * as relativeTime from 'dayjs/plugin/relativeTime';
|
||||
import { CancellationToken, Disposable, Event, EventEmitter, ThemeIcon, TimelineItem, TimelineProvider, Uri, workspace, TimelineChangeEvent } from 'vscode';
|
||||
import { Model } from './model';
|
||||
import { Repository } from './repository';
|
||||
import { debounce } from './decorators';
|
||||
import { Status } from './api/git';
|
||||
|
||||
dayjs.extend(advancedFormat);
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
// TODO[ECA]: Localize all the strings
|
||||
// TODO[ECA]: Localize or use a setting for date format
|
||||
|
||||
export class GitTimelineProvider implements TimelineProvider {
|
||||
private _onDidChange = new EventEmitter<TimelineChangeEvent>();
|
||||
get onDidChange(): Event<TimelineChangeEvent> {
|
||||
return this._onDidChange.event;
|
||||
}
|
||||
|
||||
readonly id = 'git-history';
|
||||
readonly label = 'Git History';
|
||||
|
||||
private _disposable: Disposable;
|
||||
|
||||
private _repo: Repository | undefined;
|
||||
private _repoDisposable: Disposable | undefined;
|
||||
private _repoStatusDate: Date | undefined;
|
||||
|
||||
constructor(private readonly _model: Model) {
|
||||
this._disposable = Disposable.from(
|
||||
_model.onDidOpenRepository(this.onRepositoriesChanged, this),
|
||||
workspace.registerTimelineProvider('*', this),
|
||||
);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._disposable.dispose();
|
||||
}
|
||||
|
||||
async provideTimeline(uri: Uri, _token: CancellationToken): Promise<TimelineItem[]> {
|
||||
// console.log(`GitTimelineProvider.provideTimeline: uri=${uri} state=${this._model.state}`);
|
||||
|
||||
const repo = this._model.getRepository(uri);
|
||||
if (!repo) {
|
||||
this._repoDisposable?.dispose();
|
||||
this._repoStatusDate = undefined;
|
||||
this._repo = undefined;
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
if (this._repo?.root !== repo.root) {
|
||||
this._repoDisposable?.dispose();
|
||||
|
||||
this._repo = repo;
|
||||
this._repoStatusDate = new Date();
|
||||
this._repoDisposable = Disposable.from(
|
||||
repo.onDidChangeRepository(uri => this.onRepositoryChanged(repo, uri)),
|
||||
repo.onDidRunGitStatus(() => this.onRepositoryStatusChanged(repo))
|
||||
);
|
||||
}
|
||||
|
||||
// TODO[ECA]: Ensure that the uri is a file -- if not we could get the history of the repo?
|
||||
|
||||
const commits = await repo.logFile(uri);
|
||||
|
||||
let dateFormatter: dayjs.Dayjs;
|
||||
const items = commits.map<TimelineItem>(c => {
|
||||
let message = c.message;
|
||||
|
||||
const index = message.indexOf('\n');
|
||||
if (index !== -1) {
|
||||
message = `${message.substring(0, index)} \u2026`;
|
||||
}
|
||||
|
||||
dateFormatter = dayjs(c.authorDate);
|
||||
|
||||
const item = new TimelineItem(message, c.authorDate?.getTime() ?? 0);
|
||||
item.id = c.hash;
|
||||
item.iconPath = new (ThemeIcon as any)('git-commit');
|
||||
item.description = `${dateFormatter.fromNow()} \u2022 ${c.authorName}`;
|
||||
item.detail = `${c.authorName} (${c.authorEmail}) \u2014 ${c.hash.substr(0, 8)}\n${dateFormatter.fromNow()} (${dateFormatter.format('MMMM Do, YYYY h:mma')})\n\n${c.message}`;
|
||||
item.command = {
|
||||
title: 'Open Diff',
|
||||
command: 'git.openDiff',
|
||||
arguments: [uri, `${c.hash}^`, c.hash]
|
||||
};
|
||||
|
||||
return item;
|
||||
});
|
||||
|
||||
const index = repo.indexGroup.resourceStates.find(r => r.resourceUri.fsPath === uri.fsPath);
|
||||
if (index) {
|
||||
const date = this._repoStatusDate ?? new Date();
|
||||
dateFormatter = dayjs(date);
|
||||
|
||||
let status;
|
||||
switch (index.type) {
|
||||
case Status.INDEX_MODIFIED:
|
||||
status = 'Modified';
|
||||
break;
|
||||
case Status.INDEX_ADDED:
|
||||
status = 'Added';
|
||||
break;
|
||||
case Status.INDEX_DELETED:
|
||||
status = 'Deleted';
|
||||
break;
|
||||
case Status.INDEX_RENAMED:
|
||||
status = 'Renamed';
|
||||
break;
|
||||
case Status.INDEX_COPIED:
|
||||
status = 'Copied';
|
||||
break;
|
||||
default:
|
||||
status = '';
|
||||
break;
|
||||
}
|
||||
|
||||
const item = new TimelineItem('Staged Changes', date.getTime());
|
||||
item.id = 'index';
|
||||
// TODO[ECA]: Replace with a better icon -- reflecting its status maybe?
|
||||
item.iconPath = new (ThemeIcon as any)('git-commit');
|
||||
item.description = `${dateFormatter.fromNow()} \u2022 You`;
|
||||
item.detail = `You \u2014 Index\n${dateFormatter.fromNow()} (${dateFormatter.format('MMMM Do, YYYY h:mma')})\n${status}`;
|
||||
item.command = {
|
||||
title: 'Open Comparison',
|
||||
command: 'git.openDiff',
|
||||
arguments: [uri, 'HEAD', '~']
|
||||
};
|
||||
|
||||
items.push(item);
|
||||
}
|
||||
|
||||
|
||||
const working = repo.workingTreeGroup.resourceStates.find(r => r.resourceUri.fsPath === uri.fsPath);
|
||||
if (working) {
|
||||
const date = new Date();
|
||||
dateFormatter = dayjs(date);
|
||||
|
||||
let status;
|
||||
switch (working.type) {
|
||||
case Status.INDEX_MODIFIED:
|
||||
status = 'Modified';
|
||||
break;
|
||||
case Status.INDEX_ADDED:
|
||||
status = 'Added';
|
||||
break;
|
||||
case Status.INDEX_DELETED:
|
||||
status = 'Deleted';
|
||||
break;
|
||||
case Status.INDEX_RENAMED:
|
||||
status = 'Renamed';
|
||||
break;
|
||||
case Status.INDEX_COPIED:
|
||||
status = 'Copied';
|
||||
break;
|
||||
default:
|
||||
status = '';
|
||||
break;
|
||||
}
|
||||
|
||||
const item = new TimelineItem('Uncommited Changes', date.getTime());
|
||||
item.id = 'working';
|
||||
// TODO[ECA]: Replace with a better icon -- reflecting its status maybe?
|
||||
item.iconPath = new (ThemeIcon as any)('git-commit');
|
||||
item.description = `${dateFormatter.fromNow()} \u2022 You`;
|
||||
item.detail = `You \u2014 Working Tree\n${dateFormatter.fromNow()} (${dateFormatter.format('MMMM Do, YYYY h:mma')})\n${status}`;
|
||||
item.command = {
|
||||
title: 'Open Comparison',
|
||||
command: 'git.openDiff',
|
||||
arguments: [uri, index ? '~' : 'HEAD', '']
|
||||
};
|
||||
|
||||
items.push(item);
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
private onRepositoriesChanged(_repo: Repository) {
|
||||
// console.log(`GitTimelineProvider.onRepositoriesChanged`);
|
||||
|
||||
// TODO[ECA]: Being naive for now and just always refreshing each time there is a new repository
|
||||
this.fireChanged();
|
||||
}
|
||||
|
||||
private onRepositoryChanged(_repo: Repository, _uri: Uri) {
|
||||
// console.log(`GitTimelineProvider.onRepositoryChanged: uri=${uri.toString(true)}`);
|
||||
|
||||
this.fireChanged();
|
||||
}
|
||||
|
||||
private onRepositoryStatusChanged(_repo: Repository) {
|
||||
// console.log(`GitTimelineProvider.onRepositoryStatusChanged`);
|
||||
|
||||
// This is crappy, but for now just save the last time a status was run and use that as the timestamp for staged items
|
||||
this._repoStatusDate = new Date();
|
||||
|
||||
this.fireChanged();
|
||||
}
|
||||
|
||||
@debounce(500)
|
||||
private fireChanged() {
|
||||
this._onDidChange.fire();
|
||||
}
|
||||
}
|
||||
3
extensions/git/src/typings/refs.d.ts
vendored
3
extensions/git/src/typings/refs.d.ts
vendored
@@ -4,4 +4,5 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/// <reference path='../../../../src/vs/vscode.d.ts'/>
|
||||
/// <reference path='../../../../src/vs/vscode.proposed.d.ts'/>
|
||||
/// <reference path='../../../../src/vs/vscode.proposed.d.ts'/>
|
||||
/// <reference path="../../../types/lib.textEncoder.d.ts" />
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Event } from 'vscode';
|
||||
import { Event, Disposable } from 'vscode';
|
||||
import { dirname, sep } from 'path';
|
||||
import { Readable } from 'stream';
|
||||
import { promises as fs, createReadStream } from 'fs';
|
||||
@@ -33,15 +33,15 @@ export function combinedDisposable(disposables: IDisposable[]): IDisposable {
|
||||
export const EmptyDisposable = toDisposable(() => null);
|
||||
|
||||
export function fireEvent<T>(event: Event<T>): Event<T> {
|
||||
return (listener, thisArgs = null, disposables?) => event(_ => (listener as any).call(thisArgs), null, disposables);
|
||||
return (listener: (e: T) => any, thisArgs?: any, disposables?: Disposable[]) => event(_ => (listener as any).call(thisArgs), null, disposables);
|
||||
}
|
||||
|
||||
export function mapEvent<I, O>(event: Event<I>, map: (i: I) => O): Event<O> {
|
||||
return (listener, thisArgs = null, disposables?) => event(i => listener.call(thisArgs, map(i)), null, disposables);
|
||||
return (listener: (e: O) => any, thisArgs?: any, disposables?: Disposable[]) => event(i => listener.call(thisArgs, map(i)), null, disposables);
|
||||
}
|
||||
|
||||
export function filterEvent<T>(event: Event<T>, filter: (e: T) => boolean): Event<T> {
|
||||
return (listener, thisArgs = null, disposables?) => event(e => filter(e) && listener.call(thisArgs, e), null, disposables);
|
||||
return (listener: (e: T) => any, thisArgs?: any, disposables?: Disposable[]) => event(e => filter(e) && listener.call(thisArgs, e), null, disposables);
|
||||
}
|
||||
|
||||
export function latchEvent<T>(event: Event<T>): Event<T> {
|
||||
@@ -57,7 +57,7 @@ export function latchEvent<T>(event: Event<T>): Event<T> {
|
||||
}
|
||||
|
||||
export function anyEvent<T>(...events: Event<T>[]): Event<T> {
|
||||
return (listener, thisArgs = null, disposables?) => {
|
||||
return (listener: (e: T) => any, thisArgs?: any, disposables?: Disposable[]) => {
|
||||
const result = combinedDisposable(events.map(event => event(i => listener.call(thisArgs, i))));
|
||||
|
||||
if (disposables) {
|
||||
@@ -73,7 +73,7 @@ export function done<T>(promise: Promise<T>): Promise<void> {
|
||||
}
|
||||
|
||||
export function onceEvent<T>(event: Event<T>): Event<T> {
|
||||
return (listener, thisArgs = null, disposables?) => {
|
||||
return (listener: (e: T) => any, thisArgs?: any, disposables?: Disposable[]) => {
|
||||
const result = event(e => {
|
||||
result.dispose();
|
||||
return listener.call(thisArgs, e);
|
||||
@@ -84,7 +84,7 @@ export function onceEvent<T>(event: Event<T>): Event<T> {
|
||||
}
|
||||
|
||||
export function debounceEvent<T>(event: Event<T>, delay: number): Event<T> {
|
||||
return (listener, thisArgs = null, disposables?) => {
|
||||
return (listener: (e: T) => any, thisArgs?: any, disposables?: Disposable[]) => {
|
||||
let timer: NodeJS.Timer;
|
||||
return event(e => {
|
||||
clearTimeout(timer);
|
||||
|
||||
Reference in New Issue
Block a user