Source control is a must when it comes to development. It doesn’t matter if you’ve got a large team of developers or if you’re the only one. You need to set up source control. For my own pet projects, I use Subversion. Simple to set up and simple to use, especially with TortoiseSVN. I’m not going to go into a long intro on how to set up and use Subversion or TortoiseSVN. There are many quickstarts kicking around the web on how to do this. You can google those as well as I can. What I am going to discuss is initial repository setup.
I usually create a Subversion repository per project. If I ever need to move a repository to another computer, I simply copy the directory containing the repository. No complicated export/import scripts required. Same is true with backup/restore. If something goes horribly wrong (which it never has, but just in case), I can retrieve the source repository from backup without affecting any other repository. The standard structure within a repository includes a number of project-level directories, including trunk, tags, and branches:
Trunk is where mainline development occurs. Most of the time, I will be working from svn://Eddings/Sample/trunk. When you create a release, I want to tag it, which involves making a copy of the files in trunk (or elsewhere) to the tags directory. For instance, using TortoiseSVN, I would Branch/Tag… to svn://Eddings/Sample/tags/v1.0. (N.B. Subversion uses a copy-on-write mechanism so that I’m only storing one copy of a file, even if I tag/branch it multiple times. It also uses a binary differencing mechanism to keep the repository small. So if I add a 10 MB binary file to my repository, change a few bytes, and push my changes back into the repository, the space occupied by the file will be 10 MB plus the few bytes of changes plus some versioning information.) Branches is exactly the same as tags, except it is typically used as temporary scratch space for changes that will later be merged into trunk or tags. For instance, let’s say I released v1.0, which is tagged. I am working on v2.0 in trunk, but I need to make a bug fix in v1.0. I branch v1.0 into branches/v1.0-Remediation and make the fix. When I’m ready to release, I tag branches/v1.0-Remediation as tags/v1.01 and delete branches/v1.0-Remediation. I will probably reverse integrate the bug fixes into the trunk.
Typically I also point my new repository to a globally-maintained user list. (Unfortunately Subversion doesn’t integrate with Windows auth out-of-the-box. I believe you can do it, but you need to run Subversion under Apache. More trouble than it’s worth for a single developer, in my opinion.) So there’s another manual step…
The main take-home message is that when I create a new repository, there is a bunch of initial setup that needs to be done. Why not automate it? I wanted to learn PowerShell and this seemed like a good mini-project to do it on. This is based on my own exploration of PowerShell and there might be better ways of doing things in PowerShell. So your mileage may vary. Don’t take this as gospel or best practices, but as one man’s fumblings through learning PowerShell.
Why PowerShell?
First question is why do we have scripting environments in the first place. Usually it’s because we need to glue together a bunch of commands to do something more complex. The complexity doesn’t warrant a full-fledged application and we want to be able to change it easily. Scripting to the rescue. It’s the main reason we’ve had the Windows Command Shell and the Windows Scripting Host on the Windows platform and bash, ksh, tcsh, … on Unix platforms.
Next question… Why PowerShell and not something with more sex appeal like Ruby? PowerShell has an attraction for me because it targets the .NET platform. 90% of learning a new environment is learning the libraries. PowerShell lets me leverage my .NET knowledge in a scripting environment. Maybe I’ll learn Ruby one day, but Ruby is a one-trick pony in my opinion — specifically Ruby on Rails. RoR is one hell of a trick for web applications, but I’m not ready to learn a whole new set of libraries just to write web apps. I don’t like web applications that much!
Just like bash, ksh, tcsh, and other command shells before it, PowerShell lets me glue together simpler commands to create complex scripts. The Unix shells did this by having standard mechanisms to pipe the results of one command to the input of another, but everything is text. This means that each command requires its own parsing routines to handle the incoming text. PowerShell communicates between cmdlets using .NET objects. No more dealing with raw strings. Now I can examine objects, query them using reflection, or use them to perform actions. Cool stuff!
Getting Started with PowerShell
There are a number of good references for PowerShell that you should definitely keep close at hand.
- PowerShell Cheat Sheet (docx or pdf)
- PowerShell Quick Start
I’m not going to walk through all the features of PowerShell as others have already done that. What I will do is discuss how I solved different pieces of the puzzle in building a script to create a new Subversion repository. Let’s look at the script piece-by-piece.
Execution Policy
Before we can execute a PowerShell script, we must modify execution policy. By default, PowerShell ships in a locked-down mode that prevents scripts from running. You can execute cmdlets at the PowerShell prompt, but you can’t execute “.ps1” files containing scripts. To enable running script files, open a PowerShell prompt and execute:
Set-ExecutionPolicy unrestricted
Note that PowerShell features cmdlet completion using the tab key. So typing set-e[TAB] will auto-type the rest for you. If there are multiple cmdlets with the same name, you can tab multiple times to cycle through the options. You can even use wildcards to pattern match cmdlets and cycle through matching cmdlets using tab.
CreateSvnRepo.ps1
PowerShell code is in green. Discussion is in black. A discussion of implementation details are after each section of code. I’m not going to discuss the purpose of each section as that is noted in the comments heading each major section.
# CreateSvnRepo v1.2 # Copyright © 2007 by James Kovacs # All rights reserved. # THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF # ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED # TO THE IMPLIED WARRANTIES OF MERCHANTIBILITY AND/OR FITNESS FOR A # PARTICULAR PURPOSE.
Just an informational header. Comments in PowerShell are denoted by the hash (#) sign.
# Check usage If($args.Length -ne 1) { Write-Host "Usage: .\CreateSvnRepo.ps1" Write-Host " where RepoName is the name of the new repository" Write-Host "N.B. RepoName cannot accept a file path or url." Write-Host " CreateSvnRepo assumes that svn and svnadmin are in your path." Exit(0) }
We need to check the command line arguments and possibly print usage information. The script expects to be called like this:
.\CreateSvnRepo.ps1 <RepoName>
where RepoName is the name of the new repository.
Variables in PowerShell are prefixed by $. There are a few default variables such as $args, which is the command line arguments. (N.B. Like in .NET, the executed script is not included in the list of arguments.) $args is a string[] and we can check its Length property, just like in C# or VB.NET. Comparison operators in PowerShell do not use conventional operator syntax, but instead use “dash” syntax. For instance, -ne is not equal, -eq equal, -gt greater than, -ge greater than or equal, -lt less than, -le less than or equal, etc. There are also bitwise, matching, and type comparison operators as well as case-insensitive versions where appropriate. See Comparison Operators in the PowerShell Quick Start for a full list.
Write-Host serves the same purpose as Console.WriteLine. Notice that cmdlet calls do not use parentheses to denote arguments as this matches the flavour of calling commands in scripting environments. Also note the standard syntax used for cmdlets, which is VERB dash SINGULAR NOUN. Even if you expect multiple items, the singular noun is used. For instance, to get a list of running processes, execute Get-Process. Cmdlets are case-insensitive.
# Set up variables needed by script $newRepoName = $args[0]
Variables are created when they are assigned. We do not have to explicitly set the type of a variable, though we can by prefixing the variable with [type]. For example, [int]$i = 42.
$currentWorkingDir = (Get-Location).Path.Replace("\", "/")
Get-Location is a cmdlet that returns the current working directory as a System.Management.Automation.PathInfo object. To execute the cmdlet and get its result, we wrap the cmdlet in parentheses. Without the parentheses, PowerShell assumes we’re looking for a cmdlet or executable named Get-Location.Path.Replace. (This is because executables often have names such as DoStuff.exe.) Once we have a PathInfo object, we can examine its Path property. Notice that the Path property returns a string and we can perform normal .NET string operations on it, in this case changing backslashes to slashes since svnadmin (the Subversion command for creating repositories) uses Unix-style path separators.
Speaking of Unix, PowerShell includes a lot of Windows and Unix aliases. Aliases are names that execute cmdlets, possibly with parameters. For instance, both “dir” and “ls” actually execute “Get-Item *”. An alias for Get-Location is “pwd” or print working directory, which should be familiar to all Unix-heads. Get-Alias lists all the currently defined aliases and Set-Alias allows you to add or modify an alias.
$newRepoSvnPath = "file:///$currentWorkingDir/$newRepoName"
There are two types of strings in PowerShell, expanded and non-expanded strings. Single-quotes are used to denote non-expanded strings, similar to string literals in C# using @”No special characters here”. Double-quotes are used to denote expanded strings. Any variables in the string are replaced with their value. So if $fortyTwo = 42, then ‘The answer to life, the universe, and everything is $fortyTwo’ has the value The answer to life, the universe, and everything is $fortyTwo whereas “The answer to life, the universe, and everything is $fortyTwo” has the value The answer to life, the universe, and everything is 42.
$repoConfigFile = Join-Path $newRepoName 'conf\svnserve.conf'
Join-Path performs the same function as System.IO.Path.Combine in that it saves you from having to worry about whether the strings have directory separators in the correct places.
$tempPath = [System.IO.Path]::GetTempPath() $tempDirName = [System.IO.Path]::GetRandomFileName()
Here I need to access some static methods for which there is no equivalent cmdlet (as far as I know). We identify the type using square brackets followed by :: and the name of the static method.
$workingCopy = Join-Path $tempPath $tempDirName $dirNames = 'branches', 'tags', 'trunk'
The only new thing here is that we’re creating an array of strings. Nothing special, just comma-separate the objects and assign them to a variable.
# Ensure that we were passed only a repository name and not a path $invalidChars = [System.IO.Path]::GetInvalidFileNameChars() If($newRepoName.IndexOfAny($invalidChars) -ne -1) { Write-Host "You must specify a valid repository name. It cannot be an absolute or relative path." Exit(-1) }
More of the same, except now we’re calling a method on $newRepoName, which is of type System.String. This is a standard .NET method call with parameters. So we use parentheses around the arguments, as you can see in the call to IndexOfAny. This is the main difference between calling cmdlets and calling methods on objects.
# Ensure that repository doesn't already exist If(Test-Path $newRepoName) { Write-Host "Error: Repository, $newRepoName, already exists." Exit(-1) }
Here’s a new cmdlet, Test-Path, that will let us know if the path already exists or not.
The help system in PowerShell is quite useful. You access via Get-Help (or the shorter alias, help). Get-Help cmdlet will give you information on a command. Get-Help Get-Help gets help on the help system itself! Get-Help accepts wildcards. So if you want to find out everything you can do with Paths, type Get-Help *-Path. If you want to find everything you can Set, type Get-Help Set-*. This is an excellent way to poke around and find out more about available cmdlets.
# Create directory to hold the new repository Write-Host "Creating new repository: $newRepoName" New-Item -path . -name $newRepoName -type directory
We’re using the New-Item cmdlet, but this time we’re using named parameters. New-Item can be used to create directories, files, registry keys, and more. PowerShell uses a provider model. So as long as there is an appropriate provider, you can interact with any information store. Anyone can create a provider to extend the functionality of PowerShell. For instance, people are implementing (or have implemented) PowerShell providers for Subversion, Newsgator, SharePoint 2007, …
If(!(Test-Path $newRepoName)) { Write-Host "Unable to create directory, $newRepoName. Verify that you have permission to create this directory." Exit(-1) }
We can use ! or -not to negate a boolean value. Here we want to make sure that we successfully created the directory.
# Create the repository svnadmin create $newRepoName
Svnadmin is an executable that ships with Subversion. It is used for creating repositories, among other things. I execute it just like I would from the command line. The only trick is that I’m passing it the value of a PowerShell variable.
# Clear repo configuration directory $configFiles = Get-ChildItem -path $repoConfigDirectory ForEach ($configFile in $configFiles) { Remove-Item -path $configFile.FullName }
# Overwrite configuration file to point to global password directory
"[general]`r`nanon-access=none`r`npassword-db=../../passwd`r`n`r`nrealm=Default`r`n" | Out-File -filePath $repoConfigFile -encoding ASCII
Now we’re entering territory that is usually reserved for Perl-heads. (If you’ve ever read Perl code, you’ll know what I’m talking about.) I’m creating an expanded string (denoted by double quotes) that includes escaped character sequences, which are denoted by the back tick (`). I find the back tick a bit odd to read, but it has the advantage that you don’t have to escape slashes, which are used as path separators. So `r`n is a carriage return/line feed, equivalent to \r\n in most curly-brace languages. Next we write the string to a file by piping the string to Out-File. I set the encoding to ASCII since svnserve.exe doesn’t like Unicode encoding, which is the default. If you want to write Unicode text, you can out the output redirection operator (>). If you want to append to the file rather than overwrite, use the >> operator.
# Create a temporary working copy so we can commit initial setup in one go Write-Host "Creating temporary working copy in $workingCopy" New-Item -path $tempPath -name $tempDirName -type directory svn checkout --non-interactive $newRepoSvnPath $workingCopy
Nothing interesting here. We’re just checking out a working copy into a temporary directory. Svn.exe is the main command line tool for Subversion that allows us to perform checkouts, updates, commits, branches, and a myriad of other operations. I typically use TortoiseSVN for performing these operations, but svn.exe is good when you need to script things. There are numerous ways I could have interacted with Subversion (Scott Hanselman has some other suggestions), but I wanted the one with the least dependencies. (i.e. You just need PowerShell and Subversion installed.)
Write-Host "Creating directories in working copy" ForEach ($dirName in $dirNames) { $path = Join-Path $workingCopy $dirName svn mkdir $path }
The only thing new here is the ForEach keyword, which does what you’d expect and iterates through the string[] of $dirNames.
# Commit changes to the repository Write-Host "Committing changes" svn commit $workingCopy --message "Initial repository setup"
Once again, we’re just executing svn.exe to commit our newly created directories to the repository.
# Perform cleanup Write-Host "Cleaning up working copy" Remove-Item -path $workingCopy -recurse -force
Lastly we clean up the mess of temporary files and directories that we created.
So that’s it. A simple PowerShell script that quickly creates a new Subversion repository for whichever pet project I’m working on. I honestly haven’t even scratched the surface of what PowerShell can do. Hopefully this gives you some ideas and a starting place for your own PowerShell scripts. Honesty the biggest benefit of PowerShell for .NET developers is that it lets us seamlessly leverage the .NET Framework. Not having another API to lug around in my head is a good thing and makes me more productive right away.
Feel free to modify the script to your heart’s content to suit your own environment. The full script can be found here.
UPDATE: Corrected the encoding of the svnserve.conf to use ASCII encoding.