Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
860b2346c6 | ||
|
|
e7366188ac | ||
|
|
dfd12a10cb | ||
|
|
09dffc3933 | ||
|
|
aa0da565e1 |
@@ -0,0 +1,37 @@
|
||||
name: Sync GitHub
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '**'
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Configure Git
|
||||
run: |
|
||||
git config --global user.name "Andrew Sync"
|
||||
git config --global user.email "sync@digitaladapt.com"
|
||||
|
||||
- name: Add GitHub Remote
|
||||
env:
|
||||
SYNC_TOKEN: ${{ secrets.SYNC_GITHUB_TOKEN }}
|
||||
SYNC_TARGET: ${{ vars.SYNC_GITHUB_TARGET }}
|
||||
run: |
|
||||
git remote add github "https://digitaladapt:${SYNC_TOKEN}@github.com/$SYNC_TARGET"
|
||||
|
||||
- name: Push Current Branch
|
||||
run: |
|
||||
git push github HEAD:${GITHUB_REF_NAME}
|
||||
|
||||
- name: Push Tags
|
||||
run: |
|
||||
git push github --tags
|
||||
|
||||
Executable
+68
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
notify() {
|
||||
local color="$1"
|
||||
local msg="$2"
|
||||
local hostname=$(hostname)
|
||||
echo "$msg"
|
||||
ntfy pub -T "${color}_square" -t "Docker Health on ${hostname}" docker "$msg" >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
declare -A compose_projects
|
||||
declare -a standalone_containers
|
||||
|
||||
# Find unhealthy containers
|
||||
while read -r container; do
|
||||
health=$(docker inspect \
|
||||
--format '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' \
|
||||
"$container")
|
||||
|
||||
# Ignore missing health checks and containers still starting
|
||||
if [[ "$health" != "unhealthy" ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
project_dir=$(docker inspect \
|
||||
--format '{{index .Config.Labels "com.docker.compose.project.working_dir"}}' \
|
||||
"$container")
|
||||
|
||||
if [[ -n "$project_dir" && "$project_dir" != "<no value>" ]]; then
|
||||
compose_projects["$project_dir"]+="$container "
|
||||
else
|
||||
standalone_containers+=("$container")
|
||||
fi
|
||||
|
||||
done < <(docker ps -q)
|
||||
|
||||
# Restart compose projects once each
|
||||
for project_dir in "${!compose_projects[@]}"; do
|
||||
containers="${compose_projects[$project_dir]}"
|
||||
|
||||
notify "orange" "Unhealthy containers in $(basename "$project_dir"): $containers"
|
||||
|
||||
if [[ -f "$project_dir/compose.yaml" ]]; then
|
||||
if docker compose \
|
||||
--project-directory "$project_dir" \
|
||||
up -d --force-recreate; then
|
||||
|
||||
notify "green" "Recovery succeeded: $(basename "$project_dir")"
|
||||
else
|
||||
notify "red" "Recovery FAILED: $(basename "$project_dir")"
|
||||
fi
|
||||
else
|
||||
notify "black" "Missing compose.yaml in $project_dir"
|
||||
fi
|
||||
done
|
||||
|
||||
# Restart standalone containers
|
||||
for container in "${standalone_containers[@]}"; do
|
||||
notify "orange" "Unhealthy standalone container $container"
|
||||
|
||||
if docker restart "$container" >/dev/null; then
|
||||
notify "green" "Recovery succeeded: $container"
|
||||
else
|
||||
notify "red" "Recovery FAILED: $container"
|
||||
fi
|
||||
done
|
||||
|
||||
Executable
+73
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# perform a "docker compose pull" on each location specified (recursively), or current location if none specified.
|
||||
#
|
||||
# will only search symbolic links when passed "-l" option, must be before any locations
|
||||
# will only search hidden directories when passed "-h" option, must be before any locations
|
||||
# pull-all.sh [-l][-h] # current location implied
|
||||
# pull-all.sh [-l][-h] ~/my-projects /var/group-projects
|
||||
|
||||
# check for "-l" and "-h" in command prompt
|
||||
followLinks="-H"
|
||||
checkHidden="*/.*/*compose.yaml"
|
||||
while getopts "lh" option; do
|
||||
case $option in
|
||||
l)
|
||||
followLinks="-L"
|
||||
;;
|
||||
h)
|
||||
checkHidden=""
|
||||
;;
|
||||
esac
|
||||
done
|
||||
shift "$((OPTIND-1))"
|
||||
|
||||
if [[ "-H" == "$followLinks" ]]; then
|
||||
echo "use -l to follow symbolic links"
|
||||
fi
|
||||
|
||||
if [[ -n "$checkHidden" ]]; then
|
||||
echo "use -h to check hidden directories"
|
||||
fi
|
||||
|
||||
# function to process each location
|
||||
function process_docker_pull () {
|
||||
startedIn=`pwd`
|
||||
location=$(realpath $(dirname "$@"))
|
||||
line="--- --- --- --- --- --- --- --- ---"
|
||||
# use "--" to tell printf there are no more commands, only strings to print
|
||||
printf -- "--- docker compose pull %s %s ---\n" "$location" "${line:${#location}}"
|
||||
cd "$location"
|
||||
|
||||
# populate $runningServices[]
|
||||
mapfile -t runningServices < <(
|
||||
docker compose ps --services --status running
|
||||
)
|
||||
|
||||
quietDocker=$(docker compose pull --ignore-buildable --ignore-pull-failures)
|
||||
|
||||
if docker compose up -d --dry-run 2>&1 | grep -q "Recreate"; then
|
||||
# Only restart services that were already running
|
||||
if ((${#runningServices[@]} > 0)); then
|
||||
echo "restarting the following services: ${runningServices[@]}"
|
||||
quietDocker=$(docker compose up -d "${runningServices[@]}")
|
||||
else
|
||||
echo "updates downloaded, no services were running"
|
||||
fi
|
||||
else
|
||||
messageSuffix=""
|
||||
if ((${#runningServices[@]} > 0)); then
|
||||
messageSuffix=", services already current: ${runningServices[@]}"
|
||||
fi
|
||||
echo "no changes detected$messageSuffix"
|
||||
fi
|
||||
|
||||
cd "$startedIn"
|
||||
}
|
||||
|
||||
# find all folders named ".git" under given locations,
|
||||
# and call process_git_pull on each location that was found.
|
||||
# we then work on the folder that contained the ".git" folder.
|
||||
|
||||
find "$followLinks" "$@" -type f \( -name 'compose.yaml' -o -name 'docker-compose.yaml' \) -not -path "$checkHidden" | sort | while read -r file; do process_docker_pull "$file"; done
|
||||
|
||||
+563
@@ -0,0 +1,563 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
from urllib.parse import quote
|
||||
|
||||
import requests
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Configuration
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
GITEA_URL = "https://" + os.environ.get("GITEA_DOMAIN", "").rstrip("/")
|
||||
GITEA_TOKEN = os.environ.get("GITEA_TOKEN", "")
|
||||
USERNAME = os.environ.get("GITEA_ADMIN", "andrew")
|
||||
|
||||
BRANCH = "main"
|
||||
TAG_PATTERN = "v*"
|
||||
|
||||
# Number of repositories to request per API page.
|
||||
PAGE_SIZE = 50
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Desired branch protection configuration
|
||||
#
|
||||
# Only fields listed here are managed by this script.
|
||||
# Other Gitea branch-protection settings are left untouched.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
BRANCH_DESIRED = {
|
||||
"rule_name": BRANCH,
|
||||
|
||||
# Direct pushes
|
||||
"enable_push": True,
|
||||
"enable_push_whitelist": True,
|
||||
"push_whitelist_usernames": [USERNAME],
|
||||
"push_whitelist_teams": [],
|
||||
"push_whitelist_deploy_keys": False,
|
||||
|
||||
# Force pushes -- explicitly disabled
|
||||
"enable_force_push": False,
|
||||
"enable_force_push_whitelist": False,
|
||||
"force_push_whitelist_usernames": [],
|
||||
"force_push_whitelist_teams": [],
|
||||
"force_push_whitelist_deploy_keys": False,
|
||||
|
||||
# Pull request approvals
|
||||
"required_approvals": 1,
|
||||
"enable_approvals_whitelist": True,
|
||||
"approvals_whitelist_username": [USERNAME],
|
||||
"approvals_whitelist_teams": [],
|
||||
|
||||
# Pull request merging
|
||||
"enable_merge_whitelist": True,
|
||||
"merge_whitelist_usernames": [USERNAME],
|
||||
"merge_whitelist_teams": [],
|
||||
|
||||
# Status checks
|
||||
"enable_status_check": False,
|
||||
"status_check_contexts": [],
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Gitea nullable branch-protection fields
|
||||
#
|
||||
# Gitea returns None for these fields when the corresponding whitelist
|
||||
# functionality is disabled. Treat those values as equivalent to the
|
||||
# explicit values above when comparing configurations.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
BRANCH_NULL_EQUIVALENTS = {
|
||||
"enable_force_push_whitelist": False,
|
||||
"force_push_whitelist_usernames": [],
|
||||
"force_push_whitelist_teams": [],
|
||||
"force_push_whitelist_deploy_keys": False,
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Desired tag protection configuration
|
||||
#
|
||||
# Tags matching TAG_PATTERN are protected. Only USERNAME may create/delete
|
||||
# those tags.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
TAG_DESIRED = {
|
||||
"name_pattern": TAG_PATTERN,
|
||||
"whitelist_usernames": [USERNAME],
|
||||
"whitelist_teams": [],
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# API session
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
session = requests.Session()
|
||||
session.headers.update({
|
||||
"Authorization": f"token {GITEA_TOKEN}",
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json",
|
||||
})
|
||||
|
||||
|
||||
def api(method, path, **kwargs):
|
||||
"""Make a request to the Gitea API."""
|
||||
|
||||
url = f"{GITEA_URL}/api/v1{path}"
|
||||
|
||||
response = session.request(method, url, **kwargs)
|
||||
|
||||
if not response.ok:
|
||||
print(
|
||||
f"ERROR {method} {path}: "
|
||||
f"{response.status_code} {response.text}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
if response.status_code == 204:
|
||||
return None
|
||||
|
||||
return response.json()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Value comparison
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def values_equal(key, current, desired):
|
||||
"""
|
||||
Compare a value returned by Gitea against the desired value.
|
||||
|
||||
Some Gitea branch-protection fields are returned as None when their
|
||||
associated feature is disabled. Those fields are explicitly handled
|
||||
above so that None and their configured disabled value are equivalent.
|
||||
"""
|
||||
|
||||
if current is None and key in BRANCH_NULL_EQUIVALENTS:
|
||||
return desired == BRANCH_NULL_EQUIVALENTS[key]
|
||||
|
||||
return current == desired
|
||||
|
||||
|
||||
def describe_changes(current, desired, ignored=()):
|
||||
"""
|
||||
Return human-readable descriptions of managed fields that differ.
|
||||
"""
|
||||
|
||||
changes = []
|
||||
|
||||
for key, wanted in desired.items():
|
||||
if key in ignored:
|
||||
continue
|
||||
|
||||
actual = current.get(key)
|
||||
|
||||
if not values_equal(key, actual, wanted):
|
||||
changes.append(
|
||||
f"{key}: {actual!r} -> {wanted!r}"
|
||||
)
|
||||
|
||||
return changes
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Repository enumeration
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def get_all_repositories():
|
||||
"""Enumerate every repository the authenticated user can administer."""
|
||||
|
||||
repos = []
|
||||
page = 1
|
||||
|
||||
while True:
|
||||
batch = api(
|
||||
"GET",
|
||||
"/user/repos",
|
||||
params={
|
||||
"limit": PAGE_SIZE,
|
||||
"page": page,
|
||||
},
|
||||
)
|
||||
|
||||
if not batch:
|
||||
break
|
||||
|
||||
repos.extend(batch)
|
||||
|
||||
if len(batch) < PAGE_SIZE:
|
||||
break
|
||||
|
||||
page += 1
|
||||
|
||||
return repos
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Branch protection
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def get_branch_protection(owner, repo):
|
||||
"""Return the existing protection for BRANCH, or None."""
|
||||
|
||||
protections = api(
|
||||
"GET",
|
||||
f"/repos/{quote(owner)}/{quote(repo)}/branch_protections",
|
||||
)
|
||||
|
||||
for protection in protections:
|
||||
if protection.get("rule_name") == BRANCH:
|
||||
return protection
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def describe_branch_protection(protection):
|
||||
"""Return a concise description of the current branch protection."""
|
||||
|
||||
if protection is None:
|
||||
return "NO PROTECTION"
|
||||
|
||||
return (
|
||||
f"push={protection.get('enable_push')} "
|
||||
f"push_allowlist={protection.get('push_whitelist_usernames')} "
|
||||
f"force_push={protection.get('enable_force_push')} "
|
||||
f"approvals={protection.get('required_approvals')} "
|
||||
f"approval_allowlist="
|
||||
f"{protection.get('approvals_whitelist_username')} "
|
||||
f"merge_allowlist="
|
||||
f"{protection.get('merge_whitelist_usernames')}"
|
||||
)
|
||||
|
||||
|
||||
def apply_branch_protection(owner, repo, existing):
|
||||
"""Create or update the branch protection."""
|
||||
|
||||
encoded_owner = quote(owner)
|
||||
encoded_repo = quote(repo)
|
||||
|
||||
if existing is None:
|
||||
api(
|
||||
"POST",
|
||||
f"/repos/{encoded_owner}/{encoded_repo}/branch_protections",
|
||||
json=BRANCH_DESIRED,
|
||||
)
|
||||
|
||||
return "CREATED"
|
||||
|
||||
# PATCH only the fields explicitly managed by this script.
|
||||
payload = {
|
||||
key: value
|
||||
for key, value in BRANCH_DESIRED.items()
|
||||
if key != "rule_name"
|
||||
}
|
||||
|
||||
api(
|
||||
"PATCH",
|
||||
f"/repos/{encoded_owner}/{encoded_repo}/branch_protections/"
|
||||
f"{quote(BRANCH)}",
|
||||
json=payload,
|
||||
)
|
||||
|
||||
return "UPDATED"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tag protection
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def get_tag_protection(owner, repo):
|
||||
"""Return the existing protection for TAG_PATTERN, or None."""
|
||||
|
||||
protections = api(
|
||||
"GET",
|
||||
f"/repos/{quote(owner)}/{quote(repo)}/tag_protections",
|
||||
)
|
||||
|
||||
for protection in protections:
|
||||
if protection.get("name_pattern") == TAG_PATTERN:
|
||||
return protection
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def describe_tag_protection(protection):
|
||||
"""Return a concise description of the current tag protection."""
|
||||
|
||||
if protection is None:
|
||||
return "NO PROTECTION"
|
||||
|
||||
return (
|
||||
f"users={protection.get('whitelist_usernames')} "
|
||||
f"teams={protection.get('whitelist_teams')}"
|
||||
)
|
||||
|
||||
|
||||
def apply_tag_protection(owner, repo, existing):
|
||||
"""Create or update the tag protection."""
|
||||
|
||||
encoded_owner = quote(owner)
|
||||
encoded_repo = quote(repo)
|
||||
|
||||
if existing is None:
|
||||
api(
|
||||
"POST",
|
||||
f"/repos/{encoded_owner}/{encoded_repo}/tag_protections",
|
||||
json=TAG_DESIRED,
|
||||
)
|
||||
|
||||
return "CREATED"
|
||||
|
||||
# PATCH only the fields explicitly managed by this script.
|
||||
payload = {
|
||||
key: value
|
||||
for key, value in TAG_DESIRED.items()
|
||||
if key != "name_pattern"
|
||||
}
|
||||
|
||||
api(
|
||||
"PATCH",
|
||||
f"/repos/{encoded_owner}/{encoded_repo}/tag_protections/"
|
||||
f"{quote(str(existing['id']))}",
|
||||
json=payload,
|
||||
)
|
||||
|
||||
return "UPDATED"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Main
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description=(
|
||||
"Apply standardized Gitea branch and tag protection."
|
||||
)
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--apply",
|
||||
action="store_true",
|
||||
help=(
|
||||
"Actually modify repositories. Without this, only show "
|
||||
"what would happen."
|
||||
),
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Validate configuration
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
if not GITEA_URL or GITEA_URL == "https://":
|
||||
print("GITEA_DOMAIN is not set.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
if not GITEA_TOKEN:
|
||||
print("GITEA_TOKEN is not set.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Header
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
print(f"Gitea: {GITEA_URL}")
|
||||
print(f"Branch: {BRANCH}")
|
||||
print(f"Tag pattern: {TAG_PATTERN}")
|
||||
print(f"User: {USERNAME}")
|
||||
print()
|
||||
|
||||
if not args.apply:
|
||||
print("*** DRY RUN ***")
|
||||
print("Use --apply to actually make changes.")
|
||||
print()
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Enumerate repositories
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
print("Enumerating repositories...")
|
||||
|
||||
repositories = get_all_repositories()
|
||||
|
||||
print(f"Found {len(repositories)} repositories.")
|
||||
print()
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Counters
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
branch_changed = 0
|
||||
branch_unchanged = 0
|
||||
|
||||
tag_changed = 0
|
||||
tag_unchanged = 0
|
||||
|
||||
skipped = 0
|
||||
failed = 0
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Process repositories
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
for repo in repositories:
|
||||
owner = repo["owner"]["login"]
|
||||
name = repo["name"]
|
||||
|
||||
print(f"[{owner}/{name}]")
|
||||
|
||||
# Archived repositories cannot have their protection modified.
|
||||
if repo.get("archived", False):
|
||||
print(" SKIP: archived")
|
||||
skipped += 1
|
||||
print()
|
||||
continue
|
||||
|
||||
try:
|
||||
# ---------------------------------------------------------------
|
||||
# Branch protection
|
||||
# ---------------------------------------------------------------
|
||||
|
||||
protection = get_branch_protection(owner, name)
|
||||
|
||||
print(
|
||||
f" Branch: {describe_branch_protection(protection)}"
|
||||
)
|
||||
|
||||
if protection is None:
|
||||
print(
|
||||
f" Would CREATE protection for {BRANCH}"
|
||||
)
|
||||
|
||||
if args.apply:
|
||||
result = apply_branch_protection(
|
||||
owner,
|
||||
name,
|
||||
protection,
|
||||
)
|
||||
print(f" {result}")
|
||||
|
||||
branch_changed += 1
|
||||
|
||||
else:
|
||||
branch_changes = describe_changes(
|
||||
protection,
|
||||
BRANCH_DESIRED,
|
||||
ignored=("rule_name",),
|
||||
)
|
||||
|
||||
if not branch_changes:
|
||||
print(" OK: already matches")
|
||||
branch_unchanged += 1
|
||||
|
||||
else:
|
||||
print(
|
||||
f" Would UPDATE protection for {BRANCH}"
|
||||
)
|
||||
|
||||
for change in branch_changes:
|
||||
print(f" {change}")
|
||||
|
||||
if args.apply:
|
||||
result = apply_branch_protection(
|
||||
owner,
|
||||
name,
|
||||
protection,
|
||||
)
|
||||
print(f" {result}")
|
||||
|
||||
branch_changed += 1
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
# Tag protection
|
||||
# ---------------------------------------------------------------
|
||||
|
||||
tag_protection = get_tag_protection(owner, name)
|
||||
|
||||
print(
|
||||
f" Tags: {describe_tag_protection(tag_protection)}"
|
||||
)
|
||||
|
||||
if tag_protection is None:
|
||||
print(
|
||||
f" Would CREATE protection for {TAG_PATTERN}"
|
||||
)
|
||||
|
||||
if args.apply:
|
||||
result = apply_tag_protection(
|
||||
owner,
|
||||
name,
|
||||
tag_protection,
|
||||
)
|
||||
print(f" {result}")
|
||||
|
||||
tag_changed += 1
|
||||
|
||||
else:
|
||||
tag_changes = describe_changes(
|
||||
tag_protection,
|
||||
TAG_DESIRED,
|
||||
ignored=("name_pattern",),
|
||||
)
|
||||
|
||||
if not tag_changes:
|
||||
print(" OK: already matches")
|
||||
tag_unchanged += 1
|
||||
|
||||
else:
|
||||
print(
|
||||
f" Would UPDATE protection for {TAG_PATTERN}"
|
||||
)
|
||||
|
||||
for change in tag_changes:
|
||||
print(f" {change}")
|
||||
|
||||
if args.apply:
|
||||
result = apply_tag_protection(
|
||||
owner,
|
||||
name,
|
||||
tag_protection,
|
||||
)
|
||||
print(f" {result}")
|
||||
|
||||
tag_changed += 1
|
||||
|
||||
except requests.HTTPError:
|
||||
print(" FAILED")
|
||||
failed += 1
|
||||
|
||||
print()
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Summary
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
print("----------------------------------------")
|
||||
print(f"Repositories: {len(repositories)}")
|
||||
print()
|
||||
print(f"Branch changed: {branch_changed}")
|
||||
print(f"Branch unchanged: {branch_unchanged}")
|
||||
print()
|
||||
print(f"Tags changed: {tag_changed}")
|
||||
print(f"Tags unchanged: {tag_unchanged}")
|
||||
print()
|
||||
print(f"Skipped: {skipped}")
|
||||
print(f"Failed: {failed}")
|
||||
|
||||
if not args.apply:
|
||||
print()
|
||||
print("Dry run complete. Nothing was changed.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -66,6 +66,12 @@ Plug 'airblade/vim-gitgutter'
|
||||
Plug 'vim-airline/vim-airline'
|
||||
Plug 'vim-airline/vim-airline-themes'
|
||||
|
||||
" twig support (php templating)
|
||||
Plug 'lumiliet/vim-twig'
|
||||
|
||||
" AnsiEsc to make editing colorful files better
|
||||
Plug 'powerman/vim-plugin-AnsiEsc'
|
||||
|
||||
call plug#end()
|
||||
|
||||
" config git-gutter
|
||||
|
||||
Reference in New Issue
Block a user