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 +21 -0
- package/README.md +130 -0
- package/index.js +128 -0
- package/markdown-assets/endspacer.png +0 -0
- package/package.json +28 -0
- package/types.d.ts +46 -0
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 <hh.lohmann@gmail.com>](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;
|