Skip to content

Commit

Permalink
feat: implement cache based listing (#29)
Browse files Browse the repository at this point in the history
Co-authored-by: melMass <[email protected]>
  • Loading branch information
amtoine and melMass authored Oct 24, 2023
1 parent d46bedb commit f454d80
Show file tree
Hide file tree
Showing 6 changed files with 182 additions and 27 deletions.
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?
| 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
}

0 comments on commit f454d80

Please sign in to comment.