ForEach-Git.ps1

image

Problem

I have a set of independently deployable microservices, each with its own repo, and I need to make some mass changes. I can use an editor like VSCode to do mass, multiline changes, but doing all the git commands can be painful.

Solution

Create a script to execute commands on each repo sub folder. It’s called ForEach-Git with an alias of feg and is here. Check out the comment-based help, with examples. It runs on Windows and Linux, and requires Posh-Git to be installed.

Without any parameters, the default scriptblock returns objects with the folder names, branch, and status

/home/test/code> feg

Name     Branch Working
----     ------ -------
ServiceA main     False
ServiceB main      True
ServiceC main     False
ServiceD main     False

The script works on subfolders that have a .git subfolder, and always returns to your current directory. You usually pass in a scriptblock that runs in the repo’s folder. The scriptblock is passed the directory object, and gitstatus object, if you want to use them.

Some Common Usages

Create a new branch for a set of folders. You can pipe an array of names through, or use -HasWorking or -OnBranch to filter.

"ServiceA","ServiceB" | feg { git checkout -B 'test' } -ShowFolder

Commit all the changes on repos that have outstanding changes and push them

feg { git add . && git commit -m 'important' && git push } -HasWorking -ShowFolder

Merge main into all the repos currently in the test branch

feg { git fetch origin main:main && git merge main } -OnBranch test -ShowFolder

Push all your changes and call the New-PR script to create a PR in Azure DevOps (new blog coming)

feg { git push -u origin test && New-PR } -OnBranch test -ShowFolder

Once all your changes are complete, get main and delete the test branch

feg { git fetch origin main:main && git checkout main && git branch -D test } -OnBranch test -ShowFolder

Scenarios

For the test, lets create a local “remote” repo. Note using semicolon for directory creation to avoid it short-circuiting if it fails, but && for the others since they must all succeed.

mkdir /home/test/gitbak

feg { param($dir,$gitStatus)  mkdir "/home/test/gitbak/$($dir.name)" ; git remote add origin "/home/test/gitbak/$($dir.name)" && cd "/home/test/gitbak/$($dir.name)" && git init }

Initialized empty Git repository in /home/test/gitbak/ServiceA/.git/
Initialized empty Git repository in /home/test/gitbak/ServiceB/.git/
Initialized empty Git repository in /home/test/gitbak/ServiceC/.git/
Initialized empty Git repository in /home/test/gitbak/ServiceD/.git/

For a few folders let’s check out a branch, test. The ShowFolder switch shows the folder names as it processes them, and can help when resuming in the event of an error (as I’ll show below).

/home/test/code> "ServiceA","ServiceC","ServiceD" | feg { git co -B test } -ShowFolder

>>> 0 ServiceA folder
Switched to a new branch 'test'

>>> 2 ServiceC folder
Switched to a new branch 'test'

>>> 3 ServiceD folder
Switched to a new branch 'test'

Let’s set up a failure to show how to resume. First just update A with the remote.

/home/test/code> feg -Include "ServiceA" { git push -u origin test }
Enumerating objects: 9, done.
Counting objects: 100% (9/9), done.
Delta compression using up to 4 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (9/9), 633 bytes | 633.00 KiB/s, done.
Total 9 (delta 0), reused 0 (delta 0)
To /home/test/gitbak/ServiceA
 * [new branch]      test -> test
Branch 'test' set up to track remote branch 'test' from 'origin'.

Now let’s try pushing all on test. Note that A works, but C fails. The error messages says we can use -StartWith 1 to start with C.

/home/test/code> feg  { git push } -OnBranch test -ShowFolder

>>> 0 ServiceA folder
Everything up-to-date

>>> 1 ServiceC folder
fatal: No configured push destination.
Either specify the URL from the command-line or configure a remote repository using

    git remote add <name> <url>

and then push using the remote name

    git push <name>

WARNING: Error processing /home/test/code/ServiceC
WARNING: To processing again from there, add '-StartWith 1'
Exception: /mnt/c/code/ccc/Tools/PowerShell/ForEach-Git.ps1:118
Line |
 118 |  …                              throw "Non-zero exit code $LASTEXITCODE"
     |                                 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     | Non-zero exit code 128

We can fix that by setting the remote, but skipping A.

/home/test/code> feg  { git push -u origin test } -OnBranch test -ShowFolder -StartWith 1

>>> 0 ServiceA folder

>>> 1 ServiceC folder
Branch 'test' set up to track remote branch 'test' from 'origin'.
Everything up-to-date

>>> 2 ServiceD folder
Branch 'test' set up to track remote branch 'test' from 'origin'.
Everything up-to-date

This was a rather contrived example, but there are many times I’ve got half way through many repos and got an error and had to restart from where I left off since the commands couldn’t be re-run on the previous repos (e.g. delete a branch).