LESSON 01
Repository initialization

Every Git repository is, at its core, a directory called .git sitting alongside
your working tree.
git init creates that directory and seeds it with the
minimum structure Git needs to start tracking changes.
In this lesson you'll reimplement git init in Go, and along the way understand
what each file means.
What does git init actually do?#
Run it on an empty folder, then peek inside:
mkdir demo && cd demo
git init
ls .gitYou'll see something like:
Five entries are mandatory; the rest are convenience.
The bare minimum a Git client needs to consider a directory a valid repository is:
| Entry | Purpose |
|---|---|
HEAD | A pointer to the current branch |
objects/ | Content-addressable storage for blobs, trees, commits, tags |
refs/heads/ | Branch tips |
refs/tags/ | Tag refs |
config | Repository-local configuration |
Everything else (description, hooks/, info/exclude) is optional. Git
itself works without them.
The Go program#
We'll build this up in three steps:
- Scaffold the project and CLI.
- Wire up an empty
initsubcommand. - Make
initactually create the repository.
Step 1. Bootstrap the module#
We'll use urfave/cli v3 for argument parsing. It handles subcommands, flags, and help output, which we'll need as soon as we add more commands.
go mod init mygit
go get github.com/urfave/cli/v3Step 2. Scaffold the CLI#
Create main.go with a single init subcommand that, for now, just prints
a placeholder. This confirms the CLI plumbing works before we touch the
filesystem.
package main
import (
"context"
"fmt"
"os"
"github.com/urfave/cli/v3"
)
func main() {
cmd := &cli.Command{
Name: "mygit",
Usage: "a tiny Git",
Commands: []*cli.Command{
{
Name: "init",
Usage: "create an empty Git repository",
Action: func(ctx context.Context, cmd *cli.Command) error {
fmt.Println("init: not implemented yet")
return nil
},
},
},
}
if err := cmd.Run(context.Background(), os.Args); err != nil {
fmt.Fprintln(os.Stderr, "fatal:", err)
os.Exit(1)
}
}Build and run:
go build -o mygit
./mygit initYou should see init: not implemented yet. Try ./mygit --help and
./mygit init --help too — urfave/cli generates both for free.
Step 3. Implement init#
Now replace the placeholder action with a runInit function that creates
the directories and files Git needs.
import (
// ...existing imports
"path/filepath"
)
// inside the init subcommand:
Action: func(ctx context.Context, cmd *cli.Command) error {
return runInit()
},And add runInit below main:
func runInit() error {
gitDir := ".git"
dirs := []string{
gitDir,
filepath.Join(gitDir, "objects"),
filepath.Join(gitDir, "refs", "heads"),
filepath.Join(gitDir, "refs", "tags"),
}
for _, d := range dirs {
if err := os.MkdirAll(d, 0o755); err != nil {
return fmt.Errorf("mkdir %s: %w", d, err)
}
}
files := map[string]string{
filepath.Join(gitDir, "HEAD"): "ref: refs/heads/main\n",
filepath.Join(gitDir, "config"): defaultConfig,
}
for path, content := range files {
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
return fmt.Errorf("write %s: %w", path, err)
}
}
abs, _ := filepath.Abs(gitDir)
fmt.Printf("Initialized empty Git repository in %s/\n", abs)
return nil
}
const defaultConfig = `[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
`Rebuild and run it on a fresh directory:
go build -o mygit
mkdir scratch && cd scratch
../mygit initThe output should match real Git's, byte for byte:
Initialized empty Git repository in /tmp/scratch/.git/Verify it's a real repo#
The acid test: hand the directory to actual Git and see if it accepts it.
git statusIf you get On branch main and No commits yet, you've built something Git
considers a valid repository.
You can now git add files, and Git will read your HEAD and config like it would its own.
Original implementation#
Git's own init is a few hundred lines of C spread across three files:
git.c (dispatch), builtin/init-db.c (CLI parsing), and setup.c
(repository creation). The shape is the same as your Go program. The
difference is mostly polish: hash-format negotiation, shared-repo
permissions, template copying, --separate-git-dir.
{ "init", cmd_init_db },
{ "init-db", cmd_init_db },const struct option init_db_options[] = {
OPT_STRING(0, "template", &template_dir, N_("template-directory"),
N_("directory from which templates will be used")),
OPT_SET_INT(0, "bare", &is_bare_repository_cfg,
N_("create a bare repository"), 1),
{
.type = OPTION_CALLBACK,
.long_name = "shared",
.value = &init_shared_repository,
.argh = N_("permissions"),
.help = N_("specify that the git repository is to be shared amongst several users"),
.flags = PARSE_OPT_OPTARG | PARSE_OPT_NONEG,
.callback = shared_callback
},
OPT_BIT('q', "quiet", &flags, N_("be quiet"), INIT_DB_QUIET),
OPT_STRING(0, "separate-git-dir", &real_git_dir, N_("gitdir"),
N_("separate git dir from working tree")),
OPT_STRING('b', "initial-branch", &initial_branch, N_("name"),
N_("override the name of the initial branch")),
OPT_STRING(0, "object-format", &object_format, N_("hash"),
N_("specify the hash algorithm to use")),
OPT_STRING(0, "ref-format", &ref_format, N_("format"),
N_("specify the reference format to use")),
OPT_END()
};if (argc == 1) {
int mkdir_tried = 0;
retry:
if (chdir(argv[0]) < 0) {
if (!mkdir_tried) {
int saved;
saved = repo_settings_get_shared_repository(the_repository);
repo_settings_set_shared_repository(the_repository, 0);
switch (safe_create_leading_directories_const(the_repository, argv[0])) {
case SCLD_OK:
case SCLD_PERMS:
break;
case SCLD_EXISTS:
errno = EEXIST;
/* fallthru */
default:
die_errno(_("cannot mkdir %s"), argv[0]);
break;
}
repo_settings_set_shared_repository(the_repository, saved);
if (mkdir(argv[0], 0777) < 0)
die_errno(_("cannot mkdir %s"), argv[0]);
mkdir_tried = 1;
goto retry;
}
die_errno(_("cannot chdir to %s"), argv[0]);
}
}int init_db(const char *git_dir, const char *real_git_dir,
const char *template_dir, int hash,
enum ref_storage_format ref_storage_format,
const char *initial_branch,
int init_shared_repository, unsigned int flags)
{
int reinit;
int exist_ok = flags & INIT_DB_EXIST_OK;
char *original_git_dir = real_pathdup(git_dir, 1);
struct repository_format repo_fmt = REPOSITORY_FORMAT_INIT;
if (real_git_dir) {
struct stat st;
if (!exist_ok && !stat(git_dir, &st))
die(_("%s already exists"), git_dir);
if (!exist_ok && !stat(real_git_dir, &st))
die(_("%s already exists"), real_git_dir);
set_git_dir(real_git_dir, 1);
git_dir = repo_get_git_dir(the_repository);
separate_git_dir(git_dir, original_git_dir);
} else {
set_git_dir(git_dir, 1);
git_dir = repo_get_git_dir(the_repository);
}
startup_info->have_repository = 1;
check_repository_format(&repo_fmt);
repository_format_configure(&repo_fmt, hash, ref_storage_format);
repo_config(the_repository, git_default_core_config, NULL);
safe_create_dir(the_repository, git_dir, 0);
reinit = create_default_files(template_dir, original_git_dir,
&repo_fmt, init_shared_repository);
if (!(flags & INIT_DB_SKIP_REFDB))
create_reference_database(initial_branch, flags & INIT_DB_QUIET);
create_object_directory();
if (!(flags & INIT_DB_QUIET)) {
int len = strlen(git_dir);
printf(reinit
? _("Reinitialized existing Git repository in %s%s\n")
: _("Initialized empty Git repository in %s%s\n"),
git_dir, len && git_dir[len-1] != '/' ? "/" : "");
}
clear_repository_format(&repo_fmt);
free(original_git_dir);
return 0;
}What we skipped (on purpose)#
Real git init does a few more things you don't need yet:
- Reinit detection. If
.gitalready exists, real Git updates it instead of creating it. We'll add that next. - Template directory. Git copies hook samples and
info/excludefrom/usr/share/git-core/templates. Skip; they're optional. --bareflag. A bare repo has no working tree; the contents of.gitlive at the top level. One-line change once we add flag parsing.--initial-branch=<name>. We hardcodedmain. Easy upgrade.
Exercise#
Extend your init to:
- Accept an optional
<directory>argument:mygit init my-repo. - Detect an existing
.git/and printReinitialized existing Git repository in <path>/.git/instead. - Add a
--initial-branchflag.
Once that works, you've matched ~95% of git init's real behavior. The
remaining 5% is templates and bare-repo plumbing, both worth poking at but
neither is on the critical path to a working Git.
Stuck or want to compare notes? Hop into the community.
Join the community