git-copy-file-folder 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 hh lohmann <hh.lohmann@gmail.com>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,130 @@
1
+ ###### npm-package
2
+
3
+ # Copy file / folder from Git repo without cloning
4
+
5
+ Copy a file or folder from a Git repo instead of cloning, i.e. without a connection to the source
6
+
7
+ Changes in the original repo will not affect the copy of the file / folder and vice versa.
8
+
9
+ Helpful to reuse specific contents of another repo in a current one without mixing both repos.
10
+
11
+ *[hh lohmann &lt;hh.lohmann@gmail.com&gt;](mailto:hh.lohmann@gmail.com?subject=git-copy-file-folder)*
12
+
13
+ <!-- see https://hh-lohmann.github.io/github-readme-pages-switch -->
14
+ <p align="center" id="github_readme_pages_switch" style="display:none;">
15
+ <b><i>This page may be displayed more optimal in its
16
+ <a href="https://hh-lohmann.github.io/git-copy-file-folder">GitHub Pages view</a>
17
+ </i></b>
18
+ </p>
19
+
20
+
21
+ ## Caution
22
+
23
+ * Does not check if a file / folder with the same name already exists in your target - this is up to you, especially in case you explicitly want to overwrite / reset existing files (cf. [Details](#details))
24
+
25
+
26
+ ## Synopsis
27
+
28
+ ```js
29
+ import { gitCopyFileFolder } from 'git-copy-file-folder'
30
+
31
+ gitCopyFileFolder( sourceRepo, fileOrFolder)
32
+
33
+ gitCopyFileFolder( sourceRepo, fileOrFolder, targetPath )
34
+ ```
35
+
36
+
37
+ ## Parameters
38
+
39
+ ### sourceRepo
40
+ URL of the repo from which **[fileOrFolder](#fileorfolder)** should be copied
41
+
42
+ ### fileOrFolder
43
+ Name / path for the file / folder to be copied from the **[sourceRepo](#sourcerepo)**
44
+ * Interpreted relative to the source repo's root, i.e. `src/index.js` of repo `x` would be `x/src/index.js`
45
+
46
+ ### targetPath
47
+ Optional: Existing path to which **[fileOrFolder](#fileorfolder)** should be copied to
48
+ * Default: current folder
49
+
50
+
51
+ ## Returns
52
+
53
+ * `true` on success, `false` else
54
+
55
+
56
+ ## Examples
57
+
58
+ ```js
59
+ gitCopyFileFolder(
60
+ 'https://github.com/acmecorp/solve-all-problems',
61
+ 'secretsolutions'
62
+ )
63
+
64
+ gitCopyFileFolder(
65
+ 'https://github.com/acmecorp/solve-all-problems',
66
+ 'secretsolutions/solution-42.js',
67
+ 'ripped-stuff/acme/'
68
+ )
69
+ ```
70
+
71
+
72
+ ## Installation
73
+
74
+ Pick for your preferred package manager:
75
+
76
+ ```shell
77
+ npm i git-copy-file-folder
78
+ ```
79
+
80
+ ```shell
81
+ pnpm i git-copy-file-folder
82
+ ```
83
+
84
+ ```shell
85
+ bun i git-copy-file-folder
86
+ ```
87
+
88
+ ```shell
89
+ # For Yarn you should double check docs for your and / or
90
+ # current Yarn version, newer versions do not treat `i package_name`
91
+ # as an alias for `add ...` and exclude global installations
92
+ yarn add git-copy-file-folder
93
+ ```
94
+
95
+
96
+ ## Details
97
+
98
+ * Only one file / folder per call (multiple files / folders or globbing goes beyond the current time budget for this project)
99
+
100
+ * Does not check if a file / folder with the same name already exists in your target before probably overwriting it, partly for simplicity, partly to give surrounding code full control about e.g. deciding if overwriting an old version with a newer one or a diverged / corrupted version with the original one may be explicitly intended.
101
+
102
+ * Copying takes place via a temporary [sparse clone](#git-clone-sparse) and a [sparse-checkout](#git-sparse-checkout) there (deleted after copying the file / folder requested)
103
+
104
+
105
+ ## Tests
106
+
107
+ * Code tests to be run with Node.js / Bun available in [Source Code](#source-code)
108
+
109
+
110
+ ## Source Code
111
+
112
+ * GitHub: <https://github.com/hh-lohmann/git-copy-file-folder>
113
+
114
+
115
+ ## License
116
+
117
+ * See LICENSE file included here and in [Source Code](#source-code)
118
+
119
+
120
+ ## References
121
+
122
+ ### Git: clone sparse
123
+ * <https://git-scm.com/docs/git-clone#Documentation/git-clone.txt---sparse>
124
+
125
+ ### Git: sparse-checkout
126
+ * <https://git-scm.com/docs/git-sparse-checkout>
127
+
128
+
129
+ <!-- see https://hh-lohmann.github.io/html-endspacer -->
130
+ <p id="endspacer" data-version="0.2.0" title="Endspacer - helps to align scrolling and positioning link targets | Scroll up to content or click / touch to jump to page top" align="center"><a href="#top"><img alt="Endspacer: './markdown-assets/endspacer.png' missing - see https://hh-lohmann.github.io/html-endspacer" src="./markdown-assets/endspacer.png" height="1000" width="100%"><br>[top]</a></p>
package/index.js ADDED
@@ -0,0 +1,128 @@
1
+ // @ts-check
2
+
3
+ import {basename} from 'node:path';
4
+ import {cpSync,existsSync,rmSync,statSync} from 'node:fs';
5
+ import {spawnSync} from 'node:child_process';
6
+ import {tmpdir} from 'node:os';
7
+ import pkg from './package.json' with { type: "json" };
8
+
9
+ /** @import {_CheckArgsGitCopyFileFolder,_CleanUpBeforeExit,_CopyFileFolder,GitCopyFileFolder} from './types.d.ts' */
10
+
11
+ /** @type {_CheckArgsGitCopyFileFolder} */
12
+ const _checkArgsGitCopyFileFolder=function(passedArgs,errPrefix){
13
+ if(passedArgs.length<2) throw TypeError(`${errPrefix}not enough arguments`);
14
+ if(passedArgs.length>3) throw TypeError(`${errPrefix}too many arguments`);
15
+ ['sourceRepo','fileOrFolder','targetPath'].forEach((value,i)=>{
16
+ if(i===passedArgs.length) return;
17
+ if(typeof passedArgs[i]!=='string'||passedArgs[i]==='') throw TypeError(`${errPrefix}${value} must be a non-empty string`);
18
+ })
19
+ return true;
20
+ }
21
+
22
+ /** Check if Git is available
23
+ * @type {(errPrefix:string)=>boolean}
24
+ */
25
+ const _checkGit=function(errPrefix){
26
+ if(spawnSync('git',['--version']).pid) return true;
27
+ throw ReferenceError(`${errPrefix}Could not call Git - check installation and / or PATH environment variable`);
28
+ }
29
+
30
+ /** Check if target path exists
31
+ * @type {(namePath:string,errPrefix:string)=>boolean}
32
+ */
33
+ const _checkTargetExists=function(namePath,errPrefix){
34
+ if(existsSync(namePath)) return true;
35
+ throw ReferenceError(`${errPrefix}Target path "${namePath}" does not exist`);
36
+ }
37
+
38
+ /** Check if repo to copy from is reachable
39
+ * - NB: not necessarily "does not exist", may be no connection / rights etc.
40
+ * @type {(url:string,errPrefix:string)=>boolean}
41
+ */
42
+ const _checkSourceRepoReachable=function(url,errPrefix){
43
+ if(spawnSync('git',['ls-remote',url]).status===0) return true;
44
+ throw ReferenceError(`${errPrefix}Source repo "${url}" not reachable - you may check spelling / access rights / connectivity`);
45
+ }
46
+
47
+ /** @type {_CleanUpBeforeExit} */
48
+ const _cleanUpBeforeExit={
49
+ _errPrefix: '_cleanUpBeforeExit: ',
50
+ _toDelete: {},
51
+ _pushToDeleteList(list='',entry='',context){
52
+ if(!context) throw SyntaxError(`${this._errPrefix}"context" has to be defined`);
53
+ if(!Object.hasOwn(this._toDelete,context)){this._toDelete[context]={files:[],folders:[]}}
54
+ this._toDelete[context][list].push(entry);
55
+ },
56
+ addDeleteFile(namePath='',context){
57
+ if(namePath==='') return;
58
+ this._pushToDeleteList('files',namePath,context);
59
+ },
60
+ addDeleteFolder(namePath='',context){
61
+ if(namePath==='') return;
62
+ this._pushToDeleteList('folders',namePath,context);
63
+ },
64
+ clean(context){
65
+ if(!context) throw SyntaxError(`${this._errPrefix}"context" has to be defined`);
66
+ if(!Object.hasOwn(this._toDelete,context)) return;
67
+ Object.keys(this._toDelete[context]).forEach(value=>{
68
+ while(this._toDelete[context][value].length!==0){
69
+ const myDeleteNext=this._toDelete[context][value].pop();
70
+ if(!myDeleteNext) return;
71
+ if(!existsSync(myDeleteNext)) throw Error(`${this._errPrefix}"${myDeleteNext}" not found - please check if previous steps were interrupted`);
72
+ try{
73
+ rmSync(myDeleteNext,{recursive:true});
74
+ }
75
+ catch(err){
76
+ throw Error(`${this._errPrefix}"${myDeleteNext}" could not be deleted - please check for possible corruption`);
77
+ }
78
+ }
79
+ })
80
+ }
81
+ }
82
+
83
+ /** Create temporary sparse clone for repo to copy from
84
+ * @type {(source:string,target:string,errPrefix:string)=>boolean}
85
+ */
86
+ const _cloneRepoTempSparseDepth1NoBlobs=function(source,target,errPrefix){
87
+ if(spawnSync('git',['clone','--sparse','--depth=1','--filter=blob:none',source,target]).status===0) return true;
88
+ throw Error(`${errPrefix}Cloning failed (reason unknown)`);
89
+ }
90
+
91
+ /** @type {_CopyFileFolder} */
92
+ const _copyFileFolder=function(fileOrFolder,targetPath,sourceRepoName,tempSparseRepo,errPrefix,cleanUpContext){
93
+ const normalizeNamePath=function(namePath=''){
94
+ if(namePath.slice(0,1)==='/') return namePath.slice(1);
95
+ if(namePath.slice(0,2)==='./') return namePath.slice(2);
96
+ return namePath;
97
+ }
98
+ const normalizeTarget=function(source=''){
99
+ if(statSync(source).isDirectory()) return targetPath+'/'+fileOrFolder;
100
+ return targetPath+'/'+basename(fileOrFolder);
101
+ }
102
+ fileOrFolder=normalizeNamePath(fileOrFolder);
103
+ spawnSync('git',['sparse-checkout','add','./'+fileOrFolder],{cwd:tempSparseRepo});
104
+ const source=tempSparseRepo+'/'+fileOrFolder;
105
+ if(!existsSync(source)){
106
+ _cleanUpBeforeExit.clean(cleanUpContext);
107
+ throw ReferenceError(`${errPrefix}File / folder "${fileOrFolder}" not available in repo "${sourceRepoName}"- you may check spelling / source repo`);
108
+ }
109
+ const target=normalizeTarget(source);
110
+ cpSync(source,target,{recursive:true});
111
+ return true;
112
+ }
113
+
114
+ /** @type {GitCopyFileFolder} */
115
+ export const gitCopyFileFolder=function(sourceRepo,fileOrFolder,targetPath='.'){
116
+ const _myCleanUpBeforeExitContext=crypto.randomUUID();
117
+ const _errPrefix=pkg.name+': ';
118
+ _checkGit(_errPrefix);
119
+ _checkArgsGitCopyFileFolder(arguments,_errPrefix);
120
+ _checkSourceRepoReachable(sourceRepo,_errPrefix);
121
+ _checkTargetExists(targetPath,_errPrefix);
122
+ const tempClonePath=`${tmpdir()}/random_${crypto.randomUUID().split('-')[0]}`;
123
+ _cleanUpBeforeExit.addDeleteFolder(tempClonePath,_myCleanUpBeforeExitContext);
124
+ _cloneRepoTempSparseDepth1NoBlobs(sourceRepo,tempClonePath,_errPrefix);
125
+ _copyFileFolder(fileOrFolder,targetPath,sourceRepo,tempClonePath,_errPrefix,_myCleanUpBeforeExitContext)
126
+ _cleanUpBeforeExit.clean(_myCleanUpBeforeExitContext);
127
+ return true;
128
+ }
Binary file
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "git-copy-file-folder",
3
+ "description": "Copy a file or folder from a Git repo instead of cloning, i.e. without a connection to the source",
4
+ "keywords": [],
5
+ "author": {
6
+ "name": "hh lohmann",
7
+ "email": "hh.lohmann@gmail.com"
8
+ },
9
+ "version": "1.0.0",
10
+ "license": "MIT",
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/hh-lohmann/git-copy-file-folder.git"
14
+ },
15
+ "bugs": "https://github.com/hh-lohmann/git-copy-file-folder/issues",
16
+ "homepage": "https://hh-lohmann.github.io/git-copy-file-folder",
17
+ "type": "module",
18
+ "exports": {
19
+ "types": "./types.d.ts",
20
+ "default": "./index.js"
21
+ },
22
+ "scripts": {
23
+ "test": "node --test ./tests/**/*.test.*"
24
+ },
25
+ "dependencies": {
26
+ "node-test-bootstrap": "github:hh-lohmann/node-test-bootstrap"
27
+ }
28
+ }
package/types.d.ts ADDED
@@ -0,0 +1,46 @@
1
+ /** Check given arguments for gitCopyFileFolder
2
+ * - Signature of gitCopyFileFolder is addressed internally
3
+ */
4
+ export type _CheckArgsGitCopyFileFolder=(passedArgs:IArguments,errPrefix:string)=>boolean;
5
+
6
+
7
+ /** Method object: Clean up runtime artifacts before exiting
8
+ * - "context" to distnguish possible multiple calls of a 'git-copy-file-folder' import in the same parent file
9
+ * @method addDeleteFile(namePath,context) - Add "namePath" to list of files to be deleted in "context"
10
+ * @method addDeleteFolder(namePath,context) - Add "namePath" to list of folders to be deleted in "context"
11
+ * @method clean(context) - Run clean up for "context"
12
+ * @type {object}
13
+ */
14
+ export type _CleanUpBeforeExit={
15
+ _errPrefix:string,
16
+ _toDelete:{[key:string]:{[key:string]:string[]}},
17
+ _pushToDeleteList:(list:string,entry:string,context:string)=>void,
18
+ addDeleteFile:(namePath:string,context:string)=>void,
19
+ addDeleteFolder:(namePath:string,context:string)=>void,
20
+ clean:(context:string)=>void
21
+ }
22
+
23
+ /** Copy file / folder from temporary sparse repo to target
24
+ * @example _copyFileFolder=function( 'src/', '../todo', '/tmp/sparse-source' )
25
+ * @param fileOrFolder - Name / path for file / folder to copy
26
+ * @param targetPath - Path to copy to, default: current dir
27
+ * @param sourceRepoName - Name of source repo to copy from
28
+ * @param tempSparseRepo - Temporary sparse repo to copy from
29
+ * @param errPrefix - Prefix for error messages
30
+ * @param cleanUpContext - context for _cleanUpBeforeExit()
31
+ * @returns true on success
32
+ */
33
+ export type _CopyFileFolder=(fileOrFolder:string,targetPath:string,sourceRepoName:string,tempSparseRepo:string,errPrefix:string,cleanUpContext:string)=>boolean;
34
+
35
+
36
+ /** Clone repo to new one with no connections to the original
37
+ * - New repo will have an empty history and no remotes, everything
38
+ * else will be as in the original
39
+ * @example gitCopyFileFolder('https://github.com/x-y/z','myRepo')
40
+ * @param sourceRepo - URL of the repo to copy from
41
+ * @param fileOrFolder - Name / path for repo to create
42
+ * @param [targetPath] - Optional: Branch name for new repo, default: 'main'
43
+ * @returns `true` on success, `false` else
44
+ */
45
+ export const gitCopyFileFolder:GitCopyFileFolder;
46
+ export type GitCopyFileFolder=(sourceRepo:string,fileOrFolder:string,targetPath?:string)=>boolean;