Creating a GitHub Repository Tool - Part 4
I've come a long way with the project and the end is in sight. Hopefully, we can wrap up building my GitHub repository tool today. At this point, I have a PowerShell class-based tool with several functions for working with the GitHub repository data. I have a rich object.
PS C:\> $in[10]
Name : PSReleaseTools
Description : 🚢 A set of commands for working with PowerShell 7.x releases.
LastUpdate : 8/29/2024 9:07:55 AM
Visibility : Public
DefaultBranch : master
LatestReleaseName : PSReleaseTools_v1.12.0
LatestReleaseDate : 6/4/2022 10:08:49 AM
LastPush : 8/4/2024 4:57:03 PM
StargazerCount : 112
WatcherCount : 11
URL : https://github.com/jdhitsolutions/PSReleaseTools
DiskUsage : 3175
ID : MDEwOlJlcG9zaXRvcnk3ODA1NTc4NA==
InfoDate : 10/10/2024 3:21:25 PM
UpdateAge : 43.05:12:53.9554832
isReleased : True
ReleaseAge : 860.04:11:59.9556128
IsEmpty : False
Since last time, I've extended this object even further.
More Type Extensions
One thing I had to do was revised the type extension that defined the ReleaseAge
property. If the repository didn't have a release, the previous code was throwing an error. I revised the definition to check for a null value using the PowerShell 7 ternary operator.
Update-TypeData -TypeName GitHubRepoInfo -MemberType ScriptProperty -MemberName ReleaseAge -Value {
($Null -eq $this.LatestReleaseName) ? $null : (Get-Date) - $this.LatestReleaseDate
} -force
I also realized that I could make the object even easier to use. I might want to run a command like this:
$in | where {$_.Visibility -eq 'public' -AND $_.IsReleased} | Sort-Object ReleaseAge | Select-Object ReleaseAge,name,LatestReleaseName,description -first 5
I can simplify this by adding custom boolean properties indicating if the repository is public or private.
Update-TypeData -TypeName GitHubRepoInfo -MemberType ScriptProperty -MemberName IsPublic -Value { $this.Visibility -eq 'Public' } -Force
Update-TypeData -TypeName GitHubRepoInfo -MemberType ScriptProperty -MemberName IsPrivate -Value { $this.Visibility -eq 'Private' } -Force
With this, I can reduce the amount of typing.
$in | where {$_.IsPublic -AND $_.IsReleased} | Sort-Object ReleaseAge | Select-Object ReleaseAge,name,LatestReleaseName,description -first 5
When creating tools, and especially custom objects, find ways to reduce the friction in using them. Another good example is the use of alias
properties. I have a few properties that are good candidates. Here's an example:
$in | where IsPublic | Sort StargazerCount -Descending | Select-Object Name,StarGazerCount,WatcherCount -first 5
Yes, I know I could use wildcards. But an alias feels a little cleaner.
Update-TypeData -TypeName GitHubRepoInfo -MemberType AliasProperty -MemberName Stars -Value StargazerCount -Force
Update-TypeData -TypeName GitHubRepoInfo -MemberType AliasProperty -MemberName Watchers -Value WatcherCount -Force
Compare the difference.
$in | where IsPublic | Sort Stars -Descending | Select-Object Name,Stars,Watchers -first 5
Even better, the output uses the aliases.
Property Sets
At this point, here's what the object looks like.
Thinking again about ease of use, are there situations where the user might want to specify a subset of properties on a regular basis? I'm thinking the following is a core set of properties that I want to use repeatedly.
$in | Select LastUpdate,Name,Visibility,URL,Description -first 5 | Format-Table -Wrap
But I'm lazy. I don't want to have to remember to type these properties every time. Instead, I can create a property set.
If I don't plan on using custom formatting, I can define a default property set.
Update-TypeData -Typename GitHubRepoInfo -DefaultDisplayPropertySet LastUpdate,Name,Visibility,URL,Description -Force
This gives me a type of default formatting.
PS C:\> $in[0]
LastUpdate : 10/10/2024 1:52:05 PM
Name : PSGalleryReport
Visibility : Public
URL : https://github.com/jdhitsolutions/PSGalleryReport
Description : A set of reports in PDF and Markdown format about recently published and popular modules in the PowerShell Gallery. The reports are generated by a
GitHub Action. This is NOT a PowerShell module.
But I plan on defining custom formatting, and there are other groups of properties I think I want to bundle together to save typing.
$in | Select Name,LatestReleaseName,LatestReleaseDate,ReleaseAge,Url
$in | Select Name,Stars,Watchers,Url,Description
To define these as property sets, I need to use an XML file. This can't be done with Update-TypeData
. But let me make this easy for you. Install the PSTypeExtensionTools module from the PowerShell Gallery. I wrote a command called New-PSPropertySet that will create the XML file for you.
New-PSPropertySet -Typename GitHubRepoInfo -Name ReleaseInfo -Properties Name,LatestReleaseName,LatestReleaseDate,ReleaseAge,Url -FilePath .\GitHubRepoInfo.types.ps1xml
You can also add property sets to the same file.
New-PSPropertySet -Typename GitHubRepoInfo -Name Counts -Properties Name,Stars,Watchers,Url,Description -FilePath .\GitHubRepoInfo.types.ps1xml -Append
To load these settings into your session, you can import the XML file.
Update-TypeData -AppendPath .\GitHubRepoInfo.types.ps1xml
The property sets are now available.
PS C:\> $in | Get-Member -MemberType PropertySet
TypeName: GitHubRepoInfo
Name MemberType Definition
---- ---------- ----------
Counts PropertySet Counts {Name, Stars, Watchers, Url, Description}
ReleaseInfo PropertySet ReleaseInfo {Name, LatestReleaseName, LatestReleaseDate, ReleaseAge, Url}
PS C:\> $in | where {$_.IsPublic -ANd $_.LatestReleaseDate.year -eq 2024 -And $_.IsReleased} | Sort ReleaseAge | Select-Object ReleaseInfo | Format-Table
This at least makes the Select-Object
command a little easier to type.
Here's the other property set in action.
$in | where IsPublic | sort Stars,watchers -Descending | Select -first 5 | Format-Table Counts -Wrap
I can't stress enough the importance of finding ways to make your code and output easier to use.
Formatting
Which leads to the final topic, formatting. I have defined a rich object with many properties. Most of them I don't need to see all the time. Instead I would like a default formatted display that presents as much useful information as possible. I can define a custom format file.
You've seen me create the format.ps1xml file before using the New-PSFormatXMLFile function from the PSScriptTools module
All I need is a reference object with values for any property I want to capture.
$in[10] | New-PSFormatXML -GroupBy Name -Properties LastUpdate,LastPush,LatestReleaseName,Description -FormatType List -Path .\GitHubRepoInfo.format.ps1xml
I then modified the XML file to customize the grouping and use ANSI formatting via $PSStyle.
You can see that I've added emojis and color highlighting based on the visibility property. Even though Windows Terminal renders URLs as hyperlinks, I also added a hyperlink to the name property so I can have a link and save space on the screen.
To use the file, I'll run Update-FormatData
.
Update-FormatData .\GitHubRepoInfo.format.ps1xml
Private repos will have the name displayed in red. I can click the name to open the URL. The list is colorized and formatted for easy reading with as much relevant information as possible.
Remember those property sets I created earlier? I can do something similar with the format file and created named views.
$in[10] | New-PSFormatXML -Properties Name,LatestReleaseName,LatestReleaseDate,ReleaseAge,Url -FormatType Table -ViewName ReleaseInfo -Path .\GitHubRepoInfo.format.ps1xml -Append
Reloading the format file adds the new view. You need to specify the appropriate format cmdlet and the view name.
In this example, I duplicated the property set with the named view, but that is not a requirement. You can create as many custom views as you need. I recommend that you document them because they are not easily discoverable otherwise.
Summary
I now have a pretty complete tool set. I can probably define a few more custom views. The likely next step is to turn this into a module, but I will leave that exercise to you. You can download a zip file from Dropbox with my code and XML files. One thing to take into account if building a module is a better way to handle the default export and import paths. I have hard-coded values for my computer. I would find a way to make it a user-configurable setting.
Was this series helpful? Did you learn anything new? If nothing else, I hope that my process and decision making were informative. These are things you should apply to any project you are working on.