Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions build/lib/i18n.resources.json
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,10 @@
{
"name": "vs/workbench/services/userDataSync",
"project": "vscode-workbench"
},
{
"name": "vs/workbench/contrib/timeline",
"project": "vscode-workbench"
}
]
}
1 change: 1 addition & 0 deletions extensions/git/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -1765,6 +1765,7 @@
},
"dependencies": {
"byline": "^5.0.0",
"dayjs": "1.8.19",
"file-type": "^7.2.0",
"iconv-lite": "^0.4.24",
"jschardet": "2.1.1",
Expand Down
4 changes: 3 additions & 1 deletion extensions/git/src/api/git.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ export interface Commit {
readonly hash: string;
readonly message: string;
readonly parents: string[];
readonly authorEmail?: string | undefined;
readonly authorDate?: Date;
readonly authorName?: string;
readonly authorEmail?: string;
}

export interface Submodule {
Expand Down
11 changes: 11 additions & 0 deletions extensions/git/src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2332,6 +2332,17 @@ export class CommandCenter {
return result && result.stash;
}

@command('git.openDiff', { repository: false })
async openDiff(uri: Uri, hash: string) {
const basename = path.basename(uri.fsPath);

if (hash === '~') {
return commands.executeCommand('vscode.diff', toGitUri(uri, hash), toGitUri(uri, `HEAD`), `${basename} (Index)`);
}

return commands.executeCommand('vscode.diff', toGitUri(uri, `${hash}^`), toGitUri(uri, hash), `${basename} (${hash.substr(0, 8)}^) \u27f7 ${basename} (${hash.substr(0, 8)})`);
}

private createCommand(id: string, key: string, method: Function, options: CommandOptions): (...args: any[]) => any {
const result = (...args: any[]) => {
let result: Promise<any>;
Expand Down
104 changes: 69 additions & 35 deletions extensions/git/src/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { EventEmitter } from 'events';
import iconv = require('iconv-lite');
import * as filetype from 'file-type';
import { assign, groupBy, IDisposable, toDisposable, dispose, mkdirp, readBytes, detectUnicodeEncoding, Encoding, onceEvent, splitInChunks, Limiter } from './util';
import { CancellationToken, Progress } from 'vscode';
import { CancellationToken, Progress, Uri } from 'vscode';
import { URI } from 'vscode-uri';
import { detectEncoding } from './encoding';
import { Ref, RefType, Branch, Remote, GitErrorCodes, LogOptions, Change, Status } from './api/git';
Expand Down Expand Up @@ -45,6 +45,15 @@ interface MutableRemote extends Remote {
isReadOnly: boolean;
}

// TODO[ECA]: Move to git.d.ts once we are good with the api
/**
* Log file options.
*/
export interface LogFileOptions {
/** Max number of log entries to retrieve. If not specified, the default is 32. */
readonly maxEntries?: number;
}

function parseVersion(raw: string): string {
return raw.replace(/^git version /, '');
}
Expand Down Expand Up @@ -318,7 +327,7 @@ function getGitErrorCode(stderr: string): string | undefined {
return undefined;
}

const COMMIT_FORMAT = '%H\n%ae\n%P\n%B';
const COMMIT_FORMAT = '%H\n%aN\n%aE\n%at\n%P\n%B';

export class Git {

Expand Down Expand Up @@ -503,7 +512,9 @@ export interface Commit {
hash: string;
message: string;
parents: string[];
authorEmail?: string | undefined;
authorDate?: Date;
authorName?: string;
authorEmail?: string;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, I'll wait for this to come in before merging #89005.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The merge probably wouldn't go very well, since I changed the parsing from how it was. But I can easily add commitDate in if we want.

}

export class GitStatusParser {
Expand Down Expand Up @@ -634,14 +645,43 @@ export function parseGitmodules(raw: string): Submodule[] {
return result;
}

export function parseGitCommit(raw: string): Commit | null {
const match = /^([0-9a-f]{40})\n(.*)\n(.*)(\n([^]*))?$/m.exec(raw.trim());
if (!match) {
return null;
}
const commitRegex = /([0-9a-f]{40})\n(.*)\n(.*)\n(.*)\n(.*)(?:\n([^]*?))?(?:\x00)/gm;

export function parseGitCommits(data: string): Commit[] {
let commits: Commit[] = [];

let ref;
let name;
let email;
let date;
let parents;
let message;
let match;

do {
match = commitRegex.exec(data);
if (match === null) {
break;
}

const parents = match[3] ? match[3].split(' ') : [];
return { hash: match[1], message: match[5], parents, authorEmail: match[2] };
[, ref, name, email, date, parents, message] = match;

if (message[message.length - 1] === '\n') {
message = message.substr(0, message.length - 1);
}

// Stop excessive memory usage by using substr -- https://bugs.chromium.org/p/v8/issues/detail?id=2869
commits.push({
hash: ` ${ref}`.substr(1),
message: ` ${message}`.substr(1),
parents: parents ? parents.split(' ') : [],
authorDate: new Date(Number(date) * 1000),
authorName: ` ${name}`.substr(1),
authorEmail: ` ${email}`.substr(1)
});
} while (true);

return commits;
}

interface LsTreeElement {
Expand Down Expand Up @@ -760,38 +800,28 @@ export class Repository {

async log(options?: LogOptions): Promise<Commit[]> {
const maxEntries = options && typeof options.maxEntries === 'number' && options.maxEntries > 0 ? options.maxEntries : 32;
const args = ['log', '-' + maxEntries, `--pretty=format:${COMMIT_FORMAT}%x00%x00`];
const args = ['log', '-' + maxEntries, `--format:${COMMIT_FORMAT}`, '-z'];

const gitResult = await this.run(args);
if (gitResult.exitCode) {
const result = await this.run(args);
if (result.exitCode) {
// An empty repo
return [];
}

const s = gitResult.stdout;
const result: Commit[] = [];
let index = 0;
while (index < s.length) {
let nextIndex = s.indexOf('\x00\x00', index);
if (nextIndex === -1) {
nextIndex = s.length;
}
return parseGitCommits(result.stdout);
}

let entry = s.substr(index, nextIndex - index);
if (entry.startsWith('\n')) {
entry = entry.substring(1);
}
async logFile(uri: Uri, options?: LogFileOptions): Promise<Commit[]> {
const maxEntries = options?.maxEntries ?? 32;
const args = ['log', `-${maxEntries}`, `--format=${COMMIT_FORMAT}`, '-z', '--', uri.fsPath];

const commit = parseGitCommit(entry);
if (!commit) {
break;
}

result.push(commit);
index = nextIndex + 2;
const result = await this.run(args);
if (result.exitCode) {
// No file history, e.g. a new file or untracked
return [];
}

return result;
return parseGitCommits(result.stdout);
}

async bufferString(object: string, encoding: string = 'utf8', autoGuessEncoding = false): Promise<string> {
Expand Down Expand Up @@ -1853,8 +1883,12 @@ export class Repository {
}

async getCommit(ref: string): Promise<Commit> {
const result = await this.run(['show', '-s', `--format=${COMMIT_FORMAT}`, ref]);
return parseGitCommit(result.stdout) || Promise.reject<Commit>('bad commit format');
const result = await this.run(['show', '-s', `--format=${COMMIT_FORMAT}`, '-z', ref]);
const commits = parseGitCommits(result.stdout);
if (commits.length === 0) {
return Promise.reject<Commit>('bad commit format');
}
return commits[0];
}

async updateSubmodules(paths: string[]): Promise<void> {
Expand Down
4 changes: 3 additions & 1 deletion extensions/git/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { GitExtensionImpl } from './api/extension';
import * as path from 'path';
import * as fs from 'fs';
import { createIPCServer, IIPCServer } from './ipc/ipcServer';
import { GitTimelineProvider } from './timelineProvider';

const deactivateTasks: { (): Promise<any>; }[] = [];

Expand Down Expand Up @@ -82,7 +83,8 @@ async function createModel(context: ExtensionContext, outputChannel: OutputChann
new GitContentProvider(model),
new GitFileSystemProvider(model),
new GitDecorations(model),
new GitProtocolHandler()
new GitProtocolHandler(),
new GitTimelineProvider(model)
);

await checkGitVersion(info);
Expand Down
8 changes: 7 additions & 1 deletion extensions/git/src/repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import * as nls from 'vscode-nls';
import { Branch, Change, GitErrorCodes, LogOptions, Ref, RefType, Remote, Status } from './api/git';
import { AutoFetcher } from './autofetch';
import { debounce, memoize, throttle } from './decorators';
import { Commit, CommitOptions, ForcePushMode, GitError, Repository as BaseRepository, Stash, Submodule } from './git';
import { Commit, CommitOptions, ForcePushMode, GitError, Repository as BaseRepository, Stash, Submodule, LogFileOptions } from './git';
import { StatusBarCommands } from './statusbar';
import { toGitUri } from './uri';
import { anyEvent, combinedDisposable, debounceEvent, dispose, EmptyDisposable, eventToPromise, filterEvent, find, IDisposable, isDescendant, onceEvent } from './util';
Expand Down Expand Up @@ -304,6 +304,7 @@ export const enum Operation {
Apply = 'Apply',
Blame = 'Blame',
Log = 'Log',
LogFile = 'LogFile',
}

function isReadOnly(operation: Operation): boolean {
Expand Down Expand Up @@ -868,6 +869,11 @@ export class Repository implements Disposable {
return this.run(Operation.Log, () => this.repository.log(options));
}

logFile(uri: Uri, options?: LogFileOptions): Promise<Commit[]> {
// TODO: This probably needs per-uri granularity
return this.run(Operation.LogFile, () => this.repository.logFile(uri, options));
}

@throttle
async status(): Promise<void> {
await this.run(Operation.Status);
Expand Down
20 changes: 10 additions & 10 deletions extensions/git/src/test/git.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/

import 'mocha';
import { GitStatusParser, parseGitCommit, parseGitmodules, parseLsTree, parseLsFiles } from '../git';
import { GitStatusParser, parseGitCommits, parseGitmodules, parseLsTree, parseLsFiles } from '../git';
import * as assert from 'assert';
import { splitInChunks } from '../util';

Expand Down Expand Up @@ -191,42 +191,42 @@ suite('git', () => {
const GIT_OUTPUT_SINGLE_PARENT = `52c293a05038d865604c2284aa8698bd087915a1
[email protected]
8e5a374372b8393906c7e380dbb09349c5385554
This is a commit message.`;
This is a commit message.\x00`;

assert.deepEqual(parseGitCommit(GIT_OUTPUT_SINGLE_PARENT), {
assert.deepEqual(parseGitCommits(GIT_OUTPUT_SINGLE_PARENT), [{
hash: '52c293a05038d865604c2284aa8698bd087915a1',
message: 'This is a commit message.',
parents: ['8e5a374372b8393906c7e380dbb09349c5385554'],
authorEmail: '[email protected]',
});
}]);
});

test('multiple parent commits', function () {
const GIT_OUTPUT_MULTIPLE_PARENTS = `52c293a05038d865604c2284aa8698bd087915a1
[email protected]
8e5a374372b8393906c7e380dbb09349c5385554 df27d8c75b129ab9b178b386077da2822101b217
This is a commit message.`;
This is a commit message.\x00`;

assert.deepEqual(parseGitCommit(GIT_OUTPUT_MULTIPLE_PARENTS), {
assert.deepEqual(parseGitCommits(GIT_OUTPUT_MULTIPLE_PARENTS), [{
hash: '52c293a05038d865604c2284aa8698bd087915a1',
message: 'This is a commit message.',
parents: ['8e5a374372b8393906c7e380dbb09349c5385554', 'df27d8c75b129ab9b178b386077da2822101b217'],
authorEmail: '[email protected]',
});
}]);
});

test('no parent commits', function () {
const GIT_OUTPUT_NO_PARENTS = `52c293a05038d865604c2284aa8698bd087915a1
[email protected]

This is a commit message.`;
This is a commit message.\x00`;

assert.deepEqual(parseGitCommit(GIT_OUTPUT_NO_PARENTS), {
assert.deepEqual(parseGitCommits(GIT_OUTPUT_NO_PARENTS), [{
hash: '52c293a05038d865604c2284aa8698bd087915a1',
message: 'This is a commit message.',
parents: [],
authorEmail: '[email protected]',
});
}]);
});
});

Expand Down
Loading