├── README.md └── gitstatus.elv /README.md: -------------------------------------------------------------------------------- 1 | # Gitstatus for Elvish Shell 2 | 3 | **[Gitstatus](https://github.com/romkatv/gitstatus)** is a 10x faster 4 | alternative to git status and git describe. Its primary use case is to enable 5 | fast git prompt in interactive shells. 6 | 7 | **[Elvish](https://elv.sh)** is a friendly interactive shell and an expressive 8 | programming language. It runs on Linux, BSDs, macOS and Windows. 9 | 10 | This Elvish package automatically installs, runs and queries gitstatus and 11 | returns the result in a [Map](https://elv.sh/ref/language.html#map): 12 | 13 | ```shell 14 | $ pprint (gitstatus:query $pwd) 15 | [ 16 | &stashes= 0 17 | &unstaged= 0 18 | &commits-ahead= 0 19 | &tag= '' 20 | &action= '' 21 | &index-size= 0 22 | &untracked= 1 23 | &conflicted= 1 24 | &workdir= /Users/denis/Documents/Code/elvish-gitstatus 25 | &local-branch= master 26 | &remote-branch= '' 27 | &commit= '' 28 | &is-repository= $true 29 | &remote-url= '' 30 | &upstream-branch= $nil 31 | &commits-behind= 0 32 | &staged= 0 33 | &remote-name= '' 34 | ] 35 | ``` 36 | 37 | The resulting map can then be used to change the prompt. For example: 38 | 39 | ```shell 40 | edit:prompt = { 41 | git = (gitstatus:query $pwd) 42 | 43 | if (bool $git[is-repository]) { 44 | 45 | # show the branch, or current commit if not on a branch 46 | branch = '' 47 | if (eq $git[local-branch] "") { 48 | branch = $git[commit][:8] 49 | } else { 50 | branch = $git[local-branch] 51 | } 52 | 53 | put '|' 54 | put (styled $branch red) 55 | 56 | # show a state indicator 57 | if (or (> $git[unstaged] 0) (> $git[untracked] 0)) { 58 | put (styled '*' yellow) 59 | } elif (> $git[staged] 0) { 60 | put (styled '*' green) 61 | } elif (> $git[commits-ahead] 0) { 62 | put (styled '^' yellow) 63 | } elif (> $git[commits-behind] 0) { 64 | put (styled '⌄' yellow) 65 | } 66 | 67 | } 68 | } 69 | ``` 70 | 71 | ## Installation 72 | 73 | Using the [Elvish package manager](https://elv.sh/ref/epm.html): 74 | 75 | ```shell 76 | use epm 77 | epm:install github.com/href/elvish-gitstatus 78 | ``` 79 | 80 | ## Usage 81 | 82 | To query a folder: 83 | 84 | ```shell 85 | use github.com/href/elvish-gitstatus/gitstatus 86 | gitstatus:query /foo/bar 87 | ``` 88 | 89 | To query the current folder: 90 | 91 | ```shell 92 | gitstatus:query $pwd 93 | ``` 94 | 95 | To update gitstatus: 96 | 97 | ```shell 98 | gitstatus:update 99 | ``` 100 | 101 | ## Notes 102 | 103 | Gitstatus is run in the background as a separate process. The binaries are 104 | automatically downloaded from the gitstatus repository and run once per shell 105 | process (no sharing between shell processes). 106 | 107 | Processes should use fairly small amounts of memory (<2MiB on my system). 108 | 109 | I have not yet tested this outside of my Macbook and I use the latest commit 110 | of Elvish. So your mileage my vary on other systems (issues and PRs welcome). 111 | 112 | Gitstatus needs to store a binary locally. This is currently done at the 113 | following folder: 114 | 115 | ~/.elvish/package-data/gitstatus 116 | 117 | This means that you might need to update the .gitignore file of your dotfiles 118 | if you check them into git. 119 | 120 | ## Fields 121 | 122 | ``` 123 | result = gitstatus:query /foo/bar 124 | ``` 125 | 126 | **`result[is-repository]`** 127 | 128 | `$true` if the given folder is part of a git repository. Note that all other 129 | fields are set to `$nil` if the given folder is not part of a git repository. 130 | 131 | **`result[workdir]`** 132 | 133 | The root folder of the git repository. 134 | 135 | **`result[commit]`** 136 | 137 | The commit hash of the current commit. 138 | 139 | **`result[local-branch]`** 140 | 141 | The name of the local branch. 142 | 143 | **`result[upstream-branch]`** 144 | 145 | The name of the upstream branch. 146 | 147 | **`result[remote-name]`** 148 | 149 | The name of the remote. 150 | 151 | **`result[remote-url]`** 152 | 153 | The URL of the remote. 154 | 155 | **`result[action]`** 156 | 157 | The current repository state or active action (e.g. "rebase"). 158 | 159 | **`result[index-size]`** 160 | 161 | The number of files in the index. 162 | 163 | **`result[staged]`** 164 | 165 | The number of staged files. 166 | Limited to 1 by default (see configuration section). 167 | 168 | **`result[unstaged]`** 169 | 170 | The number of unstaged files. 171 | Limited to 1 by default (see configuration section). 172 | 173 | **`result[conflicted]`** 174 | 175 | The number of conflicted files. 176 | Limited to 1 by default (see configuration section). 177 | 178 | **`result[untracked]`** 179 | 180 | The number of untracked files. 181 | Limited to 1 by default (see configuration section). 182 | 183 | **`result[commits-ahead]`** 184 | 185 | The number of commits ahead of the remote. 186 | 187 | **`result[commits-behind]`** 188 | 189 | The number of commits behind the remote. 190 | 191 | **`result[stashes]`** 192 | 193 | The number of stashes. 194 | 195 | **`result[tag]`** 196 | 197 | The current tag. 198 | 199 | ## Configuration 200 | 201 | Configuration can be done via environment variables. It should be done before 202 | querying gitstatus via `gitstatus:query`. If done later, the daemon has to 203 | be restarted using `gitstatus:stop` and `gitstatus:start`. 204 | 205 | ### Max Staged, Unstaged, Untracked 206 | 207 | By default, gitstatus limits the number of staged, unstaged and untracked files 208 | that it enumerates. So those values are either set to 1 or to 0. This is of 209 | course faster than providing an accurate count. 210 | 211 | If you need to know the exact number of files (or of there are one or many), 212 | then you should set the configuration as follows (here with the example value 213 | of 10): 214 | 215 | ``` 216 | use gitstatus 217 | 218 | $E:GITSTATUS_MAX_NUM_STAGED = "10" 219 | $E:GITSTATUS_MAX_NUM_UNSTAGED = "10" 220 | $E:GITSTATUS_MAX_NUM_UNTRACKED = "10" 221 | $E:GITSTATUS_MAX_NUM_CONFLICTED = "10" 222 | ``` 223 | 224 | If you want an accurate count use "-1", disabling the limit. 225 | -------------------------------------------------------------------------------- /gitstatus.elv: -------------------------------------------------------------------------------- 1 | use builtin 2 | use file 3 | use str 4 | 5 | # the folder where the gitstatusd related data is stored 6 | var appdir = ~/.elvish/package-data/gitstatus 7 | 8 | # use the same exact calls as gitstatus (despite having the platform module) 9 | var arch = (str:to-lower (uname -m)) 10 | var os = (str:to-lower (uname -s)) 11 | 12 | # the downloaded binary 13 | var binary = $appdir"/gitstatusd-"$os"-"$arch 14 | 15 | # separators in the gitstatusd API 16 | var rs = (str:from-codepoints 30) 17 | var us = (str:from-codepoints 31) 18 | 19 | # runtime related data to keep track of the daemon 20 | var state = [ 21 | &running=$false 22 | &stdout=$nil 23 | &stdin=$nil 24 | ] 25 | 26 | # configurable arguments to the gitstatusd binary 27 | if (not (has-env GITSTATUS_MAX_NUM_STAGED)) { 28 | set-env GITSTATUS_MAX_NUM_STAGED "1" 29 | } 30 | 31 | if (not (has-env GITSTATUS_MAX_NUM_UNSTAGED)) { 32 | set-env GITSTATUS_MAX_NUM_UNSTAGED "1" 33 | } 34 | 35 | if (not (has-env GITSTATUS_MAX_NUM_UNTRACKED)) { 36 | set-env GITSTATUS_MAX_NUM_UNTRACKED "1" 37 | } 38 | 39 | if (not (has-env GITSTATUS_MAX_NUM_UNTRACKED)) { 40 | set-env GITSTATUS_MAX_NUM_UNTRACKED "1" 41 | } 42 | 43 | if (not (has-env GITSTATUS_MAX_NUM_CONFLICTED)) { 44 | set-env GITSTATUS_MAX_NUM_CONFLICTED "1" 45 | } 46 | 47 | # default version uses an external call to bash 48 | fn get-response { 49 | read-upto $rs < $state[stdout] 50 | } 51 | 52 | # pipes the GET request of the given URL to stdout, using curl or wget 53 | fn http-get {|url| 54 | if (has-external curl) { 55 | curl -L -s -f $url 56 | return 57 | } 58 | 59 | if (has-external wget) { 60 | wget -q -O- $url 61 | return 62 | } 63 | 64 | fail("found no http client to download gitstatusd with") 65 | } 66 | 67 | # not all GitHub releases come with a binary release, so we need to find 68 | # out which releases are available for the current platform 69 | fn latest-version { 70 | http-get https://raw.githubusercontent.com/romkatv/gitstatus/master/install.info ^ 71 | | grep -i (uname -s) ^ 72 | | grep -i (uname -m) ^ 73 | | head -n 1 ^ 74 | | awk '{print $4}' ^ 75 | | cut -d '"' -f 2 76 | } 77 | 78 | # get the download URL for the given version 79 | fn download-url {|version| 80 | put "https://github.com/romkatv/gitstatus/releases/download/"$version"/gitstatusd-"$os"-"$arch".tar.gz" 81 | } 82 | 83 | # returns true if the gitstatusd daemon is running 84 | fn is-running { 85 | put $state[running] 86 | } 87 | 88 | # cross-platform CPU count 89 | fn cpu-count { 90 | try { 91 | put (getconf _NPROCESSORS_ONLN) 92 | } catch { 93 | put (str:split ": " (sysctl hw.ncpu)) | drop 1 94 | } 95 | } 96 | 97 | # returns the number of threads gitstatusd should use 98 | fn thread-count { 99 | var cpus = (cpu-count) 100 | 101 | # see https://github.com/romkatv/gitstatus/issues/34 102 | # would be better, but there doesn't seem 103 | # to be a min function in Elvish at this point 104 | if (< $cpus 16) { 105 | put (* $cpus 2) 106 | } else { 107 | put 32 108 | } 109 | } 110 | 111 | # stops the gitstatusd daemon 112 | fn stop { 113 | if (not is-running) { 114 | fail "gitstatusd is already stopped" 115 | } 116 | 117 | # closing the pipes stops the process 118 | for k [stdin stdout] { 119 | file:close $state[$k][r] 120 | file:close $state[$k][w] 121 | set state[$k] = $nil 122 | } 123 | 124 | set state[running] = $false 125 | } 126 | 127 | # installs the given version 128 | fn install {|version| 129 | 130 | if (is-running) { 131 | stop 132 | } 133 | 134 | mkdir -p $appdir 135 | http-get (download-url $version) | tar -x -z -C $appdir -f - 136 | chmod 0700 $binary 137 | } 138 | 139 | # installs the gitstatusd binary and creates the necessary paths, if necessary 140 | # does nothing if gitstatusd is in PATH 141 | fn ensure-installed { 142 | if (has-external gitstatusd) { 143 | return # already in PATH, lets use that 144 | } 145 | 146 | if (not ?(test -e $binary)) { 147 | install (latest-version) 148 | } 149 | } 150 | 151 | # updates gitstatusd to the latest release (keep the old version) 152 | fn update { 153 | rm $binary 154 | ensure-installed 155 | } 156 | 157 | # starts the gitstatusd daemon in the background 158 | fn start { 159 | if (is-running) { 160 | fail "gitstatusd is already running" 161 | } else { 162 | ensure-installed 163 | } 164 | 165 | for k [stdin stdout] { 166 | set state[$k] = (file:pipe) 167 | } 168 | 169 | if (has-external gitstatusd) { 170 | # use from PATH 171 | var binary = gitstatusd 172 | } 173 | 174 | (external $binary) ^ 175 | --num-threads=(thread-count) ^ 176 | --max-num-staged=$E:GITSTATUS_MAX_NUM_STAGED ^ 177 | --max-num-unstaged=$E:GITSTATUS_MAX_NUM_UNSTAGED ^ 178 | --max-num-untracked=$E:GITSTATUS_MAX_NUM_UNTRACKED ^ 179 | --max-num-conflicted=$E:GITSTATUS_MAX_NUM_CONFLICTED ^ 180 | < $state[stdin] ^ 181 | > $state[stdout] ^ 182 | 2> /dev/null & 183 | 184 | set state[running] = $true 185 | } 186 | 187 | # parses the raw gitstatusd response 188 | fn parse-response {|response| 189 | var @output = (str:split "\x1f" $response) 190 | 191 | var result = [ 192 | &is-repository=(eq $output[1] 1) 193 | &workdir=$nil 194 | &commit=$nil 195 | &local-branch=$nil 196 | &upstream-branch=$nil 197 | &remote-name=$nil 198 | &remote-url=$nil 199 | &action=$nil 200 | &index-size=$nil 201 | &staged=$nil 202 | &unstaged=$nil 203 | &untracked=$nil 204 | &conflicted=$nil 205 | &commits-ahead=$nil 206 | &commits-behind=$nil 207 | &stashes=$nil 208 | &tag=$nil 209 | ] 210 | 211 | if (bool $result[is-repository]) { 212 | set result[workdir] = $output[2] 213 | set result[commit] = $output[3] 214 | set result[local-branch] = $output[4] 215 | set result[remote-branch] = $output[5] 216 | set result[remote-name] = $output[6] 217 | set result[remote-url] = $output[7] 218 | set result[action] = $output[8] 219 | set result[index-size] = $output[9] 220 | set result[staged] = $output[10] 221 | set result[unstaged] = $output[11] 222 | set result[conflicted] = $output[12] 223 | set result[untracked] = $output[13] 224 | set result[commits-ahead] = $output[14] 225 | set result[commits-behind] = $output[15] 226 | set result[stashes] = $output[16] 227 | set result[tag] = $output[17] 228 | } 229 | 230 | put $result 231 | } 232 | 233 | # runs the query against the given path and returns the result in a map 234 | fn query {|repository| 235 | if (not (is-running)) { 236 | start 237 | } 238 | 239 | echo $us$repository$rs > $state[stdin] 240 | put (parse-response (get-response)) 241 | } 242 | --------------------------------------------------------------------------------