DEV Community

MartinJ
MartinJ

Posted on • Edited on

NgSysV2-5.2: An Introduction to Git

This post series is indexed at NgateSystems.com. You'll find a super-useful keyword search facility there too.

Last reviewed: Feb'26

1. Introduction

In British slang, a Git is an abusive term for a sullen, contemptible person. As far as is known, Linus Torvalds never intended GIT to be an acronym, so one can only assume that the development of this world-leading version control software gave him a particularly rough time!

Words of abuse may pass your own lips from time to time, when you are using Git, but this is an invaluable tool, one now central to modern IT practice. Git is a version control system designed to manage the entire history of a software project. What kept Linus going? Let's find out.

When you're new to IT and are still busy creating your first VSCode project, the last thing on your mind will be turning that project into a Git repository (whatever that might be!). But with experience, you'll realise that managing source code is quite a challenge. You will make changes, find yourself in a mess and want everything to return to a stable state. Hmm. How are you going to arrange that?

Post 1.1 mentioned VSCode's "timeline" facility. This lets you view a time-stamped list of "saves" for a file and enables you to select any of them to replace the current version. This is fine in its own way, but has obvious limitations - for example, what if you wanted to reset a whole bunch of interrelated files to a particular point? Git, by contrast, is "timeline" on steroids (and then some!)

Git's "repository" mechanism wraps a project's files up in a framework that tracks changes between "commit" points. A "commit" lets you "sign off" on a collection of file changes as a "recovery" point for your project. Then, if things go badly wrong, you can use Git to roll back the entire project to whatever commit point you choose. Git helps you answer "how did this software change, when did it change, and why?" A "commit" is more than a simple file change; it is a recorded step in a project's evolution. Each commit represents a meaningful change, such as fixing a bug, adding a feature, or improving behaviour. Over time, commits provide a structured history of the software's development.

Git also provides a "push" feature that lets you create and maintain a copy of your local project on the web. With this in place, your carefully crafted project won't vanish into history when you leave your laptop on the bus! Typically, you would use this to create a secure backup "recovery" copy of your code when you deploy a new version of your webapp. In many workflow designs, a Git push triggers a deployment.

But in describing the "remote repository" concept in this way, I'm grossly understating its significance. In the world of systems engineering, the remote repository is the point through which large teams of developers, each operating on a local copy of the project code, coordinate their work. Git software provides the means of imposing order on what would otherwise be a nightmarish struggle. When Git is in control and the remote repository reflects the source of the production system, developers always know exactly what code is running and how it came into being.

A description of these "team" arrangements is well outside the scope of this post - the Pro Git book would be an excellent place to take you to a higher level. For now, here are detailed instructions for the things you are most likely to want to do as a beginner:

  • Turn the code for an existing local project into a repository and create a commit point
  • Recover to a commit point* Create a remote repo on the web and synchronise this with a local repo.
  • Create a remote Git repo on the web and synchronise this with your local repo.
  • Create "branches" on both your local and remote repositories to isolate your production code
  • Forward a "hot-fix" on "main" into "dev" using "git merge"
  • Create standardised Git workflows to safeguard your ongoing development cycle

2. Turning the code for an existing project into a local Git repository and creating a commit point

This is quite a complex procedure, but if you take it step by step and use ChatGPT to resolve any problems, you should be fine. Here we go:

  1. First of all, you'll need to install Git software on your local machine. You'll find instructions for this at Git Downloads. The library code that this procedure installs on your machine lets you use both "raw" Git commands in terminal sessions and "packaged" Git tools in VSCode. In this post, I'll be using "raw" commands. Horrible as these are, I think they give you a better sense of what's going on than VSCode's built-in tool.

  2. Now open a terminal session for your project and type the command:

    git init
    

    This will respond by telling you that an empty git repository has been established in your project. If you look at your project in Windows Explorer, you'll find that you can actually "see" the repository in your project folder - it takes the form of a .git folder in the project root. This folder contains all the information required to version-control the files in your project, together with a log that stores your commit history. If you ever get into a total mess with your Git install and feel you need to start over, all you need to do is delete this folder!

  3. At this point, although you've got a repository, Git isn't actually doing anything useful. None of your files is currently "tracked" by the version control system - that is to say that Git doesn't know which files you want it to manage yet. To request that Git tracks all your files, you can use a git add . command. But first, note the following two warnings.

    You need to think carefully before you ask Git to track all your files, because you are likely to eventually want to use a server-based remote to back up your local repository. It's important that this doesn't reveal security keys such as your Firebase Project and Service Account Keys. Google patrols GitHub (the website where remote repositories are held), looking for repositories (even private ones) that contain security keys. They will "cane" you if they find you own one. Ensure that any files containing such items are included in your project's .gitignore file. Also, when working in VSCode, it is recommended that you disable its Smart Commit feature. Use File > Preferences > Settings to search for Smart Commit and disable it if it is enabled. As mentioned earlier, VSCode provides powerful shortcut tools for managing Git repositories, but these can obscure your understanding of how basic Git commands work.

    Warnings duly noted, submit your add command:

    git add . 
    

    Ignore any warnings about line-ending inconsistencies here. You can confidently leave VSCode and Git to sort this out between them. Your files are now tracked by Git.

  4. Now create a commit point for your local repository with a second new Git command:

    git commit -am "Your commit message"
    

    Here, the "Your commit message" field is a unique tag that identifies the state of your repository at this point and enables you to restore it later, if required. Writing "commit messages" is an art form. You're forced by the system to keep the first line of the message short - 50 characters max. A typical commit statement would look like git commit -am "Stable login added". The "-a" flag here tells git to include all tracked files that have been modified or deleted, and the "m" flag tells it to label the commit with the supplied message. A useful command that gives you a simple list of all the commits you've made to your local repo is git log --oneline.

3. Recovering to a commit point

This is where things start to get useful. Try a little experiment. Make a few random changes to several files in your project and save them. Then, imagine you've changed your mind and want to revert your project to its initial state.

  1. Type the following into your terminal session:

    git restore .
    

    Now, check your project. Yes, all the changes made since your last commit have been washed away - as before, the "." option has told Git to apply the command to all files. Wonderful!

  2. The simple "restore" command introduced above restores your project to the most recent commit. How would you proceed if you wanted to restore an earlier commit? First, you need to find the "short hash" - the code that Git itself uses to identify commit points. There are several ways of doing this, but since you're using terminal sessions here, I recommend that you type in the following:

    git log --pretty=format:"%h : %s"
    

    The result will be a list of all your commits. Individual entries here will look like: c9f6037 : Stable login added. The c9f6037 bit here is the "short hash" for the full 40-character commit identifier. You can now restore all files and folders in your project to the "Stable login added" commit point with the following command:

    git restore --source c9f6037 .
    

    Other forms of the command are available to restore selected elements. Ask ChatGPT for guidance if you find you need it.

4. Creating a remote Git repo on the web and synchronising this with your local repo.

As suggested earlier, creating a remote repo on the web will provide a secure backup for your local work. This will involve some investment of your time, but I can guarantee that the time will come when you'll be glad you made it.

  1. To get started, you first need to create a GitHub account. GitHub is a developer platform owned and operated by Microsoft. You can create a (free - thank you, Microsoft) account at Github Home Page, but I'm not going to describe this procedure here because it's a bit complicated and may change in future. Ask ChatGPT for advice if you get into trouble.

  2. Once you're through this and logged into GitHub, click your profile icon, select "repositories/your repositories" (an empty list at present) and click the "New Repository" button. Give your repository a name - you'll probably want to choose something based on your VSCode project name - and tell GitHub whether it is to be public or private. If you select public, others can view it on the web and "clone" it (i.e., download it) as a local repository on their devices. You'll probably want to select "private" at this stage - you can always change your mind later. Decline creating .gitignore or readme files too - you'll complicate things enormously if you add anything to your new remote repo at this point. You can also safely ignore the offer to create a license. For now, simply click the green "Create repository" button at the bottom of the page. This will create a remote repository that is now waiting for you to populate it with a copy of your local project.

  3. Back in VSCode, you now need to link your local repository to your new remote repository and "push" your local content into it. You do this with a series of Git commands submitted via a terminal session. To save your typing, GitHub will have provided the following default script for you to cut and paste:

    git remote add origin https://github.com/mygitaccount/myreponame
    git branch -M main
    git push origin main
    

    In this script:

    • The "add origin" command links your local repo to the "remote" at "https://github.com/mygitaccount/myreponame".
    • The "branch -M main" bit forcibly renames your local repo as "main". This post hasn't introduced you to the concept of "branches" yet, so just park this for the present. It will make sense shortly.
    • The "push" command copies the content of your local repo to a GitHub version with a branch named "main".

    You can confirm this by clicking "myreponame" on GitHub's "My Repositories" page and exploring its file hierarchy. You should find that the storage here mirrors your local repo.

  4. With your remote repo in place as a remote copy of your project source, you can now experiment with the procedure for maintaining it. Repeat your previous exercise: make a few changes to some testbed project files and use VSCode to commit them, as described earlier, with a command such as git commit -am "New Changes added". Now push your local repo to its linked remote repository with the following command:

    git push origin main
    

    A visit to your repository's GitHub page should confirm that the changes have been received. Note that this is not just a copy of the latest state of your project's files - GitHub now contains the full history of your project. In a team development context, GitHub provides a shared space for developers to coordinate their work.

If you want, you can leave things here - you now have a secure backup copy of your project's code and a mechanism for maintaining it. But unless you're developing something purely to satisfy your own interests, there's a lot more to think about. In a high-stakes, commercial environment, the "pipeline" that deploys development work into production storage and thence to users' desktops is a complex, rigorously controlled procedure. Please read on to get some insight into the world of "Continuous Integration" (CI).

5. Creating "branches" on your local and remote repositories to separate your production code from your development code

Suppose you've begun work on an enhancement to the current version of your system, and a crisis arises in the implemented version: a fault has been found, and a fix is urgently needed. What do you do? If you apply the fix to the code in your local repository, you may deploy untested development code along with the fix. Once you've experienced this situation, you will never want to find yourself there again! Let's stop it from happening.

The answer is obvious: create separate production and development environments within your local repository. Git "branches" were developed specifically to enable you to do this.

At the point when you last deployed your application, your local repository had only one branch - the "main" one you created at the outset. The following Git command creates a new branch called "dev" and switches you to it

git switch -c dev

You can always confirm which branch you're on by running git status.

You would create a new branch when you want to initiate a new line of development. Git enables you to do this without interfering with stable code. When the work is ready to be implemented, Git enables you to merge the changes safely into the main project history.

One important point to note is that when you created as above, this is not a copy of the "main" branch. Rather, it is a "peg" to which new versions of files representing a development track can be pinned

Also, naively, you might imagine that when working on your new "dev" branch, your view of files on "main" will be unchanged. This is not automatically the case. If you try changing "dev" branch files and switching back to their "main" versions, you'll find that the changes appear here too. How do you make branches work as expected?

First of all, you need to understand that file changes only get "pinned" to a branch when you "commit" them to it. You do this by making a commit on the branch:

git commit -m "Change test.txt on dev"

But if you do this right now, you'll find that you get a Changes not staged for commit error message followed by a list of the files that currently aren't "staged". So, what is staging?

Take a deep breath.

You need to be clear in your mind that, just as files are known to git only when you "track" them, files are included in a commit only when they are "staged". Even if your file was already tracked (because it was in your file hierarchy when you created your repository and ran git add .), you've now edited it, so a new version has been created. This is currently "dangling" loose. Git will only know which branch to pin it to when you "add" it to the staging area and commit to that branch.

When staging files, you could do them individually with git add myfile.text or, alternatively, do a preliminary git add . to add all current unstaged files. But most people will use the following command:

git commit -am "Change test.txt on dev"

This stages and commits all tracked and modified files in one go. However, if you had created some new files, you would need to get these tracked before committing, so it would be better to cover all bases with

git add .
git commit -am "Change test.txt on dev"

Try this out by switching to dev and changing the content of a test file. Now save the file and do a "git commit -am "Change test.txt on dev". When you switch back to main with a git switch main and re-open the test file, you should find that the changes are absent. Well done! You've now got the basic skills you need to create a robust production/development codebase control system.

Let's put this into practice. Assuming you have now acquired a Git account and successfully pushed your "main" local branch to a remote copy, here's how you would start from scratch to set up a clean dev branch mirroring your current main branch, and then give this its own remote copy.

First, clear the decks by making absolutely sure that "main" is clean and synced.

git switch main
git fetch origin
git status

You should see: "On branch main"

Since your current dev branch is only experimental, you should now reset it cleanly as an exact copy of your local "main" production branch

git branch -D dev # `-D` = force delete local branch only.
git switch -c dev # create fresh dev branch from main

At this point, dev is an exact copy of main. It's time now to complete the picture by giving the dev branch its own backup on the remote server. You do this with the following one-line command :

git push origin dev

You should see output like:

remote: 
remote: Create a pull request for 'dev' on GitHub by visiting:
remote:
To [remote repo address]
 " * [new branch]      dev -> dev
branch 'dev' set up to track 'origin/dev'"

This looks as if it's asking you to do something, but it's actually simply a combined conversation between Git on the server and git on the local repo that says, "Your remote repository did not previously have a dev branch. I have now created it". A visit to your remote repo should confirm this.

Perhaps you're surprised that you don't have to reference the url of the remote Git server when you launched the git push origin dev push command. This is because you're just creating a branch here, not a new remote. But it's a sophisticated command that does the following:

  • creates origin/dev
  • uploads history
  • links local dev to remote dev (otherwise kknow as "upstream tracking")

Once you've done this, you can backup your local dev branch whenevr you like by switching to it and issuing a simple git push.

6. Merging a branch - how to forward a "hot-fix" on "main" into "dev"

Once you've implemented a change to the production system you'll want to see this cleanly transmitted to the dev branch. You do this using the Git "Merge" command:

git switch dev   # Switch to the branch you want to merge INTO
git merge main   # Merge dev's changes INTO dev

This looks simple enough, but if you think hard about the complex conditions the process may encounter, your head will start to ache!

The best way of thinking about this is that merge "combines the independent changes both branches have made since they split".

This will be fine until it is found that the two branches changed the same thing in different ways. For example, one branch might have changed a file to say const x = 3; while the other changed it to const x = 4. In this situation, Git will report the problem as a "conflicted file", abandon the merge and leave you to sort things out.

A typical conflicted file error message will look like this:

CONFLICT (content): Merge conflict in temp.js
Automatic merge failed; fix conflicts and then commit the result.

It's easy to panic in this situation because it often occurs at points of maximum jeopardy in the development cycle. Also, Git does nothing to soften the blow - to quote an unknown developer, it "punishes you cryptically".

Here's what you should do in this situation:

  1. Don’t panic (nothing is broken)
    . Git has paused the merge intentionally.
    . Your repo is safe.

  2. Back in your VSCode editor, the lines in the temp.js file that are causing the problem will have been "marked up" with something like:

    <<<<<<< HEAD
    const x = 4;
    =======
    const x = 3;
    >>>>>>> main (Incoming Change)
    

This is a "graphical" representation of the problem with the two sections of conflicted code laid out on either side of a "fence" represented by the "============" characters. On the "uphill" side of the fence, you have HEAD, the destination version of the code (dev), and on the "downhill" side (incoming change), you have the source (main)

What you need to decide is "what should this file say?” You need to design the final state, and the answer will usually be one of:
. keep one side of the fence only (and delete the other)
. or combine both
. or rewrite entirely

In practice, your files will contain a number of these markers and your task now is to edit your conflicted files until all the <<<<<<<, ======= and >>>>>>> markers have gone, leaving only lines that state precisely "what this file should do"

You're nearly there, but there's one final step you need to take before your git merge will work properly.

When a conflict occurs, Git no longer has a normal version of the file. Instead, it temporarily stores three versions of the file in the Git repo's index:

  1. merge base (the file versions' common ancestor)
  2. your "uphill" branch (dev/HEAD)
  3. your "downhill" branch (main)

When you manually fix the file, you create a fourth version — the correct merged result. But Git does not assume you’re finished. You must explicitly confirm that the conflict has been resolved by "re-tracking" the file:

git add temp.js

You do this on the "uphill" "Head" branch (dev in this case) and follow it with a git commit -am "merge test complete" or similar. Git should now respond by telling you that the merge has been completed successfully

Addendum. The git merge command joins two branches in a way that preserves the history of commits on both partners. There is another form of join - the git rebase command that joins the partners in a way that pretends that all the commits on the "uphill" branch happened after the commits on the "downhill" branch. You would use this when you simply want to "tidy" your branch and aren't concerned about maintaining a rigorous commit history. This is for experts only and won't be discussed further here

7. Putting it all together - safeguarding your ongoing development cycle with Git

Life as a solo-developer situation usually consists of making frequent fixes and minor enhancements to the main production branch while more protracted work on a major extension continues on the dev branch. Each of these will ultimately lead to re-deployment of the application.

Here's the barebones of the two deployment procedures:

Minor enhancements to main

git switch main
# Move to the main branch (deployment branch)

git status
# Verify what will be committed; ensure no unexpected changes

git add .
# Stage all your changes (or specific files, if preferred)

git commit -m "Fix: "
# Create a permanent commit representing exactly what you will deploy

git push origin main
# Upload this commit to the remote repository
# Remote now becomes the source of truth for the deployment

# Run deployment script at this point - always runs on main

git switch dev   
# Now prepare to bring your dev branch up to date

git merge main   
# Merge main's changes into dev

git commit       
# Only if conflicts arise (a successful merge commits automatically )

git push origin dev 
# update dev's backup on the remote

Implementation of dev branch

git switch dev
git log dev..main --oneline
# check that main contains no commits that are absent from dev

git add .
git commit -m "Super new feature"
# Commit your new feature on dev

git push origin dev
# update the dev branch on the remote

git switch main
git merge dev
# bring main up to date with dev enhancement

git push origin main
# update main's backup on the remote

# Run deployment script at this point - always runs on main

If you ask nicely, ChatGPT will give you scripts to automate these processes.

8. Authentication issues and SSH

One of the most annoying issues you may encounter when using Git on Microsoft Windows to push local changes to the remote occurs when your terminal session cheerfully reports "Error: repository not found".

Of course, the repo may have been deleted, but this is unlikely. The message is usually just a smokescreen stating that VSCode has been unable to authenticate you with Git.

The most likely reason is that you have been working on multiple projects, each with its own Git repo and associated security keys. In this instance, the error message indicates that VSCode tried to push to your local repo with the wrong set of keys.

When you first pulled or pushed to your remote repo for a project, VSCode would have asked you to log in to that repo. At this point, it will have saved a copy of these in the Windows Credential Manager. This saves you the trouble of re-entering them the next time you want to access the repo. The problem is that, by default, the Credential Manager only holds a copy of the first entry you use. So, when you switch to another project, you're served incorrect credentials.

There are several ways to fix this. One is simply to open the credential manager and delete the Git entry you see there. When you try to push to the remote now, you'll be prompted to log in to Git, and an appropriate new entry will be created.

A better way is to clear out the Git entry, then prevent the problem from recurring by telling Windows to store permissions per repository. You do this by opening a terminal session on each of your projects and running the following command:

git config --local credential.helper manager-core
Enter fullscreen mode Exit fullscreen mode

However, there's a more interesting way of resolving these problems. Here's the background:

For serious IT developments, the security of the codebase stored in a Git repo is a major concern. A huge amount of capital will be invested in its creation, and it is a juicy target for competitors keen to poach others' work. How safe are keys stored in Credential Manager, and how safe is the traffic passing from VSCode to Git when you sync an update or pull a new remote copy? If these thoughts concern you, you might find it useful to explore an alternative protocol called SSH (Secure Shell).

SSH was developed in 1995 by Tatu Ylönen. It's a cryptographic network protocol used to securely access and manage systems over the internet. It allows for secure remote login and command execution on another machine, as well as file transfers between systems. It works by establishing an encrypted communication channel that prevents third parties from eavesdropping or tampering with the communication.

For present purposes, because Git allows you to log in using SSH keys as well as the conventional HTTPS-based arrangement you've used so far, SSH provides an alternative to Windows Credential Manager for repo authentication. Suffice it to say that SSH's many security and practical advantages make it the standard security mechanism for Git operation by professional IT teams.

When you have a spare moment and would like to explore something new, interesting, and useful, I suggest you ask ChatGPT for an SSH tutorial. The technology has applications in many areas beyond Git, so time spent here will be time well invested.

Top comments (0)