Exclude .claude, .agents and AGENTS.md from git in the new project

After the templates are copied, create-project.sh adds /.claude, /.agents and
/AGENTS.md to .git/info/exclude of the new project as its own step, "Git
excludes". No .gitignore or tracked file changes and nothing is committed.
The step is skipped when the framework steps are skipped, and a path git
already tracks stays tracked and is named in the step.

The .git/info/exclude code of exclude_env_file moves into the shared helper
exclude_from_git (git.sh), which both now use; the .env behavior is unchanged.

Refs #66
Task: MIL-009#2
This commit is contained in:
2026-10-08 14:06:37 +08:00
parent 1b1b6bec2e
commit 4058ab4e3e
6 changed files with 81 additions and 16 deletions
+1
View File
@@ -19,6 +19,7 @@ create_all() {
add_framework add_framework
install_framework install_framework
copy_templates copy_templates
exclude_framework_files
create_env_file create_env_file
} }
+5 -1
View File
@@ -30,8 +30,12 @@ readonly HINT_LICENSE="use a Gitea license key (letters, digits, '.', '+' or '-'
readonly HINT_DIRECTORY="must not be empty, start with '-' or contain control characters" readonly HINT_DIRECTORY="must not be empty, start with '-' or contain control characters"
readonly HINT_TOKEN="8 to 255 letters, digits or _ . ~ + / = -" readonly HINT_TOKEN="8 to 255 letters, digits or _ . ~ + / = -"
readonly ENV_FILE_NAME=".env" readonly ENV_FILE_NAME=".env"
# What the framework installs into the new project that git must not list: the
# folders of the skills and the copy of AGENTS.md, anchored to the project
# root. They go into .git/info/exclude of the new project.
readonly FRAMEWORK_EXCLUDES=(/.claude /.agents /AGENTS.md)
readonly PLAN_STEPS=("GitHub repository" "Gitea repository" "Push mirror" readonly PLAN_STEPS=("GitHub repository" "Gitea repository" "Push mirror"
"Local project" "Framework" "Skills and hooks" "Templates" "Project .env") "Local project" "Framework" "Skills and hooks" "Templates" "Git excludes" "Project .env")
# shellcheck disable=SC2034 # read through namerefs (parse_env_file) # shellcheck disable=SC2034 # read through namerefs (parse_env_file)
readonly CONFIG_KEYS=(GITHUB_API_URL GITHUB_WEB_URL GITEA_URL GITEA_API_URL readonly CONFIG_KEYS=(GITHUB_API_URL GITHUB_WEB_URL GITEA_URL GITEA_API_URL
GITEA_SSH_PORT MIRROR_INTERVAL FRAMEWORK_REPO GITEA_SSH_PORT MIRROR_INTERVAL FRAMEWORK_REPO
+2 -13
View File
@@ -26,19 +26,8 @@ env_file_key_list() {
# file: the entry goes into .git/info/exclude, which is never committed. It # file: the entry goes into .git/info/exclude, which is never committed. It
# does nothing when .env is already ignored. # does nothing when .env is already ignored.
exclude_env_file() { exclude_env_file() {
local dir="$1" gitdir exclude local dir="$1"
if git_project "$dir" check-ignore -q -- "$ENV_FILE_NAME"; then exclude_from_git "$dir" "RepoFoundry: the credentials file of this project" "$ENV_FILE_NAME" ||
return 0
fi
gitdir="$(git_project "$dir" rev-parse --absolute-git-dir)"
exclude="$gitdir/info/exclude"
mkdir -p -- "$gitdir/info"
# Start on a fresh line when the file does not end with one.
if [[ -s $exclude && -n "$(tail -c 1 -- "$exclude")" ]]; then
printf '\n' >>"$exclude"
fi
printf '%s\n' "# RepoFoundry: the credentials file of this project" "$ENV_FILE_NAME" >>"$exclude"
git_project "$dir" check-ignore -q -- "$ENV_FILE_NAME" ||
die "could not make git ignore $ENV_FILE_NAME in $dir; nothing was written to it" die "could not make git ignore $ENV_FILE_NAME in $dir; nothing was written to it"
} }
+37 -1
View File
@@ -4,7 +4,7 @@
# #
# Part of create-project.sh: sourced by it, never run on its own. # Part of create-project.sh: sourced by it, never run on its own.
# #
# Provides: is_framework_skipped, init_framework_submodules, add_framework, run_framework_script, install_skills, install_hooks, install_framework, copy_template, copy_templates # Provides: is_framework_skipped, init_framework_submodules, add_framework, run_framework_script, install_skills, install_hooks, install_framework, copy_template, copy_templates, exclude_framework_files
# is_framework_skipped: succeed when the framework steps are left out because # is_framework_skipped: succeed when the framework steps are left out because
# SSH to Gitea is not available (the Maintainer agreed to that). # SSH to Gitea is not available (the Maintainer agreed to that).
@@ -165,3 +165,39 @@ copy_templates() {
notes="$notes; ${STATE[template_note]}" notes="$notes; ${STATE[template_note]}"
finish_step "$label" "created" "($notes)" finish_step "$label" "created" "($notes)"
} }
# exclude_framework_files: make git ignore .claude, .agents and AGENTS.md in
# the new project, through its .git/info/exclude (never .gitignore, so no
# tracked file changes and nothing is committed). Only when the framework
# steps ran. A path git already tracks stays tracked: an exclusion does not
# apply to it, so the step names it.
exclude_framework_files() {
local label="Git excludes" dir="${PROJECT[directory]}" entry path tracked=() note written
if is_framework_skipped; then
finish_step "$label" "skipped" "(no SSH access to Gitea)"
return 0
fi
begin_step "$label"
exclude_from_git "$dir" "RepoFoundry: the files installed from the framework" "${FRAMEWORK_EXCLUDES[@]}" ||
die "could not make git ignore the framework files in $dir; check $dir/.git/info/exclude"
written="$REPLY"
for entry in "${FRAMEWORK_EXCLUDES[@]}"; do
path="${entry#/}"
if [[ -n "$(git_project "$dir" ls-files -- "$path")" ]]; then
tracked+=("$path")
fi
done
if ((written == 0)); then
note="${FRAMEWORK_EXCLUDES[*]} already excluded"
else
note="${FRAMEWORK_EXCLUDES[*]} added to .git/info/exclude"
fi
if ((${#tracked[@]})); then
note="$note; git tracks ${tracked[*]}, so it is not ignored"
fi
if ((written == 0)); then
finish_step "$label" "reused" "($note)"
else
finish_step "$label" "created" "($note)"
fi
}
+35 -1
View File
@@ -4,7 +4,7 @@
# #
# Part of create-project.sh: sourced by it, never run on its own. # Part of create-project.sh: sourced by it, never run on its own.
# #
# Provides: git_project, fetch_origin # Provides: git_project, exclude_from_git, fetch_origin
# git_project DIR ARGS...: run git in DIR. Prompts are switched off and stdin # git_project DIR ARGS...: run git in DIR. Prompts are switched off and stdin
# is closed (git must not eat the answers meant for later prompts), so a # is closed (git must not eat the answers meant for later prompts), so a
@@ -16,6 +16,40 @@ git_project() {
git -C "$dir" "$@" </dev/null git -C "$dir" "$@" </dev/null
} }
# exclude_from_git DIR COMMENT ENTRY...: make git ignore each ENTRY in DIR
# without touching a tracked file: the entries go into .git/info/exclude,
# which is never committed. An entry that already takes effect (checked by its
# pattern, so a tracked path counts) is left out, and with none left nothing
# is written. The comment line goes above the entries that are written. The
# number of entries written is left in REPLY; the return status is 1 when an
# entry still does not take effect afterwards.
exclude_from_git() {
local dir="$1" comment="$2" gitdir exclude entry path missing=()
shift 2
for entry in "$@"; do
path="${entry#/}"
if ! git_project "$dir" check-ignore --no-index -q -- "${path%/}"; then
missing+=("$entry")
fi
done
REPLY="${#missing[@]}"
if ((REPLY == 0)); then
return 0
fi
gitdir="$(git_project "$dir" rev-parse --absolute-git-dir)"
exclude="$gitdir/info/exclude"
mkdir -p -- "$gitdir/info"
# Start on a fresh line when the file does not end with one.
if [[ -s $exclude && -n "$(tail -c 1 -- "$exclude")" ]]; then
printf '\n' >>"$exclude"
fi
printf '%s\n' "# $comment" "${missing[@]}" >>"$exclude"
for entry in "${missing[@]}"; do
path="${entry#/}"
git_project "$dir" check-ignore --no-index -q -- "${path%/}" || return 1
done
}
# fetch_origin DIR: fetch the Gitea repository into the project. Over SSH the # fetch_origin DIR: fetch the Gitea repository into the project. Over SSH the
# user's key is used. Over HTTPS the token reaches git through a private # user's key is used. Over HTTPS the token reaches git through a private
# GIT_ASKPASS helper and the environment of this one command: it is never part # GIT_ASKPASS helper and the environment of this one command: it is never part
+1
View File
@@ -51,6 +51,7 @@ print_plan() {
say "$(printf ' %-18s: %s' "Framework" "add $(framework_url) as a submodule")" say "$(printf ' %-18s: %s' "Framework" "add $(framework_url) as a submodule")"
say "$(printf ' %-18s: %s' "Skills and hooks" "install once; plan gate $(yes_no "${PROJECT[is_plan_gate_enabled]}")")" say "$(printf ' %-18s: %s' "Skills and hooks" "install once; plan gate $(yes_no "${PROJECT[is_plan_gate_enabled]}")")"
say "$(printf ' %-18s: %s' "Templates" "AGENTS.md and docs/artifact-registry.md (you are asked before a file is replaced)")" say "$(printf ' %-18s: %s' "Templates" "AGENTS.md and docs/artifact-registry.md (you are asked before a file is replaced)")"
say "$(printf ' %-18s: %s' "Git excludes" "${FRAMEWORK_EXCLUDES[*]} go into .git/info/exclude (no tracked file changes)")"
else else
say "$(printf ' %-18s: %s' "Framework" "NOT possible without SSH to Gitea; you will be asked whether to go on without it")" say "$(printf ' %-18s: %s' "Framework" "NOT possible without SSH to Gitea; you will be asked whether to go on without it")"
fi fi