Skip to content

Commit

Permalink
#36 View and compare uncommitted changes. New actions from the new Un…
Browse files Browse the repository at this point in the history
…committed Changes context menu.
  • Loading branch information
mhutchie committed May 22, 2019
1 parent ab2f861 commit f050acb
Show file tree
Hide file tree
Showing 8 changed files with 359 additions and 176 deletions.
2 changes: 1 addition & 1 deletion media/main.css
Expand Up @@ -281,7 +281,7 @@ svg.openFolderIcon, svg.closedFolderIcon, svg.fileIcon{
.gitFile{
color:var(--vscode-gitDecoration-modifiedResourceForeground);
}
.gitFile.A{
.gitFile.A, .gitFile.U{
color:var(--vscode-gitDecoration-addedResourceForeground);
}
.gitFile.D{
Expand Down
83 changes: 64 additions & 19 deletions src/dataSource.ts
@@ -1,7 +1,7 @@
import * as cp from 'child_process';
import { getConfig } from './config';
import { GitCommandStatus, GitCommit, GitCommitDetails, GitCommitNode, GitFileChange, GitFileChangeType, GitRefData, GitResetMode, GitUnsavedChanges } from './types';
import { getPathFromStr } from './utils';
import { getPathFromStr, UNCOMMITTED } from './utils';

const eolRegex = /\r\n|\r|\n/g;
const headRegex = /^\(HEAD detached at [0-9A-Za-z]+\)/g;
Expand Down Expand Up @@ -74,7 +74,7 @@ export class DataSource {
if (refData.head === commits[i].hash) {
unsavedChanges = getConfig().showUncommittedChanges() ? await this.getGitUnsavedChanges(repo) : null;
if (unsavedChanges !== null) {
commits.unshift({ hash: '*', parentHashes: [refData.head], author: '*', email: '', date: Math.round((new Date()).getTime() / 1000), message: 'Uncommitted Changes (' + unsavedChanges.changes + ')' });
commits.unshift({ hash: UNCOMMITTED, parentHashes: [refData.head], author: '*', email: '', date: Math.round((new Date()).getTime() / 1000), message: 'Uncommitted Changes (' + unsavedChanges.changes + ')' });
}
break;
}
Expand Down Expand Up @@ -126,26 +126,48 @@ export class DataSource {
});
}
})),
this.getDiffTreeNameStatus(repo, commitHash, commitHash),
this.getDiffTreeNumStat(repo, commitHash, commitHash)
this.getDiffTreeNameStatus(repo, commitHash + '~', commitHash),
this.getDiffTreeNumStat(repo, commitHash + '~', commitHash)
]).then(results => {
results[0].fileChanges = generateFileChanges(results[1], results[2], 1);
results[0].fileChanges = generateFileChanges(results[1], results[2], []);
resolve(results[0]);
}).catch(() => resolve(null));
});
}

public uncommittedDetails(repo: string) {
return new Promise<GitCommitDetails | null>(resolve => {
Promise.all([
this.getDiffTreeNameStatus(repo, 'HEAD', ''),
this.getDiffTreeNumStat(repo, 'HEAD', ''),
this.getUntrackedFiles(repo)
]).then(results => {
resolve({
hash: UNCOMMITTED, parents: [], author: '', email: '', date: 0, committer: '', body: '',
fileChanges: generateFileChanges(results[0], results[1], results[2])
});
}).catch(() => resolve(null));
});
}

public compareCommits(repo: string, fromHash: string, toHash: string) {
return new Promise<GitFileChange[] | null>(resolve => {
Promise.all([
this.getDiffTreeNameStatus(repo, fromHash, toHash),
this.getDiffTreeNumStat(repo, fromHash, toHash)
]).then(results => resolve(generateFileChanges(results[0], results[1], 0))).catch(() => resolve(null));
let promises = [
this.getDiffTreeNameStatus(repo, fromHash, toHash === UNCOMMITTED ? '' : toHash),
this.getDiffTreeNumStat(repo, fromHash, toHash === UNCOMMITTED ? '' : toHash)
];
if (toHash === UNCOMMITTED) promises.push(this.getUntrackedFiles(repo));

Promise.all(promises)
.then(results => resolve(generateFileChanges(results[0], results[1], toHash === UNCOMMITTED ? results[2] : [])))
.catch(() => resolve(null));
});
}

public getCommitFile(repo: string, commitHash: string, filePath: string) {
return this.spawnGit(['show', commitHash + ':' + filePath], repo, stdout => stdout, '');
public getCommitFile(repo: string, commitHash: string, filePath: string, type: GitFileChangeType) {
return commitHash === UNCOMMITTED && type === 'D'
? new Promise<string>(resolve => resolve(''))
: this.spawnGit(['show', commitHash + ':' + filePath], repo, stdout => stdout, '');
}

public async getRemoteUrl(repo: string) {
Expand Down Expand Up @@ -231,6 +253,10 @@ export class DataSource {
return this.runGitCommand('cherry-pick ' + commitHash + (parentIndex > 0 ? ' -m ' + parentIndex : ''), repo);
}

public cleanUntrackedFiles(repo: string, directories: boolean) {
return this.runGitCommand('clean -f' + (directories ? 'd' : ''), repo);
}

public revertCommit(repo: string, commitHash: string, parentIndex: number) {
return this.runGitCommand('revert --no-edit ' + commitHash + (parentIndex > 0 ? ' -m ' + parentIndex : ''), repo);
}
Expand Down Expand Up @@ -302,14 +328,29 @@ export class DataSource {
});
}

private getUntrackedFiles(repo: string) {
return new Promise<string[]>(resolve => {
this.execGit('-c core.quotepath=false status -s --untracked-files --porcelain', repo, (err, stdout) => {
let files = [];
if (!err) {
let lines = stdout.split(eolRegex);
for (let i = 0; i < lines.length; i++) {
if (lines[i].startsWith('??')) files.push(lines[i].substr(3));
}
}
resolve(files);
});
});
}

private getDiffTreeNameStatus(repo: string, fromHash: string, toHash: string) {
return new Promise<string[]>((resolve, reject) => this.execGit('-c core.quotepath=false diff-tree --name-status -r -m --root --find-renames --diff-filter=AMDR ' + fromHash + (fromHash !== toHash ? ' ' + toHash : ''), repo, (err, stdout) => {
return new Promise<string[]>((resolve, reject) => this.execGit('-c core.quotepath=false diff --name-status -r -m --root --find-renames --diff-filter=AMDR ' + fromHash + (toHash !== '' ? ' ' + toHash : ''), repo, (err, stdout) => {
if (err) reject(); else resolve(stdout.split(eolRegex));
}));
}

private getDiffTreeNumStat(repo: string, fromHash: string, toHash: string) {
return new Promise<string[]>((resolve, reject) => this.execGit('-c core.quotepath=false diff-tree --numstat -r -m --root --find-renames --diff-filter=AMDR ' + fromHash + (fromHash !== toHash ? ' ' + toHash : ''), repo, (err, stdout) => {
return new Promise<string[]>((resolve, reject) => this.execGit('-c core.quotepath=false diff --numstat -r -m --root --find-renames --diff-filter=AMDR ' + fromHash + (toHash !== '' ? ' ' + toHash : ''), repo, (err, stdout) => {
if (err) reject(); else resolve(stdout.split(eolRegex));
}));
}
Expand Down Expand Up @@ -387,20 +428,24 @@ function escapeRefName(str: string) {
}

// Generates a list of file changes from each diff-tree output
function generateFileChanges(nameStatusResults: string[], numStatResults: string[], startAt: number) {
let fileChanges: GitFileChange[] = [], fileLookup: { [file: string]: number } = {};
function generateFileChanges(nameStatusResults: string[], numStatResults: string[], unstagedFiles: string[]) {
let fileChanges: GitFileChange[] = [], fileLookup: { [file: string]: number } = {}, i = 0;

for (let i = startAt; i < nameStatusResults.length - 1; i++) {
for (i = 0; i < nameStatusResults.length - 1; i++) {
let line = nameStatusResults[i].split('\t');
if (line.length < 2) break;
if (line.length < 2) continue;
let oldFilePath = getPathFromStr(line[1]), newFilePath = getPathFromStr(line[line.length - 1]);
fileLookup[newFilePath] = fileChanges.length;
fileChanges.push({ oldFilePath: oldFilePath, newFilePath: newFilePath, type: <GitFileChangeType>line[0][0], additions: null, deletions: null });
}

for (let i = startAt; i < numStatResults.length - 1; i++) {
for (i = 0; i < unstagedFiles.length; i++) {
fileChanges.push({ oldFilePath: unstagedFiles[i], newFilePath: unstagedFiles[i], type: 'U', additions: null, deletions: null });
}

for (i = 0; i < numStatResults.length - 1; i++) {
let line = numStatResults[i].split('\t');
if (line.length !== 3) break;
if (line.length !== 3) continue;
let fileName = line[2].replace(/(.*){.* => (.*)}/, '$1$2').replace(/.* => (.*)/, '$1');
if (typeof fileLookup[fileName] === 'number') {
fileChanges[fileLookup[fileName]].additions = parseInt(line[0]);
Expand Down
13 changes: 8 additions & 5 deletions src/diffDocProvider.ts
@@ -1,6 +1,7 @@
import * as vscode from 'vscode';
import { DataSource } from './dataSource';
import { getPathFromStr } from './utils';
import { GitFileChangeType } from './types';
import { getPathFromStr, UNCOMMITTED } from './utils';

export class DiffDocProvider implements vscode.TextDocumentContentProvider {
public static scheme = 'git-graph';
Expand Down Expand Up @@ -29,7 +30,7 @@ export class DiffDocProvider implements vscode.TextDocumentContentProvider {
if (document) return document.value;

let request = decodeDiffDocUri(uri);
return this.dataSource.getCommitFile(request.repo, request.commit, request.filePath).then((data) => {
return this.dataSource.getCommitFile(request.repo, request.commit, request.filePath, request.type).then((data) => {
let document = new DiffDocument(data);
this.docs.set(uri.toString(), document);
return document.value;
Expand All @@ -49,13 +50,15 @@ class DiffDocument {
}
}

export function encodeDiffDocUri(repo: string, path: string, commit: string): vscode.Uri {
return vscode.Uri.parse(DiffDocProvider.scheme + ':' + getPathFromStr(path) + '?commit=' + encodeURIComponent(commit) + '&repo=' + encodeURIComponent(repo));
export function encodeDiffDocUri(repo: string, path: string, commit: string, type: GitFileChangeType): vscode.Uri {
return commit === UNCOMMITTED && type !== 'D'
? vscode.Uri.file(repo + '/' + path)
: vscode.Uri.parse(DiffDocProvider.scheme + ':' + getPathFromStr(path) + '?commit=' + encodeURIComponent(commit) + '&type=' + type + '&repo=' + encodeURIComponent(repo));
}

export function decodeDiffDocUri(uri: vscode.Uri) {
let queryArgs = decodeUriQueryArgs(uri.query);
return { filePath: uri.path, commit: queryArgs.commit, repo: queryArgs.repo };
return { filePath: uri.path, commit: queryArgs.commit, type: <GitFileChangeType>queryArgs.type, repo: queryArgs.repo };
}

function decodeUriQueryArgs(query: string) {
Expand Down
34 changes: 16 additions & 18 deletions src/gitGraphView.ts
Expand Up @@ -3,12 +3,11 @@ import * as vscode from 'vscode';
import { AvatarManager } from './avatarManager';
import { getConfig } from './config';
import { DataSource } from './dataSource';
import { encodeDiffDocUri } from './diffDocProvider';
import { ExtensionState } from './extensionState';
import { RepoFileWatcher } from './repoFileWatcher';
import { RepoManager } from './repoManager';
import { GitFileChangeType, GitGraphViewState, GitRepoSet, RequestMessage, ResponseMessage } from './types';
import { abbrevCommit, copyToClipboard } from './utils';
import { GitGraphViewState, GitRepoSet, RequestMessage, ResponseMessage } from './types';
import { copyToClipboard, UNCOMMITTED, viewDiff, viewScm } from './utils';

export class GitGraphView {
public static currentPanel: GitGraphView | undefined;
Expand Down Expand Up @@ -125,10 +124,16 @@ export class GitGraphView {
status: await this.dataSource.cherrypickCommit(msg.repo, msg.commitHash, msg.parentIndex)
});
break;
case 'cleanUntrackedFiles':
this.sendMessage({
command: 'cleanUntrackedFiles',
status: await this.dataSource.cleanUntrackedFiles(msg.repo, msg.directories)
});
break;
case 'commitDetails':
this.sendMessage({
command: 'commitDetails',
commitDetails: await this.dataSource.commitDetails(msg.repo, msg.commitHash)
commitDetails: await (msg.commitHash !== UNCOMMITTED ? this.dataSource.commitDetails(msg.repo, msg.commitHash) : this.dataSource.uncommittedDetails(msg.repo))
});
break;
case 'compareCommits':
Expand Down Expand Up @@ -243,7 +248,13 @@ export class GitGraphView {
case 'viewDiff':
this.sendMessage({
command: 'viewDiff',
success: await this.viewDiff(msg.repo, msg.fromHash, msg.toHash, msg.oldFilePath, msg.newFilePath, msg.type)
success: await viewDiff(msg.repo, msg.fromHash, msg.toHash, msg.oldFilePath, msg.newFilePath, msg.type)
});
break;
case 'viewScm':
this.sendMessage({
command: 'viewScm',
success: await viewScm()
});
break;
}
Expand Down Expand Up @@ -359,19 +370,6 @@ export class GitGraphView {
loadRepo: loadRepo
});
}

private viewDiff(repo: string, fromHash: string, toHash: string, oldFilePath: string, newFilePath: string, type: GitFileChangeType) {
let abbrevFromHash = abbrevCommit(fromHash), abbrevToHash = abbrevCommit(toHash), pathComponents = newFilePath.split('/');
let desc = fromHash === toHash
? (type === 'A' ? 'Added in ' + abbrevToHash : type === 'D' ? 'Deleted in ' + abbrevToHash : abbrevFromHash + '^ ↔ ' + abbrevToHash)
: (type === 'A' ? 'Added between ' + abbrevFromHash + ' & ' + abbrevToHash : type === 'D' ? 'Deleted between ' + abbrevFromHash + ' & ' + abbrevToHash : abbrevFromHash + ' ↔ ' + abbrevToHash);
let title = pathComponents[pathComponents.length - 1] + ' (' + desc + ')';
return new Promise<boolean>((resolve) => {
vscode.commands.executeCommand('vscode.diff', encodeDiffDocUri(repo, oldFilePath, fromHash === toHash ? fromHash + '^' : fromHash), encodeDiffDocUri(repo, newFilePath, toHash), title, { preview: true })
.then(() => resolve(true))
.then(() => resolve(false));
});
}
}

function getNonce() {
Expand Down
28 changes: 25 additions & 3 deletions src/types.ts
Expand Up @@ -94,7 +94,7 @@ export type RefLabelAlignment = 'Normal' | 'Branches (on the left) & Tags (on th
export type TabIconColourTheme = 'colour' | 'grey';
export type GitCommandStatus = string | null;
export type GitResetMode = 'soft' | 'mixed' | 'hard';
export type GitFileChangeType = 'A' | 'M' | 'D' | 'R';
export type GitFileChangeType = 'A' | 'M' | 'D' | 'R' | 'U';


/* Request / Response Messages */
Expand Down Expand Up @@ -191,6 +191,16 @@ export interface ResponseCreateBranch {
status: GitCommandStatus;
}

export interface RequestCleanUntrackedFiles {
command: 'cleanUntrackedFiles';
repo: string;
directories: boolean;
}
export interface ResponseCleanUntrackedFiles {
command: 'cleanUntrackedFiles';
status: GitCommandStatus;
}

export interface RequestDeleteBranch {
command: 'deleteBranch';
repo: string;
Expand Down Expand Up @@ -367,11 +377,20 @@ export interface ResponseViewDiff {
success: boolean;
}

export interface RequestViewScm {
command: 'viewScm';
}
export interface ResponseViewScm {
command: 'viewScm';
success: boolean;
}

export type RequestMessage =
RequestAddTag
| RequestCheckoutBranch
| RequestCheckoutCommit
| RequestCherrypickCommit
| RequestCleanUntrackedFiles
| RequestCommitDetails
| RequestCompareCommits
| RequestCopyToClipboard
Expand All @@ -390,13 +409,15 @@ export type RequestMessage =
| RequestResetToCommit
| RequestRevertCommit
| RequestSaveRepoState
| RequestViewDiff;
| RequestViewDiff
| RequestViewScm;

export type ResponseMessage =
ResponseAddTag
| ResponseCheckoutBranch
| ResponseCheckoutCommit
| ResponseCherrypickCommit
| ResponseCleanUntrackedFiles
| ResponseCompareCommits
| ResponseCommitDetails
| ResponseCopyToClipboard
Expand All @@ -415,4 +436,5 @@ export type ResponseMessage =
| ResponseRenameBranch
| ResponseResetToCommit
| ResponseRevertCommit
| ResponseViewDiff;
| ResponseViewDiff
| ResponseViewScm;

0 comments on commit f050acb

Please sign in to comment.