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

@robertream

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

@dahlbyk dahlbyk referenced this pull request
Closed

Posh-git is too slow #50

@dahlbyk
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...

@robertream
@robertream

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.

@dahlbyk
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

@robertream

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

@tclem
@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?

@dahlbyk
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?

@dahlbyk
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).

@robertream

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
@jamesmanning

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!

@robertream
@dahlbyk
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".

@jelster
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Jun 2, 2012
  1. @robertream
Commits on Jun 5, 2012
  1. @robertream
Commits on Jun 15, 2012
  1. @robertream

    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. @robertream
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
View
4 GitPrompt.ps1
@@ -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
-}
+}
View
131 GitPromptAsync.ps1
@@ -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
+ }
+ }
+}
View
9 posh-git.psm1
@@ -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.