Accept MIL-009 and add the Git excludes step (task 2) #71

Merged
Tirsvad merged 3 commits from mil-009-exclude-framework-files into main 2026-10-08 08:13:53 +02:00
6 changed files with 81 additions and 16 deletions
Showing only changes of commit 4058ab4e3e - Show all commits
+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