tl;dr:
Broken string interpolation:
dir -recurse -directory -force -filter .git | % { git -C "$($_.FullName)\.." gc }
That is, simply enclosing $_.FullName in $(...) inside the double-quoted string is sufficient to fix the problem (plus adding -Force to dir (Get-ChildItem) to make sure it finds the hidden .git subdirectories;
adding -directory to limit matching to directories and using -filter instead of -include is not strictly needed, but makes for much better performance).
Understanding the need for $(...) in double-quoted strings is important in general - see below.
For the task at hand, the above is a simple, robust, and efficient solution: targeting the desired directory is left to git via its -C option, without having to change the calling session's current directory.
Changing to a different directory before invoking a command:
4c74356b41's answer bypasses the problem by focusing on the 2nd part of the OP's question - how to change to a different location before invoking a command - while avoiding the broken string interpolation ("$_.FullName\.."). Their (first) solution is effective, however, because git recognizes a repository by its .git subfolder too, so the \.. part to reference the parent dir. is not needed.
Still, their first command has the side effect of changing the session's current directory, and the Start-Process cmdlet is best avoided for console applications such as git.
See the bottom of this post for an explanation and a more robust idiom.
Broken String Interpolation
Expanding $_.FullName inside "..." (double-quoted strings) will not work as-is: since you're accessing a property of the object referenced by the $_ variable, you must enclose your expression in the subexpression operator, $(...), as follows:
"$($_.FullName)\.."
As an aside: you actually don't need string interpolation at all in your case, because git also recognizes a repo by its .git subfolder, so the \.. component to refer to the parent dir. is not needed; a direct reference to $_.FullName is enough:
... | % { git -C $_.FullName gc }
How is "$_.FullName\.." expanded (interpolated), and why is it broken?
Pipeline variable $_ is expanded by itself, by calling the value's .ToString() method.
$_ is an instance of type [System.IO.DirectoryInfo] in this case, and calling .ToString() on it yields the mere name of the given directory, not its path (e.g., if $_ represents C:\Users\jdoe, "$_" expands to just joe).
.FullName\.. is interpreted as a literal part of the string.
Let's say $_ represents directory C:\Users\jdoe; "$_.FullName\.." is therefore expanded to the following, which is clearly not the intent:
jdoe.FullName\..
In other words: Inside a double-quoted string, $_.FullName is not recognized as a single expression, unless it is enclosed in $(...), the subexpression operator.
The corrected form of the string, "$($_.FullName)/..", then yields the expected result:
C:\Users\jdoe\..
Here are test commands you can run as-is that illustrate the difference:
dir -recurse $HOME | % { "$_.FullName\.." } | Select -First 2 # WRONG
dir -recurse $HOME | % { "$($_.FullName)\.." } | Select -First 2 # OK
For a full discussion of the rules governing PowerShell's string interpolation (expansion) see this answer of mine.
Additionally, a quirk in how PowerShell (Windows in general) processes filesystem paths may obscure the problem with the string interpolation in your case:
Appending \.. to a non-existing path component still results in a valid path - the two components effectively cancel each other out.
If we use the string-interpolation-gone-wrong example from above:
Set-Location jdoe.FullName\..
is an effective no-op, because PowerShell lexically concludes that you mean the current directory (given that jdoe.FullName\.. is a relative path), even though no subdirectory named jdoe.FullName exists.
Thus, in the context of using something like this:
... | % { Set-Location "$_.FullName\.."; git gc }
the Set-Location command has no effect, and all git invocations run in the current directory.
Changing to a Different Directory in Every Iteration of a Pipeline
For synchronous invocation of console (command-line) utilities such as git, the following technique is the best choice:
dir -recurse -directory -force -filter .git | % { pushd $_.FullName; git gc; popd }
Note how the git call is sandwiched between pushd (Push-Location) and popd (Pop-Location) calls, which ensures that the calling session's current location is ultimately not changed.
Note: If a terminating error were to occur after the pushd call, the current location would still change, but note that invoking external utilities never results in terminating errors (only PowerShell-native calls and .NET framework methods can generate terminating errors, which can be handled with try ... catch or trap statements).
Setting the Working Directory for a GUI App or New Console Window
Start-Process with its -WorkingDirectory parameter is the right tool in the following scenarios:
Note:
Start-Process is asynchronous by default, which means that it launches the specified command, but doesn't wait for it to complete before returning to the PowerShell prompt (or moving on to the next command in a script).
Add -Wait if you actually want to wait for the process to terminate (which typically happens when the window is closed).
It generally makes little sense to run console applications in the current console window with Start-Process -NoNewWindow -Wait (without -Wait, output would arrive asynchronously and thus unpredictably in the console):
The console application invoked won't be connected to the current session's input and output streams, so it won't receive input from the PowerShell pipeline, and its output won't be sent to PowerShell's success and error streams (which means that you cannot send the output through the pipeline or capture it with > or >>; you can, however, send a file as input via -RedirectStandardInput, and, similarly, capture stdout and stderr output in files with -RedirectStandardOutput and -RedirectStandardError)
Additionally, direct invocation is not only syntactically simpler (compare git gc to Start-Process gc -ArgumentList gc -NoNewWindow -Wait), but noticeably faster.
The only scenarios in which it makes sense to use Start-Process with a console application are: (a) starting it in a new console window (omit -NoNewWindow), (b) running it as a different user (use the -Verb and -Credential parameters), or (c) running it with a pristine environment (use -UseNewEnvironment).
Setting the Working Directory for a Background Command
Finally, Start-Job is the right tool for: