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
6 changes: 6 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,12 @@ make gogenerate

# optional: keep Go modules tidy if go.mod/go.sum changed
go mod tidy

# keep remote refs fresh so base-branch auto-detection is accurate
git fetch origin --prune

# Run before committing or finishing tasks, to ensure we pass the static check
make bazel_lint_changed
```

## Testing
Expand Down
10 changes: 10 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -865,6 +865,16 @@ bazel_ddltest: failpoint-enable bazel_ci_simple_prepare
bazel_lint: bazel_prepare
bazel build $(BAZEL_CMD_CONFIG) //... --//build:with_nogo_flag=$(NOGO_FLAG)

.PHONY: bazel_lint_changed
bazel_lint_changed: bazel_prepare
@PKGS="$$(build/get_changed_bazel_pkgs.sh)"; \
echo "$$PKGS"; \
if [ -z "$$PKGS" ]; then \
echo "No changed bazel packages detected, skip bazel_lint_changed."; \
exit 0; \
fi; \
bazel build $(BAZEL_CMD_CONFIG) $$PKGS --//build:with_nogo_flag=$(NOGO_FLAG)

.PHONY: docker
docker: ## Build TiDB Docker image
docker build -t "$(DOCKERPREFIX)tidb:latest" --build-arg 'GOPROXY=$(shell go env GOPROXY),' -f Dockerfile .
Expand Down
89 changes: 89 additions & 0 deletions build/detect_base_branch.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
#!/usr/bin/env bash
#
# Copyright 2026 PingCAP, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

set -euo pipefail

find_pingcap_remotes() {
local remote url type

# Only consider remotes that point to the official pingcap/tidb repository.
git remote -v |
while read -r remote url type; do
[ "$type" = "(fetch)" ] || continue
case "$url" in
https://github.com/pingcap/tidb | \
https://github.com/pingcap/tidb.git | \
git@github.com:pingcap/tidb | \
git@github.com:pingcap/tidb.git | \
ssh://git@github.com/pingcap/tidb | \
ssh://git@github.com/pingcap/tidb.git)
printf '%s\n' "$remote"
;;
esac
done | sort -u
}

find_base_branch() {
local best_branch=""
local best_score="-1"
local ref branch merge_base score remote
local -a candidate_patterns=()

while IFS= read -r remote; do
[ -n "$remote" ] || continue
candidate_patterns+=(
"refs/remotes/$remote/master"
"refs/remotes/$remote/release-*"
"refs/remotes/$remote/feature/*"
)
done < <(find_pingcap_remotes)

if [ "${#candidate_patterns[@]}" -eq 0 ]; then
echo "ERROR: failed to find remote that points to github.com/pingcap/tidb" >&2
return 1
fi

while IFS= read -r ref; do
[ -n "$ref" ] || continue

branch="${ref#refs/remotes/}"
merge_base="$(git merge-base HEAD "$branch" 2>/dev/null || true)"
[ -n "$merge_base" ] || continue

# Prefer the candidate whose merge-base with HEAD is the newest.
score="$(git show -s --format=%ct "$merge_base" 2>/dev/null || true)"
[[ "$score" =~ ^[0-9]+$ ]] || continue

if [ -z "$best_branch" ] ||
[ "$score" -gt "$best_score" ] ||
{ [ "$score" -eq "$best_score" ] && [[ "$branch" < "$best_branch" ]]; }; then
best_branch="$branch"
best_score="$score"
fi
done < <(
git for-each-ref --format='%(refname)' "${candidate_patterns[@]}" |
sort -u
)

if [ -z "$best_branch" ]; then
echo "ERROR: failed to detect base branch from <pingcap-remote>/master, <pingcap-remote>/release-*, <pingcap-remote>/feature/*" >&2
return 1
fi

echo "$best_branch"
}

find_base_branch
133 changes: 133 additions & 0 deletions build/get_changed_bazel_pkgs.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
#!/usr/bin/env bash
#
# Copyright 2026 PingCAP, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

set -euo pipefail

detect_base_ref() {
local script_dir
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
"$script_dir/detect_base_branch.sh"
}

collect_changed_files() {
local base_ref="$1"

# Include committed, staged, unstaged, and untracked changes so
# "run before commit" can still lint pending local edits.
{
git diff --name-only "$base_ref...HEAD"
git diff --name-only --cached
git diff --name-only
git ls-files --others --exclude-standard
} | sed '/^[[:space:]]*$/d' | sort -u
}

is_high_impact_change() {
local path="$1"
# Changes to Bazel dependency/config wiring should lint the whole repo.
case "$path" in
DEPS.bzl | WORKSPACE | WORKSPACE.bazel | build/BUILD.bazel)
return 0
;;
build/linter/* | *.bzl | BUILD | BUILD.bazel | */BUILD | */BUILD.bazel)
return 0
;;
esac
return 1
}

map_file_to_bazel_target() {
local path="$1"
local d

# Walk up by path even when the file no longer exists (e.g. deletions).
d="$(dirname -- "$path")"
while :; do
if [ "$d" = "." ]; then
if [ -f BUILD ] || [ -f BUILD.bazel ]; then
printf '//:all\n'
return 0
fi
return 1
fi

if [ -f "$d/BUILD" ] || [ -f "$d/BUILD.bazel" ]; then
printf '//%s:all\n' "$d"
return 0
fi

if [ "$d" = "/" ]; then
return 1
fi
d="$(dirname -- "$d")"
done
}

get_changed_pkgs() {
local BASE_REF="${1:-}"
local repo_root
local f target
local -a targets=()
local need_full_lint=0
local first_high_impact=""

if [ -z "$BASE_REF" ]; then
BASE_REF="$(detect_base_ref)" || {
echo "ERROR: failed to detect base ref" >&2
return 2
}
echo "INFO: auto-detected base ref: $BASE_REF" >&2
fi

repo_root="$(git rev-parse --show-toplevel 2>/dev/null)" || return 1
cd -- "$repo_root"

git rev-parse --verify -q "$BASE_REF" >/dev/null 2>&1 || {
echo "ERROR: base ref not found: $BASE_REF" >&2
return 2
}

while IFS= read -r f; do
[ -n "$f" ] || continue

if is_high_impact_change "$f"; then
need_full_lint=1
if [ -z "$first_high_impact" ]; then
first_high_impact="$f"
fi
continue
fi

target="$(map_file_to_bazel_target "$f" || true)"
if [ -n "$target" ]; then
targets+=("$target")
fi
done < <(collect_changed_files "$BASE_REF")

if [ "$need_full_lint" -eq 1 ]; then
echo "INFO: fallback to //... due to high-impact change: $first_high_impact" >&2
printf '//...\n'
return 0
fi

if [ "${#targets[@]}" -eq 0 ]; then
return 0
fi

printf '%s\n' "${targets[@]}" | sort -u
}

get_changed_pkgs "${1:-}"