|
5 | 5 |
|
6 | 6 | import TelemetryReporter from '@vscode/extension-telemetry'; |
7 | 7 | import * as fs from 'fs'; |
| 8 | +import * as fsPromises from 'fs/promises'; |
8 | 9 | import * as path from 'path'; |
9 | 10 | import picomatch from 'picomatch'; |
10 | 11 | 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'; |
@@ -1878,10 +1879,132 @@ export class Repository implements Disposable { |
1878 | 1879 | this.globalState.update(`${Repository.WORKTREE_ROOT_STORAGE_KEY}:${this.root}`, newWorktreeRoot); |
1879 | 1880 | } |
1880 | 1881 |
|
| 1882 | + // Copy worktree include files. We explicitly do not await this |
| 1883 | + // since we don't want to block the worktree creation on the |
| 1884 | + // copy operation. |
| 1885 | + this._copyWorktreeIncludeFiles(worktreePath!); |
| 1886 | + |
1881 | 1887 | return worktreePath!; |
1882 | 1888 | }); |
1883 | 1889 | } |
1884 | 1890 |
|
| 1891 | + private async _getWorktreeIncludeFiles(): Promise<Set<string>> { |
| 1892 | + const config = workspace.getConfiguration('git', Uri.file(this.root)); |
| 1893 | + const worktreeIncludeFiles = config.get<string[]>('worktreeIncludeFiles', ['**/node_modules{,/**}']); |
| 1894 | + |
| 1895 | + if (worktreeIncludeFiles.length === 0) { |
| 1896 | + return new Set<string>(); |
| 1897 | + } |
| 1898 | + |
| 1899 | + try { |
| 1900 | + // Expand the glob patterns |
| 1901 | + const matchedFiles = new Set<string>(); |
| 1902 | + for (const pattern of worktreeIncludeFiles) { |
| 1903 | + for await (const file of fsPromises.glob(pattern, { cwd: this.root })) { |
| 1904 | + matchedFiles.add(file); |
| 1905 | + } |
| 1906 | + } |
| 1907 | + |
| 1908 | + // Collect unique directories from all the matched files. Check |
| 1909 | + // first whether directories are ignored in order to limit the |
| 1910 | + // number of git check-ignore calls. |
| 1911 | + const directoriesToCheck = new Set<string>(); |
| 1912 | + for (const file of matchedFiles) { |
| 1913 | + let parent = path.dirname(file); |
| 1914 | + while (parent && parent !== '.') { |
| 1915 | + directoriesToCheck.add(path.join(this.root, parent)); |
| 1916 | + parent = path.dirname(parent); |
| 1917 | + } |
| 1918 | + } |
| 1919 | + |
| 1920 | + const gitIgnoredDirectories = await this.checkIgnore(Array.from(directoriesToCheck)); |
| 1921 | + |
| 1922 | + // Files under a git ignored directory are ignored |
| 1923 | + const gitIgnoredFiles = new Set<string>(); |
| 1924 | + const filesToCheck: string[] = []; |
| 1925 | + |
| 1926 | + for (const file of matchedFiles) { |
| 1927 | + const fullPath = path.join(this.root, file); |
| 1928 | + let parent = path.dirname(fullPath); |
| 1929 | + let isUnderIgnoredDir = false; |
| 1930 | + |
| 1931 | + while (parent !== this.root && parent.length > this.root.length) { |
| 1932 | + if (gitIgnoredDirectories.has(parent)) { |
| 1933 | + isUnderIgnoredDir = true; |
| 1934 | + break; |
| 1935 | + } |
| 1936 | + parent = path.dirname(parent); |
| 1937 | + } |
| 1938 | + |
| 1939 | + if (isUnderIgnoredDir) { |
| 1940 | + gitIgnoredFiles.add(fullPath); |
| 1941 | + } else { |
| 1942 | + filesToCheck.push(fullPath); |
| 1943 | + } |
| 1944 | + } |
| 1945 | + |
| 1946 | + // Check the files that are not under a git ignored directories |
| 1947 | + const filesToCheckResults = await this.checkIgnore(Array.from(filesToCheck)); |
| 1948 | + filesToCheckResults.forEach(ignoredFile => gitIgnoredFiles.add(ignoredFile)); |
| 1949 | + |
| 1950 | + return gitIgnoredFiles; |
| 1951 | + } catch (err) { |
| 1952 | + this.logger.warn(`[Repository][_getWorktreeIncludeFiles] Failed to get worktree include files: ${err}`); |
| 1953 | + return new Set<string>(); |
| 1954 | + } |
| 1955 | + } |
| 1956 | + |
| 1957 | + private async _copyWorktreeIncludeFiles(worktreePath: string): Promise<void> { |
| 1958 | + const ignoredFiles = await this._getWorktreeIncludeFiles(); |
| 1959 | + if (ignoredFiles.size === 0) { |
| 1960 | + return; |
| 1961 | + } |
| 1962 | + |
| 1963 | + try { |
| 1964 | + // Copy files |
| 1965 | + let copiedFiles = 0; |
| 1966 | + const results = await window.withProgress({ |
| 1967 | + location: ProgressLocation.Notification, |
| 1968 | + title: l10n.t('Copying additional files to the worktree'), |
| 1969 | + cancellable: false |
| 1970 | + }, async (progress) => { |
| 1971 | + const limiter = new Limiter<void>(10); |
| 1972 | + const files = Array.from(ignoredFiles); |
| 1973 | + |
| 1974 | + return Promise.allSettled(files.map(sourceFile => |
| 1975 | + limiter.queue(async () => { |
| 1976 | + const targetFile = path.join(worktreePath, relativePath(this.root, sourceFile)); |
| 1977 | + await fsPromises.mkdir(path.dirname(targetFile), { recursive: true }); |
| 1978 | + await fsPromises.cp(sourceFile, targetFile, { |
| 1979 | + force: true, |
| 1980 | + recursive: false, |
| 1981 | + verbatimSymlinks: true |
| 1982 | + }); |
| 1983 | + |
| 1984 | + copiedFiles++; |
| 1985 | + progress.report({ |
| 1986 | + increment: 100 / ignoredFiles.size, |
| 1987 | + message: l10n.t('({0}/{1})', copiedFiles, ignoredFiles.size) |
| 1988 | + }); |
| 1989 | + }) |
| 1990 | + )); |
| 1991 | + }); |
| 1992 | + |
| 1993 | + // When expanding the glob patterns, both directories and files are matched however |
| 1994 | + // directories cannot be copied so we filter out `ERR_FS_EISDIR` errors as those are |
| 1995 | + // expected. |
| 1996 | + const errors = results.filter(r => r.status === 'rejected' && |
| 1997 | + (r.reason as NodeJS.ErrnoException).code !== 'ERR_FS_EISDIR'); |
| 1998 | + |
| 1999 | + if (errors.length > 0) { |
| 2000 | + this.logger.warn(`[Repository][_copyWorktreeIncludeFiles] Failed to copy ${errors.length} files to worktree.`); |
| 2001 | + window.showWarningMessage(l10n.t('Failed to copy {0} files to the worktree.', errors.length)); |
| 2002 | + } |
| 2003 | + } catch (err) { |
| 2004 | + this.logger.warn(`[Repository][_copyWorktreeIncludeFiles] Failed to copy files to worktree: ${err}`); |
| 2005 | + } |
| 2006 | + } |
| 2007 | + |
1885 | 2008 | async deleteWorktree(path: string, options?: { force?: boolean }): Promise<void> { |
1886 | 2009 | await this.run(Operation.Worktree(false), async () => { |
1887 | 2010 | const worktree = this.repositoryResolver.getRepository(path); |
|
0 commit comments