Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

implement cache based listing #29

Merged
merged 29 commits into from
Oct 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
e107264
add a test to make sure all repos are listed
amtoine Oct 11, 2023
09883a6
add `gm list --update` to update the cache of repos
amtoine Oct 11, 2023
f6992ec
use `gm list` and update cache on clone and remove
amtoine Oct 12, 2023
0436fad
add `gm cache`
amtoine Oct 13, 2023
18d0013
add a test for getting the cache path
amtoine Oct 13, 2023
fc52fa5
remove nested repositories
amtoine Oct 13, 2023
fc30873
remove `find` from the dependencies
amtoine Oct 14, 2023
c3213ca
add a test to make sure `foo.bar` and `foo` are both listed
amtoine Oct 14, 2023
7810e33
fix `list-repos-in-store`
amtoine Oct 14, 2023
2badc7e
be more explicit in the `list-all-repos-in-store` test
amtoine Oct 20, 2023
c11f4b7
use `/` in the tests directly
amtoine Oct 20, 2023
396b0cf
sanitize the path to the cache
amtoine Oct 20, 2023
25d02ee
fix the `get-repo-cache` test by sanitizing paths
amtoine Oct 20, 2023
dd31234
remove drive from Windows paths when sanitizing
amtoine Oct 20, 2023
1ef49ec
cd into the store before globbing
amtoine Oct 20, 2023
6ce80ee
do not use hardcoded `$env.GIT_REPOS_STORE`
amtoine Oct 20, 2023
8130316
make the FIXME about *globbing* more clear
amtoine Oct 20, 2023
9dff625
add a debug statement when the repo store does not exist
amtoine Oct 20, 2023
a1fab43
set the log level of Nushell to "DEBUG" in the CI
amtoine Oct 20, 2023
0c76ca1
sanitize the `$BASE` of the fake store in tests
amtoine Oct 20, 2023
616f964
add a note about `$BASE` not being a `const`
amtoine Oct 20, 2023
3af06bc
sanitize the paths out of `list-repos-in-store`
amtoine Oct 20, 2023
87b5970
remove last mentions to `path_sep`
amtoine Oct 20, 2023
39af729
sanitize the listed paths before remove the `HEAD`
amtoine Oct 20, 2023
b12bd94
docs: add cache to the readme import instructions
melMass Oct 21, 2023
f6e1d6b
move the cache update to `gm cache`
amtoine Oct 21, 2023
fa3f5ea
update the cache without `gm cache --update`
amtoine Oct 21, 2023
bc9b8f9
minor formatting
amtoine Oct 21, 2023
c8af081
chore: revert b12bd94948b8973192d3c74535e0704a88589521
melMass Oct 24, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ defaults:
run:
shell: bash

env:
NU_LOG_LEVEL: DEBUG

jobs:
tests:
strategy:
Expand Down
17 changes: 8 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@
A collection of Nushell tools to manage `git` repositories.

# Table of content
- [*what is `nu-git-manager`*](#bulb-what-is-nu-git-manager-toc)
- [*requirements*](#link-requirements-toc)
- [*installation*](#recycle-installation-toc)
- [*usage*](#gear-usage-toc)
- [*getting help*](#pray-getting-help-toc)
- [*some ideas of advanced (?) usage*](#exclamation-some-ideas-of-advanced--usage-toc)
- [nu-git-manager](#nu-git-manager)
- [Table of content](#table-of-content)
- [:bulb: what is `nu-git-manager` \[toc\]](#bulb-what-is-nu-git-manager-toc)
- [:link: requirements \[toc\]](#link-requirements-toc)
- [:recycle: installation \[toc\]](#recycle-installation-toc)
- [:gear: usage \[toc\]](#gear-usage-toc)
- [:pray: getting help \[toc\]](#pray-getting-help-toc)
- [:exclamation: some ideas of advanced (?) usage \[toc\]](#exclamation-some-ideas-of-advanced--usage-toc)

## :bulb: what is `nu-git-manager` [[toc](#table-of-content)]
like [`ghq`](https://github.com/x-motemen/ghq), `nu-git-manager` aims at being a fully-featured
Expand All @@ -26,9 +28,6 @@ it provides two main modules:
- `gh` (optional) 2.29.0 (used by `sugar gh`)
- with Pacman and `pacman -S community/github-cli`
- with Nix and `nix run nixpkgs#gh`
- `find` 4.9.0
- with Pacman and `pacman -S core/findutils`
- with Nix and `nix run nixpkgs#findutils`

## :recycle: installation [[toc](#table-of-content)]
- install [Nupm] (**recommended**) by following the [Nupm instructions]
Expand Down
2 changes: 1 addition & 1 deletion nu-git-manager/fs/path.nu
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# sanitize a Windows path
export def "path sanitize" []: path -> path {
str replace --all '\' '/'
str replace --regex '^.:' '' | str replace --all '\' '/'
}
49 changes: 41 additions & 8 deletions nu-git-manager/fs/store.nu
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use std log
use path.nu "path sanitize"

export def get-repo-store-path []: nothing -> path {
Expand All @@ -6,17 +7,49 @@ export def get-repo-store-path []: nothing -> path {
) | path expand | path sanitize
}

export def get-repo-store-cache-path []: nothing -> path {
$env.XDG_CACHE_HOME?
melMass marked this conversation as resolved.
Show resolved Hide resolved
| default ($nu.home-path | path join ".cache")
| path join "nu-git-manager/cache.nuon"
| path expand
| path sanitize
}

export def check-cache-file [cache_file: path]: nothing -> nothing {
if not ($cache_file | path exists) {
error make --unspanned {
msg: (
$"(ansi red_bold)cache_not_found(ansi reset):\n"
+ $"please run `(ansi default_dimmed)gm cache --update(ansi reset)` to create the cache"
)
}
}
}

export def list-repos-in-store []: nothing -> list<path> {
if not (get-repo-store-path | path exists) {
log debug $"the store does not exist: `(get-repo-store-path)`"
return []
}

if $nu.os-info.name == "windows" {
# FIXME: this is super slow on windows
glob **/*.git --not [**/*.venv **/node_modules/** **/target/** **/build/** */]
} else {
# FIXME: do not use external `find` command
^find (get-repo-store-path) -name ".git"
| lines
} | each { path split | range 0..(-2) | path join }
# FIXME: glob does not work very well with Windows and absolute paths: the easy fix is to `cd`
# first and then perform the globbing
# related to https://github.com/nushell/nushell/issues/7125
cd (get-repo-store-path)
let heads: list<string> = glob "**/HEAD" --not [
**/.git/**/refs/remotes/**/HEAD,
**/.git/modules/**/HEAD,
**/logs/HEAD
]
# NOTE: we need to keep the trailing `/` here to avoid telling that `foo.bar` is a duplicate of
# `foo`, because `foo/` is not contained in `foo.bar/`
let repos = $heads | each { path sanitize } | str replace --regex '(.git/)?HEAD$' ''

let sorted = $repos | sort
let pairs = $sorted | range 1.. | zip ($sorted | range ..(-2))
$pairs
| filter {|it| not ($it.0 | str starts-with $it.1)}
| each { get 0 }
| prepend $sorted.0
| str trim --right --char "/"
}
72 changes: 64 additions & 8 deletions nu-git-manager/mod.nu
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use std log

use fs/store.nu [get-repo-store-path, list-repos-in-store]
use fs/store.nu [
check-cache-file, get-repo-store-path, get-repo-store-cache-path, list-repos-in-store
]
use git/url.nu [parse-git-url, get-fetch-push-urls]

def "nu-complete git-protocols" []: nothing -> table<value: string, description: string> {
Expand Down Expand Up @@ -70,6 +72,15 @@ export def "gm clone" [

git -C $local_path remote set-url $remote $urls.fetch
git -C $local_path remote set-url $remote --push $urls.push

let cache_file = get-repo-store-cache-path
check-cache-file $cache_file

print --no-newline "updating cache... "
open $cache_file | append $local_path | uniq | sort | save --force $cache_file
print "done"

null
}

# list all the local repositories in your local store
Expand All @@ -86,12 +97,15 @@ export def "gm clone" [
export def "gm list" [
--full-path # show the full path instead of only the "owner + group + repo" name
]: nothing -> list<path> {
let cache_file = get-repo-store-cache-path
check-cache-file $cache_file

let repos = open $cache_file
if $full_path {
list-repos-in-store
$repos
} else {
let root = get-repo-store-path
list-repos-in-store | each {
str replace $root '' | str trim --left --char (char path_sep)
$repos | each {
str replace (get-repo-store-path) '' | str trim --left --char "/"
}
}
}
Expand All @@ -111,6 +125,38 @@ export def "gm root" []: nothing -> path {
get-repo-store-path
}

# get the path to the cache of the local store of repositories managed by `nu-git-manager`
#
# `nu-git-manager` will look for a cache in the following places, in order:
# - `$env.XDG_CACHE_HOME | path join "nu-git-manager/cache.nuon"
# - `~/.cache/nu-git-manager/cache.nuon`
#
# # Example
# a contrived example, assuming you are in `~`
# > XDG_CACHE_HOME=foo gm root
# ~/foo/nu-git-manager/cache.nuon
#
# update the cache of repositories
# > gm cache --update
export def "gm cache" [
--update # will dump the content of the store to the cache of `nu-git-manager`
]: nothing -> path {
let cache_file = get-repo-store-cache-path

if $update {
rm --recursive --force $cache_file
mkdir ($cache_file | path dirname)

print --no-newline "updating cache... "
list-repos-in-store | save --force $cache_file
print "done"

return
}

get-repo-store-cache-path
}

# remove one of the repositories from your local store
#
# # Examples
Expand All @@ -127,9 +173,9 @@ export def "gm remove" [
--fuzzy # remove after fuzzy-finding the repo(s) to clean
]: nothing -> nothing {
let root = get-repo-store-path
let choices = list-repos-in-store
let choices = gm list
| each {
str replace $root '' | str trim --left --char (char path_sep)
str replace $root '' | str trim --left --char "/"
}
| find $pattern

Expand Down Expand Up @@ -165,9 +211,19 @@ export def "gm remove" [

let prompt = $"are you (ansi defu)sure(ansi reset) you want to (ansi red_bold)remove(ansi reset) (ansi yellow)($repo_to_remove)(ansi reset)? "
match (["no", "yes"] | input list $prompt) {
"no" => { log info $"user chose to (ansi green_bold)keep(ansi reset) (ansi yellow)($repo_to_remove)(ansi reset)" },
"no" => {
log info $"user chose to (ansi green_bold)keep(ansi reset) (ansi yellow)($repo_to_remove)(ansi reset)"
return
},
"yes" => { rm --recursive --force --verbose ($root | path join $repo_to_remove) },
}

let cache_file = get-repo-store-cache-path
check-cache-file $cache_file

print --no-newline "updating cache... "
open $cache_file | where $it != ($root | path join $repo_to_remove) | save --force $cache_file
print "done"

null
}
66 changes: 65 additions & 1 deletion tests/mod.nu
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
use std assert

use ../nu-git-manager/git/url.nu [parse-git-url, get-fetch-push-urls]
use ../nu-git-manager/fs/store.nu get-repo-store-path
use ../nu-git-manager/fs/store.nu [
get-repo-store-path, get-repo-store-cache-path, list-repos-in-store
]
use ../nu-git-manager/fs/path.nu "path sanitize"

export def path-sanitization [] {
Expand Down Expand Up @@ -88,3 +90,65 @@ export def get-store-root [] {
assert equal $actual ($case.expected | path expand | path sanitize)
}
}

export def get-repo-cache [] {
let cases = [
[env, expected];

[{XDG_CACHE_HOME: null}, "~/.cache/nu-git-manager/cache.nuon"],
[{XDG_CACHE_HOME: "~/xdg"}, "~/xdg/nu-git-manager/cache.nuon"],
]

for case in $cases {
let actual = with-env $case.env { get-repo-store-cache-path }
assert equal $actual ($case.expected | path expand | path sanitize)
}
}

export def list-all-repos-in-store [] {
# NOTE: `$BASE` is a constant, hence the capitalized name, but `path sanitize` is not a
# parse-time command
let BASE = (
$nu.temp-path | path join "nu-git-manager/tests/list-all-repos-in-store" | path sanitize
)

if ($BASE | path exists) {
rm --recursive --verbose --force $BASE
}
mkdir $BASE

let store = [
[is_bare, in_store, path];

[false, true, "a/normal/"],
[true, true, "a/bare/"],
[false, true, "b/c/d/normal/"],
[true, true, "b/c/d/bare/"],
[false, false, "a/normal/b/nested/"],
[false, false, "a/normal/.git/modules/foo/"],
[false, true, "a/normal.but.more.complex/"],
]

for repo in $store {
if $repo.is_bare {
git init --bare ($BASE | path join $repo.path)
} else {
git init ($BASE | path join $repo.path)
}
}

# NOTE: remove the path to BASE so that the test output is easy to read
let actual = with-env {GIT_REPOS_HOME: $BASE} { list-repos-in-store } | each {
str replace $BASE '' | str trim --left --char "/"
}
let expected = $store | where in_store | get path | each {
# NOTE: `list-repos-in-store` does not add `/` at the end of the paths
str trim --right --char "/"
}

# NOTE: need to sort the result to make sure the order of the `git init` does not influence the
# results of the test
assert equal ($actual | sort) ($expected | sort)

rm --recursive --verbose --force $BASE
}