Compare commits
3
Commits
v0.6.0-rc1
...
v0.6.0-rc4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
469a2e4651 | ||
|
|
518a22d87e | ||
|
|
8c1b4c45f5 |
@@ -193,24 +193,36 @@ jobs:
|
|||||||
API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }}
|
API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }}
|
||||||
TITLE: "Incompatible with upcoming TrueNAS: ${{ steps.report.outputs.refs }}"
|
TITLE: "Incompatible with upcoming TrueNAS: ${{ steps.report.outputs.refs }}"
|
||||||
run: |
|
run: |
|
||||||
body="$(jq -Rs . < /tmp/issue.md)"
|
# python3, not jq: jq is not guaranteed on a self-hosted runner, and a bug
|
||||||
title="$(printf '%s' "$TITLE" | jq -Rs .)"
|
# report that dies on a missing tool is a warning system that does not warn.
|
||||||
|
python3 - <<'PY'
|
||||||
|
import json, os, urllib.error, urllib.request
|
||||||
|
|
||||||
# Same title => same issue. Comment on it instead of filing a new one.
|
api, token, title = os.environ["API"], os.environ["TOKEN"], os.environ["TITLE"]
|
||||||
number="$(curl -sf -H "Authorization: token $TOKEN" \
|
with open("/tmp/issue.md", encoding="utf-8") as fh:
|
||||||
"$API/issues?state=all&type=issues" \
|
body = fh.read()
|
||||||
| jq -r --arg t "$TITLE" '.[] | select(.title == $t) | .number' | head -1)"
|
headers = {"Authorization": f"token {token}",
|
||||||
|
"Content-Type": "application/json"}
|
||||||
|
|
||||||
if [ -n "$number" ]; then
|
def call(url, method, data=None):
|
||||||
curl -sS -X POST "$API/issues/$number/comments" \
|
req = urllib.request.Request(
|
||||||
-H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
|
url, method=method, headers=headers,
|
||||||
-d "$(printf '{"body":%s}' "$body")" -o /dev/null -w 'comment -> %{http_code}\n'
|
data=json.dumps(data).encode() if data else None)
|
||||||
curl -sS -X PATCH "$API/issues/$number" \
|
with urllib.request.urlopen(req) as r: # noqa: S310
|
||||||
-H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
|
return json.load(r) if r.length != 0 else {}
|
||||||
-d '{"state":"open"}' -o /dev/null -w 'reopen -> %{http_code}\n'
|
|
||||||
else
|
# Same title => same issue. Comment on it rather than filing a new one every
|
||||||
curl -sS -X POST "$API/issues" \
|
# morning: a bot that duplicates itself daily gets muted, and then it is not
|
||||||
-H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
|
# a warning system any more.
|
||||||
-d "$(printf '{"title":%s,"body":%s}' "$title" "$body")" \
|
issues = call(f"{api}/issues?state=all&type=issues", "GET")
|
||||||
-o /dev/null -w 'create -> %{http_code}\n'
|
match = next((i for i in issues if i["title"] == title), None)
|
||||||
fi
|
|
||||||
|
if match:
|
||||||
|
n = match["number"]
|
||||||
|
call(f"{api}/issues/{n}/comments", "POST", {"body": body})
|
||||||
|
call(f"{api}/issues/{n}", "PATCH", {"state": "open"})
|
||||||
|
print(f"commented on and reopened issue #{n}")
|
||||||
|
else:
|
||||||
|
made = call(f"{api}/issues", "POST", {"title": title, "body": body})
|
||||||
|
print(f"filed issue #{made['number']}")
|
||||||
|
PY
|
||||||
|
|||||||
@@ -164,27 +164,45 @@ jobs:
|
|||||||
*-rc*|*-beta*|*-alpha*) prerelease=true ;;
|
*-rc*|*-beta*|*-alpha*) prerelease=true ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
# jq -Rs so the notes are JSON-encoded properly: the changelog is full of
|
# python3, not jq. The changelog is full of quotes, backticks and newlines,
|
||||||
# quotes, backticks and newlines, and hand-built JSON would mangle them.
|
# so the body must be properly JSON-encoded -- but `jq` is not guaranteed on
|
||||||
body="$(jq -Rs . < /tmp/notes.md)"
|
# a self-hosted Gitea runner, and a publish step that dies on a missing tool
|
||||||
payload="$(printf '{"tag_name":%s,"name":%s,"body":%s,"prerelease":%s}' \
|
# leaves a tag with no release behind it. python3 is guaranteed: setup-python
|
||||||
"$(printf '%s' "$TAG" | jq -Rs .)" \
|
# ran above.
|
||||||
"$(printf '%s' "$TAG" | jq -Rs .)" \
|
python3 - "$TAG" "$API" "$TOKEN" "$prerelease" <<'PY'
|
||||||
"$body" "$prerelease")"
|
import json, sys, urllib.error, urllib.request
|
||||||
|
|
||||||
existing="$(curl -sf -H "Authorization: token $TOKEN" \
|
tag, api, token, prerelease = sys.argv[1:5]
|
||||||
"$API/releases/tags/$TAG" 2>/dev/null || true)"
|
with open("/tmp/notes.md", encoding="utf-8") as fh:
|
||||||
|
body = fh.read()
|
||||||
|
|
||||||
if [ -n "$existing" ]; then
|
payload = {
|
||||||
id="$(printf '%s' "$existing" | jq -r .id)"
|
"tag_name": tag, "name": tag, "body": body,
|
||||||
echo "Release $TAG exists (id=$id) — updating notes."
|
"prerelease": prerelease == "true",
|
||||||
curl -sS -X PATCH "$API/releases/$id" \
|
}
|
||||||
-H "Authorization: token $TOKEN" \
|
headers = {
|
||||||
-H "Content-Type: application/json" \
|
"Authorization": f"token {token}",
|
||||||
-d "$payload" -o /dev/null -w 'PATCH -> %{http_code}\n'
|
"Content-Type": "application/json",
|
||||||
else
|
}
|
||||||
curl -sS -X POST "$API/releases" \
|
|
||||||
-H "Authorization: token $TOKEN" \
|
def call(url, method, data=None):
|
||||||
-H "Content-Type: application/json" \
|
req = urllib.request.Request(
|
||||||
-d "$payload" -o /dev/null -w 'POST -> %{http_code}\n'
|
url, method=method, headers=headers,
|
||||||
fi
|
data=json.dumps(data).encode() if data else None)
|
||||||
|
with urllib.request.urlopen(req) as r: # noqa: S310
|
||||||
|
return r.status, json.load(r) if r.length != 0 else {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
_, existing = call(f"{api}/releases/tags/{tag}", "GET")
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
if e.code != 404:
|
||||||
|
raise
|
||||||
|
existing = None
|
||||||
|
|
||||||
|
if existing:
|
||||||
|
status, _ = call(f"{api}/releases/{existing['id']}", "PATCH", payload)
|
||||||
|
print(f"updated release {tag} -> {status}")
|
||||||
|
else:
|
||||||
|
status, _ = call(f"{api}/releases", "POST", payload)
|
||||||
|
print(f"created release {tag} (prerelease={payload['prerelease']}) -> {status}")
|
||||||
|
PY
|
||||||
|
|||||||
@@ -66,6 +66,18 @@ worse than no alert, because one day it carries a security fix.
|
|||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
- **Installing the patch permanently blocked updating it.** `install.sh` does
|
||||||
|
`chmod +x update.sh`, and git recorded `update.sh` as `100644` — so the chmod was a
|
||||||
|
*tracked modification*, and `update.sh` refuses to run over a dirty tree. Install
|
||||||
|
once and you could never update again; the error even told you to run
|
||||||
|
`git checkout -- .`, which just undoes the exec bit so the next install can re-dirty
|
||||||
|
it. A real box sat on an old version for exactly this reason.
|
||||||
|
|
||||||
|
Fixed on both sides: the scripts `install.sh` chmods are now executable in git (so
|
||||||
|
the chmod is a no-op), and `update.sh`'s dirty check now looks at **content**, not
|
||||||
|
file mode — `git diff --numstat` reports `0 0` for a mode-only change. A test
|
||||||
|
asserts every script in `install.sh`'s chmod loop is already `100755` in git.
|
||||||
|
|
||||||
- **Nested snapshots were broken on TrueNAS 24.10 and 25.04, and had been all
|
- **Nested snapshots were broken on TrueNAS 24.10 and 25.04, and had been all
|
||||||
along.** `SYNC_BLOCK`'s wrapper spelled out the stock signature and forwarded five
|
along.** `SYNC_BLOCK`'s wrapper spelled out the stock signature and forwarded five
|
||||||
arguments — but those releases declare `restic_backup(middleware, job,
|
arguments — but those releases declare `restic_backup(middleware, job,
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ Clone it onto a **pool** (not the boot device — that is wiped on TrueNAS upgra
|
|||||||
then run `install.sh` as root:
|
then run `install.sh` as root:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://git.onetick.ninja/flan/truenas-truecloud-patch.git \
|
git clone https://github.com/sudolulo/truenas-truecloud-patch.git \
|
||||||
/mnt/tank/truenas-truecloud-patch # replace `tank` with your pool
|
/mnt/tank/truenas-truecloud-patch # replace `tank` with your pool
|
||||||
cd /mnt/tank/truenas-truecloud-patch
|
cd /mnt/tank/truenas-truecloud-patch
|
||||||
sudo bash install.sh
|
sudo bash install.sh
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ python3 /mnt/tank/truenas-truecloud-patch/patch/create_task.py verify
|
|||||||
| What you see | What it means |
|
| What you see | What it means |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `[OK] providers`, `[OK]`/`[SKIP] nested_snapshots` | Fine. Nothing to do. |
|
| `[OK] providers`, `[OK]`/`[SKIP] nested_snapshots` | Fine. Nothing to do. |
|
||||||
| `WARNING: … pattern not found` (UI) | The Angular bundle changed. The UI dropdown reverts to Storj-only, but **backups keep working** — create tasks with `create_task.py` meanwhile, and [open an issue](https://git.onetick.ninja/flan/truenas-truecloud-patch/issues) with your TrueNAS version. |
|
| `WARNING: … pattern not found` (UI) | The Angular bundle changed. The UI dropdown reverts to Storj-only, but **backups keep working** — create tasks with `create_task.py` meanwhile, and [open an issue](https://github.com/sudolulo/truenas-truecloud-patch/issues) with your TrueNAS version. |
|
||||||
| `WARNING: truecloud-patch is NOT COMPATIBLE with this TrueNAS version` | This TrueNAS changed middleware underneath the patch, and the named module was **deliberately not applied** — see `incompatible.json` for exactly which assumption broke. TrueNAS is left stock, so nothing is half-patched. Check [TrueNAS compatibility](../README.md#truenas-compatibility), then `bash update.sh` once a release supports your version; it re-applies itself on the next boot. This is **not** the kill switch and needs no manual reset. |
|
| `WARNING: truecloud-patch is NOT COMPATIBLE with this TrueNAS version` | This TrueNAS changed middleware underneath the patch, and the named module was **deliberately not applied** — see `incompatible.json` for exactly which assumption broke. TrueNAS is left stock, so nothing is half-patched. Check [TrueNAS compatibility](../README.md#truenas-compatibility), then `bash update.sh` once a release supports your version; it re-applies itself on the next boot. This is **not** the kill switch and needs no manual reset. |
|
||||||
| `[FAIL] providers` | **Your B2/S3 backups will not run.** middlewared is fine, but the credential/URL handling is gone. Open an issue with your version. |
|
| `[FAIL] providers` | **Your B2/S3 backups will not run.** middlewared is fine, but the credential/URL handling is gone. Open an issue with your version. |
|
||||||
| `[FAIL] nested_snapshots` | The stock guard is back, so tasks with `snapshot = true` on a nested dataset will fail validation. Turn the option off on those tasks until it's fixed. |
|
| `[FAIL] nested_snapshots` | The stock guard is back, so tasks with `snapshot = true` on a nested dataset will fail validation. Turn the option off on those tasks until it's fixed. |
|
||||||
|
|||||||
+1
-1
@@ -109,7 +109,7 @@ If a module shows `[FAIL]`:
|
|||||||
risk.
|
risk.
|
||||||
4. **If the detail says the module doesn't exist**, a TrueNAS update renamed
|
4. **If the detail says the module doesn't exist**, a TrueNAS update renamed
|
||||||
or restructured the internal API.
|
or restructured the internal API.
|
||||||
[Open an issue](https://git.onetick.ninja/flan/truenas-truecloud-patch/issues)
|
[Open an issue](https://github.com/sudolulo/truenas-truecloud-patch/issues)
|
||||||
with your TrueNAS version number and the full verify output.
|
with your TrueNAS version number and the full verify output.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
+2
-2
@@ -4,7 +4,7 @@
|
|||||||
# Prerequisites: run as root on TrueNAS SCALE with middlewared running.
|
# Prerequisites: run as root on TrueNAS SCALE with middlewared running.
|
||||||
# Clone this repository to a persistent ZFS pool first:
|
# Clone this repository to a persistent ZFS pool first:
|
||||||
#
|
#
|
||||||
# git clone https://git.onetick.ninja/flan/truenas-truecloud-patch \
|
# git clone https://github.com/sudolulo/truenas-truecloud-patch \
|
||||||
# /mnt/<pool>/truenas-truecloud-patch
|
# /mnt/<pool>/truenas-truecloud-patch
|
||||||
# cd /mnt/<pool>/truenas-truecloud-patch && bash install.sh
|
# cd /mnt/<pool>/truenas-truecloud-patch && bash install.sh
|
||||||
#
|
#
|
||||||
@@ -72,7 +72,7 @@ done
|
|||||||
if [ ! -f "$PATCH_DIR/patch/apply.sh" ]; then
|
if [ ! -f "$PATCH_DIR/patch/apply.sh" ]; then
|
||||||
echo "ERROR: patch files not found at $PATCH_DIR/patch/" >&2
|
echo "ERROR: patch files not found at $PATCH_DIR/patch/" >&2
|
||||||
echo "Run install.sh from a clone of the repository on a persistent pool:" >&2
|
echo "Run install.sh from a clone of the repository on a persistent pool:" >&2
|
||||||
echo " git clone https://git.onetick.ninja/flan/truenas-truecloud-patch \\" >&2
|
echo " git clone https://github.com/sudolulo/truenas-truecloud-patch \\" >&2
|
||||||
echo " /mnt/<pool>/truenas-truecloud-patch" >&2
|
echo " /mnt/<pool>/truenas-truecloud-patch" >&2
|
||||||
echo " cd /mnt/<pool>/truenas-truecloud-patch && bash install.sh" >&2
|
echo " cd /mnt/<pool>/truenas-truecloud-patch && bash install.sh" >&2
|
||||||
exit 1
|
exit 1
|
||||||
|
|||||||
+3
-3
@@ -111,7 +111,7 @@ def main():
|
|||||||
print(
|
print(
|
||||||
"[truecloud-patch] WARNING: filterByProviders pattern not found in any JS bundle.\n"
|
"[truecloud-patch] WARNING: filterByProviders pattern not found in any JS bundle.\n"
|
||||||
"[truecloud-patch] The TrueNAS webui may have been restructured in this version.\n"
|
"[truecloud-patch] The TrueNAS webui may have been restructured in this version.\n"
|
||||||
"[truecloud-patch] File an issue at https://git.onetick.ninja/flan/truenas-truecloud-patch\n"
|
"[truecloud-patch] File an issue at https://github.com/sudolulo/truenas-truecloud-patch\n"
|
||||||
f"[truecloud-patch] TrueNAS version info: {_tnversion()}"
|
f"[truecloud-patch] TrueNAS version info: {_tnversion()}"
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
@@ -136,7 +136,7 @@ def main():
|
|||||||
f"[truecloud-patch] WARNING: {count} replacement(s) in {path}; "
|
f"[truecloud-patch] WARNING: {count} replacement(s) in {path}; "
|
||||||
f"expected exactly 1 — skipping write to avoid corrupting the bundle.\n"
|
f"expected exactly 1 — skipping write to avoid corrupting the bundle.\n"
|
||||||
f"[truecloud-patch] File an issue at "
|
f"[truecloud-patch] File an issue at "
|
||||||
f"https://git.onetick.ninja/flan/truenas-truecloud-patch"
|
f"https://github.com/sudolulo/truenas-truecloud-patch"
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -154,7 +154,7 @@ def main():
|
|||||||
"[truecloud-patch] The UI is UNCHANGED and still works. This means the "
|
"[truecloud-patch] The UI is UNCHANGED and still works. This means the "
|
||||||
"pattern no longer fits this TrueNAS build.\n"
|
"pattern no longer fits this TrueNAS build.\n"
|
||||||
"[truecloud-patch] File an issue at "
|
"[truecloud-patch] File an issue at "
|
||||||
"https://git.onetick.ninja/flan/truenas-truecloud-patch"
|
"https://github.com/sudolulo/truenas-truecloud-patch"
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
Regular → Executable
@@ -94,3 +94,40 @@ class TestTheReadmeStaysAReadme:
|
|||||||
assert "24.10" in text[:text.index("## Install")], (
|
assert "24.10" in text[:text.index("## Install")], (
|
||||||
"the minimum TrueNAS version must be visible above the install steps"
|
"the minimum TrueNAS version must be visible above the install steps"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestInstallDoesNotDirtyTheCheckout:
|
||||||
|
"""install.sh chmod +x's scripts. If git records them as 100644, that chmod is a
|
||||||
|
TRACKED MODIFICATION -- and update.sh refuses to run over a dirty tree.
|
||||||
|
|
||||||
|
So installing once permanently blocked updating, for every user, with a message
|
||||||
|
telling them to `git checkout -- .` (which would just undo the exec bit and let
|
||||||
|
the next install re-dirty it). Found on a real box that had been stuck on an old
|
||||||
|
version for exactly this reason.
|
||||||
|
|
||||||
|
Every script install.sh makes executable must already be executable in git.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def test_every_chmodded_script_is_already_executable_in_git(self):
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
with open(os.path.join(ROOT, "install.sh"), encoding="utf-8") as fh:
|
||||||
|
m = re.search(r"^for _exe in (.+?); do", fh.read(), re.M)
|
||||||
|
assert m, "could not find install.sh's chmod loop"
|
||||||
|
scripts = m.group(1).split()
|
||||||
|
|
||||||
|
out = subprocess.run(
|
||||||
|
["git", "ls-files", "-s", *scripts],
|
||||||
|
cwd=ROOT, capture_output=True, text=True, check=True,
|
||||||
|
).stdout
|
||||||
|
|
||||||
|
not_exec = [
|
||||||
|
line.split("\t")[-1] for line in out.strip().splitlines()
|
||||||
|
if not line.startswith("100755")
|
||||||
|
]
|
||||||
|
assert not not_exec, (
|
||||||
|
"install.sh chmod +x's these, but git records them as non-executable — "
|
||||||
|
"so installing dirties the checkout and update.sh then refuses to run:\n "
|
||||||
|
+ "\n ".join(not_exec)
|
||||||
|
)
|
||||||
|
|||||||
@@ -205,3 +205,31 @@ class TestSignificance:
|
|||||||
assert version_tuple("0.4.2") > version_tuple("0.4.1")
|
assert version_tuple("0.4.2") > version_tuple("0.4.1")
|
||||||
# Pre-release suffixes are dropped, not ranked above the release.
|
# Pre-release suffixes are dropped, not ranked above the release.
|
||||||
assert version_tuple("v0.5.0-rc1") == version_tuple("v0.5.0")
|
assert version_tuple("v0.5.0-rc1") == version_tuple("v0.5.0")
|
||||||
|
|
||||||
|
|
||||||
|
class TestCandidateNotesResolveToTheBaseVersion:
|
||||||
|
"""`notes v0.6.0-rc1` must return v0.6.0's section.
|
||||||
|
|
||||||
|
A candidate ships the same code as the release it is a candidate for, and the
|
||||||
|
CHANGELOG only ever has the one section. Without this, the release workflow cut
|
||||||
|
v0.6.0-rc1, passed every gate, and then died extracting the body -- so the tag
|
||||||
|
existed but nothing was ever published. Caught in an rc, which is the entire
|
||||||
|
point of having them.
|
||||||
|
"""
|
||||||
|
|
||||||
|
CHANGELOG = "# C\n\n## v0.6.0 — 2026-07-13\n\n### Added\n- the thing\n\n## v0.5.1 — 2026-07-13\n\n- older\n"
|
||||||
|
|
||||||
|
def test_an_rc_resolves_to_its_base_version(self):
|
||||||
|
body = extract_notes(self.CHANGELOG, "v0.6.0-rc1")
|
||||||
|
assert "the thing" in body
|
||||||
|
assert "older" not in body
|
||||||
|
|
||||||
|
def test_rc10_too(self):
|
||||||
|
assert "the thing" in extract_notes(self.CHANGELOG, "v0.6.0-rc10")
|
||||||
|
|
||||||
|
def test_the_plain_version_still_works(self):
|
||||||
|
assert "the thing" in extract_notes(self.CHANGELOG, "v0.6.0")
|
||||||
|
|
||||||
|
def test_a_genuinely_missing_section_still_raises(self):
|
||||||
|
with pytest.raises(KeyError):
|
||||||
|
extract_notes(self.CHANGELOG, "v9.9.9-rc1")
|
||||||
|
|||||||
@@ -107,10 +107,15 @@ def changelog_versions(text: str) -> list[str]:
|
|||||||
def extract_notes(text: str, version: str) -> str:
|
def extract_notes(text: str, version: str) -> str:
|
||||||
"""The body of one version's section, without its heading.
|
"""The body of one version's section, without its heading.
|
||||||
|
|
||||||
|
A release candidate resolves to its BASE version: v0.6.0-rc2 ships the same code
|
||||||
|
as v0.6.0 and therefore the same notes, and the CHANGELOG only ever has the one
|
||||||
|
section. Without this, the release workflow cut the tag, passed every gate, and
|
||||||
|
then died extracting the body -- so the candidate existed but was never published.
|
||||||
|
|
||||||
Raises KeyError if the version has no section -- a release with an empty or
|
Raises KeyError if the version has no section -- a release with an empty or
|
||||||
wrong body is worse than a failed release.
|
wrong body is worse than a failed release.
|
||||||
"""
|
"""
|
||||||
want = normalise(version)
|
want = base_version(version)
|
||||||
lines = text.splitlines()
|
lines = text.splitlines()
|
||||||
|
|
||||||
start = None
|
start = None
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ cd "$PATCH_DIR"
|
|||||||
|
|
||||||
if ! git rev-parse --git-dir >/dev/null 2>&1; then
|
if ! git rev-parse --git-dir >/dev/null 2>&1; then
|
||||||
echo "ERROR: $PATCH_DIR is not a git clone — nothing to update." >&2
|
echo "ERROR: $PATCH_DIR is not a git clone — nothing to update." >&2
|
||||||
echo " Re-clone from https://git.onetick.ninja/flan/truenas-truecloud-patch" >&2
|
echo " Re-clone from https://github.com/sudolulo/truenas-truecloud-patch" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -136,9 +136,21 @@ fi
|
|||||||
|
|
||||||
# A dirty tree means someone edited or scp'd files in place; merging over that
|
# A dirty tree means someone edited or scp'd files in place; merging over that
|
||||||
# silently loses their changes, or conflicts halfway through.
|
# silently loses their changes, or conflicts halfway through.
|
||||||
if [ -n "$(git status --porcelain --untracked-files=no)" ]; then
|
#
|
||||||
|
# CONTENT changes only. A mode-only change (100644 -> 100755) is not somebody's work
|
||||||
|
# and must not block an update -- and it is not hypothetical: install.sh chmod +x's
|
||||||
|
# these very scripts, so on any version where git recorded one as 100644, INSTALLING
|
||||||
|
# dirtied the checkout and update.sh then refused to run. Install once, and updating
|
||||||
|
# was blocked forever, with an error telling the user to `git checkout -- .` (which
|
||||||
|
# merely undoes the exec bit so the next install can re-dirty it). A real box sat on
|
||||||
|
# an old version for exactly this reason.
|
||||||
|
#
|
||||||
|
# `git diff --numstat` reports "0 0 file" for a mode-only change, so anything with a
|
||||||
|
# nonzero insert or delete count is a genuine edit.
|
||||||
|
_dirty=$(git diff --numstat HEAD -- . | awk '$1 != 0 || $2 != 0 { print $3 }')
|
||||||
|
if [ -n "$_dirty" ]; then
|
||||||
echo "ERROR: the working tree has uncommitted changes:" >&2
|
echo "ERROR: the working tree has uncommitted changes:" >&2
|
||||||
git status --short --untracked-files=no >&2
|
printf ' M %s\n' $_dirty >&2
|
||||||
echo "" >&2
|
echo "" >&2
|
||||||
echo " Refusing to update over them. Commit, stash, or discard them first:" >&2
|
echo " Refusing to update over them. Commit, stash, or discard them first:" >&2
|
||||||
echo " git -C $PATCH_DIR checkout -- ." >&2
|
echo " git -C $PATCH_DIR checkout -- ." >&2
|
||||||
|
|||||||
Reference in New Issue
Block a user