Skip to content
This repository

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 June 01, 2012
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
tclem commented June 05, 2012
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

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

Showing 4 unique commits by 1 author.

Jun 01, 2012
Robert Ream asynchronous git prompt updates d79162b
Jun 05, 2012
Robert Ream added asynchronous switch to Start-GitPrompt 426851d
Jun 15, 2012
Robert Ream updates to a repository if the current location is not in the reposit…
…ory were using the current location to update the status, which corrupted the status
9563378
Robert Ream ok, this should finally fix the problem 5b36630
This page is out of date. Refresh to see the latest.
4  GitPrompt.ps1
@@ -128,6 +128,6 @@ function Global:Write-VcsStatus { $Global:VcsPromptStatuses | foreach { & $_ } }
128 128
 
129 129
 # Add scriptblock that will execute for Write-VcsStatus
130 130
 $Global:VcsPromptStatuses += {
131  
-    $Global:GitStatus = Get-GitStatus
  131
+    $Global:GitStatus = Get-GitPromptStatus
132 132
     Write-GitStatus $GitStatus
133  
-}
  133
+}
131  GitPromptAsync.ps1
... ...
@@ -0,0 +1,131 @@
  1
+function Start-GitPrompt([switch] $Asynchronous) {
  2
+    if (-not $GitPromptState) {
  3
+        if ($Asynchronous) {
  4
+            $timer = New-Object Timers.Timer -Property @{
  5
+                Interval = 250
  6
+                AutoReset = $true
  7
+                Enabled = $true
  8
+            }
  9
+            Set-Variable GitPromptState -Scope Global -Value (New-Object PSObject -Property @{
  10
+                Asynchronous = $true
  11
+                Repositories = @{ }
  12
+                Timer = $timer
  13
+                Elapsed = Register-ObjectEvent $timer Elapsed -Action { Update-GitPromptRepositories }
  14
+            })
  15
+        }
  16
+        else {
  17
+            Set-Variable GitPromptState -Scope Global -Value (New-Object PSObject -Property @{
  18
+                Asynchronous = $false
  19
+                Repositories = @{ }
  20
+            })
  21
+        }
  22
+    }
  23
+}
  24
+
  25
+function Update-GitPromptRepositories {
  26
+    if ($GitPromptState) {
  27
+        $repositories = $GitPromptState.Repositories
  28
+        foreach($key in $repositories.Keys) {
  29
+            $repository = $repositories[$key]
  30
+            if ($repository.LastStatusUpdate -lt $repository.LastUpdate) {
  31
+                if ($repository.LastStatusUpdate.AddMilliseconds($repository.StatusUpdateIntervalBuffer) -lt [DateTime]::Now) {
  32
+                    Push-Location $repository.Path
  33
+                    try { $repository.Status = Get-GitStatus $repository.Path }
  34
+                    finally { Pop-Location }
  35
+                }
  36
+                $repository.LastStatusUpdate = [DateTime]::Now
  37
+            }
  38
+        }
  39
+    }
  40
+}
  41
+
  42
+function Stop-GitPrompt {
  43
+    if ($GitPromptState) {
  44
+        if ($GitPromptState.Asynchronous) {
  45
+            $GitPromptState.Timer.Enabled = $false
  46
+            Unregister-Event $GitPromptState.Elapsed.Id
  47
+        }
  48
+        $GitPromptState.Repositories.Keys | ForEach-Object { Stop-GitPromptRepository $_ }
  49
+        Remove-Variable GitPromptState -Scope Global
  50
+    }
  51
+}
  52
+
  53
+function Start-GitPromptRepository($Path) {
  54
+    if ($GitPromptState -and (-not $GitPromptState.Repositories.ContainsKey($Path))) {
  55
+        $repositories = $GitPromptState.Repositories.Clone()
  56
+        $watcher = New-Object IO.FileSystemWatcher $Path, '*.*' -Property @{
  57
+            IncludeSubdirectories = $true
  58
+            EnableRaisingEvents = $true
  59
+        }
  60
+        $watcherAction = [scriptblock]::Create( { Publish-GitPromptRepositoryUpdated $Path $eventArgs.Name }.ToString().Replace('$Path', $Path) )
  61
+
  62
+        $repositories[$Path] = New-Object PSObject -Property @{
  63
+            Path = $Path
  64
+            Status = (Get-GitStatus $Path)
  65
+            StatusUpdateIntervalBuffer = 500
  66
+            LastStatusUpdate = ([DateTime]::Now)
  67
+            LastUpdate = ([DateTime]::Now)
  68
+            Watcher = $watcher
  69
+            Changed = Register-ObjectEvent $watcher Changed -Action $watcherAction
  70
+            Created = Register-ObjectEvent $watcher Created -Action $watcherAction
  71
+            Deleted = Register-ObjectEvent $watcher Deleted -Action $watcherAction
  72
+            Renamed = Register-ObjectEvent $watcher Renamed -Action $watcherAction
  73
+            Ignore = @('^\.git$', '^\.git\\index\.lock$', '^\.git\\objects\\')
  74
+        }
  75
+        $GitPromptState.Repositories = $repositories
  76
+    }
  77
+}
  78
+
  79
+function Publish-GitPromptRepositoryUpdated($Path, $File) {
  80
+    if ($GitPromptState -and $GitPromptState.Repositories.ContainsKey($Path)) {
  81
+        $repository = $GitPromptState.Repositories.Get_Item($Path)
  82
+        if ($repository.Ignore | Where-Object { $File -match $_ }) { return }
  83
+        $repository.LastUpdate = [DateTime]::Now
  84
+    }
  85
+}
  86
+
  87
+function Stop-GitPromptRepository($Path) {
  88
+    if ($GitPromptState -and $GitPromptState.Repositories.ContainsKey($Path)) {
  89
+        $repository = $GitPromptState.Repositories[$Path]
  90
+        $repository.Watcher.EnableRaisingEvents = $false
  91
+        Unregister-Event $repository.Changed.Id
  92
+        Unregister-Event $repository.Created.Id
  93
+        Unregister-Event $repository.Deleted.Id
  94
+        Unregister-Event $repository.Renamed.Id
  95
+
  96
+        $repositories = $GitPromptState.Repositories.Clone()
  97
+        $repositories.Remove($Path)
  98
+        $GitPromptState.Repositories = $repositories
  99
+    }
  100
+}
  101
+
  102
+function Update-GitPromptRepository($Path) {
  103
+    if ($GitPromptState -and $GitPromptState.Repositories.ContainsKey($Path) -and (-not $GitPromptState.Asynchronous)) {
  104
+        $repository = $GitPromptState.Repositories[$Path]
  105
+        if ($repository.LastStatusUpdate -lt $repository.LastUpdate) {
  106
+            Push-Location $repository.Path
  107
+            try { $repository.Status = Get-GitStatus $repository.Path }
  108
+            finally { Pop-Location }
  109
+            $repository.LastStatusUpdate = [DateTime]::Now
  110
+        }
  111
+    }
  112
+}
  113
+
  114
+function Get-GitPromptStatus {
  115
+    $Path = Get-GitDirectory
  116
+    if ($Path) {
  117
+        $repositoryPath = Split-Path -parent $Path
  118
+        if ($GitPromptState) {
  119
+            if ($GitPromptState.Repositories.ContainsKey($repositoryPath)) {
  120
+                Update-GitPromptRepository $repositoryPath
  121
+            }
  122
+            else {
  123
+                Start-GitPromptRepository $repositoryPath
  124
+            }
  125
+            $GitPromptState.Repositories[$repositoryPath].Status
  126
+        }
  127
+        else {
  128
+            Get-GitStatus $Path
  129
+        }
  130
+    }
  131
+}
9  posh-git.psm1
@@ -6,6 +6,7 @@ Push-Location $psScriptRoot
6 6
 . ./Utils.ps1
7 7
 . ./GitUtils.ps1
8 8
 . ./GitPrompt.ps1
  9
+. ./GitPromptAsync.ps1
9 10
 . ./GitTabExpansion.ps1
10 11
 . ./TortoiseGit.ps1
11 12
 Pop-Location
@@ -15,7 +16,12 @@ if (!$Env:HOME) { $Env:HOME = "$Env:USERPROFILE" }
15 16
 
16 17
 Export-ModuleMember -Function @(
17 18
         'Write-GitStatus',
18  
-        'Get-GitStatus', 
  19
+        'Get-GitStatus',
  20
+        'Start-GitPrompt',
  21
+        'Update-GitPromptRepositories',
  22
+        'Publish-GitPromptRepositoryUpdated',
  23
+        'Stop-GitPrompt',
  24
+        'Get-GitPromptStatus',
19 25
         'Enable-GitColors', 
20 26
         'Get-GitDirectory',
21 27
         'TabExpansion',
@@ -24,4 +30,3 @@ Export-ModuleMember -Function @(
24 30
         'Stop-SshAgent',
25 31
         'Add-SshKey',
26 32
         'tgit')
27  
-
Commit_comment_tip

Tip: You can add notes to lines in a file. Hover to the left of a line to make a note

Something went wrong with that request. Please try again.