Saturday, October 17, 2026

Git: Preventing Automatic Merging of Text Files

For text files, a Git merge (say, a feature branch into main) automatically tries to merge individual files (.py, .cs, .cpp, .json, .xml, etc.). There are scenarios when it is better to force a manual merge for text files. The .gitattributes file allows the merge algorithm used to be configured. There may be some folders in a Git repo where automatic merging is desired and other folders where automatic merging should be verboten.

The standalone -merge option in a .gitattributes file forces a manual merge. An instance of a .gitattributes file containing -merge as the sole attribute value is as follows, where the * is a wildcard matching all files:

* -merge

A repo can contain multiple .gitattributes files controlling the merge behavior at different levels of the folder hierarchy. An example of such a topology is as follows:

AnyRep/
├── .gitattributes
├── Source/
│   ├── main.py
│   └── service.py
└── CapCutProjects/
    ├── .gitattributes
    ├── Project1/
    │   └── draft_info.json
    └── Project2/
        └── draft_info.json

Binary files are not subject to auto-merge (.dll, .exe, .jpg, .png, .zip, .mp4, .wav, etc.).

The documentation for using the merge keyword in .gitattributes can be found at Performing a three-way merge.

I used the following .gitattributes file for a repo containing CapCut projects (JSON files) because I do not want to permit auto-merge.

# -merge never auto-merge void -text files (CapCut project files are json/text)
* -text -merge

Why the -text option? That is a topic for a future blog.


Friday, October 16, 2026

Fixing Empty Folders Not Being Committed to Git Using .gitkeep

Git does not track empty folders, meaning they are not included in a push. If you create an empty folder, commit your changes, and push them to a remote repository, that folder will not exist when another developer clones or pulls the repository.

The stock solution is to create an empty file named .gitkeep inside each empty folder. Git tracks the .gitkeep file. This preserves the folder when the repository is pushed.

For example, where the audio and video folders are empty:

MyProject/
├── Resources/
│   ├── audio/
│   │   └── .gitkeep
│   └── video/
│       └── .gitkeep

The .gitkeep filename is a bog-standard convention. The .gitkeep filename is not part of the formal Git specification.

Real-World Example: Using Git to Manage CapCut Projects, Including Empty Folders

I encountered this problem while setting up Git repositories for remote video production teams using CapCut.

A stock CapCut project contains directories, such as the following:

ExampleCapCutProjectFolder/
├── qr_upload/
├── matting/
├── subdraft/
├── adjust_mask/
├── smart_crop/
└── Resources/
    ├── audioAlg/
    ├── videoAlg/
    └── digitalHuman/

In the standard CapCut project created by my team, the following folders were empty: qr_upload, matting, subdraft, adjust_mask, smart_crop, audioAlg, and videoAlg. CapCut will not open a project if these folders are missing, hence the issue being addressed. A cloned version of the repo must include the standard directory hierarchy.

The solution, as noted above, is to create a .gitkeep file in each empty directory:

ExampleCapCutProjectFolder/
├── qr_upload/
│   └── .gitkeep
├── matting/
│   └── .gitkeep
├── subdraft/
│   └── .gitkeep
├── adjust_mask/
│   └── .gitkeep
├── smart_crop/
│   └── .gitkeep
└── Resources/
    ├── audioAlg/
    │   └── .gitkeep
    ├── videoAlg/
    │   └── .gitkeep
    └── digitalHuman/

CapCut ignores the .gitkeep files, so using a .gitkeep file is a simple, albeit tedious, solution to the empty folder problem.

If only someone would write a blog on how to automate the creation of .gitkeep files in empty folders.


Sunday, October 11, 2026

Validating Git Branch Names in Azure DevOps Pull Request Validation Pipelines

I was given the requirement to deploy individual work items (stories and bugs) to PPE and Prod. Business analysts (BAs) would approve a story, and that story would be deployed. The client used a single-branch Git strategy: main was the only branch, and deployments to both PPE and Prod used artifacts from a specific build of main.

After reviewing the standard Git branching strategies, I chose GitFlow to support this workflow. There was one issue: my developers would finally have to follow the branch naming standard, which required the work item ID for the story or bug to be embedded in the branch name. The branch naming standard is as follows:

bug/<username>/<work item id>-<work item title as a branch name>
story/<username>/<work item id>-<work item title as a branch name>

Azure DevOps Git triggers no validation event when a branch is pushed to origin. Within the workflow, the first actionable event where a branch can be validated is when a pull request is created (see Set build validation). The construct used is an ADO pull request validation pipeline. This type of pipeline is associated with the pull request’s target branch (main). It initially runs against the PR source branch when the PR is created. It runs again whenever the source branch is updated.

This post is not a tutorial on how to write a YAML pipeline and a corresponding Python or PowerShell script to validate a branch name. The focus is on the steps to configure a pull request validation pipeline. These are as follows:
  1. Go to Repos → Branches.
  2. Find the main branch.
  3. Select More options (⋯) → Branch policies.
  4. Under Build validation, select Add (+).
  5. Select the validation pipeline and save the policy.
As Microsoft’s documentation demonstrates in Set build validation, the pull request validation pipeline dialog (a.k.a. Add build policy) is just a mechanism to associate a pipeline with the main branch so it triggers when a PR is created targeting main:



Azure CLI creates a pull request validation pipeline using the following command:

az repos policy build create 
                         --blocking {false, true}
                         --branch
                         --build-definition-id
                         --display-name
                         --enabled {false, true}
                         --manual-queue-only {false, true}
                         --queue-on-source-update-only {false, true}
                         --repository-id
                         --valid-duration
                         [--branch-match-type {exact, prefix}]
                         [--detect {false, true}]
                         [--org]
                         [--path-filter]
                         [--project]
                         [--subscription]

Appendix A: Pre Push Git Hooks

Git’s pre-push hook (see githooks pre-push) provides a client-side mechanism for validating branch names before they are pushed to the remote repository. It is technically feasible as a DevOps engineer to author a script that creates a pre-push Git hook to validate the branch name.

Most DevOps engineers know it is nearly impossible to get a development team to comply with per-laptop mandates like pre-push Git hooks. Even if there were ninety-nine percent compliance, a pull request validation pipeline would be required to ensure the branch name follows the standard.


Monday, October 5, 2026

Azure DevOps Git Repositories: Limiting Individual File Sizes

My content teams check CapCut video projects into ADO Git repos. Our videos, which are well over one GB, are kept in blob storage. As a defensive measure, a maximum file size has been assigned. This prevents video files from accidentally polluting my CapCut video project Git repos.

The steps to configure the individual file size limit for all Git repos in an ADO project are as follows:

  1. Open your project in Azure DevOps.
  2. Select Project settings in the lower-left corner.
  3. Under Repos, select Repositories.
  4. Select All Repositories.
  5. Open the Policies tab.
  6. Under Repository Policies, locate Maximum file size.

The default repo policy is as follows, where Maximum file size is set to Off:


Once enabled, the Maximum file size can be specified via the dropdown shown below:


Remember, this applies to all Git repos in an ADO project. This policy can also be configured per Git repo.


Sunday, October 4, 2026

Azure DevOps Project Wiki: Collapsible Images

In an Azure DevOps Project Wiki, expandable images can be incorporated. When the image is collapsed, it looks as follows:






Expanding the "Show screenshot" text displays the image within the Wiki:



The underlying text that supports the feature is a combination of Markdown and HTML:

4. Set the main track audio to mute
   <details>
   <summary>Show screenshot</summary>

   ![Insert Media](Images/02MainTrack.png)

   </details>

The <details> HTML element creates a section that can be expanded and collapsed. Everything between the opening <details> tag and closing </details> tag is hidden when the section is collapsed.

The <summary> element defines the text that the user clicks to expand or collapse the section:

<summary>Show screenshot</summary>

In this example, the text displayed in the Wiki is "Show screenshot."

The image itself uses normal Markdown image syntax:

![Insert Media](Images/02MainTrack.png)

Insert Media is the alternative text for the image, while Images/02MainTrack.png is the relative path to the image stored in the Wiki repository.

The important point is that Markdown can be placed inside the <details> element. This allows the standard Markdown image reference to be wrapped inside an HTML collapsible section.

The result is a Wiki that remains concise for experienced users while allowing any user to expand individual steps and view the accompanying screenshot.

Saturday, October 3, 2026

C# Dependency Injection: What Happens When the Same Service Is Registered Twice?

In a .NET 10 web service, I recently found ILogger registered twice in Program.cs: 

services.AddSingleton<ILogger>(sp =>
    new ControlPlaneLogger(
        sp.GetRequiredService<IArchitecturalPlanesFactory>()
          .GetControlPlaneLogger()));
services.AddSingleton<ILogger>(sp =>
    new ControlPlaneLogger(
        sp.GetRequiredService<IArchitecturalPlanesFactory>()
          .GetDataPlaneLogger()));

No class in the project injected ILogger, so I wanted to know what happens when the same interface is registered more than once. The duplicate registrations in Program.cs did not throw an exception.

I unearthed the following from Microsoft's documentation (Service registration):

In plain English, the last registered ILogger is the one injected into a class constructor. In the original duplicate-registration example, the bolded registration is the one DI will resolve for ILogger:

services.AddSingleton<ILogger>(sp =>
    new ControlPlaneLogger(
        sp.GetRequiredService<IArchitecturalPlanesFactory>()
          .GetControlPlaneLogger()));
services.AddSingleton<ILogger>(sp =>
    new ControlPlaneLogger(
        sp.GetRequiredService<IArchitecturalPlanesFactory>()
          .GetDataPlaneLogger()));




Friday, October 2, 2026

Claude Code Blurts Out "Footgun": A Legitimate Software Anti-Pattern

While working with Claude Code, Sonnet 5.x diagnosed some C# I inherited as containing a "footgun." I had never heard the term footgun before. I mean, the most lethal footwork I'd seen was Rosa Klebb’s poison-tipped shoe blades immortalized in 'From Russia with Love.'

In modern computer science nomenclature, a footgun is a design that is technically valid but makes it unnecessarily easy for a developer to make a mistake.

A close-to-real-world example of this is as follows:

ILogger dataPlaneLogger =
           _architecturalPlanesFactory.GetDataPlaneLogger();

The factory method indicates that the logger is Data Plane specific. But the returned type is ILogger, which for all intents and purposes could be anything (log to a UDP port, log to Cloudflare R2 blob store, log to App Insights).

The factory makes the ambiguity explicit:

ILogger GetDataPlaneLogger();
ILogger GetControlPlaneLogger();

After that assignment, the type system no longer knows that this is the Data Plane logger. A variable named dataPlaneLogger cannot enforce that it was returned by method GetDataPlaneLogger. It is just another ILogger.

This is exacerbated by dependency injection. Consider a .NET 10 / C# class that requires a logging sink for the Control Plane and a logger sink for the Data Plane:

public class PlaneService(
                 ILogger controlPlaneLogger,
                 ILogger dataPlaneLogger)
{
}

The parameter names communicate intent to a human, but dependency injection resolves both services as the same type: ILogger. The primary constructor has two parameters of the same type, so there is no type-level distinction between logging to the Control Plane and logging to the Data Plane.

This ambiguity is an example of a footgun.