diff --git a/extensions/git/package.json b/extensions/git/package.json index 369d4c95ec9..99ef7bd9f5c 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -1054,6 +1054,61 @@ "command": "git.revealInExplorer", "when": "scmProvider == git && scmResourceGroup == workingTree", "group": "2_view" + }, + { + "command": "git.openChange", + "when": "scmProvider == git && scmResourceGroup == workingTree", + "group": "navigation" + }, + { + "command": "git.openChange", + "when": "scmProvider == git && scmResourceGroup == untracked", + "group": "navigation" + }, + { + "command": "git.openHEADFile", + "when": "scmProvider == git && scmResourceGroup == untracked", + "group": "navigation" + }, + { + "command": "git.openFile", + "when": "scmProvider == git && scmResourceGroup == untracked", + "group": "navigation" + }, + { + "command": "git.stage", + "when": "scmProvider == git && scmResourceGroup == untracked", + "group": "1_modification" + }, + { + "command": "git.clean", + "when": "scmProvider == git && scmResourceGroup == untracked && !gitFreshRepository", + "group": "1_modification" + }, + { + "command": "git.clean", + "when": "scmProvider == git && scmResourceGroup == untracked && !gitFreshRepository", + "group": "inline" + }, + { + "command": "git.stage", + "when": "scmProvider == git && scmResourceGroup == untracked", + "group": "inline" + }, + { + "command": "git.openFile2", + "when": "scmProvider == git && scmResourceGroup == untracked && config.git.showInlineOpenFileAction && config.git.openDiffOnClick", + "group": "inline0" + }, + { + "command": "git.openChange", + "when": "scmProvider == git && scmResourceGroup == untracked && config.git.showInlineOpenFileAction && !config.git.openDiffOnClick", + "group": "inline0" + }, + { + "command": "git.ignore", + "when": "scmProvider == git && scmResourceGroup == untracked", + "group": "1_modification@3" } ], "editor/title": [ @@ -1457,6 +1512,21 @@ ], "default": "committerdate", "description": "%config.branchSortOrder%" + }, + "git.handleUntracked": { + "type": "string", + "enum": [ + "withchanges", + "separate", + "hide" + ], + "enumDescriptions": [ + "%config.handleUntracked.withchanges%", + "%config.handleUntracked.separate%", + "%config.handleUntracked.hide%" + ], + "default": "withchanges", + "description": "%config.handleUntracked%" } } }, diff --git a/extensions/git/package.nls.json b/extensions/git/package.nls.json index 653ebc7c708..d541a0fd9b9 100644 --- a/extensions/git/package.nls.json +++ b/extensions/git/package.nls.json @@ -1,6 +1,6 @@ { - "displayName": "Git", - "description": "Git SCM Integration", + "displayName": "Hide Untracked Files (Git)", + "description": "Copy of the built-in Git extension which hides untracked files", "command.clone": "Clone", "command.init": "Initialize Repository", "command.openRepository": "Open Repository", @@ -131,6 +131,10 @@ "config.openDiffOnClick": "Controls whether the diff editor should be opened when clicking a change. Otherwise the regular editor will be opened.", "config.supportCancellation": "Controls whether a notification comes up when running the Sync action, which allows the user to cancel the operation.", "config.branchSortOrder": "Controls the sort order for branches.", + "config.handleUntracked": "Controls how untracked files are presented in the activity bar.", + "config.handleUntracked.withchanges": "Show with other unstaged changes, commit under \"Commit All\"", + "config.handleUntracked.separate": "Separate in list and badge counter, don't commit under \"Commit All\"", + "config.handleUntracked.hide": "Exclude from list and badge counter, don't commit under \"Commit All\"", "colors.added": "Color for added resources.", "colors.modified": "Color for modified resources.", "colors.deleted": "Color for deleted resources.", diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index 5bc5ab75a7c..4975aa5d372 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -3,19 +3,19 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Uri, commands, Disposable, window, workspace, QuickPickItem, OutputChannel, Range, WorkspaceEdit, Position, LineChange, SourceControlResourceState, TextDocumentShowOptions, ViewColumn, ProgressLocation, TextEditor, MessageOptions, WorkspaceFolder } from 'vscode'; -import { Git, CommitOptions, Stash, ForcePushMode } from './git'; -import { Repository, Resource, ResourceGroupType } from './repository'; -import { Model } from './model'; -import { toGitUri, fromGitUri } from './uri'; -import { grep, isDescendant, pathEquals } from './util'; -import { applyLineChanges, intersectDiffWithRange, toLineRanges, invertLineChange, getModifiedRange } from './staging'; -import * as path from 'path'; import { lstat, Stats } from 'fs'; import * as os from 'os'; +import * as path from 'path'; +import { commands, Disposable, LineChange, MessageOptions, OutputChannel, Position, ProgressLocation, QuickPickItem, Range, SourceControlResourceState, TextDocumentShowOptions, TextEditor, Uri, ViewColumn, window, workspace, WorkspaceEdit, WorkspaceFolder } from 'vscode'; import TelemetryReporter from 'vscode-extension-telemetry'; import * as nls from 'vscode-nls'; -import { Ref, RefType, Branch, GitErrorCodes, Status } from './api/git'; +import { Branch, GitErrorCodes, Ref, RefType, Status } from './api/git'; +import { CommitOptions, ForcePushMode, Git, Stash } from './git'; +import { Model } from './model'; +import { Repository, Resource, ResourceGroupType } from './repository'; +import { applyLineChanges, getModifiedRange, intersectDiffWithRange, invertLineChange, toLineRanges } from './staging'; +import { fromGitUri, toGitUri } from './uri'; +import { grep, isDescendant, pathEquals } from './util'; const localize = nls.loadMessageBundle(); @@ -872,7 +872,8 @@ export class CommandCenter { } const workingTree = selection.filter(s => s.resourceGroupType === ResourceGroupType.WorkingTree); - const scmResources = [...workingTree, ...resolved, ...unresolved]; + const untracked = selection.filter(s => s.resourceGroupType === ResourceGroupType.Untracked); + const scmResources = [...workingTree, ...untracked, ...resolved, ...unresolved]; this.outputChannel.appendLine(`git.stage.scmResources ${scmResources.length}`); if (!scmResources.length) { @@ -913,7 +914,49 @@ export class CommandCenter { } } - await repository.add([]); + const handleUntracked = + workspace + .getConfiguration('git', Uri.file(repository.root)) + .get<'withchanges' | 'separate' | 'hide'>('handleUntracked') || + 'withchanges'; + let includeUntracked; + switch (handleUntracked) { + case 'withchanges': + includeUntracked = true; + break; + case 'separate': + if (repository.untrackedGroup.resourceStates.length > 0) { + const message = localize( + 'also add untracked files', + 'Would you like to also add and stage untracked files?' + ); + + const yes = localize('yes', "Yes"); + const no = localize('no', 'No'); + const pick = await window.showInformationMessage( + message, + { modal: true }, + yes, + no + ); + + if (pick === yes) { + includeUntracked = true; + } else if (pick === no) { + includeUntracked = false; + } else { + return; + } + } else { + // Doesn't matter + includeUntracked = false; + } + break; + case 'hide': + includeUntracked = false; + break; + } + await repository.add([], includeUntracked ? undefined : { update: true }); } private async _stageDeletionConflict(repository: Repository, uri: Uri): Promise { @@ -1137,8 +1180,8 @@ export class CommandCenter { resourceStates = [resource]; } - const scmResources = resourceStates - .filter(s => s instanceof Resource && s.resourceGroupType === ResourceGroupType.WorkingTree) as Resource[]; + const scmResources = resourceStates.filter(s => s instanceof Resource + && (s.resourceGroupType === ResourceGroupType.WorkingTree || s.resourceGroupType === ResourceGroupType.Untracked)) as Resource[]; if (!scmResources.length) { return; @@ -2159,7 +2202,8 @@ export class CommandCenter { } private async _stash(repository: Repository, includeUntracked = false): Promise { - const noUnstagedChanges = repository.workingTreeGroup.resourceStates.length === 0; + const noUnstagedChanges = repository.workingTreeGroup.resourceStates.length === 0 + && (!includeUntracked || repository.untrackedGroup.resourceStates.length === 0); const noStagedChanges = repository.indexGroup.resourceStates.length === 0; if (noUnstagedChanges && noStagedChanges) { diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index 82ca6fe0fd4..5e181ad309c 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -3,17 +3,17 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Uri, Command, EventEmitter, Event, scm, SourceControl, SourceControlInputBox, SourceControlResourceGroup, SourceControlResourceState, SourceControlResourceDecorations, SourceControlInputBoxValidation, Disposable, ProgressLocation, window, workspace, WorkspaceEdit, ThemeColor, Decoration, Memento, SourceControlInputBoxValidationType, OutputChannel, LogLevel, env, ProgressOptions, CancellationToken } from 'vscode'; -import { Repository as BaseRepository, Commit, Stash, GitError, Submodule, CommitOptions, ForcePushMode } from './git'; -import { anyEvent, filterEvent, eventToPromise, dispose, find, isDescendant, IDisposable, onceEvent, EmptyDisposable, debounceEvent, combinedDisposable } from './util'; -import { memoize, throttle, debounce } from './decorators'; -import { toGitUri } from './uri'; -import { AutoFetcher } from './autofetch'; -import * as path from 'path'; -import * as nls from 'vscode-nls'; import * as fs from 'fs'; +import * as path from 'path'; +import { CancellationToken, Command, Disposable, env, Event, EventEmitter, LogLevel, Memento, OutputChannel, ProgressLocation, ProgressOptions, scm, SourceControl, SourceControlInputBox, SourceControlInputBoxValidation, SourceControlInputBoxValidationType, SourceControlResourceDecorations, SourceControlResourceGroup, SourceControlResourceState, ThemeColor, Uri, window, workspace, WorkspaceEdit, Decoration } from 'vscode'; +import * 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 { StatusBarCommands } from './statusbar'; -import { Branch, Ref, Remote, RefType, GitErrorCodes, Status, LogOptions, Change } from './api/git'; +import { toGitUri } from './uri'; +import { anyEvent, combinedDisposable, debounceEvent, dispose, EmptyDisposable, eventToPromise, filterEvent, find, IDisposable, isDescendant, onceEvent } from './util'; import { IFileWatcher, watch } from './watch'; const timeout = (millis: number) => new Promise(c => setTimeout(c, millis)); @@ -33,7 +33,8 @@ export const enum RepositoryState { export const enum ResourceGroupType { Merge, Index, - WorkingTree + WorkingTree, + Untracked } export class Resource implements SourceControlResourceState { @@ -570,6 +571,9 @@ export class Repository implements Disposable { private _workingTreeGroup: SourceControlResourceGroup; get workingTreeGroup(): GitResourceGroup { return this._workingTreeGroup as GitResourceGroup; } + private _untrackedGroup: SourceControlResourceGroup; + get untrackedGroup(): GitResourceGroup { return this._untrackedGroup as GitResourceGroup; } + private _HEAD: Branch | undefined; get HEAD(): Branch | undefined { return this._HEAD; @@ -642,6 +646,7 @@ export class Repository implements Disposable { this.mergeGroup.resourceStates = []; this.indexGroup.resourceStates = []; this.workingTreeGroup.resourceStates = []; + this.untrackedGroup.resourceStates = []; this._sourceControl.count = 0; } @@ -709,6 +714,7 @@ export class Repository implements Disposable { this._mergeGroup = this._sourceControl.createResourceGroup('merge', localize('merge changes', "MERGE CHANGES")); this._indexGroup = this._sourceControl.createResourceGroup('index', localize('staged changes', "STAGED CHANGES")); this._workingTreeGroup = this._sourceControl.createResourceGroup('workingTree', localize('changes', "CHANGES")); + this._untrackedGroup = this._sourceControl.createResourceGroup('untracked', localize('untracked changes', 'UNTRACKED')); const updateIndexGroupVisibility = () => { const config = workspace.getConfiguration('git', root); @@ -722,11 +728,16 @@ export class Repository implements Disposable { const onConfigListenerForBranchSortOrder = filterEvent(workspace.onDidChangeConfiguration, e => e.affectsConfiguration('git.branchSortOrder', root)); onConfigListenerForBranchSortOrder(this.updateModelState, this, this.disposables); + const onConfigListenerForUntracked = filterEvent(workspace.onDidChangeConfiguration, e => e.affectsConfiguration('git.handleUntracked', root)); + onConfigListenerForUntracked(this.updateModelState, this, this.disposables); + this.mergeGroup.hideWhenEmpty = true; + this.untrackedGroup.hideWhenEmpty = true; this.disposables.push(this.mergeGroup); this.disposables.push(this.indexGroup); this.disposables.push(this.workingTreeGroup); + this.disposables.push(this.untrackedGroup); this.disposables.push(new AutoFetcher(this, globalState)); @@ -912,8 +923,8 @@ export class Repository implements Disposable { return this.run(Operation.HashObject, () => this.repository.hashObject(data)); } - async add(resources: Uri[]): Promise { - await this.run(Operation.Add, () => this.repository.add(resources.map(r => r.fsPath))); + async add(resources: Uri[], opts?: { update?: boolean }): Promise { + await this.run(Operation.Add, () => this.repository.add(resources.map(r => r.fsPath), opts)); } async rm(resources: Uri[]): Promise { @@ -1496,16 +1507,44 @@ export class Repository implements Disposable { this._submodules = submodules!; this.rebaseCommit = rebaseCommit; + const handleUntracked = + config.get<'withchanges' | 'separate' | 'hide'>('handleUntracked') || + 'withchanges'; const index: Resource[] = []; const workingTree: Resource[] = []; const merge: Resource[] = []; + const untracked: Resource[] = []; status.forEach(raw => { const uri = Uri.file(path.join(this.repository.root, raw.path)); - const renameUri = raw.rename ? Uri.file(path.join(this.repository.root, raw.rename)) : undefined; + const renameUri = raw.rename + ? Uri.file(path.join(this.repository.root, raw.rename)) + : undefined; switch (raw.x + raw.y) { - case '??': return workingTree.push(new Resource(ResourceGroupType.WorkingTree, uri, Status.UNTRACKED, useIcons)); + case '??': + switch (handleUntracked) { + case 'withchanges': + return workingTree.push( + new Resource( + ResourceGroupType.WorkingTree, + uri, + Status.UNTRACKED, + useIcons + ) + ); + case 'separate': + return untracked.push( + new Resource( + ResourceGroupType.Untracked, + uri, + Status.UNTRACKED, + useIcons + ) + ); + case 'hide': + return undefined; + } case '!!': return workingTree.push(new Resource(ResourceGroupType.WorkingTree, uri, Status.IGNORED, useIcons)); case 'DD': return merge.push(new Resource(ResourceGroupType.Merge, uri, Status.BOTH_DELETED, useIcons)); case 'AU': return merge.push(new Resource(ResourceGroupType.Merge, uri, Status.ADDED_BY_US, useIcons)); @@ -1536,6 +1575,7 @@ export class Repository implements Disposable { this.mergeGroup.resourceStates = merge; this.indexGroup.resourceStates = index; this.workingTreeGroup.resourceStates = workingTree; + this.untrackedGroup.resourceStates = untracked; // set count badge this.setCountBadge(); @@ -1546,12 +1586,28 @@ export class Repository implements Disposable { } private setCountBadge(): void { - const countBadge = workspace.getConfiguration('git').get('countBadge'); - let count = this.mergeGroup.resourceStates.length + this.indexGroup.resourceStates.length + this.workingTreeGroup.resourceStates.length; + const config = workspace.getConfiguration('git'); + const countBadge = config.get('countBadge'); + const handleUntracked = + config.get<'withchanges' | 'separate' | 'hide'>('handleUntracked') || + 'withchanges'; + let count = + this.mergeGroup.resourceStates.length + + this.indexGroup.resourceStates.length + + this.workingTreeGroup.resourceStates.length; switch (countBadge) { case 'off': count = 0; break; - case 'tracked': count = count - this.workingTreeGroup.resourceStates.filter(r => r.type === Status.UNTRACKED || r.type === Status.IGNORED).length; break; + case 'tracked': + if (handleUntracked === 'withchanges') { + count -= this.workingTreeGroup.resourceStates.filter(r => r.type === Status.UNTRACKED || r.type === Status.IGNORED).length; + } + break; + case 'all': + if (handleUntracked === 'separate') { + count += this.untrackedGroup.resourceStates.length; + } + break; } this._sourceControl.count = count; @@ -1653,7 +1709,7 @@ export class Repository implements Disposable { const head = HEAD.name || tagName || (HEAD.commit || '').substr(0, 8); return head - + (this.workingTreeGroup.resourceStates.length > 0 ? '*' : '') + + (this.workingTreeGroup.resourceStates.length + this.untrackedGroup.resourceStates.length > 0 ? '*' : '') + (this.indexGroup.resourceStates.length > 0 ? '+' : '') + (this.mergeGroup.resourceStates.length > 0 ? '!' : ''); }