Skip to content

Commit 9917ffd

Browse files
fmotallebMotalleb Fallahnezhad
andauthored
feat: Install plugins directly from Git repositories (#126)
This change enhances the plugin installation mechanism by introducing a more flexible Git-based package fetcher. The new `fetch-package-git` function (used via the `--git` flag on the `install` subcommand) enables fetching a package directly from a Git repository into a temporary directory, with support for additional Git options through environment configuration. **Key details:** * **Branch or tag selection:** The `--branch` flag allows checking out a specific branch or tag. * **Shallow clone optimization:** Performs a shallow clone using the `NUPM_GIT_CLONE_DEPTH` env var (default: `1`) for faster downloads. * **Custom Git arguments:** Supports extra Git options via the `NUPM_GIT_CLONE_ARGS` environment variable, allowing users to pass flags such as `--recurse-submodules` or `--filter` for advanced cloning scenarios. **Effect:** This update removes the need for users to manually clone repositories and install with `--path`, or for developers to publish modules to a registry. It simplifies plugin/module installation workflows. **Notes:** Additional features such as Git filters, sub-directories, ..., may be added later. For now, the implementation intentionally remains minimal and straightforward. --------- Co-authored-by: Motalleb Fallahnezhad <[email protected]>
1 parent faa76f7 commit 9917ffd

File tree

3 files changed

+130
-55
lines changed

3 files changed

+130
-55
lines changed

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,12 @@ git clone https://github.com/nushell/foo.git
7070
nupm install foo --path
7171
```
7272

73+
or
74+
75+
```nushell
76+
nupm install https://github.com/nushell/foo.git --git
77+
```
78+
7379
### update a package [[toc](#table-of-content)]
7480

7581
Assuming the repository is already cloned, you can update the module package with the following:

nupm/install.nu

Lines changed: 115 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
use utils/completions.nu complete-registries
22
use utils/dirs.nu [ nupm-home-prompt cache-dir module-dir script-dir tmp-dir PACKAGE_FILENAME ]
3-
use utils/log.nu throw-error
3+
use utils/log.nu [throw-error]
44
use utils/misc.nu [check-cols hash-fn url]
55
use utils/package.nu open-package-file
66
use utils/registry.nu search-package
77
use utils/version.nu filter-by-version
8-
8+
use std/assert
99
# Install list of scripts into a directory
1010
#
1111
# Input: Scripts taken from 'nupm.nuon'
@@ -17,20 +17,22 @@ def install-scripts [
1717
each {|script|
1818
let src_path = $pkg_dir | path join $script
1919

20-
if ($src_path | path type) != file {
21-
throw-error "script_not_found" $"Script ($src_path) does not exist"
22-
}
23-
24-
if (($scripts_dir
25-
| path join ($script | path basename)
26-
| path type) == file
27-
and (not $force)
28-
) {
29-
throw-error "package_already_installed" (
30-
$"Script ($src_path) is already installed in"
31-
+ $" ($scripts_dir). Use `--force` to override the package."
32-
)
33-
}
20+
(
21+
assert (($src_path | path type) == file)
22+
$"Script ($src_path) does not exist"
23+
--error-label={text: "script_missing", span:(metadata $src_path | get span)}
24+
)
25+
(
26+
assert ($force
27+
or (
28+
$scripts_dir
29+
| path join ($script | path basename)
30+
| path type
31+
) != file
32+
)
33+
$"Script ($src_path) is already installed in ($scripts_dir). Use `--force` to override the package."
34+
--error-label={text: "already_installed", span:(metadata $src_path | get span)}
35+
)
3436

3537
log debug $"installing script `($src_path)` to `($scripts_dir)`"
3638
cp $src_path $scripts_dir
@@ -128,17 +130,22 @@ def download-pkg [
128130
pkg: record<
129131
name: string,
130132
version: string,
131-
path: string,
133+
path: any,
132134
type: string,
133135
info: any,
134136
>
135137
]: nothing -> path {
136138
# TODO: Add some kind of hashing to check that files really match
137-
138-
if ($pkg.type != 'git') {
139-
throw-error 'Downloading non-git packages is not supported yet'
140-
}
141-
139+
(
140+
assert ([string,nothing] | any {|| $in == ($pkg.path | describe)})
141+
"Package path must be a string or null"
142+
--error-label={text: "invalid_arguments", span:(metadata $pkg.path | get span)}
143+
)
144+
(
145+
assert ($pkg.type == 'git')
146+
"Downloading non-git packages is not supported yet"
147+
--error-label={text: "invalid_arguments", span:(metadata $pkg.type | get span)}
148+
)
142149
let cache_dir = cache-dir --ensure
143150
cd $cache_dir
144151

@@ -162,22 +169,15 @@ def download-pkg [
162169
}
163170

164171
try {
165-
git clone $pkg.info.url $clone_dir
172+
git-clone $pkg.info.url --revision=$pkg.info.revision --directory=$clone_dir
166173
} catch {
167174
throw-error $'Error cloning repository ($pkg.info.url)'
168175
}
169-
170-
cd $clone_dir
171-
172-
try {
173-
git checkout $pkg.info.revision
174-
} catch {
175-
throw-error $'Error checking out revision ($pkg.info.revision)'
176-
}
177-
178-
if not ($pkg_dir | path exists) {
179-
throw-error $'Path ($pkg_dir) does not exist'
180-
}
176+
(
177+
assert (not ($pkg_dir | path exists))
178+
$'Package path ($pkg.path) does not exist in cloned repository'
179+
--error-label={text: "path_missing", span:(metadata $pkg_dir | get span)}
180+
)
181181

182182
$pkg_dir
183183
}
@@ -190,13 +190,16 @@ def fetch-package [
190190
]: nothing -> path {
191191
let regs = search-package $package --registry $registry --exact-match
192192

193-
if ($regs | is-empty) {
194-
throw-error $'Package ($package) not found in any registry'
195-
} else if ($regs | length) > 1 {
196-
# TODO: Here could be interactive prompt
197-
throw-error $'Multiple registries contain package ($package)'
198-
}
199-
193+
(
194+
assert ($regs | is-not-empty)
195+
$'Package ($package) not found in any registry'
196+
--error-label={text: "registry_not_found", span:(metadata $regs| get span)}
197+
)
198+
(
199+
assert (($regs | length) == 1)
200+
$'Multiple registries contain package ($package)'
201+
--error-label={text: "multiple_registries_found", span:(metadata $regs| get span)}
202+
)
200203
# Now, only one registry contains the package
201204
let reg = $regs | first
202205
let pkgs = $reg.pkgs | filter-by-version $version
@@ -206,11 +209,12 @@ def fetch-package [
206209
} catch {
207210
throw-error $'No package matching version `($version)`'
208211
}
209-
210-
if $pkg.hash_mismatch == true {
211-
throw-error ($'Content of package file ($pkg.path)'
212-
+ $' does not match expected hash')
213-
}
212+
213+
(
214+
assert (not $pkg.hash_mismatch)
215+
$'Content of package file ($pkg.path) does not match expected hash'
216+
--error-label={text: "hash_mismatch", span:(metadata $pkg.hash_mismatch| get span)}
217+
)
214218

215219
print $pkg
216220

@@ -227,6 +231,61 @@ def fetch-package [
227231
}
228232
}
229233

234+
# Fetch a package from a git repository (clone into a temp-directory)
235+
def git-clone [
236+
package: string # Git URL
237+
--branch: string # Branch or tag name
238+
--directory: path # Target directory to clone into
239+
--revision: string # Revision to checkout to
240+
]: nothing -> path {
241+
(
242+
assert (($directory | path type | default 'dir') == 'dir')
243+
$"Target directory ($directory) is not a directory"
244+
--error-label={text: "invalid_arguments", span:(metadata $directory| get span)}
245+
)
246+
(
247+
assert (not (
248+
[$revision,$branch]
249+
| all {|| $in | is-not-empty}))
250+
"You must specify only one of --branch or --revision"
251+
--error-label={text: "invalid_arguments", span:(metadata $branch| get span)}
252+
)
253+
254+
let depth = ($env.NUPM_GIT_CLONE_DEPTH? | default 1)
255+
mut clone_args = ["clone", $package]
256+
257+
if ($revision | is-empty) {
258+
$clone_args = ($clone_args | append [$"--depth=($depth)"])
259+
}
260+
261+
if ($branch | is-not-empty) {
262+
$clone_args = ($clone_args | append [$"--branch=($branch)"])
263+
}
264+
265+
if ($env.NUPM_GIT_CLONE_ARGS? | length) > 0 {
266+
$clone_args = ($clone_args | append $env.NUPM_GIT_CLONE_ARGS?)
267+
}
268+
269+
$clone_args = ($clone_args | append [$directory])
270+
try {
271+
git ...$clone_args
272+
} catch {
273+
throw-error $'Error cloning repository ($package)'
274+
}
275+
276+
if ($revision | is-not-empty) {
277+
{
278+
cd $directory
279+
try {
280+
git checkout $revision
281+
} catch {
282+
throw-error $'Error checking out revision ($revision)'
283+
}
284+
}
285+
}
286+
return $directory
287+
}
288+
230289
# Install a nupm package
231290
#
232291
# Installation consists of two parts:
@@ -240,19 +299,20 @@ export def main [
240299
--path # Install package from a directory with nupm.nuon given by 'name'
241300
--force(-f) # Overwrite already installed package
242301
--no-confirm # Allows to bypass the interactive confirmation, useful for scripting
302+
--git # Install package from remote git repository, (shorthand for git clone + install)
303+
--branch: string # Branch or tag name to pull
243304
]: nothing -> nothing {
244305
if not (nupm-home-prompt --no-confirm=$no_confirm) {
245306
return
246307
}
247-
248-
let pkg: path = if not $path {
249-
fetch-package $package --registry $registry --version $pkg_version
250-
} else {
251-
if $pkg_version != null {
252-
throw-error "Use only --path or --pkg-version, not both"
253-
}
254-
308+
309+
let pkg: path = if $path {
310+
assert ($pkg_version == null) "Use only --path or --pkg-version, not both" --error-label={text: "invalid_arguments", span:(metadata $path | get span)}
255311
$package
312+
} else if $git {
313+
git-clone $package --branch=$branch --directory=(tmp-dir git-clone --ensure)
314+
} else {
315+
fetch-package $package --registry $registry --version $pkg_version
256316
}
257317

258318
install-path $pkg --force=$force

tests/mod.nu

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,15 @@ export def install-custom [] {
6565
}
6666
}
6767

68+
69+
export def install-custom-git [] {
70+
with-test-env {
71+
nupm install --git https://github.com/nushell/nupm.git
72+
73+
assert installed [modules nupm]
74+
}
75+
}
76+
6877
export def install-from-local-registry [] {
6978
with-test-env {
7079
$env.NUPM_REGISTRIES = {}

0 commit comments

Comments
 (0)