Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

asynchronous git prompt updates #51

Open
wants to merge 4 commits into from

6 participants

Robert Ream Keith Dahlby Timothy Clem nulltoken James Manning Josh Elster
Robert Ream

these changes speed up the git prompt, with the side effect that the prompt will be eventually consistent

Start-Prompt turns the asynchronous prompt on
Stop-Prompt turns the asynchronous prompt off

Keith Dahlby dahlbyk referenced this pull request
Closed

Posh-git is too slow #50

Keith Dahlby
Owner

Very interesting... I'd considered trying something like this long ago but never got very far - nice work! Are there any leaks if the shell is closed without calling Stop-Prompt?

Let me give this a try for a while...

Robert Ream
Robert Ream

With the latest commit I made the default behavior for Start-GitPrompt is to update the status synchronously, in which case it caches the status and only invalidates the cache asynchronously, and only when files are changed in the repository. This is similar to #52, but it allows for ignore file patterns to filter out non-essential file changes. My intention is for these patterns to be based on the ,gitignore file settings.

Keith Dahlby
Owner

I almost wonder if it would be worth adding a repository watcher component to libgit2sharp to provide Git-aware change notifications (excluding ignored files, etc). Seems like it could be useful to something like GH4W.

/cc @nulltoken @tclem @haacked @xpaulbettsx @aroben

Robert Ream

yeah, it seems like a feature that would be useful for other CLR based git tools as well

Timothy Clem
nulltoken

I almost wonder if it would be worth adding a repository watcher component to libgit2sharp to provide Git-aware change notifications (excluding ignored files, etc).

In order to avoid duplicating the "ignore" logic, wouldn't this invoke repo.Index.retreiveStatus(filepath) for each and every "modified/added" notification?

Keith Dahlby
Owner

In order to avoid duplicating the "ignore" logic, wouldn't this invoke repo.Index.retreiveStatus(filepath) for each and every "modified/added" notification?

Potentially, though the exposed file status could be lazy to avoid the perf hit unless it's needed.

nulltoken

Potentially, though the exposed file status could be lazy to avoid the perf hit unless it's needed.

I'm not sure I'm able to picture the whole design. Let me try:

  • So, a FileSystemWatcher would notify a component of each and every change in the workdir
  • This component would cache all those changes, maybe deduplicate them
  • When an external client (the UI) needs to display the refreshed status, the component would lazily retrrieve the status of each file

Is that it?

Keith Dahlby
Owner

You would also need to monitor the repo (e.g. HEAD changing with a checkout).

The posh-git use case is pretty simple - if anything interesting (either in repo, or in workdir and not ignored) changed, fetch a full status update. Other consumers might be interested in more fine-grained events (e.g. HEAD changed, index changed, workdir changed).

Robert Ream

Perhaps first start out with factoring out any .gitignore filter predicates so that consumers of the library can compose them with a watcher as they see fit.... SRP/SoC

robertream added some commits
James Manning

while it would be tied to the 'console' host of PowerShell, I was wondering if it seems reasonable to have something similar to this approach with status on a timer, but having it update the console async to the prompt?

Since I currently have my prompt put the git status on its own line, it seems reasonable that at prompt rendering time, just store off the current cursor position (where the git status should be written) in a global variable (maybe $global:StatusTargetLocation = [console]::CursorTop) and then fire off the git status job/timer thingy. When it returns, we can write the status string we calculated out to that location in the console.

We could likely use (as-is or close) the WriteTo-Pos function from this blog post (and potentially other bits/tricks from the post as well :)

http://blogs.technet.com/b/heyscriptingguy/archive/2010/03/26/hey-scripting-guy-march-26-2010.aspx

It would definitely depend on the status being on its own line (since we won't know ahead of time how long the status string will end up being and definitely don't want to either overwrite or shift whatever the rest of the contents of their prompt), and would only work in the console host, but does this seem like it would work?

Similar to the previous async approach, you'd want to ensure there was only one outstanding status call at a time instead of hitting enter 10 times firing off git status 10 times :)

Thanks!

Robert Ream
Keith Dahlby
Owner

Cursor manipulation makes me pretty nervous...seems like it would be really difficult to test sufficiently to have confidence that it will "just work".

Josh Elster
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Jun 2, 2012
  1. Robert Ream
Commits on Jun 5, 2012
  1. Robert Ream
Commits on Jun 15, 2012
  1. Robert Ream

    updates to a repository if the current location is not in the reposit…

    robertream authored
    …ory were using the current location to update the status, which corrupted the status
  2. Robert Ream
This page is out of date. Refresh to see the latest.
Showing with 140 additions and 4 deletions.
  1. +2 −2 GitPrompt.ps1
  2. +131 −0 GitPromptAsync.ps1
  3. +7 −2 posh-git.psm1
4 GitPrompt.ps1
View
@@ -128,6 +128,6 @@ function Global:Write-VcsStatus { $Global:VcsPromptStatuses | foreach { & $_ } }
# Add scriptblock that will execute for Write-VcsStatus
$Global:VcsPromptStatuses += {
- $Global:GitStatus = Get-GitStatus
+ $Global:GitStatus = Get-GitPromptStatus
Write-GitStatus $GitStatus
-}
+}
131 GitPromptAsync.ps1
View
@@ -0,0 +1,131 @@
+function Start-GitPrompt([switch] $Asynchronous) {
+ if (-not $GitPromptState) {
+ if ($Asynchronous) {
+ $timer = New-Object Timers.Timer -Property @{
+ Interval = 250
+ AutoReset = $true
+ Enabled = $true
+ }
+ Set-Variable GitPromptState -Scope Global -Value (New-Object PSObject -Property @{
+ Asynchronous = $true
+ Repositories = @{ }
+ Timer = $timer
+ Elapsed = Register-ObjectEvent $timer Elapsed -Action { Update-GitPromptRepositories }
+ })
+ }
+ else {
+ Set-Variable GitPromptState -Scope Global -Value (New-Object PSObject -Property @{
+ Asynchronous = $false
+ Repositories = @{ }
+ })
+ }
+ }
+}
+
+function Update-GitPromptRepositories {
+ if ($GitPromptState) {
+ $repositories = $GitPromptState.Repositories
+ foreach($key in $repositories.Keys) {
+ $repository = $repositories[$key]
+ if ($repository.LastStatusUpdate -lt $repository.LastUpdate) {
+ if ($repository.LastStatusUpdate.AddMilliseconds($repository.StatusUpdateIntervalBuffer) -lt [DateTime]::Now) {
+ Push-Location $repository.Path
+ try { $repository.Status = Get-GitStatus $repository.Path }
+ finally { Pop-Location }
+ }
+ $repository.LastStatusUpdate = [DateTime]::Now
+ }
+ }
+ }
+}
+
+function Stop-GitPrompt {
+ if ($GitPromptState) {
+ if ($GitPromptState.Asynchronous) {
+ $GitPromptState.Timer.Enabled = $false
+ Unregister-Event $GitPromptState.Elapsed.Id
+ }
+ $GitPromptState.Repositories.Keys | ForEach-Object { Stop-GitPromptRepository $_ }
+ Remove-Variable GitPromptState -Scope Global
+ }
+}
+
+function Start-GitPromptRepository($Path) {
+ if ($GitPromptState -and (-not $GitPromptState.Repositories.ContainsKey($Path))) {
+ $repositories = $GitPromptState.Repositories.Clone()
+ $watcher = New-Object IO.FileSystemWatcher $Path, '*.*' -Property @{
+ IncludeSubdirectories = $true
+ EnableRaisingEvents = $true
+ }
+ $watcherAction = [scriptblock]::Create( { Publish-GitPromptRepositoryUpdated $Path $eventArgs.Name }.ToString().Replace('$Path', $Path) )
+
+ $repositories[$Path] = New-Object PSObject -Property @{
+ Path = $Path
+ Status = (Get-GitStatus $Path)
+ StatusUpdateIntervalBuffer = 500
+ LastStatusUpdate = ([DateTime]::Now)
+ LastUpdate = ([DateTime]::Now)
+ Watcher = $watcher
+ Changed = Register-ObjectEvent $watcher Changed -Action $watcherAction
+ Created = Register-ObjectEvent $watcher Created -Action $watcherAction
+ Deleted = Register-ObjectEvent $watcher Deleted -Action $watcherAction
+ Renamed = Register-ObjectEvent $watcher Renamed -Action $watcherAction
+ Ignore = @('^\.git$', '^\.git\\index\.lock$', '^\.git\\objects\\')
+ }
+ $GitPromptState.Repositories = $repositories
+ }
+}
+
+function Publish-GitPromptRepositoryUpdated($Path, $File) {
+ if ($GitPromptState -and $GitPromptState.Repositories.ContainsKey($Path)) {
+ $repository = $GitPromptState.Repositories.Get_Item($Path)
+ if ($repository.Ignore | Where-Object { $File -match $_ }) { return }
+ $repository.LastUpdate = [DateTime]::Now
+ }
+}
+
+function Stop-GitPromptRepository($Path) {
+ if ($GitPromptState -and $GitPromptState.Repositories.ContainsKey($Path)) {
+ $repository = $GitPromptState.Repositories[$Path]
+ $repository.Watcher.EnableRaisingEvents = $false
+ Unregister-Event $repository.Changed.Id
+ Unregister-Event $repository.Created.Id
+ Unregister-Event $repository.Deleted.Id
+ Unregister-Event $repository.Renamed.Id
+
+ $repositories = $GitPromptState.Repositories.Clone()
+ $repositories.Remove($Path)
+ $GitPromptState.Repositories = $repositories
+ }
+}
+
+function Update-GitPromptRepository($Path) {
+ if ($GitPromptState -and $GitPromptState.Repositories.ContainsKey($Path) -and (-not $GitPromptState.Asynchronous)) {
+ $repository = $GitPromptState.Repositories[$Path]
+ if ($repository.LastStatusUpdate -lt $repository.LastUpdate) {
+ Push-Location $repository.Path
+ try { $repository.Status = Get-GitStatus $repository.Path }
+ finally { Pop-Location }
+ $repository.LastStatusUpdate = [DateTime]::Now
+ }
+ }
+}
+
+function Get-GitPromptStatus {
+ $Path = Get-GitDirectory
+ if ($Path) {
+ $repositoryPath = Split-Path -parent $Path
+ if ($GitPromptState) {
+ if ($GitPromptState.Repositories.ContainsKey($repositoryPath)) {
+ Update-GitPromptRepository $repositoryPath
+ }
+ else {
+ Start-GitPromptRepository $repositoryPath
+ }
+ $GitPromptState.Repositories[$repositoryPath].Status
+ }
+ else {
+ Get-GitStatus $Path
+ }
+ }
+}
9 posh-git.psm1
View
@@ -6,6 +6,7 @@ Push-Location $psScriptRoot
. ./Utils.ps1
. ./GitUtils.ps1
. ./GitPrompt.ps1
+. ./GitPromptAsync.ps1
. ./GitTabExpansion.ps1
. ./TortoiseGit.ps1
Pop-Location
@@ -15,7 +16,12 @@ if (!$Env:HOME) { $Env:HOME = "$Env:USERPROFILE" }
Export-ModuleMember -Function @(
'Write-GitStatus',
- 'Get-GitStatus',
+ 'Get-GitStatus',
+ 'Start-GitPrompt',
+ 'Update-GitPromptRepositories',
+ 'Publish-GitPromptRepositoryUpdated',
+ 'Stop-GitPrompt',
+ 'Get-GitPromptStatus',
'Enable-GitColors',
'Get-GitDirectory',
'TabExpansion',
@@ -24,4 +30,3 @@ Export-ModuleMember -Function @(
'Stop-SshAgent',
'Add-SshKey',
'tgit')
-
Something went wrong with that request. Please try again.