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.
TIPYou can use the command
git switch -c branch_nameto 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.
NOTEBecause we leverage squash merges into main the
-dwill 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.
TIPA clean
.gitignorewill be your first line of defense. Ifgit statusis 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.
TIPKeep messages clear and meaningful (e.g., “fix login redirect bug” rather than “stuff”)
For anything needing more detail, omit the
-mflag 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.
TIPTo skip this entirely on new branches, set
git config --global push.autoSetupRemote true. With this enabled, a plaingit pushon 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-upstreamflag.
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.
NOTEUnder the hood it’s a
git fetchfollowed by agit 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.
NOTEYou’ll often see
git checkoutused for both branch switching and file restoration in older documentation, tutorials and forums. This double duty has caused enough pain thatgit switchandgit restorewere 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/
*.publishsettingsTIPRun
git status --ignoredperiodically 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
- Get the repository (naturally skip this step if you already have it)
git clone https://repository.gitcd repository_name- Get the latest version and create a new branch
git switch maingit pullgit switch -c feature/cool-feature- Work and commit loop
git statusgit add .git commit -m "descriptive commit message"- Ready for review / deployment
git push- Navigate to GitHub and open a pull request using your new branch
- Get review and perform a
squash mergewhen approved. (Once the PR is approved the branch on the server will be automatically cleaned up) - Reset for next feature / cleanup
git switch maingit pullgit branch -D feature/cool-featureLong 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.
git switch feature/cool-featuregit pull origin mainCode 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.
git switch -c feature/cool-featuregit statusgit add .git commit -m "..."git pushYou’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.
git switch -c feature/cool-featuregit switch maingit reset --hard origin/maingit switch feature/cool-featurePR has a merge conflict
Someone merged to main after you branched, and now your PR shows conflicts. Resolve them locally on your feature branch.
- Get the latest changes into your branch
git switch feature/cool-featuregit pull origin main- Resolve any conflicts in your editor
- Push them back to the server. The PR will automatically update.
git statusgit add .git commit -m "..."git pushNeed 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.
git revert commit_shagit pushWhere 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.
git refloggit checkout commit_shaNOTEGit 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 sourceon: 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 fienforce-prod-source.yml
name: Enforce production sourceon: 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 fiRulesets
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