+ + +

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;
+
+ +

§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);
+
+ +

§4.

+ +
+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;
+        }
+    }
+}
+
+ + +