Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 44 additions & 7 deletions src/pipeline/doc_links_md.c
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,18 @@
* document wrote the path.
*
* Unresolved:
* missing the path is not in the graph, but its directory is: the
* document names something that is not there (doc rot)
* missing the path is not in the graph, but its directory is, or a
* directory above it that the reference wrote is: the document
* names something that is not there (doc rot; a moved or
* renamed directory)
* ambiguous a file name alone that is not next to the document but exists
* elsewhere in the repository: the document does not say which
* Not a reference to this repository at all (LOCAL: no edge, no row): a link
* back into the same document; a path that is neither indexed nor of a type
* the indexer reads (an image, an archive, an extensionless page); a path
* whose directory is not in the repository either (a file a tutorial asks
* the reader to create, a system path, a path above the root); and, in a
* code span, a rooted single segment without a file extension, which is a
* none of whose directories is in the repository either (a file a tutorial
* asks the reader to create, a system path, a path above the root); and, in
* a code span, a rooted single segment without a file extension, which is a
* URL route (`/docs`, `/health`), not the directory of that name.
*/
#include "pipeline/doc_links.h"
Expand Down Expand Up @@ -408,6 +410,25 @@ static const cbm_gbuf_node_t *mdr_folder(const mdr_index_t *x, const cbm_gbuf_t
return path[0] ? (const cbm_gbuf_node_t *)cbm_ht_get(x->folders, path) : NULL;
}

/* Whether a directory above `path` that the reference itself wrote, one
* longer than the `base_len` bytes it was read from, is a Folder. */
static bool mdr_written_ancestor(const mdr_index_t *x, const cbm_gbuf_t *graph, const char *path,
size_t base_len) {
char dir[MDR_PATH_CAP];
size_t n = strlen(path);
if (n >= sizeof(dir)) {
return false;
}
memcpy(dir, path, n + SKIP_ONE);
for (char *s = strrchr(dir, '/'); s && (size_t)(s - dir) > base_len; s = strrchr(dir, '/')) {
*s = '\0';
if (mdr_folder(x, graph, dir)) {
return true;
}
}
return false;
}

static bool mdr_code_label(const char *label) {
for (size_t i = 0; label && MDR_CODE_LABELS[i]; i++) {
if (strcmp(label, MDR_CODE_LABELS[i]) == 0) {
Expand Down Expand Up @@ -1126,8 +1147,19 @@ static void mdr_resolve_path(const mdr_index_t *x, const cbm_gbuf_t *graph, cons
char tries[MDR_TRIES][MDR_PATH_CAP];
int nt = 0;
int attempted = 0;
size_t base_len[MDR_TRIES]; /* the directory each try reads from, before what p wrote */
if (!rooted) {
attempted++;
char lead[MDR_PATH_CAP]; /* p's leading `./` and `../` segments */
char base[MDR_PATH_CAP];
size_t ll = 0;
while (ll < sizeof(lead) - PAIR_LEN - 1 && (strncmp(p + ll, "./", PAIR_LEN) == 0 ||
strncmp(p + ll, "../", PAIR_LEN + 1) == 0)) {
ll += p[ll + 1] == '/' ? PAIR_LEN : PAIR_LEN + 1;
}
memcpy(lead, p, ll);
lead[ll] = '\0';
base_len[nt] = mdr_join(doc_dir, lead, base, sizeof(base)) ? strlen(base) : 0;
nt += mdr_join(doc_dir, p, tries[nt], MDR_PATH_CAP);
}
if (!relative && !r->name_only) {
Expand All @@ -1137,7 +1169,7 @@ static void mdr_resolve_path(const mdr_index_t *x, const cbm_gbuf_t *graph, cons
q++;
}
if (mdr_join("", q, tries[nt], MDR_PATH_CAP) && (nt == 0 || strcmp(tries[0], tries[nt]))) {
nt++;
base_len[nt++] = 0;
}
}
for (int i = 0; i < nt; i++) {
Expand Down Expand Up @@ -1174,7 +1206,8 @@ static void mdr_resolve_path(const mdr_index_t *x, const cbm_gbuf_t *graph, cons
return;
}
/* doc rot when the reference points into the repository: its directory is
* there (a bare name in a link: the document's own directory) */
* there (a bare name in a link: the document's own directory), or a
* directory above it that the reference wrote is (one moved or renamed) */
for (int i = 0; i < nt; i++) {
char parent[MDR_PATH_CAP];
const char *ps = strrchr(tries[i], '/');
Expand All @@ -1185,6 +1218,10 @@ static void mdr_resolve_path(const mdr_index_t *x, const cbm_gbuf_t *graph, cons
out->reason = CBM_DOCLINK_REASON_MISSING;
return;
}
if (pl > 0 && strchr(p, '/') && mdr_written_ancestor(x, graph, tries[i], base_len[i])) {
out->reason = CBM_DOCLINK_REASON_MISSING;
return;
}
if (pl == 0 && r->link && !rooted) {
out->reason = CBM_DOCLINK_REASON_MISSING;
return;
Expand Down
50 changes: 50 additions & 0 deletions tests/test_doc_links_md.c
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,55 @@ TEST(doc_links_md_incremental) {
PASS();
}

/* #2588: a reference into a directory that was moved or renamed named a
* path whose directory is gone, so it counted as no reference to this
* repository: no edge and no row. A directory above it that the reference
* wrote is still there, so it is doc rot. A path none of whose directories
* the repository has stays no reference. */
TEST(doc_links_md_moved_directory_issue2588) {
char tmp[256];
snprintf(tmp, sizeof(tmp), "/tmp/cbm_dlmd_mv_XXXXXX");
ASSERT_NOT_NULL(cbm_mkdtemp(tmp));
char repo[512];
snprintf(repo, sizeof(repo), "%s/repo", tmp);
th_write_file(TH_PATH(repo, "docs/notes.md"), "# Notes\n"
"\n"
"See `src/util/rounding.py::clamp` and "
"[rounding](../src/util/rounding.py).\n"
"Create `scratch/new/thing.py` first.\n");
th_write_file(TH_PATH(repo, "src/ledger.py"), "def balance(ledger):\n"
" return sum(ledger)\n");
th_write_file(TH_PATH(repo, "src/util/rounding.py"), "def clamp(x, lo, hi):\n"
" return max(lo, min(hi, x))\n");
char db[512];
char full_db[512];
snprintf(db, sizeof(db), "%s/inc.db", tmp);
snprintf(full_db, sizeof(full_db), "%s/full.db", tmp);
ASSERT_EQ(dm_index(repo, db, NULL), 0);
char props[512];
char reason[64];
ASSERT_EQ(md_edge(db, "Notes", "rounding.clamp", props, sizeof(props)), 1);

/* src/util moves to src/helpers: both references are missing, on the
* incremental route as in a full index */
th_write_file(TH_PATH(repo, "src/helpers/rounding.py"), "def clamp(x, lo, hi):\n"
" return max(lo, min(hi, x))\n");
ASSERT_EQ(th_rmtree(TH_PATH(repo, "src/util")), 0);
ASSERT_EQ(dm_step(repo, db, full_db, "directory moved", CBM_INCREMENTAL_ROUTE_FORCED_FULL), 0);
ASSERT_EQ(md_edge(db, "Notes", "rounding.clamp", props, sizeof(props)), 0);
dm_row(db, "docs/notes.md", "src/util/rounding.py::clamp", reason, sizeof(reason), NULL, 0);
ASSERT_STR_EQ(reason, "missing");
dm_row(db, "docs/notes.md", "../src/util/rounding.py", reason, sizeof(reason), NULL, 0);
ASSERT_STR_EQ(reason, "missing");

/* no directory of it in the repository: still no reference */
dm_row(db, "docs/notes.md", "scratch/new/thing.py", reason, sizeof(reason), NULL, 0);
ASSERT_STR_EQ(reason, "");

th_cleanup(tmp);
PASS();
}

/* ── ADRs ────────────────────────────────────────────────────────── */

static void md_write_adrs(const char *tmp, const char *first_status) {
Expand Down Expand Up @@ -866,6 +915,7 @@ SUITE(doc_links_md) {
RUN_TEST(doc_links_md_classify_span);
RUN_TEST(doc_links_md_resolve);
RUN_TEST(doc_links_md_incremental);
RUN_TEST(doc_links_md_moved_directory_issue2588);
RUN_TEST(doc_links_md_adr);
RUN_TEST(doc_links_md_adr_label_choice);
RUN_TEST(doc_links_md_adr_reported);
Expand Down
Loading