Compare commits
7 Commits
master
...
feature/gi
| Author | SHA1 | Date |
|---|---|---|
|
|
02d830aeba | |
|
|
c644faaeb3 | |
|
|
31c3f4bc08 | |
|
|
6c14f69dac | |
|
|
34481b92fb | |
|
|
d8b49609bb | |
|
|
84614e708d |
|
|
@ -0,0 +1,3 @@
|
|||
|
||||
# Don't index SpecStory auto-save files, but allow explicit context inclusion via @ references
|
||||
.specstory/**
|
||||
31
.env.example
31
.env.example
|
|
@ -16,7 +16,7 @@ API_VERSION=1.0.0
|
|||
MAX_UPLOAD_SIZE_MB=5
|
||||
|
||||
# LibreOffice soffice service port (used by docker/soffice service)
|
||||
SOFFICE_HOST=localhost
|
||||
SOFFICE_HOST=rag-soffice #localhost
|
||||
SOFFICE_PORT=8003
|
||||
|
||||
# ============================================
|
||||
|
|
@ -25,33 +25,16 @@ SOFFICE_PORT=8003
|
|||
# CHROMA_SERVER_HOST: ChromaDB 服务器地址
|
||||
# - 使用 host 网络模式: localhost
|
||||
# - 远程服务器: 192.168.1.100 或 chromadb.example.com
|
||||
CHROMA_SERVER_HOST=localhost
|
||||
CHROMA_SERVER_PORT=8002
|
||||
CHROMA_SERVER_HOST=rag-chromadb #localhost
|
||||
CHROMA_SERVER_PORT=8000 #8002
|
||||
CHROMA_COLLECTION_NAME=rag_collection
|
||||
|
||||
# ============================================
|
||||
# LLM 配置 (用于文本生成)
|
||||
# Ollama 配置
|
||||
# ============================================
|
||||
# LLM provider: ollama, openai 等
|
||||
LLM_PROVIDER=ollama
|
||||
LLM_BASE_URL=http://localhost:11434/v1
|
||||
LLM_MODEL=qwen3:8b
|
||||
LLM_API_KEY= # 如使用openai等需要API Key的服务
|
||||
|
||||
# ============================================
|
||||
# Embedding 配置 (用于向量检索)
|
||||
# ============================================
|
||||
# Embedding provider: ollama, openai 等
|
||||
EMBEDDING_PROVIDER=ollama
|
||||
EMBEDDING_BASE_URL=http://localhost:11434
|
||||
EMBEDDING_MODEL=qwen3-embedding:0.6b
|
||||
EMBEDDING_API_KEY= # 如使用openai等需要API Key的服务
|
||||
|
||||
# ============================================
|
||||
# 兼容旧版本配置 (已废弃,仍可用但推荐使用上面的配置)
|
||||
# ============================================
|
||||
OLLAMA_BASE_URL=http://localhost:11434
|
||||
OLLAMA_MODEL=qwen3:8b
|
||||
# OLLAMA_BASE_URL: Ollama 服务地址
|
||||
OLLAMA_BASE_URL=http://host.docker.internal:11434
|
||||
OLLAMA_MODEL=qwen3:1.7b
|
||||
OLLAMA_EMBEDDING_MODEL=qwen3-embedding:0.6b
|
||||
|
||||
# ============================================
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
# SpecStory project identity file
|
||||
/.project.json
|
||||
# SpecStory explanation file
|
||||
/.what-is-this.md
|
||||
|
|
@ -29,6 +29,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|||
libssl-dev \
|
||||
libcrypto++-dev \
|
||||
libgmp-dev \
|
||||
git \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# 配置 pip 镜像源(加速 Python 包安装)
|
||||
|
|
@ -40,7 +41,7 @@ COPY requirements.txt /app/
|
|||
|
||||
# 安装 Python 依赖
|
||||
RUN pip install --upgrade pip && \
|
||||
pip install -r requirements.txt
|
||||
pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
|
||||
|
||||
# 复制项目文件
|
||||
COPY . /app/
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@
|
|||
# 检查 Ollama 是否运行
|
||||
curl http://localhost:11434/api/tags
|
||||
|
||||
# 下载所需的模型(如果未下载)
|
||||
# 下载所需的模型(如果未下载)(可以使用更小的模型)
|
||||
ollama pull qwen3:235b # LLM模型,用于文本生成
|
||||
ollama pull qwen3-embedding:8b # Embedding模型,用于向量化
|
||||
```
|
||||
|
|
@ -54,7 +54,7 @@ ollama pull qwen3-embedding:8b # Embedding模型,用于向量化
|
|||
```env
|
||||
# ChromaDB 配置(Docker 模式,使用 host 网络)
|
||||
CHROMA_SERVER_HOST=localhost
|
||||
CHROMA_SERVER_PORT=8002
|
||||
CHROMA_SERVER_PORT=8000
|
||||
|
||||
# Ollama 配置(使用 host 网络模式)
|
||||
OLLAMA_BASE_URL=http://localhost:11434
|
||||
|
|
@ -370,7 +370,7 @@ docker-compose up -d chromadb
|
|||
docker-compose up -d soffice-service
|
||||
|
||||
# 5. 验证 ChromaDB 服务运行
|
||||
curl http://localhost:8002/docs
|
||||
curl http://localhost:8000/docs
|
||||
|
||||
# 6. 验证 LibreOffice 服务运行
|
||||
curl http://localhost:8003/health
|
||||
|
|
|
|||
134
api/main.py
134
api/main.py
|
|
@ -1365,6 +1365,32 @@ async def create_config(config: Dict[str, Any]):
|
|||
config["host"].lower(),
|
||||
folder_path
|
||||
])
|
||||
elif config_type == "git":
|
||||
# Git配置需要:仓库地址或服务器信息
|
||||
if config.get("git_mode") == "single":
|
||||
# 单仓库模式需要:仓库地址
|
||||
if not config.get("git_repo_url"):
|
||||
raise HTTPException(status_code=400, detail="Git单仓库配置必须包含仓库地址")
|
||||
# 为单仓库生成唯一标识符
|
||||
repo_url = config["git_repo_url"].lower().replace("/", "_").replace(":", "_").replace(".", "_").replace("@", "_")
|
||||
unique_id_parts.extend([
|
||||
"single",
|
||||
repo_url
|
||||
])
|
||||
else:
|
||||
# 服务器模式需要:服务器地址、路径
|
||||
if not config.get("git_server_url") or not config.get("git_server_path"):
|
||||
raise HTTPException(status_code=400, detail="Git服务器配置必须包含服务器地址和路径")
|
||||
# 为服务器模式生成唯一标识符
|
||||
server_url = config["git_server_url"].lower().replace("/", "_").replace(":", "_").replace(".", "_")
|
||||
server_path = config["git_server_path"].lower().replace("/", "_").replace(":", "_").replace("\\", "_")
|
||||
if server_path.startswith("_"):
|
||||
server_path = server_path[1:]
|
||||
unique_id_parts.extend([
|
||||
"server",
|
||||
server_url,
|
||||
server_path
|
||||
])
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail=f"不支持的配置类型: {config_type}")
|
||||
|
||||
|
|
@ -1401,8 +1427,25 @@ async def create_config(config: Dict[str, Any]):
|
|||
existing_config_data.get('folder_path') == config.get('folder_path')):
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail=f"已存在相同服务器和路径的文件夹配置。如需调整,请点击配置列表中的配置并修改配置内容。"
|
||||
detail=f"已存在相同源的文件夹配置。如需调整,请点击配置列表中的配置并修改配置内容。"
|
||||
)
|
||||
elif config_type == 'git':
|
||||
# For git configs, same source means same repo url or server info
|
||||
if config.get('git_mode') == 'single' and existing_config_data.get('git_mode') == 'single':
|
||||
# Single repo mode: same repo url
|
||||
if existing_config_data.get('git_repo_url') == config.get('git_repo_url'):
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail=f"已存在相同源的Git单仓库配置。如需调整,请点击配置列表中的配置并修改配置内容。"
|
||||
)
|
||||
elif config.get('git_mode') != 'single' and existing_config_data.get('git_mode') != 'single':
|
||||
# Server mode: same server url and path
|
||||
if (existing_config_data.get('git_server_url') == config.get('git_server_url') and
|
||||
existing_config_data.get('git_server_path') == config.get('git_server_path')):
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail=f"已存在相同源的Git服务器配置。如需调整,请点击配置列表中的配置并修改配置内容。"
|
||||
)
|
||||
except sqlite3.OperationalError as e:
|
||||
# 表不存在的情况,会在后面创建表
|
||||
logger.warning(f"SQLite table error: {e}. This is expected if the table doesn't exist yet.")
|
||||
|
|
@ -1977,5 +2020,94 @@ async def get_table_structure(params: TableParams):
|
|||
raise HTTPException(status_code=500, detail=f"Database connection failed: {str(e)}")
|
||||
|
||||
|
||||
# Git connection test endpoint
|
||||
from fastapi import Body
|
||||
|
||||
@app.post("/git/test-connection")
|
||||
async def test_git_connection(connection_data: Dict[str, Any] = Body(...)):
|
||||
"""
|
||||
Test Git connection
|
||||
|
||||
Args:
|
||||
connection_data: Connection data including git_mode, git_protocol, and connection details
|
||||
|
||||
Returns:
|
||||
Connection test result
|
||||
"""
|
||||
try:
|
||||
git_mode = connection_data.get("git_mode")
|
||||
git_repo_url = connection_data.get("git_repo_url")
|
||||
git_branch = connection_data.get("git_branch")
|
||||
git_ssh_host = connection_data.get("git_ssh_host")
|
||||
git_ssh_port = connection_data.get("git_ssh_port", 22)
|
||||
git_ssh_username = connection_data.get("git_ssh_username", "git")
|
||||
git_ssh_password = connection_data.get("git_ssh_password")
|
||||
git_server_url = connection_data.get("git_server_url")
|
||||
git_server_port = connection_data.get("git_server_port", 22)
|
||||
git_server_username = connection_data.get("git_server_username", "git")
|
||||
git_server_password = connection_data.get("git_server_password")
|
||||
git_server_path = connection_data.get("git_server_path")
|
||||
git_protocol = connection_data.get("git_protocol")
|
||||
git_token = connection_data.get("git_token")
|
||||
|
||||
# 协议类型验证逻辑
|
||||
if git_protocol == "https" and not git_token:
|
||||
raise HTTPException(status_code=400, detail="HTTPS令牌是必填项")
|
||||
|
||||
# 根据Git模式验证必填字段
|
||||
if git_mode == "single":
|
||||
if git_protocol == "ssh" and (not git_ssh_host or not git_ssh_username):
|
||||
raise HTTPException(status_code=400, detail="SSH主机地址和用户名是必填项")
|
||||
if not git_repo_url:
|
||||
raise HTTPException(status_code=400, detail="单仓库模式必须包含Git仓库地址")
|
||||
elif git_mode == "server":
|
||||
if not git_server_url or not git_server_username or not git_server_path:
|
||||
raise HTTPException(status_code=400, detail="服务器模式必须包含Git服务器地址、用户名和路径")
|
||||
|
||||
# 导入GitSync类和GitDataSourceConfig
|
||||
from sync.git_sync import GitSync
|
||||
from config import GitDataSourceConfig
|
||||
|
||||
# 创建GitDataSourceConfig对象
|
||||
config = GitDataSourceConfig(
|
||||
name="test",
|
||||
git_mode=git_mode,
|
||||
git_repo_url=git_repo_url,
|
||||
git_branch=git_branch,
|
||||
git_ssh_host=git_ssh_host,
|
||||
git_ssh_port=git_ssh_port,
|
||||
git_ssh_username=git_ssh_username,
|
||||
git_ssh_password=git_ssh_password,
|
||||
git_server_url=git_server_url,
|
||||
git_server_port=git_server_port,
|
||||
git_server_username=git_server_username,
|
||||
git_server_password=git_server_password,
|
||||
git_server_path=git_server_path,
|
||||
git_protocol=git_protocol,
|
||||
git_token=git_token
|
||||
)
|
||||
|
||||
# 创建GitSync实例
|
||||
git_sync = GitSync(config)
|
||||
|
||||
# 测试连接
|
||||
success, repositories = git_sync.test_connection()
|
||||
|
||||
# 构建响应
|
||||
if success:
|
||||
response = {"message": "Git连接成功!"}
|
||||
if repositories:
|
||||
response["repositories"] = repositories
|
||||
return response
|
||||
else:
|
||||
raise HTTPException(status_code=500, detail="Git连接失败")
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error testing Git connection: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Git连接失败: {str(e)}")
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
150
config.py
150
config.py
|
|
@ -111,6 +111,47 @@ class FolderDataSourceConfig(BaseDataSourceConfig):
|
|||
self.ignore_patterns = ignore_patterns # 忽略的文件模式列表
|
||||
|
||||
|
||||
class GitDataSourceConfig(BaseDataSourceConfig):
|
||||
"""Git data source configuration"""
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
git_mode: str,
|
||||
git_repo_url: Optional[str] = None,
|
||||
git_branch: Optional[str] = "main",
|
||||
git_ssh_host: Optional[str] = None,
|
||||
git_ssh_port: int = 22,
|
||||
git_ssh_username: str = "git",
|
||||
git_ssh_password: Optional[str] = None,
|
||||
git_server_url: Optional[str] = None,
|
||||
git_server_port: int = 22,
|
||||
git_server_username: str = "git",
|
||||
git_server_password: Optional[str] = None,
|
||||
git_server_path: Optional[str] = None,
|
||||
git_protocol: str = "https",
|
||||
git_token: Optional[str] = None,
|
||||
git_interval: int = 300,
|
||||
git_repositories: Optional[List[Dict[str, str]]] = None
|
||||
):
|
||||
super().__init__(name, "git")
|
||||
self.git_mode = git_mode # git模式:single或server
|
||||
self.git_repo_url = git_repo_url # 单仓库模式的仓库地址
|
||||
self.git_branch = git_branch # 分支名称
|
||||
self.git_ssh_host = git_ssh_host # 单仓库模式的SSH主机地址
|
||||
self.git_ssh_port = git_ssh_port # 单仓库模式的SSH端口
|
||||
self.git_ssh_username = git_ssh_username # 单仓库模式的SSH用户名
|
||||
self.git_ssh_password = git_ssh_password # 单仓库模式的SSH密码
|
||||
self.git_server_url = git_server_url # 服务器模式的服务器地址
|
||||
self.git_server_port = git_server_port # 服务器模式的端口
|
||||
self.git_server_username = git_server_username # 服务器模式的用户名
|
||||
self.git_server_password = git_server_password # 服务器模式的密码
|
||||
self.git_server_path = git_server_path # 服务器模式的路径
|
||||
self.git_protocol = git_protocol # 协议类型:https或ssh
|
||||
self.git_token = git_token # HTTPS令牌
|
||||
self.git_interval = git_interval # 轮询间隔(秒)
|
||||
self.git_repositories = git_repositories or [] # 用户选择的仓库和分支
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
"""
|
||||
Application settings
|
||||
|
|
@ -138,24 +179,13 @@ class Settings(BaseSettings):
|
|||
CHROMA_DB_PATH: str = "./chroma_db" # Only used for PersistentClient mode
|
||||
CHROMA_COLLECTION_NAME: str = "rag_collection"
|
||||
|
||||
# LLM Settings
|
||||
# LLM provider: "ollama", "vllm", "openai", "deepseek" etc.
|
||||
LLM_PROVIDER: str = "ollama"
|
||||
LLM_BASE_URL: str = "http://localhost:11434"
|
||||
LLM_MODEL: str = "qwen3:8b"
|
||||
LLM_API_KEY: Optional[str] = None
|
||||
|
||||
# Embedding Settings
|
||||
# Embedding provider: "ollama", "vllm", "openai" etc.
|
||||
EMBEDDING_PROVIDER: str = "ollama"
|
||||
EMBEDDING_BASE_URL: str = "http://localhost:11434"
|
||||
EMBEDDING_MODEL: str = "qwen3-embedding:0.6b"
|
||||
EMBEDDING_API_KEY: Optional[str] = None
|
||||
|
||||
# Legacy Ollama Settings (for backward compatibility)
|
||||
# Ollama Settings
|
||||
# Configure OLLAMA_BASE_URL in .env file based on your deployment
|
||||
# - Local: http://localhost:11434
|
||||
# - Remote: http://192.168.1.100:11434
|
||||
OLLAMA_BASE_URL: str = "http://localhost:11434"
|
||||
OLLAMA_MODEL: str = "qwen3:8b"
|
||||
OLLAMA_EMBEDDING_MODEL: str = "qwen3-embedding:0.6b"
|
||||
OLLAMA_MODEL: str = "qwen3:1.7b" # LLM model for text generation
|
||||
OLLAMA_EMBEDDING_MODEL: str = "qwen3-embedding:0.6b" # Embedding model for vectorization
|
||||
|
||||
# RAG Settings
|
||||
EMBEDDING_DIMENSION: int = 768
|
||||
|
|
@ -195,71 +225,6 @@ class Settings(BaseSettings):
|
|||
extra="ignore" # Ignore extra fields in .env file that are not defined in Settings
|
||||
)
|
||||
|
||||
def get_llm_config(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Get LLM configuration with backward compatibility for legacy OLLAMA_* settings.
|
||||
|
||||
Returns:
|
||||
Dictionary with provider, base_url, model, and api_key
|
||||
"""
|
||||
if self.LLM_PROVIDER == "ollama":
|
||||
return {
|
||||
"provider": "ollama",
|
||||
"base_url": self.LLM_BASE_URL or self.OLLAMA_BASE_URL,
|
||||
"model": self.LLM_MODEL or self.OLLAMA_MODEL,
|
||||
"api_key": self.LLM_API_KEY
|
||||
}
|
||||
elif self.LLM_PROVIDER == "openai":
|
||||
return {
|
||||
"provider": "openai",
|
||||
"base_url": self.LLM_BASE_URL or "https://api.openai.com/v1",
|
||||
"model": self.LLM_MODEL,
|
||||
"api_key": self.LLM_API_KEY
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"provider": self.LLM_PROVIDER,
|
||||
"base_url": self.LLM_BASE_URL,
|
||||
"model": self.LLM_MODEL,
|
||||
"api_key": self.LLM_API_KEY
|
||||
}
|
||||
|
||||
def get_embedding_config(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Get Embedding configuration with backward compatibility for legacy OLLAMA_* settings.
|
||||
|
||||
Returns:
|
||||
Dictionary with provider, base_url, model, and api_key
|
||||
"""
|
||||
if self.EMBEDDING_PROVIDER == "ollama":
|
||||
return {
|
||||
"provider": "ollama",
|
||||
"base_url": self.EMBEDDING_BASE_URL or self.OLLAMA_BASE_URL,
|
||||
"model": self.EMBEDDING_MODEL or self.OLLAMA_EMBEDDING_MODEL,
|
||||
"api_key": self.EMBEDDING_API_KEY
|
||||
}
|
||||
elif self.EMBEDDING_PROVIDER == "vllm":
|
||||
return {
|
||||
"provider": "vllm",
|
||||
"base_url": self.EMBEDDING_BASE_URL,
|
||||
"model": self.EMBEDDING_MODEL,
|
||||
"api_key": self.EMBEDDING_API_KEY
|
||||
}
|
||||
elif self.EMBEDDING_PROVIDER == "openai":
|
||||
return {
|
||||
"provider": "openai",
|
||||
"base_url": self.EMBEDDING_BASE_URL or "https://api.openai.com/v1",
|
||||
"model": self.EMBEDDING_MODEL,
|
||||
"api_key": self.EMBEDDING_API_KEY
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"provider": self.EMBEDDING_PROVIDER,
|
||||
"base_url": self.EMBEDDING_BASE_URL,
|
||||
"model": self.EMBEDDING_MODEL,
|
||||
"api_key": self.EMBEDDING_API_KEY
|
||||
}
|
||||
|
||||
def get_data_sources(self) -> List[BaseDataSourceConfig]:
|
||||
"""
|
||||
Get list of data source configurations
|
||||
|
|
@ -336,6 +301,27 @@ class Settings(BaseSettings):
|
|||
recursive=ds_config.get('recursive', True),
|
||||
ignore_patterns=ds_config.get('ignore_patterns', None)
|
||||
))
|
||||
elif source_type == 'git':
|
||||
# Create git data source
|
||||
configs.append(GitDataSourceConfig(
|
||||
name=name, # 使用数据库表中的name列
|
||||
git_mode=ds_config.get('git_mode', 'single'),
|
||||
git_repo_url=ds_config.get('git_repo_url', None),
|
||||
git_branch=ds_config.get('git_branch', 'main'),
|
||||
git_ssh_host=ds_config.get('git_ssh_host', None),
|
||||
git_ssh_port=ds_config.get('git_ssh_port', 22),
|
||||
git_ssh_username=ds_config.get('git_ssh_username', 'git'),
|
||||
git_ssh_password=ds_config.get('git_ssh_password', None),
|
||||
git_server_url=ds_config.get('git_server_url', None),
|
||||
git_server_port=ds_config.get('git_server_port', 22),
|
||||
git_server_username=ds_config.get('git_server_username', 'git'),
|
||||
git_server_password=ds_config.get('git_server_password', None),
|
||||
git_server_path=ds_config.get('git_server_path', None),
|
||||
git_protocol=ds_config.get('git_protocol', 'https'),
|
||||
git_token=ds_config.get('git_token', None),
|
||||
git_interval=ds_config.get('git_interval', 300),
|
||||
git_repositories=ds_config.get('git_repositories', None)
|
||||
))
|
||||
else:
|
||||
from loguru import logger
|
||||
logger.warning(f"Unknown data source type: {source_type}, skipping")
|
||||
|
|
|
|||
|
|
@ -52,7 +52,10 @@ services:
|
|||
container_name: rag-api
|
||||
# 使用宿主机网络模式,可以直接访问宿主机上的服务(Ollama、MySQL 等)
|
||||
# 注意:使用 host 网络模式时,不能使用 ports 映射,容器直接使用宿主机的网络
|
||||
network_mode: host
|
||||
# network_mode: host # 注释/删除host网络模式(Windows下无效)
|
||||
# 添加端口映射(Windows下开发)
|
||||
ports:
|
||||
- "${API_PORT:-8001}:8001"
|
||||
# 自动读取 .env 文件(如果存在)
|
||||
env_file:
|
||||
- .env
|
||||
|
|
@ -66,6 +69,8 @@ services:
|
|||
# 可选:挂载本地 NLTK 数据,避免容器内重复下载
|
||||
- ./nltk_data:/app/nltk_data:ro
|
||||
- ./static:/app/static:ro
|
||||
# 挂载测试git服务器目录
|
||||
- ./test-git-server:/app/test-git-server:rw
|
||||
environment:
|
||||
# 所有配置都从 .env 文件读取,使用 ${VAR:-default} 语法提供默认值
|
||||
# API 配置
|
||||
|
|
@ -92,7 +97,7 @@ services:
|
|||
# Ollama 配置
|
||||
- OLLAMA_BASE_URL=${OLLAMA_BASE_URL:-http://localhost:11434}
|
||||
- OLLAMA_MODEL=${OLLAMA_MODEL:-qwen3:1.7b}
|
||||
- OLLAMA_EMBEDDING_MODEL=${OLLAMA_EMBEDDING_MODEL:-qwen3-embedding:0.6b}
|
||||
- OLLAMA_EMBEDDING_MODEL=${OLLAMA_EMBEDDING_MODEL:-qwen3:embedding:0.6b}
|
||||
|
||||
# RAG 配置
|
||||
- EMBEDDING_DIMENSION=${EMBEDDING_DIMENSION:-768}
|
||||
|
|
@ -139,4 +144,3 @@ services:
|
|||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ RUN apt-get update \
|
|||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt /app/requirements.txt
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
RUN pip install --no-cache-dir -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
|
||||
|
||||
COPY docker/soffice/app.py /app/app.py
|
||||
COPY docker/soffice/README.md /app/README.md
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
54
main.py
54
main.py
|
|
@ -24,22 +24,44 @@ def main():
|
|||
logger.info(f"Starting {settings.API_TITLE} v{settings.API_VERSION}")
|
||||
logger.info(f"Server will run on {settings.API_HOST}:{settings.API_PORT}")
|
||||
|
||||
# Configure uvicorn for production with better concurrency
|
||||
uvicorn.run(
|
||||
"api.main:app",
|
||||
host=settings.API_HOST,
|
||||
port=settings.API_PORT,
|
||||
reload=False,
|
||||
log_level="info",
|
||||
# Enable access logs for better monitoring
|
||||
access_log=True,
|
||||
# Set timeout for long-running requests
|
||||
timeout_keep_alive=120,
|
||||
# Limit max requests to prevent memory issues
|
||||
limit_max_requests=1000,
|
||||
# Graceful shutdown timeout
|
||||
timeout_graceful_shutdown=30
|
||||
)
|
||||
try:
|
||||
# Test importing the API module first to catch import errors
|
||||
logger.info("Testing API module import...")
|
||||
from api import main as api_main
|
||||
logger.info("API module imported successfully")
|
||||
|
||||
# Import the app directly
|
||||
from api.main import app
|
||||
logger.info("App imported successfully")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to import API module: {e}")
|
||||
import traceback
|
||||
logger.error(traceback.format_exc())
|
||||
raise
|
||||
|
||||
try:
|
||||
# Configure uvicorn for production with better concurrency
|
||||
logger.info("Starting uvicorn server...")
|
||||
uvicorn.run(
|
||||
app,
|
||||
host=settings.API_HOST,
|
||||
port=settings.API_PORT,
|
||||
reload=False,
|
||||
log_level="debug",
|
||||
# Enable access logs for better monitoring
|
||||
access_log=True,
|
||||
# Set timeout for long-running requests
|
||||
timeout_keep_alive=120,
|
||||
# Limit max requests to prevent memory issues
|
||||
limit_max_requests=1000,
|
||||
# Graceful shutdown timeout
|
||||
timeout_graceful_shutdown=30
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to start server: {e}")
|
||||
import traceback
|
||||
logger.error(traceback.format_exc())
|
||||
raise
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ from llama_index.core.query_engine import RetrieverQueryEngine
|
|||
from llama_index.core.response_synthesizers import ResponseMode
|
||||
from llama_index.core.base.response.schema import StreamingResponse
|
||||
from llama_index.llms.ollama import Ollama
|
||||
from llama_index.llms.openai import OpenAI
|
||||
from llama_index.core import PromptTemplate
|
||||
from typing import AsyncIterator, Optional, Tuple
|
||||
import asyncio
|
||||
|
|
@ -83,71 +82,42 @@ class RAGEngine:
|
|||
"""Main RAG engine for query processing"""
|
||||
|
||||
@staticmethod
|
||||
def check_llm_connection(provider: str, base_url: str, model: str, api_key: Optional[str] = None) -> Tuple[bool, str]:
|
||||
def check_ollama_connection() -> Tuple[bool, str]:
|
||||
"""
|
||||
Check if LLM server is accessible and connection can be established
|
||||
Check if Ollama server is accessible and connection can be established
|
||||
|
||||
Args:
|
||||
provider: LLM provider (ollama, vllm, openai)
|
||||
base_url: Base URL for the LLM service
|
||||
model: Model name
|
||||
api_key: Optional API key for authentication
|
||||
|
||||
Returns:
|
||||
Tuple of (is_connected: bool, error_message: str)
|
||||
If connected, error_message will be empty string
|
||||
"""
|
||||
try:
|
||||
headers = {}
|
||||
if api_key:
|
||||
headers["Authorization"] = f"Bearer {api_key}"
|
||||
logger.info(f"Checking Ollama connection to {settings.OLLAMA_BASE_URL}...")
|
||||
|
||||
if provider == "ollama":
|
||||
logger.info(f"Checking Ollama connection to {base_url}...")
|
||||
with httpx.Client(timeout=10.0) as client:
|
||||
response = client.get(f"{base_url}/api/tags")
|
||||
if response.status_code == 200:
|
||||
models = response.json().get("models", [])
|
||||
model_names = [m.get("name", "unknown") for m in models]
|
||||
logger.info(f"✓ Ollama connection successful (found {len(models)} model(s): {', '.join(model_names[:3])}{'...' if len(model_names) > 3 else ''})")
|
||||
return True, ""
|
||||
else:
|
||||
error_message = f"Ollama API returned status {response.status_code}: {response.text}"
|
||||
logger.error(f"✗ Ollama connection failed: {error_message}")
|
||||
return False, error_message
|
||||
elif provider == "openai":
|
||||
logger.info(f"Checking OpenAI-compatible API connection to {base_url}...")
|
||||
if not api_key:
|
||||
logger.warning("OpenAI provider requires API key, skipping connection check")
|
||||
# Test connection by calling Ollama API
|
||||
with httpx.Client(timeout=10.0) as client:
|
||||
response = client.get(f"{settings.OLLAMA_BASE_URL}/api/tags")
|
||||
if response.status_code == 200:
|
||||
models = response.json().get("models", [])
|
||||
model_names = [m.get("name", "unknown") for m in models]
|
||||
logger.info(f"✓ Ollama connection successful (found {len(models)} model(s): {', '.join(model_names[:3])}{'...' if len(model_names) > 3 else ''})")
|
||||
return True, ""
|
||||
with httpx.Client(timeout=10.0, headers=headers) as client:
|
||||
response = client.get(f"{base_url}/models")
|
||||
if response.status_code == 200:
|
||||
models = response.json().get("data", [])
|
||||
model_names = [m.get("id", "unknown") for m in models]
|
||||
logger.info(f"✓ OpenAI-compatible API connection successful (found {len(models)} model(s): {', '.join(model_names[:3])}{'...' if len(model_names) > 3 else ''})")
|
||||
return True, ""
|
||||
else:
|
||||
error_message = f"OpenAI API returned status {response.status_code}: {response.text}"
|
||||
logger.error(f"✗ OpenAI connection failed: {error_message}")
|
||||
return False, error_message
|
||||
else:
|
||||
logger.warning(f"Unknown LLM provider: {provider}, skipping connection check")
|
||||
return True, ""
|
||||
|
||||
else:
|
||||
error_message = f"Ollama API returned status {response.status_code}: {response.text}"
|
||||
logger.error(f"✗ Ollama connection failed: {error_message}")
|
||||
return False, error_message
|
||||
except httpx.ConnectError as e:
|
||||
error_message = f"Cannot connect to {provider} server at {base_url}. " \
|
||||
f"Please check if server is running and accessible."
|
||||
logger.error(f"✗ {provider} connection failed: {error_message}")
|
||||
error_message = f"Cannot connect to Ollama server at {settings.OLLAMA_BASE_URL}. " \
|
||||
f"Please check if Ollama server is running and accessible."
|
||||
logger.error(f"✗ Ollama connection failed: {error_message}")
|
||||
return False, error_message
|
||||
except httpx.TimeoutException:
|
||||
error_message = f"Connection to {provider} server at {base_url} timed out. " \
|
||||
f"Please check if server is running and accessible."
|
||||
logger.error(f"✗ {provider} connection failed: {error_message}")
|
||||
error_message = f"Connection to Ollama server at {settings.OLLAMA_BASE_URL} timed out. " \
|
||||
f"Please check if Ollama server is running and accessible."
|
||||
logger.error(f"✗ Ollama connection failed: {error_message}")
|
||||
return False, error_message
|
||||
except Exception as e:
|
||||
error_message = f"Unexpected error while checking {provider} connection: {str(e)}"
|
||||
logger.error(f"✗ {provider} connection check failed: {error_message}")
|
||||
error_message = f"Unexpected error while checking Ollama connection: {str(e)}"
|
||||
logger.error(f"✗ Ollama connection check failed: {error_message}")
|
||||
return False, error_message
|
||||
|
||||
def __init__(
|
||||
|
|
@ -159,56 +129,41 @@ class RAGEngine:
|
|||
request_timeout: float = 120.0,
|
||||
):
|
||||
self.vector_store_manager = vector_store_manager
|
||||
# Configurable LLM / prompt parameters
|
||||
self._user_prompt_template = prompt_template
|
||||
self._system_prompt = system_prompt
|
||||
self._temperature = temperature
|
||||
self._request_timeout = request_timeout
|
||||
|
||||
llm_config = settings.get_llm_config()
|
||||
provider = llm_config["provider"]
|
||||
base_url = llm_config["base_url"]
|
||||
model = llm_config["model"]
|
||||
api_key = llm_config["api_key"]
|
||||
|
||||
is_connected, error_message = self.check_llm_connection(provider, base_url, model, api_key)
|
||||
# Check Ollama connection before initializing
|
||||
import os
|
||||
skip_ollama_check = os.environ.get("SKIP_OLLAMA_CHECK", "0") == "1"
|
||||
if not skip_ollama_check:
|
||||
is_connected, error_message = self.check_ollama_connection()
|
||||
else:
|
||||
logger.info("Skipping Ollama connection check (SKIP_OLLAMA_CHECK is set)")
|
||||
is_connected, error_message = True, ""
|
||||
if not is_connected:
|
||||
error_msg = (
|
||||
f"Error: Cannot connect to {provider} server.\n"
|
||||
f"Warning: Cannot connect to Ollama server.\n"
|
||||
f" {error_message}\n"
|
||||
f"Connection info: {base_url}\n"
|
||||
f"Connection info: {settings.OLLAMA_BASE_URL}\n"
|
||||
f"Please check:\n"
|
||||
f" 1. {provider} server is running\n"
|
||||
f" 2. {provider} server is accessible from this host\n"
|
||||
f" 3. LLM_BASE_URL is correctly configured\n"
|
||||
f" 4. Firewall rules allow connection"
|
||||
f" 1. Ollama server is running\n"
|
||||
f" 2. Ollama server is accessible from this host\n"
|
||||
f" 3. OLLAMA_BASE_URL is correctly configured\n"
|
||||
f" 4. Firewall rules allow connection to Ollama port"
|
||||
)
|
||||
logger.error(error_msg)
|
||||
raise RuntimeError(error_msg)
|
||||
logger.warning(error_msg)
|
||||
# 继续初始化,即使Ollama连接失败
|
||||
is_connected = True
|
||||
|
||||
if provider == "ollama":
|
||||
self.llm = Ollama(
|
||||
model=model,
|
||||
base_url=base_url,
|
||||
temperature=self._temperature,
|
||||
request_timeout=self._request_timeout,
|
||||
)
|
||||
elif provider == "openai":
|
||||
self.llm = OpenAI(
|
||||
model=model,
|
||||
base_url=base_url,
|
||||
api_key=api_key,
|
||||
temperature=self._temperature,
|
||||
timeout=self._request_timeout,
|
||||
)
|
||||
else:
|
||||
self.llm = Ollama(
|
||||
model=model,
|
||||
base_url=base_url,
|
||||
temperature=self._temperature,
|
||||
request_timeout=self._request_timeout,
|
||||
)
|
||||
|
||||
self._llm_provider = provider
|
||||
self.llm = Ollama(
|
||||
model=settings.OLLAMA_MODEL,
|
||||
base_url=settings.OLLAMA_BASE_URL,
|
||||
temperature=self._temperature,
|
||||
request_timeout=self._request_timeout,
|
||||
)
|
||||
|
||||
def extract_text_from_chunk(self, chunk) -> Optional[str]:
|
||||
if hasattr(chunk, 'delta'):
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ from chromadb.config import Settings as ChromaSettings
|
|||
from llama_index.vector_stores.chroma import ChromaVectorStore
|
||||
from llama_index.core import VectorStoreIndex, StorageContext
|
||||
from llama_index.embeddings.ollama import OllamaEmbedding
|
||||
from llama_index.embeddings.openai import OpenAIEmbedding
|
||||
from loguru import logger
|
||||
from config import settings
|
||||
|
||||
|
|
@ -25,7 +24,8 @@ class VectorStoreManager:
|
|||
self.vector_store = None
|
||||
self.index = None
|
||||
self.embed_model = None
|
||||
self._initialize()
|
||||
# 懒加载:不在__init__中初始化,而是在首次使用时初始化
|
||||
self._initialized = False
|
||||
|
||||
def _cleanup_lock_files(self):
|
||||
"""Clean up ChromaDB lock files if they exist (only for PersistentClient mode)"""
|
||||
|
|
@ -62,6 +62,12 @@ class VectorStoreManager:
|
|||
except Exception as e:
|
||||
logger.debug(f"Error during lock file cleanup: {e}")
|
||||
|
||||
def initialize(self):
|
||||
"""Initialize the vector store if not already initialized"""
|
||||
if not self._initialized:
|
||||
self._initialize()
|
||||
self._initialized = True
|
||||
|
||||
def _initialize(self, retry_count: int = 3):
|
||||
"""
|
||||
Initialize ChromaDB client and collection
|
||||
|
|
@ -107,31 +113,11 @@ class VectorStoreManager:
|
|||
metadata={"hnsw:space": "cosine"}
|
||||
)
|
||||
|
||||
# Initialize embedding model based on provider
|
||||
embed_config = settings.get_embedding_config()
|
||||
provider = embed_config["provider"]
|
||||
base_url = embed_config["base_url"]
|
||||
model = embed_config["model"]
|
||||
api_key = embed_config["api_key"]
|
||||
|
||||
if provider == "ollama":
|
||||
self.embed_model = OllamaEmbedding(
|
||||
model_name=model,
|
||||
base_url=base_url
|
||||
)
|
||||
elif provider == "openai":
|
||||
self.embed_model = OpenAIEmbedding(
|
||||
model=model,
|
||||
base_url=base_url,
|
||||
api_key=api_key
|
||||
)
|
||||
else:
|
||||
self.embed_model = OllamaEmbedding(
|
||||
model_name=model,
|
||||
base_url=base_url
|
||||
)
|
||||
|
||||
logger.info(f"Initialized {provider} embedding model: {model}")
|
||||
# Initialize embedding model
|
||||
self.embed_model = OllamaEmbedding(
|
||||
model_name=settings.OLLAMA_EMBEDDING_MODEL,
|
||||
base_url=settings.OLLAMA_BASE_URL
|
||||
)
|
||||
|
||||
# Create ChromaVectorStore
|
||||
self.vector_store = ChromaVectorStore(chroma_collection=self.collection)
|
||||
|
|
@ -240,6 +226,7 @@ class VectorStoreManager:
|
|||
Format: {db_source}_{table_name}_{id} or {db_source}_{table_name}_{id}_chunk_{index}
|
||||
"""
|
||||
try:
|
||||
self.initialize()
|
||||
# Get all IDs and metadata from ChromaDB collection
|
||||
# Note: 'ids' is automatically returned, don't include it in the include parameter
|
||||
results = self.collection.get(include=['metadatas'])
|
||||
|
|
@ -291,6 +278,7 @@ class VectorStoreManager:
|
|||
True if document exists, False otherwise
|
||||
"""
|
||||
try:
|
||||
self.initialize()
|
||||
# Try to get the document from ChromaDB
|
||||
results = self.collection.get(ids=[doc_id])
|
||||
return len(results.get('ids', [])) > 0
|
||||
|
|
@ -316,6 +304,7 @@ class VectorStoreManager:
|
|||
- chunk_index: Chunk index (if available)
|
||||
"""
|
||||
try:
|
||||
self.initialize()
|
||||
# Query ChromaDB by metadata filter
|
||||
# ChromaDB supports filtering by metadata using where clause
|
||||
# Simple format: {"metadata_field": value} is equivalent to {"metadata_field": {"$eq": value}}
|
||||
|
|
@ -396,6 +385,7 @@ class VectorStoreManager:
|
|||
update if ID exists, insert if not (for incremental sync).
|
||||
"""
|
||||
try:
|
||||
self.initialize()
|
||||
if not documents:
|
||||
logger.warning("No documents to add")
|
||||
return
|
||||
|
|
@ -795,6 +785,7 @@ class VectorStoreManager:
|
|||
doc_ids: List of document IDs to delete
|
||||
"""
|
||||
try:
|
||||
self.initialize()
|
||||
if not doc_ids:
|
||||
return
|
||||
|
||||
|
|
@ -812,6 +803,7 @@ class VectorStoreManager:
|
|||
db_source: Source string to filter documents (e.g., 'ruoyi-gitlink_pms_product_requirement')
|
||||
"""
|
||||
try:
|
||||
self.initialize()
|
||||
if not target_db_source:
|
||||
return
|
||||
|
||||
|
|
@ -830,6 +822,7 @@ class VectorStoreManager:
|
|||
True if collection has documents, False otherwise
|
||||
"""
|
||||
try:
|
||||
self.initialize()
|
||||
count = self.collection.count()
|
||||
return count > 0
|
||||
except Exception as e:
|
||||
|
|
@ -849,6 +842,7 @@ class VectorStoreManager:
|
|||
Raises:
|
||||
RuntimeError: If vector store is not initialized
|
||||
"""
|
||||
self.initialize()
|
||||
if self.index is None:
|
||||
raise RuntimeError("Vector store index not initialized")
|
||||
|
||||
|
|
@ -861,8 +855,10 @@ class VectorStoreManager:
|
|||
def reset(self):
|
||||
"""Reset the vector store (delete all data)"""
|
||||
try:
|
||||
self.initialize()
|
||||
self.chroma_client.delete_collection(name=settings.CHROMA_COLLECTION_NAME)
|
||||
self._initialize()
|
||||
self._initialized = True
|
||||
logger.info("Vector store reset successfully")
|
||||
except Exception as e:
|
||||
logger.error(f"Error resetting vector store: {e}")
|
||||
|
|
@ -879,6 +875,7 @@ class VectorStoreManager:
|
|||
指定db_source的metadata中的content_column
|
||||
"""
|
||||
try:
|
||||
self.initialize()
|
||||
# 使用ChromaDB的where过滤条件
|
||||
origin_res = self.collection.get(
|
||||
where={"db_source": {"$eq": target_db_source}},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
"""Simple server runner"""
|
||||
import uvicorn
|
||||
from api.main import app
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run(
|
||||
app,
|
||||
host="0.0.0.0",
|
||||
port=8001,
|
||||
log_level="debug"
|
||||
)
|
||||
|
|
@ -19,6 +19,7 @@
|
|||
<option value="">选择类型</option>
|
||||
<option value="database">数据库</option>
|
||||
<option value="folder">文件夹</option>
|
||||
<option value="git">Git代码库</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -75,6 +76,7 @@
|
|||
<select id="configType" name="type" required>
|
||||
<option value="database">数据库 (database)</option>
|
||||
<option value="folder">文件夹 (folder)</option>
|
||||
<option value="git">Git代码库 (git)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" id="dbTypeGroup" style="display: none;">
|
||||
|
|
|
|||
|
|
@ -224,6 +224,7 @@ function generateConfigForm(config) {
|
|||
<select id="formType" disabled>
|
||||
<option value="database" ${config.type === 'database' ? 'selected' : ''}>数据库 (database)</option>
|
||||
<option value="folder" ${config.type === 'folder' ? 'selected' : ''}>文件夹 (folder)</option>
|
||||
<option value="git" ${config.type === 'git' ? 'selected' : ''}>Git代码库 (git)</option>
|
||||
</select>
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -285,6 +286,113 @@ function generateConfigForm(config) {
|
|||
bindFolderEvents();
|
||||
}
|
||||
|
||||
// Git配置
|
||||
if (config.type === 'git') {
|
||||
const gitSection = document.createElement('div');
|
||||
gitSection.innerHTML = `
|
||||
<h3 class="section-title">Git配置</h3>
|
||||
<div class="form-group">
|
||||
<label for="formGitMode">Git模式 <span class="required">*</span></label>
|
||||
<select id="formGitMode" required>
|
||||
<option value="single" ${config.git_mode === 'single' ? 'selected' : ''}>单仓库模式</option>
|
||||
<option value="server" ${config.git_mode === 'server' ? 'selected' : ''}>服务器模式</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="singleRepoConfig" style="${config.git_mode === 'single' ? 'display: block;' : 'display: none;'}">
|
||||
<div class="form-group">
|
||||
<label for="formGitRepoUrl">Git仓库地址 <span class="required">*</span></label>
|
||||
<input type="text" id="formGitRepoUrl" value="${config.git_repo_url || ''}" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="formGitBranch">分支名称 <span class="required">*</span></label>
|
||||
<input type="text" id="formGitBranch" value="${config.git_branch || 'main'}" required>
|
||||
</div>
|
||||
</div>
|
||||
<div id="serverRepoConfig" style="${config.git_mode === 'server' ? 'display: block;' : 'display: none;'}">
|
||||
<div class="form-group">
|
||||
<label for="formGitServerUrl">Git服务器地址 <span class="required">*</span></label>
|
||||
<input type="text" id="formGitServerUrl" value="${config.git_server_url || ''}" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="formGitServerPort">Git服务器端口 <span class="required">*</span></label>
|
||||
<input type="number" id="formGitServerPort" value="${config.git_server_port || 22}" min="1" max="65535" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="formGitServerUsername">Git服务器用户名 <span class="required">*</span></label>
|
||||
<input type="text" id="formGitServerUsername" value="${config.git_server_username || 'git'}" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="formGitServerPassword">Git服务器密码 <span class="required">*</span></label>
|
||||
<input type="password" id="formGitServerPassword" value="${config.git_server_password || ''}" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="formGitServerPath">Git服务器路径 <span class="required">*</span></label>
|
||||
<input type="text" id="formGitServerPath" value="${config.git_server_path || ''}" required>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="button" id="testGitSshConnectionBtn" class="btn secondary" style="margin-top: 10px;">🔌 测试SSH连接</button>
|
||||
<button type="button" id="getGitRepositoriesBtn" class="btn primary" style="margin-top: 10px; margin-left: 10px;">📋 获取仓库列表</button>
|
||||
</div>
|
||||
<div class="form-group" id="gitRepositoriesSection" style="display: none;">
|
||||
<label>选择仓库及分支 <span class="required">*</span></label>
|
||||
<div id="gitRepositoriesList" class="repositories-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="formGitProtocol">协议类型 <span class="required">*</span></label>
|
||||
<select id="formGitProtocol" required>
|
||||
<option value="https" ${config.git_protocol === 'https' ? 'selected' : ''}>HTTPS</option>
|
||||
<option value="ssh" ${config.git_protocol === 'ssh' ? 'selected' : ''}>SSH</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="httpsConfig" style="${config.git_protocol === 'https' ? 'display: block;' : 'display: none;'}">
|
||||
<div class="form-group">
|
||||
<label for="formGitToken">HTTPS令牌 <span class="required">*</span></label>
|
||||
<input type="password" id="formGitToken" value="${config.git_token || ''}" required>
|
||||
</div>
|
||||
</div>
|
||||
<div id="sshConfig" style="${config.git_protocol === 'ssh' ? 'display: block;' : 'display: none;'}">
|
||||
<div class="form-group">
|
||||
<label for="formGitSshHost">SSH主机地址 <span class="required">*</span></label>
|
||||
<input type="text" id="formGitSshHost" value="${config.git_ssh_host || ''}" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="formGitSshPort">SSH端口 <span class="required">*</span></label>
|
||||
<input type="number" id="formGitSshPort" value="${config.git_ssh_port || 22}" min="1" max="65535" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="formGitSshUsername">SSH用户名 <span class="required">*</span></label>
|
||||
<input type="text" id="formGitSshUsername" value="${config.git_ssh_username || 'git'}" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="formGitSshPassword">SSH密码 <span class="required">*</span></label>
|
||||
<input type="password" id="formGitSshPassword" value="${config.git_ssh_password || ''}" required>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="button" id="testSingleGitSshConnectionBtn" class="btn secondary" style="margin-top: 10px;">🔌 测试SSH连接</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="formGitInterval">轮询间隔 (秒)</label>
|
||||
<input type="number" id="formGitInterval" value="${config.git_interval || 300}" min="60" max="3600">
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="button" id="testGitConnectionBtn" class="btn secondary" style="margin-top: 10px;">🔌 测试Git连接</button>
|
||||
</div>
|
||||
`;
|
||||
formElement.appendChild(gitSection);
|
||||
|
||||
// 绑定Git模式切换事件
|
||||
document.getElementById('formGitMode').addEventListener('change', function() {
|
||||
const mode = this.value;
|
||||
document.getElementById('singleRepoConfig').style.display = mode === 'single' ? 'block' : 'none';
|
||||
document.getElementById('serverRepoConfig').style.display = mode === 'server' ? 'block' : 'none';
|
||||
});
|
||||
|
||||
// 绑定Git事件
|
||||
bindGitEvents();
|
||||
}
|
||||
|
||||
if (config.type === 'database') {
|
||||
// 数据库连接配置(放在前面,方便先测试连接)
|
||||
const connectionSection = document.createElement('div');
|
||||
|
|
@ -1085,6 +1193,19 @@ async function handleAddConfigDirectly() {
|
|||
username: '',
|
||||
password: ''
|
||||
};
|
||||
} else if (configType === 'git') {
|
||||
tempConfig = {
|
||||
type: configType,
|
||||
git_mode: 'single',
|
||||
git_repo_url: '',
|
||||
git_branch: 'main',
|
||||
git_server_url: '',
|
||||
git_server_path: '',
|
||||
git_protocol: 'https',
|
||||
git_username: '',
|
||||
git_password: '',
|
||||
git_interval: 300
|
||||
};
|
||||
} else {
|
||||
alert('不支持的配置类型');
|
||||
return;
|
||||
|
|
@ -1150,6 +1271,20 @@ async function handleAddConfig(event) {
|
|||
username: '',
|
||||
password: ''
|
||||
};
|
||||
} else if (configType === 'git') {
|
||||
configData = {
|
||||
name: configName,
|
||||
type: configType,
|
||||
git_mode: 'single',
|
||||
git_repo_url: '',
|
||||
git_branch: 'main',
|
||||
git_server_url: '',
|
||||
git_server_path: '',
|
||||
git_protocol: 'https',
|
||||
git_username: '',
|
||||
git_password: '',
|
||||
git_interval: 300
|
||||
};
|
||||
} else {
|
||||
alert('不支持的配置类型');
|
||||
return;
|
||||
|
|
@ -1217,12 +1352,23 @@ async function saveConfig() {
|
|||
// 文件夹:folder_主机_文件夹路径(替换特殊字符)
|
||||
const folderName = formData.folder_path ? formData.folder_path.replace(/[\\/:*?"<>|]/g, '_') : 'unknown';
|
||||
generatedName = `folder_${formData.host || 'unknown'}_${folderName}`;
|
||||
} else if (formData.type === 'git') {
|
||||
// Git配置:git_模式_仓库信息
|
||||
if (formData.git_mode === 'single') {
|
||||
const repoName = formData.git_repo_url ? formData.git_repo_url.split('/').pop().replace('.git', '') : 'unknown';
|
||||
generatedName = `git_single_${repoName}_${formData.git_branch || 'main'}`;
|
||||
} else {
|
||||
const serverName = formData.git_server_url ? formData.git_server_url.replace(/[\\/:*?"<>|]/g, '_') : 'unknown';
|
||||
const pathName = formData.git_server_path ? formData.git_server_path.replace(/[\\/:*?"<>|]/g, '_') : 'unknown';
|
||||
generatedName = `git_server_${serverName}_${pathName}`;
|
||||
}
|
||||
} else {
|
||||
// 不支持的配置类型
|
||||
alert('不支持的配置类型');
|
||||
return;
|
||||
}
|
||||
|
||||
formData.name = generatedName;
|
||||
}
|
||||
|
||||
// 根据不同类型检查特定字段
|
||||
|
|
@ -1238,12 +1384,23 @@ async function saveConfig() {
|
|||
missingFields.push('内容列');
|
||||
}
|
||||
} else if (formData.type === 'folder') {
|
||||
if (!formData.folder_path) missingFields.push('文件夹路径');
|
||||
if (!formData.host) missingFields.push('主机地址');
|
||||
if (!formData.port) missingFields.push('端口');
|
||||
if (!formData.username) missingFields.push('用户名');
|
||||
if (!formData.password) missingFields.push('密码');
|
||||
if (!formData.folder_path) missingFields.push('文件夹路径');
|
||||
if (!formData.host) missingFields.push('主机地址');
|
||||
if (!formData.port) missingFields.push('端口');
|
||||
if (!formData.username) missingFields.push('用户名');
|
||||
if (!formData.password) missingFields.push('密码');
|
||||
} else if (formData.type === 'git') {
|
||||
if (!formData.git_mode) missingFields.push('Git模式');
|
||||
if (formData.git_mode === 'single') {
|
||||
if (!formData.git_repo_url) missingFields.push('Git仓库地址');
|
||||
if (!formData.git_branch) missingFields.push('分支名称');
|
||||
} else if (formData.git_mode === 'server') {
|
||||
if (!formData.git_server_url) missingFields.push('Git服务器地址');
|
||||
if (!formData.git_server_path) missingFields.push('Git服务器路径');
|
||||
}
|
||||
if (!formData.git_protocol) missingFields.push('协议类型');
|
||||
if (!formData.git_token) missingFields.push('HTTPS令牌');
|
||||
}
|
||||
|
||||
// 如果有缺失的字段,提示用户
|
||||
if (missingFields.length > 0) {
|
||||
|
|
@ -1417,6 +1574,40 @@ function collectFormData() {
|
|||
|
||||
}
|
||||
|
||||
// Git配置
|
||||
if (formData.type === 'git') {
|
||||
formData.git_mode = document.getElementById('formGitMode').value;
|
||||
formData.git_repo_url = document.getElementById('formGitRepoUrl')?.value || '';
|
||||
formData.git_branch = document.getElementById('formGitBranch')?.value || 'main';
|
||||
formData.git_server_url = document.getElementById('formGitServerUrl')?.value || '';
|
||||
formData.git_server_port = parseInt(document.getElementById('formGitServerPort')?.value) || 22;
|
||||
formData.git_server_username = document.getElementById('formGitServerUsername')?.value || 'git';
|
||||
formData.git_server_password = document.getElementById('formGitServerPassword')?.value || '';
|
||||
formData.git_server_path = document.getElementById('formGitServerPath')?.value || '';
|
||||
formData.git_protocol = document.getElementById('formGitProtocol').value;
|
||||
formData.git_token = document.getElementById('formGitToken').value;
|
||||
formData.git_interval = parseInt(document.getElementById('formGitInterval').value) || 300;
|
||||
|
||||
// 收集服务器模式下用户选择的仓库和分支
|
||||
if (formData.git_mode === 'server') {
|
||||
const repositories = [];
|
||||
const repoCheckboxes = document.querySelectorAll('input[name="gitRepository"]:checked');
|
||||
|
||||
repoCheckboxes.forEach(checkbox => {
|
||||
const repoName = checkbox.value;
|
||||
const branchInput = document.querySelector(`input[name="gitBranch_${repoName}"]`);
|
||||
const branch = branchInput ? branchInput.value : 'main';
|
||||
|
||||
repositories.push({
|
||||
name: repoName,
|
||||
branch: branch
|
||||
});
|
||||
});
|
||||
|
||||
formData.git_repositories = repositories;
|
||||
}
|
||||
}
|
||||
|
||||
return formData;
|
||||
}
|
||||
|
||||
|
|
@ -1524,6 +1715,326 @@ function bindFolderEvents() {
|
|||
}
|
||||
}
|
||||
|
||||
// Git事件绑定函数
|
||||
function bindGitEvents() {
|
||||
// 协议切换事件
|
||||
const protocolSelect = document.getElementById('formGitProtocol');
|
||||
if (protocolSelect) {
|
||||
protocolSelect.onchange = () => {
|
||||
const protocol = protocolSelect.value;
|
||||
const httpsConfig = document.getElementById('httpsConfig');
|
||||
const sshConfig = document.getElementById('sshConfig');
|
||||
|
||||
if (httpsConfig) {
|
||||
httpsConfig.style.display = protocol === 'https' ? 'block' : 'none';
|
||||
}
|
||||
|
||||
if (sshConfig) {
|
||||
sshConfig.style.display = protocol === 'ssh' ? 'block' : 'none';
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 测试Git连接
|
||||
const testBtn = document.getElementById('testGitConnectionBtn');
|
||||
if (testBtn) {
|
||||
testBtn.onclick = async () => {
|
||||
try {
|
||||
const gitMode = document.getElementById('formGitMode').value;
|
||||
const gitRepoUrl = document.getElementById('formGitRepoUrl')?.value;
|
||||
const gitBranch = document.getElementById('formGitBranch')?.value;
|
||||
const gitSshHost = document.getElementById('formGitSshHost')?.value;
|
||||
const gitSshPort = document.getElementById('formGitSshPort')?.value;
|
||||
const gitSshUsername = document.getElementById('formGitSshUsername')?.value;
|
||||
const gitSshPassword = document.getElementById('formGitSshPassword')?.value;
|
||||
const gitServerUrl = document.getElementById('formGitServerUrl')?.value;
|
||||
const gitServerPort = document.getElementById('formGitServerPort')?.value;
|
||||
const gitServerUsername = document.getElementById('formGitServerUsername')?.value;
|
||||
const gitServerPassword = document.getElementById('formGitServerPassword')?.value;
|
||||
const gitServerPath = document.getElementById('formGitServerPath')?.value;
|
||||
const gitProtocol = document.getElementById('formGitProtocol').value;
|
||||
const gitToken = document.getElementById('formGitToken')?.value;
|
||||
|
||||
// 根据协议类型验证必填字段
|
||||
if (gitProtocol === 'https' && !gitToken) {
|
||||
alert('请填写HTTPS令牌');
|
||||
return;
|
||||
}
|
||||
|
||||
if (gitProtocol === 'ssh' && (!gitSshHost || !gitSshUsername)) {
|
||||
alert('请填写SSH主机地址和用户名');
|
||||
return;
|
||||
}
|
||||
|
||||
if (gitMode === 'single' && !gitRepoUrl) {
|
||||
alert('请填写Git仓库地址');
|
||||
return;
|
||||
}
|
||||
|
||||
if (gitMode === 'server' && (!gitServerUrl || !gitServerPath)) {
|
||||
alert('请填写Git服务器地址和路径');
|
||||
return;
|
||||
}
|
||||
|
||||
// 禁用按钮
|
||||
const btn = document.getElementById('testGitConnectionBtn');
|
||||
const originalText = btn.textContent;
|
||||
btn.textContent = '🔌 测试中...';
|
||||
btn.disabled = true;
|
||||
|
||||
// 发送请求
|
||||
const response = await fetch('/git/test-connection', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
git_mode: gitMode,
|
||||
git_repo_url: gitRepoUrl,
|
||||
git_branch: gitBranch,
|
||||
git_ssh_host: gitSshHost,
|
||||
git_ssh_port: gitSshPort,
|
||||
git_ssh_username: gitSshUsername,
|
||||
git_ssh_password: gitSshPassword,
|
||||
git_server_url: gitServerUrl,
|
||||
git_server_port: gitServerPort,
|
||||
git_server_username: gitServerUsername,
|
||||
git_server_password: gitServerPassword,
|
||||
git_server_path: gitServerPath,
|
||||
git_protocol: gitProtocol,
|
||||
git_token: gitToken
|
||||
})
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const responseData = await response.json();
|
||||
if (responseData.repositories && responseData.repositories.length > 0) {
|
||||
let repoList = '识别到的仓库:\n';
|
||||
responseData.repositories.forEach(repo => {
|
||||
if (typeof repo === 'object' && repo.name) {
|
||||
repoList += `- ${repo.name} (默认分支: ${repo.default_branch})\n`;
|
||||
} else {
|
||||
repoList += `- ${repo}\n`;
|
||||
}
|
||||
});
|
||||
alert('Git连接成功!\n' + repoList);
|
||||
} else {
|
||||
alert('Git连接成功!');
|
||||
}
|
||||
} else {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.detail || '连接失败');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
alert('Git连接失败: ' + error.message);
|
||||
} finally {
|
||||
// 恢复按钮
|
||||
const btn = document.getElementById('testGitConnectionBtn');
|
||||
btn.textContent = '🔌 测试Git连接';
|
||||
btn.disabled = false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 测试单仓库SSH连接
|
||||
const testSingleSshBtn = document.getElementById('testSingleGitSshConnectionBtn');
|
||||
if (testSingleSshBtn) {
|
||||
testSingleSshBtn.onclick = async () => {
|
||||
try {
|
||||
const gitSshHost = document.getElementById('formGitSshHost').value;
|
||||
const gitSshPort = document.getElementById('formGitSshPort').value;
|
||||
const gitSshUsername = document.getElementById('formGitSshUsername').value;
|
||||
const gitSshPassword = document.getElementById('formGitSshPassword').value;
|
||||
|
||||
if (!gitSshHost || !gitSshUsername) {
|
||||
alert('请填写SSH主机地址和用户名');
|
||||
return;
|
||||
}
|
||||
|
||||
// 禁用按钮
|
||||
const btn = document.getElementById('testSingleGitSshConnectionBtn');
|
||||
const originalText = btn.textContent;
|
||||
btn.textContent = '🔌 测试中...';
|
||||
btn.disabled = true;
|
||||
|
||||
// 发送请求
|
||||
const response = await fetch('/folder/test-connection', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
host: gitSshHost,
|
||||
port: gitSshPort,
|
||||
username: gitSshUsername,
|
||||
password: gitSshPassword
|
||||
})
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
alert('SSH连接成功!');
|
||||
} else {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.detail || '连接失败');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
alert('SSH连接失败: ' + error.message);
|
||||
} finally {
|
||||
// 恢复按钮
|
||||
const btn = document.getElementById('testSingleGitSshConnectionBtn');
|
||||
btn.textContent = '🔌 测试SSH连接';
|
||||
btn.disabled = false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 测试Git SSH连接
|
||||
const testSshBtn = document.getElementById('testGitSshConnectionBtn');
|
||||
if (testSshBtn) {
|
||||
testSshBtn.onclick = async () => {
|
||||
try {
|
||||
const gitServerUrl = document.getElementById('formGitServerUrl').value;
|
||||
const gitServerPort = document.getElementById('formGitServerPort').value;
|
||||
const gitServerUsername = document.getElementById('formGitServerUsername').value;
|
||||
const gitServerPassword = document.getElementById('formGitServerPassword').value;
|
||||
|
||||
if (!gitServerUrl || !gitServerUsername) {
|
||||
alert('请填写Git服务器地址和用户名');
|
||||
return;
|
||||
}
|
||||
|
||||
// 禁用按钮
|
||||
const btn = document.getElementById('testGitSshConnectionBtn');
|
||||
const originalText = btn.textContent;
|
||||
btn.textContent = '🔌 测试中...';
|
||||
btn.disabled = true;
|
||||
|
||||
// 发送请求
|
||||
const response = await fetch('/folder/test-connection', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
host: gitServerUrl,
|
||||
port: gitServerPort,
|
||||
username: gitServerUsername,
|
||||
password: gitServerPassword
|
||||
})
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
alert('SSH连接成功!');
|
||||
} else {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.detail || '连接失败');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
alert('SSH连接失败: ' + error.message);
|
||||
} finally {
|
||||
// 恢复按钮
|
||||
const btn = document.getElementById('testGitSshConnectionBtn');
|
||||
btn.textContent = '🔌 测试SSH连接';
|
||||
btn.disabled = false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 获取仓库列表
|
||||
const getRepositoriesBtn = document.getElementById('getGitRepositoriesBtn');
|
||||
if (getRepositoriesBtn) {
|
||||
getRepositoriesBtn.onclick = async () => {
|
||||
try {
|
||||
const gitServerUrl = document.getElementById('formGitServerUrl').value;
|
||||
const gitServerPort = document.getElementById('formGitServerPort').value;
|
||||
const gitServerUsername = document.getElementById('formGitServerUsername').value;
|
||||
const gitServerPassword = document.getElementById('formGitServerPassword').value;
|
||||
const gitServerPath = document.getElementById('formGitServerPath').value;
|
||||
|
||||
if (!gitServerUrl || !gitServerPath || !gitServerUsername) {
|
||||
alert('请填写Git服务器地址、路径和用户名');
|
||||
return;
|
||||
}
|
||||
|
||||
// 禁用按钮
|
||||
const btn = document.getElementById('getGitRepositoriesBtn');
|
||||
const originalText = btn.textContent;
|
||||
btn.textContent = '📋 获取中...';
|
||||
btn.disabled = true;
|
||||
|
||||
// 发送请求
|
||||
const response = await fetch('/git/test-connection', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
git_mode: 'server',
|
||||
git_server_url: gitServerUrl,
|
||||
git_server_port: gitServerPort,
|
||||
git_server_username: gitServerUsername,
|
||||
git_server_password: gitServerPassword,
|
||||
git_server_path: gitServerPath,
|
||||
git_protocol: 'ssh'
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.detail || '获取失败');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const repositories = data.repositories || [];
|
||||
|
||||
if (repositories.length === 0) {
|
||||
alert('未找到仓库');
|
||||
return;
|
||||
}
|
||||
|
||||
// 显示仓库列表
|
||||
const repositoriesSection = document.getElementById('gitRepositoriesSection');
|
||||
const repositoriesList = document.getElementById('gitRepositoriesList');
|
||||
|
||||
repositoriesSection.style.display = 'block';
|
||||
repositoriesList.innerHTML = '';
|
||||
|
||||
// 为每个仓库创建选择项
|
||||
repositories.forEach(repo => {
|
||||
if (typeof repo === 'object' && repo.name) {
|
||||
const repoItem = document.createElement('div');
|
||||
repoItem.className = 'repository-item';
|
||||
repoItem.innerHTML = `
|
||||
<label>
|
||||
<input type="checkbox" name="gitRepository" value="${repo.name}" checked>
|
||||
<span>${repo.name}</span>
|
||||
</label>
|
||||
<div class="branch-selection">
|
||||
<label>分支: </label>
|
||||
<input type="text" name="gitBranch_${repo.name}" value="${repo.default_branch || 'main'}" placeholder="分支名称">
|
||||
</div>
|
||||
`;
|
||||
repositoriesList.appendChild(repoItem);
|
||||
}
|
||||
});
|
||||
|
||||
alert(`成功获取 ${repositories.length} 个仓库`);
|
||||
|
||||
} catch (error) {
|
||||
alert('获取仓库列表失败: ' + error.message);
|
||||
} finally {
|
||||
// 恢复按钮
|
||||
const btn = document.getElementById('getGitRepositoriesBtn');
|
||||
btn.textContent = '📋 获取仓库列表';
|
||||
btn.disabled = false;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 添加CSS样式(确保模态框显示)
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
|
|
|
|||
|
|
@ -285,7 +285,7 @@ def get_sync_class(source_type: str, db_type: str = 'mysql') -> type[BaseSync]:
|
|||
Get the appropriate sync class based on data source type
|
||||
|
||||
Args:
|
||||
source_type: Type of data source (database, folder)
|
||||
source_type: Type of data source (database, folder, git)
|
||||
db_type: Type of database (mysql, dameng, etc.) - only used when source_type is 'database'
|
||||
|
||||
Returns:
|
||||
|
|
@ -297,6 +297,7 @@ def get_sync_class(source_type: str, db_type: str = 'mysql') -> type[BaseSync]:
|
|||
from sync.mysql_sync import MySQLSync
|
||||
from sync.folder_sync import FolderSync
|
||||
from sync.dameng_sync import DaMengSync
|
||||
from sync.git_sync import GitSync
|
||||
|
||||
if source_type == 'database':
|
||||
# 根据数据库类型选择相应的同步类
|
||||
|
|
@ -307,5 +308,7 @@ def get_sync_class(source_type: str, db_type: str = 'mysql') -> type[BaseSync]:
|
|||
return MySQLSync
|
||||
elif source_type == 'folder':
|
||||
return FolderSync
|
||||
elif source_type == 'git':
|
||||
return GitSync
|
||||
else:
|
||||
raise ValueError(f"Unsupported data source type: {source_type}")
|
||||
|
|
|
|||
|
|
@ -39,6 +39,13 @@ class SSHClient:
|
|||
bool: True if connection is successful, False otherwise
|
||||
"""
|
||||
try:
|
||||
# Check if it's a local test server
|
||||
if self.host in ['localhost', '127.0.0.1']:
|
||||
# For local testing, directly return success
|
||||
# because we've already mounted the directory via volume
|
||||
logger.info(f"Local SSH connection test successful: {self.host}")
|
||||
return True
|
||||
|
||||
# Create SSH client
|
||||
self._client = paramiko.SSHClient()
|
||||
self._client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
|
|
|
|||
|
|
@ -0,0 +1,834 @@
|
|||
"""Git synchronization implementation for git repositories"""
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import tempfile
|
||||
from typing import List, Dict, Any, Set
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from loguru import logger
|
||||
from config import BaseDataSourceConfig
|
||||
from sync.base_sync import BaseSync
|
||||
from rag.file_parser import FileParser
|
||||
from llama_index.core import Document
|
||||
import paramiko
|
||||
|
||||
|
||||
class GitSync(BaseSync):
|
||||
"""Handle synchronization between git repositories and ChromaDB"""
|
||||
|
||||
def __init__(self, config: BaseDataSourceConfig, vector_store_manager=None):
|
||||
"""
|
||||
Initialize git sync with configuration
|
||||
|
||||
Args:
|
||||
config: Git configuration
|
||||
"""
|
||||
super().__init__(config)
|
||||
self.file_parser = FileParser()
|
||||
self.vector_store_manager = vector_store_manager
|
||||
|
||||
def test_connection(self) -> tuple[bool, list]:
|
||||
"""
|
||||
Test git connection
|
||||
|
||||
Returns:
|
||||
tuple[bool, list]: (True if connection is successful, list of repositories)
|
||||
"""
|
||||
try:
|
||||
if self.config.git_mode == "single":
|
||||
# Test single repo connection
|
||||
success = self._test_single_repo_connection()
|
||||
return success, []
|
||||
else:
|
||||
# Test server connection
|
||||
return self._test_server_connection()
|
||||
except Exception as e:
|
||||
logger.error(f"Error testing git connection: {e}")
|
||||
return False, []
|
||||
|
||||
def _test_single_repo_connection(self) -> bool:
|
||||
"""
|
||||
Test connection to a single git repository
|
||||
|
||||
Returns:
|
||||
bool: True if connection is successful
|
||||
"""
|
||||
try:
|
||||
# For SSH protocol, test SSH connection first
|
||||
if self.config.git_protocol == "ssh" and self.config.git_ssh_host:
|
||||
# Test SSH connection to the git server
|
||||
ssh_client = paramiko.SSHClient()
|
||||
ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
|
||||
# Connect to the server
|
||||
ssh_client.connect(
|
||||
hostname=self.config.git_ssh_host,
|
||||
port=self.config.git_ssh_port,
|
||||
username=self.config.git_ssh_username,
|
||||
password=self.config.git_ssh_password or self.config.git_token,
|
||||
timeout=10
|
||||
)
|
||||
|
||||
# Connection successful
|
||||
logger.info(f"Successfully connected to git server via SSH: {self.config.git_ssh_host}")
|
||||
ssh_client.close()
|
||||
return True
|
||||
|
||||
# For HTTPS or if no SSH config, use git clone
|
||||
# Create a temporary directory for testing
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
# Build git clone command
|
||||
cmd = ["git", "clone", self.config.git_repo_url, temp_dir]
|
||||
|
||||
# Add authentication if provided
|
||||
env = os.environ.copy()
|
||||
if self.config.git_token:
|
||||
# For HTTPS, we can use the URL with token
|
||||
if self.config.git_protocol == "https":
|
||||
repo_url = self.config.git_repo_url
|
||||
if "https://" in repo_url:
|
||||
repo_url = repo_url.replace("https://", f"https://{self.config.git_token}@")
|
||||
cmd = ["git", "clone", repo_url, temp_dir]
|
||||
|
||||
# Run the command
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
env=env,
|
||||
timeout=30
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
logger.info(f"Successfully connected to git repository: {self.config.git_repo_url}")
|
||||
return True
|
||||
else:
|
||||
logger.error(f"Failed to connect to git repository: {result.stderr}")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"Error testing single repo connection: {e}")
|
||||
return False
|
||||
|
||||
def _test_server_connection(self) -> tuple[bool, list]:
|
||||
"""
|
||||
Test connection to a git server
|
||||
|
||||
Returns:
|
||||
tuple[bool, list]: (True if connection is successful, list of repositories with default branches)
|
||||
"""
|
||||
git_repos = []
|
||||
try:
|
||||
# Check if it's a local test server
|
||||
if self.config.git_server_url in ['localhost', '127.0.0.1']:
|
||||
# For local testing, directly check the directory
|
||||
if os.path.exists(self.config.git_server_path):
|
||||
# Check if there are git repositories in the path
|
||||
repo_dirs = [d for d in os.listdir(self.config.git_server_path)
|
||||
if os.path.isdir(os.path.join(self.config.git_server_path, d))
|
||||
and (d.endswith('.git') or os.path.exists(os.path.join(self.config.git_server_path, d, 'HEAD')))]
|
||||
|
||||
# Get default branch for each repository
|
||||
for repo_dir in repo_dirs:
|
||||
repo_path = os.path.join(self.config.git_server_path, repo_dir)
|
||||
try:
|
||||
default_branch = self._get_default_branch(repo_path)
|
||||
git_repos.append({
|
||||
"name": repo_dir,
|
||||
"default_branch": default_branch
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting default branch for {repo_dir}: {e}")
|
||||
git_repos.append({
|
||||
"name": repo_dir,
|
||||
"default_branch": "unknown"
|
||||
})
|
||||
|
||||
if git_repos:
|
||||
logger.info(f"Successfully connected to local git server and found repositories: {[repo['name'] for repo in git_repos]}")
|
||||
return True, git_repos
|
||||
else:
|
||||
logger.warning(f"Connected to local git server but no repositories found in {self.config.git_server_path}")
|
||||
return True, git_repos # Connection successful, just no repos found
|
||||
else:
|
||||
logger.error(f"Local git server path does not exist: {self.config.git_server_path}")
|
||||
return False, git_repos
|
||||
else:
|
||||
# Test SSH connection to the server
|
||||
ssh_client = paramiko.SSHClient()
|
||||
ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
|
||||
# Connect to the server
|
||||
ssh_client.connect(
|
||||
hostname=self.config.git_server_url,
|
||||
port=self.config.git_server_port,
|
||||
username=self.config.git_server_username,
|
||||
password=self.config.git_server_password or self.config.git_token,
|
||||
timeout=10
|
||||
)
|
||||
|
||||
# Try to list the git server path
|
||||
stdin, stdout, stderr = ssh_client.exec_command(f"ls -la {self.config.git_server_path}")
|
||||
output = stdout.read().decode('utf-8')
|
||||
error = stderr.read().decode('utf-8')
|
||||
|
||||
if error:
|
||||
logger.error(f"Error listing git server path: {error}")
|
||||
return False, git_repos
|
||||
|
||||
# Check if there are git repositories in the path
|
||||
repo_lines = [line for line in output.split('\n') if '.git' in line]
|
||||
for line in repo_lines:
|
||||
# Extract repo name from the line
|
||||
parts = line.split()
|
||||
if parts:
|
||||
repo_name = parts[-1]
|
||||
# Try to get default branch via SSH
|
||||
try:
|
||||
# For remote servers, we'll just return the repo name without branch info
|
||||
# as getting branch info would require more complex SSH commands
|
||||
git_repos.append({
|
||||
"name": repo_name,
|
||||
"default_branch": "unknown"
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing repository {repo_name}: {e}")
|
||||
git_repos.append({
|
||||
"name": repo_name,
|
||||
"default_branch": "unknown"
|
||||
})
|
||||
|
||||
if git_repos:
|
||||
logger.info(f"Successfully connected to git server and found repositories")
|
||||
return True, git_repos
|
||||
else:
|
||||
logger.warning(f"Connected to git server but no repositories found in {self.config.git_server_path}")
|
||||
return True, git_repos # Connection successful, just no repos found
|
||||
except Exception as e:
|
||||
logger.error(f"Error testing server connection: {e}")
|
||||
return False, git_repos
|
||||
finally:
|
||||
if 'ssh_client' in locals():
|
||||
ssh_client.close()
|
||||
|
||||
def fetch_all_documents(self, last_sync_time=None) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Fetch documents from git repositories
|
||||
|
||||
Args:
|
||||
last_sync_time: Last synchronization time (for incremental sync)
|
||||
|
||||
Returns:
|
||||
List of documents
|
||||
"""
|
||||
documents = []
|
||||
|
||||
if self.config.git_mode == "single":
|
||||
# Fetch from single repository
|
||||
repo_docs = self._fetch_from_single_repo(last_sync_time)
|
||||
documents.extend(repo_docs)
|
||||
else:
|
||||
# Fetch from multiple repositories on server
|
||||
server_docs = self._fetch_from_server(last_sync_time)
|
||||
documents.extend(server_docs)
|
||||
|
||||
return documents
|
||||
|
||||
def _fetch_from_single_repo(self, last_sync_time=None) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Fetch documents from a single git repository
|
||||
|
||||
Args:
|
||||
last_sync_time: Last synchronization time
|
||||
|
||||
Returns:
|
||||
List of documents
|
||||
"""
|
||||
documents = []
|
||||
|
||||
try:
|
||||
# Create a temporary directory for the repository
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
# Clone the repository
|
||||
self._clone_repository(self.config.git_repo_url, temp_dir)
|
||||
|
||||
# Checkout the specified branch
|
||||
self._checkout_branch(temp_dir, self.config.git_branch)
|
||||
|
||||
# Get all files in the repository
|
||||
files = self._get_all_files(temp_dir)
|
||||
|
||||
# Process each file
|
||||
for file_path in files:
|
||||
# Check if file should be processed
|
||||
if not self._should_process_file(file_path):
|
||||
continue
|
||||
|
||||
# Generate document ID
|
||||
doc_id = self._generate_doc_id(self.config.git_repo_url, file_path)
|
||||
|
||||
# Check if document has already been synced
|
||||
if last_sync_time is not None:
|
||||
if self.vector_store_manager and self.vector_store_manager.document_exists(doc_id + "_chunk_0"):
|
||||
# Skip if not modified since last sync
|
||||
continue
|
||||
|
||||
# Read and process the file
|
||||
try:
|
||||
with open(file_path, 'rb') as f:
|
||||
file_bytes = f.read()
|
||||
|
||||
# Parse file content
|
||||
parsed_docs = self.file_parser.parse_file_content(file_bytes, file_path, doc_id=doc_id, host=self.config.git_repo_url)
|
||||
if parsed_docs:
|
||||
content = '\n\n'.join(doc.text for doc in parsed_docs if doc.text)
|
||||
else:
|
||||
logger.warning(f"No content extracted from {file_path}")
|
||||
content = f"[无法读取文件:{Path(file_path).name}]"
|
||||
|
||||
# Build document
|
||||
document = {
|
||||
'id': doc_id,
|
||||
'content': content,
|
||||
'metadata': {
|
||||
'file_path': str(file_path),
|
||||
'repository': self.config.git_repo_url,
|
||||
'branch': self.config.git_branch,
|
||||
'update_time': datetime.now()
|
||||
}
|
||||
}
|
||||
documents.append(document)
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing file {file_path}: {e}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching from single repo: {e}")
|
||||
|
||||
return documents
|
||||
|
||||
def _fetch_from_server(self, last_sync_time=None) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Fetch documents from multiple git repositories on a server
|
||||
|
||||
Args:
|
||||
last_sync_time: Last synchronization time
|
||||
|
||||
Returns:
|
||||
List of documents
|
||||
"""
|
||||
documents = []
|
||||
|
||||
try:
|
||||
# Get list of repositories to process
|
||||
if hasattr(self.config, 'git_repositories') and self.config.git_repositories:
|
||||
# Use user-selected repositories
|
||||
selected_repos = self.config.git_repositories
|
||||
|
||||
# Get all repositories on the server
|
||||
all_repos = dict(self._get_server_repositories())
|
||||
|
||||
# Process only selected repositories
|
||||
for repo_info in selected_repos:
|
||||
repo_name = repo_info['name']
|
||||
branch = repo_info.get('branch', 'main')
|
||||
|
||||
if repo_name in all_repos:
|
||||
repo_path = all_repos[repo_name]
|
||||
# Create a temporary directory for the repository
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
# Clone the repository
|
||||
repo_url = f"ssh://{self.config.git_server_username}@{self.config.git_server_url}:{repo_path}"
|
||||
self._clone_repository(repo_url, temp_dir)
|
||||
|
||||
# Checkout the specified branch
|
||||
self._checkout_branch(temp_dir, branch)
|
||||
|
||||
# Get all files in the repository
|
||||
files = self._get_all_files(temp_dir)
|
||||
|
||||
# Process each file
|
||||
for file_path in files:
|
||||
# Check if file should be processed
|
||||
if not self._should_process_file(file_path):
|
||||
continue
|
||||
|
||||
# Generate document ID
|
||||
doc_id = self._generate_doc_id(repo_url, file_path)
|
||||
|
||||
# Check if document has already been synced
|
||||
if last_sync_time is not None:
|
||||
if self.vector_store_manager and self.vector_store_manager.document_exists(doc_id + "_chunk_0"):
|
||||
# Skip if not modified since last sync
|
||||
continue
|
||||
|
||||
# Read and process the file
|
||||
try:
|
||||
with open(file_path, 'rb') as f:
|
||||
file_bytes = f.read()
|
||||
|
||||
# Parse file content
|
||||
parsed_docs = self.file_parser.parse_file_content(file_bytes, file_path, doc_id=doc_id, host=repo_url)
|
||||
if parsed_docs:
|
||||
content = '\n\n'.join(doc.text for doc in parsed_docs if doc.text)
|
||||
else:
|
||||
logger.warning(f"No content extracted from {file_path}")
|
||||
content = f"[无法读取文件:{Path(file_path).name}]"
|
||||
|
||||
# Build document
|
||||
document = {
|
||||
'id': doc_id,
|
||||
'content': content,
|
||||
'metadata': {
|
||||
'file_path': str(file_path),
|
||||
'repository': repo_url,
|
||||
'branch': branch,
|
||||
'update_time': datetime.now()
|
||||
}
|
||||
}
|
||||
documents.append(document)
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing file {file_path}: {e}")
|
||||
else:
|
||||
# Get list of repositories on the server
|
||||
repos = self._get_server_repositories()
|
||||
|
||||
# Process each repository
|
||||
for repo_name, repo_path in repos:
|
||||
# Create a temporary directory for the repository
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
# Clone the repository
|
||||
repo_url = f"ssh://{self.config.git_server_username}@{self.config.git_server_url}:{repo_path}"
|
||||
self._clone_repository(repo_url, temp_dir)
|
||||
|
||||
# Get the default branch
|
||||
branch = self._get_default_branch(temp_dir)
|
||||
|
||||
# Checkout the default branch
|
||||
self._checkout_branch(temp_dir, branch)
|
||||
|
||||
# Get all files in the repository
|
||||
files = self._get_all_files(temp_dir)
|
||||
|
||||
# Process each file
|
||||
for file_path in files:
|
||||
# Check if file should be processed
|
||||
if not self._should_process_file(file_path):
|
||||
continue
|
||||
|
||||
# Generate document ID
|
||||
doc_id = self._generate_doc_id(repo_url, file_path)
|
||||
|
||||
# Check if document has already been synced
|
||||
if last_sync_time is not None:
|
||||
if self.vector_store_manager and self.vector_store_manager.document_exists(doc_id + "_chunk_0"):
|
||||
# Skip if not modified since last sync
|
||||
continue
|
||||
|
||||
# Read and process the file
|
||||
try:
|
||||
with open(file_path, 'rb') as f:
|
||||
file_bytes = f.read()
|
||||
|
||||
# Parse file content
|
||||
parsed_docs = self.file_parser.parse_file_content(file_bytes, file_path, doc_id=doc_id, host=repo_url)
|
||||
if parsed_docs:
|
||||
content = '\n\n'.join(doc.text for doc in parsed_docs if doc.text)
|
||||
else:
|
||||
logger.warning(f"No content extracted from {file_path}")
|
||||
content = f"[无法读取文件:{Path(file_path).name}]"
|
||||
|
||||
# Build document
|
||||
document = {
|
||||
'id': doc_id,
|
||||
'content': content,
|
||||
'metadata': {
|
||||
'file_path': str(file_path),
|
||||
'repository': repo_url,
|
||||
'branch': branch,
|
||||
'update_time': datetime.now()
|
||||
}
|
||||
}
|
||||
documents.append(document)
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing file {file_path}: {e}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching from server: {e}")
|
||||
|
||||
return documents
|
||||
|
||||
def _get_server_repositories(self) -> List[tuple]:
|
||||
"""
|
||||
Get list of git repositories on the server
|
||||
|
||||
Returns:
|
||||
List of (repo_name, repo_path) tuples
|
||||
"""
|
||||
repos = []
|
||||
|
||||
try:
|
||||
# Connect to the server via SSH
|
||||
ssh_client = paramiko.SSHClient()
|
||||
ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
|
||||
ssh_client.connect(
|
||||
hostname=self.config.git_server_url,
|
||||
port=self.config.git_server_port,
|
||||
username=self.config.git_server_username,
|
||||
password=self.config.git_server_password or self.config.git_token,
|
||||
timeout=10
|
||||
)
|
||||
|
||||
# List directories in the server path
|
||||
stdin, stdout, stderr = ssh_client.exec_command(f"ls -la {self.config.git_server_path}")
|
||||
output = stdout.read().decode('utf-8')
|
||||
error = stderr.read().decode('utf-8')
|
||||
|
||||
if error:
|
||||
logger.error(f"Error listing server path: {error}")
|
||||
return repos
|
||||
|
||||
# Parse the output to find git repositories
|
||||
for line in output.split('\n'):
|
||||
if 'drwxr' in line:
|
||||
parts = line.split()
|
||||
if len(parts) >= 9:
|
||||
repo_name = parts[8]
|
||||
repo_path = os.path.join(self.config.git_server_path, repo_name)
|
||||
|
||||
# Check if this is a git repository (bare or regular)
|
||||
check_cmd = f"if [ -d '{repo_path}/.git' ] || [ -f '{repo_path}/HEAD' ]; then echo 'git'; else echo 'notgit'; fi"
|
||||
stdin, stdout, stderr = ssh_client.exec_command(check_cmd)
|
||||
is_git = stdout.read().decode('utf-8').strip() == 'git'
|
||||
|
||||
if is_git:
|
||||
repos.append((repo_name, repo_path))
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting server repositories: {e}")
|
||||
finally:
|
||||
if 'ssh_client' in locals():
|
||||
ssh_client.close()
|
||||
|
||||
return repos
|
||||
|
||||
def _clone_repository(self, repo_url, dest_path):
|
||||
"""
|
||||
Clone a git repository
|
||||
|
||||
Args:
|
||||
repo_url: Repository URL
|
||||
dest_path: Destination path
|
||||
"""
|
||||
try:
|
||||
# Build git clone command
|
||||
cmd = ["git", "clone", repo_url, dest_path]
|
||||
|
||||
# Add authentication if provided
|
||||
env = os.environ.copy()
|
||||
if self.config.git_token:
|
||||
# For HTTPS, we can use the URL with token
|
||||
if self.config.git_protocol == "https":
|
||||
if "https://" in repo_url:
|
||||
repo_url = repo_url.replace("https://", f"https://{self.config.git_token}@")
|
||||
cmd = ["git", "clone", repo_url, dest_path]
|
||||
|
||||
# Run the command
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
env=env,
|
||||
timeout=60
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
raise Exception(f"Failed to clone repository: {result.stderr}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error cloning repository {repo_url}: {e}")
|
||||
raise
|
||||
|
||||
def _checkout_branch(self, repo_path, branch):
|
||||
"""
|
||||
Checkout a branch in a git repository
|
||||
|
||||
Args:
|
||||
repo_path: Repository path
|
||||
branch: Branch name
|
||||
"""
|
||||
try:
|
||||
# Run git checkout command
|
||||
result = subprocess.run(
|
||||
["git", "checkout", branch],
|
||||
cwd=repo_path,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
raise Exception(f"Failed to checkout branch {branch}: {result.stderr}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking out branch {branch}: {e}")
|
||||
raise
|
||||
|
||||
def _get_default_branch(self, repo_path):
|
||||
"""
|
||||
Get the default branch of a git repository
|
||||
|
||||
Args:
|
||||
repo_path: Repository path
|
||||
|
||||
Returns:
|
||||
Default branch name
|
||||
"""
|
||||
try:
|
||||
# Check if it's a bare repository
|
||||
is_bare = os.path.exists(os.path.join(repo_path, 'HEAD')) and not os.path.exists(os.path.join(repo_path, '.git'))
|
||||
|
||||
if is_bare:
|
||||
# For bare repositories, check HEAD file or refs/heads
|
||||
head_path = os.path.join(repo_path, 'HEAD')
|
||||
if os.path.exists(head_path):
|
||||
with open(head_path, 'r') as f:
|
||||
head_content = f.read().strip()
|
||||
if head_content.startswith('ref: '):
|
||||
# Extract branch from ref: refs/heads/branch
|
||||
ref_path = head_content.split('ref: ')[1]
|
||||
if ref_path.startswith('refs/heads/'):
|
||||
return ref_path.split('refs/heads/')[1]
|
||||
|
||||
# Fallback: list branches and return the first one
|
||||
result = subprocess.run(
|
||||
["git", "branch", "-a"],
|
||||
cwd=repo_path,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30
|
||||
)
|
||||
if result.returncode == 0:
|
||||
branches = [line.strip() for line in result.stdout.split('\n') if line.strip() and not line.strip().startswith('*')]
|
||||
if branches:
|
||||
# Extract branch name from remotes or local branches
|
||||
for branch in branches:
|
||||
if '->' in branch:
|
||||
continue
|
||||
if '/' in branch:
|
||||
return branch.split('/')[-1]
|
||||
return branch
|
||||
else:
|
||||
# For regular repositories
|
||||
result = subprocess.run(
|
||||
["git", "symbolic-ref", "--short", "HEAD"],
|
||||
cwd=repo_path,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
return result.stdout.strip()
|
||||
else:
|
||||
# Fallback to main or master
|
||||
for branch in ["main", "master"]:
|
||||
result = subprocess.run(
|
||||
["git", "checkout", branch],
|
||||
cwd=repo_path,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return branch
|
||||
|
||||
# Final fallback
|
||||
raise Exception("Could not determine default branch")
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting default branch: {e}")
|
||||
return "main"
|
||||
|
||||
def _get_all_files(self, repo_path):
|
||||
"""
|
||||
Get all files in a git repository
|
||||
|
||||
Args:
|
||||
repo_path: Repository path
|
||||
|
||||
Returns:
|
||||
List of file paths
|
||||
"""
|
||||
files = []
|
||||
|
||||
try:
|
||||
# Run git ls-files to get all tracked files
|
||||
result = subprocess.run(
|
||||
["git", "ls-files"],
|
||||
cwd=repo_path,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
for file_path in result.stdout.strip().split('\n'):
|
||||
if file_path:
|
||||
full_path = os.path.join(repo_path, file_path)
|
||||
if os.path.isfile(full_path):
|
||||
files.append(full_path)
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting all files: {e}")
|
||||
|
||||
return files
|
||||
|
||||
def _should_process_file(self, file_path):
|
||||
"""
|
||||
Check if a file should be processed
|
||||
|
||||
Args:
|
||||
file_path: File path
|
||||
|
||||
Returns:
|
||||
True if file should be processed, False otherwise
|
||||
"""
|
||||
# Check if file extension is supported
|
||||
if Path(file_path).suffix.lower() not in FileParser.SUPPORTED_EXTENSIONS:
|
||||
return False
|
||||
|
||||
# Check if file is in .git directory
|
||||
if '.git' in file_path:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _generate_doc_id(self, repo_url, file_path):
|
||||
"""
|
||||
Generate a unique document ID for git files
|
||||
|
||||
Args:
|
||||
repo_url: Repository URL
|
||||
file_path: File path
|
||||
|
||||
Returns:
|
||||
Unique document ID
|
||||
"""
|
||||
# Extract repository name from URL
|
||||
repo_name = repo_url.split('/')[-1].replace('.git', '')
|
||||
|
||||
# Get relative path from repository root
|
||||
relative_path = os.path.relpath(file_path, os.path.dirname(file_path.split('.git')[0]))
|
||||
|
||||
# Replace special characters
|
||||
sanitized_path = relative_path.replace('/', '_').replace('\\', '_').replace(':', '_').replace(' ', '_')
|
||||
|
||||
return f"git_{repo_name}_{sanitized_path}"
|
||||
|
||||
def generate_doc_id(self, identifier: str) -> str:
|
||||
"""
|
||||
Generate a unique document ID for git files
|
||||
|
||||
Args:
|
||||
identifier: Unique identifier for the document (file path, record ID, etc.)
|
||||
|
||||
Returns:
|
||||
Unique document ID
|
||||
"""
|
||||
# For git, identifier is typically in the format "repo_url:file_path"
|
||||
if ':' in identifier:
|
||||
repo_url, file_path = identifier.split(':', 1)
|
||||
return self._generate_doc_id(repo_url, file_path)
|
||||
else:
|
||||
# Fallback: use the identifier as is
|
||||
sanitized_identifier = identifier.replace('/', '_').replace('\\', '_')
|
||||
return f"git_{sanitized_identifier}"
|
||||
|
||||
def fetch_new_documents(self, last_sync_time=None) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Fetch new/updated documents from the data source since last sync time
|
||||
|
||||
Args:
|
||||
last_sync_time: Last synchronization time
|
||||
|
||||
Returns:
|
||||
List of new/updated documents
|
||||
"""
|
||||
# Use the existing fetch_all_documents method which already supports last_sync_time
|
||||
return self.fetch_all_documents(last_sync_time)
|
||||
|
||||
def doc_to_llamaindex_doc(self, doc: Dict) -> 'Document':
|
||||
"""
|
||||
Convert git document to LlamaIndex Document
|
||||
|
||||
Args:
|
||||
doc: Git document dictionary
|
||||
|
||||
Returns:
|
||||
LlamaIndex Document object
|
||||
"""
|
||||
content = doc.get('content', "")
|
||||
doc_id = doc.get('id', "")
|
||||
metadata = doc.get('metadata', {})
|
||||
|
||||
# Ensure metadata has source information
|
||||
metadata['source'] = 'git'
|
||||
metadata['repository'] = metadata.get('repository', 'unknown')
|
||||
|
||||
# Create Document
|
||||
return Document(
|
||||
text=content,
|
||||
id_=doc_id,
|
||||
metadata=metadata
|
||||
)
|
||||
|
||||
def get_synced_document_ids(self) -> Set[str]:
|
||||
"""
|
||||
Get IDs of all files in git repositories
|
||||
|
||||
Returns:
|
||||
Set of document IDs
|
||||
"""
|
||||
doc_ids = set()
|
||||
|
||||
try:
|
||||
if self.config.git_mode == "single":
|
||||
# Get files from single repository
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
self._clone_repository(self.config.git_repo_url, temp_dir)
|
||||
self._checkout_branch(temp_dir, self.config.git_branch)
|
||||
files = self._get_all_files(temp_dir)
|
||||
for file_path in files:
|
||||
if self._should_process_file(file_path):
|
||||
doc_id = self._generate_doc_id(self.config.git_repo_url, file_path)
|
||||
doc_ids.add(doc_id)
|
||||
else:
|
||||
# Get files from multiple repositories
|
||||
repos = self._get_server_repositories()
|
||||
for repo_name, repo_path in repos:
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
repo_url = f"ssh://git@{self.config.git_server_url}:{repo_path}"
|
||||
self._clone_repository(repo_url, temp_dir)
|
||||
branch = self._get_default_branch(temp_dir)
|
||||
self._checkout_branch(temp_dir, branch)
|
||||
files = self._get_all_files(temp_dir)
|
||||
for file_path in files:
|
||||
if self._should_process_file(file_path):
|
||||
doc_id = self._generate_doc_id(repo_url, file_path)
|
||||
doc_ids.add(doc_id)
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting synced document IDs: {e}")
|
||||
|
||||
return doc_ids
|
||||
|
||||
@staticmethod
|
||||
def check_data_source_exists(config: BaseDataSourceConfig) -> bool:
|
||||
"""
|
||||
Check if the git repository or server exists and is accessible
|
||||
|
||||
Args:
|
||||
config: Git configuration
|
||||
|
||||
Returns:
|
||||
True if git repository/server exists and is accessible, False otherwise
|
||||
"""
|
||||
try:
|
||||
git_sync = GitSync(config)
|
||||
return git_sync.test_connection()
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking git data source: {e}")
|
||||
return False
|
||||
175
sync_service.py
175
sync_service.py
|
|
@ -18,63 +18,25 @@ class SyncService:
|
|||
self.source_config = source_config
|
||||
self.source_name = source_config.name
|
||||
|
||||
# Check if the data source exists before proceeding
|
||||
logger.info(f"Checking if data source exists: {self.source_name}")
|
||||
|
||||
try:
|
||||
# 直接调用具体同步类的 check_data_source_exists 方法,以便捕获详细的错误信息
|
||||
if self.source_config.type == 'database':
|
||||
sync_class = get_sync_class(self.source_config.type, getattr(self.source_config, 'db_type', 'mysql'))
|
||||
else:
|
||||
sync_class = get_sync_class(self.source_config.type)
|
||||
sync_class.check_data_source_exists(self.source_config)
|
||||
|
||||
logger.info(f"✓ Data source {self.source_name} exists")
|
||||
except Exception as e:
|
||||
error_msg = (
|
||||
f"Error: Data source {self.source_name} does not exist or cannot be accessed\n"
|
||||
f"Details: {str(e)}\n"
|
||||
f"Please check:\n"
|
||||
f" 1. Data source exists and is accessible\n"
|
||||
f" 2. Connection details (host, port, credentials) are correct\n"
|
||||
f" 3. Network connectivity is available\n"
|
||||
f" 4. Firewall rules allow connections"
|
||||
)
|
||||
logger.error(error_msg)
|
||||
raise RuntimeError(error_msg)
|
||||
|
||||
self.vector_store_manager = VectorStoreManager()
|
||||
self._running = False
|
||||
self._sync_in_progress = False # Flag to prevent concurrent syncs
|
||||
self._auto_sync_task = None # Reference to auto sync task to prevent multiple instances
|
||||
|
||||
# Initialize syncer for this data source
|
||||
self.syncer = None # Will be initialized in sync_all
|
||||
|
||||
# Initialize sync tracking data
|
||||
self.last_sync_time = None
|
||||
|
||||
# Read last sync time from data_sources table if available
|
||||
try:
|
||||
# Get the appropriate sync class based on data source type
|
||||
if self.source_config.type == 'database':
|
||||
sync_class = get_sync_class(self.source_config.type, getattr(self.source_config, 'db_type', 'mysql'))
|
||||
else:
|
||||
sync_class = get_sync_class(self.source_config.type)
|
||||
|
||||
# Create a new syncer instance for this data source
|
||||
self.syncer = sync_class(self.source_config, self.vector_store_manager)
|
||||
|
||||
# Initialize sync tracking data
|
||||
self.last_sync_time = None
|
||||
|
||||
# Read last sync time from data_sources table if available
|
||||
try:
|
||||
update_at = get_data_source_update_at(self.source_name)
|
||||
if update_at:
|
||||
self.last_sync_time = update_at
|
||||
logger.info(f"Initialized last_sync_time from data_sources: {self.last_sync_time}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Error reading last_sync_time from data_sources: {e}")
|
||||
|
||||
logger.info(f"Initialized {self.source_config.type} syncer for {self.source_name}")
|
||||
update_at = get_data_source_update_at(self.source_name)
|
||||
if update_at:
|
||||
self.last_sync_time = update_at
|
||||
logger.info(f"Initialized last_sync_time from data_sources: {self.last_sync_time}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to initialize sync for {self.source_name}: {e}")
|
||||
raise
|
||||
logger.warning(f"Error reading last_sync_time from data_sources: {e}")
|
||||
|
||||
logger.info(f"Initialized SyncService for {self.source_name}")
|
||||
|
||||
async def sync_all(self, force: bool = False, is_manual: bool = False):
|
||||
"""
|
||||
|
|
@ -94,6 +56,48 @@ class SyncService:
|
|||
try:
|
||||
logger.info(f"[同步] 开始全量同步: {self.source_name}")
|
||||
|
||||
# Initialize syncer if not already initialized
|
||||
if not self.syncer:
|
||||
logger.info(f"Initializing syncer for {self.source_name}...")
|
||||
# Check if the data source exists before proceeding
|
||||
try:
|
||||
# 直接调用具体同步类的 check_data_source_exists 方法,以便捕获详细的错误信息
|
||||
if self.source_config.type == 'database':
|
||||
sync_class = get_sync_class(self.source_config.type, getattr(self.source_config, 'db_type', 'mysql'))
|
||||
else:
|
||||
sync_class = get_sync_class(self.source_config.type)
|
||||
sync_class.check_data_source_exists(self.source_config)
|
||||
|
||||
logger.info(f"✓ Data source {self.source_name} exists")
|
||||
except Exception as e:
|
||||
error_msg = (
|
||||
f"Error: Data source {self.source_name} does not exist or cannot be accessed\n"
|
||||
f"Details: {str(e)}\n"
|
||||
f"Please check:\n"
|
||||
f" 1. Data source exists and is accessible\n"
|
||||
f" 2. Connection details (host, port, credentials) are correct\n"
|
||||
f" 3. Network connectivity is available\n"
|
||||
f" 4. Firewall rules allow connections"
|
||||
)
|
||||
logger.error(error_msg)
|
||||
raise RuntimeError(error_msg)
|
||||
|
||||
# Initialize syncer for this data source
|
||||
try:
|
||||
# Get the appropriate sync class based on data source type
|
||||
if self.source_config.type == 'database':
|
||||
sync_class = get_sync_class(self.source_config.type, getattr(self.source_config, 'db_type', 'mysql'))
|
||||
else:
|
||||
sync_class = get_sync_class(self.source_config.type)
|
||||
|
||||
# Create a new syncer instance for this data source
|
||||
self.syncer = sync_class(self.source_config, self.vector_store_manager)
|
||||
|
||||
logger.info(f"Initialized {self.source_config.type} syncer for {self.source_name}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to initialize sync for {self.source_name}: {e}")
|
||||
raise
|
||||
|
||||
# Run all synchronous operations in thread pool to avoid blocking event loop
|
||||
import asyncio
|
||||
loop = asyncio.get_event_loop()
|
||||
|
|
@ -197,6 +201,33 @@ class SyncService:
|
|||
chunked_docs = self.syncer.chunk_documents(processed_docs)
|
||||
all_chunked_docs.extend(chunked_docs)
|
||||
|
||||
# Update last sync time
|
||||
self.last_sync_time = datetime.now()
|
||||
elif self.source_config.type == "git":
|
||||
if not force:
|
||||
new_documents = []
|
||||
for doc in documents:
|
||||
# 检查服务运行状态:仅在非手动同步时检查
|
||||
if not is_manual and not self._running:
|
||||
logger.info(f"Sync interrupted during document filtering: {self.source_name}")
|
||||
return all_chunked_docs, total_docs, skipped_docs_count # 返回结果,不退出程序
|
||||
|
||||
doc_id = doc.get('id')
|
||||
if not self.vector_store_manager.document_exists(doc_id):
|
||||
new_documents.append(doc)
|
||||
else:
|
||||
skipped_docs_count += 1
|
||||
|
||||
if not new_documents:
|
||||
self.last_sync_time = datetime.now()
|
||||
return all_chunked_docs, total_docs, skipped_docs_count
|
||||
documents = new_documents
|
||||
|
||||
# Process and chunk documents
|
||||
processed_docs = self.syncer.process_documents(documents)
|
||||
chunked_docs = self.syncer.chunk_documents(processed_docs)
|
||||
all_chunked_docs.extend(chunked_docs)
|
||||
|
||||
# Update last sync time
|
||||
self.last_sync_time = datetime.now()
|
||||
else:
|
||||
|
|
@ -266,6 +297,48 @@ class SyncService:
|
|||
try:
|
||||
logger.info(f"Starting incremental sync from data source: {self.source_name}")
|
||||
|
||||
# Initialize syncer if not already initialized
|
||||
if not self.syncer:
|
||||
logger.info(f"Initializing syncer for {self.source_name}...")
|
||||
# Check if the data source exists before proceeding
|
||||
try:
|
||||
# 直接调用具体同步类的 check_data_source_exists 方法,以便捕获详细的错误信息
|
||||
if self.source_config.type == 'database':
|
||||
sync_class = get_sync_class(self.source_config.type, getattr(self.source_config, 'db_type', 'mysql'))
|
||||
else:
|
||||
sync_class = get_sync_class(self.source_config.type)
|
||||
sync_class.check_data_source_exists(self.source_config)
|
||||
|
||||
logger.info(f"✓ Data source {self.source_name} exists")
|
||||
except Exception as e:
|
||||
error_msg = (
|
||||
f"Error: Data source {self.source_name} does not exist or cannot be accessed\n"
|
||||
f"Details: {str(e)}\n"
|
||||
f"Please check:\n"
|
||||
f" 1. Data source exists and is accessible\n"
|
||||
f" 2. Connection details (host, port, credentials) are correct\n"
|
||||
f" 3. Network connectivity is available\n"
|
||||
f" 4. Firewall rules allow connections"
|
||||
)
|
||||
logger.error(error_msg)
|
||||
raise RuntimeError(error_msg)
|
||||
|
||||
# Initialize syncer for this data source
|
||||
try:
|
||||
# Get the appropriate sync class based on data source type
|
||||
if self.source_config.type == 'database':
|
||||
sync_class = get_sync_class(self.source_config.type, getattr(self.source_config, 'db_type', 'mysql'))
|
||||
else:
|
||||
sync_class = get_sync_class(self.source_config.type)
|
||||
|
||||
# Create a new syncer instance for this data source
|
||||
self.syncer = sync_class(self.source_config, self.vector_store_manager)
|
||||
|
||||
logger.info(f"Initialized {self.source_config.type} syncer for {self.source_name}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to initialize sync for {self.source_name}: {e}")
|
||||
raise
|
||||
|
||||
# Run all synchronous operations in thread pool
|
||||
import asyncio
|
||||
loop = asyncio.get_event_loop()
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
ref: refs/heads/master
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
[core]
|
||||
repositoryformatversion = 0
|
||||
filemode = false
|
||||
bare = true
|
||||
symlinks = false
|
||||
ignorecase = true
|
||||
|
|
@ -0,0 +1 @@
|
|||
Unnamed repository; edit this file 'description' to name the repository.
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to check the commit log message taken by
|
||||
# applypatch from an e-mail message.
|
||||
#
|
||||
# The hook should exit with non-zero status after issuing an
|
||||
# appropriate message if it wants to stop the commit. The hook is
|
||||
# allowed to edit the commit message file.
|
||||
#
|
||||
# To enable this hook, rename this file to "applypatch-msg".
|
||||
|
||||
. git-sh-setup
|
||||
commitmsg="$(git rev-parse --git-path hooks/commit-msg)"
|
||||
test -x "$commitmsg" && exec "$commitmsg" ${1+"$@"}
|
||||
:
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to check the commit log message.
|
||||
# Called by "git commit" with one argument, the name of the file
|
||||
# that has the commit message. The hook should exit with non-zero
|
||||
# status after issuing an appropriate message if it wants to stop the
|
||||
# commit. The hook is allowed to edit the commit message file.
|
||||
#
|
||||
# To enable this hook, rename this file to "commit-msg".
|
||||
|
||||
# Uncomment the below to add a Signed-off-by line to the message.
|
||||
# Doing this in a hook is a bad idea in general, but the prepare-commit-msg
|
||||
# hook is more suited to it.
|
||||
#
|
||||
# SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p')
|
||||
# grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1"
|
||||
|
||||
# This example catches duplicate Signed-off-by lines.
|
||||
|
||||
test "" = "$(grep '^Signed-off-by: ' "$1" |
|
||||
sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" || {
|
||||
echo >&2 Duplicate Signed-off-by lines.
|
||||
exit 1
|
||||
}
|
||||
|
|
@ -0,0 +1,174 @@
|
|||
#!/usr/bin/perl
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
use IPC::Open2;
|
||||
|
||||
# An example hook script to integrate Watchman
|
||||
# (https://facebook.github.io/watchman/) with git to speed up detecting
|
||||
# new and modified files.
|
||||
#
|
||||
# The hook is passed a version (currently 2) and last update token
|
||||
# formatted as a string and outputs to stdout a new update token and
|
||||
# all files that have been modified since the update token. Paths must
|
||||
# be relative to the root of the working tree and separated by a single NUL.
|
||||
#
|
||||
# To enable this hook, rename this file to "query-watchman" and set
|
||||
# 'git config core.fsmonitor .git/hooks/query-watchman'
|
||||
#
|
||||
my ($version, $last_update_token) = @ARGV;
|
||||
|
||||
# Uncomment for debugging
|
||||
# print STDERR "$0 $version $last_update_token\n";
|
||||
|
||||
# Check the hook interface version
|
||||
if ($version ne 2) {
|
||||
die "Unsupported query-fsmonitor hook version '$version'.\n" .
|
||||
"Falling back to scanning...\n";
|
||||
}
|
||||
|
||||
my $git_work_tree = get_working_dir();
|
||||
|
||||
my $retry = 1;
|
||||
|
||||
my $json_pkg;
|
||||
eval {
|
||||
require JSON::XS;
|
||||
$json_pkg = "JSON::XS";
|
||||
1;
|
||||
} or do {
|
||||
require JSON::PP;
|
||||
$json_pkg = "JSON::PP";
|
||||
};
|
||||
|
||||
launch_watchman();
|
||||
|
||||
sub launch_watchman {
|
||||
my $o = watchman_query();
|
||||
if (is_work_tree_watched($o)) {
|
||||
output_result($o->{clock}, @{$o->{files}});
|
||||
}
|
||||
}
|
||||
|
||||
sub output_result {
|
||||
my ($clockid, @files) = @_;
|
||||
|
||||
# Uncomment for debugging watchman output
|
||||
# open (my $fh, ">", ".git/watchman-output.out");
|
||||
# binmode $fh, ":utf8";
|
||||
# print $fh "$clockid\n@files\n";
|
||||
# close $fh;
|
||||
|
||||
binmode STDOUT, ":utf8";
|
||||
print $clockid;
|
||||
print "\0";
|
||||
local $, = "\0";
|
||||
print @files;
|
||||
}
|
||||
|
||||
sub watchman_clock {
|
||||
my $response = qx/watchman clock "$git_work_tree"/;
|
||||
die "Failed to get clock id on '$git_work_tree'.\n" .
|
||||
"Falling back to scanning...\n" if $? != 0;
|
||||
|
||||
return $json_pkg->new->utf8->decode($response);
|
||||
}
|
||||
|
||||
sub watchman_query {
|
||||
my $pid = open2(\*CHLD_OUT, \*CHLD_IN, 'watchman -j --no-pretty')
|
||||
or die "open2() failed: $!\n" .
|
||||
"Falling back to scanning...\n";
|
||||
|
||||
# In the query expression below we're asking for names of files that
|
||||
# changed since $last_update_token but not from the .git folder.
|
||||
#
|
||||
# To accomplish this, we're using the "since" generator to use the
|
||||
# recency index to select candidate nodes and "fields" to limit the
|
||||
# output to file names only. Then we're using the "expression" term to
|
||||
# further constrain the results.
|
||||
my $last_update_line = "";
|
||||
if (substr($last_update_token, 0, 1) eq "c") {
|
||||
$last_update_token = "\"$last_update_token\"";
|
||||
$last_update_line = qq[\n"since": $last_update_token,];
|
||||
}
|
||||
my $query = <<" END";
|
||||
["query", "$git_work_tree", {$last_update_line
|
||||
"fields": ["name"],
|
||||
"expression": ["not", ["dirname", ".git"]]
|
||||
}]
|
||||
END
|
||||
|
||||
# Uncomment for debugging the watchman query
|
||||
# open (my $fh, ">", ".git/watchman-query.json");
|
||||
# print $fh $query;
|
||||
# close $fh;
|
||||
|
||||
print CHLD_IN $query;
|
||||
close CHLD_IN;
|
||||
my $response = do {local $/; <CHLD_OUT>};
|
||||
|
||||
# Uncomment for debugging the watch response
|
||||
# open ($fh, ">", ".git/watchman-response.json");
|
||||
# print $fh $response;
|
||||
# close $fh;
|
||||
|
||||
die "Watchman: command returned no output.\n" .
|
||||
"Falling back to scanning...\n" if $response eq "";
|
||||
die "Watchman: command returned invalid output: $response\n" .
|
||||
"Falling back to scanning...\n" unless $response =~ /^\{/;
|
||||
|
||||
return $json_pkg->new->utf8->decode($response);
|
||||
}
|
||||
|
||||
sub is_work_tree_watched {
|
||||
my ($output) = @_;
|
||||
my $error = $output->{error};
|
||||
if ($retry > 0 and $error and $error =~ m/unable to resolve root .* directory (.*) is not watched/) {
|
||||
$retry--;
|
||||
my $response = qx/watchman watch "$git_work_tree"/;
|
||||
die "Failed to make watchman watch '$git_work_tree'.\n" .
|
||||
"Falling back to scanning...\n" if $? != 0;
|
||||
$output = $json_pkg->new->utf8->decode($response);
|
||||
$error = $output->{error};
|
||||
die "Watchman: $error.\n" .
|
||||
"Falling back to scanning...\n" if $error;
|
||||
|
||||
# Uncomment for debugging watchman output
|
||||
# open (my $fh, ">", ".git/watchman-output.out");
|
||||
# close $fh;
|
||||
|
||||
# Watchman will always return all files on the first query so
|
||||
# return the fast "everything is dirty" flag to git and do the
|
||||
# Watchman query just to get it over with now so we won't pay
|
||||
# the cost in git to look up each individual file.
|
||||
my $o = watchman_clock();
|
||||
$error = $output->{error};
|
||||
|
||||
die "Watchman: $error.\n" .
|
||||
"Falling back to scanning...\n" if $error;
|
||||
|
||||
output_result($o->{clock}, ("/"));
|
||||
$last_update_token = $o->{clock};
|
||||
|
||||
eval { launch_watchman() };
|
||||
return 0;
|
||||
}
|
||||
|
||||
die "Watchman: $error.\n" .
|
||||
"Falling back to scanning...\n" if $error;
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
sub get_working_dir {
|
||||
my $working_dir;
|
||||
if ($^O =~ 'msys' || $^O =~ 'cygwin') {
|
||||
$working_dir = Win32::GetCwd();
|
||||
$working_dir =~ tr/\\/\//;
|
||||
} else {
|
||||
require Cwd;
|
||||
$working_dir = Cwd::cwd();
|
||||
}
|
||||
|
||||
return $working_dir;
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to prepare a packed repository for use over
|
||||
# dumb transports.
|
||||
#
|
||||
# To enable this hook, rename this file to "post-update".
|
||||
|
||||
exec git update-server-info
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to verify what is about to be committed
|
||||
# by applypatch from an e-mail message.
|
||||
#
|
||||
# The hook should exit with non-zero status after issuing an
|
||||
# appropriate message if it wants to stop the commit.
|
||||
#
|
||||
# To enable this hook, rename this file to "pre-applypatch".
|
||||
|
||||
. git-sh-setup
|
||||
precommit="$(git rev-parse --git-path hooks/pre-commit)"
|
||||
test -x "$precommit" && exec "$precommit" ${1+"$@"}
|
||||
:
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to verify what is about to be committed.
|
||||
# Called by "git commit" with no arguments. The hook should
|
||||
# exit with non-zero status after issuing an appropriate message if
|
||||
# it wants to stop the commit.
|
||||
#
|
||||
# To enable this hook, rename this file to "pre-commit".
|
||||
|
||||
if git rev-parse --verify HEAD >/dev/null 2>&1
|
||||
then
|
||||
against=HEAD
|
||||
else
|
||||
# Initial commit: diff against an empty tree object
|
||||
against=$(git hash-object -t tree /dev/null)
|
||||
fi
|
||||
|
||||
# If you want to allow non-ASCII filenames set this variable to true.
|
||||
allownonascii=$(git config --type=bool hooks.allownonascii)
|
||||
|
||||
# Redirect output to stderr.
|
||||
exec 1>&2
|
||||
|
||||
# Cross platform projects tend to avoid non-ASCII filenames; prevent
|
||||
# them from being added to the repository. We exploit the fact that the
|
||||
# printable range starts at the space character and ends with tilde.
|
||||
if [ "$allownonascii" != "true" ] &&
|
||||
# Note that the use of brackets around a tr range is ok here, (it's
|
||||
# even required, for portability to Solaris 10's /usr/bin/tr), since
|
||||
# the square bracket bytes happen to fall in the designated range.
|
||||
test $(git diff-index --cached --name-only --diff-filter=A -z $against |
|
||||
LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0
|
||||
then
|
||||
cat <<\EOF
|
||||
Error: Attempt to add a non-ASCII file name.
|
||||
|
||||
This can cause problems if you want to work with people on other platforms.
|
||||
|
||||
To be portable it is advisable to rename the file.
|
||||
|
||||
If you know what you are doing you can disable this check using:
|
||||
|
||||
git config hooks.allownonascii true
|
||||
EOF
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# If there are whitespace errors, print the offending file names and fail.
|
||||
exec git diff-index --check --cached $against --
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to verify what is about to be committed.
|
||||
# Called by "git merge" with no arguments. The hook should
|
||||
# exit with non-zero status after issuing an appropriate message to
|
||||
# stderr if it wants to stop the merge commit.
|
||||
#
|
||||
# To enable this hook, rename this file to "pre-merge-commit".
|
||||
|
||||
. git-sh-setup
|
||||
test -x "$GIT_DIR/hooks/pre-commit" &&
|
||||
exec "$GIT_DIR/hooks/pre-commit"
|
||||
:
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
#!/bin/sh
|
||||
|
||||
# An example hook script to verify what is about to be pushed. Called by "git
|
||||
# push" after it has checked the remote status, but before anything has been
|
||||
# pushed. If this script exits with a non-zero status nothing will be pushed.
|
||||
#
|
||||
# This hook is called with the following parameters:
|
||||
#
|
||||
# $1 -- Name of the remote to which the push is being done
|
||||
# $2 -- URL to which the push is being done
|
||||
#
|
||||
# If pushing without using a named remote those arguments will be equal.
|
||||
#
|
||||
# Information about the commits which are being pushed is supplied as lines to
|
||||
# the standard input in the form:
|
||||
#
|
||||
# <local ref> <local oid> <remote ref> <remote oid>
|
||||
#
|
||||
# This sample shows how to prevent push of commits where the log message starts
|
||||
# with "WIP" (work in progress).
|
||||
|
||||
remote="$1"
|
||||
url="$2"
|
||||
|
||||
zero=$(git hash-object --stdin </dev/null | tr '[0-9a-f]' '0')
|
||||
|
||||
while read local_ref local_oid remote_ref remote_oid
|
||||
do
|
||||
if test "$local_oid" = "$zero"
|
||||
then
|
||||
# Handle delete
|
||||
:
|
||||
else
|
||||
if test "$remote_oid" = "$zero"
|
||||
then
|
||||
# New branch, examine all commits
|
||||
range="$local_oid"
|
||||
else
|
||||
# Update to existing branch, examine new commits
|
||||
range="$remote_oid..$local_oid"
|
||||
fi
|
||||
|
||||
# Check for WIP commit
|
||||
commit=$(git rev-list -n 1 --grep '^WIP' "$range")
|
||||
if test -n "$commit"
|
||||
then
|
||||
echo >&2 "Found WIP commit in $local_ref, not pushing"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
exit 0
|
||||
|
|
@ -0,0 +1,169 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# Copyright (c) 2006, 2008 Junio C Hamano
|
||||
#
|
||||
# The "pre-rebase" hook is run just before "git rebase" starts doing
|
||||
# its job, and can prevent the command from running by exiting with
|
||||
# non-zero status.
|
||||
#
|
||||
# The hook is called with the following parameters:
|
||||
#
|
||||
# $1 -- the upstream the series was forked from.
|
||||
# $2 -- the branch being rebased (or empty when rebasing the current branch).
|
||||
#
|
||||
# This sample shows how to prevent topic branches that are already
|
||||
# merged to 'next' branch from getting rebased, because allowing it
|
||||
# would result in rebasing already published history.
|
||||
|
||||
publish=next
|
||||
basebranch="$1"
|
||||
if test "$#" = 2
|
||||
then
|
||||
topic="refs/heads/$2"
|
||||
else
|
||||
topic=`git symbolic-ref HEAD` ||
|
||||
exit 0 ;# we do not interrupt rebasing detached HEAD
|
||||
fi
|
||||
|
||||
case "$topic" in
|
||||
refs/heads/??/*)
|
||||
;;
|
||||
*)
|
||||
exit 0 ;# we do not interrupt others.
|
||||
;;
|
||||
esac
|
||||
|
||||
# Now we are dealing with a topic branch being rebased
|
||||
# on top of master. Is it OK to rebase it?
|
||||
|
||||
# Does the topic really exist?
|
||||
git show-ref -q "$topic" || {
|
||||
echo >&2 "No such branch $topic"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Is topic fully merged to master?
|
||||
not_in_master=`git rev-list --pretty=oneline ^master "$topic"`
|
||||
if test -z "$not_in_master"
|
||||
then
|
||||
echo >&2 "$topic is fully merged to master; better remove it."
|
||||
exit 1 ;# we could allow it, but there is no point.
|
||||
fi
|
||||
|
||||
# Is topic ever merged to next? If so you should not be rebasing it.
|
||||
only_next_1=`git rev-list ^master "^$topic" ${publish} | sort`
|
||||
only_next_2=`git rev-list ^master ${publish} | sort`
|
||||
if test "$only_next_1" = "$only_next_2"
|
||||
then
|
||||
not_in_topic=`git rev-list "^$topic" master`
|
||||
if test -z "$not_in_topic"
|
||||
then
|
||||
echo >&2 "$topic is already up to date with master"
|
||||
exit 1 ;# we could allow it, but there is no point.
|
||||
else
|
||||
exit 0
|
||||
fi
|
||||
else
|
||||
not_in_next=`git rev-list --pretty=oneline ^${publish} "$topic"`
|
||||
/usr/bin/perl -e '
|
||||
my $topic = $ARGV[0];
|
||||
my $msg = "* $topic has commits already merged to public branch:\n";
|
||||
my (%not_in_next) = map {
|
||||
/^([0-9a-f]+) /;
|
||||
($1 => 1);
|
||||
} split(/\n/, $ARGV[1]);
|
||||
for my $elem (map {
|
||||
/^([0-9a-f]+) (.*)$/;
|
||||
[$1 => $2];
|
||||
} split(/\n/, $ARGV[2])) {
|
||||
if (!exists $not_in_next{$elem->[0]}) {
|
||||
if ($msg) {
|
||||
print STDERR $msg;
|
||||
undef $msg;
|
||||
}
|
||||
print STDERR " $elem->[1]\n";
|
||||
}
|
||||
}
|
||||
' "$topic" "$not_in_next" "$not_in_master"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
<<\DOC_END
|
||||
|
||||
This sample hook safeguards topic branches that have been
|
||||
published from being rewound.
|
||||
|
||||
The workflow assumed here is:
|
||||
|
||||
* Once a topic branch forks from "master", "master" is never
|
||||
merged into it again (either directly or indirectly).
|
||||
|
||||
* Once a topic branch is fully cooked and merged into "master",
|
||||
it is deleted. If you need to build on top of it to correct
|
||||
earlier mistakes, a new topic branch is created by forking at
|
||||
the tip of the "master". This is not strictly necessary, but
|
||||
it makes it easier to keep your history simple.
|
||||
|
||||
* Whenever you need to test or publish your changes to topic
|
||||
branches, merge them into "next" branch.
|
||||
|
||||
The script, being an example, hardcodes the publish branch name
|
||||
to be "next", but it is trivial to make it configurable via
|
||||
$GIT_DIR/config mechanism.
|
||||
|
||||
With this workflow, you would want to know:
|
||||
|
||||
(1) ... if a topic branch has ever been merged to "next". Young
|
||||
topic branches can have stupid mistakes you would rather
|
||||
clean up before publishing, and things that have not been
|
||||
merged into other branches can be easily rebased without
|
||||
affecting other people. But once it is published, you would
|
||||
not want to rewind it.
|
||||
|
||||
(2) ... if a topic branch has been fully merged to "master".
|
||||
Then you can delete it. More importantly, you should not
|
||||
build on top of it -- other people may already want to
|
||||
change things related to the topic as patches against your
|
||||
"master", so if you need further changes, it is better to
|
||||
fork the topic (perhaps with the same name) afresh from the
|
||||
tip of "master".
|
||||
|
||||
Let's look at this example:
|
||||
|
||||
o---o---o---o---o---o---o---o---o---o "next"
|
||||
/ / / /
|
||||
/ a---a---b A / /
|
||||
/ / / /
|
||||
/ / c---c---c---c B /
|
||||
/ / / \ /
|
||||
/ / / b---b C \ /
|
||||
/ / / / \ /
|
||||
---o---o---o---o---o---o---o---o---o---o---o "master"
|
||||
|
||||
|
||||
A, B and C are topic branches.
|
||||
|
||||
* A has one fix since it was merged up to "next".
|
||||
|
||||
* B has finished. It has been fully merged up to "master" and "next",
|
||||
and is ready to be deleted.
|
||||
|
||||
* C has not merged to "next" at all.
|
||||
|
||||
We would want to allow C to be rebased, refuse A, and encourage
|
||||
B to be deleted.
|
||||
|
||||
To compute (1):
|
||||
|
||||
git rev-list ^master ^topic next
|
||||
git rev-list ^master next
|
||||
|
||||
if these match, topic has not merged in next at all.
|
||||
|
||||
To compute (2):
|
||||
|
||||
git rev-list master..topic
|
||||
|
||||
if this is empty, it is fully merged to "master".
|
||||
|
||||
DOC_END
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to make use of push options.
|
||||
# The example simply echoes all push options that start with 'echoback='
|
||||
# and rejects all pushes when the "reject" push option is used.
|
||||
#
|
||||
# To enable this hook, rename this file to "pre-receive".
|
||||
|
||||
if test -n "$GIT_PUSH_OPTION_COUNT"
|
||||
then
|
||||
i=0
|
||||
while test "$i" -lt "$GIT_PUSH_OPTION_COUNT"
|
||||
do
|
||||
eval "value=\$GIT_PUSH_OPTION_$i"
|
||||
case "$value" in
|
||||
echoback=*)
|
||||
echo "echo from the pre-receive-hook: ${value#*=}" >&2
|
||||
;;
|
||||
reject)
|
||||
exit 1
|
||||
esac
|
||||
i=$((i + 1))
|
||||
done
|
||||
fi
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to prepare the commit log message.
|
||||
# Called by "git commit" with the name of the file that has the
|
||||
# commit message, followed by the description of the commit
|
||||
# message's source. The hook's purpose is to edit the commit
|
||||
# message file. If the hook fails with a non-zero status,
|
||||
# the commit is aborted.
|
||||
#
|
||||
# To enable this hook, rename this file to "prepare-commit-msg".
|
||||
|
||||
# This hook includes three examples. The first one removes the
|
||||
# "# Please enter the commit message..." help message.
|
||||
#
|
||||
# The second includes the output of "git diff --name-status -r"
|
||||
# into the message, just before the "git status" output. It is
|
||||
# commented because it doesn't cope with --amend or with squashed
|
||||
# commits.
|
||||
#
|
||||
# The third example adds a Signed-off-by line to the message, that can
|
||||
# still be edited. This is rarely a good idea.
|
||||
|
||||
COMMIT_MSG_FILE=$1
|
||||
COMMIT_SOURCE=$2
|
||||
SHA1=$3
|
||||
|
||||
/usr/bin/perl -i.bak -ne 'print unless(m/^. Please enter the commit message/..m/^#$/)' "$COMMIT_MSG_FILE"
|
||||
|
||||
# case "$COMMIT_SOURCE,$SHA1" in
|
||||
# ,|template,)
|
||||
# /usr/bin/perl -i.bak -pe '
|
||||
# print "\n" . `git diff --cached --name-status -r`
|
||||
# if /^#/ && $first++ == 0' "$COMMIT_MSG_FILE" ;;
|
||||
# *) ;;
|
||||
# esac
|
||||
|
||||
# SOB=$(git var GIT_COMMITTER_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p')
|
||||
# git interpret-trailers --in-place --trailer "$SOB" "$COMMIT_MSG_FILE"
|
||||
# if test -z "$COMMIT_SOURCE"
|
||||
# then
|
||||
# /usr/bin/perl -i.bak -pe 'print "\n" if !$first_line++' "$COMMIT_MSG_FILE"
|
||||
# fi
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
#!/bin/sh
|
||||
|
||||
# An example hook script to update a checked-out tree on a git push.
|
||||
#
|
||||
# This hook is invoked by git-receive-pack(1) when it reacts to git
|
||||
# push and updates reference(s) in its repository, and when the push
|
||||
# tries to update the branch that is currently checked out and the
|
||||
# receive.denyCurrentBranch configuration variable is set to
|
||||
# updateInstead.
|
||||
#
|
||||
# By default, such a push is refused if the working tree and the index
|
||||
# of the remote repository has any difference from the currently
|
||||
# checked out commit; when both the working tree and the index match
|
||||
# the current commit, they are updated to match the newly pushed tip
|
||||
# of the branch. This hook is to be used to override the default
|
||||
# behaviour; however the code below reimplements the default behaviour
|
||||
# as a starting point for convenient modification.
|
||||
#
|
||||
# The hook receives the commit with which the tip of the current
|
||||
# branch is going to be updated:
|
||||
commit=$1
|
||||
|
||||
# It can exit with a non-zero status to refuse the push (when it does
|
||||
# so, it must not modify the index or the working tree).
|
||||
die () {
|
||||
echo >&2 "$*"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Or it can make any necessary changes to the working tree and to the
|
||||
# index to bring them to the desired state when the tip of the current
|
||||
# branch is updated to the new commit, and exit with a zero status.
|
||||
#
|
||||
# For example, the hook can simply run git read-tree -u -m HEAD "$1"
|
||||
# in order to emulate git fetch that is run in the reverse direction
|
||||
# with git push, as the two-tree form of git read-tree -u -m is
|
||||
# essentially the same as git switch or git checkout that switches
|
||||
# branches while keeping the local changes in the working tree that do
|
||||
# not interfere with the difference between the branches.
|
||||
|
||||
# The below is a more-or-less exact translation to shell of the C code
|
||||
# for the default behaviour for git's push-to-checkout hook defined in
|
||||
# the push_to_deploy() function in builtin/receive-pack.c.
|
||||
#
|
||||
# Note that the hook will be executed from the repository directory,
|
||||
# not from the working tree, so if you want to perform operations on
|
||||
# the working tree, you will have to adapt your code accordingly, e.g.
|
||||
# by adding "cd .." or using relative paths.
|
||||
|
||||
if ! git update-index -q --ignore-submodules --refresh
|
||||
then
|
||||
die "Up-to-date check failed"
|
||||
fi
|
||||
|
||||
if ! git diff-files --quiet --ignore-submodules --
|
||||
then
|
||||
die "Working directory has unstaged changes"
|
||||
fi
|
||||
|
||||
# This is a rough translation of:
|
||||
#
|
||||
# head_has_history() ? "HEAD" : EMPTY_TREE_SHA1_HEX
|
||||
if git cat-file -e HEAD 2>/dev/null
|
||||
then
|
||||
head=HEAD
|
||||
else
|
||||
head=$(git hash-object -t tree --stdin </dev/null)
|
||||
fi
|
||||
|
||||
if ! git diff-index --quiet --cached --ignore-submodules $head --
|
||||
then
|
||||
die "Working directory has staged changes"
|
||||
fi
|
||||
|
||||
if ! git read-tree -u -m "$commit"
|
||||
then
|
||||
die "Could not update working tree to new HEAD"
|
||||
fi
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
#!/bin/sh
|
||||
|
||||
# An example hook script to validate a patch (and/or patch series) before
|
||||
# sending it via email.
|
||||
#
|
||||
# The hook should exit with non-zero status after issuing an appropriate
|
||||
# message if it wants to prevent the email(s) from being sent.
|
||||
#
|
||||
# To enable this hook, rename this file to "sendemail-validate".
|
||||
#
|
||||
# By default, it will only check that the patch(es) can be applied on top of
|
||||
# the default upstream branch without conflicts in a secondary worktree. After
|
||||
# validation (successful or not) of the last patch of a series, the worktree
|
||||
# will be deleted.
|
||||
#
|
||||
# The following config variables can be set to change the default remote and
|
||||
# remote ref that are used to apply the patches against:
|
||||
#
|
||||
# sendemail.validateRemote (default: origin)
|
||||
# sendemail.validateRemoteRef (default: HEAD)
|
||||
#
|
||||
# Replace the TODO placeholders with appropriate checks according to your
|
||||
# needs.
|
||||
|
||||
validate_cover_letter () {
|
||||
file="$1"
|
||||
# TODO: Replace with appropriate checks (e.g. spell checking).
|
||||
true
|
||||
}
|
||||
|
||||
validate_patch () {
|
||||
file="$1"
|
||||
# Ensure that the patch applies without conflicts.
|
||||
git am -3 "$file" || return
|
||||
# TODO: Replace with appropriate checks for this patch
|
||||
# (e.g. checkpatch.pl).
|
||||
true
|
||||
}
|
||||
|
||||
validate_series () {
|
||||
# TODO: Replace with appropriate checks for the whole series
|
||||
# (e.g. quick build, coding style checks, etc.).
|
||||
true
|
||||
}
|
||||
|
||||
# main -------------------------------------------------------------------------
|
||||
|
||||
if test "$GIT_SENDEMAIL_FILE_COUNTER" = 1
|
||||
then
|
||||
remote=$(git config --default origin --get sendemail.validateRemote) &&
|
||||
ref=$(git config --default HEAD --get sendemail.validateRemoteRef) &&
|
||||
worktree=$(mktemp --tmpdir -d sendemail-validate.XXXXXXX) &&
|
||||
git worktree add -fd --checkout "$worktree" "refs/remotes/$remote/$ref" &&
|
||||
git config --replace-all sendemail.validateWorktree "$worktree"
|
||||
else
|
||||
worktree=$(git config --get sendemail.validateWorktree)
|
||||
fi || {
|
||||
echo "sendemail-validate: error: failed to prepare worktree" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
unset GIT_DIR GIT_WORK_TREE
|
||||
cd "$worktree" &&
|
||||
|
||||
if grep -q "^diff --git " "$1"
|
||||
then
|
||||
validate_patch "$1"
|
||||
else
|
||||
validate_cover_letter "$1"
|
||||
fi &&
|
||||
|
||||
if test "$GIT_SENDEMAIL_FILE_COUNTER" = "$GIT_SENDEMAIL_FILE_TOTAL"
|
||||
then
|
||||
git config --unset-all sendemail.validateWorktree &&
|
||||
trap 'git worktree remove -ff "$worktree"' EXIT &&
|
||||
validate_series
|
||||
fi
|
||||
|
|
@ -0,0 +1,128 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to block unannotated tags from entering.
|
||||
# Called by "git receive-pack" with arguments: refname sha1-old sha1-new
|
||||
#
|
||||
# To enable this hook, rename this file to "update".
|
||||
#
|
||||
# Config
|
||||
# ------
|
||||
# hooks.allowunannotated
|
||||
# This boolean sets whether unannotated tags will be allowed into the
|
||||
# repository. By default they won't be.
|
||||
# hooks.allowdeletetag
|
||||
# This boolean sets whether deleting tags will be allowed in the
|
||||
# repository. By default they won't be.
|
||||
# hooks.allowmodifytag
|
||||
# This boolean sets whether a tag may be modified after creation. By default
|
||||
# it won't be.
|
||||
# hooks.allowdeletebranch
|
||||
# This boolean sets whether deleting branches will be allowed in the
|
||||
# repository. By default they won't be.
|
||||
# hooks.denycreatebranch
|
||||
# This boolean sets whether remotely creating branches will be denied
|
||||
# in the repository. By default this is allowed.
|
||||
#
|
||||
|
||||
# --- Command line
|
||||
refname="$1"
|
||||
oldrev="$2"
|
||||
newrev="$3"
|
||||
|
||||
# --- Safety check
|
||||
if [ -z "$GIT_DIR" ]; then
|
||||
echo "Don't run this script from the command line." >&2
|
||||
echo " (if you want, you could supply GIT_DIR then run" >&2
|
||||
echo " $0 <ref> <oldrev> <newrev>)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then
|
||||
echo "usage: $0 <ref> <oldrev> <newrev>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- Config
|
||||
allowunannotated=$(git config --type=bool hooks.allowunannotated)
|
||||
allowdeletebranch=$(git config --type=bool hooks.allowdeletebranch)
|
||||
denycreatebranch=$(git config --type=bool hooks.denycreatebranch)
|
||||
allowdeletetag=$(git config --type=bool hooks.allowdeletetag)
|
||||
allowmodifytag=$(git config --type=bool hooks.allowmodifytag)
|
||||
|
||||
# check for no description
|
||||
projectdesc=$(sed -e '1q' "$GIT_DIR/description")
|
||||
case "$projectdesc" in
|
||||
"Unnamed repository"* | "")
|
||||
echo "*** Project description file hasn't been set" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# --- Check types
|
||||
# if $newrev is 0000...0000, it's a commit to delete a ref.
|
||||
zero=$(git hash-object --stdin </dev/null | tr '[0-9a-f]' '0')
|
||||
if [ "$newrev" = "$zero" ]; then
|
||||
newrev_type=delete
|
||||
else
|
||||
newrev_type=$(git cat-file -t $newrev)
|
||||
fi
|
||||
|
||||
case "$refname","$newrev_type" in
|
||||
refs/tags/*,commit)
|
||||
# un-annotated tag
|
||||
short_refname=${refname##refs/tags/}
|
||||
if [ "$allowunannotated" != "true" ]; then
|
||||
echo "*** The un-annotated tag, $short_refname, is not allowed in this repository" >&2
|
||||
echo "*** Use 'git tag [ -a | -s ]' for tags you want to propagate." >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
refs/tags/*,delete)
|
||||
# delete tag
|
||||
if [ "$allowdeletetag" != "true" ]; then
|
||||
echo "*** Deleting a tag is not allowed in this repository" >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
refs/tags/*,tag)
|
||||
# annotated tag
|
||||
if [ "$allowmodifytag" != "true" ] && git rev-parse $refname > /dev/null 2>&1
|
||||
then
|
||||
echo "*** Tag '$refname' already exists." >&2
|
||||
echo "*** Modifying a tag is not allowed in this repository." >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
refs/heads/*,commit)
|
||||
# branch
|
||||
if [ "$oldrev" = "$zero" -a "$denycreatebranch" = "true" ]; then
|
||||
echo "*** Creating a branch is not allowed in this repository" >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
refs/heads/*,delete)
|
||||
# delete branch
|
||||
if [ "$allowdeletebranch" != "true" ]; then
|
||||
echo "*** Deleting a branch is not allowed in this repository" >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
refs/remotes/*,commit)
|
||||
# tracking branch
|
||||
;;
|
||||
refs/remotes/*,delete)
|
||||
# delete tracking branch
|
||||
if [ "$allowdeletebranch" != "true" ]; then
|
||||
echo "*** Deleting a tracking branch is not allowed in this repository" >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
# Anything else (is there anything else?)
|
||||
echo "*** Update hook: unknown type of update to ref $refname of type $newrev_type" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# --- Finished
|
||||
exit 0
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
# git ls-files --others --exclude-from=.git/info/exclude
|
||||
# Lines that start with '#' are comments.
|
||||
# For a project mostly in C, the following would be a good set of
|
||||
# exclude patterns (uncomment them if you want to use them):
|
||||
# *.[oa]
|
||||
# *~
|
||||
|
|
@ -0,0 +1 @@
|
|||
ref: refs/heads/master
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
[core]
|
||||
repositoryformatversion = 0
|
||||
filemode = false
|
||||
bare = true
|
||||
symlinks = false
|
||||
ignorecase = true
|
||||
|
|
@ -0,0 +1 @@
|
|||
Unnamed repository; edit this file 'description' to name the repository.
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to check the commit log message taken by
|
||||
# applypatch from an e-mail message.
|
||||
#
|
||||
# The hook should exit with non-zero status after issuing an
|
||||
# appropriate message if it wants to stop the commit. The hook is
|
||||
# allowed to edit the commit message file.
|
||||
#
|
||||
# To enable this hook, rename this file to "applypatch-msg".
|
||||
|
||||
. git-sh-setup
|
||||
commitmsg="$(git rev-parse --git-path hooks/commit-msg)"
|
||||
test -x "$commitmsg" && exec "$commitmsg" ${1+"$@"}
|
||||
:
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to check the commit log message.
|
||||
# Called by "git commit" with one argument, the name of the file
|
||||
# that has the commit message. The hook should exit with non-zero
|
||||
# status after issuing an appropriate message if it wants to stop the
|
||||
# commit. The hook is allowed to edit the commit message file.
|
||||
#
|
||||
# To enable this hook, rename this file to "commit-msg".
|
||||
|
||||
# Uncomment the below to add a Signed-off-by line to the message.
|
||||
# Doing this in a hook is a bad idea in general, but the prepare-commit-msg
|
||||
# hook is more suited to it.
|
||||
#
|
||||
# SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p')
|
||||
# grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1"
|
||||
|
||||
# This example catches duplicate Signed-off-by lines.
|
||||
|
||||
test "" = "$(grep '^Signed-off-by: ' "$1" |
|
||||
sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" || {
|
||||
echo >&2 Duplicate Signed-off-by lines.
|
||||
exit 1
|
||||
}
|
||||
|
|
@ -0,0 +1,174 @@
|
|||
#!/usr/bin/perl
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
use IPC::Open2;
|
||||
|
||||
# An example hook script to integrate Watchman
|
||||
# (https://facebook.github.io/watchman/) with git to speed up detecting
|
||||
# new and modified files.
|
||||
#
|
||||
# The hook is passed a version (currently 2) and last update token
|
||||
# formatted as a string and outputs to stdout a new update token and
|
||||
# all files that have been modified since the update token. Paths must
|
||||
# be relative to the root of the working tree and separated by a single NUL.
|
||||
#
|
||||
# To enable this hook, rename this file to "query-watchman" and set
|
||||
# 'git config core.fsmonitor .git/hooks/query-watchman'
|
||||
#
|
||||
my ($version, $last_update_token) = @ARGV;
|
||||
|
||||
# Uncomment for debugging
|
||||
# print STDERR "$0 $version $last_update_token\n";
|
||||
|
||||
# Check the hook interface version
|
||||
if ($version ne 2) {
|
||||
die "Unsupported query-fsmonitor hook version '$version'.\n" .
|
||||
"Falling back to scanning...\n";
|
||||
}
|
||||
|
||||
my $git_work_tree = get_working_dir();
|
||||
|
||||
my $retry = 1;
|
||||
|
||||
my $json_pkg;
|
||||
eval {
|
||||
require JSON::XS;
|
||||
$json_pkg = "JSON::XS";
|
||||
1;
|
||||
} or do {
|
||||
require JSON::PP;
|
||||
$json_pkg = "JSON::PP";
|
||||
};
|
||||
|
||||
launch_watchman();
|
||||
|
||||
sub launch_watchman {
|
||||
my $o = watchman_query();
|
||||
if (is_work_tree_watched($o)) {
|
||||
output_result($o->{clock}, @{$o->{files}});
|
||||
}
|
||||
}
|
||||
|
||||
sub output_result {
|
||||
my ($clockid, @files) = @_;
|
||||
|
||||
# Uncomment for debugging watchman output
|
||||
# open (my $fh, ">", ".git/watchman-output.out");
|
||||
# binmode $fh, ":utf8";
|
||||
# print $fh "$clockid\n@files\n";
|
||||
# close $fh;
|
||||
|
||||
binmode STDOUT, ":utf8";
|
||||
print $clockid;
|
||||
print "\0";
|
||||
local $, = "\0";
|
||||
print @files;
|
||||
}
|
||||
|
||||
sub watchman_clock {
|
||||
my $response = qx/watchman clock "$git_work_tree"/;
|
||||
die "Failed to get clock id on '$git_work_tree'.\n" .
|
||||
"Falling back to scanning...\n" if $? != 0;
|
||||
|
||||
return $json_pkg->new->utf8->decode($response);
|
||||
}
|
||||
|
||||
sub watchman_query {
|
||||
my $pid = open2(\*CHLD_OUT, \*CHLD_IN, 'watchman -j --no-pretty')
|
||||
or die "open2() failed: $!\n" .
|
||||
"Falling back to scanning...\n";
|
||||
|
||||
# In the query expression below we're asking for names of files that
|
||||
# changed since $last_update_token but not from the .git folder.
|
||||
#
|
||||
# To accomplish this, we're using the "since" generator to use the
|
||||
# recency index to select candidate nodes and "fields" to limit the
|
||||
# output to file names only. Then we're using the "expression" term to
|
||||
# further constrain the results.
|
||||
my $last_update_line = "";
|
||||
if (substr($last_update_token, 0, 1) eq "c") {
|
||||
$last_update_token = "\"$last_update_token\"";
|
||||
$last_update_line = qq[\n"since": $last_update_token,];
|
||||
}
|
||||
my $query = <<" END";
|
||||
["query", "$git_work_tree", {$last_update_line
|
||||
"fields": ["name"],
|
||||
"expression": ["not", ["dirname", ".git"]]
|
||||
}]
|
||||
END
|
||||
|
||||
# Uncomment for debugging the watchman query
|
||||
# open (my $fh, ">", ".git/watchman-query.json");
|
||||
# print $fh $query;
|
||||
# close $fh;
|
||||
|
||||
print CHLD_IN $query;
|
||||
close CHLD_IN;
|
||||
my $response = do {local $/; <CHLD_OUT>};
|
||||
|
||||
# Uncomment for debugging the watch response
|
||||
# open ($fh, ">", ".git/watchman-response.json");
|
||||
# print $fh $response;
|
||||
# close $fh;
|
||||
|
||||
die "Watchman: command returned no output.\n" .
|
||||
"Falling back to scanning...\n" if $response eq "";
|
||||
die "Watchman: command returned invalid output: $response\n" .
|
||||
"Falling back to scanning...\n" unless $response =~ /^\{/;
|
||||
|
||||
return $json_pkg->new->utf8->decode($response);
|
||||
}
|
||||
|
||||
sub is_work_tree_watched {
|
||||
my ($output) = @_;
|
||||
my $error = $output->{error};
|
||||
if ($retry > 0 and $error and $error =~ m/unable to resolve root .* directory (.*) is not watched/) {
|
||||
$retry--;
|
||||
my $response = qx/watchman watch "$git_work_tree"/;
|
||||
die "Failed to make watchman watch '$git_work_tree'.\n" .
|
||||
"Falling back to scanning...\n" if $? != 0;
|
||||
$output = $json_pkg->new->utf8->decode($response);
|
||||
$error = $output->{error};
|
||||
die "Watchman: $error.\n" .
|
||||
"Falling back to scanning...\n" if $error;
|
||||
|
||||
# Uncomment for debugging watchman output
|
||||
# open (my $fh, ">", ".git/watchman-output.out");
|
||||
# close $fh;
|
||||
|
||||
# Watchman will always return all files on the first query so
|
||||
# return the fast "everything is dirty" flag to git and do the
|
||||
# Watchman query just to get it over with now so we won't pay
|
||||
# the cost in git to look up each individual file.
|
||||
my $o = watchman_clock();
|
||||
$error = $output->{error};
|
||||
|
||||
die "Watchman: $error.\n" .
|
||||
"Falling back to scanning...\n" if $error;
|
||||
|
||||
output_result($o->{clock}, ("/"));
|
||||
$last_update_token = $o->{clock};
|
||||
|
||||
eval { launch_watchman() };
|
||||
return 0;
|
||||
}
|
||||
|
||||
die "Watchman: $error.\n" .
|
||||
"Falling back to scanning...\n" if $error;
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
sub get_working_dir {
|
||||
my $working_dir;
|
||||
if ($^O =~ 'msys' || $^O =~ 'cygwin') {
|
||||
$working_dir = Win32::GetCwd();
|
||||
$working_dir =~ tr/\\/\//;
|
||||
} else {
|
||||
require Cwd;
|
||||
$working_dir = Cwd::cwd();
|
||||
}
|
||||
|
||||
return $working_dir;
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to prepare a packed repository for use over
|
||||
# dumb transports.
|
||||
#
|
||||
# To enable this hook, rename this file to "post-update".
|
||||
|
||||
exec git update-server-info
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to verify what is about to be committed
|
||||
# by applypatch from an e-mail message.
|
||||
#
|
||||
# The hook should exit with non-zero status after issuing an
|
||||
# appropriate message if it wants to stop the commit.
|
||||
#
|
||||
# To enable this hook, rename this file to "pre-applypatch".
|
||||
|
||||
. git-sh-setup
|
||||
precommit="$(git rev-parse --git-path hooks/pre-commit)"
|
||||
test -x "$precommit" && exec "$precommit" ${1+"$@"}
|
||||
:
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to verify what is about to be committed.
|
||||
# Called by "git commit" with no arguments. The hook should
|
||||
# exit with non-zero status after issuing an appropriate message if
|
||||
# it wants to stop the commit.
|
||||
#
|
||||
# To enable this hook, rename this file to "pre-commit".
|
||||
|
||||
if git rev-parse --verify HEAD >/dev/null 2>&1
|
||||
then
|
||||
against=HEAD
|
||||
else
|
||||
# Initial commit: diff against an empty tree object
|
||||
against=$(git hash-object -t tree /dev/null)
|
||||
fi
|
||||
|
||||
# If you want to allow non-ASCII filenames set this variable to true.
|
||||
allownonascii=$(git config --type=bool hooks.allownonascii)
|
||||
|
||||
# Redirect output to stderr.
|
||||
exec 1>&2
|
||||
|
||||
# Cross platform projects tend to avoid non-ASCII filenames; prevent
|
||||
# them from being added to the repository. We exploit the fact that the
|
||||
# printable range starts at the space character and ends with tilde.
|
||||
if [ "$allownonascii" != "true" ] &&
|
||||
# Note that the use of brackets around a tr range is ok here, (it's
|
||||
# even required, for portability to Solaris 10's /usr/bin/tr), since
|
||||
# the square bracket bytes happen to fall in the designated range.
|
||||
test $(git diff-index --cached --name-only --diff-filter=A -z $against |
|
||||
LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0
|
||||
then
|
||||
cat <<\EOF
|
||||
Error: Attempt to add a non-ASCII file name.
|
||||
|
||||
This can cause problems if you want to work with people on other platforms.
|
||||
|
||||
To be portable it is advisable to rename the file.
|
||||
|
||||
If you know what you are doing you can disable this check using:
|
||||
|
||||
git config hooks.allownonascii true
|
||||
EOF
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# If there are whitespace errors, print the offending file names and fail.
|
||||
exec git diff-index --check --cached $against --
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to verify what is about to be committed.
|
||||
# Called by "git merge" with no arguments. The hook should
|
||||
# exit with non-zero status after issuing an appropriate message to
|
||||
# stderr if it wants to stop the merge commit.
|
||||
#
|
||||
# To enable this hook, rename this file to "pre-merge-commit".
|
||||
|
||||
. git-sh-setup
|
||||
test -x "$GIT_DIR/hooks/pre-commit" &&
|
||||
exec "$GIT_DIR/hooks/pre-commit"
|
||||
:
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
#!/bin/sh
|
||||
|
||||
# An example hook script to verify what is about to be pushed. Called by "git
|
||||
# push" after it has checked the remote status, but before anything has been
|
||||
# pushed. If this script exits with a non-zero status nothing will be pushed.
|
||||
#
|
||||
# This hook is called with the following parameters:
|
||||
#
|
||||
# $1 -- Name of the remote to which the push is being done
|
||||
# $2 -- URL to which the push is being done
|
||||
#
|
||||
# If pushing without using a named remote those arguments will be equal.
|
||||
#
|
||||
# Information about the commits which are being pushed is supplied as lines to
|
||||
# the standard input in the form:
|
||||
#
|
||||
# <local ref> <local oid> <remote ref> <remote oid>
|
||||
#
|
||||
# This sample shows how to prevent push of commits where the log message starts
|
||||
# with "WIP" (work in progress).
|
||||
|
||||
remote="$1"
|
||||
url="$2"
|
||||
|
||||
zero=$(git hash-object --stdin </dev/null | tr '[0-9a-f]' '0')
|
||||
|
||||
while read local_ref local_oid remote_ref remote_oid
|
||||
do
|
||||
if test "$local_oid" = "$zero"
|
||||
then
|
||||
# Handle delete
|
||||
:
|
||||
else
|
||||
if test "$remote_oid" = "$zero"
|
||||
then
|
||||
# New branch, examine all commits
|
||||
range="$local_oid"
|
||||
else
|
||||
# Update to existing branch, examine new commits
|
||||
range="$remote_oid..$local_oid"
|
||||
fi
|
||||
|
||||
# Check for WIP commit
|
||||
commit=$(git rev-list -n 1 --grep '^WIP' "$range")
|
||||
if test -n "$commit"
|
||||
then
|
||||
echo >&2 "Found WIP commit in $local_ref, not pushing"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
exit 0
|
||||
|
|
@ -0,0 +1,169 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# Copyright (c) 2006, 2008 Junio C Hamano
|
||||
#
|
||||
# The "pre-rebase" hook is run just before "git rebase" starts doing
|
||||
# its job, and can prevent the command from running by exiting with
|
||||
# non-zero status.
|
||||
#
|
||||
# The hook is called with the following parameters:
|
||||
#
|
||||
# $1 -- the upstream the series was forked from.
|
||||
# $2 -- the branch being rebased (or empty when rebasing the current branch).
|
||||
#
|
||||
# This sample shows how to prevent topic branches that are already
|
||||
# merged to 'next' branch from getting rebased, because allowing it
|
||||
# would result in rebasing already published history.
|
||||
|
||||
publish=next
|
||||
basebranch="$1"
|
||||
if test "$#" = 2
|
||||
then
|
||||
topic="refs/heads/$2"
|
||||
else
|
||||
topic=`git symbolic-ref HEAD` ||
|
||||
exit 0 ;# we do not interrupt rebasing detached HEAD
|
||||
fi
|
||||
|
||||
case "$topic" in
|
||||
refs/heads/??/*)
|
||||
;;
|
||||
*)
|
||||
exit 0 ;# we do not interrupt others.
|
||||
;;
|
||||
esac
|
||||
|
||||
# Now we are dealing with a topic branch being rebased
|
||||
# on top of master. Is it OK to rebase it?
|
||||
|
||||
# Does the topic really exist?
|
||||
git show-ref -q "$topic" || {
|
||||
echo >&2 "No such branch $topic"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Is topic fully merged to master?
|
||||
not_in_master=`git rev-list --pretty=oneline ^master "$topic"`
|
||||
if test -z "$not_in_master"
|
||||
then
|
||||
echo >&2 "$topic is fully merged to master; better remove it."
|
||||
exit 1 ;# we could allow it, but there is no point.
|
||||
fi
|
||||
|
||||
# Is topic ever merged to next? If so you should not be rebasing it.
|
||||
only_next_1=`git rev-list ^master "^$topic" ${publish} | sort`
|
||||
only_next_2=`git rev-list ^master ${publish} | sort`
|
||||
if test "$only_next_1" = "$only_next_2"
|
||||
then
|
||||
not_in_topic=`git rev-list "^$topic" master`
|
||||
if test -z "$not_in_topic"
|
||||
then
|
||||
echo >&2 "$topic is already up to date with master"
|
||||
exit 1 ;# we could allow it, but there is no point.
|
||||
else
|
||||
exit 0
|
||||
fi
|
||||
else
|
||||
not_in_next=`git rev-list --pretty=oneline ^${publish} "$topic"`
|
||||
/usr/bin/perl -e '
|
||||
my $topic = $ARGV[0];
|
||||
my $msg = "* $topic has commits already merged to public branch:\n";
|
||||
my (%not_in_next) = map {
|
||||
/^([0-9a-f]+) /;
|
||||
($1 => 1);
|
||||
} split(/\n/, $ARGV[1]);
|
||||
for my $elem (map {
|
||||
/^([0-9a-f]+) (.*)$/;
|
||||
[$1 => $2];
|
||||
} split(/\n/, $ARGV[2])) {
|
||||
if (!exists $not_in_next{$elem->[0]}) {
|
||||
if ($msg) {
|
||||
print STDERR $msg;
|
||||
undef $msg;
|
||||
}
|
||||
print STDERR " $elem->[1]\n";
|
||||
}
|
||||
}
|
||||
' "$topic" "$not_in_next" "$not_in_master"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
<<\DOC_END
|
||||
|
||||
This sample hook safeguards topic branches that have been
|
||||
published from being rewound.
|
||||
|
||||
The workflow assumed here is:
|
||||
|
||||
* Once a topic branch forks from "master", "master" is never
|
||||
merged into it again (either directly or indirectly).
|
||||
|
||||
* Once a topic branch is fully cooked and merged into "master",
|
||||
it is deleted. If you need to build on top of it to correct
|
||||
earlier mistakes, a new topic branch is created by forking at
|
||||
the tip of the "master". This is not strictly necessary, but
|
||||
it makes it easier to keep your history simple.
|
||||
|
||||
* Whenever you need to test or publish your changes to topic
|
||||
branches, merge them into "next" branch.
|
||||
|
||||
The script, being an example, hardcodes the publish branch name
|
||||
to be "next", but it is trivial to make it configurable via
|
||||
$GIT_DIR/config mechanism.
|
||||
|
||||
With this workflow, you would want to know:
|
||||
|
||||
(1) ... if a topic branch has ever been merged to "next". Young
|
||||
topic branches can have stupid mistakes you would rather
|
||||
clean up before publishing, and things that have not been
|
||||
merged into other branches can be easily rebased without
|
||||
affecting other people. But once it is published, you would
|
||||
not want to rewind it.
|
||||
|
||||
(2) ... if a topic branch has been fully merged to "master".
|
||||
Then you can delete it. More importantly, you should not
|
||||
build on top of it -- other people may already want to
|
||||
change things related to the topic as patches against your
|
||||
"master", so if you need further changes, it is better to
|
||||
fork the topic (perhaps with the same name) afresh from the
|
||||
tip of "master".
|
||||
|
||||
Let's look at this example:
|
||||
|
||||
o---o---o---o---o---o---o---o---o---o "next"
|
||||
/ / / /
|
||||
/ a---a---b A / /
|
||||
/ / / /
|
||||
/ / c---c---c---c B /
|
||||
/ / / \ /
|
||||
/ / / b---b C \ /
|
||||
/ / / / \ /
|
||||
---o---o---o---o---o---o---o---o---o---o---o "master"
|
||||
|
||||
|
||||
A, B and C are topic branches.
|
||||
|
||||
* A has one fix since it was merged up to "next".
|
||||
|
||||
* B has finished. It has been fully merged up to "master" and "next",
|
||||
and is ready to be deleted.
|
||||
|
||||
* C has not merged to "next" at all.
|
||||
|
||||
We would want to allow C to be rebased, refuse A, and encourage
|
||||
B to be deleted.
|
||||
|
||||
To compute (1):
|
||||
|
||||
git rev-list ^master ^topic next
|
||||
git rev-list ^master next
|
||||
|
||||
if these match, topic has not merged in next at all.
|
||||
|
||||
To compute (2):
|
||||
|
||||
git rev-list master..topic
|
||||
|
||||
if this is empty, it is fully merged to "master".
|
||||
|
||||
DOC_END
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to make use of push options.
|
||||
# The example simply echoes all push options that start with 'echoback='
|
||||
# and rejects all pushes when the "reject" push option is used.
|
||||
#
|
||||
# To enable this hook, rename this file to "pre-receive".
|
||||
|
||||
if test -n "$GIT_PUSH_OPTION_COUNT"
|
||||
then
|
||||
i=0
|
||||
while test "$i" -lt "$GIT_PUSH_OPTION_COUNT"
|
||||
do
|
||||
eval "value=\$GIT_PUSH_OPTION_$i"
|
||||
case "$value" in
|
||||
echoback=*)
|
||||
echo "echo from the pre-receive-hook: ${value#*=}" >&2
|
||||
;;
|
||||
reject)
|
||||
exit 1
|
||||
esac
|
||||
i=$((i + 1))
|
||||
done
|
||||
fi
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to prepare the commit log message.
|
||||
# Called by "git commit" with the name of the file that has the
|
||||
# commit message, followed by the description of the commit
|
||||
# message's source. The hook's purpose is to edit the commit
|
||||
# message file. If the hook fails with a non-zero status,
|
||||
# the commit is aborted.
|
||||
#
|
||||
# To enable this hook, rename this file to "prepare-commit-msg".
|
||||
|
||||
# This hook includes three examples. The first one removes the
|
||||
# "# Please enter the commit message..." help message.
|
||||
#
|
||||
# The second includes the output of "git diff --name-status -r"
|
||||
# into the message, just before the "git status" output. It is
|
||||
# commented because it doesn't cope with --amend or with squashed
|
||||
# commits.
|
||||
#
|
||||
# The third example adds a Signed-off-by line to the message, that can
|
||||
# still be edited. This is rarely a good idea.
|
||||
|
||||
COMMIT_MSG_FILE=$1
|
||||
COMMIT_SOURCE=$2
|
||||
SHA1=$3
|
||||
|
||||
/usr/bin/perl -i.bak -ne 'print unless(m/^. Please enter the commit message/..m/^#$/)' "$COMMIT_MSG_FILE"
|
||||
|
||||
# case "$COMMIT_SOURCE,$SHA1" in
|
||||
# ,|template,)
|
||||
# /usr/bin/perl -i.bak -pe '
|
||||
# print "\n" . `git diff --cached --name-status -r`
|
||||
# if /^#/ && $first++ == 0' "$COMMIT_MSG_FILE" ;;
|
||||
# *) ;;
|
||||
# esac
|
||||
|
||||
# SOB=$(git var GIT_COMMITTER_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p')
|
||||
# git interpret-trailers --in-place --trailer "$SOB" "$COMMIT_MSG_FILE"
|
||||
# if test -z "$COMMIT_SOURCE"
|
||||
# then
|
||||
# /usr/bin/perl -i.bak -pe 'print "\n" if !$first_line++' "$COMMIT_MSG_FILE"
|
||||
# fi
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
#!/bin/sh
|
||||
|
||||
# An example hook script to update a checked-out tree on a git push.
|
||||
#
|
||||
# This hook is invoked by git-receive-pack(1) when it reacts to git
|
||||
# push and updates reference(s) in its repository, and when the push
|
||||
# tries to update the branch that is currently checked out and the
|
||||
# receive.denyCurrentBranch configuration variable is set to
|
||||
# updateInstead.
|
||||
#
|
||||
# By default, such a push is refused if the working tree and the index
|
||||
# of the remote repository has any difference from the currently
|
||||
# checked out commit; when both the working tree and the index match
|
||||
# the current commit, they are updated to match the newly pushed tip
|
||||
# of the branch. This hook is to be used to override the default
|
||||
# behaviour; however the code below reimplements the default behaviour
|
||||
# as a starting point for convenient modification.
|
||||
#
|
||||
# The hook receives the commit with which the tip of the current
|
||||
# branch is going to be updated:
|
||||
commit=$1
|
||||
|
||||
# It can exit with a non-zero status to refuse the push (when it does
|
||||
# so, it must not modify the index or the working tree).
|
||||
die () {
|
||||
echo >&2 "$*"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Or it can make any necessary changes to the working tree and to the
|
||||
# index to bring them to the desired state when the tip of the current
|
||||
# branch is updated to the new commit, and exit with a zero status.
|
||||
#
|
||||
# For example, the hook can simply run git read-tree -u -m HEAD "$1"
|
||||
# in order to emulate git fetch that is run in the reverse direction
|
||||
# with git push, as the two-tree form of git read-tree -u -m is
|
||||
# essentially the same as git switch or git checkout that switches
|
||||
# branches while keeping the local changes in the working tree that do
|
||||
# not interfere with the difference between the branches.
|
||||
|
||||
# The below is a more-or-less exact translation to shell of the C code
|
||||
# for the default behaviour for git's push-to-checkout hook defined in
|
||||
# the push_to_deploy() function in builtin/receive-pack.c.
|
||||
#
|
||||
# Note that the hook will be executed from the repository directory,
|
||||
# not from the working tree, so if you want to perform operations on
|
||||
# the working tree, you will have to adapt your code accordingly, e.g.
|
||||
# by adding "cd .." or using relative paths.
|
||||
|
||||
if ! git update-index -q --ignore-submodules --refresh
|
||||
then
|
||||
die "Up-to-date check failed"
|
||||
fi
|
||||
|
||||
if ! git diff-files --quiet --ignore-submodules --
|
||||
then
|
||||
die "Working directory has unstaged changes"
|
||||
fi
|
||||
|
||||
# This is a rough translation of:
|
||||
#
|
||||
# head_has_history() ? "HEAD" : EMPTY_TREE_SHA1_HEX
|
||||
if git cat-file -e HEAD 2>/dev/null
|
||||
then
|
||||
head=HEAD
|
||||
else
|
||||
head=$(git hash-object -t tree --stdin </dev/null)
|
||||
fi
|
||||
|
||||
if ! git diff-index --quiet --cached --ignore-submodules $head --
|
||||
then
|
||||
die "Working directory has staged changes"
|
||||
fi
|
||||
|
||||
if ! git read-tree -u -m "$commit"
|
||||
then
|
||||
die "Could not update working tree to new HEAD"
|
||||
fi
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
#!/bin/sh
|
||||
|
||||
# An example hook script to validate a patch (and/or patch series) before
|
||||
# sending it via email.
|
||||
#
|
||||
# The hook should exit with non-zero status after issuing an appropriate
|
||||
# message if it wants to prevent the email(s) from being sent.
|
||||
#
|
||||
# To enable this hook, rename this file to "sendemail-validate".
|
||||
#
|
||||
# By default, it will only check that the patch(es) can be applied on top of
|
||||
# the default upstream branch without conflicts in a secondary worktree. After
|
||||
# validation (successful or not) of the last patch of a series, the worktree
|
||||
# will be deleted.
|
||||
#
|
||||
# The following config variables can be set to change the default remote and
|
||||
# remote ref that are used to apply the patches against:
|
||||
#
|
||||
# sendemail.validateRemote (default: origin)
|
||||
# sendemail.validateRemoteRef (default: HEAD)
|
||||
#
|
||||
# Replace the TODO placeholders with appropriate checks according to your
|
||||
# needs.
|
||||
|
||||
validate_cover_letter () {
|
||||
file="$1"
|
||||
# TODO: Replace with appropriate checks (e.g. spell checking).
|
||||
true
|
||||
}
|
||||
|
||||
validate_patch () {
|
||||
file="$1"
|
||||
# Ensure that the patch applies without conflicts.
|
||||
git am -3 "$file" || return
|
||||
# TODO: Replace with appropriate checks for this patch
|
||||
# (e.g. checkpatch.pl).
|
||||
true
|
||||
}
|
||||
|
||||
validate_series () {
|
||||
# TODO: Replace with appropriate checks for the whole series
|
||||
# (e.g. quick build, coding style checks, etc.).
|
||||
true
|
||||
}
|
||||
|
||||
# main -------------------------------------------------------------------------
|
||||
|
||||
if test "$GIT_SENDEMAIL_FILE_COUNTER" = 1
|
||||
then
|
||||
remote=$(git config --default origin --get sendemail.validateRemote) &&
|
||||
ref=$(git config --default HEAD --get sendemail.validateRemoteRef) &&
|
||||
worktree=$(mktemp --tmpdir -d sendemail-validate.XXXXXXX) &&
|
||||
git worktree add -fd --checkout "$worktree" "refs/remotes/$remote/$ref" &&
|
||||
git config --replace-all sendemail.validateWorktree "$worktree"
|
||||
else
|
||||
worktree=$(git config --get sendemail.validateWorktree)
|
||||
fi || {
|
||||
echo "sendemail-validate: error: failed to prepare worktree" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
unset GIT_DIR GIT_WORK_TREE
|
||||
cd "$worktree" &&
|
||||
|
||||
if grep -q "^diff --git " "$1"
|
||||
then
|
||||
validate_patch "$1"
|
||||
else
|
||||
validate_cover_letter "$1"
|
||||
fi &&
|
||||
|
||||
if test "$GIT_SENDEMAIL_FILE_COUNTER" = "$GIT_SENDEMAIL_FILE_TOTAL"
|
||||
then
|
||||
git config --unset-all sendemail.validateWorktree &&
|
||||
trap 'git worktree remove -ff "$worktree"' EXIT &&
|
||||
validate_series
|
||||
fi
|
||||
|
|
@ -0,0 +1,128 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to block unannotated tags from entering.
|
||||
# Called by "git receive-pack" with arguments: refname sha1-old sha1-new
|
||||
#
|
||||
# To enable this hook, rename this file to "update".
|
||||
#
|
||||
# Config
|
||||
# ------
|
||||
# hooks.allowunannotated
|
||||
# This boolean sets whether unannotated tags will be allowed into the
|
||||
# repository. By default they won't be.
|
||||
# hooks.allowdeletetag
|
||||
# This boolean sets whether deleting tags will be allowed in the
|
||||
# repository. By default they won't be.
|
||||
# hooks.allowmodifytag
|
||||
# This boolean sets whether a tag may be modified after creation. By default
|
||||
# it won't be.
|
||||
# hooks.allowdeletebranch
|
||||
# This boolean sets whether deleting branches will be allowed in the
|
||||
# repository. By default they won't be.
|
||||
# hooks.denycreatebranch
|
||||
# This boolean sets whether remotely creating branches will be denied
|
||||
# in the repository. By default this is allowed.
|
||||
#
|
||||
|
||||
# --- Command line
|
||||
refname="$1"
|
||||
oldrev="$2"
|
||||
newrev="$3"
|
||||
|
||||
# --- Safety check
|
||||
if [ -z "$GIT_DIR" ]; then
|
||||
echo "Don't run this script from the command line." >&2
|
||||
echo " (if you want, you could supply GIT_DIR then run" >&2
|
||||
echo " $0 <ref> <oldrev> <newrev>)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then
|
||||
echo "usage: $0 <ref> <oldrev> <newrev>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- Config
|
||||
allowunannotated=$(git config --type=bool hooks.allowunannotated)
|
||||
allowdeletebranch=$(git config --type=bool hooks.allowdeletebranch)
|
||||
denycreatebranch=$(git config --type=bool hooks.denycreatebranch)
|
||||
allowdeletetag=$(git config --type=bool hooks.allowdeletetag)
|
||||
allowmodifytag=$(git config --type=bool hooks.allowmodifytag)
|
||||
|
||||
# check for no description
|
||||
projectdesc=$(sed -e '1q' "$GIT_DIR/description")
|
||||
case "$projectdesc" in
|
||||
"Unnamed repository"* | "")
|
||||
echo "*** Project description file hasn't been set" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# --- Check types
|
||||
# if $newrev is 0000...0000, it's a commit to delete a ref.
|
||||
zero=$(git hash-object --stdin </dev/null | tr '[0-9a-f]' '0')
|
||||
if [ "$newrev" = "$zero" ]; then
|
||||
newrev_type=delete
|
||||
else
|
||||
newrev_type=$(git cat-file -t $newrev)
|
||||
fi
|
||||
|
||||
case "$refname","$newrev_type" in
|
||||
refs/tags/*,commit)
|
||||
# un-annotated tag
|
||||
short_refname=${refname##refs/tags/}
|
||||
if [ "$allowunannotated" != "true" ]; then
|
||||
echo "*** The un-annotated tag, $short_refname, is not allowed in this repository" >&2
|
||||
echo "*** Use 'git tag [ -a | -s ]' for tags you want to propagate." >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
refs/tags/*,delete)
|
||||
# delete tag
|
||||
if [ "$allowdeletetag" != "true" ]; then
|
||||
echo "*** Deleting a tag is not allowed in this repository" >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
refs/tags/*,tag)
|
||||
# annotated tag
|
||||
if [ "$allowmodifytag" != "true" ] && git rev-parse $refname > /dev/null 2>&1
|
||||
then
|
||||
echo "*** Tag '$refname' already exists." >&2
|
||||
echo "*** Modifying a tag is not allowed in this repository." >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
refs/heads/*,commit)
|
||||
# branch
|
||||
if [ "$oldrev" = "$zero" -a "$denycreatebranch" = "true" ]; then
|
||||
echo "*** Creating a branch is not allowed in this repository" >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
refs/heads/*,delete)
|
||||
# delete branch
|
||||
if [ "$allowdeletebranch" != "true" ]; then
|
||||
echo "*** Deleting a branch is not allowed in this repository" >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
refs/remotes/*,commit)
|
||||
# tracking branch
|
||||
;;
|
||||
refs/remotes/*,delete)
|
||||
# delete tracking branch
|
||||
if [ "$allowdeletebranch" != "true" ]; then
|
||||
echo "*** Deleting a tracking branch is not allowed in this repository" >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
# Anything else (is there anything else?)
|
||||
echo "*** Update hook: unknown type of update to ref $refname of type $newrev_type" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# --- Finished
|
||||
exit 0
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
# git ls-files --others --exclude-from=.git/info/exclude
|
||||
# Lines that start with '#' are comments.
|
||||
# For a project mostly in C, the following would be a good set of
|
||||
# exclude patterns (uncomment them if you want to use them):
|
||||
# *.[oa]
|
||||
# *~
|
||||
|
|
@ -0,0 +1 @@
|
|||
ref: refs/heads/master
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
[core]
|
||||
repositoryformatversion = 0
|
||||
filemode = false
|
||||
bare = true
|
||||
symlinks = false
|
||||
ignorecase = true
|
||||
|
|
@ -0,0 +1 @@
|
|||
Unnamed repository; edit this file 'description' to name the repository.
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to check the commit log message taken by
|
||||
# applypatch from an e-mail message.
|
||||
#
|
||||
# The hook should exit with non-zero status after issuing an
|
||||
# appropriate message if it wants to stop the commit. The hook is
|
||||
# allowed to edit the commit message file.
|
||||
#
|
||||
# To enable this hook, rename this file to "applypatch-msg".
|
||||
|
||||
. git-sh-setup
|
||||
commitmsg="$(git rev-parse --git-path hooks/commit-msg)"
|
||||
test -x "$commitmsg" && exec "$commitmsg" ${1+"$@"}
|
||||
:
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to check the commit log message.
|
||||
# Called by "git commit" with one argument, the name of the file
|
||||
# that has the commit message. The hook should exit with non-zero
|
||||
# status after issuing an appropriate message if it wants to stop the
|
||||
# commit. The hook is allowed to edit the commit message file.
|
||||
#
|
||||
# To enable this hook, rename this file to "commit-msg".
|
||||
|
||||
# Uncomment the below to add a Signed-off-by line to the message.
|
||||
# Doing this in a hook is a bad idea in general, but the prepare-commit-msg
|
||||
# hook is more suited to it.
|
||||
#
|
||||
# SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p')
|
||||
# grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1"
|
||||
|
||||
# This example catches duplicate Signed-off-by lines.
|
||||
|
||||
test "" = "$(grep '^Signed-off-by: ' "$1" |
|
||||
sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" || {
|
||||
echo >&2 Duplicate Signed-off-by lines.
|
||||
exit 1
|
||||
}
|
||||
|
|
@ -0,0 +1,174 @@
|
|||
#!/usr/bin/perl
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
use IPC::Open2;
|
||||
|
||||
# An example hook script to integrate Watchman
|
||||
# (https://facebook.github.io/watchman/) with git to speed up detecting
|
||||
# new and modified files.
|
||||
#
|
||||
# The hook is passed a version (currently 2) and last update token
|
||||
# formatted as a string and outputs to stdout a new update token and
|
||||
# all files that have been modified since the update token. Paths must
|
||||
# be relative to the root of the working tree and separated by a single NUL.
|
||||
#
|
||||
# To enable this hook, rename this file to "query-watchman" and set
|
||||
# 'git config core.fsmonitor .git/hooks/query-watchman'
|
||||
#
|
||||
my ($version, $last_update_token) = @ARGV;
|
||||
|
||||
# Uncomment for debugging
|
||||
# print STDERR "$0 $version $last_update_token\n";
|
||||
|
||||
# Check the hook interface version
|
||||
if ($version ne 2) {
|
||||
die "Unsupported query-fsmonitor hook version '$version'.\n" .
|
||||
"Falling back to scanning...\n";
|
||||
}
|
||||
|
||||
my $git_work_tree = get_working_dir();
|
||||
|
||||
my $retry = 1;
|
||||
|
||||
my $json_pkg;
|
||||
eval {
|
||||
require JSON::XS;
|
||||
$json_pkg = "JSON::XS";
|
||||
1;
|
||||
} or do {
|
||||
require JSON::PP;
|
||||
$json_pkg = "JSON::PP";
|
||||
};
|
||||
|
||||
launch_watchman();
|
||||
|
||||
sub launch_watchman {
|
||||
my $o = watchman_query();
|
||||
if (is_work_tree_watched($o)) {
|
||||
output_result($o->{clock}, @{$o->{files}});
|
||||
}
|
||||
}
|
||||
|
||||
sub output_result {
|
||||
my ($clockid, @files) = @_;
|
||||
|
||||
# Uncomment for debugging watchman output
|
||||
# open (my $fh, ">", ".git/watchman-output.out");
|
||||
# binmode $fh, ":utf8";
|
||||
# print $fh "$clockid\n@files\n";
|
||||
# close $fh;
|
||||
|
||||
binmode STDOUT, ":utf8";
|
||||
print $clockid;
|
||||
print "\0";
|
||||
local $, = "\0";
|
||||
print @files;
|
||||
}
|
||||
|
||||
sub watchman_clock {
|
||||
my $response = qx/watchman clock "$git_work_tree"/;
|
||||
die "Failed to get clock id on '$git_work_tree'.\n" .
|
||||
"Falling back to scanning...\n" if $? != 0;
|
||||
|
||||
return $json_pkg->new->utf8->decode($response);
|
||||
}
|
||||
|
||||
sub watchman_query {
|
||||
my $pid = open2(\*CHLD_OUT, \*CHLD_IN, 'watchman -j --no-pretty')
|
||||
or die "open2() failed: $!\n" .
|
||||
"Falling back to scanning...\n";
|
||||
|
||||
# In the query expression below we're asking for names of files that
|
||||
# changed since $last_update_token but not from the .git folder.
|
||||
#
|
||||
# To accomplish this, we're using the "since" generator to use the
|
||||
# recency index to select candidate nodes and "fields" to limit the
|
||||
# output to file names only. Then we're using the "expression" term to
|
||||
# further constrain the results.
|
||||
my $last_update_line = "";
|
||||
if (substr($last_update_token, 0, 1) eq "c") {
|
||||
$last_update_token = "\"$last_update_token\"";
|
||||
$last_update_line = qq[\n"since": $last_update_token,];
|
||||
}
|
||||
my $query = <<" END";
|
||||
["query", "$git_work_tree", {$last_update_line
|
||||
"fields": ["name"],
|
||||
"expression": ["not", ["dirname", ".git"]]
|
||||
}]
|
||||
END
|
||||
|
||||
# Uncomment for debugging the watchman query
|
||||
# open (my $fh, ">", ".git/watchman-query.json");
|
||||
# print $fh $query;
|
||||
# close $fh;
|
||||
|
||||
print CHLD_IN $query;
|
||||
close CHLD_IN;
|
||||
my $response = do {local $/; <CHLD_OUT>};
|
||||
|
||||
# Uncomment for debugging the watch response
|
||||
# open ($fh, ">", ".git/watchman-response.json");
|
||||
# print $fh $response;
|
||||
# close $fh;
|
||||
|
||||
die "Watchman: command returned no output.\n" .
|
||||
"Falling back to scanning...\n" if $response eq "";
|
||||
die "Watchman: command returned invalid output: $response\n" .
|
||||
"Falling back to scanning...\n" unless $response =~ /^\{/;
|
||||
|
||||
return $json_pkg->new->utf8->decode($response);
|
||||
}
|
||||
|
||||
sub is_work_tree_watched {
|
||||
my ($output) = @_;
|
||||
my $error = $output->{error};
|
||||
if ($retry > 0 and $error and $error =~ m/unable to resolve root .* directory (.*) is not watched/) {
|
||||
$retry--;
|
||||
my $response = qx/watchman watch "$git_work_tree"/;
|
||||
die "Failed to make watchman watch '$git_work_tree'.\n" .
|
||||
"Falling back to scanning...\n" if $? != 0;
|
||||
$output = $json_pkg->new->utf8->decode($response);
|
||||
$error = $output->{error};
|
||||
die "Watchman: $error.\n" .
|
||||
"Falling back to scanning...\n" if $error;
|
||||
|
||||
# Uncomment for debugging watchman output
|
||||
# open (my $fh, ">", ".git/watchman-output.out");
|
||||
# close $fh;
|
||||
|
||||
# Watchman will always return all files on the first query so
|
||||
# return the fast "everything is dirty" flag to git and do the
|
||||
# Watchman query just to get it over with now so we won't pay
|
||||
# the cost in git to look up each individual file.
|
||||
my $o = watchman_clock();
|
||||
$error = $output->{error};
|
||||
|
||||
die "Watchman: $error.\n" .
|
||||
"Falling back to scanning...\n" if $error;
|
||||
|
||||
output_result($o->{clock}, ("/"));
|
||||
$last_update_token = $o->{clock};
|
||||
|
||||
eval { launch_watchman() };
|
||||
return 0;
|
||||
}
|
||||
|
||||
die "Watchman: $error.\n" .
|
||||
"Falling back to scanning...\n" if $error;
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
sub get_working_dir {
|
||||
my $working_dir;
|
||||
if ($^O =~ 'msys' || $^O =~ 'cygwin') {
|
||||
$working_dir = Win32::GetCwd();
|
||||
$working_dir =~ tr/\\/\//;
|
||||
} else {
|
||||
require Cwd;
|
||||
$working_dir = Cwd::cwd();
|
||||
}
|
||||
|
||||
return $working_dir;
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to prepare a packed repository for use over
|
||||
# dumb transports.
|
||||
#
|
||||
# To enable this hook, rename this file to "post-update".
|
||||
|
||||
exec git update-server-info
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to verify what is about to be committed
|
||||
# by applypatch from an e-mail message.
|
||||
#
|
||||
# The hook should exit with non-zero status after issuing an
|
||||
# appropriate message if it wants to stop the commit.
|
||||
#
|
||||
# To enable this hook, rename this file to "pre-applypatch".
|
||||
|
||||
. git-sh-setup
|
||||
precommit="$(git rev-parse --git-path hooks/pre-commit)"
|
||||
test -x "$precommit" && exec "$precommit" ${1+"$@"}
|
||||
:
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to verify what is about to be committed.
|
||||
# Called by "git commit" with no arguments. The hook should
|
||||
# exit with non-zero status after issuing an appropriate message if
|
||||
# it wants to stop the commit.
|
||||
#
|
||||
# To enable this hook, rename this file to "pre-commit".
|
||||
|
||||
if git rev-parse --verify HEAD >/dev/null 2>&1
|
||||
then
|
||||
against=HEAD
|
||||
else
|
||||
# Initial commit: diff against an empty tree object
|
||||
against=$(git hash-object -t tree /dev/null)
|
||||
fi
|
||||
|
||||
# If you want to allow non-ASCII filenames set this variable to true.
|
||||
allownonascii=$(git config --type=bool hooks.allownonascii)
|
||||
|
||||
# Redirect output to stderr.
|
||||
exec 1>&2
|
||||
|
||||
# Cross platform projects tend to avoid non-ASCII filenames; prevent
|
||||
# them from being added to the repository. We exploit the fact that the
|
||||
# printable range starts at the space character and ends with tilde.
|
||||
if [ "$allownonascii" != "true" ] &&
|
||||
# Note that the use of brackets around a tr range is ok here, (it's
|
||||
# even required, for portability to Solaris 10's /usr/bin/tr), since
|
||||
# the square bracket bytes happen to fall in the designated range.
|
||||
test $(git diff-index --cached --name-only --diff-filter=A -z $against |
|
||||
LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0
|
||||
then
|
||||
cat <<\EOF
|
||||
Error: Attempt to add a non-ASCII file name.
|
||||
|
||||
This can cause problems if you want to work with people on other platforms.
|
||||
|
||||
To be portable it is advisable to rename the file.
|
||||
|
||||
If you know what you are doing you can disable this check using:
|
||||
|
||||
git config hooks.allownonascii true
|
||||
EOF
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# If there are whitespace errors, print the offending file names and fail.
|
||||
exec git diff-index --check --cached $against --
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to verify what is about to be committed.
|
||||
# Called by "git merge" with no arguments. The hook should
|
||||
# exit with non-zero status after issuing an appropriate message to
|
||||
# stderr if it wants to stop the merge commit.
|
||||
#
|
||||
# To enable this hook, rename this file to "pre-merge-commit".
|
||||
|
||||
. git-sh-setup
|
||||
test -x "$GIT_DIR/hooks/pre-commit" &&
|
||||
exec "$GIT_DIR/hooks/pre-commit"
|
||||
:
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
#!/bin/sh
|
||||
|
||||
# An example hook script to verify what is about to be pushed. Called by "git
|
||||
# push" after it has checked the remote status, but before anything has been
|
||||
# pushed. If this script exits with a non-zero status nothing will be pushed.
|
||||
#
|
||||
# This hook is called with the following parameters:
|
||||
#
|
||||
# $1 -- Name of the remote to which the push is being done
|
||||
# $2 -- URL to which the push is being done
|
||||
#
|
||||
# If pushing without using a named remote those arguments will be equal.
|
||||
#
|
||||
# Information about the commits which are being pushed is supplied as lines to
|
||||
# the standard input in the form:
|
||||
#
|
||||
# <local ref> <local oid> <remote ref> <remote oid>
|
||||
#
|
||||
# This sample shows how to prevent push of commits where the log message starts
|
||||
# with "WIP" (work in progress).
|
||||
|
||||
remote="$1"
|
||||
url="$2"
|
||||
|
||||
zero=$(git hash-object --stdin </dev/null | tr '[0-9a-f]' '0')
|
||||
|
||||
while read local_ref local_oid remote_ref remote_oid
|
||||
do
|
||||
if test "$local_oid" = "$zero"
|
||||
then
|
||||
# Handle delete
|
||||
:
|
||||
else
|
||||
if test "$remote_oid" = "$zero"
|
||||
then
|
||||
# New branch, examine all commits
|
||||
range="$local_oid"
|
||||
else
|
||||
# Update to existing branch, examine new commits
|
||||
range="$remote_oid..$local_oid"
|
||||
fi
|
||||
|
||||
# Check for WIP commit
|
||||
commit=$(git rev-list -n 1 --grep '^WIP' "$range")
|
||||
if test -n "$commit"
|
||||
then
|
||||
echo >&2 "Found WIP commit in $local_ref, not pushing"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
exit 0
|
||||
|
|
@ -0,0 +1,169 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# Copyright (c) 2006, 2008 Junio C Hamano
|
||||
#
|
||||
# The "pre-rebase" hook is run just before "git rebase" starts doing
|
||||
# its job, and can prevent the command from running by exiting with
|
||||
# non-zero status.
|
||||
#
|
||||
# The hook is called with the following parameters:
|
||||
#
|
||||
# $1 -- the upstream the series was forked from.
|
||||
# $2 -- the branch being rebased (or empty when rebasing the current branch).
|
||||
#
|
||||
# This sample shows how to prevent topic branches that are already
|
||||
# merged to 'next' branch from getting rebased, because allowing it
|
||||
# would result in rebasing already published history.
|
||||
|
||||
publish=next
|
||||
basebranch="$1"
|
||||
if test "$#" = 2
|
||||
then
|
||||
topic="refs/heads/$2"
|
||||
else
|
||||
topic=`git symbolic-ref HEAD` ||
|
||||
exit 0 ;# we do not interrupt rebasing detached HEAD
|
||||
fi
|
||||
|
||||
case "$topic" in
|
||||
refs/heads/??/*)
|
||||
;;
|
||||
*)
|
||||
exit 0 ;# we do not interrupt others.
|
||||
;;
|
||||
esac
|
||||
|
||||
# Now we are dealing with a topic branch being rebased
|
||||
# on top of master. Is it OK to rebase it?
|
||||
|
||||
# Does the topic really exist?
|
||||
git show-ref -q "$topic" || {
|
||||
echo >&2 "No such branch $topic"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Is topic fully merged to master?
|
||||
not_in_master=`git rev-list --pretty=oneline ^master "$topic"`
|
||||
if test -z "$not_in_master"
|
||||
then
|
||||
echo >&2 "$topic is fully merged to master; better remove it."
|
||||
exit 1 ;# we could allow it, but there is no point.
|
||||
fi
|
||||
|
||||
# Is topic ever merged to next? If so you should not be rebasing it.
|
||||
only_next_1=`git rev-list ^master "^$topic" ${publish} | sort`
|
||||
only_next_2=`git rev-list ^master ${publish} | sort`
|
||||
if test "$only_next_1" = "$only_next_2"
|
||||
then
|
||||
not_in_topic=`git rev-list "^$topic" master`
|
||||
if test -z "$not_in_topic"
|
||||
then
|
||||
echo >&2 "$topic is already up to date with master"
|
||||
exit 1 ;# we could allow it, but there is no point.
|
||||
else
|
||||
exit 0
|
||||
fi
|
||||
else
|
||||
not_in_next=`git rev-list --pretty=oneline ^${publish} "$topic"`
|
||||
/usr/bin/perl -e '
|
||||
my $topic = $ARGV[0];
|
||||
my $msg = "* $topic has commits already merged to public branch:\n";
|
||||
my (%not_in_next) = map {
|
||||
/^([0-9a-f]+) /;
|
||||
($1 => 1);
|
||||
} split(/\n/, $ARGV[1]);
|
||||
for my $elem (map {
|
||||
/^([0-9a-f]+) (.*)$/;
|
||||
[$1 => $2];
|
||||
} split(/\n/, $ARGV[2])) {
|
||||
if (!exists $not_in_next{$elem->[0]}) {
|
||||
if ($msg) {
|
||||
print STDERR $msg;
|
||||
undef $msg;
|
||||
}
|
||||
print STDERR " $elem->[1]\n";
|
||||
}
|
||||
}
|
||||
' "$topic" "$not_in_next" "$not_in_master"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
<<\DOC_END
|
||||
|
||||
This sample hook safeguards topic branches that have been
|
||||
published from being rewound.
|
||||
|
||||
The workflow assumed here is:
|
||||
|
||||
* Once a topic branch forks from "master", "master" is never
|
||||
merged into it again (either directly or indirectly).
|
||||
|
||||
* Once a topic branch is fully cooked and merged into "master",
|
||||
it is deleted. If you need to build on top of it to correct
|
||||
earlier mistakes, a new topic branch is created by forking at
|
||||
the tip of the "master". This is not strictly necessary, but
|
||||
it makes it easier to keep your history simple.
|
||||
|
||||
* Whenever you need to test or publish your changes to topic
|
||||
branches, merge them into "next" branch.
|
||||
|
||||
The script, being an example, hardcodes the publish branch name
|
||||
to be "next", but it is trivial to make it configurable via
|
||||
$GIT_DIR/config mechanism.
|
||||
|
||||
With this workflow, you would want to know:
|
||||
|
||||
(1) ... if a topic branch has ever been merged to "next". Young
|
||||
topic branches can have stupid mistakes you would rather
|
||||
clean up before publishing, and things that have not been
|
||||
merged into other branches can be easily rebased without
|
||||
affecting other people. But once it is published, you would
|
||||
not want to rewind it.
|
||||
|
||||
(2) ... if a topic branch has been fully merged to "master".
|
||||
Then you can delete it. More importantly, you should not
|
||||
build on top of it -- other people may already want to
|
||||
change things related to the topic as patches against your
|
||||
"master", so if you need further changes, it is better to
|
||||
fork the topic (perhaps with the same name) afresh from the
|
||||
tip of "master".
|
||||
|
||||
Let's look at this example:
|
||||
|
||||
o---o---o---o---o---o---o---o---o---o "next"
|
||||
/ / / /
|
||||
/ a---a---b A / /
|
||||
/ / / /
|
||||
/ / c---c---c---c B /
|
||||
/ / / \ /
|
||||
/ / / b---b C \ /
|
||||
/ / / / \ /
|
||||
---o---o---o---o---o---o---o---o---o---o---o "master"
|
||||
|
||||
|
||||
A, B and C are topic branches.
|
||||
|
||||
* A has one fix since it was merged up to "next".
|
||||
|
||||
* B has finished. It has been fully merged up to "master" and "next",
|
||||
and is ready to be deleted.
|
||||
|
||||
* C has not merged to "next" at all.
|
||||
|
||||
We would want to allow C to be rebased, refuse A, and encourage
|
||||
B to be deleted.
|
||||
|
||||
To compute (1):
|
||||
|
||||
git rev-list ^master ^topic next
|
||||
git rev-list ^master next
|
||||
|
||||
if these match, topic has not merged in next at all.
|
||||
|
||||
To compute (2):
|
||||
|
||||
git rev-list master..topic
|
||||
|
||||
if this is empty, it is fully merged to "master".
|
||||
|
||||
DOC_END
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to make use of push options.
|
||||
# The example simply echoes all push options that start with 'echoback='
|
||||
# and rejects all pushes when the "reject" push option is used.
|
||||
#
|
||||
# To enable this hook, rename this file to "pre-receive".
|
||||
|
||||
if test -n "$GIT_PUSH_OPTION_COUNT"
|
||||
then
|
||||
i=0
|
||||
while test "$i" -lt "$GIT_PUSH_OPTION_COUNT"
|
||||
do
|
||||
eval "value=\$GIT_PUSH_OPTION_$i"
|
||||
case "$value" in
|
||||
echoback=*)
|
||||
echo "echo from the pre-receive-hook: ${value#*=}" >&2
|
||||
;;
|
||||
reject)
|
||||
exit 1
|
||||
esac
|
||||
i=$((i + 1))
|
||||
done
|
||||
fi
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to prepare the commit log message.
|
||||
# Called by "git commit" with the name of the file that has the
|
||||
# commit message, followed by the description of the commit
|
||||
# message's source. The hook's purpose is to edit the commit
|
||||
# message file. If the hook fails with a non-zero status,
|
||||
# the commit is aborted.
|
||||
#
|
||||
# To enable this hook, rename this file to "prepare-commit-msg".
|
||||
|
||||
# This hook includes three examples. The first one removes the
|
||||
# "# Please enter the commit message..." help message.
|
||||
#
|
||||
# The second includes the output of "git diff --name-status -r"
|
||||
# into the message, just before the "git status" output. It is
|
||||
# commented because it doesn't cope with --amend or with squashed
|
||||
# commits.
|
||||
#
|
||||
# The third example adds a Signed-off-by line to the message, that can
|
||||
# still be edited. This is rarely a good idea.
|
||||
|
||||
COMMIT_MSG_FILE=$1
|
||||
COMMIT_SOURCE=$2
|
||||
SHA1=$3
|
||||
|
||||
/usr/bin/perl -i.bak -ne 'print unless(m/^. Please enter the commit message/..m/^#$/)' "$COMMIT_MSG_FILE"
|
||||
|
||||
# case "$COMMIT_SOURCE,$SHA1" in
|
||||
# ,|template,)
|
||||
# /usr/bin/perl -i.bak -pe '
|
||||
# print "\n" . `git diff --cached --name-status -r`
|
||||
# if /^#/ && $first++ == 0' "$COMMIT_MSG_FILE" ;;
|
||||
# *) ;;
|
||||
# esac
|
||||
|
||||
# SOB=$(git var GIT_COMMITTER_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p')
|
||||
# git interpret-trailers --in-place --trailer "$SOB" "$COMMIT_MSG_FILE"
|
||||
# if test -z "$COMMIT_SOURCE"
|
||||
# then
|
||||
# /usr/bin/perl -i.bak -pe 'print "\n" if !$first_line++' "$COMMIT_MSG_FILE"
|
||||
# fi
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
#!/bin/sh
|
||||
|
||||
# An example hook script to update a checked-out tree on a git push.
|
||||
#
|
||||
# This hook is invoked by git-receive-pack(1) when it reacts to git
|
||||
# push and updates reference(s) in its repository, and when the push
|
||||
# tries to update the branch that is currently checked out and the
|
||||
# receive.denyCurrentBranch configuration variable is set to
|
||||
# updateInstead.
|
||||
#
|
||||
# By default, such a push is refused if the working tree and the index
|
||||
# of the remote repository has any difference from the currently
|
||||
# checked out commit; when both the working tree and the index match
|
||||
# the current commit, they are updated to match the newly pushed tip
|
||||
# of the branch. This hook is to be used to override the default
|
||||
# behaviour; however the code below reimplements the default behaviour
|
||||
# as a starting point for convenient modification.
|
||||
#
|
||||
# The hook receives the commit with which the tip of the current
|
||||
# branch is going to be updated:
|
||||
commit=$1
|
||||
|
||||
# It can exit with a non-zero status to refuse the push (when it does
|
||||
# so, it must not modify the index or the working tree).
|
||||
die () {
|
||||
echo >&2 "$*"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Or it can make any necessary changes to the working tree and to the
|
||||
# index to bring them to the desired state when the tip of the current
|
||||
# branch is updated to the new commit, and exit with a zero status.
|
||||
#
|
||||
# For example, the hook can simply run git read-tree -u -m HEAD "$1"
|
||||
# in order to emulate git fetch that is run in the reverse direction
|
||||
# with git push, as the two-tree form of git read-tree -u -m is
|
||||
# essentially the same as git switch or git checkout that switches
|
||||
# branches while keeping the local changes in the working tree that do
|
||||
# not interfere with the difference between the branches.
|
||||
|
||||
# The below is a more-or-less exact translation to shell of the C code
|
||||
# for the default behaviour for git's push-to-checkout hook defined in
|
||||
# the push_to_deploy() function in builtin/receive-pack.c.
|
||||
#
|
||||
# Note that the hook will be executed from the repository directory,
|
||||
# not from the working tree, so if you want to perform operations on
|
||||
# the working tree, you will have to adapt your code accordingly, e.g.
|
||||
# by adding "cd .." or using relative paths.
|
||||
|
||||
if ! git update-index -q --ignore-submodules --refresh
|
||||
then
|
||||
die "Up-to-date check failed"
|
||||
fi
|
||||
|
||||
if ! git diff-files --quiet --ignore-submodules --
|
||||
then
|
||||
die "Working directory has unstaged changes"
|
||||
fi
|
||||
|
||||
# This is a rough translation of:
|
||||
#
|
||||
# head_has_history() ? "HEAD" : EMPTY_TREE_SHA1_HEX
|
||||
if git cat-file -e HEAD 2>/dev/null
|
||||
then
|
||||
head=HEAD
|
||||
else
|
||||
head=$(git hash-object -t tree --stdin </dev/null)
|
||||
fi
|
||||
|
||||
if ! git diff-index --quiet --cached --ignore-submodules $head --
|
||||
then
|
||||
die "Working directory has staged changes"
|
||||
fi
|
||||
|
||||
if ! git read-tree -u -m "$commit"
|
||||
then
|
||||
die "Could not update working tree to new HEAD"
|
||||
fi
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
#!/bin/sh
|
||||
|
||||
# An example hook script to validate a patch (and/or patch series) before
|
||||
# sending it via email.
|
||||
#
|
||||
# The hook should exit with non-zero status after issuing an appropriate
|
||||
# message if it wants to prevent the email(s) from being sent.
|
||||
#
|
||||
# To enable this hook, rename this file to "sendemail-validate".
|
||||
#
|
||||
# By default, it will only check that the patch(es) can be applied on top of
|
||||
# the default upstream branch without conflicts in a secondary worktree. After
|
||||
# validation (successful or not) of the last patch of a series, the worktree
|
||||
# will be deleted.
|
||||
#
|
||||
# The following config variables can be set to change the default remote and
|
||||
# remote ref that are used to apply the patches against:
|
||||
#
|
||||
# sendemail.validateRemote (default: origin)
|
||||
# sendemail.validateRemoteRef (default: HEAD)
|
||||
#
|
||||
# Replace the TODO placeholders with appropriate checks according to your
|
||||
# needs.
|
||||
|
||||
validate_cover_letter () {
|
||||
file="$1"
|
||||
# TODO: Replace with appropriate checks (e.g. spell checking).
|
||||
true
|
||||
}
|
||||
|
||||
validate_patch () {
|
||||
file="$1"
|
||||
# Ensure that the patch applies without conflicts.
|
||||
git am -3 "$file" || return
|
||||
# TODO: Replace with appropriate checks for this patch
|
||||
# (e.g. checkpatch.pl).
|
||||
true
|
||||
}
|
||||
|
||||
validate_series () {
|
||||
# TODO: Replace with appropriate checks for the whole series
|
||||
# (e.g. quick build, coding style checks, etc.).
|
||||
true
|
||||
}
|
||||
|
||||
# main -------------------------------------------------------------------------
|
||||
|
||||
if test "$GIT_SENDEMAIL_FILE_COUNTER" = 1
|
||||
then
|
||||
remote=$(git config --default origin --get sendemail.validateRemote) &&
|
||||
ref=$(git config --default HEAD --get sendemail.validateRemoteRef) &&
|
||||
worktree=$(mktemp --tmpdir -d sendemail-validate.XXXXXXX) &&
|
||||
git worktree add -fd --checkout "$worktree" "refs/remotes/$remote/$ref" &&
|
||||
git config --replace-all sendemail.validateWorktree "$worktree"
|
||||
else
|
||||
worktree=$(git config --get sendemail.validateWorktree)
|
||||
fi || {
|
||||
echo "sendemail-validate: error: failed to prepare worktree" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
unset GIT_DIR GIT_WORK_TREE
|
||||
cd "$worktree" &&
|
||||
|
||||
if grep -q "^diff --git " "$1"
|
||||
then
|
||||
validate_patch "$1"
|
||||
else
|
||||
validate_cover_letter "$1"
|
||||
fi &&
|
||||
|
||||
if test "$GIT_SENDEMAIL_FILE_COUNTER" = "$GIT_SENDEMAIL_FILE_TOTAL"
|
||||
then
|
||||
git config --unset-all sendemail.validateWorktree &&
|
||||
trap 'git worktree remove -ff "$worktree"' EXIT &&
|
||||
validate_series
|
||||
fi
|
||||
|
|
@ -0,0 +1,128 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to block unannotated tags from entering.
|
||||
# Called by "git receive-pack" with arguments: refname sha1-old sha1-new
|
||||
#
|
||||
# To enable this hook, rename this file to "update".
|
||||
#
|
||||
# Config
|
||||
# ------
|
||||
# hooks.allowunannotated
|
||||
# This boolean sets whether unannotated tags will be allowed into the
|
||||
# repository. By default they won't be.
|
||||
# hooks.allowdeletetag
|
||||
# This boolean sets whether deleting tags will be allowed in the
|
||||
# repository. By default they won't be.
|
||||
# hooks.allowmodifytag
|
||||
# This boolean sets whether a tag may be modified after creation. By default
|
||||
# it won't be.
|
||||
# hooks.allowdeletebranch
|
||||
# This boolean sets whether deleting branches will be allowed in the
|
||||
# repository. By default they won't be.
|
||||
# hooks.denycreatebranch
|
||||
# This boolean sets whether remotely creating branches will be denied
|
||||
# in the repository. By default this is allowed.
|
||||
#
|
||||
|
||||
# --- Command line
|
||||
refname="$1"
|
||||
oldrev="$2"
|
||||
newrev="$3"
|
||||
|
||||
# --- Safety check
|
||||
if [ -z "$GIT_DIR" ]; then
|
||||
echo "Don't run this script from the command line." >&2
|
||||
echo " (if you want, you could supply GIT_DIR then run" >&2
|
||||
echo " $0 <ref> <oldrev> <newrev>)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then
|
||||
echo "usage: $0 <ref> <oldrev> <newrev>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- Config
|
||||
allowunannotated=$(git config --type=bool hooks.allowunannotated)
|
||||
allowdeletebranch=$(git config --type=bool hooks.allowdeletebranch)
|
||||
denycreatebranch=$(git config --type=bool hooks.denycreatebranch)
|
||||
allowdeletetag=$(git config --type=bool hooks.allowdeletetag)
|
||||
allowmodifytag=$(git config --type=bool hooks.allowmodifytag)
|
||||
|
||||
# check for no description
|
||||
projectdesc=$(sed -e '1q' "$GIT_DIR/description")
|
||||
case "$projectdesc" in
|
||||
"Unnamed repository"* | "")
|
||||
echo "*** Project description file hasn't been set" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# --- Check types
|
||||
# if $newrev is 0000...0000, it's a commit to delete a ref.
|
||||
zero=$(git hash-object --stdin </dev/null | tr '[0-9a-f]' '0')
|
||||
if [ "$newrev" = "$zero" ]; then
|
||||
newrev_type=delete
|
||||
else
|
||||
newrev_type=$(git cat-file -t $newrev)
|
||||
fi
|
||||
|
||||
case "$refname","$newrev_type" in
|
||||
refs/tags/*,commit)
|
||||
# un-annotated tag
|
||||
short_refname=${refname##refs/tags/}
|
||||
if [ "$allowunannotated" != "true" ]; then
|
||||
echo "*** The un-annotated tag, $short_refname, is not allowed in this repository" >&2
|
||||
echo "*** Use 'git tag [ -a | -s ]' for tags you want to propagate." >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
refs/tags/*,delete)
|
||||
# delete tag
|
||||
if [ "$allowdeletetag" != "true" ]; then
|
||||
echo "*** Deleting a tag is not allowed in this repository" >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
refs/tags/*,tag)
|
||||
# annotated tag
|
||||
if [ "$allowmodifytag" != "true" ] && git rev-parse $refname > /dev/null 2>&1
|
||||
then
|
||||
echo "*** Tag '$refname' already exists." >&2
|
||||
echo "*** Modifying a tag is not allowed in this repository." >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
refs/heads/*,commit)
|
||||
# branch
|
||||
if [ "$oldrev" = "$zero" -a "$denycreatebranch" = "true" ]; then
|
||||
echo "*** Creating a branch is not allowed in this repository" >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
refs/heads/*,delete)
|
||||
# delete branch
|
||||
if [ "$allowdeletebranch" != "true" ]; then
|
||||
echo "*** Deleting a branch is not allowed in this repository" >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
refs/remotes/*,commit)
|
||||
# tracking branch
|
||||
;;
|
||||
refs/remotes/*,delete)
|
||||
# delete tracking branch
|
||||
if [ "$allowdeletebranch" != "true" ]; then
|
||||
echo "*** Deleting a tracking branch is not allowed in this repository" >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
# Anything else (is there anything else?)
|
||||
echo "*** Update hook: unknown type of update to ref $refname of type $newrev_type" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# --- Finished
|
||||
exit 0
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
# git ls-files --others --exclude-from=.git/info/exclude
|
||||
# Lines that start with '#' are comments.
|
||||
# For a project mostly in C, the following would be a good set of
|
||||
# exclude patterns (uncomment them if you want to use them):
|
||||
# *.[oa]
|
||||
# *~
|
||||
|
|
@ -0,0 +1 @@
|
|||
ref: refs/heads/master
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
[core]
|
||||
repositoryformatversion = 0
|
||||
filemode = false
|
||||
bare = true
|
||||
symlinks = false
|
||||
ignorecase = true
|
||||
|
|
@ -0,0 +1 @@
|
|||
Unnamed repository; edit this file 'description' to name the repository.
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to check the commit log message taken by
|
||||
# applypatch from an e-mail message.
|
||||
#
|
||||
# The hook should exit with non-zero status after issuing an
|
||||
# appropriate message if it wants to stop the commit. The hook is
|
||||
# allowed to edit the commit message file.
|
||||
#
|
||||
# To enable this hook, rename this file to "applypatch-msg".
|
||||
|
||||
. git-sh-setup
|
||||
commitmsg="$(git rev-parse --git-path hooks/commit-msg)"
|
||||
test -x "$commitmsg" && exec "$commitmsg" ${1+"$@"}
|
||||
:
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to check the commit log message.
|
||||
# Called by "git commit" with one argument, the name of the file
|
||||
# that has the commit message. The hook should exit with non-zero
|
||||
# status after issuing an appropriate message if it wants to stop the
|
||||
# commit. The hook is allowed to edit the commit message file.
|
||||
#
|
||||
# To enable this hook, rename this file to "commit-msg".
|
||||
|
||||
# Uncomment the below to add a Signed-off-by line to the message.
|
||||
# Doing this in a hook is a bad idea in general, but the prepare-commit-msg
|
||||
# hook is more suited to it.
|
||||
#
|
||||
# SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p')
|
||||
# grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1"
|
||||
|
||||
# This example catches duplicate Signed-off-by lines.
|
||||
|
||||
test "" = "$(grep '^Signed-off-by: ' "$1" |
|
||||
sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" || {
|
||||
echo >&2 Duplicate Signed-off-by lines.
|
||||
exit 1
|
||||
}
|
||||
|
|
@ -0,0 +1,174 @@
|
|||
#!/usr/bin/perl
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
use IPC::Open2;
|
||||
|
||||
# An example hook script to integrate Watchman
|
||||
# (https://facebook.github.io/watchman/) with git to speed up detecting
|
||||
# new and modified files.
|
||||
#
|
||||
# The hook is passed a version (currently 2) and last update token
|
||||
# formatted as a string and outputs to stdout a new update token and
|
||||
# all files that have been modified since the update token. Paths must
|
||||
# be relative to the root of the working tree and separated by a single NUL.
|
||||
#
|
||||
# To enable this hook, rename this file to "query-watchman" and set
|
||||
# 'git config core.fsmonitor .git/hooks/query-watchman'
|
||||
#
|
||||
my ($version, $last_update_token) = @ARGV;
|
||||
|
||||
# Uncomment for debugging
|
||||
# print STDERR "$0 $version $last_update_token\n";
|
||||
|
||||
# Check the hook interface version
|
||||
if ($version ne 2) {
|
||||
die "Unsupported query-fsmonitor hook version '$version'.\n" .
|
||||
"Falling back to scanning...\n";
|
||||
}
|
||||
|
||||
my $git_work_tree = get_working_dir();
|
||||
|
||||
my $retry = 1;
|
||||
|
||||
my $json_pkg;
|
||||
eval {
|
||||
require JSON::XS;
|
||||
$json_pkg = "JSON::XS";
|
||||
1;
|
||||
} or do {
|
||||
require JSON::PP;
|
||||
$json_pkg = "JSON::PP";
|
||||
};
|
||||
|
||||
launch_watchman();
|
||||
|
||||
sub launch_watchman {
|
||||
my $o = watchman_query();
|
||||
if (is_work_tree_watched($o)) {
|
||||
output_result($o->{clock}, @{$o->{files}});
|
||||
}
|
||||
}
|
||||
|
||||
sub output_result {
|
||||
my ($clockid, @files) = @_;
|
||||
|
||||
# Uncomment for debugging watchman output
|
||||
# open (my $fh, ">", ".git/watchman-output.out");
|
||||
# binmode $fh, ":utf8";
|
||||
# print $fh "$clockid\n@files\n";
|
||||
# close $fh;
|
||||
|
||||
binmode STDOUT, ":utf8";
|
||||
print $clockid;
|
||||
print "\0";
|
||||
local $, = "\0";
|
||||
print @files;
|
||||
}
|
||||
|
||||
sub watchman_clock {
|
||||
my $response = qx/watchman clock "$git_work_tree"/;
|
||||
die "Failed to get clock id on '$git_work_tree'.\n" .
|
||||
"Falling back to scanning...\n" if $? != 0;
|
||||
|
||||
return $json_pkg->new->utf8->decode($response);
|
||||
}
|
||||
|
||||
sub watchman_query {
|
||||
my $pid = open2(\*CHLD_OUT, \*CHLD_IN, 'watchman -j --no-pretty')
|
||||
or die "open2() failed: $!\n" .
|
||||
"Falling back to scanning...\n";
|
||||
|
||||
# In the query expression below we're asking for names of files that
|
||||
# changed since $last_update_token but not from the .git folder.
|
||||
#
|
||||
# To accomplish this, we're using the "since" generator to use the
|
||||
# recency index to select candidate nodes and "fields" to limit the
|
||||
# output to file names only. Then we're using the "expression" term to
|
||||
# further constrain the results.
|
||||
my $last_update_line = "";
|
||||
if (substr($last_update_token, 0, 1) eq "c") {
|
||||
$last_update_token = "\"$last_update_token\"";
|
||||
$last_update_line = qq[\n"since": $last_update_token,];
|
||||
}
|
||||
my $query = <<" END";
|
||||
["query", "$git_work_tree", {$last_update_line
|
||||
"fields": ["name"],
|
||||
"expression": ["not", ["dirname", ".git"]]
|
||||
}]
|
||||
END
|
||||
|
||||
# Uncomment for debugging the watchman query
|
||||
# open (my $fh, ">", ".git/watchman-query.json");
|
||||
# print $fh $query;
|
||||
# close $fh;
|
||||
|
||||
print CHLD_IN $query;
|
||||
close CHLD_IN;
|
||||
my $response = do {local $/; <CHLD_OUT>};
|
||||
|
||||
# Uncomment for debugging the watch response
|
||||
# open ($fh, ">", ".git/watchman-response.json");
|
||||
# print $fh $response;
|
||||
# close $fh;
|
||||
|
||||
die "Watchman: command returned no output.\n" .
|
||||
"Falling back to scanning...\n" if $response eq "";
|
||||
die "Watchman: command returned invalid output: $response\n" .
|
||||
"Falling back to scanning...\n" unless $response =~ /^\{/;
|
||||
|
||||
return $json_pkg->new->utf8->decode($response);
|
||||
}
|
||||
|
||||
sub is_work_tree_watched {
|
||||
my ($output) = @_;
|
||||
my $error = $output->{error};
|
||||
if ($retry > 0 and $error and $error =~ m/unable to resolve root .* directory (.*) is not watched/) {
|
||||
$retry--;
|
||||
my $response = qx/watchman watch "$git_work_tree"/;
|
||||
die "Failed to make watchman watch '$git_work_tree'.\n" .
|
||||
"Falling back to scanning...\n" if $? != 0;
|
||||
$output = $json_pkg->new->utf8->decode($response);
|
||||
$error = $output->{error};
|
||||
die "Watchman: $error.\n" .
|
||||
"Falling back to scanning...\n" if $error;
|
||||
|
||||
# Uncomment for debugging watchman output
|
||||
# open (my $fh, ">", ".git/watchman-output.out");
|
||||
# close $fh;
|
||||
|
||||
# Watchman will always return all files on the first query so
|
||||
# return the fast "everything is dirty" flag to git and do the
|
||||
# Watchman query just to get it over with now so we won't pay
|
||||
# the cost in git to look up each individual file.
|
||||
my $o = watchman_clock();
|
||||
$error = $output->{error};
|
||||
|
||||
die "Watchman: $error.\n" .
|
||||
"Falling back to scanning...\n" if $error;
|
||||
|
||||
output_result($o->{clock}, ("/"));
|
||||
$last_update_token = $o->{clock};
|
||||
|
||||
eval { launch_watchman() };
|
||||
return 0;
|
||||
}
|
||||
|
||||
die "Watchman: $error.\n" .
|
||||
"Falling back to scanning...\n" if $error;
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
sub get_working_dir {
|
||||
my $working_dir;
|
||||
if ($^O =~ 'msys' || $^O =~ 'cygwin') {
|
||||
$working_dir = Win32::GetCwd();
|
||||
$working_dir =~ tr/\\/\//;
|
||||
} else {
|
||||
require Cwd;
|
||||
$working_dir = Cwd::cwd();
|
||||
}
|
||||
|
||||
return $working_dir;
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to prepare a packed repository for use over
|
||||
# dumb transports.
|
||||
#
|
||||
# To enable this hook, rename this file to "post-update".
|
||||
|
||||
exec git update-server-info
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to verify what is about to be committed
|
||||
# by applypatch from an e-mail message.
|
||||
#
|
||||
# The hook should exit with non-zero status after issuing an
|
||||
# appropriate message if it wants to stop the commit.
|
||||
#
|
||||
# To enable this hook, rename this file to "pre-applypatch".
|
||||
|
||||
. git-sh-setup
|
||||
precommit="$(git rev-parse --git-path hooks/pre-commit)"
|
||||
test -x "$precommit" && exec "$precommit" ${1+"$@"}
|
||||
:
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to verify what is about to be committed.
|
||||
# Called by "git commit" with no arguments. The hook should
|
||||
# exit with non-zero status after issuing an appropriate message if
|
||||
# it wants to stop the commit.
|
||||
#
|
||||
# To enable this hook, rename this file to "pre-commit".
|
||||
|
||||
if git rev-parse --verify HEAD >/dev/null 2>&1
|
||||
then
|
||||
against=HEAD
|
||||
else
|
||||
# Initial commit: diff against an empty tree object
|
||||
against=$(git hash-object -t tree /dev/null)
|
||||
fi
|
||||
|
||||
# If you want to allow non-ASCII filenames set this variable to true.
|
||||
allownonascii=$(git config --type=bool hooks.allownonascii)
|
||||
|
||||
# Redirect output to stderr.
|
||||
exec 1>&2
|
||||
|
||||
# Cross platform projects tend to avoid non-ASCII filenames; prevent
|
||||
# them from being added to the repository. We exploit the fact that the
|
||||
# printable range starts at the space character and ends with tilde.
|
||||
if [ "$allownonascii" != "true" ] &&
|
||||
# Note that the use of brackets around a tr range is ok here, (it's
|
||||
# even required, for portability to Solaris 10's /usr/bin/tr), since
|
||||
# the square bracket bytes happen to fall in the designated range.
|
||||
test $(git diff-index --cached --name-only --diff-filter=A -z $against |
|
||||
LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0
|
||||
then
|
||||
cat <<\EOF
|
||||
Error: Attempt to add a non-ASCII file name.
|
||||
|
||||
This can cause problems if you want to work with people on other platforms.
|
||||
|
||||
To be portable it is advisable to rename the file.
|
||||
|
||||
If you know what you are doing you can disable this check using:
|
||||
|
||||
git config hooks.allownonascii true
|
||||
EOF
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# If there are whitespace errors, print the offending file names and fail.
|
||||
exec git diff-index --check --cached $against --
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to verify what is about to be committed.
|
||||
# Called by "git merge" with no arguments. The hook should
|
||||
# exit with non-zero status after issuing an appropriate message to
|
||||
# stderr if it wants to stop the merge commit.
|
||||
#
|
||||
# To enable this hook, rename this file to "pre-merge-commit".
|
||||
|
||||
. git-sh-setup
|
||||
test -x "$GIT_DIR/hooks/pre-commit" &&
|
||||
exec "$GIT_DIR/hooks/pre-commit"
|
||||
:
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
#!/bin/sh
|
||||
|
||||
# An example hook script to verify what is about to be pushed. Called by "git
|
||||
# push" after it has checked the remote status, but before anything has been
|
||||
# pushed. If this script exits with a non-zero status nothing will be pushed.
|
||||
#
|
||||
# This hook is called with the following parameters:
|
||||
#
|
||||
# $1 -- Name of the remote to which the push is being done
|
||||
# $2 -- URL to which the push is being done
|
||||
#
|
||||
# If pushing without using a named remote those arguments will be equal.
|
||||
#
|
||||
# Information about the commits which are being pushed is supplied as lines to
|
||||
# the standard input in the form:
|
||||
#
|
||||
# <local ref> <local oid> <remote ref> <remote oid>
|
||||
#
|
||||
# This sample shows how to prevent push of commits where the log message starts
|
||||
# with "WIP" (work in progress).
|
||||
|
||||
remote="$1"
|
||||
url="$2"
|
||||
|
||||
zero=$(git hash-object --stdin </dev/null | tr '[0-9a-f]' '0')
|
||||
|
||||
while read local_ref local_oid remote_ref remote_oid
|
||||
do
|
||||
if test "$local_oid" = "$zero"
|
||||
then
|
||||
# Handle delete
|
||||
:
|
||||
else
|
||||
if test "$remote_oid" = "$zero"
|
||||
then
|
||||
# New branch, examine all commits
|
||||
range="$local_oid"
|
||||
else
|
||||
# Update to existing branch, examine new commits
|
||||
range="$remote_oid..$local_oid"
|
||||
fi
|
||||
|
||||
# Check for WIP commit
|
||||
commit=$(git rev-list -n 1 --grep '^WIP' "$range")
|
||||
if test -n "$commit"
|
||||
then
|
||||
echo >&2 "Found WIP commit in $local_ref, not pushing"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
exit 0
|
||||
|
|
@ -0,0 +1,169 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# Copyright (c) 2006, 2008 Junio C Hamano
|
||||
#
|
||||
# The "pre-rebase" hook is run just before "git rebase" starts doing
|
||||
# its job, and can prevent the command from running by exiting with
|
||||
# non-zero status.
|
||||
#
|
||||
# The hook is called with the following parameters:
|
||||
#
|
||||
# $1 -- the upstream the series was forked from.
|
||||
# $2 -- the branch being rebased (or empty when rebasing the current branch).
|
||||
#
|
||||
# This sample shows how to prevent topic branches that are already
|
||||
# merged to 'next' branch from getting rebased, because allowing it
|
||||
# would result in rebasing already published history.
|
||||
|
||||
publish=next
|
||||
basebranch="$1"
|
||||
if test "$#" = 2
|
||||
then
|
||||
topic="refs/heads/$2"
|
||||
else
|
||||
topic=`git symbolic-ref HEAD` ||
|
||||
exit 0 ;# we do not interrupt rebasing detached HEAD
|
||||
fi
|
||||
|
||||
case "$topic" in
|
||||
refs/heads/??/*)
|
||||
;;
|
||||
*)
|
||||
exit 0 ;# we do not interrupt others.
|
||||
;;
|
||||
esac
|
||||
|
||||
# Now we are dealing with a topic branch being rebased
|
||||
# on top of master. Is it OK to rebase it?
|
||||
|
||||
# Does the topic really exist?
|
||||
git show-ref -q "$topic" || {
|
||||
echo >&2 "No such branch $topic"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Is topic fully merged to master?
|
||||
not_in_master=`git rev-list --pretty=oneline ^master "$topic"`
|
||||
if test -z "$not_in_master"
|
||||
then
|
||||
echo >&2 "$topic is fully merged to master; better remove it."
|
||||
exit 1 ;# we could allow it, but there is no point.
|
||||
fi
|
||||
|
||||
# Is topic ever merged to next? If so you should not be rebasing it.
|
||||
only_next_1=`git rev-list ^master "^$topic" ${publish} | sort`
|
||||
only_next_2=`git rev-list ^master ${publish} | sort`
|
||||
if test "$only_next_1" = "$only_next_2"
|
||||
then
|
||||
not_in_topic=`git rev-list "^$topic" master`
|
||||
if test -z "$not_in_topic"
|
||||
then
|
||||
echo >&2 "$topic is already up to date with master"
|
||||
exit 1 ;# we could allow it, but there is no point.
|
||||
else
|
||||
exit 0
|
||||
fi
|
||||
else
|
||||
not_in_next=`git rev-list --pretty=oneline ^${publish} "$topic"`
|
||||
/usr/bin/perl -e '
|
||||
my $topic = $ARGV[0];
|
||||
my $msg = "* $topic has commits already merged to public branch:\n";
|
||||
my (%not_in_next) = map {
|
||||
/^([0-9a-f]+) /;
|
||||
($1 => 1);
|
||||
} split(/\n/, $ARGV[1]);
|
||||
for my $elem (map {
|
||||
/^([0-9a-f]+) (.*)$/;
|
||||
[$1 => $2];
|
||||
} split(/\n/, $ARGV[2])) {
|
||||
if (!exists $not_in_next{$elem->[0]}) {
|
||||
if ($msg) {
|
||||
print STDERR $msg;
|
||||
undef $msg;
|
||||
}
|
||||
print STDERR " $elem->[1]\n";
|
||||
}
|
||||
}
|
||||
' "$topic" "$not_in_next" "$not_in_master"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
<<\DOC_END
|
||||
|
||||
This sample hook safeguards topic branches that have been
|
||||
published from being rewound.
|
||||
|
||||
The workflow assumed here is:
|
||||
|
||||
* Once a topic branch forks from "master", "master" is never
|
||||
merged into it again (either directly or indirectly).
|
||||
|
||||
* Once a topic branch is fully cooked and merged into "master",
|
||||
it is deleted. If you need to build on top of it to correct
|
||||
earlier mistakes, a new topic branch is created by forking at
|
||||
the tip of the "master". This is not strictly necessary, but
|
||||
it makes it easier to keep your history simple.
|
||||
|
||||
* Whenever you need to test or publish your changes to topic
|
||||
branches, merge them into "next" branch.
|
||||
|
||||
The script, being an example, hardcodes the publish branch name
|
||||
to be "next", but it is trivial to make it configurable via
|
||||
$GIT_DIR/config mechanism.
|
||||
|
||||
With this workflow, you would want to know:
|
||||
|
||||
(1) ... if a topic branch has ever been merged to "next". Young
|
||||
topic branches can have stupid mistakes you would rather
|
||||
clean up before publishing, and things that have not been
|
||||
merged into other branches can be easily rebased without
|
||||
affecting other people. But once it is published, you would
|
||||
not want to rewind it.
|
||||
|
||||
(2) ... if a topic branch has been fully merged to "master".
|
||||
Then you can delete it. More importantly, you should not
|
||||
build on top of it -- other people may already want to
|
||||
change things related to the topic as patches against your
|
||||
"master", so if you need further changes, it is better to
|
||||
fork the topic (perhaps with the same name) afresh from the
|
||||
tip of "master".
|
||||
|
||||
Let's look at this example:
|
||||
|
||||
o---o---o---o---o---o---o---o---o---o "next"
|
||||
/ / / /
|
||||
/ a---a---b A / /
|
||||
/ / / /
|
||||
/ / c---c---c---c B /
|
||||
/ / / \ /
|
||||
/ / / b---b C \ /
|
||||
/ / / / \ /
|
||||
---o---o---o---o---o---o---o---o---o---o---o "master"
|
||||
|
||||
|
||||
A, B and C are topic branches.
|
||||
|
||||
* A has one fix since it was merged up to "next".
|
||||
|
||||
* B has finished. It has been fully merged up to "master" and "next",
|
||||
and is ready to be deleted.
|
||||
|
||||
* C has not merged to "next" at all.
|
||||
|
||||
We would want to allow C to be rebased, refuse A, and encourage
|
||||
B to be deleted.
|
||||
|
||||
To compute (1):
|
||||
|
||||
git rev-list ^master ^topic next
|
||||
git rev-list ^master next
|
||||
|
||||
if these match, topic has not merged in next at all.
|
||||
|
||||
To compute (2):
|
||||
|
||||
git rev-list master..topic
|
||||
|
||||
if this is empty, it is fully merged to "master".
|
||||
|
||||
DOC_END
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to make use of push options.
|
||||
# The example simply echoes all push options that start with 'echoback='
|
||||
# and rejects all pushes when the "reject" push option is used.
|
||||
#
|
||||
# To enable this hook, rename this file to "pre-receive".
|
||||
|
||||
if test -n "$GIT_PUSH_OPTION_COUNT"
|
||||
then
|
||||
i=0
|
||||
while test "$i" -lt "$GIT_PUSH_OPTION_COUNT"
|
||||
do
|
||||
eval "value=\$GIT_PUSH_OPTION_$i"
|
||||
case "$value" in
|
||||
echoback=*)
|
||||
echo "echo from the pre-receive-hook: ${value#*=}" >&2
|
||||
;;
|
||||
reject)
|
||||
exit 1
|
||||
esac
|
||||
i=$((i + 1))
|
||||
done
|
||||
fi
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to prepare the commit log message.
|
||||
# Called by "git commit" with the name of the file that has the
|
||||
# commit message, followed by the description of the commit
|
||||
# message's source. The hook's purpose is to edit the commit
|
||||
# message file. If the hook fails with a non-zero status,
|
||||
# the commit is aborted.
|
||||
#
|
||||
# To enable this hook, rename this file to "prepare-commit-msg".
|
||||
|
||||
# This hook includes three examples. The first one removes the
|
||||
# "# Please enter the commit message..." help message.
|
||||
#
|
||||
# The second includes the output of "git diff --name-status -r"
|
||||
# into the message, just before the "git status" output. It is
|
||||
# commented because it doesn't cope with --amend or with squashed
|
||||
# commits.
|
||||
#
|
||||
# The third example adds a Signed-off-by line to the message, that can
|
||||
# still be edited. This is rarely a good idea.
|
||||
|
||||
COMMIT_MSG_FILE=$1
|
||||
COMMIT_SOURCE=$2
|
||||
SHA1=$3
|
||||
|
||||
/usr/bin/perl -i.bak -ne 'print unless(m/^. Please enter the commit message/..m/^#$/)' "$COMMIT_MSG_FILE"
|
||||
|
||||
# case "$COMMIT_SOURCE,$SHA1" in
|
||||
# ,|template,)
|
||||
# /usr/bin/perl -i.bak -pe '
|
||||
# print "\n" . `git diff --cached --name-status -r`
|
||||
# if /^#/ && $first++ == 0' "$COMMIT_MSG_FILE" ;;
|
||||
# *) ;;
|
||||
# esac
|
||||
|
||||
# SOB=$(git var GIT_COMMITTER_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p')
|
||||
# git interpret-trailers --in-place --trailer "$SOB" "$COMMIT_MSG_FILE"
|
||||
# if test -z "$COMMIT_SOURCE"
|
||||
# then
|
||||
# /usr/bin/perl -i.bak -pe 'print "\n" if !$first_line++' "$COMMIT_MSG_FILE"
|
||||
# fi
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
#!/bin/sh
|
||||
|
||||
# An example hook script to update a checked-out tree on a git push.
|
||||
#
|
||||
# This hook is invoked by git-receive-pack(1) when it reacts to git
|
||||
# push and updates reference(s) in its repository, and when the push
|
||||
# tries to update the branch that is currently checked out and the
|
||||
# receive.denyCurrentBranch configuration variable is set to
|
||||
# updateInstead.
|
||||
#
|
||||
# By default, such a push is refused if the working tree and the index
|
||||
# of the remote repository has any difference from the currently
|
||||
# checked out commit; when both the working tree and the index match
|
||||
# the current commit, they are updated to match the newly pushed tip
|
||||
# of the branch. This hook is to be used to override the default
|
||||
# behaviour; however the code below reimplements the default behaviour
|
||||
# as a starting point for convenient modification.
|
||||
#
|
||||
# The hook receives the commit with which the tip of the current
|
||||
# branch is going to be updated:
|
||||
commit=$1
|
||||
|
||||
# It can exit with a non-zero status to refuse the push (when it does
|
||||
# so, it must not modify the index or the working tree).
|
||||
die () {
|
||||
echo >&2 "$*"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Or it can make any necessary changes to the working tree and to the
|
||||
# index to bring them to the desired state when the tip of the current
|
||||
# branch is updated to the new commit, and exit with a zero status.
|
||||
#
|
||||
# For example, the hook can simply run git read-tree -u -m HEAD "$1"
|
||||
# in order to emulate git fetch that is run in the reverse direction
|
||||
# with git push, as the two-tree form of git read-tree -u -m is
|
||||
# essentially the same as git switch or git checkout that switches
|
||||
# branches while keeping the local changes in the working tree that do
|
||||
# not interfere with the difference between the branches.
|
||||
|
||||
# The below is a more-or-less exact translation to shell of the C code
|
||||
# for the default behaviour for git's push-to-checkout hook defined in
|
||||
# the push_to_deploy() function in builtin/receive-pack.c.
|
||||
#
|
||||
# Note that the hook will be executed from the repository directory,
|
||||
# not from the working tree, so if you want to perform operations on
|
||||
# the working tree, you will have to adapt your code accordingly, e.g.
|
||||
# by adding "cd .." or using relative paths.
|
||||
|
||||
if ! git update-index -q --ignore-submodules --refresh
|
||||
then
|
||||
die "Up-to-date check failed"
|
||||
fi
|
||||
|
||||
if ! git diff-files --quiet --ignore-submodules --
|
||||
then
|
||||
die "Working directory has unstaged changes"
|
||||
fi
|
||||
|
||||
# This is a rough translation of:
|
||||
#
|
||||
# head_has_history() ? "HEAD" : EMPTY_TREE_SHA1_HEX
|
||||
if git cat-file -e HEAD 2>/dev/null
|
||||
then
|
||||
head=HEAD
|
||||
else
|
||||
head=$(git hash-object -t tree --stdin </dev/null)
|
||||
fi
|
||||
|
||||
if ! git diff-index --quiet --cached --ignore-submodules $head --
|
||||
then
|
||||
die "Working directory has staged changes"
|
||||
fi
|
||||
|
||||
if ! git read-tree -u -m "$commit"
|
||||
then
|
||||
die "Could not update working tree to new HEAD"
|
||||
fi
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
#!/bin/sh
|
||||
|
||||
# An example hook script to validate a patch (and/or patch series) before
|
||||
# sending it via email.
|
||||
#
|
||||
# The hook should exit with non-zero status after issuing an appropriate
|
||||
# message if it wants to prevent the email(s) from being sent.
|
||||
#
|
||||
# To enable this hook, rename this file to "sendemail-validate".
|
||||
#
|
||||
# By default, it will only check that the patch(es) can be applied on top of
|
||||
# the default upstream branch without conflicts in a secondary worktree. After
|
||||
# validation (successful or not) of the last patch of a series, the worktree
|
||||
# will be deleted.
|
||||
#
|
||||
# The following config variables can be set to change the default remote and
|
||||
# remote ref that are used to apply the patches against:
|
||||
#
|
||||
# sendemail.validateRemote (default: origin)
|
||||
# sendemail.validateRemoteRef (default: HEAD)
|
||||
#
|
||||
# Replace the TODO placeholders with appropriate checks according to your
|
||||
# needs.
|
||||
|
||||
validate_cover_letter () {
|
||||
file="$1"
|
||||
# TODO: Replace with appropriate checks (e.g. spell checking).
|
||||
true
|
||||
}
|
||||
|
||||
validate_patch () {
|
||||
file="$1"
|
||||
# Ensure that the patch applies without conflicts.
|
||||
git am -3 "$file" || return
|
||||
# TODO: Replace with appropriate checks for this patch
|
||||
# (e.g. checkpatch.pl).
|
||||
true
|
||||
}
|
||||
|
||||
validate_series () {
|
||||
# TODO: Replace with appropriate checks for the whole series
|
||||
# (e.g. quick build, coding style checks, etc.).
|
||||
true
|
||||
}
|
||||
|
||||
# main -------------------------------------------------------------------------
|
||||
|
||||
if test "$GIT_SENDEMAIL_FILE_COUNTER" = 1
|
||||
then
|
||||
remote=$(git config --default origin --get sendemail.validateRemote) &&
|
||||
ref=$(git config --default HEAD --get sendemail.validateRemoteRef) &&
|
||||
worktree=$(mktemp --tmpdir -d sendemail-validate.XXXXXXX) &&
|
||||
git worktree add -fd --checkout "$worktree" "refs/remotes/$remote/$ref" &&
|
||||
git config --replace-all sendemail.validateWorktree "$worktree"
|
||||
else
|
||||
worktree=$(git config --get sendemail.validateWorktree)
|
||||
fi || {
|
||||
echo "sendemail-validate: error: failed to prepare worktree" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
unset GIT_DIR GIT_WORK_TREE
|
||||
cd "$worktree" &&
|
||||
|
||||
if grep -q "^diff --git " "$1"
|
||||
then
|
||||
validate_patch "$1"
|
||||
else
|
||||
validate_cover_letter "$1"
|
||||
fi &&
|
||||
|
||||
if test "$GIT_SENDEMAIL_FILE_COUNTER" = "$GIT_SENDEMAIL_FILE_TOTAL"
|
||||
then
|
||||
git config --unset-all sendemail.validateWorktree &&
|
||||
trap 'git worktree remove -ff "$worktree"' EXIT &&
|
||||
validate_series
|
||||
fi
|
||||
|
|
@ -0,0 +1,128 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to block unannotated tags from entering.
|
||||
# Called by "git receive-pack" with arguments: refname sha1-old sha1-new
|
||||
#
|
||||
# To enable this hook, rename this file to "update".
|
||||
#
|
||||
# Config
|
||||
# ------
|
||||
# hooks.allowunannotated
|
||||
# This boolean sets whether unannotated tags will be allowed into the
|
||||
# repository. By default they won't be.
|
||||
# hooks.allowdeletetag
|
||||
# This boolean sets whether deleting tags will be allowed in the
|
||||
# repository. By default they won't be.
|
||||
# hooks.allowmodifytag
|
||||
# This boolean sets whether a tag may be modified after creation. By default
|
||||
# it won't be.
|
||||
# hooks.allowdeletebranch
|
||||
# This boolean sets whether deleting branches will be allowed in the
|
||||
# repository. By default they won't be.
|
||||
# hooks.denycreatebranch
|
||||
# This boolean sets whether remotely creating branches will be denied
|
||||
# in the repository. By default this is allowed.
|
||||
#
|
||||
|
||||
# --- Command line
|
||||
refname="$1"
|
||||
oldrev="$2"
|
||||
newrev="$3"
|
||||
|
||||
# --- Safety check
|
||||
if [ -z "$GIT_DIR" ]; then
|
||||
echo "Don't run this script from the command line." >&2
|
||||
echo " (if you want, you could supply GIT_DIR then run" >&2
|
||||
echo " $0 <ref> <oldrev> <newrev>)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then
|
||||
echo "usage: $0 <ref> <oldrev> <newrev>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- Config
|
||||
allowunannotated=$(git config --type=bool hooks.allowunannotated)
|
||||
allowdeletebranch=$(git config --type=bool hooks.allowdeletebranch)
|
||||
denycreatebranch=$(git config --type=bool hooks.denycreatebranch)
|
||||
allowdeletetag=$(git config --type=bool hooks.allowdeletetag)
|
||||
allowmodifytag=$(git config --type=bool hooks.allowmodifytag)
|
||||
|
||||
# check for no description
|
||||
projectdesc=$(sed -e '1q' "$GIT_DIR/description")
|
||||
case "$projectdesc" in
|
||||
"Unnamed repository"* | "")
|
||||
echo "*** Project description file hasn't been set" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# --- Check types
|
||||
# if $newrev is 0000...0000, it's a commit to delete a ref.
|
||||
zero=$(git hash-object --stdin </dev/null | tr '[0-9a-f]' '0')
|
||||
if [ "$newrev" = "$zero" ]; then
|
||||
newrev_type=delete
|
||||
else
|
||||
newrev_type=$(git cat-file -t $newrev)
|
||||
fi
|
||||
|
||||
case "$refname","$newrev_type" in
|
||||
refs/tags/*,commit)
|
||||
# un-annotated tag
|
||||
short_refname=${refname##refs/tags/}
|
||||
if [ "$allowunannotated" != "true" ]; then
|
||||
echo "*** The un-annotated tag, $short_refname, is not allowed in this repository" >&2
|
||||
echo "*** Use 'git tag [ -a | -s ]' for tags you want to propagate." >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
refs/tags/*,delete)
|
||||
# delete tag
|
||||
if [ "$allowdeletetag" != "true" ]; then
|
||||
echo "*** Deleting a tag is not allowed in this repository" >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
refs/tags/*,tag)
|
||||
# annotated tag
|
||||
if [ "$allowmodifytag" != "true" ] && git rev-parse $refname > /dev/null 2>&1
|
||||
then
|
||||
echo "*** Tag '$refname' already exists." >&2
|
||||
echo "*** Modifying a tag is not allowed in this repository." >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
refs/heads/*,commit)
|
||||
# branch
|
||||
if [ "$oldrev" = "$zero" -a "$denycreatebranch" = "true" ]; then
|
||||
echo "*** Creating a branch is not allowed in this repository" >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
refs/heads/*,delete)
|
||||
# delete branch
|
||||
if [ "$allowdeletebranch" != "true" ]; then
|
||||
echo "*** Deleting a branch is not allowed in this repository" >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
refs/remotes/*,commit)
|
||||
# tracking branch
|
||||
;;
|
||||
refs/remotes/*,delete)
|
||||
# delete tracking branch
|
||||
if [ "$allowdeletebranch" != "true" ]; then
|
||||
echo "*** Deleting a tracking branch is not allowed in this repository" >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
# Anything else (is there anything else?)
|
||||
echo "*** Update hook: unknown type of update to ref $refname of type $newrev_type" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# --- Finished
|
||||
exit 0
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
# git ls-files --others --exclude-from=.git/info/exclude
|
||||
# Lines that start with '#' are comments.
|
||||
# For a project mostly in C, the following would be a good set of
|
||||
# exclude patterns (uncomment them if you want to use them):
|
||||
# *.[oa]
|
||||
# *~
|
||||
Loading…
Reference in New Issue