Compare commits

..

7 Commits

Author SHA1 Message Date
ccccyyytt 02d830aeba feature/git-config-dev3 2026-02-28 11:23:48 +08:00
ccccyyytt c644faaeb3 cyt_dev3 2026-02-27 22:36:39 +08:00
linlin 31c3f4bc08 合并上游master分支,解决.gitignore文件冲突 2026-02-23 16:56:21 +08:00
linlin 6c14f69dac 修改部署:在windows上部署;readme表述;pip加速 2026-02-02 22:59:42 +08:00
zhangxunhui 34481b92fb 更新soffice.tar.gz文件 2026-01-28 16:48:51 +08:00
Nigel d8b49609bb Update README.md 2026-01-27 18:27:36 +08:00
zhangxunhui 84614e708d V1.0.0.0版本 2026-01-27 18:25:45 +08:00
93 changed files with 5362 additions and 306 deletions

3
.cursorindexingignore Normal file
View File

@ -0,0 +1,3 @@
# Don't index SpecStory auto-save files, but allow explicit context inclusion via @ references
.specstory/**

View File

@ -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
# ============================================

4
.specstory/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
# SpecStory project identity file
/.project.json
# SpecStory explanation file
/.what-is-this.md

View File

@ -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/

View File

@ -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

View File

@ -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
View File

@ -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")

View File

@ -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

View File

@ -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

BIN
install/V1.0.0.0.zip Normal file

Binary file not shown.

BIN
install/soffice.tar.gz Normal file

Binary file not shown.

54
main.py
View File

@ -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__":

View File

@ -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'):

View File

@ -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}},

11
run_server.py Normal file
View File

@ -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"
)

View File

@ -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;">

View File

@ -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 = `

View File

@ -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}")

View File

@ -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())

834
sync/git_sync.py Normal file
View File

@ -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

View File

@ -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()

View File

@ -0,0 +1 @@
ref: refs/heads/master

View File

@ -0,0 +1,6 @@
[core]
repositoryformatversion = 0
filemode = false
bare = true
symlinks = false
ignorecase = true

View File

@ -0,0 +1 @@
Unnamed repository; edit this file 'description' to name the repository.

View File

@ -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+"$@"}
:

View File

@ -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
}

View File

@ -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;
}

View File

@ -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

View File

@ -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+"$@"}
:

View File

@ -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 --

View File

@ -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"
:

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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]
# *~

View File

@ -0,0 +1 @@
ref: refs/heads/master

View File

@ -0,0 +1,6 @@
[core]
repositoryformatversion = 0
filemode = false
bare = true
symlinks = false
ignorecase = true

View File

@ -0,0 +1 @@
Unnamed repository; edit this file 'description' to name the repository.

View File

@ -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+"$@"}
:

View File

@ -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
}

View File

@ -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;
}

View File

@ -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

View File

@ -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+"$@"}
:

View File

@ -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 --

View File

@ -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"
:

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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]
# *~

View File

@ -0,0 +1 @@
ref: refs/heads/master

View File

@ -0,0 +1,6 @@
[core]
repositoryformatversion = 0
filemode = false
bare = true
symlinks = false
ignorecase = true

View File

@ -0,0 +1 @@
Unnamed repository; edit this file 'description' to name the repository.

View File

@ -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+"$@"}
:

View File

@ -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
}

View File

@ -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;
}

View File

@ -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

View File

@ -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+"$@"}
:

View File

@ -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 --

View File

@ -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"
:

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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]
# *~

View File

@ -0,0 +1 @@
ref: refs/heads/master

View File

@ -0,0 +1,6 @@
[core]
repositoryformatversion = 0
filemode = false
bare = true
symlinks = false
ignorecase = true

View File

@ -0,0 +1 @@
Unnamed repository; edit this file 'description' to name the repository.

View File

@ -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+"$@"}
:

View File

@ -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
}

View File

@ -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;
}

View File

@ -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

View File

@ -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+"$@"}
:

View File

@ -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 --

View File

@ -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"
:

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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]
# *~