Automatically creating or updating gitignore files within Inform projects, so that can be put under version control with git more easily.
§1. Git is, so help us, the world's standard in version control, but is not
the easiest system to configure, especially for beginners. One thing we can
help with is the automatic setting up of .gitignore files, which tell git
which files are ephemeral and need not be under source control.
This very simple feature was added to Inform as IE-0002 in October 2022.
void Gitignoring::automatic(inform_project *proj) { Gitignoring::for_project(Projects::path(proj)); Gitignoring::for_materials(Projects::materials_path(proj)); }
§2. In .gitignore file syntax, pathnames are relative to that of the file.
P/** means "ignore P and all its contents, to any depth". Lines beginning
with a # are comments.
void Gitignoring::for_project(pathname *P) { filename *F = Filenames::in(P, I".gitignore"); text_stream *stanza_wanted = I"Build/**\nIndex/**\nmanifest.plist\nMetadata.iFiction\nnotes.rtf\nRelease.blurb\n"; Gitignoring::fix(F, stanza_wanted); } void Gitignoring::for_materials(pathname *P) { filename *F = Filenames::in(P, I".gitignore"); text_stream *stanza_wanted = I"Release/**\n"; Gitignoring::fix(F, stanza_wanted); }
§3. What we do, for each of the directories relevant to a project (i.e. the project
itself and its materials), is to see if a .gitignore file already exists. If it
does, we look for a "stanza" between appropriate comments which will represent
our contribution. If that stanza already contains the right contents, then we
do not write the file. (There is no need, and we don't want to touch the timestamp
on the file.) Otherwise, we write the file back but with out preferred contents
of the stanza replacing whatever was there before.
As a special case, if there is no .gitignore file, we create one consisting
only of our stanza.
void Gitignoring::fix(filename *F, text_stream *stanza_wanted) { gitignore_harvest H; Harvest the existing gitignore file content, if any3.2; if (H.ignore) return; if (Str::eq(stanza_wanted, H.G)) return; text_stream F_struct; text_stream *OUT = &F_struct; if (STREAM_OPEN_TO_FILE(OUT, F, ISO_ENC) == FALSE) Errors::fatal_with_file("unable to open .gitignore file for output: %f", F); WRITE("%S", H.B); WRITE("# This stanza written automatically by inform7\n"); WRITE("%S", stanza_wanted); WRITE("# End of stanza written automatically by inform7\n"); WRITE("%S", H.A); STREAM_CLOSE(OUT); }
§3.1. The process of extracting the content of any existing .gitignore file
is called "harvesting", and results in one of these:
typedef struct gitignore_harvest { int position; /* 1: before stanza, 2: inside it, 3: after it */ int ignore; /* have we seen a request not to do this? */ struct text_stream *B; /* content of file before stanza */ struct text_stream *G; /* content of stanza (not including comments) */ struct text_stream *A; /* content of file after stanza */ } gitignore_harvest;
- The structure gitignore_harvest is private to this section.
§3.2. Harvest the existing gitignore file content, if any3.2 =
H.position = 1; H.ignore = FALSE; H.B = Str::new(); H.G = Str::new(); H.A = Str::new(); if (TextFiles::exists(F)) TextFiles::read(F, TRUE, NULL, FALSE, Gitignoring::read_helper, NULL, &H);
- This code is used in §3.
void Gitignoring::read_helper(text_stream *line, text_file_position *tfp, void *state) { gitignore_harvest *H = (gitignore_harvest *) state; Str::trim_white_space(line); if (Str::eq(line, I"# This stanza written automatically by inform7")) { if (H->position == 1) H->position = 2; } else if (Str::eq(line, I"# End of stanza written automatically by inform7")) { if (H->position == 2) H->position = 3; } else if (Str::eq(line, I"# No stanza written automatically by inform7")) { H->ignore = TRUE; } else { switch (H->position) { case 1: WRITE_TO(H->B, "%S\n", line); break; case 2: WRITE_TO(H->G, "%S\n", line); break; case 3: WRITE_TO(H->A, "%S\n", line); break; } } }