Skip to content
Merged
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
18 changes: 15 additions & 3 deletions docs/tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,18 @@ folder) still sends the body: HackMD is believed to blank a body the PATCH
omits, so the server reads the current one and sends it back. That moves the
whole body twice more than a metadata change used to: one download before the
write and one upload with it. An edit landing between that read and the write
is reverted; `expected_hash` works here too and catches any made before it. A
title rarely shows through, because a YAML `title:` in the body wins, then a
leading H1, and only then the `title` field. A folder move is not waited on:
is reverted; `expected_hash` works here too and catches any made before it. The
`title` field always takes effect. The body sets a title only when a note is
created without one (a front-matter `title:`, then the first H1, then
"Untitled"), and later body edits never change it, so `hackmd_pull_note` and
`hackmd_push_note` report `title_drift` when the body's own title no longer
matches the listed one. They report it only when they can read that title for
certain (plain front matter, or an H1 that is the body's first line of text),
and push only on a push or a no-op, so a missing `title_drift` does not mean the
titles match. Bodies are stored with `\n` line endings: `\r\n` and a
lone `\r` read back as `\n`, so every body you supply, and every local file read
for sync, is converted before it is written or hashed; a body the server reads
back and resends goes as it was read. A folder move is not waited on:
the result's `folder_placement_confirmed` says whether the read-back already
shows the note there (`null` when no folder was asked for).

Expand Down Expand Up @@ -100,6 +109,9 @@ repository can work with:
1. `hackmd_pull_note` writes the note to an absolute `.md` path and records a
baseline: the exact body as it was at pull time. It returns that body's
`body_hash`, in the same form `hackmd_get_note` and a successful push report.
When it replaced a file it could read, `changes` is a bounded diff of what
the pull changed in it; `title_drift` names the title the body gives when
the listed one differs (see above).
2. You or the agent edit the file locally.
3. `hackmd_get_note` with `local_path` reports where things stand by comparing
the file, the baseline, and the remote note:
Expand Down
2 changes: 1 addition & 1 deletion src/client/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -246,11 +246,11 @@ impl NotesCache {
workspace: &Workspace,
notes: &Arc<[NoteResponse]>,
) {
let size_bytes = cached_note_bytes(workspace, notes);
evict(state, workspace, "replacement");
if self.ttl.is_zero() {
return;
}
let size_bytes = cached_note_bytes(workspace, notes);
if self.max_entries == 0 || size_bytes > self.max_bytes {
tracing::debug!(
cache_event = "skip",
Expand Down
20 changes: 19 additions & 1 deletion src/client/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,12 @@ fn bounded_body(body: &[u8], token: &str) -> String {
// so a token that begins within them is still found and replaced.
let cut = body.len().min(MAX_CHARS * 4 + token.len());
let truncated = cut < body.len();
let mut text = String::from_utf8_lossy(&body[..cut]).replace(token, "[REDACTED]");
let text = String::from_utf8_lossy(&body[..cut]);
let mut text = if token.is_empty() {
text.into_owned()
} else {
text.replace(token, "[REDACTED]")
};

// Each replacement shortens the text, which can pull a token split by the
// cut into the characters shown. Whatever prefix of it the cut left at the
Expand Down Expand Up @@ -598,6 +603,19 @@ mod tests {
assert!(conflict("/v1/teams/notes/folders/f").ends_with("folder name taken"));
}

#[test]
fn error_details_without_a_token_are_preserved_and_bounded() {
assert_eq!(
super::bounded_body(b"upstream failure", ""),
"upstream failure"
);
assert_eq!(super::bounded_body(b"", ""), "");
assert_eq!(
super::bounded_body("é".repeat(301).as_bytes(), ""),
format!("{}…", "é".repeat(300))
);
}

#[test]
fn a_token_split_by_the_cut_is_not_shown() {
// Each redaction shortens the text, so without care the start of the
Expand Down
Loading
Loading