Embercode
✕
VS CodeLesson 6 of 10

55 min read

The Source Control panel

Why this matters

Git keeps a history of your project. Each entry in that history is a commit: a saved snapshot of the files, with a short message that says what changed. The Git module earlier in this course taught the commands that create commits, such as git add and git commit. Those commands are precise, but you have to remember their names and their order.

The Source Control panel in VS Code shows the same state as a list of files, and each step becomes a button. A beginner can make a correct commit this way without memorising the commands.

The panel is not a separate system. Each button runs a Git command, and the panel reads its answers from Git. If you know what a button does, you can predict what Git will do, and you can check the panel against the terminal whenever you are unsure. This lesson shows the panel next to the command it runs, so you can see both.

Opening the panel

Press Ctrl+Shift+G on Windows and Linux, or Cmd+Shift+G on macOS. The Source Control view opens in the sidebar on the left.

The panel only works for a folder that Git tracks. A tracked folder is called a repository, and Git marks it with a hidden folder called .git at the top. If you open a folder that has no .git folder, the panel shows a button that starts a new repository. Starting one is covered in the Git module, so this lesson uses a folder that already has a repository.

Open the folder in VS Code with File, Open Folder, and pick the top folder of your project. If you open a folder above it, the panel may show no repository at all, or show one that is not the one you meant.

Three places a change can be

Git keeps track of changes in three places. Learn these names first, because the panel uses them.

  1. The working tree. These are the files as they are on your disk right now. When you save a file in the editor, the change lives here and nowhere else.
  2. The staging area. This is a waiting area. Anything placed here goes into the next commit, and nothing else does. Moving a change into this area is called staging.
  3. The history. These are the commits already saved. Once a commit exists, it stays in the history.

A concrete example helps. Suppose you change two files and create one new file. You want only the two changes in the next commit, so you stage those two files and leave the new file out. The new file stays in the working tree, and the commit does not include it.

A useful picture is a shipping box. Staging is putting items into the box before you seal it. Items you have not put in stay on the shelf. Sealing the box is the commit. The picture has one limit: a sealed commit cannot be opened and tidied. If a commit was wrong, you make a new commit that fixes it, and the old one stays in the history.

Reading the list of changes

The panel groups files by their state. In recent versions of VS Code the list has these groups:

  • Staged Changes holds the files that are in the waiting area, ready for the next commit.
  • Changes holds the files that have edits not yet staged.

A letter beside each file says what kind of change it is. The most common letters are these.

  • M means modified: a tracked file that you edited.
  • U means untracked: a new file that Git does not track yet.
  • D means deleted: a tracked file that you removed.

The terminal shows the same information with a shorter code. Here is the output of git status --short in a small practice project, after one file was edited and one new file was created:

 M app.py
?? extra.py

The space before M means the change is not staged. The ?? is Git's code for an untracked file, and the panel shows it as U. Read the two-letter codes as a pair: the first column is the staging area, and the second column is the working tree. You will meet this pair again in worked example 2.

Worked example 1: one change, from edit to commit

This is the simplest case: one file, one change, one commit. The practice project has a single file, app.py, with this content:

def greet(name):
    return "Hello, " + name

The project starts clean. The panel lists no files, and the terminal says so:

On branch main
nothing to commit, working tree clean

Step 1: make the edit. Change the second line so it adds an exclamation mark, then save with Ctrl+S. The file now reads:

def greet(name):
    return "Hello, " + name + "!"

The panel now lists app.py under Changes, with an M beside it. Nothing has been staged yet, so the commit is still empty.

Step 2: look at the diff. Click the file name in the panel. VS Code opens a diff view, which shows the last saved version next to the current one. The same comparison in the terminal is git diff app.py, and it prints this:

diff --git a/app.py b/app.py
index d282150..4504fd7 100644
--- a/app.py
+++ b/app.py
@@ -1,2 +1,2 @@
 def greet(name):
-    return "Hello, " + name
+    return "Hello, " + name + "!"

Reading a diff is a skill of its own, so it helps to go line by line:

  • A line starting with a space is unchanged. It is shown for context.
  • A line starting with - was in the last commit and is gone now.
  • A line starting with + is new.
  • The @@ -1,2 +1,2 @@ line gives the line numbers. The -1,2 part says the old version covers two lines from line 1, and +1,2 says the new version covers two lines from line 1.

The hash values such as d282150 will differ on your machine. They are fingerprints of the file contents, so they change whenever the content changes.

Step 3: stage the file. Hover over the file in the Changes group and click the + button, labelled Stage Changes. The file moves into Staged Changes. The terminal shows the same thing as M app.py, with the M now in the first column.

Step 4: write the message and commit. Type a short message in the box at the top of the panel, such as Exclamation mark in greeting. Then click the Commit button. On Windows and Linux, Ctrl+Enter inside the message box does the same thing. The panel empties, and git log --oneline shows the new commit at the top of the history:

f3597fe Exclamation mark in greeting
cb529c1 Start project

Your hash values will differ. The message is what matters, because you will read it later when you look back at the history.

Step 5: confirm. The panel is empty and git status prints nothing to commit, working tree clean again. The edit is saved in the history, and the working tree matches it exactly.

Worked example 2: staging part of the work

The next case has two files changed and one new file. You want one commit to hold only one of the changes, so you need to choose what goes into the waiting area. The practice project starts with app.py and README.md committed. Then you add the exclamation mark from worked example 1 to app.py, change README.md and create extra.py.

The terminal shows all three:

 M README.md
 M app.py
?? extra.py

In the panel, README.md and app.py are under Changes, and extra.py is also under Changes with a U beside it.

Now stage only app.py by clicking its + button. The terminal shows:

 M README.md
M  app.py
?? extra.py

The line for app.py now starts with M (an M and a space). The M in the first column means a change is staged. The space in the second column means nothing else is pending for that file. README.md still starts with a space and an M, so its change is not staged.

To see exactly what the commit will contain, run git diff --cached --stat, which compares the staging area with the last commit:

 app.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

The --cached flag in git diff --cached means "the staging area", and the panel's Staged Changes group shows the same list of files. Use the list as a check before you press Commit. If a file is there that should not be, unstage it.

Unstaging. Click the - button next to a file in Staged Changes. The file moves back to Changes, and its contents do not change. In the terminal, git restore --staged app.py does the same thing, and the status goes back to M app.py with a space in the first column.

Committing a partial set. If you commit now, the commit holds app.py only. README.md keeps its edits in the working tree, and extra.py stays untracked. Nothing from those two files reaches the history until you stage and commit them in a later step. This is the reason for the waiting area: you can split one day's work into several clean commits, each with one purpose.

Discarding a change. Discarding is different from unstaging. It throws away the edits in the working tree, so the file goes back to the state of the last commit. Use the panel's Discard Changes action on README.md, and VS Code asks you to confirm. In the terminal, the same action is git restore README.md. After it runs, README.md no longer appears in git status --short, because it matches the last commit again.

Git cannot bring discarded edits back. Only do this when you are sure you do not need the text you typed. If you are not sure, copy the edited text to a scratch file first, then discard.

Worked example 3: resolving a conflict

The hardest common case is a merge conflict. A conflict happens when two lines of work change the same lines of the same file, and Git cannot decide which version to keep. It stops and asks you.

To understand the setup, you need one more term. A branch is a named line of work. The main line is called main. Suppose a second branch, feature-a, was made from main. Both branches then changed the first line of README.md, each in its own way.

This is what happens when you merge feature-a into main from the terminal:

Auto-merging README.md
CONFLICT (content): Merge conflict in README.md
Automatic merge failed; fix conflicts and then commit the result.

The panel shows the same problem. README.md moves into a Merge Changes group, and the status line in the terminal says both modified. Click the file to open it in the editor. Git has written both versions into the file, separated by marker lines:

# Notes
 
<<<<<<< HEAD
First line, changed on main.
=======
First line, changed on feature-a.
>>>>>>> feature-a

Read the markers from top to bottom:

  • The line <<<<<<< HEAD starts the version from the branch you are on. HEAD is Git's name for the commit you are currently on, which here is main.
  • The lines between <<<<<<< and ======= are your version.
  • The lines between ======= and >>>>>>> are the other branch's version, named after that branch.

Above the conflict, VS Code shows buttons. They are labelled Accept Current Change, Accept Incoming Change and Accept Both Changes, and a Compare Changes link. Each button edits the file for you. For a conflict where neither version is right, skip the buttons and type the final text yourself.

Resolving it. Replace the whole block with the text you want, and delete all the marker lines. Here the correct first line describes both changes, so the file becomes:

# Notes
 
First line, changed on both branches.

Then click + next to README.md to stage it. Staging tells Git that the conflict is resolved. The file moves from Merge Changes into Staged Changes.

Finishing the merge. Type a message, or keep the one Git fills in, which is Merge branch 'feature-a', and press Commit. The history now shows a merge commit with two parents:

*   f02ed96 Merge branch 'feature-a'
|\
| * 79b52ec Change README on feature-a
* | 2b21eba Change README on main
|/

The lines on the left draw the two lines of work and where they join. The commit on the top line is the merge. Your hashes will differ.

Cancelling a merge. If you decide not to merge, run git merge --abort in the terminal. It restores the branch to the state before the merge started. It prints nothing when it works, and its exit code is 0.

Common mistakes

Committing with nothing staged

Pressing Commit when the Staged Changes group is empty gives Git nothing to record. Git refuses the commit and says so. If you have edits that are not staged, the last line Git prints is this:

no changes added to commit (use "git add" and/or "git commit -a")

If the working tree is clean, the line reads nothing to commit, working tree clean instead. In both cases the exit code is 1, which means the command failed. The fix is to stage the files you want first, then commit.

Leaving conflict markers in a file

The most dangerous conflict mistake is staging a file that still has marker lines. Git accepts it, because the marker text is ordinary text to Git. The program then fails to read the file. This is what Python prints when the lines are still there:

  File "conflict_app.py", line 2
    <<<<<<< HEAD
    ^^
IndentationError: expected an indented block after function definition on line 1

The error points at the first marker line, but the real fault is that the file was never finished. Before you stage a conflicted file, search it for <<<<<<<. The search in the editor, Ctrl+F, finds it in a moment.

Forgetting the new file

A new file shows as U until you stage it. If you commit without staging it, the commit is missing that file. Any code that imports the file then works on your machine, where the file exists, and fails on a teammate's machine, where the commit has no copy of it. Before a commit, check that every new file you need is in Staged Changes.

Discarding by accident

Clicking Discard Changes on the wrong file is the most costly mistake in this lesson, because the edits are gone. Read the file name in the confirmation before you click. If you are unsure, unstage the file instead, which keeps the edits.

Reading the second column as the first

A status line like M app.py is not the same as M app.py. The first says the change is not staged. The second says it is. The panel shows this with two groups, so use the groups as the main guide and the letters as a check.

Committing with an empty message

A commit needs a message. If the message is empty, Git stops the commit and prints this:

Aborting commit due to empty commit message.

VS Code runs the same Git, so the rule is the same in the panel. Write a short sentence, such as Fix greeting for names with spaces.

Writing a good commit message

A commit message is for a reader who knows nothing about your work: a teammate, or you in six months. Two habits make messages useful.

First, make the first line say what the change does. fix stuff tells the reader nothing. Add exclamation mark to greeting tells them what changed. Keep the first line short. The git log --oneline command shows only the first line of each commit, so that line is what you will read most often.

Second, use the present tense, as if giving an instruction: Add, Fix, Remove. Git's own messages use this style, such as Merge branch 'feature-a'.

If a change needs more explanation, leave a blank line after the first line and write the detail below it. The panel's message box accepts several lines.

Practice

Exercise 1. You edited README.md and the panel shows M beside it under Changes. Where is the change, and is it staged?

Show the answer

The change is in the working tree, the files on disk. It is not staged, because it sits under Changes and not under Staged Changes. The terminal would show M README.md, with a space in the first column.

Exercise 2. The terminal prints M app.py, with two spaces after the M. Is the change staged? Is any other edit pending for that file?

Show the answer

Yes, the change is staged. The M in the first column means the staged version differs from the last commit. The space in the second column means the working tree matches the staged version, so no other edit is pending.

Exercise 3. You stage app.py, then edit it again without staging the new edit. The terminal prints MM app.py. What is in the next commit, and what is in the working tree?

Show the answer

The next commit contains the version you staged, which is the version before the new edit. The working tree holds the newer version. The panel shows app.py in both groups: once under Staged Changes and once under Changes. To include the newer edit, stage it with + and commit again.

Exercise 4. This is a diff line: - return x followed by + return x * 2. What changed?

Show the answer

The line return x was in the last commit and has been replaced by return x * 2. The minus line is the removed text and the plus line is the new text. The four spaces after the sign are Python indentation, so the line sits inside a function.

Exercise 5. A merge stopped with a conflict in notes.md. Write the steps, in order, to finish the merge.

Show the answer

Open notes.md and find the marker lines that start with <<<<<<<. Replace each block with the text you want, and delete the ======= and >>>>>>> lines too. Search the file for <<<<<<< to confirm none are left. Click + next to the file to stage it. Type a message or keep Git's default, and press Commit. Then check that the history shows the merge commit.

Summary

  • The panel shows Git's three places: the working tree, the staging area and the history.
  • A change is only in the next commit if it is in Staged Changes, so check that group before you commit.
  • The letters M, U and D show whether a file was modified, is new, or was deleted.
  • Use the diff view to read exactly which lines changed before you stage them.
  • Discard Changes cannot be undone, while unstaging keeps your edits.
  • In a conflict, remove every marker line and search for <<<<<<< before you stage the file.
Check yourself: Source Control (6 questions) →

Stuck? Ask the tutor

Ask about anything in this lesson. Answers are short and use simple words.

Sign in to ask the tutor.