Create your own
Lesson illustration

Creating and Configuring a Git Repository with a Clean Commit History

Welcome back. In the previous lesson, you mapped the delivery path from a small change through review, CI, deployment, and monitoring. Git is the evidence system at the beginning of that path: it identifies what changed, who made the change, and which exact version later produced an artifact or deployment.

In this lesson, you will build a small local repository resembling the beginning of an operations automation project. You will configure your Git identity, initialize the repository, define sensible ignore rules, and create a short history in which each commit has a clear purpose. This is deliberately command-line focused; you should be able to complete the lab in about 25 minutes.


Configure Git once, then verify it

Every commit records an author name and email. This is operationally useful: when a pipeline, Terraform configuration, or deployment script changes, the history shows who made the change and gives reviewers meaningful context.

First confirm that Git is installed and set a professional identity. Substitute your own name and an email address appropriate for repositories you may eventually make public.

git --version

git config --global user.name "Your Name"
git config --global user.email "you@example.com"

git config --global init.defaultBranch main

Verify the specific values rather than scanning a long configuration listing:

git config --global --get user.name
git config --global --get user.email
git config --global --get init.defaultBranch

--global writes settings used by your account on this machine. A particular repository can override them later with the same command without --global; that is useful when a work repository requires a different email identity.

Git Tutorial for Beginners: Command-Line Fundamentals

Watch “Git Tutorial for Beginners: Command-Line Fundamentals” by Corey Schafer for a compact terminal-based view of identity setup, repository initialization, ignore rules, staging, and committing.

Watch Git identity to see the name and email configuration. Then watch repository initialization. Continue with ignore setup, then finish with staging and commits. Focus on the distinction between files on disk, the staging area, and committed history.


Create the repository, not just a directory

A project directory becomes a Git repository when it gains its hidden .git directory. That directory contains Git’s local database: commits, branch references, configuration, and staging information. Do not edit or delete its contents as part of normal work.

2.1 Git Basics - Getting a Git Repository

Read “Git Basics: Getting a Git Repository” from the official Pro Git book. It explains what git init establishes locally and how an initial commit begins version control.

In the subsection “Initializing a Repository in an Existing Directory,” read the initialization sequence. Notice that git init creates repository metadata but does not automatically put project files under version control.

Create a small lab repository. We will call it service-check, a plausible starting point for an operations utility that will gain real Bash functionality in the next module.

mkdir -p ~/devops-labs/service-check/scripts
cd ~/devops-labs/service-check

git init -b main

If your installed Git does not recognize -b main, use this instead:

git init
git branch -M main

Confirm where you are and inspect the initial state:

pwd
git status
ls -la

You should see a .git directory in ls -la. git status should report that you are on main and have nothing to commit. The repository exists, but it contains no committed project files yet.


Think in snapshots: working tree, staging area, history

Git becomes much less mysterious when you treat its three main areas as separate snapshots:

AreaWhat it containsMain command you use
Working treeFiles as they currently exist on diskEdit files; inspect with git status
Staging area (also called the index)The exact content selected for the next commitgit add <path>
Local repositoryPermanent committed snapshots and their historygit commit, git log
This diagram depicts the states a file can have in Git: untracked files have not been selected for version control; tracked files may be unmodified, modified, or staged; a commit records the staged snapshot as the new committed version.

A few rules explain most everyday Git behavior:

  • A new file is untracked until you stage it with git add.
  • A tracked file becomes modified when its working-tree content differs from the latest committed version.
  • git add takes a snapshot of the file as it exists at that moment and places that snapshot in the staging area.
  • git commit records only what is staged. It does not automatically include every changed file in the directory.
  • If you edit a file again after staging it, the new edit is not staged. Stage the file again after reviewing the final content.

This staging step is useful rather than bureaucratic. Suppose an automation change modifies a script, its documentation, and an unrelated local formatting experiment. You can stage only the script and documentation that belong to one logical change. That makes review, rollback, and incident investigation much safer later.

Use these commands routinely before committing:

git status
git diff
git diff --staged

git diff shows changes in your working tree that have not yet been staged. git diff --staged shows the precise content that your next commit would record.


Write .gitignore rules before local clutter accumulates

A .gitignore file is a versioned policy for files that Git should leave untracked. It prevents generated output and machine-specific files from obscuring git status or being accidentally committed.

For an operations repository, typical examples include:

  • runtime logs;
  • temporary output;
  • local environment-value files;
  • downloaded artifacts or locally generated reports;
  • editor-specific files when they are personal rather than team-shared.

Create the following project files:

cat > README.md <<'EOF'
# Service Check

A small operations automation lab repository.

The project will eventually contain a reusable service health-check script.
EOF

cat > .gitignore <<'EOF'
# Runtime output created locally
*.log
/logs/

# Temporary working files
/tmp/

# Local environment values: commit only safe templates
.env
.env.*
!.env.example
EOF

Read the patterns as rules, not magic:

PatternMeaning in this repository
*.logIgnore any file ending in .log, including files in subdirectories
/logs/Ignore the logs directory at the repository root
/tmp/Ignore root-level temporary output
.env and .env.*Ignore local environment-value files
!.env.exampleMake an exception so a safe example template can be committed

The .env.example exception is valuable because documentation can show which variable names are required without storing real values. A real .env file might contain database credentials, API tokens, or AWS-related values and should not be treated as source code.

Now test the rules before making any commits:

mkdir -p logs tmp
printf 'local-api-token=not-a-real-token\n' > .env
printf 'check ran locally\n' > logs/service-check.log

git status --short

You should see only these two untracked files:

?? .gitignore
?? README.md

The local .env file and log file should not appear. To inspect ignored paths deliberately, use:

git status --ignored --short
git check-ignore -v .env logs/service-check.log

The second command is especially useful when a path is unexpectedly ignored: it tells you which rule matched it.

.gitignore is preventative convenience, not a security control. It does not remove a file that is already tracked, and it does not erase a secret that was previously committed. If a real credential ever reaches Git history, rotate or revoke it immediately according to the relevant service’s process.


Build a small, readable commit history

Now create three purposeful commits. The files are simple; the discipline is the point.

Commit 1: establish the repository policy and project description

Stage only the README and ignore policy. Review the staged snapshot, then commit it.

git add README.md .gitignore

git status
git diff --staged

git commit -m "chore: initialize service check repository"

The prefix chore: is a widely used convention for maintenance or repository setup. Git does not require it, but a consistent convention makes a history easier to scan.

Verify that the working tree is clean:

git status

Commit 2: add one implementation change

Create a harmless placeholder script. We will improve its Bash design later rather than prematurely adding concepts not yet covered.

cat > scripts/check-service.sh <<'EOF'
#!/usr/bin/env bash

echo "Service check placeholder"
EOF

chmod +x scripts/check-service.sh

git status --short
git add scripts/check-service.sh
git diff --staged

git commit -m "feat: add service status check placeholder"

This is a focused commit: it introduces one new capability, however small. feat: signals an externally meaningful addition.

Commit 3: document the current behavior

Update the README to match the repository’s actual state:

cat >> README.md <<'EOF'

## Current scope

The service-check script is currently a placeholder. It will be expanded into a defensive Bash health-check utility in a later module.
EOF

git add README.md
git diff --staged

git commit -m "docs: describe current script scope"

Inspect the history:

git log --oneline --decorate -n 5
git status

Your output will use different commit IDs, but its structure should resemble this:

<commit-id> (HEAD -> main) docs: describe current script scope
<commit-id> feat: add service status check placeholder
<commit-id> chore: initialize service check repository

You now have a history that tells a coherent story:

  1. The repository began with an explanation and guardrails.
  2. A script was introduced.
  3. The documentation was updated to describe its present state.

That story is far more useful than one vague commit such as “updates,” “work done,” or “changed files.”


Commit messages that help during review and incidents

A clear history is an operational asset. When a pipeline breaks or a release must be investigated, the reader should quickly determine which change likely matters.

A good commit generally has:

  • One logical purpose. Avoid mixing a feature, a refactor, and unrelated formatting changes.
  • A concise, specific subject. Describe what changed, not merely that something changed.
  • A reviewable size. Small does not mean trivial; it means a reviewer can understand the intent and effect.
  • A truthful scope. Do not label a broad behavioral change as “minor cleanup.”
  • A clean staged snapshot. Check git diff --staged before each commit.

Compare these messages:

Weak messageWhy it is weakClearer message
updatesNo purpose, no affected areadocs: describe current script scope
fix stuffDoes not identify the failure or solutionfix: report inactive service as failure
new script and configCombines distinct changesfeat: add service status check
initial commitAcceptable only at the very beginning, but not informativechore: initialize service check repository

Prefixes such as feat:, fix:, docs:, and chore: are conventions, not Git syntax. Teams may use Conventional Commits formally, use a ticket number, or choose plain imperative messages. The enduring principle is clarity.


If Git is still tracking something that should be ignored

A frequent real-world surprise is adding a .gitignore rule and finding that Git continues to show a file as modified. This is expected if the file was committed before the rule existed: .gitignore primarily affects untracked files.

To stop tracking a file while retaining it on your local disk:

git rm --cached path/to/file
git commit -m "chore: stop tracking local file"

For example, if .env had accidentally been committed, you would use:

git rm --cached .env
git commit -m "chore: stop tracking local environment file"

That removes .env from the next repository snapshot but leaves your local .env file in place. Because it matches .gitignore, later changes remain untracked.

Learning Git - How to use the gitignore file

Watch “Learning Git - How to use the gitignore file” by DevOps Journey for a short demonstration of fixing the already-tracked-file case.

Watch untracking a file. Focus on why git rm --cached removes a path from Git’s index and repository snapshot without deleting the local working copy.

If the file contained a real secret, this command alone is not sufficient. The earlier commit can still contain the secret, so revoke or rotate it first. History rewriting may be required in a shared repository, but that workflow is intentionally outside this lesson.


Key takeaways

A Git repository is a project directory containing a .git database. Configure a meaningful name and email, initialize repositories with main as the default branch, and use git status constantly to understand the working tree.

Git separates current files, the staging area, and committed history. Use git add deliberately, inspect the staged diff, and commit only a single logical change at a time.

A committed .gitignore keeps generated local output and environment-value files out of normal version control. It does not protect secrets that have already been committed, so prevent exposure early and rotate any leaked credential immediately.

You now have the local foundation for collaborative delivery. Next, you will use branches to isolate a change from main, then merge that change back in safely.

Can't find a good explanation? Sign up and we'll make it for you

Sign up