mirror of
https://github.com/microsoft/vscode.git
synced 2025-12-24 12:19:20 +00:00
Git - get the diff and num stats for a commit (#284403)
This commit is contained in:
@@ -7,7 +7,7 @@
|
||||
|
||||
import { Model } from '../model';
|
||||
import { Repository as BaseRepository, Resource } from '../repository';
|
||||
import { InputBox, Git, API, Repository, Remote, RepositoryState, Branch, ForcePushMode, Ref, Submodule, Commit, Change, RepositoryUIState, Status, LogOptions, APIState, CommitOptions, RefType, CredentialsProvider, BranchQuery, PushErrorHandler, PublishEvent, FetchOptions, RemoteSourceProvider, RemoteSourcePublisher, PostCommitCommandsProvider, RefQuery, BranchProtectionProvider, InitOptions, SourceControlHistoryItemDetailsProvider, GitErrorCodes, CloneOptions, CommitShortStat } from './git';
|
||||
import { InputBox, Git, API, Repository, Remote, RepositoryState, Branch, ForcePushMode, Ref, Submodule, Commit, Change, RepositoryUIState, Status, LogOptions, APIState, CommitOptions, RefType, CredentialsProvider, BranchQuery, PushErrorHandler, PublishEvent, FetchOptions, RemoteSourceProvider, RemoteSourcePublisher, PostCommitCommandsProvider, RefQuery, BranchProtectionProvider, InitOptions, SourceControlHistoryItemDetailsProvider, GitErrorCodes, CloneOptions, CommitShortStat, DiffChange } from './git';
|
||||
import { Event, SourceControlInputBox, Uri, SourceControl, Disposable, commands, CancellationToken } from 'vscode';
|
||||
import { combinedDisposable, filterEvent, mapEvent } from '../util';
|
||||
import { toGitUri } from '../uri';
|
||||
@@ -199,6 +199,10 @@ export class ApiRepository implements Repository {
|
||||
return this.#repository.diffBetween(ref1, ref2, path);
|
||||
}
|
||||
|
||||
diffBetweenWithStats(ref1: string, ref2: string, path?: string): Promise<DiffChange[]> {
|
||||
return this.#repository.diffBetweenWithStats(ref1, ref2, path);
|
||||
}
|
||||
|
||||
hashObject(data: string): Promise<string> {
|
||||
return this.#repository.hashObject(data);
|
||||
}
|
||||
|
||||
5
extensions/git/src/api/git.d.ts
vendored
5
extensions/git/src/api/git.d.ts
vendored
@@ -121,6 +121,11 @@ export interface Change {
|
||||
readonly status: Status;
|
||||
}
|
||||
|
||||
export interface DiffChange extends Change {
|
||||
readonly insertions: number;
|
||||
readonly deletions: number;
|
||||
}
|
||||
|
||||
export interface RepositoryState {
|
||||
readonly HEAD: Branch | undefined;
|
||||
readonly refs: Ref[];
|
||||
|
||||
@@ -3198,7 +3198,7 @@ export class CommandCenter {
|
||||
}
|
||||
|
||||
try {
|
||||
const changes = await repository.diffBetween2(ref1.id, ref2.id);
|
||||
const changes = await repository.diffBetweenWithStats(ref1.id, ref2.id);
|
||||
|
||||
if (changes.length === 0) {
|
||||
window.showInformationMessage(l10n.t('There are no changes between "{0}" and "{1}".', ref1.displayId ?? ref1.id, ref2.displayId ?? ref2.id));
|
||||
@@ -4785,7 +4785,7 @@ export class CommandCenter {
|
||||
|
||||
const commit = await repository.getCommit(item.ref);
|
||||
const commitParentId = commit.parents.length > 0 ? commit.parents[0] : await repository.getEmptyTree();
|
||||
const changes = await repository.diffBetween2(commitParentId, commit.hash);
|
||||
const changes = await repository.diffBetweenWithStats(commitParentId, commit.hash);
|
||||
const resources = changes.map(c => toMultiFileDiffEditorUris(c, commitParentId, commit.hash));
|
||||
|
||||
const title = `${item.shortRef} - ${subject(commit.message)}`;
|
||||
@@ -5059,7 +5059,7 @@ export class CommandCenter {
|
||||
|
||||
const multiDiffSourceUri = Uri.from({ scheme: 'scm-history-item', path: `${repository.root}/${historyItemParentId}..${historyItemId}` });
|
||||
|
||||
const changes = await repository.diffBetween2(historyItemParentId, historyItemId);
|
||||
const changes = await repository.diffBetweenWithStats(historyItemParentId, historyItemId);
|
||||
const resources = changes.map(c => toMultiFileDiffEditorUris(c, historyItemParentId, historyItemId));
|
||||
const reveal = revealUri ? { modifiedUri: toGitUri(revealUri, historyItemId) } : undefined;
|
||||
|
||||
|
||||
@@ -257,7 +257,7 @@ class GitIncomingChangesFileDecorationProvider implements FileDecorationProvider
|
||||
return [];
|
||||
}
|
||||
|
||||
const changes = await this.repository.diffBetween2(ancestor, currentHistoryItemRemoteRef.id);
|
||||
const changes = await this.repository.diffBetweenWithStats(ancestor, currentHistoryItemRemoteRef.id);
|
||||
return changes;
|
||||
} catch (err) {
|
||||
return [];
|
||||
|
||||
@@ -13,7 +13,7 @@ import { EventEmitter } from 'events';
|
||||
import * as filetype from 'file-type';
|
||||
import { assign, groupBy, IDisposable, toDisposable, dispose, mkdirp, readBytes, detectUnicodeEncoding, Encoding, onceEvent, splitInChunks, Limiter, Versions, isWindows, pathEquals, isMacintosh, isDescendant, relativePathWithNoFallback, Mutable } from './util';
|
||||
import { CancellationError, CancellationToken, ConfigurationChangeEvent, LogOutputChannel, Progress, Uri, workspace } from 'vscode';
|
||||
import { Commit as ApiCommit, Ref, RefType, Branch, Remote, ForcePushMode, GitErrorCodes, LogOptions, Change, Status, CommitOptions, RefQuery as ApiRefQuery, InitOptions, Worktree } from './api/git';
|
||||
import { Commit as ApiCommit, Ref, RefType, Branch, Remote, ForcePushMode, GitErrorCodes, LogOptions, Change, Status, CommitOptions, RefQuery as ApiRefQuery, InitOptions, Worktree, DiffChange } from './api/git';
|
||||
import * as byline from 'byline';
|
||||
import { StringDecoder } from 'string_decoder';
|
||||
|
||||
@@ -1084,6 +1084,79 @@ function parseGitChanges(repositoryRoot: string, raw: string): Change[] {
|
||||
return result;
|
||||
}
|
||||
|
||||
function parseGitChangesRaw(repositoryRoot: string, raw: string): DiffChange[] {
|
||||
const changes: Change[] = [];
|
||||
const numStats = new Map<string, { insertions: number; deletions: number }>();
|
||||
|
||||
let index = 0;
|
||||
const segments = raw.trim().split('\x00').filter(s => s);
|
||||
|
||||
segmentsLoop:
|
||||
while (index < segments.length) {
|
||||
const segment = segments[index++];
|
||||
if (!segment) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (segment.startsWith(':')) {
|
||||
// Parse --raw output
|
||||
const [, , , , change] = segment.split(' ');
|
||||
const filePath = segments[index++];
|
||||
const originalUri = Uri.file(path.isAbsolute(filePath) ? filePath : path.join(repositoryRoot, filePath));
|
||||
|
||||
let uri = originalUri;
|
||||
let renameUri = originalUri;
|
||||
let status = Status.UNTRACKED;
|
||||
|
||||
switch (change[0]) {
|
||||
case 'A':
|
||||
status = Status.INDEX_ADDED;
|
||||
break;
|
||||
case 'M':
|
||||
status = Status.MODIFIED;
|
||||
break;
|
||||
case 'D':
|
||||
status = Status.DELETED;
|
||||
break;
|
||||
case 'R': {
|
||||
if (index >= segments.length) {
|
||||
break;
|
||||
}
|
||||
const newPath = segments[index++];
|
||||
if (!newPath) {
|
||||
break;
|
||||
}
|
||||
|
||||
status = Status.INDEX_RENAMED;
|
||||
uri = renameUri = Uri.file(path.isAbsolute(newPath) ? newPath : path.join(repositoryRoot, newPath));
|
||||
break;
|
||||
}
|
||||
default:
|
||||
// Unknown status
|
||||
break segmentsLoop;
|
||||
}
|
||||
|
||||
changes.push({ status, uri, originalUri, renameUri });
|
||||
} else {
|
||||
// Parse --numstat output
|
||||
const [insertions, deletions, filePath] = segment.split('\t');
|
||||
numStats.set(
|
||||
path.isAbsolute(filePath)
|
||||
? filePath
|
||||
: path.join(repositoryRoot, filePath), {
|
||||
insertions: insertions === '-' ? 0 : parseInt(insertions),
|
||||
deletions: deletions === '-' ? 0 : parseInt(deletions),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return changes.map(change => ({
|
||||
...change,
|
||||
insertions: numStats.get(change.uri.fsPath)?.insertions ?? 0,
|
||||
deletions: numStats.get(change.uri.fsPath)?.deletions ?? 0,
|
||||
}));
|
||||
}
|
||||
|
||||
export interface BlameInformation {
|
||||
readonly hash: string;
|
||||
readonly subject?: string;
|
||||
@@ -1694,8 +1767,24 @@ export class Repository {
|
||||
return result.stdout.trim();
|
||||
}
|
||||
|
||||
async diffBetween2(ref1: string, ref2: string, options: { similarityThreshold?: number }): Promise<Change[]> {
|
||||
return await this.diffFiles(`${ref1}...${ref2}`, { cached: false, similarityThreshold: options.similarityThreshold });
|
||||
async diffBetweenWithStats(ref: string, options: { path?: string; similarityThreshold?: number }): Promise<DiffChange[]> {
|
||||
const args = ['diff', '--raw', '--numstat', '--diff-filter=ADMR', '-z',];
|
||||
|
||||
if (options.similarityThreshold) {
|
||||
args.push(`--find-renames=${options.similarityThreshold}%`);
|
||||
}
|
||||
|
||||
args.push(...[ref, '--']);
|
||||
if (options.path) {
|
||||
args.push(this.sanitizeRelativePath(options.path));
|
||||
}
|
||||
|
||||
const gitResult = await this.exec(args);
|
||||
if (gitResult.exitCode) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return parseGitChangesRaw(this.repositoryRoot, gitResult.stdout);
|
||||
}
|
||||
|
||||
private async diffFiles(ref: string | undefined, options: { cached: boolean; similarityThreshold?: number }): Promise<Change[]> {
|
||||
@@ -1749,8 +1838,8 @@ export class Repository {
|
||||
}
|
||||
|
||||
|
||||
async diffTrees(treeish1: string, treeish2?: string, options?: { similarityThreshold?: number }): Promise<Change[]> {
|
||||
const args = ['diff-tree', '-r', '--name-status', '-z', '--diff-filter=ADMR'];
|
||||
async diffTrees(treeish1: string, treeish2?: string, options?: { similarityThreshold?: number }): Promise<DiffChange[]> {
|
||||
const args = ['diff-tree', '-r', '--raw', '--numstat', '--diff-filter=ADMR', '-z'];
|
||||
|
||||
if (options?.similarityThreshold) {
|
||||
args.push(`--find-renames=${options.similarityThreshold}%`);
|
||||
@@ -1769,7 +1858,7 @@ export class Repository {
|
||||
return [];
|
||||
}
|
||||
|
||||
return parseGitChanges(this.repositoryRoot, gitResult.stdout);
|
||||
return parseGitChangesRaw(this.repositoryRoot, gitResult.stdout);
|
||||
}
|
||||
|
||||
async getMergeBase(ref1: string, ref2: string, ...refs: string[]): Promise<string | undefined> {
|
||||
|
||||
@@ -339,7 +339,7 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec
|
||||
|
||||
const historyItemChangesUri: Uri[] = [];
|
||||
const historyItemChanges: SourceControlHistoryItemChange[] = [];
|
||||
const changes = await this.repository.diffBetween2(historyItemParentId, historyItemId);
|
||||
const changes = await this.repository.diffBetweenWithStats(historyItemParentId, historyItemId);
|
||||
|
||||
for (const change of changes) {
|
||||
const historyItemUri = change.uri.with({
|
||||
|
||||
@@ -10,7 +10,7 @@ import picomatch from 'picomatch';
|
||||
import { CancellationError, CancellationToken, CancellationTokenSource, Command, commands, Disposable, Event, EventEmitter, FileDecoration, FileType, l10n, LogLevel, LogOutputChannel, Memento, ProgressLocation, ProgressOptions, QuickDiffProvider, RelativePattern, scm, SourceControl, SourceControlInputBox, SourceControlInputBoxValidation, SourceControlInputBoxValidationType, SourceControlResourceDecorations, SourceControlResourceGroup, SourceControlResourceState, TabInputNotebookDiff, TabInputTextDiff, TabInputTextMultiDiff, ThemeColor, ThemeIcon, Uri, window, workspace, WorkspaceEdit } from 'vscode';
|
||||
import { ActionButton } from './actionButton';
|
||||
import { ApiRepository } from './api/api1';
|
||||
import { Branch, BranchQuery, Change, CommitOptions, FetchOptions, ForcePushMode, GitErrorCodes, LogOptions, Ref, RefType, Remote, Status, Worktree } from './api/git';
|
||||
import { Branch, BranchQuery, Change, CommitOptions, DiffChange, FetchOptions, ForcePushMode, GitErrorCodes, LogOptions, Ref, RefType, Remote, Status, Worktree } from './api/git';
|
||||
import { AutoFetcher } from './autofetch';
|
||||
import { GitBranchProtectionProvider, IBranchProtectionProviderRegistry } from './branchProtection';
|
||||
import { debounce, memoize, sequentialize, throttle } from './decorators';
|
||||
@@ -1241,7 +1241,7 @@ export class Repository implements Disposable {
|
||||
return this.run(Operation.Diff, () => this.repository.diffBetween(ref1, ref2, path));
|
||||
}
|
||||
|
||||
diffBetween2(ref1: string, ref2: string): Promise<Change[]> {
|
||||
diffBetweenWithStats(ref1: string, ref2: string, path?: string): Promise<DiffChange[]> {
|
||||
if (ref1 === this._EMPTY_TREE) {
|
||||
// Use git diff-tree to get the
|
||||
// changes in the first commit
|
||||
@@ -1251,10 +1251,11 @@ export class Repository implements Disposable {
|
||||
const scopedConfig = workspace.getConfiguration('git', Uri.file(this.root));
|
||||
const similarityThreshold = scopedConfig.get<number>('similarityThreshold', 50);
|
||||
|
||||
return this.run(Operation.Diff, () => this.repository.diffBetween2(ref1, ref2, { similarityThreshold }));
|
||||
return this.run(Operation.Diff, () =>
|
||||
this.repository.diffBetweenWithStats(`${ref1}...${ref2}`, { path, similarityThreshold }));
|
||||
}
|
||||
|
||||
diffTrees(treeish1: string, treeish2?: string): Promise<Change[]> {
|
||||
diffTrees(treeish1: string, treeish2?: string): Promise<DiffChange[]> {
|
||||
const scopedConfig = workspace.getConfiguration('git', Uri.file(this.root));
|
||||
const similarityThreshold = scopedConfig.get<number>('similarityThreshold', 50);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user