1824 words
9 minutes
Enterprise Source Control

Source Control — Philosophy#

Version History#

  • Small incremental changes get checked in and preserved
  • Each complete unit of work should be a check-in point
  • Can see changes to the code over time
  • Can compare files to see differences
  • Can rollback to a previous version if a regression was found or design decisions have changed
  • Most of the time you only have a single branch

Release Control#

  • Branch per environment [Dev (main) | Stage | Prod]
  • Each branch up to Prod is used as a stepping stone in a promotion process
  • Should be validated in Dev before moving to Stage
  • Should be validated in Stage before moving to Prod
  • Never make changes in the Stage and Prod branches directly.
  • Cherry picking changes should be avoided if possible

When to Feature Flag#

If changes are expected to not release by the end of the sprint, feature flagging should be your next consideration so the new code path can be released during a normal release cycle and tested separate from the current flow


Using Git#

Commands#

clone#

git clone https://repository.git — Creates a local copy of a remote repository, downloading all files, branches, and commit history into a new directory on your machine. This is typically the first command you run when joining and existing project.

branch#

git branch — Lists all local branches in the repository, marking the currently checked-out branch with an asterisk. Add -a to include remote branches or -v to see the latest commit on each.

git branch branch_name — Creates a new branch with the given name pointing at your current commit, but does not switch you to it. Follow with a git switch branch_name to switch to it.

TIP

You can use the command git switch -c branch_name to create and switch in one operation.

git branch -D branch_name — Force deletes the named local branch, even if it has unmerged changes that would otherwise be lost. Use lowercase -d instead for a safer delete that blocks if the branch hasn’t been merged.

NOTE

Because we leverage squash merges into main the -d will always think that there are unmerged changes.

switch#

git switch branch_name — Switches your working directory to the specified branch, updating your files to match that branch’s latest commit. Add -c to create and switch to a new branch in one step.

status#

git status — Shows the current state of your working directory and staging area, including which files have been modified, staged for commit, or are untracked. It’s the go-to command for checking where you stand before staging, committing or switching branches.

add#

git add . — Stages all new and modified files in the current directory (and its subdirectories) for the next commit. Use with care, it can be quick to sweep in files you didn’t intend to include, so a git status check before hand is a good habit.

TIP

A clean .gitignore will be your first line of defense. If git status is consistently showing files you don’t want to track, node_modules/, build artifacts, .env files, etc., add them to .gitignore.

commit#

git commit -m "message" — Records your staged changes to the local repository with a short inline message describing what changed.

TIP

Keep messages clear and meaningful (e.g., “fix login redirect bug” rather than “stuff”)

For anything needing more detail, omit the -m flag to open your editor and write a full multi-line message with a subject line and body.

push#

git push --set-upstream origin branch_name — Pushes a local branch to the remote (origin) for the first time and links it to a remote tracking branch of the same name, so future git push and git pull commands work without specifying the remote and branch.

TIP

To skip this entirely on new branches, set git config --global push.autoSetupRemote true. With this enabled, a plain git push on a brand new local branch will automatically create the matching upstream branch on origin and link them, so you do not have to remember the --set-upstream flag.

pull#

git pull — Fetches the latest changes from the remote branch and merges them into your current local branch in one step. Run it before starting new work and before pushing to minimize merge conflicts.

NOTE

Under the hood it’s a git fetch followed by a git merge.

restore#

git restore file_name — Discards local changes to the specified file, restoring it to match the latest commit on your current branch. Add --staged to unstage a file without losing your edits, making it the modern replacement for git reset HEAD file_name. You can also combine it with --staged --worktree to fully revert a staged change.

NOTE

You’ll often see git checkout used for both branch switching and file restoration in older documentation, tutorials and forums. This double duty has caused enough pain that git switch and git restore were later introduced as safer more explicit alternatives.

fetch#

git fetch --prune — Downloads the latest refs and objects from the remote and simultaneously removes any remote-tracking branches in your local repo whose upstream counterparts no longer exist. This is a great housekeeping command to run periodically so git branch -a doesn’t accumulate stale references to branches that have already been deleted on the remote.


Leveraging .gitignore#

Set this file up early, preferably before your first commit. Once a file is tracked, adding it to .gitignore will not untrack it. You’ll need to git rm --cached file_name (which removes it from the repo but keeps it locally) followed by a commit. Catching this early avoids the need for cleanups later.

# visual studio nonsense
.vs/
*.suo
*.user
*.userosscache
*.csproj.user
*.vspscc
*.dbmdl
*.jfm
# artifacts
[Bb]in/
[Oo]bj/
[Dd]ebug/
[Rr]elease/
[Dd]ist/
[Ll]ogs/
[Pp]ublish/
[Tt]est[Rr]esult*/
[Pp]ackages/
*.nupkg
node_modules/
*.publishsettings
TIP

Run git status --ignored periodically to see what’s actually being ignored.


Local Git Setup#

Because we will be leveraging feature branches for getting code into main, it will be common practice to push your feature branch to the server for a PR and review. Applying the following setting will make this practice easier and less verbose.

git config --global push.autoSetupRemote true


Developer Workflow#

Feature Coding#

  1. Get the repository (naturally skip this step if you already have it)
Terminal window
git clone https://repository.git
cd repository_name
  1. Get the latest version and create a new branch
Terminal window
git switch main
git pull
git switch -c feature/cool-feature
  1. Work and commit loop
Terminal window
git status
git add .
git commit -m "descriptive commit message"
  1. Ready for review / deployment
Terminal window
git push
  1. Navigate to GitHub and open a pull request using your new branch
  2. Get review and perform a squash merge when approved. (Once the PR is approved the branch on the server will be automatically cleaned up)
  3. Reset for next feature / cleanup
Terminal window
git switch main
git pull
git branch -D feature/cool-feature

Long Running Branches#

If a feature branch lives more than a day or two, main may have moved forward and you will want to pull those changes in to avoid painful merge later.

Terminal window
git switch feature/cool-feature
git pull origin main

Code Promotion#

Code promotion is handled through PRs between main -> stage and stage -> prod. With these PRs, the source is enforced such that you cannot accidentally merge from some other branch into stage or prod.

You typically want to perform a merge commit so that the git history matches as you move between main, stage, and prod.


Git Gotchas!#

Changes on main…#

If you made changes on main only to remember that force pushes have been disabled, you can create a branch with your existing changes the same way you would otherwise, and your existing changes will carry over. From here you can commit and go through the rest of the process.

Working on the wrong branch#

If you have uncomitted changes, create a new branch, make your commit and work as normal. You can then discard any changes on main.

Terminal window
git switch -c feature/cool-feature
git status
git add .
git commit -m "..."
git push

You’re on main with commits that should be on a branch#

If you already have commits, the trick is to create a new branch, switch back to main and reset back to match the remote.

Terminal window
git switch -c feature/cool-feature
git switch main
git reset --hard origin/main
git switch feature/cool-feature

PR has a merge conflict#

Someone merged to main after you branched, and now your PR shows conflicts. Resolve them locally on your feature branch.

  1. Get the latest changes into your branch
Terminal window
git switch feature/cool-feature
git pull origin main
  1. Resolve any conflicts in your editor
  2. Push them back to the server. The PR will automatically update.
Terminal window
git status
git add .
git commit -m "..."
git push

Need to undo a commit that’s already pushed#

DO NOT git reset on anything that has been pushed. It rewrites history and will cause problems for anyone else who has pulled. Instead we want to use git revert instead, which creates a new commit that undoes the changes from a previous one.

Terminal window
git revert commit_sha
git push

Where did my changes go? (reflog safety net)#

If you ever thing you have lost commits due to a bad reset, rebase, or force delete, check the reflog.

Terminal window
git reflog
git checkout commit_sha
NOTE

Git keeps a log of every HEAD movement for roughly 90 days, so almost nothing is truly lost as long as the commit existed at some point. Find the SHA from before things went wrong, check it out, and create a branch from it to preserve the work.


GitHub Setup#

Branch Source Enforcement Workflows#

Create a repository named gh-workflows, or a name of your choosing, to store these global workflows.

Under the repository Settings > Actions > General #Access select the appropriate access level, either org or enterprise depending on your needs. This step is critical, otherwise other repositories will not be able to leverage these workflows, in addition to them not being available to select within the ruleset settings.

Workflows#

enforce-stage-source.yml

name: Enforce stage source
on:
pull_request: {}
jobs:
check-source:
runs-on: ubuntu-latest
steps:
- name: Validate source branch
run: |
if [[ "${{ github.head_ref }}" != "main" ]]; then
echo "::error::Stage PRs must come from main, not ${{ github.head_ref }}"
exit 1
fi

enforce-prod-source.yml

name: Enforce production source
on:
pull_request: {}
jobs:
check-source:
runs-on: ubuntu-latest
steps:
- name: Validate source branch
run: |
if [[ "${{ github.head_ref }}" != "stage" ]]; then
echo "::error::Production PRs must come from stage, not ${{ github.head_ref }}"
exit 1
fi

Rulesets#

These rulesets can be configured at the org or enterprise level as you see fit.

Protect Main Branches#

Target repositories: All repositories
Target branches: Default
Restrict deletions: Yes
Require pull request before merging: Yes
Required approvals: 0
Dismiss stale pull request approvals when new commits are pushed: Yes
Allowed merge methods: Squash
Block force pushes: Yes
Require code scanning results: Yes

Protect Stage Branches#

Target repositories: All repositories
Target branches: stage
Restrict deletions: Yes
Require pull request before merging: Yes
Required approvals: 0
Dismiss stale pull request approvals when new commits are pushed: Yes
Allowed merge methods: Merge
Block force pushes: Yes
Require workflows to pass before merging: Yes
Do not require workflows on creation: Yes
Enforce stage source
  1. Add workflow
  2. Select gh-workflows repository
  3. Select main branch
  4. Enter path to enforce-stage-source.yml file

Protect Main Branches#

Target repositories: All repositories
Target branches: prod
Restrict deletions: Yes
Require pull request before merging: Yes
Required approvals: 0
Dismiss stale pull request approvals when new commits are pushed: Yes
Allowed merge methods: Merge
Block force pushes: Yes
Require workflows to pass before merging: Yes
Do not require workflows on creation: Yes
Enforce production source
  1. Add workflow
  2. Select gh-workflows repository
  3. Select main branch
  4. Enter path to enforce-prod-source.yml file

Repository Level Settings#

To keep each repository tidy, enable the following setting. It will automatically delete feature branches upon successful merge of a PR.

Under Settings > General #Pull Requests
Automatically delete head branches: Yes

~ SK

Enterprise Source Control
https://www.kichka.dev/posts/source-control/
Author
Stephen Kichka
Published at
2026-04-28
License
CC BY-NC-SA 4.0