Prompts and Directories - Even Better Git (and Mercurial) with PowerShell
I love PowerShell and spent years and years working with it since it first came out. I've actually got 15 or so pages of PowerShell posts on this blog going way back.
PowerShell is insanely powerful. I would even go so far as to say it was (is) ahead of its time. PowerShell is totally dynamic and is almost like having JavaScript at the command line, but even more thoughtfully designed. I appreciate folks that love their bash shells and what not, but PowerShell can do some wonderful things.
A long time ago (2009) Mark Embling blogged about a nice prompt with TabExpansion that he'd made to make working with PowerShell and Git (his favorite source control system) nicer. Later, Jeremy Skinner added TabExpansion for common commands. A little while later Keith Dahlby started with their code (with their blessing) and posh-git was born. Expanding even more, Jeremy later created posh-hg (for Mercurial, my favorite source control system).
All of these are currently in use in various forms. Just recently (days ago, even) while I was trying figure out how to get these two separate but similar PowerShell scripts to live together when Keith created a small shared function that makes sharing prompts easier.
I think that Git Bash on Windows needs to go away. It's just not useful to say that a Windows user has to run Bash in order to use Git. PowerShell with Git (or your favorite VCS) is demonstrably better for Windows folks. I also feel that the installation for posh-git, while it uses PsGet (think NuGet for PowerShell, which is INSANELY awesome), just could be easier.
I happened to be tweeting about this and ended up doing a Skype+Join.me 3-way pair programming session with Keith and Paul Betts to explore some ideas on the topic. While a customized prompt is cool, I wouldn't rest until we'd modified "dir/ls" to show source control status. I'm not talking about a PowerShell Provider, I'm talking about extending the View for the result of a dir (get-childitem and a FileInfo) itself.
I want to be able to take a fresh machine and fresh PowerShell installation, invoke one PsGet command and get Git and Hg (and whoever else) integration with PowerShell, a new prompt AND new (yet to be written at this point) directory listing support.
We did some pairing as I sat in a pub and drove while Keith and Paul told me I was a lousy typist. We got a nice prototype working and I went home. After the kids went to sleep I was asking questions on a mailing list and ended up getting an answer from James Brundage, noted PowerShell expert. I'd met James at a Nerd Dinner in Seattle once and gave him a book. He was kind enough to do a screen sharing session with me and refactor my directory spike (and some of posh-git) into a more useful form. It's still a spike, but Keith and I are going to merge all three of them (posh-git, posh-hg and my VCS dir stuff) into one usable and easy to install module. I'm sure we'll both blog about it when it's cleaner. I'm hoping we'll get it all integrated into a single install line:
(new-object Net.WebClient).DownloadString("http://psget.net/GetPsGet.ps1") | iex
install-module posh-git
Here's the general idea that encapsulates a number of these ideas. Rather than scripts that are plugged into your PowerShell $profile, we'll have a module or two like this.
C:\Users\Scott\Documents\WindowsPowerShell\Modules
$ dir
Mode LastWriteTime Length Name
---- ------------- ------ ----
d---- 1/31/2012 10:37 PM Posh-Git
d---- 1/31/2012 10:37 PM Posh-GitDir
d---- 1/31/2012 12:27 AM PsGet
The Posh-Git folder is the Posh-Git source as it is, as a module and imported in your profile like this.
Import-Module Posh-Git
Import-Module Posh-GitDir
Posh-GitDir is my extension module that will change dir/ls/get-childitem and add a Git Status column. I've added extra columns with file information before in PowerShell, except in a cheesy way and I never actually overrode dir directly.
First, we'll make a post-gitdir.Types.ps1xml that adds the new ScriptProperty that pulls details for each file out of a $GitStatus variable that's added each time the prompt is drawn.
<?xml version="1.0" encoding="utf-8" ?>
<Types>
<Type>
<Name>System.IO.FileInfo</Name>
<Members>
<ScriptProperty>
<Name>Git</Name>
<GetScriptBlock>
$retVal = ""
if ($GitStatus.Index.Added -contains $this.Name) { $retVal += "+" } `
elseif ($GitStatus.Index.Modified -contains $this.Name) { $retVal += "~" } `
elseif ($GitStatus.Index.Unmerged -contains $this.Name) { $retVal += "!" } `
else { $retVal += " " }
$retVal += " "
if ($GitStatus.Working.Added -contains $this.Name) { $retVal += "+" } `
elseif ($GitStatus.Working.Modified -contains $this.Name) { $retVal += "~" } `
elseif ($GitStatus.Working.Unmerged -contains $this.Name) { $retVal += "!" } `
else { $retVal += " " }
$retVal
</GetScriptBlock>
</ScriptProperty>
</Members>
</Type>
<Type>
<Name>System.IO.DirectoryInfo</Name>
<Members>
<ScriptProperty>
<Name>Git</Name>
<GetScriptBlock>
""
</GetScriptBlock>
</ScriptProperty>
</Members>
</Type>
</Types>
This adds the Git column to the output as a ScriptProperty, but doesn't change the default view.
$ dir | get-member
TypeName: Get-ChildItem
Name MemberType Definition
---- ---------- ----------
Mode CodeProperty System.String Mode{get=Mode;}
Create Method System.Void Create(System.Security.AccessControl.DirectorySecurity director...
CreateObjRef Method System.Runtime.Remoting.ObjRef CreateObjRef(type requestedType)
...snip...
BaseName ScriptProperty System.Object BaseName {get=$this.Name;}
Git ScriptProperty System.Object Git {get="";}
I'd have to always select manually, which is tedious.
$ dir | select Name, Git
Name Git
---- ---
CommandType.cs ~
Connection.cs ~
ConnectionExtensions.cs ~
ConnectionManager.cs +
ConnectionScope.cs +
GuidConnectionIdFactory.cs ~
We want to change the dir "view" itself. We have to copy the default view for a directory at: "C:\Windows\system32\WindowsPowerShell\v1.0\FileSystem.format.ps1xml" and add our new column.
<?xml version="1.0" encoding="utf-16"?>
<Configuration>
<ViewDefinitions>
<View>
<Name>Dir-Git</Name>
<ViewSelectedBy>
<TypeName>Dir-Git</TypeName>
</ViewSelectedBy>
<TableControl>
<TableHeaders>
<TableColumnHeader>
<Label>Git</Label>
<Width>4</Width>
<Alignment>left</Alignment>
</TableColumnHeader>
...snip...
</TableHeaders>
<TableRowEntries>
<TableRowEntry>
<Wrap/>
<TableColumnItems>
<TableColumnItem>
<PropertyName>Git</PropertyName>
</TableColumnItem>
...snip...
</TableColumnItems>
</TableRowEntry>
</TableRowEntries>
</TableControl>
</View>
</ViewDefinitions>
</Configuration>
For the design, I want the Indexed and Working files in two columns, showing Added+, Modified~ and Unmerged! files like this. Deleted files won't show up cause they aren't there.
$ dir #snipped out directories and files for clarity
Mode Git LastWriteTime Length Name
---- --- ------------- ------ ----
d---- 1/31/2012 5:14 PM Configuration
d---- 1/31/2012 5:14 PM Hosting
-a--- + 1/31/2012 5:14 PM 2170 ConnectionManager.cs
-a--- + 1/31/2012 5:14 PM 402 ConnectionScope.cs
-a--- ~ 1/31/2012 5:14 PM 280 GuidConnectionIdFactory.cs
-a--- 1/31/2012 5:14 PM 273 IConnection.cs
-a--- ~ 1/31/2012 5:14 PM 165 IConnectionIdFactory.cs
-a--- + 1/31/2012 5:14 PM 304 IConnectionManager.cs
-a--- 1/31/2012 5:14 PM 118 packages.config
-a--- ~ 1/31/2012 5:14 PM 8296 PersistentConnection.cs
-a--- + 1/31/2012 5:14 PM 1118 PersistentConnectionFactory.cs
-a--- 1/31/2012 5:14 PM 623 PersistentResponse.cs
-a--- ~ 1/31/2012 5:14 PM 1288 SignalCommand.cs
-a--- ~ 1/31/2012 5:14 PM 7386 SignalR.csproj
-a--- 1/31/2012 5:14 PM 23076 TaskAsyncHelper.cs
C:\Users\Scott\Desktop\github\SignalR\SignalR [master +1 ~0 -0 | +10 ~58 -11 !]
We import these modules in our $profile.
Import-Module Posh-Git
Import-Module Posh-GitDir
The posh-git module adds the custom prompt (if you haven't changed yours) with a new function called Write-VcsStatus that is shared between Hg and Git (and any other systems that want to play with us). It only adds the prompt if the user hasn't already customized their prompt. If they have, they'll need to incorporate Write-VcsStatus themselves.
$defaultPromptHash = "HEYStcKFSSj9jrfqnb9f+A=="
$md5 = [Security.Cryptography.MD5]::Create()
$thePrompt = [Text.Encoding]::Unicode.GetBytes((Get-Command prompt | Select-Object -ExpandProperty Definition))
$thePromptHash = [Convert]::ToBase64String($md5.ComputeHash($thePrompt))
if ($thePromptHash -eq $defaultPromptHash) #using the default prompt?
{
#recommend our own
function prompt(){
# Reset color, which can be messed up by Enable-GitColors
$Host.UI.RawUI.ForegroundColor = $GitPromptSettings.DefaultForegroundColor
Write-Host($pwd) -nonewline -foregroundcolor white
Write-VcsStatus
Write-Host ""
return "$ "
}
}
else {
Write-Debug "Make sure your prompt includes a called to Write-VcsStatus!"
}
The craziness that James Brundage came up with to override dir/ls/get-childitem was this. He said he'll do a complete tutorial on his blog with technical details for the generic case.
. ([ScriptBlock]::Create("
function Get-ChildItem {
$([Management.Automation.ProxyCommand]::GetCmdletBindingAttribute((Get-Command Get-ChildItem -CommandType Cmdlet)))
param(
$([Management.Automation.ProxyCommand]::GetParamBlock((Get-Command Get-ChildItem -CommandType Cmdlet)))
)
process {
Microsoft.PowerShell.Management\Get-ChildItem @psBoundParameters |
ForEach-Object {
`$null = `$_.pstypenames.Insert(0, 'Dir-Git')
`$_
}
}
}
"))
Tie it all up with a .psd1 file that has the list of Scripts, Types, Formats and the Module.
@{
ModuleVersion="1.0.0.0"
Author="Scott Hanselman"
Description="Posh-GitDir"
CompanyName="Hanselman and Friends"
RequiredModules="Posh-Git"
ScriptsToProcess="prompt.ps1"
TypesToProcess="posh-gitdir.Types.ps1xml"
FormatsToProcess="posh-gitdir.Format.ps1xml"
ModuleToProcess="posh-gitdir.psm1"
}
To recap, this new module requires the posh-git module, it ands our new "Dir-Git" type, adds the Git ScriptProperty in Types, and shows how to Format it, and overrides get-childitem in the psm1. If you didn't want to override dir proper, maybe you could make a dir-git or dir-hg.
Next steps are for us to integrate them into one module, bring in an inproc library to access the source info (rather than regex'ing the output of git status and hg status) which would speed it up 10x I'm sure, as well as better NuGet support.
In this screenshot you can see posh-git and posh-hg living together. The first directory is a hg repo with a 1 file not in control. The second directory is a git repo with 1 file added in the Index, 10 new added in working, 58 modified, and 11 deleted.
Keith and Jeremy have done some amazing work. Open Source, baby. I'm looking forward to pairing with them in coming days and buttoning this up. I've been a hardcore Tortoise (Tortoise-Hg, Tortoise-SVN, Tortoise-Git) source control user, but the addition of PowerShell is shaking my faith in a good way. At leat to the point that I think it's worth my spare time to see this through.
Thoughts?
Related Links
- Awesome Visual Studio Command Prompt and PowerShell icons with Overlays
- Accessing EXIF Photo Data from JPEGs with PowerShell
About Scott
Scott Hanselman is a former professor, former Chief Architect in finance, now speaker, consultant, father, diabetic, and Microsoft employee. He is a failed stand-up comic, a cornrower, and a book author.
About Newsletter
On github, repository provides download link for entry repository. This is very useful for such modules, because you do not even need to package your module.
Function Prompt {
"[{0} {1}]PS {2}>" -f (get-date).ToShortDateString(),(get-date).ToLongTimeString(),(get-location)
}
That worked even on nuget 1.5, I think.
My module is already in my github. But, give us a few weeks.
Chrissie - yes, I know NuGet has a custom prompt. I wasn't questioning that. I'm talking about the custom source control prompts.
When you will think about this feature, please, add me to chat.
I was about to solve problem with shared stuff like TabExpansions, Prompts directly in PsGet by adding some sort of optional contact in modules. But if this problem could be solved in PsGet agnostic way, this will be much better, and of course, I want PsGet to support this :).
I tried it and it works as advertized even with colors, allthough the colors don't work well on a white background.
Not sure if I can change the backgroundcolor of the nuget PS.
Your servant
Chrissie1 ;-)
I like the other things you are doing with this. Maybe I'm missing something but I just don't see the dir/ls stuff being helpful.
Mike - I think the shared TabExpansion/status problem is largely solved (check out recent posh-git commits), but I'm open to suggestions to generalize the solution.
Don - The finished* product will likely make the ls/dir integration optional, so if you don't find it useful you can turn it off.
* As if OSS is ever finished. Even log4net.
① PAUSE: Something better than throwing the module at an admin or checking out the entire catalog to add something.
② Content: The list is quite small (due in part to ①, perhaps). Scott blogging about it will help significantly. Should the modules in the PowerShell Community Extensions be broken into more granular pieces and incorporated into PsGet? Should Codeplex offer some kind of streamlined experience to add a project to PsGet or their own directory?
Even further off-topic, and super pedantic: The alias for Install-Module should be ismo (rather than ipmo). According to MSDN: Cmdlet Verbs, is is the preferred alias prefix for "Install", and ip for "Import".
I think that Git Bash on Windows needs to go away. It's just not useful. PowerShell with Git (or your favorite VCS) is demonstrably better for Windows folks. (emphasis added)
Scott, I would love for you to demonstrate this. I'm not sure I'm buying it. I think you should support this claim by doing a video with a Bash guru and a PS guru, comparing and contrasting the advantages/disadvantages.
If you REALLY want to get it working you need to:
- Make C:\Users\YOU\Documents\WindowsPowerShell\Modules
- Make a posh-git and posh-gitdir folder in Modules\ spelling is crucial
- Put posh-git in one and my posh-gitdir spike in the other
- Add Import Posh-Git and Import Posh-GitDir in your PowerShell profile which his located at $profile.
>> ① PAUSE: Something better than throwing the module at an admin or checking out the entire catalog to add something.
Agree. Directory.xml should be modified and then pulled to main repository and this is not something very user friendly. But that was most easy to implement.
>> ② Content: The list is quite small (due in part to ①, perhaps). Scott blogging about it will help significantly. Should the modules in the PowerShell Community Extensions be broken into more granular pieces and incorporated into PsGet? Should Codeplex offer some kind of streamlined experience to add a project to PsGet or their own directory?
Codeplex does not provide direct download link :(. I was thinking about hosting copies of modules somewhere else. But this kills the beauty of the frictionless expirience of getting latest version of the module.
>> Even further off-topic, and super pedantic: The alias for Install-Module should be ismo (rather than ipmo). According to MSDN: Cmdlet Verbs, is is the preferred alias prefix for "Install", and ip for "Import".
You are right. I think for now I will keep them both, and later I will retire ipmo.
Seems I have job for this weekend :). Thanks for your feedback.
>> I think the shared TabExpansion/status problem is largely solved (check out recent posh-git commits), but I'm open to suggestions to generalize the solution.
Wow, looks awesome, some feedback https://github.com/dahlbyk/posh-git/commit/36d61ca85cd6e241b8cd8068eb45699dc40101e7#commitcomment-924238
I like PS as a scripting language, but every time I decide I want to start using it as my standard console, stuff like the above pops up. PS is a decent language, but the shell environments seem to be generations behind the un*x equivalents.
https://github.com/somewhatabstract/posh-svn
http://studioshell.codeplex.com/
Would have been even cooler if it was on Github of course ;-)
Thought you might like to know about a small quirk. If you change directories using the wrong case, the directory changes, but the Wip column does not populate.
Cheers Tim.
We do in fact have JavaScript at the command line. Heard of Node.js?
iwr http://psget.net/GetPsGet.ps1 | iex
Et voila !
Comments are closed.
uglynice colors you use. You can then even open and close the solution when checking out as to not confuse VS to much with all the magic.