Create a Git Repository: git init, .gitignore, clone
Start a Git repository with git init, make your first commit, keep build output and secrets out with .gitignore, and clone with --branch and --depth.
A repository is a project folder whose history Git manages. You get one in two ways: you turn an existing folder into a repository with git init, or you copy an existing repository with git clone. This part does both. You create the project that the rest of the guide works on, make the first commit, keep build output and secrets out of it with .gitignore, and clone it with the options you will use most.
Prerequisites
- Git installed and configured as in Part 1:
user.name,user.emailandinit.defaultBranch main. - A terminal with Bash (on Windows, Git Bash, which comes with Git for Windows).
The Project for This Guide
Every part of this guide works on the same small project, status-page: a status page for a team's services. It has a list of services, a script that checks the list, a script that builds an HTML page from it, and a stylesheet. It is small on purpose, so that the Git commands, not the code, are the interesting part. By Part 6 it has a "server" to push to and a teammate, Sam, who pushes too.
Some later steps change a file with a sed -i command, so that you get exactly the same change and the same output. That is GNU sed, as on Linux and in Git Bash. On macOS, write sed -i '' instead of sed -i, or make the same edit in your editor.
Create a Repository with git init
Create the project folder and turn it into a repository:
git init created one thing: a hidden .git directory. That directory is the repository. The rest of the folder is the working tree. Nothing in the folder is tracked yet.
Now create the project's files, starting with a scripts folder for the two scripts (mkdir scripts). README.md:
# status-page
A small status page for the teams services.
- `services.txt`: one service per line, `<name> <https URL>`
- `scripts/check.sh`: checks the format of services.txt
- `scripts/build.sh`: writes the page to build/index.html
services.txt, the data:
# name URL
api https://api.example.com/health
web https://www.example.com/
index.html and style.css, the page:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Service status</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<h1>Service status</h1>
<ul id="services"></ul>
</body>
</html>
body {
font-family: system-ui, sans-serif;
margin: 2rem;
}
h1 {
font-size: 1.5rem;
}
scripts/check.sh checks that every line of services.txt has a name and an HTTPS URL:
#!/usr/bin/env bash
# Check services.txt: each line is "<name> <https URL>"; blank lines and # comments are allowed.
set -euo pipefail
cd "$(dirname "$0")/.."
errors=0
n=0
while read -r name url extra || [[ -n ${name:-} ]]; do
n=$((n + 1))
[[ -z $name || $name == \#* ]] && continue
if [[ -z $url || -n $extra || $url != https://* ]]; then
echo "services.txt:$n: want '<name> <https URL>', got '$name $url${extra:+ $extra}'" >&2
errors=$((errors + 1))
fi
done < services.txt
if ((errors > 0)); then
echo "check failed: $errors bad line(s)" >&2
exit 1
fi
echo "services.txt: OK"
scripts/build.sh runs the check and writes build/index.html with one list item per service:
#!/usr/bin/env bash
# Write build/index.html: index.html with one list item per service in services.txt.
set -euo pipefail
cd "$(dirname "$0")/.."
scripts/check.sh
mkdir -p build
{
sed '/<ul id="services">/,$d' index.html
echo ' <ul id="services">'
while read -r name url || [[ -n ${name:-} ]]; do
[[ -z $name || $name == \#* ]] && continue
printf ' <li><a href="%s">%s</a></li>\n' "$url" "$name"
done < services.txt
echo ' </ul>'
sed '1,/<ul id="services">/d' index.html
} > build/index.html
cp style.css build/
echo "built build/index.html ($(grep -c '<li>' build/index.html) services)"
Last, a .gitattributes file. It tells Git to store and check out shell scripts with LF line endings on every system, so the scripts still run after someone commits them from Windows (Part 9 explains line endings):
# Shell scripts always use LF line endings, also in Windows checkouts
*.sh text eol=lf
Save the scripts with LF line endings (on Windows, check the line ending setting in your editor's status bar; a script saved with CRLF fails with a bad interpreter error). Make them executable and ask Git what it sees:
All files are untracked: they are in the working tree, but not in the repository. Git also tells you it is on branch main with "No commits yet".
Make the First Commit
Committing takes two steps. git add puts the current content of files into the staging area, and git commit stores the staging area as a commit:
git add . staged everything under the current directory (A means added). The second command records the execute bit of the scripts; on Linux and macOS chmod already did that, but Windows file systems have no execute bit, so there it is needed. The commit output shows the branch (main), that this is the root commit (it has no parent), the commit's short hash and its message, then a summary. create mode 100755 shows that Git recorded the execute bit of the two scripts: Git tracks file content, the file name and whether the file is executable, but not owners, timestamps or other permissions.
Part 3 goes deeper into staging, diffs and commit messages.
What Is Inside .git
You never need to edit .git by hand, but knowing what is in it removes a lot of mystery:
objects/is the database: every file version, directory listing and commit, compressed and named by its hash.refs/holds branches (refs/heads/) and tags (refs/tags/), each a small file with a commit hash.HEADsays which branch you are on. Here it points torefs/heads/main.indexis the staging area,configthe repository's own settings,hooks/sample scripts (Part 9).git repo info(Git 2.52+) reports how the repository stores data: SHA-1 object names and the classicfilesformat for refs, the defaults until Git 3.0.
Deleting .git deletes the whole history and leaves only the current files. Copying the project folder with .git copies the history too.
Keep Files Out with .gitignore
Some files in a project folder must never be committed: build output that can be regenerated, dependencies, logs, editor files, and above all local settings with secrets. Build the page and create a local settings file to see the problem:
?? marks untracked files. Without help, a git add . would commit the build output and the token. A .gitignore file in the top folder of the project tells Git which paths to leave alone:
# build output
build/
# local settings and secrets, but keep the example file
.env
.env.*
!.env.example
# logs and editor or OS files
*.log
*.swp
.DS_Store
.idea/
.vscode/
git check-ignore -v is the command to reach for when a file is ignored and you do not know why, or is not ignored and you expected it to be. --ignored lists ignored files, marked !!.
The patterns follow a few rules:
| Pattern | Matches |
|---|---|
*.log |
Any file ending in .log, in any directory |
build/ |
A directory called build anywhere, and everything in it (a trailing / matches only directories) |
/build/ |
Only the build directory at the top of the repository |
docs/*.pdf |
PDF files directly in docs/ (a / in the middle anchors the pattern to the .gitignore file's folder) |
**/cache/ |
A cache directory at any depth |
!.env.example |
Exception: track this file even though .env.* matches it |
# text |
A comment |
Three more places hold ignore rules:
- A
.gitignorein a subfolder applies to that folder. .git/info/excludeworks like.gitignorebut is never committed: use it for files only you have.- A global file for every repository on your computer, for editor and OS files, so each project does not need to list them. Git reads
~/.config/git/ignoreby default;core.excludesFilepoints it elsewhere.
.gitignore only affects untracked files. If a file is already committed, adding it to .gitignore changes nothing; you have to remove it from the index first with git rm --cached (Part 3, and this post for the details). Git also does not track empty directories; the usual workaround is a .gitkeep file (why, and the alternatives).
GitHub's gitignore repository has starting templates for most languages and frameworks.
Clone a Repository
git clone copies a repository with its full history and sets it up for you: it creates the folder, downloads all objects, creates a remote called origin that points back to where you cloned from, and checks out the default branch.
git clone https://github.com/OWNER/REPO.git # into ./REPO
git clone https://github.com/OWNER/REPO.git my-dir # into ./my-dir
git clone git@github.com:OWNER/REPO.git # over SSH (Part 6 sets up keys)
The URL can also be a path on your own disk, which is how this guide clones without a hosting service. Clone the project into a second folder:
The clone has the same two commits with the same hashes, a remote origin pointing at the original folder, and a local main that tracks origin/main (Part 6 explains remotes and tracking). Note that .gitignored files (.env, build/) did not come along: only committed content travels.
Clone options you will use
| Option | What it does |
|---|---|
--branch <name>, -b |
Check out this branch or tag instead of the default branch |
--single-branch |
Download only that one branch's history (how to clone a specific branch) |
--depth 1 |
A shallow clone: only the newest commit. Common in CI |
--filter=blob:none |
A partial clone: full history, but file contents are downloaded only when needed (Part 10) |
--recurse-submodules |
Also clone the submodules (Part 10) |
--origin <name> |
Name the remote something other than origin |
--bare |
Only the repository, with no working tree: the format servers use (Part 6) |
For a local path, Git copies (or hard-links) the object files directly, and some options are ignored. A file:// URL makes Git use the same transfer process it uses over a network:
The shallow clone has only the newest commit. That is enough to build or test the code, but commands that need older history (git log beyond that commit, git blame, git merge-base) see a truncated history. git fetch --unshallow downloads the rest.
URL forms
| Form | Example | Notes |
|---|---|---|
| HTTPS | https://github.com/OWNER/REPO.git |
Works through most firewalls; needs a token or credential helper to push |
| SSH, short form | git@github.com:OWNER/REPO.git |
Uses your SSH key; the usual choice for daily work |
| SSH, URL form | ssh://git@host:2222/srv/git/repo.git |
Needed for a port other than 22 |
| Git protocol | git://host/repo.git |
Read-only and unencrypted; GitHub stopped serving it in 2022 |
Local path or file:// |
/srv/git/repo.git |
Another folder or a network share |
To find out where an existing clone came from, run git remote get-url origin (more ways).
Common Mistakes
- Running
git initin your home directory. Every file under it is now "untracked", and other repositories below it look nested. If it happened by mistake, delete only the new~/.gitdirectory (check withls -A ~first). - Creating a repository inside another one.
git addthen warns about an "embedded git repository" and records only a pointer. Use a submodule on purpose (Part 10) or keep the projects apart. - Adding
.gitignoreafter committing the files. The rules do not apply to tracked files. Rungit rm --cachedon them. - Committing secrets "for now". Anything committed stays in the history, also after you delete the file. Ignore
.envfiles from the first commit, and see Part 8 if it already happened. - Expecting
--depthto work with a local path. Usefile://, as above.
Summary
git initturns a folder into a repository by creating.git;git clonecopies a repository with its history and sets uporigin.- The first commit is
git addplusgit commit; Git records content, names and the execute bit. .gitignorekeeps build output, dependencies and secrets out;git check-ignore -vexplains any rule.--branch,--single-branch,--depthand--filtermake clones faster and smaller when you do not need everything.
Next: Tracking Changes, where you stage, inspect and commit changes, and undo the ones you did not mean.
Keep Learning
Tags
Found an issue?