Browse Source

Fixes change detection if worktree is outside .git

main
Eric Amodio 1 year ago
parent
commit
6d5b91afb8
5 changed files with 139 additions and 52 deletions
  1. +28
    -4
      src/env/node/git/git.ts
  2. +28
    -12
      src/env/node/git/localGitProvider.ts
  3. +6
    -0
      src/git/gitProvider.ts
  4. +9
    -2
      src/git/gitProviderService.ts
  5. +68
    -34
      src/git/models/repository.ts

+ 28
- 4
src/env/node/git/git.ts View File

@ -1474,11 +1474,35 @@ export class Git {
} }
} }
async rev_parse__git_dir(cwd: string): Promise<string | undefined> {
const data = await this.git<string>({ cwd: cwd, errors: GitErrorHandling.Ignore }, 'rev-parse', '--git-dir');
// Make sure to normalize: https://github.com/git-for-windows/git/issues/2478
async rev_parse__git_dir(cwd: string): Promise<{ path: string; commonPath?: string } | undefined> {
const data = await this.git<string>(
{ cwd: cwd, errors: GitErrorHandling.Ignore },
'rev-parse',
'--git-dir',
'--git-common-dir',
);
if (data.length === 0) return undefined;
// Keep trailing spaces which are part of the directory name // Keep trailing spaces which are part of the directory name
return data.length === 0 ? undefined : normalizePath(data.trimLeft().replace(/[\r|\n]+$/, ''));
let [dotGitPath, commonDotGitPath] = data.split('\n').map(r => r.trimStart());
// Make sure to normalize: https://github.com/git-for-windows/git/issues/2478
if (!isAbsolute(dotGitPath)) {
dotGitPath = joinPaths(cwd, dotGitPath);
}
dotGitPath = normalizePath(dotGitPath);
if (commonDotGitPath) {
if (!isAbsolute(commonDotGitPath)) {
commonDotGitPath = joinPaths(cwd, commonDotGitPath);
}
commonDotGitPath = normalizePath(commonDotGitPath);
return { path: dotGitPath, commonPath: commonDotGitPath !== dotGitPath ? commonDotGitPath : undefined };
}
return { path: dotGitPath };
} }
async rev_parse__show_toplevel(cwd: string): Promise<string | undefined> { async rev_parse__show_toplevel(cwd: string): Promise<string | undefined> {

+ 28
- 12
src/env/node/git/localGitProvider.ts View File

@ -29,6 +29,7 @@ import {
WorktreeDeleteErrorReason, WorktreeDeleteErrorReason,
} from '../../../git/errors'; } from '../../../git/errors';
import type { import type {
GitDir,
GitProvider, GitProvider,
GitProviderDescriptor, GitProviderDescriptor,
NextComparisonUrisResult, NextComparisonUrisResult,
@ -184,7 +185,7 @@ const stashSummaryRegex =
const reflogCommands = ['merge', 'pull']; const reflogCommands = ['merge', 'pull'];
interface RepositoryInfo { interface RepositoryInfo {
gitDir?: string;
gitDir?: GitDir;
user?: GitUser | null; user?: GitUser | null;
} }
@ -2494,10 +2495,35 @@ export class LocalGitProvider implements GitProvider, Disposable {
} }
@debug() @debug()
async getGitDir(repoPath: string): Promise<GitDir> {
const repo = this._repoInfoCache.get(repoPath);
if (repo?.gitDir != null) return repo.gitDir;
const gitDirPaths = await this.git.rev_parse__git_dir(repoPath);
let gitDir: GitDir;
if (gitDirPaths != null) {
gitDir = {
uri: Uri.file(gitDirPaths.path),
commonUri: gitDirPaths.commonPath != null ? Uri.file(gitDirPaths.commonPath) : undefined,
};
} else {
gitDir = {
uri: this.getAbsoluteUri('.git', repoPath),
};
}
this._repoInfoCache.set(repoPath, { ...repo, gitDir: gitDir });
return gitDir;
}
@debug()
async getLastFetchedTimestamp(repoPath: string): Promise<number | undefined> { async getLastFetchedTimestamp(repoPath: string): Promise<number | undefined> {
try { try {
const gitDir = await this.getGitDir(repoPath); const gitDir = await this.getGitDir(repoPath);
const stats = await workspace.fs.stat(this.container.git.getAbsoluteUri(`${gitDir}/FETCH_HEAD`, repoPath));
const stats = await workspace.fs.stat(
this.container.git.getAbsoluteUri(Uri.joinPath(gitDir.commonUri ?? gitDir.uri, 'FETCH_HEAD'), repoPath),
);
// If the file is empty, assume the fetch failed, and don't update the timestamp // If the file is empty, assume the fetch failed, and don't update the timestamp
if (stats.size > 0) return stats.mtime; if (stats.size > 0) return stats.mtime;
} catch {} } catch {}
@ -2505,16 +2531,6 @@ export class LocalGitProvider implements GitProvider, Disposable {
return undefined; return undefined;
} }
private async getGitDir(repoPath: string): Promise<string> {
const repo = this._repoInfoCache.get(repoPath);
if (repo?.gitDir != null) return repo.gitDir;
const gitDir = normalizePath((await this.git.rev_parse__git_dir(repoPath)) || '.git');
this._repoInfoCache.set(repoPath, { ...repo, gitDir: gitDir });
return gitDir;
}
@log() @log()
async getLog( async getLog(
repoPath: string, repoPath: string,

+ 6
- 0
src/git/gitProvider.ts View File

@ -28,6 +28,11 @@ import type { RemoteProviders } from './remotes/remoteProviders';
import type { RichRemoteProvider } from './remotes/richRemoteProvider'; import type { RichRemoteProvider } from './remotes/richRemoteProvider';
import type { GitSearch, SearchQuery } from './search'; import type { GitSearch, SearchQuery } from './search';
export interface GitDir {
readonly uri: Uri;
readonly commonUri?: Uri;
}
export const enum GitProviderId { export const enum GitProviderId {
Git = 'git', Git = 'git',
GitHub = 'github', GitHub = 'github',
@ -270,6 +275,7 @@ export interface GitProvider extends Disposable {
options?: { filters?: GitDiffFilter[] | undefined; similarityThreshold?: number | undefined }, options?: { filters?: GitDiffFilter[] | undefined; similarityThreshold?: number | undefined },
): Promise<GitFile[] | undefined>; ): Promise<GitFile[] | undefined>;
getFileStatusForCommit(repoPath: string, uri: Uri, ref: string): Promise<GitFile | undefined>; getFileStatusForCommit(repoPath: string, uri: Uri, ref: string): Promise<GitFile | undefined>;
getGitDir?(repoPath: string): Promise<GitDir | undefined>;
getLastFetchedTimestamp(repoPath: string): Promise<number | undefined>; getLastFetchedTimestamp(repoPath: string): Promise<number | undefined>;
getLog( getLog(
repoPath: string, repoPath: string,

+ 9
- 2
src/git/gitProviderService.ts View File

@ -32,6 +32,7 @@ import { getBestPath, getScheme, isAbsolute, maybeUri, normalizePath } from '../
import { cancellable, fastestSettled, getSettledValue, isPromise, PromiseCancelledError } from '../system/promise'; import { cancellable, fastestSettled, getSettledValue, isPromise, PromiseCancelledError } from '../system/promise';
import { VisitedPathsTrie } from '../system/trie'; import { VisitedPathsTrie } from '../system/trie';
import type { import type {
GitDir,
GitProvider, GitProvider,
GitProviderDescriptor, GitProviderDescriptor,
GitProviderId, GitProviderId,
@ -278,7 +279,7 @@ export class GitProviderService implements Disposable {
for (const folder of e.removed) { for (const folder of e.removed) {
const repository = this._repositories.getClosest(folder.uri); const repository = this._repositories.getClosest(folder.uri);
if (repository != null) { if (repository != null) {
this._repositories.remove(repository.uri);
this._repositories.remove(repository.uri, false);
removed.push(repository); removed.push(repository);
} }
} }
@ -427,7 +428,7 @@ export class GitProviderService implements Disposable {
for (const repository of [...this._repositories.values()]) { for (const repository of [...this._repositories.values()]) {
if (repository?.provider.id === id) { if (repository?.provider.id === id) {
this._repositories.remove(repository.uri);
this._repositories.remove(repository.uri, false);
removed.push(repository); removed.push(repository);
} }
} }
@ -1513,6 +1514,12 @@ export class GitProviderService implements Disposable {
} }
@debug() @debug()
getGitDir(repoPath: string): Promise<GitDir | undefined> {
const { provider, path } = this.getProvider(repoPath);
return Promise.resolve(provider.getGitDir?.(path));
}
@debug()
getLastFetchedTimestamp(repoPath: string | Uri): Promise<number | undefined> { getLastFetchedTimestamp(repoPath: string | Uri): Promise<number | undefined> {
const { provider, path } = this.getProvider(repoPath); const { provider, path } = this.getProvider(repoPath);
return provider.getLastFetchedTimestamp(path); return provider.getLastFetchedTimestamp(path);

+ 68
- 34
src/git/models/repository.ts View File

@ -200,7 +200,6 @@ export class Repository implements Disposable {
private _providers: RemoteProviders | undefined; private _providers: RemoteProviders | undefined;
private _remotes: Promise<GitRemote[]> | undefined; private _remotes: Promise<GitRemote[]> | undefined;
private _remotesDisposable: Disposable | undefined; private _remotesDisposable: Disposable | undefined;
private _repoWatcherDisposable: Disposable | undefined;
private _suspended: boolean; private _suspended: boolean;
constructor( constructor(
@ -239,38 +238,68 @@ export class Repository implements Disposable {
this._suspended = suspended; this._suspended = suspended;
this._closed = closed; this._closed = closed;
const watcher = workspace.createFileSystemWatcher(
new RelativePattern(
this.uri,
'{\
**/.git/config,\
**/.git/index,\
**/.git/HEAD,\
**/.git/*_HEAD,\
**/.git/MERGE_*,\
**/.git/refs/**,\
**/.git/rebase-merge/**,\
**/.git/sequencer/**,\
**/.git/worktrees/**,\
**/.gitignore\
}',
),
);
this._disposable = Disposable.from( this._disposable = Disposable.from(
watcher,
watcher.onDidChange(this.onRepositoryChanged, this),
watcher.onDidCreate(this.onRepositoryChanged, this),
watcher.onDidDelete(this.onRepositoryChanged, this),
this.setupRepoWatchers(),
configuration.onDidChange(this.onConfigurationChanged, this), configuration.onDidChange(this.onConfigurationChanged, this),
); );
this.onConfigurationChanged(); this.onConfigurationChanged();
} }
private setupRepoWatchers() {
let disposable: Disposable | undefined;
void this.setupRepoWatchersCore().then(d => (disposable = d));
return {
dispose: () => void disposable?.dispose(),
};
}
private async setupRepoWatchersCore() {
const disposables: Disposable[] = [];
const watcher = workspace.createFileSystemWatcher(new RelativePattern(this.uri, '**/.gitignore'));
disposables.push(
watcher,
watcher.onDidChange(this.onGitIgnoreChanged, this),
watcher.onDidCreate(this.onGitIgnoreChanged, this),
watcher.onDidDelete(this.onGitIgnoreChanged, this),
);
function watch(this: Repository, uri: Uri, pattern: string) {
const watcher = workspace.createFileSystemWatcher(new RelativePattern(uri, pattern));
disposables.push(
watcher,
watcher.onDidChange(e => this.onRepositoryChanged(e, uri)),
watcher.onDidCreate(e => this.onRepositoryChanged(e, uri)),
watcher.onDidDelete(e => this.onRepositoryChanged(e, uri)),
);
return watcher;
}
const gitDir = await this.container.git.getGitDir(this.path);
if (gitDir != null) {
if (gitDir?.commonUri == null) {
watch.call(
this,
gitDir.uri,
'{index,HEAD,*_HEAD,MERGE_*,config,refs/**,rebase-merge/**,sequencer/**,worktrees/**}',
);
} else {
watch.call(this, gitDir.uri, '{index,HEAD,*_HEAD,MERGE_*,rebase-merge/**,sequencer/**}');
watch.call(this, gitDir.commonUri, '{config,refs/**,worktrees/**}');
}
}
return Disposable.from(...disposables);
}
dispose() { dispose() {
this.stopWatchingFileSystem(); this.stopWatchingFileSystem();
this._remotesDisposable?.dispose(); this._remotesDisposable?.dispose();
this._repoWatcherDisposable?.dispose();
this._disposable.dispose(); this._disposable.dispose();
} }
@ -311,24 +340,29 @@ export class Repository implements Disposable {
} }
@debug() @debug()
private onRepositoryChanged(uri: Uri | undefined) {
private onGitIgnoreChanged(_uri: Uri) {
this.fireChange(RepositoryChange.Ignores);
}
@debug()
private onRepositoryChanged(uri: Uri | undefined, base: Uri) {
// TODO@eamodio Revisit -- as I can't seem to get this to work as a negative glob pattern match when creating the watcher
if (uri?.path.includes('/fsmonitor--daemon/')) {
return;
}
this._lastFetched = undefined; this._lastFetched = undefined;
const match = const match =
uri != null uri != null
? /(?<ignore>\/\.gitignore)|\.git\/(?<type>config|index|HEAD|FETCH_HEAD|ORIG_HEAD|CHERRY_PICK_HEAD|MERGE_HEAD|REBASE_HEAD|rebase-merge|refs\/(?:heads|remotes|stash|tags)|worktrees)/.exec(
uri.path,
? // Move worktrees first, since if it is in a worktree it isn't affecting this repo directly
/(worktrees|index|HEAD|FETCH_HEAD|ORIG_HEAD|CHERRY_PICK_HEAD|MERGE_HEAD|REBASE_HEAD|rebase-merge|config|refs\/(?:heads|remotes|stash|tags))/.exec(
this.container.git.getRelativePath(uri, base),
) )
: undefined; : undefined;
if (match?.groups != null) {
const { ignore, type } = match.groups;
if (ignore) {
this.fireChange(RepositoryChange.Ignores);
return;
}
switch (type) {
if (match != null) {
switch (match[1]) {
case 'config': case 'config':
this.resetCaches(); this.resetCaches();
this.fireChange(RepositoryChange.Config, RepositoryChange.Remotes); this.fireChange(RepositoryChange.Config, RepositoryChange.Remotes);

Loading…
Cancel
Save