{
21 | return commands.getCommands(filterInternal);
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/.storybook/main.js:
--------------------------------------------------------------------------------
1 | const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
2 | const path = require('path');
3 |
4 | module.exports = {
5 | stories: [path.join(__dirname, '../browser/stories/**/*.stories.*')],
6 | addons: ['@storybook/addon-actions', '@storybook/addon-links'],
7 | webpackFinal: async config => {
8 | config.module.rules.push({
9 | test: /\.(ts|tsx)$/,
10 | include: [path.join(__dirname, '../browser/src'), path.join(__dirname, '../browser/stories')],
11 | use: [
12 | {
13 | loader: require.resolve('ts-loader'),
14 | },
15 | {
16 | loader: require.resolve('react-docgen-typescript-loader'),
17 | },
18 | ],
19 | });
20 | config.plugins.push(
21 | new TsconfigPathsPlugin({
22 | configFile: path.join(__dirname, '../browser/tsconfig.json'),
23 | }),
24 | );
25 | config.resolve.extensions.push('.js', '.ts', '.tsx');
26 |
27 | return config;
28 | },
29 | };
30 |
--------------------------------------------------------------------------------
/browser/src/components/Footer/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { Button } from 'react-bootstrap';
3 |
4 | type FooterProps = {
5 | canGoForward: boolean;
6 | canGoBack: boolean;
7 | goForward: () => void;
8 | goBack: () => void;
9 | };
10 | export default function Footer(props: FooterProps) {
11 | return (
12 |
13 |
21 |
29 |
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/src/commands/commit/compare.ts:
--------------------------------------------------------------------------------
1 | import { IGitCompareCommandHandler } from '../../commandHandlers/types';
2 | import { CommitDetails } from '../../common/types';
3 | import { BaseCommitCommand } from '../baseCommitCommand';
4 |
5 | export class CompareCommand extends BaseCommitCommand {
6 | constructor(commit: CommitDetails, private handler: IGitCompareCommandHandler) {
7 | super(commit);
8 | if (handler.selectedCommit) {
9 | const committer = `${commit.logEntry.author!.name} <${
10 | commit.logEntry.author!.email
11 | }> on ${commit.logEntry.author!.date!.toLocaleString()}`;
12 | this.setTitle(`$(git-compare) Compare with ${handler.selectedCommit!.logEntry.hash.short}, ${committer}`);
13 | this.setDetail(commit.logEntry.subject);
14 | }
15 | this.setCommand('git.commit.compare');
16 | this.setCommandArguments([commit]);
17 | }
18 | public async preExecute(): Promise {
19 | return !!this.handler.selectedCommit;
20 | }
21 | public execute() {
22 | this.handler.compare(this.data);
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/browser/src/components/LogView/LogEntryList/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { LogEntry, Ref } from '../../../definitions';
3 | import LogEntryView from '../LogEntry';
4 |
5 | interface ResultProps {
6 | logEntries: LogEntry[];
7 | onViewCommit(entry: LogEntry): void;
8 | onAction(entry: LogEntry, name: string): void;
9 | onRefAction(logEntry: LogEntry, ref: Ref, name: string): void;
10 | }
11 |
12 | export default class LogEntryList extends React.Component {
13 | public ref: HTMLDivElement;
14 | public render() {
15 | if (!Array.isArray(this.props.logEntries)) {
16 | return null;
17 | }
18 |
19 | const results = this.props.logEntries.map(entry => (
20 |
27 | ));
28 | return (this.ref = ref)}>{results}
;
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/jest.extension.config.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line @typescript-eslint/no-var-requires
2 | const path = require('path');
3 |
4 | module.exports = {
5 | testEnvironment: 'node',
6 | // Be specific, as we don't want mocks from other test suites messing this up.
7 | roots: [path.join(__dirname, 'dist/test/extension')],
8 | moduleFileExtensions: ['js'],
9 | testMatch: [path.join(__dirname, 'dist/test/extension/**/*.test.js')],
10 | setupFiles: [path.join(__dirname, 'dist/test/setup.js')],
11 | collectCoverage: true,
12 | verbose: true,
13 | debug: true,
14 | // Must for debugging and VSC integration.
15 | runInBand: true,
16 | cache: false,
17 | // Custom reporter so we can override where messages are written.
18 | // We want output written into console, not process.stdout streams.
19 | // See ./build/postInstall.js
20 | reporters: ['jest-standard-reporter'],
21 | useStderr: true,
22 | // Ensure it dies properly on CI (test step didn't exit).
23 | forceExit: true,
24 | // Ensure it dies properly on CI (test step didn't exit).
25 | detectOpenHandles: true,
26 | };
27 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 DonJayamanne
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/adapter/parsers/types.ts:
--------------------------------------------------------------------------------
1 | import { ActionedDetails, CommittedFile, LogEntry, Status } from '../../types';
2 |
3 | export const IFileStatParser = 'IFileStatParser'; // Symbol.for('IFileStatParser');
4 |
5 | export interface IFileStatParser {
6 | parse(gitRootPath: string, filesWithNumStat: string[], filesWithStats: string[]): CommittedFile[];
7 | }
8 | export const IFileStatStatusParser = Symbol.for('IFileStatStatusParser');
9 | export interface IFileStatStatusParser {
10 | canParse(status: string): boolean;
11 | parse(status: string): Status | undefined;
12 | }
13 |
14 | export const IActionDetailsParser = Symbol.for('IActionDetailsParser');
15 | export interface IActionDetailsParser {
16 | parse(name: string, email: string, unixTime: string): ActionedDetails | undefined;
17 | }
18 | export const ILogParser = Symbol.for('ILogParser');
19 | export interface ILogParser {
20 | parse(
21 | gitRepoPath: string,
22 | summaryEntry: string,
23 | itemEntrySeparator: string,
24 | logFormatArgs: string[],
25 | filesWithNumStat?: string,
26 | filesWithNameStatus?: string,
27 | ): LogEntry;
28 | }
29 |
--------------------------------------------------------------------------------
/src/adapter/avatar/gravatar.ts:
--------------------------------------------------------------------------------
1 | import * as gravatar from 'gravatar';
2 | import { inject, injectable } from 'inversify';
3 | import { IServiceContainer } from '../../ioc/types';
4 | import { Avatar, IGitService } from '../../types';
5 | import { GitOriginType } from '../repository/types';
6 | import { BaseAvatarProvider } from './base';
7 | import { IAvatarProvider } from './types';
8 |
9 | @injectable()
10 | export class GravatarAvatarProvider extends BaseAvatarProvider implements IAvatarProvider {
11 | public constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) {
12 | super(serviceContainer, GitOriginType.any);
13 | }
14 | protected async getAvatarsImplementation(repository: IGitService): Promise {
15 | const authors = await repository.getAuthors();
16 |
17 | return authors.map(user => {
18 | return {
19 | login: user.name,
20 | url: '',
21 | avatarUrl: gravatar.url(user.email, { protocol: 'https' }),
22 | name: user.name,
23 | email: user.email,
24 | };
25 | });
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/browser/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "sourceMap": true,
4 | "target": "es6",
5 | "jsx": "react",
6 | "module": "commonjs",
7 | "moduleResolution": "node",
8 | "emitDecoratorMetadata": true,
9 | "experimentalDecorators": true,
10 | "declaration": false,
11 | "noImplicitAny": false,
12 | "noImplicitReturns": false,
13 | "removeComments": true,
14 | "strictNullChecks": false,
15 | "outDir": "build",
16 | "lib": [
17 | "es6",
18 | "dom"
19 | ],
20 | "types": [],
21 | "strict": true,
22 | "noFallthroughCasesInSwitch": true,
23 | "noImplicitThis": true,
24 | "noUnusedLocals": true,
25 | "noUnusedParameters": false,
26 | "suppressImplicitAnyIndexErrors": true,
27 | "baseUrl": ".",
28 | "rootDirs": [
29 | "src",
30 | "stories"
31 | ],
32 | },
33 | "include": [
34 | "src/**/*"
35 | ],
36 | "exclude": [
37 | "dist",
38 | "build",
39 | "node_modules"
40 | ]
41 | }
42 |
--------------------------------------------------------------------------------
/test/non-extension/adapter/parsers/actionDetails/parser.test.ts:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai';
2 | import { ActionDetailsParser } from '../../../../../src/adapter/parsers/actionDetails/parser';
3 |
4 | describe('Adapter Parser ActionDetails', () => {
5 | test('Information is returned correctly', () => {
6 | const date = new Date();
7 | const name = `Don Jayamanne ${date.getTime()}`;
8 | const email = `don.jayamanne@yahoo.com ${date.getTime()}`;
9 | const unixTime = (date.getTime() / 1000).toString();
10 | const info = new ActionDetailsParser().parse(name, email, unixTime)!;
11 | expect(info).to.have.property('email', email, 'email property not in parsed details');
12 | expect(info).to.have.property('name', name, 'name property not in parsed details');
13 | expect(info.date.toLocaleDateString()).is.equal(date.toLocaleDateString(), 'time is incorrect');
14 | });
15 |
16 | test('Undefined is returned if information is empty', () => {
17 | const info = new ActionDetailsParser().parse('', '', '')!;
18 | expect(info).to.be.an('undefined', 'Action details must be undefined');
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/browser/src/components/LogView/Commit/Avatar/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { connect } from 'react-redux';
3 | import { ActionedDetails } from '../../../../definitions';
4 | import { AvatarsState, RootState } from '../../../../reducers/index';
5 |
6 | type AvatarProps = {
7 | result: ActionedDetails;
8 | avatars: AvatarsState;
9 | };
10 |
11 | function Avatar(props: AvatarProps) {
12 | let avatarUrl = '';
13 | if (props.result) {
14 | const avatar = props.avatars.find(
15 | item =>
16 | item.name === props.result.name ||
17 | item.login === props.result.name ||
18 | item.email === props.result.email,
19 | );
20 | avatarUrl = avatar ? avatar.avatarUrl : '';
21 | }
22 | if (avatarUrl) {
23 | return
;
24 | } else {
25 | return null;
26 | }
27 | }
28 |
29 | function mapStateToProps(state: RootState, wrapper: { result: ActionedDetails }) {
30 | return {
31 | avatars: state.avatars,
32 | result: wrapper.result,
33 | };
34 | }
35 |
36 | export default connect(mapStateToProps)(Avatar);
37 |
--------------------------------------------------------------------------------
/src/common/log.ts:
--------------------------------------------------------------------------------
1 | import { injectable } from 'inversify';
2 | import { Disposable, workspace } from 'vscode';
3 | import { ILogService } from './types';
4 |
5 | @injectable()
6 | export class Logger implements ILogService {
7 | private enabled = true;
8 | private traceEnabled = false;
9 | private disposable: Disposable;
10 | constructor() {
11 | this.updateEnabledFlag();
12 | this.disposable = workspace.onDidChangeConfiguration(() => this.updateEnabledFlag());
13 | }
14 | public dispose() {
15 | this.disposable.dispose();
16 | }
17 | public log(...args: any[]): void {
18 | if (!this.enabled) {
19 | return;
20 | }
21 | console.log(...args);
22 | }
23 | public error(...args: any[]): void {
24 | console.error(...args);
25 | }
26 | public trace(...args: any[]): void {
27 | if (!this.enabled) {
28 | return;
29 | }
30 | if (!this.traceEnabled) {
31 | return;
32 | }
33 | console.warn(...args);
34 | }
35 | private updateEnabledFlag() {
36 | const logLevel = workspace.getConfiguration('gitHistory').get('logLevel', 'None');
37 | this.traceEnabled = logLevel === 'Debug';
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/resources/fileicons/vs_minimal_icons.json:
--------------------------------------------------------------------------------
1 | {
2 | "iconDefinitions": {
3 | "_folder_dark": {
4 | "iconPath": "./images/Folder_16x_inverse.svg"
5 | },
6 | "_folder_open_dark": {
7 | "iconPath": "./images/FolderOpen_16x_inverse.svg"
8 | },
9 | "_file_dark": {
10 | "iconPath": "./images/Document_16x_inverse.svg"
11 | },
12 | "_folder_light": {
13 | "iconPath": "./images/Folder_16x.svg"
14 | },
15 | "_folder_open_light": {
16 | "iconPath": "./images/FolderOpen_16x.svg"
17 | },
18 | "_file_light": {
19 | "iconPath": "./images/Document_16x.svg"
20 | }
21 | },
22 |
23 | "folderExpanded": "_folder_open_dark",
24 | "folder": "_folder_dark",
25 | "file": "_file_dark",
26 | "fileExtensions": {
27 | // icons by file extension
28 | },
29 | "fileNames": {
30 | // icons by file name
31 | },
32 | "languageIds": {
33 | // icons by language id
34 | },
35 | "light": {
36 | "folderExpanded": "_folder_open_light",
37 | "folder": "_folder_light",
38 | "file": "_file_light",
39 | "fileExtensions": {
40 | // icons by file extension
41 | },
42 | "fileNames": {
43 | // icons by file name
44 | },
45 | "languageIds": {
46 | // icons by language id
47 | }
48 | },
49 | "highContrast": {
50 | // overrides for high contrast
51 | }
52 | }
--------------------------------------------------------------------------------
/browser/src/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import * as ReactDOM from 'react-dom';
3 | import { Provider } from 'react-redux';
4 | import { BrowserRouter as Router, Route } from 'react-router-dom';
5 | import { ResultActions } from './actions/results';
6 | import { initialize } from './actions/messagebus';
7 | import App from './containers/App';
8 | import { ISettings } from './definitions';
9 | import configureStore from './store';
10 |
11 | const defaultSettings: ISettings = window['settings'];
12 |
13 | const store = configureStore({
14 | settings: defaultSettings,
15 | graph: {},
16 | vscode: {
17 | theme: document.body.className,
18 | locale: window['locale'],
19 | configuration: window['configuration'],
20 | },
21 | });
22 |
23 | ReactDOM.render(
24 |
25 |
26 |
27 |
28 |
29 |
30 |
,
31 | document.getElementById('root'),
32 | );
33 |
34 | initialize(window['vscode']);
35 |
36 | store.dispatch(ResultActions.getCommits());
37 | store.dispatch(ResultActions.getBranches());
38 | store.dispatch(ResultActions.getAuthors());
39 | store.dispatch(ResultActions.fetchAvatars());
40 |
--------------------------------------------------------------------------------
/src/nodes/factory.ts:
--------------------------------------------------------------------------------
1 | import { injectable } from 'inversify';
2 | import { CommitDetails, CompareCommitDetails, CompareFileCommitDetails, FileCommitDetails } from '../common/types';
3 | import { CommittedFile } from '../types';
4 | import { DirectoryNode, FileNode, INodeFactory } from './types';
5 |
6 | @injectable()
7 | export class StandardNodeFactory implements INodeFactory {
8 | public createDirectoryNode(commit: CommitDetails, relativePath: string) {
9 | return new DirectoryNode(commit, relativePath);
10 | }
11 | public createFileNode(commit: CommitDetails, committedFile: CommittedFile) {
12 | return new FileNode(
13 | new FileCommitDetails(commit.workspaceFolder, commit.branch, commit.logEntry, committedFile),
14 | );
15 | }
16 | }
17 |
18 | @injectable()
19 | export class ComparisonNodeFactory implements INodeFactory {
20 | public createDirectoryNode(commit: CommitDetails, relativePath: string) {
21 | return new DirectoryNode(commit, relativePath);
22 | }
23 | public createFileNode(commit: CommitDetails, committedFile: CommittedFile) {
24 | const compareCommit = commit as CompareCommitDetails;
25 |
26 | return new FileNode(new CompareFileCommitDetails(compareCommit, compareCommit.rightCommit, committedFile));
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/browser/src/actions/messagebus.ts:
--------------------------------------------------------------------------------
1 | const vsc = {
2 | postMessage: (message: any) => {
3 | /*Noop*/
4 | },
5 | };
6 |
7 | function uuid() {
8 | return (
9 | '_' +
10 | Math.random()
11 | .toString(36)
12 | .substr(2, 9)
13 | );
14 | }
15 |
16 | function createPromiseFromMessageEvent(requestId): Promise {
17 | return new Promise((resolve, reject) => {
18 | const handleEvent = (e: MessageEvent) => {
19 | if (requestId === e.data.requestId) {
20 | window.removeEventListener('message', handleEvent);
21 |
22 | if (e.data.error) {
23 | reject(e.data.error);
24 | } else {
25 | resolve(e.data.payload);
26 | }
27 | }
28 | };
29 |
30 | window.addEventListener('message', handleEvent);
31 | });
32 | }
33 |
34 | export function post(cmd: string, payload: any): Promise {
35 | const requestId = uuid();
36 |
37 | vsc.postMessage({
38 | requestId,
39 | cmd,
40 | payload,
41 | });
42 |
43 | return createPromiseFromMessageEvent(requestId);
44 | }
45 |
46 | export function initialize(vscodeApi: any) {
47 | vsc.postMessage = vscodeApi.postMessage.bind();
48 | }
49 |
--------------------------------------------------------------------------------
/src/commands/fileCommit/compareFileWithWorkspace.ts:
--------------------------------------------------------------------------------
1 | import * as path from 'path';
2 | import { IGitFileHistoryCommandHandler } from '../../commandHandlers/types';
3 | import { FileCommitDetails } from '../../common/types';
4 | import { IServiceContainer } from '../../ioc/types';
5 | import { IFileSystem } from '../../platform/types';
6 | import { BaseFileCommitCommand } from '../baseFileCommitCommand';
7 |
8 | export class CompareFileWithWorkspaceCommand extends BaseFileCommitCommand {
9 | constructor(
10 | fileCommit: FileCommitDetails,
11 | private handler: IGitFileHistoryCommandHandler,
12 | private serviceContainer: IServiceContainer,
13 | ) {
14 | super(fileCommit);
15 | this.setTitle('$(git-compare) Compare against workspace file');
16 | this.setCommand('git.commit.FileEntry.CompareAgainstWorkspace');
17 | this.setCommandArguments([fileCommit]);
18 | }
19 | public async preExecute(): Promise {
20 | const localFile = path.join(this.data.workspaceFolder, this.data.committedFile.relativePath);
21 | const fileSystem = this.serviceContainer.get(IFileSystem);
22 | return fileSystem.fileExistsAsync(localFile);
23 | }
24 | public execute() {
25 | this.handler.compareFileWithWorkspace(this.data);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/browser/src/reducers/index.ts:
--------------------------------------------------------------------------------
1 | import { routerReducer as routing } from 'react-router-redux';
2 | import { combineReducers } from 'redux';
3 | import { ActionedUser, Avatar, ISettings, LogEntriesResponse, RefType } from '../definitions';
4 | import authors from './authors';
5 | import avatars from './avatars';
6 | import branches from './branches';
7 | import settings from './settings';
8 | import { default as graph, IGraphState } from './graph';
9 | import logEntries from './logEntries';
10 | import vscode, { IVSCodeSettings } from './vscode';
11 |
12 | export type LogEntriesState = LogEntriesResponse & {
13 | isLoading: boolean;
14 | isLoadingCommit?: string;
15 | };
16 |
17 | export type BranchesState = { name: string; type: RefType; current: boolean; remote: string; remoteType: number }[];
18 | export type AuthorsState = ActionedUser[];
19 | export type AvatarsState = Avatar[];
20 | export type RootState = {
21 | vscode: IVSCodeSettings;
22 | logEntries?: LogEntriesState;
23 | branches?: BranchesState;
24 | avatars?: AvatarsState;
25 | authors?: AuthorsState;
26 | settings?: ISettings;
27 | graph: IGraphState;
28 | };
29 |
30 | export default combineReducers({
31 | routing,
32 | avatars,
33 | authors,
34 | logEntries,
35 | branches,
36 | settings,
37 | graph,
38 | vscode,
39 | } as any);
40 |
--------------------------------------------------------------------------------
/src/ioc/types.ts:
--------------------------------------------------------------------------------
1 | export interface Newable {
2 | new (...args: any[]): T;
3 | }
4 | export interface Abstract {
5 | prototype: T;
6 | }
7 | export type ServiceIdentifier = string | symbol | Newable | Abstract;
8 |
9 | export type ClassType = new (...args: any[]) => T;
10 |
11 | export const IServiceManager = Symbol.for('IServiceManager');
12 |
13 | export interface IServiceManager {
14 | add(serviceIdentifier: ServiceIdentifier, constructor: ClassType, name?: string | number | symbol): void;
15 | addSingleton(
16 | serviceIdentifier: ServiceIdentifier,
17 | constructor: ClassType,
18 | name?: string | number | symbol,
19 | ): void;
20 | addSingletonInstance(
21 | serviceIdentifier: ServiceIdentifier,
22 | instance: T,
23 | name?: string | number | symbol,
24 | ): void;
25 | get(serviceIdentifier: ServiceIdentifier, name?: string | number | symbol): T;
26 | getAll(serviceIdentifier: ServiceIdentifier, name?: string | number | symbol): T[];
27 | }
28 |
29 | export const IServiceContainer = Symbol.for('IServiceContainer');
30 | export interface IServiceContainer {
31 | get(serviceIdentifier: ServiceIdentifier, name?: string | number | symbol): T;
32 | getAll(serviceIdentifier: ServiceIdentifier, name?: string | number | symbol): T[];
33 | }
34 |
--------------------------------------------------------------------------------
/src/adapter/repository/types.ts:
--------------------------------------------------------------------------------
1 | // import { FsUri } from '../../types';
2 |
3 | export type GitLogArgs = {
4 | logArgs: string[];
5 | fileStatArgs: string[];
6 | counterArgs: string[];
7 | };
8 | export const IGitArgsService = Symbol.for('IGitArgsService');
9 |
10 | export interface IGitArgsService {
11 | getAuthorsArgs(): string[];
12 | getCommitArgs(hash: string): string[];
13 | getCommitParentHashesArgs(hash: string): string[];
14 | getCommitWithNumStatArgs(hash: string): string[];
15 | getCommitNameStatusArgs(hash: string): string[];
16 | getCommitWithNumStatArgsForMerge(hash: string): string[];
17 | getCommitNameStatusArgsForMerge(hash: string): string[];
18 | getObjectHashArgs(object: string): string[];
19 | getLogArgs(
20 | pageIndex?: number,
21 | pageSize?: number,
22 | branches?: string[],
23 | searchText?: string,
24 | relativeFilePath?: string,
25 | lineNumber?: number,
26 | author?: string,
27 | ): GitLogArgs;
28 | getDiffCommitWithNumStatArgs(hash1: string, hash2: string): string[];
29 | getDiffCommitNameStatusArgs(hash1: string, hash2: string): string[];
30 | getPreviousCommitHashForFileArgs(hash: string, file: string): string[];
31 | }
32 |
33 | export enum GitOriginType {
34 | any = 1,
35 | github = 2,
36 | bitbucket = 3,
37 | tfs = 4,
38 | vsts = 5,
39 | }
40 |
--------------------------------------------------------------------------------
/src/application/serviceRegistry.ts:
--------------------------------------------------------------------------------
1 | import { IServiceManager } from '../ioc/types';
2 | import { ApplicationShell } from './applicationShell';
3 | import { CommandManager } from './commandManager';
4 | import { DisposableRegistry } from './disposableRegistry';
5 | import { DocumentManager } from './documentManager';
6 | import { WorkspaceStateStoreFactory } from './stateStore';
7 | import { IApplicationShell } from './types';
8 | import { ICommandManager } from './types/commandManager';
9 | import { IDisposableRegistry } from './types/disposableRegistry';
10 | import { IDocumentManager } from './types/documentManager';
11 | import { IStateStoreFactory } from './types/stateStore';
12 | import { IWorkspaceService } from './types/workspace';
13 | import { WorkspaceService } from './workspace';
14 |
15 | export function registerTypes(serviceManager: IServiceManager) {
16 | serviceManager.addSingleton(IApplicationShell, ApplicationShell);
17 | serviceManager.addSingleton(ICommandManager, CommandManager);
18 | serviceManager.addSingleton(IDisposableRegistry, DisposableRegistry);
19 | serviceManager.addSingleton(IDocumentManager, DocumentManager);
20 | serviceManager.addSingleton(IWorkspaceService, WorkspaceService);
21 | serviceManager.add(IStateStoreFactory, WorkspaceStateStoreFactory);
22 | }
23 |
--------------------------------------------------------------------------------
/test/common.ts:
--------------------------------------------------------------------------------
1 | import * as path from 'path';
2 | import * as fs from 'fs-extra';
3 |
4 | export const extensionRootPath = path.resolve(__dirname, '..', '..');
5 | export const tempFolder = path.join(extensionRootPath, 'temp');
6 | export const tempRepoFolder = path.join(extensionRootPath, 'temp', 'testing');
7 | export const noop = () => {
8 | //Noop.
9 | };
10 | export const sleep = (timeout: number) => new Promise(resolve => setTimeout(resolve, timeout));
11 |
12 | /**
13 | * Wait for a condition to be fulfilled within a timeout.
14 | */
15 | export async function waitForCondition(
16 | condition: () => Promise,
17 | timeoutMs: number,
18 | errorMessage: string,
19 | ): Promise {
20 | return new Promise(async (resolve, reject) => {
21 | const timeout = setTimeout(() => {
22 | clearTimeout(timeout);
23 | // tslint:disable-next-line: no-use-before-declare
24 | clearTimeout(timer);
25 | reject(new Error(errorMessage));
26 | }, timeoutMs);
27 | const timer = setInterval(async () => {
28 | if (!(await condition().catch(() => false))) {
29 | return;
30 | }
31 | clearTimeout(timeout);
32 | clearTimeout(timer);
33 | resolve();
34 | }, 10);
35 | });
36 | }
37 |
38 | fs.mkdirpSync(tempFolder);
39 | fs.mkdirpSync(tempRepoFolder);
40 |
--------------------------------------------------------------------------------
/src/application/stateStore.ts:
--------------------------------------------------------------------------------
1 | import { inject, injectable } from 'inversify';
2 | import { Memento } from 'vscode';
3 | import { IServiceContainer } from '../ioc/types';
4 | import { IStateStore, IStateStoreFactory } from './types/stateStore';
5 |
6 | export class WorkspaceMementoStore implements IStateStore {
7 | constructor(private store: Memento) {}
8 | public has(key: string): boolean {
9 | return this.store.get(key) !== undefined;
10 | }
11 | public async set(key: string, data: T): Promise {
12 | await this.store.update(key, data);
13 | }
14 | public get(key: string): T | undefined {
15 | return this.store.get(key);
16 | }
17 | }
18 |
19 | @injectable()
20 | export class WorkspaceStateStoreFactory implements IStateStoreFactory {
21 | constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) {}
22 |
23 | public createStore(): IStateStore {
24 | return new WorkspaceMementoStore(this.serviceContainer.get('workspaceMementoStore'));
25 | }
26 | }
27 |
28 | @injectable()
29 | export class GlobalStateStoreFactory implements IStateStoreFactory {
30 | constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) {}
31 |
32 | public createStore(): IStateStore {
33 | return new WorkspaceMementoStore(this.serviceContainer.get('globalMementoStore'));
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/commandHandlers/commit/gitCommitDetails.ts:
--------------------------------------------------------------------------------
1 | import { inject, injectable } from 'inversify';
2 | import { ICommandManager } from '../../application/types/commandManager';
3 | import { CommitDetails } from '../../common/types';
4 | import { IServiceContainer } from '../../ioc/types';
5 | import { ICommitViewerFactory } from '../../viewers/types';
6 | import { IGitCommitViewDetailsCommandHandler } from '../types';
7 |
8 | @injectable()
9 | export class GitCommitViewDetailsCommandHandler implements IGitCommitViewDetailsCommandHandler {
10 | constructor(
11 | @inject(IServiceContainer) private serviceContainer: IServiceContainer,
12 | @inject(ICommandManager) private commandManager: ICommandManager,
13 | ) {}
14 |
15 | public async viewDetails(commit: CommitDetails) {
16 | await this.commandManager.executeCommand('setContext', 'git.commit.selected', true);
17 | this.serviceContainer
18 | .get(ICommitViewerFactory)
19 | .getCommitViewer()
20 | .showCommit(commit);
21 | }
22 |
23 | public async viewCommitTree(commit: CommitDetails) {
24 | await this.commandManager.executeCommand('setContext', 'git.commit.selected', true);
25 | this.serviceContainer
26 | .get(ICommitViewerFactory)
27 | .getCommitViewer()
28 | .showCommitTree(commit);
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/test/non-extension/__mocks__/vsc/strings.ts:
--------------------------------------------------------------------------------
1 | /*---------------------------------------------------------------------------------------------
2 | * Copyright (c) Microsoft Corporation. All rights reserved.
3 | * Licensed under the MIT License. See License.txt in the project root for license information.
4 | *--------------------------------------------------------------------------------------------*/
5 | 'use strict';
6 |
7 | /* eslint-disable */
8 |
9 | export namespace vscMockStrings {
10 | /**
11 | * Determines if haystack starts with needle.
12 | */
13 | export function startsWith(haystack: string, needle: string): boolean {
14 | if (haystack.length < needle.length) {
15 | return false;
16 | }
17 |
18 | for (let i = 0; i < needle.length; i++) {
19 | if (haystack[i] !== needle[i]) {
20 | return false;
21 | }
22 | }
23 |
24 | return true;
25 | }
26 |
27 | /**
28 | * Determines if haystack ends with needle.
29 | */
30 | export function endsWith(haystack: string, needle: string): boolean {
31 | let diff = haystack.length - needle.length;
32 | if (diff > 0) {
33 | return haystack.indexOf(needle, diff) === diff;
34 | } else if (diff === 0) {
35 | return haystack === needle;
36 | } else {
37 | return false;
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/application/documentManager.ts:
--------------------------------------------------------------------------------
1 | import { injectable } from 'inversify';
2 | import { TextDocument, TextDocumentShowOptions, TextEditor, Uri, ViewColumn, window, workspace } from 'vscode';
3 | import { IDocumentManager } from './types/documentManager';
4 |
5 | @injectable()
6 | export class DocumentManager implements IDocumentManager {
7 | public openTextDocument(uri: Uri): Thenable;
8 | public openTextDocument(fileName: string): Thenable;
9 | public openTextDocument(
10 | options?: { language?: string | undefined; content?: string | undefined } | undefined,
11 | ): Thenable;
12 | public openTextDocument(options?: any);
13 | public openTextDocument(...args: any[]) {
14 | return workspace.openTextDocument.call(window, ...args);
15 | }
16 | public showTextDocument(
17 | document: TextDocument,
18 | column?: ViewColumn | undefined,
19 | preserveFocus?: boolean | undefined,
20 | ): Thenable;
21 | public showTextDocument(
22 | document: TextDocument,
23 | options?: TextDocumentShowOptions | undefined,
24 | ): Thenable;
25 | public showTextDocument(uri: Uri, options?: TextDocumentShowOptions | undefined): Thenable;
26 | public showTextDocument(document: any, column?: any, preserveFocus?: any);
27 | public showTextDocument(...args: any[]) {
28 | // @ts-ignore
29 | return window.showTextDocument.call(window, ...args);
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/commandHandlers/commit/gitCheckout.ts:
--------------------------------------------------------------------------------
1 | import { inject, injectable } from 'inversify';
2 | import { IApplicationShell } from '../../application/types';
3 | import { CommitDetails } from '../../common/types';
4 | import { IServiceContainer } from '../../ioc/types';
5 | import { IGitServiceFactory } from '../../types';
6 | import { ICommitViewerFactory } from '../../viewers/types';
7 | import { command } from '../registration';
8 | import { IGitCheckoutCommandHandler } from '../types';
9 |
10 | @injectable()
11 | export class GitCheckoutCommandHandler implements IGitCheckoutCommandHandler {
12 | constructor(
13 | @inject(IServiceContainer) private serviceContainer: IServiceContainer,
14 | @inject(ICommitViewerFactory) private commitViewerFactory: ICommitViewerFactory,
15 | @inject(IApplicationShell) private applicationShell: IApplicationShell,
16 | ) {}
17 |
18 | @command('git.commit.checkout', IGitCheckoutCommandHandler)
19 | public async checkoutCommit(commit: CommitDetails) {
20 | commit = commit ? commit : this.commitViewerFactory.getCommitViewer().selectedCommit;
21 | const gitService = await this.serviceContainer
22 | .get(IGitServiceFactory)
23 | .createGitService(commit.workspaceFolder);
24 |
25 | gitService.checkout(commit.logEntry.hash.full).catch(err => {
26 | if (typeof err === 'string') {
27 | this.applicationShell.showErrorMessage(err);
28 | }
29 | });
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/formatters/commitFormatter.ts:
--------------------------------------------------------------------------------
1 | import { injectable } from 'inversify';
2 | import { EOL } from 'os';
3 | import { LogEntry } from '../types';
4 | import { ICommitViewFormatter } from './types';
5 |
6 | @injectable()
7 | export class CommitViewFormatter implements ICommitViewFormatter {
8 | public format(item: LogEntry): string {
9 | const sb: string[] = [];
10 |
11 | if (item.hash && item.hash.full) {
12 | sb.push(`sha1 : ${item.hash.full}`);
13 | }
14 | if (item.author) {
15 | sb.push(this.formatAuthor(item));
16 | }
17 | if (item.author && item.author.date) {
18 | const authorDate = item.author.date.toLocaleString();
19 | sb.push(`Author Date : ${authorDate}`);
20 | }
21 | if (item.committer) {
22 | sb.push(`Committer : ${item.committer.name} <${item.committer.email}>`);
23 | }
24 | if (item.committer && item.committer.date) {
25 | const committerDate = item.committer.date.toLocaleString();
26 | sb.push(`Commit Date : ${committerDate}`);
27 | }
28 | if (item.subject) {
29 | sb.push(`Subject : ${item.subject}`);
30 | }
31 | if (item.body) {
32 | sb.push(`Body : ${item.body}`);
33 | }
34 | if (item.notes) {
35 | sb.push(`Notes : ${item.notes}`);
36 | }
37 |
38 | return sb.join(EOL);
39 | }
40 | public formatAuthor(logEntry: LogEntry): string {
41 | return `Author : ${logEntry.author!.name} <${logEntry.author!.email}>`;
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/browser/stories/index.stories.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { Author as HeaderAuthors } from '../src/components/Header/author';
3 | import { Author } from '../src/components/LogView/Commit/Author';
4 | import { storiesOf } from '@storybook/react';
5 | import { action } from '@storybook/addon-actions';
6 | import { linkTo } from '@storybook/addon-links';
7 | import { Welcome } from '@storybook/react/demo';
8 |
9 | storiesOf('Welcome', module).add('to Storybook', () => );
10 |
11 | storiesOf('RoundedButton', module)
12 | .add('with text', () => Hello Button12341234
, { info: { inline: true } })
13 | .add(
14 | 'Header Authors (dropdown)',
15 | () => ,
16 | {
17 | info: { inline: true },
18 | },
19 | )
20 | .add(
21 | 'Author',
22 | () => (
23 |
28 | ),
29 | {
30 | info: { inline: true },
31 | },
32 | )
33 | .add(
34 | 'with some emoji',
35 | () => (
36 |
37 |
38 | 😀 😎 👍 💯
39 |
40 |
41 | ),
42 | { info: { inline: true } },
43 | );
44 |
--------------------------------------------------------------------------------
/src/commands/baseCommand.ts:
--------------------------------------------------------------------------------
1 | import { ICommand } from '../common/types';
2 |
3 | export abstract class BaseCommand implements ICommand {
4 | private _arguments: any[] = [];
5 | public get arguments() {
6 | return this._arguments;
7 | }
8 | private _command = '';
9 | private _title = '';
10 | private _description = '';
11 | private _detail?: string;
12 | private _tooltip?: string;
13 | public get command() {
14 | return this._command;
15 | }
16 | public get title() {
17 | return this._title;
18 | }
19 | public get label() {
20 | return this._title;
21 | }
22 | public get description() {
23 | return this._description;
24 | }
25 | public get detail() {
26 | return this._detail;
27 | }
28 | public get tooltip() {
29 | return this._tooltip;
30 | }
31 | constructor(public readonly data: T) {}
32 | public abstract execute();
33 | public async preExecute(): Promise {
34 | return true;
35 | }
36 | protected setTitle(value: string) {
37 | this._title = value;
38 | }
39 | protected setCommand(value: string) {
40 | this._command = value;
41 | }
42 | protected setCommandArguments(args: any[]) {
43 | this._arguments = args;
44 | }
45 | protected setDescription(value: string) {
46 | this._description = value;
47 | }
48 | protected setDetail(value: string) {
49 | this._detail = value;
50 | }
51 | protected setTooltip(value: string) {
52 | this._tooltip = value;
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/common/helpers.ts:
--------------------------------------------------------------------------------
1 | import * as tmp from 'tmp';
2 |
3 | export async function createTemporaryFile(
4 | extension: string,
5 | temporaryDirectory?: string,
6 | ): Promise<{ filePath: string; cleanupCallback: Function }> {
7 | const options: { postfix: string; dir?: string } = { postfix: extension };
8 | if (temporaryDirectory) {
9 | options.dir = temporaryDirectory;
10 | }
11 |
12 | return new Promise<{ filePath: string; cleanupCallback: Function }>((resolve, reject) => {
13 | tmp.file(options, (err: Error, tmpFile: string, _fd: any, cleanupCallback: any) => {
14 | if (err) {
15 | return reject(err);
16 | }
17 | resolve({ filePath: tmpFile, cleanupCallback: cleanupCallback });
18 | });
19 | });
20 | }
21 |
22 | export function formatDate(date: Date) {
23 | const lang = process.env.language;
24 | const dateOptions = {
25 | weekday: 'short',
26 | day: 'numeric',
27 | month: 'short',
28 | year: 'numeric',
29 | hour: 'numeric',
30 | minute: 'numeric',
31 | };
32 |
33 | // @ts-ignore
34 | return date.toLocaleString(lang, dateOptions);
35 | }
36 |
37 | export async function asyncFilter(arr: T[], callback): Promise {
38 | return (await Promise.all(arr.map(async item => ((await callback(item)) ? item : undefined)))).filter(
39 | i => i !== undefined,
40 | ) as T[];
41 | }
42 |
43 | export const sleep = (timeout: number) => new Promise(resolve => setTimeout(resolve, timeout));
44 |
45 | export function noop() {
46 | //Noop.
47 | }
48 |
--------------------------------------------------------------------------------
/src/adapter/parsers/fileStatStatus/parser.ts:
--------------------------------------------------------------------------------
1 | import { injectable, multiInject } from 'inversify';
2 | import { ILogService } from '../../../common/types';
3 | import { Status } from '../../../types';
4 | import { IFileStatStatusParser } from '../types';
5 |
6 | @injectable()
7 | export class FileStatStatusParser implements IFileStatStatusParser {
8 | constructor(@multiInject(ILogService) private loggers: ILogService[]) {}
9 | public canParse(status: string): boolean {
10 | const parsedStatus = this.parse(status);
11 | return parsedStatus !== undefined && parsedStatus !== null;
12 | }
13 | public parse(status: string): Status | undefined {
14 | status = status || '';
15 | status = status.length === 0 ? '' : status.trim().substring(0, 1);
16 | switch (status) {
17 | case 'A':
18 | return Status.Added;
19 | case 'M':
20 | return Status.Modified;
21 | case 'D':
22 | return Status.Deleted;
23 | case 'C':
24 | return Status.Copied;
25 | case 'R':
26 | return Status.Renamed;
27 | case 'T':
28 | return Status.TypeChanged;
29 | case 'X':
30 | return Status.Unknown;
31 | case 'U':
32 | return Status.Unmerged;
33 | case 'B':
34 | return Status.Broken;
35 | default: {
36 | this.loggers.forEach(logger => logger.error(`Unrecognized file stat status '${status}`));
37 | return;
38 | }
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/commandHandlers/commit/compareViewExplorer.ts:
--------------------------------------------------------------------------------
1 | import { inject, injectable } from 'inversify';
2 | import { ICommandManager } from '../../application/types/commandManager';
3 | import { ICommitViewerFactory } from '../../viewers/types';
4 | import { command } from '../registration';
5 | import { IGitCompareCommitViewExplorerCommandHandler } from '../types';
6 |
7 | @injectable()
8 | export class GitCompareCommitViewExplorerCommandHandler implements IGitCompareCommitViewExplorerCommandHandler {
9 | constructor(
10 | @inject(ICommandManager) private commandManager: ICommandManager,
11 | @inject(ICommitViewerFactory) private commitViewerFactory: ICommitViewerFactory,
12 | ) {}
13 |
14 | @command('git.commit.compare.view.hide', IGitCompareCommitViewExplorerCommandHandler)
15 | public async hide() {
16 | await this.commandManager.executeCommand('setContext', 'git.commit.compare.view.show', false);
17 | }
18 |
19 | @command('git.commit.compare.view.show', IGitCompareCommitViewExplorerCommandHandler)
20 | public async show() {
21 | await this.commandManager.executeCommand('setContext', 'git.commit.compare.view.show', true);
22 | }
23 |
24 | @command('git.commit.compare.view.showFilesOnly', IGitCompareCommitViewExplorerCommandHandler)
25 | public async showFilesView() {
26 | this.commitViewerFactory.getCommitViewer().showFilesView();
27 | }
28 |
29 | @command('git.commit.compare.view.showFolderView', IGitCompareCommitViewExplorerCommandHandler)
30 | public async showFolderView() {
31 | this.commitViewerFactory.getCommitViewer().showFolderView();
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | parser: '@typescript-eslint/parser', // Specifies the ESLint parser
3 | extends: [
4 | 'plugin:@typescript-eslint/recommended', // Uses the recommended rules from the @typescript-eslint/eslint-plugin
5 | 'prettier/@typescript-eslint', // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier
6 | 'plugin:prettier/recommended', // Enables eslint-plugin-prettier and displays prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array.
7 | ],
8 | parserOptions: {
9 | ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features
10 | sourceType: 'module', // Allows for the use of imports
11 | },
12 | rules: {
13 | "@typescript-eslint/no-non-null-assertion": "off", // allow non-null assertion
14 | "@typescript-eslint/interface-name-prefix": "off", // skip interface names which does not start with "I" prefix
15 | "@typescript-eslint/no-explicit-any": "off", // allow datatype any
16 | "@typescript-eslint/explicit-function-return-type": "off", // ignore missing return types for the time being
17 | "@typescript-eslint/no-empty-interface": "off", // allow empty interfaces
18 | "@typescript-eslint/ban-ts-ignore": "off", // allow @ts-ignore comment for the time being
19 | "@typescript-eslint/no-unused-vars": "off",
20 | "@typescript-eslint/no-use-before-define": "off",
21 | // Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs
22 | // e.g. "@typescript-eslint/explicit-function-return-type": "off",
23 | },
24 | };
25 |
--------------------------------------------------------------------------------
/src/commandHandlers/commit/commitViewExplorer.ts:
--------------------------------------------------------------------------------
1 | import { inject, injectable } from 'inversify';
2 | import { ICommandManager } from '../../application/types/commandManager';
3 | import { CommitDetails } from '../../common/types';
4 | import { ICommitViewerFactory } from '../../viewers/types';
5 | import { command } from '../registration';
6 | import { IGitCommitViewExplorerCommandHandler } from '../types';
7 |
8 | @injectable()
9 | export class GitCommitViewExplorerCommandHandler implements IGitCommitViewExplorerCommandHandler {
10 | constructor(
11 | @inject(ICommandManager) private commandManager: ICommandManager,
12 | @inject(ICommitViewerFactory) private commitViewerFactory: ICommitViewerFactory,
13 | ) {}
14 |
15 | @command('git.commit.view.hide', IGitCommitViewExplorerCommandHandler)
16 | public async hideCommitView(_commit: CommitDetails) {
17 | await this.commandManager.executeCommand('setContext', 'git.commit.view.show', false);
18 | }
19 |
20 | @command('git.commit.view.show', IGitCommitViewExplorerCommandHandler)
21 | public async showCommitView(_commit: CommitDetails) {
22 | await this.commandManager.executeCommand('setContext', 'git.commit.view.show', true);
23 | }
24 |
25 | @command('git.commit.view.showFilesOnly', IGitCommitViewExplorerCommandHandler)
26 | public async showFilesView(_commit: CommitDetails) {
27 | this.commitViewerFactory.getCommitViewer().showFilesView();
28 | }
29 |
30 | @command('git.commit.view.showFolderView', IGitCommitViewExplorerCommandHandler)
31 | public async showFolderView(_commit: CommitDetails) {
32 | this.commitViewerFactory.getCommitViewer().showFolderView();
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/test/runTest.ts:
--------------------------------------------------------------------------------
1 | import * as path from 'path';
2 | import * as fs from 'fs-extra';
3 | import { runTests } from 'vscode-test';
4 | import { extensionRootPath, tempRepoFolder } from './common';
5 | import { setupDefaultRepo } from './extension/repoSetup';
6 |
7 | async function main() {
8 | try {
9 | // The folder containing the Extension Manifest package.json
10 | // Passed to `--extensionDevelopmentPath`
11 | const extensionDevelopmentPath = extensionRootPath;
12 |
13 | // The path to the extension test script
14 | // Passed to --extensionTestsPath
15 | const extensionTestsPath = path.resolve(__dirname, './extension/testRunner');
16 | fs.mkdirpSync(path.join(tempRepoFolder, 'test_gitHistory'));
17 |
18 | // We'll check if this file exists, if it does, then tests failed.
19 | const testResultFailedFile = path.join(tempRepoFolder, 'tests.failed');
20 | if (fs.existsSync(testResultFailedFile)) {
21 | fs.unlinkSync(testResultFailedFile);
22 | }
23 |
24 | await setupDefaultRepo();
25 |
26 | // Download VS Code, unzip it and run the integration test
27 | await runTests({
28 | extensionDevelopmentPath,
29 | extensionTestsPath,
30 | launchArgs: [path.join(tempRepoFolder, 'test_gitHistory'), '--disable-extensions'],
31 | extensionTestsEnv: { IS_TEST_MODE: '1' },
32 | });
33 |
34 | if (fs.existsSync(testResultFailedFile)) {
35 | console.error('Failed to run tests');
36 | process.exit(1);
37 | }
38 | } catch (err) {
39 | console.error('Failed to run tests');
40 | process.exit(1);
41 | }
42 | }
43 |
44 | main();
45 |
--------------------------------------------------------------------------------
/src/commandHandlers/registration.ts:
--------------------------------------------------------------------------------
1 | import { interfaces } from 'inversify';
2 | import { Disposable } from 'vscode';
3 | import { ICommandHandler } from './types';
4 |
5 | type CommandHandler = (...args: any[]) => any;
6 | type CommandHandlerInfo = { commandName: string; handlerMethodName: string };
7 |
8 | export class CommandHandlerRegister implements Disposable {
9 | private static Handlers = new Map, CommandHandlerInfo[]>();
10 | public static register(
11 | commandName: string,
12 | handlerMethodName: string,
13 | serviceIdentifier: interfaces.ServiceIdentifier,
14 | ) {
15 | if (!CommandHandlerRegister.Handlers.has(serviceIdentifier)) {
16 | CommandHandlerRegister.Handlers.set(serviceIdentifier, []);
17 | }
18 | const commandList = CommandHandlerRegister.Handlers.get(serviceIdentifier)!;
19 | commandList.push({ commandName, handlerMethodName });
20 | CommandHandlerRegister.Handlers.set(serviceIdentifier, commandList);
21 | }
22 | public static getHandlers(): IterableIterator<
23 | [interfaces.ServiceIdentifier, CommandHandlerInfo[]]
24 | > {
25 | return CommandHandlerRegister.Handlers.entries();
26 | }
27 | public dispose() {
28 | CommandHandlerRegister.Handlers.clear();
29 | }
30 | }
31 |
32 | export function command(commandName: string, serviceIdentifier: interfaces.ServiceIdentifier) {
33 | return function(
34 | _target: ICommandHandler,
35 | propertyKey: string,
36 | descriptor: TypedPropertyDescriptor,
37 | ) {
38 | CommandHandlerRegister.register(commandName, propertyKey, serviceIdentifier);
39 |
40 | return descriptor;
41 | };
42 | }
43 |
--------------------------------------------------------------------------------
/.github/workflows/vsce-publish.yml:
--------------------------------------------------------------------------------
1 | name: Publish and Upload
2 |
3 | on:
4 | release:
5 | types: [published]
6 |
7 | jobs:
8 | build:
9 | if: "!github.event.release.prerelease"
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v2
13 | - uses: actions/setup-node@v1
14 | with:
15 | node-version: '16.14.x'
16 | - run: npm install
17 | # - run: npm install -g @vscode/vsce github-release-notes
18 | - name: Read package.json verison
19 | uses: tyankatsu0105/read-package-version-actions@v1
20 | id: package-version
21 | # - name: Generate and Commit Changelog
22 | # env:
23 | # GREN_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
24 | # run: |
25 | # git fetch origin main
26 | # git checkout main
27 | # gren changelog --override
28 | # git -c user.name="Don Jayamanne" -c user.email="don.jayamanne@yahoo.com" commit --no-verify -m "Updated CHANGELOG.md - Github Actions" -- CHANGELOG.md
29 | # git push
30 | - name: vsce publish
31 | run: vsce publish -p "${{ secrets.VSCE_TOKEN }}"
32 | - name: vsce package
33 | run: vsce package
34 | - name: Upload to Release
35 | uses: actions/upload-release-asset@v1.0.1
36 | env:
37 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
38 | with:
39 | upload_url: ${{ github.event.release.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps
40 | asset_path: ${{ format('githistory-{0}.vsix', steps.package-version.outputs.version) }}
41 | asset_name: ${{ format('githistory-{0}.vsix', steps.package-version.outputs.version) }}
42 | asset_content_type: application/zip
43 |
--------------------------------------------------------------------------------
/src/commandHandlers/commit/revert.ts:
--------------------------------------------------------------------------------
1 | import { inject, injectable } from 'inversify';
2 | import { IApplicationShell } from '../../application/types';
3 | import { CommitDetails } from '../../common/types';
4 | import { IServiceContainer } from '../../ioc/types';
5 | import { IGitServiceFactory } from '../../types';
6 | import { ICommitViewerFactory } from '../../viewers/types';
7 | import { command } from '../registration';
8 | import { IGitRevertCommandHandler } from '../types';
9 |
10 | @injectable()
11 | export class GitRevertCommandHandler implements IGitRevertCommandHandler {
12 | constructor(
13 | @inject(IServiceContainer) private serviceContainer: IServiceContainer,
14 | @inject(ICommitViewerFactory) private commitViewerFactory: ICommitViewerFactory,
15 | @inject(IApplicationShell) private applicationShell: IApplicationShell,
16 | ) {}
17 |
18 | @command('git.commit.revert', IGitRevertCommandHandler)
19 | public async revertCommit(commit: CommitDetails, showPrompt = true) {
20 | commit = commit ? commit : this.commitViewerFactory.getCommitViewer().selectedCommit;
21 | const gitService = await this.serviceContainer
22 | .get(IGitServiceFactory)
23 | .createGitService(commit.workspaceFolder);
24 |
25 | const msg = `Are you sure you want to revert this '${commit.logEntry.hash.short}' commit?`;
26 | const yesNo = showPrompt
27 | ? await this.applicationShell.showQuickPick(['Yes', 'No'], { placeHolder: msg })
28 | : 'Yes';
29 |
30 | if (yesNo === undefined || yesNo === 'No') {
31 | return;
32 | }
33 |
34 | gitService.revertCommit(commit.logEntry.hash.full).catch(err => {
35 | if (typeof err === 'string') {
36 | this.applicationShell.showErrorMessage(err);
37 | }
38 | });
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/commandHandlers/file/mimeTypes.ts:
--------------------------------------------------------------------------------
1 | import * as path from 'path';
2 | import { Uri } from 'vscode';
3 |
4 | type MapExtToMediaMimes = {
5 | [index: string]: string;
6 | };
7 |
8 | // Known media mimes that we can handle
9 | const mapExtToMediaMimes: MapExtToMediaMimes = {
10 | '.bmp': 'image/bmp',
11 | '.gif': 'image/gif',
12 | '.jpg': 'image/jpg',
13 | '.jpeg': 'image/jpg',
14 | '.jpe': 'image/jpg',
15 | '.png': 'image/png',
16 | '.tiff': 'image/tiff',
17 | '.tif': 'image/tiff',
18 | '.ico': 'image/x-icon',
19 | '.tga': 'image/x-tga',
20 | '.psd': 'image/vnd.adobe.photoshop',
21 | '.webp': 'image/webp',
22 | '.mid': 'audio/midi',
23 | '.midi': 'audio/midi',
24 | '.mp4a': 'audio/mp4',
25 | '.mpga': 'audio/mpeg',
26 | '.mp2': 'audio/mpeg',
27 | '.mp2a': 'audio/mpeg',
28 | '.mp3': 'audio/mpeg',
29 | '.m2a': 'audio/mpeg',
30 | '.m3a': 'audio/mpeg',
31 | '.oga': 'audio/ogg',
32 | '.ogg': 'audio/ogg',
33 | '.spx': 'audio/ogg',
34 | '.aac': 'audio/x-aac',
35 | '.wav': 'audio/x-wav',
36 | '.wma': 'audio/x-ms-wma',
37 | '.mp4': 'video/mp4',
38 | '.mp4v': 'video/mp4',
39 | '.mpg4': 'video/mp4',
40 | '.mpeg': 'video/mpeg',
41 | '.mpg': 'video/mpeg',
42 | '.mpe': 'video/mpeg',
43 | '.m1v': 'video/mpeg',
44 | '.m2v': 'video/mpeg',
45 | '.ogv': 'video/ogg',
46 | '.qt': 'video/quicktime',
47 | '.mov': 'video/quicktime',
48 | '.webm': 'video/webm',
49 | '.mkv': 'video/x-matroska',
50 | '.mk3d': 'video/x-matroska',
51 | '.mks': 'video/x-matroska',
52 | '.wmv': 'video/x-ms-wmv',
53 | '.flv': 'video/x-flv',
54 | '.avi': 'video/x-msvideo',
55 | '.movie': 'video/x-sgi-movie',
56 | };
57 |
58 | export function isTextFile(file: Uri): boolean {
59 | const extension = path.extname(file.fsPath);
60 | return !mapExtToMediaMimes[extension];
61 | }
62 |
--------------------------------------------------------------------------------
/src/logger.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from 'vscode';
2 |
3 | let outInfoChannel: vscode.OutputChannel;
4 | let outLogChannel: vscode.OutputChannel;
5 | const logLevel = vscode.workspace.getConfiguration('gitHistory').get('logLevel') as string;
6 |
7 | function getInfoChannel() {
8 | if (outInfoChannel === undefined) {
9 | outInfoChannel = vscode.window.createOutputChannel('Git History Info');
10 | }
11 | return outInfoChannel;
12 | }
13 |
14 | export function getLogChannel() {
15 | if (outLogChannel === undefined) {
16 | outLogChannel = vscode.window.createOutputChannel('Git History');
17 | }
18 | return outLogChannel;
19 | }
20 |
21 | export function logError(error: any) {
22 | getLogChannel().appendLine(`[Error-${getTimeAndms()}] ${error.toString()}`.replace(/(\r\n|\n|\r)/gm, ''));
23 | getLogChannel().show();
24 | vscode.window.showErrorMessage("There was an error, please view details in the 'Git History Log' output window");
25 | }
26 |
27 | export function logInfo(message: string) {
28 | if (logLevel === 'Info' || logLevel === 'Debug') {
29 | getLogChannel().appendLine(`[Info -${getTimeAndms()}] ${message}`);
30 | }
31 | }
32 |
33 | export function logDebug(message: string) {
34 | if (logLevel === 'Debug') {
35 | getLogChannel().appendLine(`[Debug-${getTimeAndms()}] ${message}`);
36 | }
37 | }
38 |
39 | function getTimeAndms(): string {
40 | const time = new Date();
41 | const hours = `0${time.getHours()}`.slice(-2);
42 | const minutes = `0${time.getMinutes()}`.slice(-2);
43 | const seconds = `0${time.getSeconds()}`.slice(-2);
44 | const milliSeconds = `00${time.getMilliseconds()}`.slice(-3);
45 | return `${hours}:${minutes}:${seconds}.${milliSeconds}`;
46 | }
47 |
48 | export function showInfo(message: string) {
49 | getInfoChannel().clear();
50 | getInfoChannel().appendLine(message);
51 | getInfoChannel().show();
52 | }
53 |
--------------------------------------------------------------------------------
/src/commandHandlers/commit/gitRebase.ts:
--------------------------------------------------------------------------------
1 | import { inject, injectable } from 'inversify';
2 | import { IApplicationShell } from '../../application/types';
3 | import { CommitDetails } from '../../common/types';
4 | import { IServiceContainer } from '../../ioc/types';
5 | import { IGitServiceFactory } from '../../types';
6 | import { ICommitViewerFactory } from '../../viewers/types';
7 | import { command } from '../registration';
8 | import { IGitRebaseCommandHandler } from '../types';
9 |
10 | @injectable()
11 | export class GitRebaseCommandHandler implements IGitRebaseCommandHandler {
12 | constructor(
13 | @inject(IServiceContainer) private serviceContainer: IServiceContainer,
14 | @inject(ICommitViewerFactory) private commitViewerFactory: ICommitViewerFactory,
15 | @inject(IApplicationShell) private applicationShell: IApplicationShell,
16 | ) {}
17 |
18 | @command('git.commit.rebase', IGitRebaseCommandHandler)
19 | public async rebase(commit: CommitDetails, showPrompt = true) {
20 | commit = commit ? commit : this.commitViewerFactory.getCommitViewer().selectedCommit;
21 | const gitService = await this.serviceContainer
22 | .get(IGitServiceFactory)
23 | .createGitService(commit.workspaceFolder);
24 | const currentBranch = gitService.getCurrentBranch();
25 |
26 | const msg = `Rebase ${currentBranch} onto '${commit.logEntry.hash.short}'?`;
27 | const yesNo = showPrompt
28 | ? await this.applicationShell.showQuickPick(['Yes', 'No'], { placeHolder: msg })
29 | : 'Yes';
30 |
31 | if (yesNo === undefined || yesNo === 'No') {
32 | return;
33 | }
34 |
35 | gitService.rebase(commit.logEntry.hash.full).catch(err => {
36 | if (typeof err === 'string') {
37 | this.applicationShell.showErrorMessage(err);
38 | }
39 | });
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/browser/src/components/LogView/BranchGraph/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { connect } from 'react-redux';
3 | import { RootState } from '../../../reducers';
4 | import { BranchGrapProps, drawGitGraph } from './svgGenerator';
5 |
6 | class BrachGraph extends React.Component {
7 | componentDidUpdate(prevProps: BranchGrapProps) {
8 | if (this.props.hideGraph) {
9 | drawGitGraph(this.svg, this.svg.nextSibling as HTMLElement, 0, this.props.itemHeight, [], true);
10 | return;
11 | }
12 | if (prevProps.updateTick === this.props.updateTick) {
13 | return;
14 | }
15 |
16 | // Hack, first clear before rebuilding.
17 | // Remember, we will need to support apending results, as opposed to clearing page
18 | drawGitGraph(this.svg, this.svg.nextSibling as HTMLElement, 0, this.props.itemHeight, []);
19 | drawGitGraph(this.svg, this.svg.nextSibling as HTMLElement, 0, this.props.itemHeight, this.props.logEntries);
20 | }
21 |
22 | private svg: SVGSVGElement;
23 | render() {
24 | return ;
25 | }
26 | }
27 |
28 | function mapStateToProps(state: RootState): BranchGrapProps {
29 | const hideGraph =
30 | state &&
31 | state.logEntries &&
32 | ((state.settings.searchText && state.settings.searchText.length > 0) ||
33 | (state.settings.file && state.settings.file.length > 0) ||
34 | (state.settings.authorFilter && state.settings.authorFilter.length > 0) ||
35 | state.logEntries.isLoading);
36 |
37 | return {
38 | logEntries: state.logEntries.items,
39 | hideGraph,
40 | itemHeight: state.graph.itemHeight,
41 | updateTick: state.graph.updateTick,
42 | };
43 | }
44 |
45 | export default connect(mapStateToProps)(BrachGraph);
46 |
--------------------------------------------------------------------------------
/src/commandHandlers/commit/gitCherryPick.ts:
--------------------------------------------------------------------------------
1 | import { inject, injectable } from 'inversify';
2 | import { IApplicationShell } from '../../application/types';
3 | import { CommitDetails } from '../../common/types';
4 | import { IServiceContainer } from '../../ioc/types';
5 | import { IGitServiceFactory } from '../../types';
6 | import { ICommitViewerFactory } from '../../viewers/types';
7 | import { command } from '../registration';
8 | import { IGitCherryPickCommandHandler } from '../types';
9 |
10 | @injectable()
11 | export class GitCherryPickCommandHandler implements IGitCherryPickCommandHandler {
12 | constructor(
13 | @inject(IServiceContainer) private serviceContainer: IServiceContainer,
14 | @inject(ICommitViewerFactory) private commitViewerFactory: ICommitViewerFactory,
15 | @inject(IApplicationShell) private applicationShell: IApplicationShell,
16 | ) {}
17 |
18 | @command('git.commit.cherryPick', IGitCherryPickCommandHandler)
19 | public async cherryPickCommit(commit: CommitDetails, showPrompt = true) {
20 | commit = commit ? commit : this.commitViewerFactory.getCommitViewer().selectedCommit;
21 | const gitService = await this.serviceContainer
22 | .get(IGitServiceFactory)
23 | .createGitService(commit.workspaceFolder);
24 | const currentBranch = gitService.getCurrentBranch();
25 |
26 | const msg = `Cherry pick ${commit.logEntry.hash.short} into ${currentBranch}?`;
27 | const yesNo = showPrompt
28 | ? await this.applicationShell.showQuickPick(['Yes', 'No'], { placeHolder: msg })
29 | : 'Yes';
30 |
31 | if (yesNo === undefined || yesNo === 'No') {
32 | return;
33 | }
34 |
35 | gitService.cherryPick(commit.logEntry.hash.full).catch(err => {
36 | if (typeof err === 'string') {
37 | this.applicationShell.showErrorMessage(err);
38 | }
39 | });
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/test/non-extension/adapter/parsers/fileStatus/parser.test.ts:
--------------------------------------------------------------------------------
1 | import { assert } from 'chai';
2 | import * as TypeMoq from 'typemoq';
3 | import { FileStatStatusParser } from '../../../../../src/adapter/parsers/fileStatStatus/parser';
4 | import { ILogService } from '../../../../../src/common/types';
5 | import { Status } from '../../../../../src/types';
6 |
7 | describe('Adapter Parser File Status', () => {
8 | test('Ensure status can be parsed correctly', () => {
9 | const parser = new FileStatStatusParser([TypeMoq.Mock.ofType().object]);
10 | ['A', 'M', 'D', 'C', 'R', 'C1234', 'R1234', 'U', 'X', 'B', 'T'].forEach(status => {
11 | assert.isTrue(parser.canParse(status), `Status '${status}' must be parseable`);
12 | });
13 | ['a', 'm', 'd', 'C', 'R', 'C1234', 'R1234', 'U', 'X', 'B', 'T', 'Z', '1', '2', 'x', 'fc1234', 'eR1234'].forEach(
14 | status => {
15 | assert.isFalse(
16 | parser.canParse(status.toLocaleLowerCase()),
17 | `Status '${status.toLocaleLowerCase()}' must not be parseable`,
18 | );
19 | },
20 | );
21 | });
22 |
23 | test('Ensure status is parsed correctly', () => {
24 | const parser = new FileStatStatusParser([TypeMoq.Mock.ofType().object]);
25 | const statuses = [
26 | ['A', Status.Added],
27 | ['M', Status.Modified],
28 | ['D', Status.Deleted],
29 | ['C', Status.Copied],
30 | ['R', Status.Renamed],
31 | ['C1234', Status.Copied],
32 | ['U', Status.Unmerged],
33 | ['X', Status.Unknown],
34 | ['B', Status.Broken],
35 | ['T', Status.TypeChanged],
36 | ['R1234', Status.Renamed],
37 | ];
38 | statuses.forEach(status => {
39 | assert.equal(
40 | parser.parse((status[0] as any) as string),
41 | status[1] as Status,
42 | `Status '${status[0]}' not parsed correctly`,
43 | );
44 | });
45 | });
46 | });
47 |
--------------------------------------------------------------------------------
/test/extension/testRunner.ts:
--------------------------------------------------------------------------------
1 | import * as jest from 'jest';
2 | import * as fs from 'fs-extra';
3 | import * as path from 'path';
4 | import { AggregatedResult } from '@jest/test-result';
5 | import { tempRepoFolder } from '../common';
6 |
7 | const extensionRoot = path.join(__dirname, '..', '..', '..');
8 | type Output = { results: AggregatedResult };
9 | export function run(): Promise {
10 | // jest doesn't seem to provide a way to inject global/dynamic imports.
11 | // Basically if we have a `require`, jest assumes that it is a module on disc.
12 | // However `vscode` isn't a module in disc, its provided by vscode host environment.
13 | // Hack - put vscode namespace into global variable `process`, and re-export as as mock in jest (see __mocks__/vscode.ts).
14 | // Using global variables don't work, as jest seems to fiddle with that as well.
15 | (process as any).__VSCODE = require('vscode');
16 |
17 | const jestConfigFile = path.join(extensionRoot, 'jest.extension.config.js');
18 | // eslint-disable-next-line @typescript-eslint/no-var-requires
19 | const jestConfig = require(jestConfigFile);
20 | return new Promise((resolve, reject) => {
21 | jest.runCLI(jestConfig, [extensionRoot])
22 | .catch(error => {
23 | delete (process as any).__VSCODE;
24 | console.error('Calling jest.runCLI failed', error);
25 | reject(error);
26 | })
27 | .then(output => {
28 | delete (process as any).__VSCODE;
29 | if (!output) {
30 | return resolve();
31 | }
32 | const results = output as Output;
33 | if (results.results.numFailedTestSuites || results.results.numFailedTests) {
34 | // Do not reject, VSC does not exit gracefully, hence test job hangs.
35 | // We don't want that, specially on CI.
36 | fs.appendFileSync(path.join(tempRepoFolder, 'tests.failed'), 'failed');
37 | }
38 | resolve();
39 | });
40 | });
41 | }
42 |
--------------------------------------------------------------------------------
/src/application/workspace.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation. All rights reserved.
2 | // Licensed under the MIT License.
3 |
4 | import { injectable } from 'inversify';
5 | import {
6 | CancellationToken,
7 | ConfigurationChangeEvent,
8 | Event,
9 | FileSystemWatcher,
10 | GlobPattern,
11 | Uri,
12 | workspace,
13 | WorkspaceConfiguration,
14 | WorkspaceFolder,
15 | WorkspaceFoldersChangeEvent,
16 | } from 'vscode';
17 | import { IWorkspaceService } from './types/workspace';
18 |
19 | @injectable()
20 | export class WorkspaceService implements IWorkspaceService {
21 | public get onDidChangeConfiguration(): Event {
22 | return workspace.onDidChangeConfiguration;
23 | }
24 | public get workspaceFolders(): ReadonlyArray | undefined {
25 | return workspace.workspaceFolders;
26 | }
27 | public get onDidChangeWorkspaceFolders(): Event {
28 | return workspace.onDidChangeWorkspaceFolders;
29 | }
30 | public getConfiguration(section?: string, resource?: Uri): WorkspaceConfiguration {
31 | return workspace.getConfiguration(section, resource);
32 | }
33 | public asRelativePath(pathOrUri: string | Uri, includeWorkspaceFolder?: boolean): string {
34 | return workspace.asRelativePath(pathOrUri, includeWorkspaceFolder);
35 | }
36 | public createFileSystemWatcher(
37 | globPattern: GlobPattern,
38 | ignoreCreateEvents?: boolean,
39 | ignoreChangeEvents?: boolean,
40 | ignoreDeleteEvents?: boolean,
41 | ): FileSystemWatcher {
42 | return workspace.createFileSystemWatcher(
43 | globPattern,
44 | ignoreCreateEvents,
45 | ignoreChangeEvents,
46 | ignoreDeleteEvents,
47 | );
48 | }
49 | public findFiles(
50 | include: GlobPattern,
51 | exclude?: GlobPattern,
52 | maxResults?: number,
53 | token?: CancellationToken,
54 | ): Thenable {
55 | return workspace.findFiles(include, exclude, maxResults, token);
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Git History, Search and More (including _git log_)
2 |
3 | * View and search git log along with the graph and details.
4 | * View a previous copy of the file.
5 | * View and search the history
6 | * View the history of one or all branches (git log)
7 | * View the history of a file
8 | * View the history of a line in a file (Git Blame).
9 | * View the history of an author
10 | * Compare:
11 | * Compare branches
12 | * Compare commits
13 | * Compare files across commits
14 | * Miscellaneous features:
15 | * Github avatars
16 | * Cherry-picking commits
17 | * Create Tag
18 | * Create Branch
19 | * Reset commit (soft and hard)
20 | * Reverting commits
21 | * Create branches from a commits
22 | * View commit information in a treeview (snapshot of all changes)
23 | * Merge and rebase
24 |
25 | Open the file to view the history, and then
26 | Press F1 and select/type "Git: View History", "Git: View File History" or "Git: View Line History".
27 |
28 | ## Available Commands
29 | * View Git History (git log) (git.viewHistory)
30 | * View File History (git.viewFileHistory)
31 | * View Line History (git.viewLineHistory)
32 |
33 | ## Keyboard Shortcuts
34 | You can add keyboard short cuts for the above commands by following the directions on the website [customization documentation](https://code.visualstudio.com/docs/customization/keybindings).
35 |
36 | NOTE: The file for which the history is to be viewed, must already be opened.
37 |
38 | 
39 |
40 | 
41 |
42 | 
43 |
44 | 
45 |
46 |
47 | ## Source
48 |
49 | [GitHub](https://github.com/DonJayamanne/gitHistoryVSCode)
50 |
51 | ## Big thanks to [ole](https://github.com/ole1986)
52 |
53 | ## License
54 |
55 | [MIT](https://raw.githubusercontent.com/DonJayamanne/gitHistoryVSCode/main/LICENSE)
56 |
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | // Available variables which can be used inside of strings.
2 | // ${workspaceRoot}: the root folder of the team
3 | // ${file}: the current opened file
4 | // ${fileBasename}: the current opened file's basename
5 | // ${fileDirname}: the current opened file's dirname
6 | // ${fileExtname}: the current opened file's extension
7 | // ${cwd}: the current working directory of the spawned process
8 | // A task runner that calls a custom npm script that compiles the extension.
9 | {
10 | "version": "2.0.0",
11 | "presentation": {
12 | "echo": true,
13 | "reveal": "always",
14 | "focus": false,
15 | "panel": "shared"
16 | },
17 | "tasks": [
18 | {
19 | "type": "npm",
20 | "script": "lint",
21 | "problemMatcher": [
22 | "$eslint-stylish"
23 | ]
24 | },
25 | {
26 | "type": "npm",
27 | "script": "lint-staged",
28 | "problemMatcher": [
29 | "$eslint-stylish"
30 | ]
31 | },
32 | {
33 | "type": "npm",
34 | "label": "test-compile",
35 | "script": "test-compile",
36 | "problemMatcher": [
37 | "$tsc-watch"
38 | ]
39 | },
40 | {
41 | "label": "webpack-dev",
42 | "type": "npm",
43 | "isBackground": true,
44 | "script": "webpack-dev",
45 | "problemMatcher": [
46 | {
47 | "pattern": [
48 | {
49 | "regexp": ".",
50 | "file": 1,
51 | "location": 2,
52 | "message": 3
53 | }
54 | ],
55 | "background": {
56 | "activeOnStart": true,
57 | "beginsPattern": {
58 | "regexp": "webpack is watching the files"
59 | },
60 | "endsPattern": {
61 | "regexp": "Entrypoint main = extension.js"
62 | }
63 | }
64 | }
65 | ]
66 | }
67 | ]
68 | }
69 |
--------------------------------------------------------------------------------
/src/adapter/helpers.ts:
--------------------------------------------------------------------------------
1 | import { EnumEx } from '../common/enumHelper';
2 | import { CommitInfo } from '../types';
3 | export class Helpers {
4 | public static GetLogArguments() {
5 | const args: string[] = [];
6 | for (const item of EnumEx.getValues(CommitInfo)) {
7 | if (item !== CommitInfo.NewLine) {
8 | args.push(Helpers.GetCommitInfoFormatCode(item));
9 | }
10 | }
11 |
12 | return args;
13 | }
14 | public static GetCommitInfoFormatCode(info: CommitInfo): string {
15 | switch (info) {
16 | case CommitInfo.FullHash: {
17 | return '%H';
18 | }
19 | case CommitInfo.ShortHash: {
20 | return '%h';
21 | }
22 | case CommitInfo.TreeFullHash: {
23 | return '%T';
24 | }
25 | case CommitInfo.TreeShortHash: {
26 | return '%t';
27 | }
28 | case CommitInfo.ParentFullHash: {
29 | return '%P';
30 | }
31 | case CommitInfo.ParentShortHash: {
32 | return '%p';
33 | }
34 | case CommitInfo.AuthorName: {
35 | return '%an';
36 | }
37 | case CommitInfo.AuthorEmail: {
38 | return '%ae';
39 | }
40 | case CommitInfo.AuthorDateUnixTime: {
41 | return '%at';
42 | }
43 | case CommitInfo.CommitterName: {
44 | return '%cn';
45 | }
46 | case CommitInfo.CommitterEmail: {
47 | return '%ce';
48 | }
49 | case CommitInfo.CommitterDateUnixTime: {
50 | return '%ct';
51 | }
52 | case CommitInfo.RefsNames: {
53 | return '%D';
54 | }
55 | case CommitInfo.Subject: {
56 | return '%s';
57 | }
58 | case CommitInfo.Body: {
59 | return '%b';
60 | }
61 | case CommitInfo.Notes: {
62 | return '%N';
63 | }
64 | case CommitInfo.NewLine: {
65 | return '%n';
66 | }
67 | default: {
68 | throw new Error(`Unrecognized Commit Info type ${info}`);
69 | }
70 | }
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | // A launch configuration that compiles the extension and then opens it inside a new window
2 | {
3 | "version": "0.1.0",
4 | "configurations": [
5 | {
6 | "name": "Launch Extension",
7 | "type": "extensionHost",
8 | "request": "launch",
9 | "runtimeExecutable": "${execPath}",
10 | "args": [
11 | "--extensionDevelopmentPath=${workspaceRoot}"
12 | ],
13 | "stopOnEntry": false,
14 | "smartStep": true,
15 | "sourceMaps": true,
16 | "outFiles": [
17 | "${workspaceRoot}/dist/**/*.js"
18 | ],
19 | "preLaunchTask": "webpack-dev",
20 | "skipFiles": [
21 | "/**"
22 | ]
23 | },
24 | {
25 | "name": "Launch Unit Tests",
26 | "type": "node",
27 | "request": "launch",
28 | "cwd": "${workspaceFolder}",
29 | "args": [
30 | "--inspect-brk",
31 | "${workspaceRoot}/node_modules/.bin/jest",
32 | "--runInBand",
33 | "--config",
34 | "${workspaceRoot}/jest.config.js"
35 | ],
36 | "console": "integratedTerminal",
37 | "internalConsoleOptions": "neverOpen",
38 | "skipFiles": [
39 | "/**"
40 | ]
41 | },
42 | {
43 | "name": "Run Extension Tests",
44 | "type": "extensionHost",
45 | "request": "launch",
46 | "runtimeExecutable": "${execPath}",
47 | "args": [
48 | // Ensure this folder exists.
49 | "${workspaceFolder}/temp/testing/test_gitHistory",
50 | "--disable-extensions",
51 | "--extensionDevelopmentPath=${workspaceFolder}",
52 | "--extensionTestsPath=${workspaceFolder}/dist/test/extension/testRunner"
53 | ],
54 | "outFiles": [
55 | "${workspaceFolder}/dist/test/**/*.js"
56 | ],
57 | "skipFiles": [
58 | "/**"
59 | ],
60 | "env": {
61 | "IS_TEST_MODE": "1"
62 | },
63 | "sourceMaps": true,
64 | "internalConsoleOptions": "openOnSessionStart",
65 | "preLaunchTask": "test-compile"
66 | }
67 | ]
68 | }
69 |
--------------------------------------------------------------------------------
/test/mocks.ts:
--------------------------------------------------------------------------------
1 | import { Container } from 'inversify';
2 | import { Abstract, IServiceContainer, IServiceManager, Newable } from '../src/ioc/types';
3 |
4 | export class TestServiceContainer implements IServiceContainer, IServiceManager {
5 | private cont: Container;
6 | constructor() {
7 | this.cont = new Container();
8 | }
9 | public add(
10 | serviceIdentifier: string | symbol | Newable | Abstract,
11 | constructor: new (...args: any[]) => T,
12 | name?: string | number | symbol | undefined,
13 | ): void {
14 | if (name) {
15 | this.cont
16 | .bind(serviceIdentifier)
17 | .to(constructor)
18 | .whenTargetNamed(name);
19 | } else {
20 | this.cont.bind(serviceIdentifier).to(constructor);
21 | }
22 | }
23 | public addSingleton(
24 | serviceIdentifier: string | symbol | Newable | Abstract,
25 | constructor: new (...args: any[]) => T,
26 | name?: string | number | symbol | undefined,
27 | ): void {
28 | if (name) {
29 | this.cont
30 | .bind(serviceIdentifier)
31 | .to(constructor)
32 | .whenTargetNamed(name);
33 | } else {
34 | this.cont.bind(serviceIdentifier).to(constructor);
35 | }
36 | }
37 | public addSingletonInstance(
38 | serviceIdentifier: string | symbol | Newable | Abstract,
39 | instance: T,
40 | name?: string | number | symbol | undefined,
41 | ): void {
42 | if (name) {
43 | this.cont
44 | .bind(serviceIdentifier)
45 | .toConstantValue(instance)
46 | .whenTargetNamed(name);
47 | } else {
48 | this.cont.bind(serviceIdentifier).toConstantValue(instance);
49 | }
50 | }
51 | public get(
52 | serviceIdentifier: string | symbol | Newable | Abstract,
53 | name?: string | number | symbol | undefined,
54 | ): T {
55 | return name ? this.cont.getNamed(serviceIdentifier, name) : this.cont.get(serviceIdentifier);
56 | }
57 | public getAll(
58 | serviceIdentifier: string | symbol | Newable | Abstract,
59 | name?: string | number | symbol | undefined,
60 | ): T[] {
61 | return name ? this.cont.getAllNamed(serviceIdentifier, name) : this.cont.getAll(serviceIdentifier);
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/commandHandlers/fileCommit/fileCompare.ts:
--------------------------------------------------------------------------------
1 | import { inject, injectable } from 'inversify';
2 | import { IApplicationShell, ICommandManager } from '../../application/types';
3 | import { FileCommitDetails } from '../../common/types';
4 | import { IServiceContainer } from '../../ioc/types';
5 | import { FileNode } from '../../nodes/types';
6 | import { IGitServiceFactory } from '../../types';
7 | import { command } from '../registration';
8 | import { IGitCompareFileCommandHandler } from '../types';
9 |
10 | @injectable()
11 | export class GitCompareFileCommitCommandHandler implements IGitCompareFileCommandHandler {
12 | private _previouslySelectedCommit?: FileCommitDetails;
13 |
14 | constructor(
15 | @inject(IServiceContainer) private serviceContainer: IServiceContainer,
16 | @inject(ICommandManager) private commandManager: ICommandManager,
17 | @inject(IApplicationShell) private application: IApplicationShell,
18 | ) {}
19 |
20 | public get selectedCommit(): FileCommitDetails | undefined {
21 | return this._previouslySelectedCommit;
22 | }
23 |
24 | @command('git.commit.FileEntry.selectForComparison', IGitCompareFileCommandHandler)
25 | public async select(nodeOrFileCommit: FileNode | FileCommitDetails): Promise {
26 | const fileCommit = nodeOrFileCommit instanceof FileCommitDetails ? nodeOrFileCommit : nodeOrFileCommit.data!;
27 | await this.commandManager.executeCommand('setContext', 'git.commit.FileEntry.selectForComparison', true);
28 | this._previouslySelectedCommit = fileCommit;
29 | }
30 |
31 | @command('git.commit.FileEntry.compare', IGitCompareFileCommandHandler)
32 | public async compare(nodeOrFileCommit: FileNode | FileCommitDetails): Promise {
33 | if (!this.selectedCommit) {
34 | await this.application.showErrorMessage('Please select another file to compare with');
35 | return;
36 | }
37 | const fileCommit = nodeOrFileCommit instanceof FileCommitDetails ? nodeOrFileCommit : nodeOrFileCommit.data!;
38 | const gitService = await this.serviceContainer
39 | .get(IGitServiceFactory)
40 | .createGitService(fileCommit.workspaceFolder);
41 | const fileDiffs = await gitService.getDifferences(
42 | this.selectedCommit.logEntry.hash.full,
43 | fileCommit.logEntry.hash.full,
44 | );
45 | await this.commandManager.executeCommand('git.commit.diff.view', this.selectedCommit!, fileCommit, fileDiffs);
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/commandHandlers/handlerManager.ts:
--------------------------------------------------------------------------------
1 | import { inject, injectable } from 'inversify';
2 | import { ICommandManager } from '../application/types/commandManager';
3 | import { IDisposableRegistry } from '../application/types/disposableRegistry';
4 | import { IServiceContainer } from '../ioc/types';
5 | import { CommandHandlerRegister } from './registration';
6 | import { ICommandHandler, ICommandHandlerManager } from './types';
7 | import { sendTelemetryEvent, sendTelemetryWhenDone } from '../common/telemetry';
8 | import { StopWatch } from '../common/stopWatch';
9 |
10 | @injectable()
11 | export class CommandHandlerManager implements ICommandHandlerManager {
12 | constructor(
13 | @inject(IDisposableRegistry) private disposableRegistry: IDisposableRegistry,
14 | @inject(ICommandManager) private commandManager: ICommandManager,
15 | @inject(IServiceContainer) private serviceContainer: IServiceContainer,
16 | ) {}
17 |
18 | public registerHandlers() {
19 | for (const item of CommandHandlerRegister.getHandlers()) {
20 | const serviceIdentifier = item[0];
21 | const handlers = item[1];
22 | const target = this.serviceContainer.get(serviceIdentifier);
23 | handlers.forEach(handlerInfo =>
24 | this.registerCommand(handlerInfo.commandName, handlerInfo.handlerMethodName, target),
25 | );
26 | }
27 | }
28 |
29 | private registerCommand(commandName: string, handlerMethodName: string, target: ICommandHandler) {
30 | const handler = target[handlerMethodName] as (...args: any[]) => void;
31 | const disposable = this.commandManager.registerCommand(commandName, (...args: any[]) => {
32 | const stopWatch = new StopWatch();
33 | let result: Promise | undefined | any;
34 | try {
35 | result = handler.apply(target, args);
36 | } finally {
37 | // Track commands that we have created and attached to, no PII here hence cast `commandName` to any.
38 | if (result && result.then && typeof result.then === 'function') {
39 | // This is an async handler.
40 | sendTelemetryWhenDone(commandName as any, result);
41 | } else {
42 | // This is a synchronous handler.
43 | sendTelemetryEvent(commandName as any, stopWatch.elapsedTime);
44 | }
45 | }
46 | });
47 | this.disposableRegistry.register(disposable);
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/nodes/types.ts:
--------------------------------------------------------------------------------
1 | import * as path from 'path';
2 | import { Uri } from 'vscode';
3 | import { CommitDetails, FileCommitDetails } from '../common/types';
4 | import { CommittedFile } from '../types';
5 | import { DirectoryTreeItem, FileTreeItem } from './treeNodes';
6 |
7 | export interface INode {
8 | /**
9 | * A human readable string which is rendered prominent.
10 | */
11 | readonly label: string;
12 | readonly resource: Uri;
13 | readonly data?: T;
14 | readonly children: INode[];
15 | }
16 |
17 | export abstract class AbstractCommitNode implements INode {
18 | public children: AbstractCommitNode[] = [];
19 | // @ts-ignore
20 | private _label: string;
21 | // @ts-ignore
22 | private _resource: Uri;
23 | public get label() {
24 | return this._label;
25 | }
26 | public get resource() {
27 | return this._resource;
28 | }
29 | constructor(public readonly data: T | undefined) {}
30 | protected setLabel(value: string) {
31 | this._label = value;
32 | }
33 | protected setResoruce(value: string | Uri) {
34 | this._resource = typeof value === 'string' ? Uri.file(value) : value;
35 | }
36 | }
37 |
38 | export class DirectoryNode extends AbstractCommitNode {
39 | constructor(commit: CommitDetails, relativePath: string) {
40 | super(commit);
41 | const folderName = path.basename(relativePath);
42 | this.setLabel(folderName);
43 | this.setResoruce(relativePath);
44 | }
45 | }
46 |
47 | export class FileNode extends AbstractCommitNode {
48 | constructor(commit: FileCommitDetails) {
49 | super(commit);
50 | const fileName = path.basename(commit.committedFile.relativePath);
51 | this.setLabel(fileName);
52 | this.setResoruce(commit.committedFile.relativePath);
53 | }
54 | }
55 |
56 | export const INodeFactory = Symbol.for('INodeFactory');
57 |
58 | export interface INodeFactory {
59 | createDirectoryNode(commit: CommitDetails, relativePath: string): DirectoryNode;
60 | createFileNode(commit: CommitDetails, committedFile: CommittedFile): FileNode;
61 | }
62 |
63 | export const INodeBuilder = Symbol.for('INodeBuilder');
64 |
65 | export interface INodeBuilder {
66 | buildTree(commit: CommitDetails, committedFiles: CommittedFile[]): (DirectoryNode | FileNode)[];
67 | buildList(commit: CommitDetails, committedFiles: CommittedFile[]): FileNode[];
68 | getTreeItem(node: DirectoryNode | FileNode): Promise;
69 | }
70 |
--------------------------------------------------------------------------------
/src/viewers/commitViewerFactory.ts:
--------------------------------------------------------------------------------
1 | import { inject, injectable, named } from 'inversify';
2 | import { OutputChannel } from 'vscode';
3 | import { ICommandManager } from '../application/types/commandManager';
4 | import { IFileCommitCommandFactory } from '../commandFactories/types';
5 | import { ICommitViewFormatter } from '../formatters/types';
6 | import { NodeBuilder } from '../nodes/nodeBuilder';
7 | import { INodeFactory } from '../nodes/types';
8 | import { IPlatformService } from '../platform/types';
9 | import { IOutputChannel } from '../types';
10 | import { CommitViewer } from './commitViewer';
11 | import { ICommitViewer, ICommitViewerFactory } from './types';
12 |
13 | @injectable()
14 | export class CommitViewerFactory implements ICommitViewerFactory {
15 | // @ts-ignore
16 | private commitViewer: ICommitViewer;
17 | // @ts-ignore
18 | private compareViewer: ICommitViewer;
19 | constructor(
20 | @inject(IOutputChannel) private outputChannel: OutputChannel,
21 | @inject(ICommitViewFormatter) private commitFormatter: ICommitViewFormatter,
22 | @inject(ICommandManager) private commandManager: ICommandManager,
23 | @inject(IPlatformService) private platformService: IPlatformService,
24 | @inject(IFileCommitCommandFactory) private fileCommitFactory: IFileCommitCommandFactory,
25 | @inject(INodeFactory) @named('standard') private standardNodeFactory: INodeFactory,
26 | @inject(INodeFactory) @named('comparison') private compareNodeFactory: INodeFactory,
27 | ) {}
28 | public getCommitViewer(): ICommitViewer {
29 | if (this.commitViewer) {
30 | return this.commitViewer;
31 | }
32 |
33 | return (this.commitViewer = new CommitViewer(
34 | this.outputChannel,
35 | this.commitFormatter,
36 | this.commandManager,
37 | new NodeBuilder(this.fileCommitFactory, this.standardNodeFactory, this.platformService),
38 | 'commitViewProvider',
39 | 'git.commit.view.show',
40 | ));
41 | }
42 | public getCompareCommitViewer(): ICommitViewer {
43 | if (this.compareViewer) {
44 | return this.compareViewer;
45 | }
46 |
47 | return (this.compareViewer = new CommitViewer(
48 | this.outputChannel,
49 | this.commitFormatter,
50 | this.commandManager,
51 | new NodeBuilder(this.fileCommitFactory, this.compareNodeFactory, this.platformService),
52 | 'compareCommitViewProvider',
53 | 'git.commit.compare.view.show',
54 | ));
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/adapter/avatar/base.ts:
--------------------------------------------------------------------------------
1 | import { injectable, unmanaged } from 'inversify';
2 | import { IStateStore, IStateStoreFactory } from '../../application/types/stateStore';
3 | import { IWorkspaceService } from '../../application/types/workspace';
4 | import { IServiceContainer } from '../../ioc/types';
5 | import { Avatar, AvatarResponse, IGitService } from '../../types';
6 | import { GitOriginType } from '../repository/types';
7 | import { IAvatarProvider } from './types';
8 |
9 | @injectable()
10 | export abstract class BaseAvatarProvider implements IAvatarProvider {
11 | protected readonly httpProxy: string;
12 | private readonly avatarStateStore: IStateStore;
13 | public constructor(
14 | protected serviceContainer: IServiceContainer,
15 | @unmanaged() private remoteRepoType: GitOriginType,
16 | ) {
17 | const workspace = this.serviceContainer.get(IWorkspaceService);
18 | this.httpProxy = workspace.getConfiguration('http').get('proxy', '');
19 | const stateStoreFactory = this.serviceContainer.get(IStateStoreFactory);
20 | this.avatarStateStore = stateStoreFactory.createStore();
21 | }
22 |
23 | public async getAvatars(repository: IGitService): Promise {
24 | const workspace = this.serviceContainer.get(IWorkspaceService);
25 | const cacheExpiration = workspace.getConfiguration('gitHistory').get('avatarCacheExpiration', 60); // in minutes (zero to disable cache)
26 |
27 | const remoteUrl = await repository.getOriginUrl();
28 | const key = `Git:Avatars:${remoteUrl}`;
29 |
30 | const cachedAvatars = this.avatarStateStore.get(key);
31 |
32 | const retry =
33 | cacheExpiration === 0 ||
34 | !cachedAvatars ||
35 | (cachedAvatars &&
36 | cachedAvatars.timestamp &&
37 | cachedAvatars.timestamp + cacheExpiration * 60 * 1000 < new Date().getTime());
38 |
39 | if (retry) {
40 | const avatars = await this.getAvatarsImplementation(repository);
41 | await this.avatarStateStore.set(key, { timestamp: new Date().getTime(), items: avatars });
42 | return avatars;
43 | } else if (cachedAvatars) {
44 | return cachedAvatars.items;
45 | }
46 |
47 | return [];
48 | }
49 | public supported(remoteRepo: GitOriginType): boolean {
50 | return remoteRepo === this.remoteRepoType;
51 | }
52 | protected abstract getAvatarsImplementation(repository: IGitService): Promise;
53 | }
54 |
--------------------------------------------------------------------------------
/src/commandHandlers/commit/gitMerge.ts:
--------------------------------------------------------------------------------
1 | import { inject, injectable } from 'inversify';
2 | import { IApplicationShell } from '../../application/types';
3 | import { CommitDetails } from '../../common/types';
4 | import { IServiceContainer } from '../../ioc/types';
5 | import { IGitServiceFactory } from '../../types';
6 | import { ICommitViewerFactory } from '../../viewers/types';
7 | import { command } from '../registration';
8 | import { IGitMergeCommandHandler } from '../types';
9 |
10 | @injectable()
11 | export class GitMergeCommandHandler implements IGitMergeCommandHandler {
12 | constructor(
13 | @inject(IServiceContainer) private serviceContainer: IServiceContainer,
14 | @inject(ICommitViewerFactory) private commitViewerFactory: ICommitViewerFactory,
15 | @inject(IApplicationShell) private applicationShell: IApplicationShell,
16 | ) {}
17 |
18 | @command('git.commit.merge', IGitMergeCommandHandler)
19 | public async merge(commit: CommitDetails, showPrompt = true) {
20 | commit = commit ? commit : this.commitViewerFactory.getCommitViewer().selectedCommit;
21 | const gitService = await this.serviceContainer
22 | .get(IGitServiceFactory)
23 | .createGitService(commit.workspaceFolder);
24 | const currentBranch = gitService.getCurrentBranch();
25 |
26 | const commitBranches = (await gitService.getRefsContainingCommit(commit.logEntry.hash.full))
27 | .filter(value => value.name!.length > 0)
28 | .map(x => x.name!);
29 |
30 | const branchMsg = `Choose the commit/branch to merge into ${currentBranch}`;
31 | const rev = await this.applicationShell.showQuickPick([commit.logEntry.hash.full, ...commitBranches], {
32 | placeHolder: branchMsg,
33 | });
34 |
35 | let type: string;
36 | if (rev === undefined || rev.length === 0) {
37 | return;
38 | }
39 | if (rev === commit.logEntry.hash.full) {
40 | type = 'commit';
41 | } else {
42 | type = 'branch';
43 | }
44 |
45 | const msg = `Merge ${type} '${rev}' into ${currentBranch}?`;
46 | const yesNo = showPrompt
47 | ? await this.applicationShell.showQuickPick(['Yes', 'No'], { placeHolder: msg })
48 | : 'Yes';
49 |
50 | if (yesNo === undefined || yesNo === 'No') {
51 | return;
52 | }
53 |
54 | gitService.merge(rev).catch(err => {
55 | if (typeof err === 'string') {
56 | this.applicationShell.showErrorMessage(err);
57 | }
58 | });
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/common/uiLogger.ts:
--------------------------------------------------------------------------------
1 | import { injectable } from 'inversify';
2 | import { Disposable, OutputChannel, workspace } from 'vscode';
3 | import { getLogChannel } from '../logger';
4 | import { ILogService } from './types';
5 |
6 | @injectable()
7 | export class OutputPanelLogger implements ILogService {
8 | private readonly outputChannel: OutputChannel;
9 | private enabled: boolean;
10 | private traceEnabled: boolean;
11 | private disposable: Disposable;
12 | constructor() {
13 | this.outputChannel = getLogChannel();
14 | this.enabled = false;
15 | this.traceEnabled = false;
16 |
17 | this.updateEnabledFlag();
18 | this.disposable = workspace.onDidChangeConfiguration(() => this.updateEnabledFlag());
19 | }
20 | public dispose() {
21 | this.disposable.dispose();
22 | }
23 | public log(...args: any[]): void {
24 | if (!this.enabled) {
25 | return;
26 | }
27 |
28 | const formattedText = this.formatArgs(...args);
29 | this.outputChannel.appendLine(formattedText);
30 | }
31 | public error(...args: any[]): void {
32 | const formattedText = this.formatArgs(...args);
33 | this.outputChannel.appendLine(formattedText);
34 | this.outputChannel.show();
35 | }
36 | public trace(...args: any[]): void {
37 | if (!this.traceEnabled) {
38 | return;
39 | }
40 | const formattedText = this.formatArgs(...args);
41 | this.outputChannel.appendLine(formattedText);
42 | }
43 | public formatArgs(...args: any[]): string {
44 | return args
45 | .map(arg => {
46 | if (arg instanceof Error) {
47 | const error: { [key: string]: any } = {};
48 | Object.getOwnPropertyNames(arg).forEach(key => {
49 | error[key] = arg[key];
50 | });
51 |
52 | return JSON.stringify(error);
53 | } else if (arg !== null && arg !== undefined && typeof arg === 'object') {
54 | return JSON.stringify(arg);
55 | } else if (typeof arg === 'string' && arg.startsWith('--format=')) {
56 | return '--pretty=oneline';
57 | } else {
58 | return `${arg}`;
59 | }
60 | })
61 | .join(' ');
62 | }
63 |
64 | private updateEnabledFlag() {
65 | const logLevel = workspace.getConfiguration('gitHistory').get('logLevel', 'None');
66 | this.enabled = logLevel !== 'None';
67 | this.traceEnabled = logLevel === 'Debug';
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/browser/src/components/LogView/Commit/Author/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { connect } from 'react-redux';
3 | import { ActionedDetails } from '../../../../definitions';
4 | import { RootState } from '../../../../reducers/index';
5 | import { ResultActions } from '../../../../actions/results';
6 | import { GoEye } from 'react-icons/go';
7 |
8 | type AuthorProps = {
9 | result: ActionedDetails;
10 | locale: string;
11 | selectAuthor(author: string): void;
12 | };
13 |
14 | export function Author(props: AuthorProps) {
15 | function selectAuthor(event: React.MouseEvent) {
16 | event.preventDefault();
17 | event.stopPropagation();
18 | props.selectAuthor(props.result.name);
19 | }
20 | return (
21 |
22 |
28 |
29 |
30 |
31 |
32 |
33 | {props.result.name}
34 |
35 |
on {formatDateTime(props.locale, props.result.date)}
36 |
37 | );
38 | }
39 |
40 | function formatDateTime(locale: string, date?: Date) {
41 | if (date && typeof date.toLocaleDateString !== 'function') {
42 | return '';
43 | }
44 |
45 | const dateOptions = {
46 | weekday: 'short',
47 | day: 'numeric',
48 | month: 'short',
49 | year: 'numeric',
50 | hour: 'numeric',
51 | minute: 'numeric',
52 | };
53 | try {
54 | locale = typeof locale === 'string' ? locale.replace('_', '-') : locale;
55 | return date.toLocaleString(locale);
56 | } catch {
57 | // @ts-ignore
58 | return date.toLocaleString(undefined, dateOptions);
59 | }
60 | }
61 |
62 | function mapStateToProps(state: RootState, wrapper: { result: ActionedDetails }) {
63 | return {
64 | result: wrapper.result,
65 | locale: state.vscode.locale,
66 | };
67 | }
68 |
69 | function mapDispatchToProps(dispatch) {
70 | return {
71 | selectAuthor: (text: string) => dispatch(ResultActions.selectAuthor(text)),
72 | };
73 | }
74 |
75 | export default connect(mapStateToProps, mapDispatchToProps)(Author);
76 |
--------------------------------------------------------------------------------
/src/adapter/repository/gitRemoteService.ts:
--------------------------------------------------------------------------------
1 | import { Repository } from './git.d';
2 | import { IGitCommandExecutor } from '..';
3 | import { GitOriginType } from '.';
4 | import { captureTelemetry } from '../../common/telemetry';
5 |
6 | export class GitRemoteService {
7 | constructor(private readonly repo: Repository, private readonly gitCmdExecutor: IGitCommandExecutor) {}
8 | private get currentBranch(): string {
9 | return this.repo.state.HEAD!.name || '';
10 | }
11 |
12 | public async getBranchesConfiguredForPullForRemote(remoteName: string): Promise {
13 | const gitShowRemoteOutput = await this.gitCmdExecutor.exec(
14 | this.repo.rootUri.fsPath,
15 | ...['remote', 'show', remoteName, '-n'],
16 | );
17 |
18 | const lines = gitShowRemoteOutput
19 | .split(/\r?\n/g)
20 | .map(line => line.trim())
21 | .filter(line => line.length > 0);
22 |
23 | const startLineIndex = lines.findIndex(line => line.startsWith('Local branches configured for'));
24 | const endLineIndex = lines.findIndex(line => line.startsWith('Local ref configured for'));
25 |
26 | if (startLineIndex === -1 || endLineIndex == -1) {
27 | // TODO: Capture telemetry, something is wrong.
28 | return [];
29 | }
30 | if (startLineIndex > endLineIndex) {
31 | // TODO: Capture telemetry, something is wrong.
32 | return [];
33 | }
34 |
35 | // Branch name is first word in the line
36 | return lines.slice(startLineIndex + 1, endLineIndex).map(line => line.split(' ')[0]);
37 | }
38 | public async getOriginType(url?: string): Promise {
39 | if (!url) {
40 | url = await this.getOriginUrl();
41 | }
42 |
43 | if (url.indexOf('github.com') > 0) {
44 | return GitOriginType.github;
45 | } else if (url.indexOf('bitbucket') > 0) {
46 | return GitOriginType.bitbucket;
47 | } else if (url.indexOf('visualstudio') > 0) {
48 | return GitOriginType.vsts;
49 | }
50 | return undefined;
51 | }
52 |
53 | @captureTelemetry()
54 | public async getOriginUrl(branchName?: string): Promise {
55 | branchName = branchName || this.currentBranch;
56 |
57 | const branch = await this.repo.getBranch(branchName);
58 |
59 | if (branch.upstream) {
60 | const remoteIndex = this.repo.state.remotes.findIndex(x => x.name === branch.upstream!.remote);
61 | return this.repo.state.remotes[remoteIndex].fetchUrl || '';
62 | }
63 |
64 | return '';
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/ioc/serviceManager.ts:
--------------------------------------------------------------------------------
1 | import { Container } from 'inversify';
2 | import { Abstract, IServiceManager, Newable } from './types';
3 | export class ServiceManager implements IServiceManager {
4 | constructor(private container: Container) {}
5 | public add(
6 | serviceIdentifier: string | symbol | Newable | Abstract,
7 | constructor: new (...args: any[]) => T,
8 | name?: string | number | symbol | undefined,
9 | ): void {
10 | if (name) {
11 | this.container
12 | .bind(serviceIdentifier)
13 | .to(constructor)
14 | .inSingletonScope()
15 | .whenTargetNamed(name);
16 | } else {
17 | this.container
18 | .bind(serviceIdentifier)
19 | .to(constructor)
20 | .inSingletonScope();
21 | }
22 | }
23 | public addSingleton(
24 | serviceIdentifier: string | symbol | Newable | Abstract,
25 | constructor: new (...args: any[]) => T,
26 | name?: string | number | symbol | undefined,
27 | ): void {
28 | if (name) {
29 | this.container
30 | .bind(serviceIdentifier)
31 | .to(constructor)
32 | .inSingletonScope()
33 | .whenTargetNamed(name);
34 | } else {
35 | this.container
36 | .bind(serviceIdentifier)
37 | .to(constructor)
38 | .inSingletonScope();
39 | }
40 | }
41 | public addSingletonInstance(
42 | serviceIdentifier: string | symbol | Newable | Abstract,
43 | instance: T,
44 | name?: string | number | symbol | undefined,
45 | ): void {
46 | if (name) {
47 | this.container
48 | .bind(serviceIdentifier)
49 | .toConstantValue(instance)
50 | .whenTargetNamed(name);
51 | } else {
52 | this.container.bind(serviceIdentifier).toConstantValue(instance);
53 | }
54 | }
55 | public get(
56 | serviceIdentifier: string | symbol | Newable | Abstract,
57 | name?: string | number | symbol | undefined,
58 | ): T {
59 | return name ? this.container.getNamed(serviceIdentifier, name) : this.container.get(serviceIdentifier);
60 | }
61 | public getAll(
62 | serviceIdentifier: string | symbol | Newable | Abstract,
63 | name?: string | number | symbol | undefined,
64 | ): T[] {
65 | return name
66 | ? this.container.getAllNamed(serviceIdentifier, name)
67 | : this.container.getAll(serviceIdentifier);
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/commandHandlers/commit/compare.ts:
--------------------------------------------------------------------------------
1 | import { inject, injectable } from 'inversify';
2 | import { IApplicationShell, ICommandManager } from '../../application/types';
3 | import { CommitDetails, CompareCommitDetails } from '../../common/types';
4 | import { IServiceContainer } from '../../ioc/types';
5 | import { IGitServiceFactory } from '../../types';
6 | import { ICommitViewerFactory } from '../../viewers/types';
7 | import { command } from '../registration';
8 | import { IGitCompareCommandHandler } from '../types';
9 |
10 | @injectable()
11 | export class GitCompareCommitCommandHandler implements IGitCompareCommandHandler {
12 | private _previouslySelectedCommit?: CommitDetails;
13 |
14 | constructor(
15 | @inject(IServiceContainer) private serviceContainer: IServiceContainer,
16 | @inject(ICommandManager) private commandManager: ICommandManager,
17 | @inject(ICommitViewerFactory) private commitViewerFactory: ICommitViewerFactory,
18 | @inject(IApplicationShell) private application: IApplicationShell,
19 | ) {}
20 |
21 | public get selectedCommit(): CommitDetails | undefined {
22 | return this._previouslySelectedCommit;
23 | }
24 |
25 | @command('git.commit.compare.selectForComparison', IGitCompareCommandHandler)
26 | public async select(commit: CommitDetails): Promise {
27 | await this.commandManager.executeCommand('setContext', 'git.commit.compare.selectedForComparison', true);
28 | this._previouslySelectedCommit = commit;
29 | }
30 |
31 | @command('git.commit.compare', IGitCompareCommandHandler)
32 | public async compare(commit: CommitDetails): Promise {
33 | if (!this.selectedCommit) {
34 | await this.application.showErrorMessage('Please select another file to compare with');
35 | return;
36 | }
37 | await this.commandManager.executeCommand('setContext', 'git.commit.compare.compared', true);
38 | await this.commandManager.executeCommand('setContext', 'git.commit.compare.view.show', true);
39 | // display explorer view when running compare
40 | await this.commandManager.executeCommand('workbench.view.explorer');
41 | const gitService = await this.serviceContainer
42 | .get(IGitServiceFactory)
43 | .createGitService(commit.workspaceFolder);
44 | const fileDiffs = await gitService.getDifferences(
45 | this.selectedCommit.logEntry.hash.full,
46 | commit.logEntry.hash.full,
47 | );
48 | const compareCommit = new CompareCommitDetails(this.selectedCommit, commit, fileDiffs);
49 | this.commitViewerFactory.getCompareCommitViewer().showCommitTree(compareCommit);
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/platform/fileSystem.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation. All rights reserved.
2 | // Licensed under the MIT License.
3 |
4 | 'use strict';
5 |
6 | import * as fs from 'fs';
7 | import * as fse from 'fs-extra';
8 | import { inject, injectable } from 'inversify';
9 | import * as path from 'path';
10 | import { IFileSystem, IPlatformService } from './types';
11 |
12 | @injectable()
13 | export class FileSystem implements IFileSystem {
14 | constructor(@inject(IPlatformService) private platformService: IPlatformService) {}
15 |
16 | public get directorySeparatorChar(): string {
17 | return path.sep;
18 | }
19 |
20 | public objectExistsAsync(filePath: string, statCheck: (s: fs.Stats) => boolean): Promise {
21 | return new Promise(resolve => {
22 | fse.stat(filePath, (error, stats) => {
23 | if (error) {
24 | return resolve(false);
25 | }
26 |
27 | return resolve(statCheck(stats));
28 | });
29 | });
30 | }
31 |
32 | public fileExistsAsync(filePath: string): Promise {
33 | return this.objectExistsAsync(filePath, stats => stats.isFile());
34 | }
35 |
36 | public directoryExistsAsync(filePath: string): Promise {
37 | return this.objectExistsAsync(filePath, stats => stats.isDirectory());
38 | }
39 |
40 | public createDirectoryAsync(directoryPath: string): Promise {
41 | return fse.mkdirp(directoryPath);
42 | }
43 |
44 | public getSubDirectoriesAsync(rootDir: string): Promise {
45 | return new Promise(resolve => {
46 | fs.readdir(rootDir, (error, files) => {
47 | if (error) {
48 | return resolve([]);
49 | }
50 | const subDirs: string[] = [];
51 | files.forEach(name => {
52 | const fullPath = path.join(rootDir, name);
53 | try {
54 | if (fs.statSync(fullPath).isDirectory()) {
55 | subDirs.push(fullPath);
56 | }
57 | } catch (ex) {}
58 | });
59 | resolve(subDirs);
60 | });
61 | });
62 | }
63 |
64 | public arePathsSame(path1: string, path2: string): boolean {
65 | const path1ToCompare = path.normalize(path1);
66 | const path2ToCompare = path.normalize(path2);
67 | if (this.platformService.isWindows) {
68 | return path1ToCompare.toUpperCase() === path2ToCompare.toUpperCase();
69 | } else {
70 | return path1ToCompare === path2ToCompare;
71 | }
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/browser/src/reducers/logEntries.ts:
--------------------------------------------------------------------------------
1 | import { handleActions } from 'redux-actions';
2 | import * as Actions from '../constants/actions';
3 | import { LogEntry } from '../definitions';
4 | import { LogEntriesResponse } from '../types';
5 | import { LogEntriesState } from './';
6 |
7 | const initialState: LogEntriesState = {
8 | count: 0,
9 | isLoading: false,
10 | isLoadingCommit: undefined,
11 | items: [],
12 | pageIndex: 0,
13 | };
14 |
15 | function fixDates(logEntry: LogEntry) {
16 | if (logEntry.author && typeof logEntry.author.date === 'string') {
17 | logEntry.author.date = new Date(logEntry.author.date);
18 | }
19 | if (logEntry.committer && typeof logEntry.committer.date === 'string') {
20 | logEntry.committer.date = new Date(logEntry.committer.date);
21 | }
22 | }
23 |
24 | export default handleActions(
25 | {
26 | [Actions.FETCHED_COMMITS]: (state, action: ReduxActions.Action) => {
27 | action.payload!.items.forEach(x => {
28 | fixDates(x);
29 | });
30 |
31 | return {
32 | ...state,
33 | ...action.payload!,
34 | selected: undefined,
35 | isLoading: false,
36 | isLoadingCommit: undefined,
37 | };
38 | },
39 |
40 | [Actions.UPDATE_COMMIT_IN_LIST]: (state, action: ReduxActions.Action) => {
41 | const index = state.items.findIndex(item => item.hash.full === action.payload.hash.full);
42 |
43 | if (index >= 0) {
44 | const logEntry = JSON.parse(JSON.stringify(action.payload));
45 | fixDates(logEntry);
46 | state.items.splice(index, 1, logEntry);
47 | }
48 | return {
49 | ...state,
50 | isLoadingCommit: undefined,
51 | };
52 | },
53 | [Actions.FETCHED_COMMIT]: (state, action: ReduxActions.Action) => {
54 | fixDates(action.payload);
55 |
56 | return {
57 | ...state,
58 | isLoadingCommit: undefined,
59 | selected: action.payload,
60 | };
61 | },
62 |
63 | [Actions.IS_FETCHING_COMMIT]: (state, action: ReduxActions.Action) => {
64 | return { ...state, isLoadingCommit: action.payload } as LogEntriesState;
65 | },
66 | [Actions.CLEAR_SELECTED_COMMIT]: (state, action: any) => {
67 | return { ...state, selected: undefined } as LogEntriesState;
68 | },
69 |
70 | [Actions.IS_LOADING_COMMITS]: (state, action) => {
71 | return { ...state, isLoading: true } as LogEntriesState;
72 | },
73 | },
74 | initialState,
75 | );
76 |
--------------------------------------------------------------------------------
/src/common/types.ts:
--------------------------------------------------------------------------------
1 | import { Command } from 'vscode';
2 | import { BranchSelection, CommittedFile, LogEntry } from '../types';
3 |
4 | export const ILogService = Symbol.for('ILogService');
5 |
6 | export interface ILogService {
7 | log(...args: any[]): void;
8 | trace(...args: any[]): void;
9 | error(...args: any[]): void;
10 | }
11 |
12 | export const IUiService = Symbol.for('IUiService');
13 |
14 | export interface IUiService {
15 | getBranchSelection(): Promise;
16 | selectFileCommitCommandAction(fileCommit: FileCommitDetails): Promise