aos-git 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. aos_git-0.1.0/PKG-INFO +73 -0
  2. aos_git-0.1.0/README.md +52 -0
  3. aos_git-0.1.0/pyproject.toml +36 -0
  4. aos_git-0.1.0/setup.cfg +4 -0
  5. aos_git-0.1.0/src/aos/__init__.py +0 -0
  6. aos_git-0.1.0/src/aos/ai/__init__.py +0 -0
  7. aos_git-0.1.0/src/aos/ai/adapter.py +38 -0
  8. aos_git-0.1.0/src/aos/ai/anthropic_adapter.py +39 -0
  9. aos_git-0.1.0/src/aos/ai/databricks.py +44 -0
  10. aos_git-0.1.0/src/aos/ai/factory.py +25 -0
  11. aos_git-0.1.0/src/aos/cli.py +20 -0
  12. aos_git-0.1.0/src/aos/commands/__init__.py +0 -0
  13. aos_git-0.1.0/src/aos/commands/commit.py +23 -0
  14. aos_git-0.1.0/src/aos/commands/merge.py +37 -0
  15. aos_git-0.1.0/src/aos/commands/pull.py +43 -0
  16. aos_git-0.1.0/src/aos/commands/push.py +22 -0
  17. aos_git-0.1.0/src/aos/commands/setup.py +47 -0
  18. aos_git-0.1.0/src/aos/commands/status.py +53 -0
  19. aos_git-0.1.0/src/aos/config/__init__.py +0 -0
  20. aos_git-0.1.0/src/aos/config/settings.py +71 -0
  21. aos_git-0.1.0/src/aos/conflict/__init__.py +0 -0
  22. aos_git-0.1.0/src/aos/conflict/applier.py +20 -0
  23. aos_git-0.1.0/src/aos/conflict/detector.py +18 -0
  24. aos_git-0.1.0/src/aos/conflict/parser.py +76 -0
  25. aos_git-0.1.0/src/aos/conflict/preview.py +38 -0
  26. aos_git-0.1.0/src/aos/conflict/resolver.py +65 -0
  27. aos_git-0.1.0/src/aos_git.egg-info/PKG-INFO +73 -0
  28. aos_git-0.1.0/src/aos_git.egg-info/SOURCES.txt +35 -0
  29. aos_git-0.1.0/src/aos_git.egg-info/dependency_links.txt +1 -0
  30. aos_git-0.1.0/src/aos_git.egg-info/entry_points.txt +2 -0
  31. aos_git-0.1.0/src/aos_git.egg-info/requires.txt +5 -0
  32. aos_git-0.1.0/src/aos_git.egg-info/top_level.txt +1 -0
  33. aos_git-0.1.0/tests/test_adapter.py +50 -0
  34. aos_git-0.1.0/tests/test_applier.py +40 -0
  35. aos_git-0.1.0/tests/test_config.py +29 -0
  36. aos_git-0.1.0/tests/test_detector.py +24 -0
  37. aos_git-0.1.0/tests/test_parser.py +55 -0
aos_git-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,73 @@
1
+ Metadata-Version: 2.4
2
+ Name: aos-git
3
+ Version: 0.1.0
4
+ Summary: AI-powered Git merge conflict resolver — let AI merge both sides intelligently
5
+ License: MIT
6
+ Project-URL: Homepage, https://github.com/YOUR_USERNAME/aos-git
7
+ Project-URL: Issues, https://github.com/YOUR_USERNAME/aos-git/issues
8
+ Keywords: git,merge,conflict,ai,cli
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Environment :: Console
13
+ Classifier: Topic :: Software Development :: Version Control :: Git
14
+ Requires-Python: >=3.11
15
+ Description-Content-Type: text/markdown
16
+ Requires-Dist: click>=8.1
17
+ Requires-Dist: rich>=13.0
18
+ Requires-Dist: toml>=0.10
19
+ Requires-Dist: openai>=1.0
20
+ Requires-Dist: anthropic>=0.25
21
+
22
+ # aos — AI-powered Git merge conflict resolver
23
+
24
+ `aos`는 git을 감싸는 CLI 도구입니다. `git pull` / `git merge` 시 충돌이 발생하면
25
+ AI가 양쪽 변경사항을 자동으로 분석해서 **둘 다 살리는 방향으로 통합**해드립니다.
26
+
27
+ ## 설치
28
+
29
+ ```bash
30
+ pip install aos-git
31
+ ```
32
+
33
+ ## 최초 설정
34
+
35
+ ```bash
36
+ aos setup
37
+ ```
38
+
39
+ Databricks 또는 Anthropic API 키를 입력하면 `~/.aos/config.toml`에 저장됩니다.
40
+
41
+ ## 사용법
42
+
43
+ 기존 git 명령어 대신 `aos`를 사용하세요:
44
+
45
+ | git 명령 | aos 명령 | 설명 |
46
+ |----------|----------|------|
47
+ | `git pull` | `aos pull` | 충돌 시 AI 자동 해결 |
48
+ | `git merge <branch>` | `aos merge <branch>` | 충돌 시 AI 자동 해결 |
49
+ | `git push` | `aos push` | 그대로 |
50
+ | `git commit -m "..."` | `aos commit -m "..."` | 전체 스테이징 후 커밋 |
51
+ | `git status` | `aos status` | 친화적인 상태 표시 |
52
+
53
+ ## 충돌 해결 흐름
54
+
55
+ ```
56
+ aos pull
57
+ └─→ 충돌 감지
58
+ └─→ AI가 양쪽 변경사항 분석 (타임스탬프 참고)
59
+ └─→ 통합 결과 미리보기 출력
60
+ └─→ [Y] 적용 / [n] 건너뜀 / [e] 직접 편집
61
+ ```
62
+
63
+ ## AI 제공자
64
+
65
+ - **Databricks Model Serving** (기본): 사내 Databricks 환경
66
+ - **Anthropic API**: `claude-sonnet-4-6` 등
67
+
68
+ 설정 전환: `aos setup`으로 언제든지 변경 가능
69
+
70
+ ## 요구사항
71
+
72
+ - Python 3.11+
73
+ - git
@@ -0,0 +1,52 @@
1
+ # aos — AI-powered Git merge conflict resolver
2
+
3
+ `aos`는 git을 감싸는 CLI 도구입니다. `git pull` / `git merge` 시 충돌이 발생하면
4
+ AI가 양쪽 변경사항을 자동으로 분석해서 **둘 다 살리는 방향으로 통합**해드립니다.
5
+
6
+ ## 설치
7
+
8
+ ```bash
9
+ pip install aos-git
10
+ ```
11
+
12
+ ## 최초 설정
13
+
14
+ ```bash
15
+ aos setup
16
+ ```
17
+
18
+ Databricks 또는 Anthropic API 키를 입력하면 `~/.aos/config.toml`에 저장됩니다.
19
+
20
+ ## 사용법
21
+
22
+ 기존 git 명령어 대신 `aos`를 사용하세요:
23
+
24
+ | git 명령 | aos 명령 | 설명 |
25
+ |----------|----------|------|
26
+ | `git pull` | `aos pull` | 충돌 시 AI 자동 해결 |
27
+ | `git merge <branch>` | `aos merge <branch>` | 충돌 시 AI 자동 해결 |
28
+ | `git push` | `aos push` | 그대로 |
29
+ | `git commit -m "..."` | `aos commit -m "..."` | 전체 스테이징 후 커밋 |
30
+ | `git status` | `aos status` | 친화적인 상태 표시 |
31
+
32
+ ## 충돌 해결 흐름
33
+
34
+ ```
35
+ aos pull
36
+ └─→ 충돌 감지
37
+ └─→ AI가 양쪽 변경사항 분석 (타임스탬프 참고)
38
+ └─→ 통합 결과 미리보기 출력
39
+ └─→ [Y] 적용 / [n] 건너뜀 / [e] 직접 편집
40
+ ```
41
+
42
+ ## AI 제공자
43
+
44
+ - **Databricks Model Serving** (기본): 사내 Databricks 환경
45
+ - **Anthropic API**: `claude-sonnet-4-6` 등
46
+
47
+ 설정 전환: `aos setup`으로 언제든지 변경 가능
48
+
49
+ ## 요구사항
50
+
51
+ - Python 3.11+
52
+ - git
@@ -0,0 +1,36 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "aos-git"
7
+ version = "0.1.0"
8
+ description = "AI-powered Git merge conflict resolver — let AI merge both sides intelligently"
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ requires-python = ">=3.11"
12
+ keywords = ["git", "merge", "conflict", "ai", "cli"]
13
+ classifiers = [
14
+ "Programming Language :: Python :: 3",
15
+ "License :: OSI Approved :: MIT License",
16
+ "Operating System :: OS Independent",
17
+ "Environment :: Console",
18
+ "Topic :: Software Development :: Version Control :: Git",
19
+ ]
20
+ dependencies = [
21
+ "click>=8.1",
22
+ "rich>=13.0",
23
+ "toml>=0.10",
24
+ "openai>=1.0",
25
+ "anthropic>=0.25",
26
+ ]
27
+
28
+ [project.urls]
29
+ Homepage = "https://github.com/YOUR_USERNAME/aos-git"
30
+ Issues = "https://github.com/YOUR_USERNAME/aos-git/issues"
31
+
32
+ [project.scripts]
33
+ aos = "aos.cli:main"
34
+
35
+ [tool.setuptools.packages.find]
36
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
File without changes
File without changes
@@ -0,0 +1,38 @@
1
+ from abc import ABC, abstractmethod
2
+ from aos.conflict.parser import ConflictBlock
3
+
4
+ SYSTEM_PROMPT = """당신은 git merge conflict 해결 전문가입니다.
5
+ 두 개발자가 같은 코드를 수정했을 때, 양쪽의 변경 의도를 모두 보존하여 논리적으로 통합된 코드를 반환합니다.
6
+ - 한쪽 변경사항을 일방적으로 버리지 마세요.
7
+ - 충돌 마커(<<<<<<, =======, >>>>>>>) 없이 최종 코드만 반환하세요.
8
+ - 코드 블록 마크다운(```) 없이 순수 코드만 반환하세요."""
9
+
10
+ USER_TEMPLATE = """다음 충돌을 해결해주세요.
11
+
12
+ 파일: {file_path}
13
+ BASE (공통 조상):
14
+ {base}
15
+
16
+ OURS (커밋 시각: {ours_ts}):
17
+ {ours}
18
+
19
+ THEIRS (커밋 시각: {theirs_ts}):
20
+ {theirs}
21
+
22
+ 주변 코드:
23
+ {context_before}
24
+ [충돌 위치]
25
+ {context_after}
26
+
27
+ 두 변경사항을 모두 살려 통합된 코드를 반환하세요."""
28
+
29
+
30
+ class AIAdapter(ABC):
31
+ @abstractmethod
32
+ def resolve_conflict(
33
+ self,
34
+ block: ConflictBlock,
35
+ ours_ts: str,
36
+ theirs_ts: str,
37
+ ) -> str:
38
+ """통합된 코드 문자열을 반환한다."""
@@ -0,0 +1,39 @@
1
+ from typing import Optional
2
+ import anthropic
3
+ from aos.ai.adapter import AIAdapter, SYSTEM_PROMPT, USER_TEMPLATE
4
+ from aos.conflict.parser import ConflictBlock
5
+
6
+
7
+ class AnthropicAdapter(AIAdapter):
8
+ def __init__(
9
+ self,
10
+ api_key: str,
11
+ model: str = "claude-sonnet-4-6",
12
+ _client: Optional[object] = None,
13
+ ):
14
+ self.model = model
15
+ self._client = _client or anthropic.Anthropic(api_key=api_key)
16
+
17
+ def resolve_conflict(
18
+ self,
19
+ block: ConflictBlock,
20
+ ours_ts: str,
21
+ theirs_ts: str,
22
+ ) -> str:
23
+ prompt = USER_TEMPLATE.format(
24
+ file_path=block.file_path,
25
+ base=block.base or "(없음)",
26
+ ours=block.ours,
27
+ theirs=block.theirs,
28
+ ours_ts=ours_ts or "알 수 없음",
29
+ theirs_ts=theirs_ts or "알 수 없음",
30
+ context_before=block.context_before,
31
+ context_after=block.context_after,
32
+ )
33
+ response = self._client.messages.create(
34
+ model=self.model,
35
+ max_tokens=4096,
36
+ system=SYSTEM_PROMPT,
37
+ messages=[{"role": "user", "content": prompt}],
38
+ )
39
+ return response.content[0].text
@@ -0,0 +1,44 @@
1
+ from typing import Optional
2
+ from openai import OpenAI
3
+ from aos.ai.adapter import AIAdapter, SYSTEM_PROMPT, USER_TEMPLATE
4
+ from aos.conflict.parser import ConflictBlock
5
+
6
+
7
+ class DatabricksAdapter(AIAdapter):
8
+ def __init__(
9
+ self,
10
+ host: str,
11
+ token: str,
12
+ model: str = "databricks-claude-sonnet-4-6",
13
+ _client: Optional[object] = None,
14
+ ):
15
+ self.model = model
16
+ self._client = _client or OpenAI(
17
+ api_key=token,
18
+ base_url=f"{host.rstrip('/')}/serving-endpoints",
19
+ )
20
+
21
+ def resolve_conflict(
22
+ self,
23
+ block: ConflictBlock,
24
+ ours_ts: str,
25
+ theirs_ts: str,
26
+ ) -> str:
27
+ prompt = USER_TEMPLATE.format(
28
+ file_path=block.file_path,
29
+ base=block.base or "(없음)",
30
+ ours=block.ours,
31
+ theirs=block.theirs,
32
+ ours_ts=ours_ts or "알 수 없음",
33
+ theirs_ts=theirs_ts or "알 수 없음",
34
+ context_before=block.context_before,
35
+ context_after=block.context_after,
36
+ )
37
+ response = self._client.chat.completions.create(
38
+ model=self.model,
39
+ messages=[
40
+ {"role": "system", "content": SYSTEM_PROMPT},
41
+ {"role": "user", "content": prompt},
42
+ ],
43
+ )
44
+ return response.choices[0].message.content
@@ -0,0 +1,25 @@
1
+ from aos.config.settings import Config
2
+ from aos.ai.adapter import AIAdapter
3
+ from aos.ai.databricks import DatabricksAdapter
4
+ from aos.ai.anthropic_adapter import AnthropicAdapter
5
+
6
+
7
+ def build_adapter(config: Config) -> AIAdapter:
8
+ """설정에 따라 적절한 AI 어댑터를 반환한다."""
9
+ if config.ai.provider == "databricks":
10
+ if not config.databricks or not config.databricks.host:
11
+ raise ValueError("Databricks 설정이 없습니다. 'aos setup'을 실행하세요.")
12
+ return DatabricksAdapter(
13
+ host=config.databricks.host,
14
+ token=config.databricks.token,
15
+ model=config.ai.model,
16
+ )
17
+ elif config.ai.provider == "anthropic":
18
+ if not config.anthropic or not config.anthropic.api_key:
19
+ raise ValueError("Anthropic API 키가 없습니다. 'aos setup'을 실행하세요.")
20
+ return AnthropicAdapter(
21
+ api_key=config.anthropic.api_key,
22
+ model=config.ai.model,
23
+ )
24
+ else:
25
+ raise ValueError(f"알 수 없는 AI 제공자: {config.ai.provider}")
@@ -0,0 +1,20 @@
1
+ import click
2
+ from aos.commands.pull import pull_cmd
3
+ from aos.commands.push import push_cmd
4
+ from aos.commands.commit import commit_cmd
5
+ from aos.commands.status import status_cmd
6
+ from aos.commands.merge import merge_cmd
7
+ from aos.commands.setup import setup_cmd
8
+
9
+
10
+ @click.group()
11
+ def main():
12
+ """aos — AI-powered git helper. 충돌을 AI가 해결해드립니다."""
13
+
14
+
15
+ main.add_command(pull_cmd, name="pull")
16
+ main.add_command(push_cmd, name="push")
17
+ main.add_command(commit_cmd, name="commit")
18
+ main.add_command(status_cmd, name="status")
19
+ main.add_command(merge_cmd, name="merge")
20
+ main.add_command(setup_cmd, name="setup")
File without changes
@@ -0,0 +1,23 @@
1
+ import subprocess
2
+ import click
3
+ from rich.console import Console
4
+
5
+ console = Console()
6
+
7
+
8
+ @click.command("commit")
9
+ @click.option("-m", "--message", default="", help="커밋 메시지")
10
+ def commit_cmd(message: str):
11
+ """모든 변경사항을 스테이징하고 커밋한다."""
12
+ subprocess.run(["git", "add", "-A"], check=True)
13
+ console.print("[cyan][aos] 모든 변경사항을 스테이징했습니다.[/cyan]")
14
+
15
+ if not message:
16
+ message = click.prompt("커밋 메시지를 입력하세요")
17
+
18
+ result = subprocess.run(["git", "commit", "-m", message])
19
+ if result.returncode == 0:
20
+ console.print("[green][aos] 커밋 완료.[/green]")
21
+ else:
22
+ console.print("[red][aos] 커밋 실패.[/red]")
23
+ raise click.Abort()
@@ -0,0 +1,37 @@
1
+ import subprocess
2
+ from pathlib import Path
3
+ import click
4
+ from rich.console import Console
5
+ from aos.config.settings import load_config
6
+ from aos.ai.factory import build_adapter
7
+ from aos.conflict.resolver import resolve_all_conflicts
8
+
9
+ console = Console()
10
+
11
+
12
+ @click.command("merge")
13
+ @click.argument("branch")
14
+ def merge_cmd(branch: str):
15
+ """브랜치를 병합하고, 충돌 시 AI가 해결을 제안한다."""
16
+ console.print(f"[cyan][aos] git merge {branch} 실행 중...[/cyan]")
17
+ result = subprocess.run(["git", "merge", branch], capture_output=True, text=True)
18
+ console.print(result.stdout)
19
+
20
+ if result.returncode == 0:
21
+ console.print("[green][aos] 병합 완료.[/green]")
22
+ return
23
+
24
+ if "CONFLICT" in result.stdout or "conflict" in result.stderr.lower():
25
+ console.print("[yellow][aos] 충돌이 감지되었습니다. AI 해결을 시도합니다...[/yellow]")
26
+ try:
27
+ cfg = load_config()
28
+ adapter = build_adapter(cfg)
29
+ except ValueError as e:
30
+ console.print(f"[red][aos] {e}[/red]")
31
+ raise click.Abort()
32
+
33
+ repo_root = Path.cwd()
34
+ resolve_all_conflicts(repo_root, adapter, cfg.behavior)
35
+ else:
36
+ console.print(f"[red][aos] 오류: {result.stderr}[/red]")
37
+ raise click.Abort()
@@ -0,0 +1,43 @@
1
+ import subprocess
2
+ from pathlib import Path
3
+ import click
4
+ from rich.console import Console
5
+ from aos.config.settings import load_config
6
+ from aos.ai.factory import build_adapter
7
+ from aos.conflict.resolver import resolve_all_conflicts
8
+
9
+ console = Console()
10
+
11
+
12
+ @click.command("pull")
13
+ @click.option("--remote", default="origin", help="원격 저장소 이름")
14
+ @click.option("--branch", default="", help="브랜치 이름 (기본: 현재 브랜치)")
15
+ def pull_cmd(remote: str, branch: str):
16
+ """원격에서 변경사항을 가져오고, 충돌 시 AI가 해결을 제안한다."""
17
+ cmd = ["git", "pull", remote]
18
+ if branch:
19
+ cmd.append(branch)
20
+
21
+ console.print(f"[cyan][aos] {' '.join(cmd)} 실행 중...[/cyan]")
22
+ result = subprocess.run(cmd, capture_output=True, text=True)
23
+ console.print(result.stdout)
24
+
25
+ if result.returncode == 0:
26
+ console.print("[green][aos] 완료.[/green]")
27
+ return
28
+
29
+ # 충돌 여부 확인
30
+ if "CONFLICT" in result.stdout or "conflict" in result.stderr.lower():
31
+ console.print("[yellow][aos] 충돌이 감지되었습니다. AI 해결을 시도합니다...[/yellow]")
32
+ try:
33
+ cfg = load_config()
34
+ adapter = build_adapter(cfg)
35
+ except ValueError as e:
36
+ console.print(f"[red][aos] {e}[/red]")
37
+ raise click.Abort()
38
+
39
+ repo_root = Path.cwd()
40
+ resolve_all_conflicts(repo_root, adapter, cfg.behavior)
41
+ else:
42
+ console.print(f"[red][aos] 오류: {result.stderr}[/red]")
43
+ raise click.Abort()
@@ -0,0 +1,22 @@
1
+ import subprocess
2
+ import click
3
+ from rich.console import Console
4
+
5
+ console = Console()
6
+
7
+
8
+ @click.command("push")
9
+ @click.option("--remote", default="origin")
10
+ @click.option("--branch", default="")
11
+ def push_cmd(remote: str, branch: str):
12
+ """변경사항을 원격 저장소에 올린다."""
13
+ cmd = ["git", "push", remote]
14
+ if branch:
15
+ cmd.append(branch)
16
+ console.print(f"[cyan][aos] {' '.join(cmd)} 실행 중...[/cyan]")
17
+ result = subprocess.run(cmd)
18
+ if result.returncode == 0:
19
+ console.print("[green][aos] 푸시 완료.[/green]")
20
+ else:
21
+ console.print("[red][aos] 푸시 실패. git push 출력을 확인하세요.[/red]")
22
+ raise click.Abort()
@@ -0,0 +1,47 @@
1
+ import subprocess
2
+ import click
3
+ from rich.console import Console
4
+ from aos.config.settings import (
5
+ load_config, save_config, AIConfig,
6
+ DatabricksConfig, AnthropicConfig, BehaviorConfig,
7
+ )
8
+
9
+ console = Console()
10
+
11
+
12
+ @click.command("setup")
13
+ def setup_cmd():
14
+ """AI 제공자를 설정하고 git을 구성한다."""
15
+ console.print("[bold cyan][aos] 설정을 시작합니다.[/bold cyan]")
16
+
17
+ provider = click.prompt(
18
+ "AI 제공자를 선택하세요",
19
+ type=click.Choice(["databricks", "anthropic"]),
20
+ default="databricks",
21
+ )
22
+
23
+ cfg = load_config()
24
+ cfg.ai = AIConfig(provider=provider)
25
+
26
+ if provider == "databricks":
27
+ host = click.prompt("Databricks Host (예: https://xxx.azuredatabricks.net)")
28
+ token = click.prompt("Databricks Token", hide_input=True)
29
+ model = click.prompt("모델 엔드포인트", default="databricks-claude-sonnet-4-6")
30
+ cfg.databricks = DatabricksConfig(host=host, token=token)
31
+ cfg.ai.model = model
32
+ else:
33
+ api_key = click.prompt("Anthropic API Key", hide_input=True)
34
+ model = click.prompt("모델", default="claude-sonnet-4-6")
35
+ cfg.anthropic = AnthropicConfig(api_key=api_key)
36
+ cfg.ai.model = model
37
+
38
+ lang = click.prompt("안내 메시지 언어", type=click.Choice(["ko", "en"]), default="ko")
39
+ cfg.behavior = BehaviorConfig(language=lang)
40
+
41
+ save_config(cfg)
42
+
43
+ # diff3 스타일 자동 설정 (BASE 포함 파싱을 위해 필요)
44
+ subprocess.run(["git", "config", "--global", "merge.conflictstyle", "diff3"], check=False)
45
+
46
+ console.print("[green][aos] 설정 완료! ~/.aos/config.toml 에 저장됨.[/green]")
47
+ console.print("[green] git merge.conflictstyle=diff3 적용됨.[/green]")
@@ -0,0 +1,53 @@
1
+ import subprocess
2
+ import click
3
+ from rich.console import Console
4
+ from rich.table import Table
5
+
6
+ console = Console()
7
+
8
+
9
+ @click.command("status")
10
+ def status_cmd():
11
+ """현재 git 상태를 친화적으로 표시한다."""
12
+ result = subprocess.run(
13
+ ["git", "status", "--porcelain=v1", "-b"],
14
+ capture_output=True, text=True,
15
+ )
16
+ if result.returncode != 0:
17
+ console.print("[red]git 저장소가 아니거나 오류가 발생했습니다.[/red]")
18
+ raise click.Abort()
19
+
20
+ lines = result.stdout.splitlines()
21
+ branch_line = lines[0] if lines else ""
22
+ file_lines = lines[1:]
23
+
24
+ branch = branch_line.replace("## ", "").split("...")[0]
25
+ console.print(f"\n[bold]현재 브랜치:[/bold] [cyan]{branch}[/cyan]\n")
26
+
27
+ if not file_lines:
28
+ console.print("[green]변경사항 없음. 깨끗한 상태입니다.[/green]")
29
+ return
30
+
31
+ table = Table(show_header=True, header_style="bold")
32
+ table.add_column("상태", width=12)
33
+ table.add_column("파일")
34
+
35
+ STATUS_MAP = {
36
+ "M": ("수정됨", "yellow"),
37
+ "A": ("추가됨", "green"),
38
+ "D": ("삭제됨", "red"),
39
+ "R": ("이름변경", "blue"),
40
+ "?": ("미추적", "dim"),
41
+ "U": ("충돌", "bold red"),
42
+ }
43
+
44
+ for line in file_lines:
45
+ if len(line) < 3:
46
+ continue
47
+ xy = line[:2].strip() or line[0]
48
+ filepath = line[3:]
49
+ code = xy[0] if xy else "?"
50
+ label, color = STATUS_MAP.get(code, (code, "white"))
51
+ table.add_row(f"[{color}]{label}[/{color}]", filepath)
52
+
53
+ console.print(table)
File without changes
@@ -0,0 +1,71 @@
1
+ from dataclasses import dataclass, field
2
+ from pathlib import Path
3
+ from typing import Optional
4
+ import toml
5
+
6
+ CONFIG_PATH = Path.home() / ".aos" / "config.toml"
7
+
8
+
9
+ @dataclass
10
+ class AIConfig:
11
+ provider: str = "databricks"
12
+ model: str = "databricks-claude-sonnet-4-6"
13
+
14
+
15
+ @dataclass
16
+ class DatabricksConfig:
17
+ host: str = ""
18
+ token: str = ""
19
+
20
+
21
+ @dataclass
22
+ class AnthropicConfig:
23
+ api_key: str = ""
24
+
25
+
26
+ @dataclass
27
+ class BehaviorConfig:
28
+ auto_apply: bool = False
29
+ context_lines: int = 20
30
+ language: str = "ko"
31
+
32
+
33
+ @dataclass
34
+ class Config:
35
+ ai: AIConfig = field(default_factory=AIConfig)
36
+ databricks: Optional[DatabricksConfig] = field(default_factory=DatabricksConfig)
37
+ anthropic: Optional[AnthropicConfig] = field(default_factory=AnthropicConfig)
38
+ behavior: BehaviorConfig = field(default_factory=BehaviorConfig)
39
+
40
+
41
+ def load_config() -> Config:
42
+ if not CONFIG_PATH.exists():
43
+ return Config()
44
+ data = toml.load(CONFIG_PATH)
45
+ return Config(
46
+ ai=AIConfig(**data.get("ai", {})),
47
+ databricks=DatabricksConfig(**data.get("databricks", {})),
48
+ anthropic=AnthropicConfig(**data.get("anthropic", {})),
49
+ behavior=BehaviorConfig(**data.get("behavior", {})),
50
+ )
51
+
52
+
53
+ def save_config(config: Config) -> None:
54
+ CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
55
+ data = {
56
+ "ai": {"provider": config.ai.provider, "model": config.ai.model},
57
+ "databricks": {
58
+ "host": config.databricks.host if config.databricks else "",
59
+ "token": config.databricks.token if config.databricks else "",
60
+ },
61
+ "anthropic": {
62
+ "api_key": config.anthropic.api_key if config.anthropic else "",
63
+ },
64
+ "behavior": {
65
+ "auto_apply": config.behavior.auto_apply,
66
+ "context_lines": config.behavior.context_lines,
67
+ "language": config.behavior.language,
68
+ },
69
+ }
70
+ with open(CONFIG_PATH, "w") as f:
71
+ toml.dump(data, f)
File without changes
@@ -0,0 +1,20 @@
1
+ from pathlib import Path
2
+
3
+
4
+ def apply_resolution(file_path: Path, line_start: int, resolved: str) -> None:
5
+ """파일에서 line_start 위치의 충돌 블록을 resolved로 교체한다."""
6
+ text = file_path.read_text(encoding="utf-8")
7
+ lines = text.splitlines(keepends=True)
8
+ # line_start는 1-based; <<<<<<< 줄 찾기
9
+ i = line_start - 1
10
+ if i >= len(lines) or not lines[i].startswith("<<<<<<<"):
11
+ return
12
+ # 블록 끝(>>>>>>>) 찾기
13
+ j = i + 1
14
+ while j < len(lines):
15
+ if lines[j].startswith(">>>>>>>"):
16
+ j += 1
17
+ break
18
+ j += 1
19
+ new_lines = lines[:i] + [resolved] + lines[j:]
20
+ file_path.write_text("".join(new_lines), encoding="utf-8")
@@ -0,0 +1,18 @@
1
+ from pathlib import Path
2
+
3
+ CONFLICT_MARKER = "<<<<<<< "
4
+ SKIP_DIRS = {".git", "__pycache__", "node_modules", ".venv", "venv"}
5
+
6
+
7
+ def find_conflict_files(repo_root: Path) -> list[Path]:
8
+ """repo_root 아래에서 충돌 마커가 있는 파일 목록을 반환한다."""
9
+ conflict_files = []
10
+ for path in repo_root.rglob("*"):
11
+ if path.is_file() and not any(p.name in SKIP_DIRS for p in path.parents):
12
+ try:
13
+ text = path.read_text(encoding="utf-8", errors="ignore")
14
+ if CONFLICT_MARKER in text:
15
+ conflict_files.append(path)
16
+ except (PermissionError, OSError):
17
+ pass
18
+ return conflict_files
@@ -0,0 +1,76 @@
1
+ from dataclasses import dataclass
2
+ from pathlib import Path
3
+ import subprocess
4
+
5
+
6
+ @dataclass
7
+ class ConflictBlock:
8
+ file_path: Path
9
+ line_start: int # 1-based, <<<<<<< 줄 번호
10
+ ours: str
11
+ base: str # diff3 스타일 있을 때만 채워짐
12
+ theirs: str
13
+ context_before: str
14
+ context_after: str
15
+
16
+
17
+ def parse_conflicts(file_path: Path, context_lines: int = 20) -> list[ConflictBlock]:
18
+ """파일 내 모든 충돌 블록을 파싱하여 반환한다."""
19
+ text = file_path.read_text(encoding="utf-8")
20
+ lines = text.splitlines(keepends=True)
21
+ blocks = []
22
+ i = 0
23
+ while i < len(lines):
24
+ if lines[i].startswith("<<<<<<<"):
25
+ block_start = i
26
+ ours_lines, base_lines, theirs_lines = [], [], []
27
+ section = "ours"
28
+ i += 1
29
+ while i < len(lines):
30
+ line = lines[i]
31
+ if line.startswith("|||||||"):
32
+ section = "base"
33
+ i += 1
34
+ continue
35
+ elif line.startswith("======="):
36
+ section = "theirs"
37
+ i += 1
38
+ continue
39
+ elif line.startswith(">>>>>>>"):
40
+ i += 1
41
+ break
42
+ if section == "ours":
43
+ ours_lines.append(line)
44
+ elif section == "base":
45
+ base_lines.append(line)
46
+ else:
47
+ theirs_lines.append(line)
48
+ i += 1
49
+ ctx_start = max(0, block_start - context_lines)
50
+ ctx_end = min(len(lines), i + context_lines)
51
+ blocks.append(ConflictBlock(
52
+ file_path=file_path,
53
+ line_start=block_start + 1,
54
+ ours="".join(ours_lines),
55
+ base="".join(base_lines),
56
+ theirs="".join(theirs_lines),
57
+ context_before="".join(lines[ctx_start:block_start]),
58
+ context_after="".join(lines[i:ctx_end]),
59
+ ))
60
+ else:
61
+ i += 1
62
+ return blocks
63
+
64
+
65
+ def get_merge_timestamps(repo_root: Path) -> tuple[str, str]:
66
+ """HEAD(OURS)와 MERGE_HEAD(THEIRS)의 커밋 타임스탬프를 반환한다."""
67
+ def _ts(ref: str) -> str:
68
+ try:
69
+ r = subprocess.run(
70
+ ["git", "log", "-1", "--format=%ci", ref],
71
+ capture_output=True, text=True, cwd=repo_root, timeout=5,
72
+ )
73
+ return r.stdout.strip()
74
+ except Exception:
75
+ return ""
76
+ return _ts("HEAD"), _ts("MERGE_HEAD")
@@ -0,0 +1,38 @@
1
+ from rich.console import Console
2
+ from rich.panel import Panel
3
+ from rich.text import Text
4
+ from rich.columns import Columns
5
+ from aos.conflict.parser import ConflictBlock
6
+
7
+ console = Console()
8
+
9
+
10
+ def show_conflict_preview(
11
+ block: ConflictBlock,
12
+ resolved: str,
13
+ index: int,
14
+ total: int,
15
+ ) -> None:
16
+ """충돌 블록과 AI 해결안을 터미널에 나란히 출력한다."""
17
+ console.rule(f"[bold yellow]충돌 {index}/{total}[/] — {block.file_path} (라인 {block.line_start})")
18
+
19
+ ours_text = Text(f"[OURS]\n{block.ours}", style="red")
20
+ theirs_text = Text(f"[THEIRS]\n{block.theirs}", style="blue")
21
+ resolved_text = Text(f"[AI 통합 결과]\n{resolved}", style="green")
22
+
23
+ console.print(Columns([
24
+ Panel(ours_text, title="내 변경사항", border_style="red"),
25
+ Panel(theirs_text, title="상대방 변경사항", border_style="blue"),
26
+ Panel(resolved_text, title="AI 제안", border_style="green"),
27
+ ]))
28
+
29
+
30
+ def ask_apply(language: str = "ko") -> str:
31
+ """사용자에게 적용 여부를 묻고 'y', 'n', 'e' 중 하나를 반환한다."""
32
+ msg = "적용할까요?" if language == "ko" else "Apply this resolution?"
33
+ choice = console.input(f"[bold]{msg}[/] \\[Y/n/e(직접편집)] ").strip().lower()
34
+ if choice in ("", "y"):
35
+ return "y"
36
+ elif choice == "e":
37
+ return "e"
38
+ return "n"
@@ -0,0 +1,65 @@
1
+ import click
2
+ from pathlib import Path
3
+ from rich.console import Console
4
+
5
+ from aos.conflict.detector import find_conflict_files
6
+ from aos.conflict.parser import parse_conflicts, get_merge_timestamps
7
+ from aos.conflict.preview import show_conflict_preview, ask_apply
8
+ from aos.conflict.applier import apply_resolution
9
+ from aos.ai.adapter import AIAdapter
10
+
11
+ console = Console()
12
+
13
+
14
+ def resolve_all_conflicts(repo_root: Path, adapter: AIAdapter, behavior) -> bool:
15
+ """
16
+ repo_root 내 모든 충돌을 처리한다.
17
+ 같은 파일 내 여러 충돌은 결정을 먼저 모두 수집한 뒤 역순으로 적용한다.
18
+ (앞 블록을 먼저 적용하면 뒤 블록의 줄 번호가 어긋나는 버그 방지)
19
+ 반환값: 모든 충돌이 적용되면 True, 하나라도 건너뛰면 False.
20
+ """
21
+ conflict_files = find_conflict_files(repo_root)
22
+ if not conflict_files:
23
+ console.print("[green][aos] 충돌이 없습니다.[/green]")
24
+ return True
25
+
26
+ ours_ts, theirs_ts = get_merge_timestamps(repo_root)
27
+ all_applied = True
28
+ total_blocks = sum(len(parse_conflicts(f)) for f in conflict_files)
29
+ index = 0
30
+
31
+ for file_path in conflict_files:
32
+ blocks = parse_conflicts(file_path, context_lines=behavior.context_lines)
33
+ # 1단계: 모든 블록에 대해 AI 해결 + 사용자 결정 수집
34
+ decisions: list[tuple] = [] # (block, resolved, choice)
35
+ for block in blocks:
36
+ index += 1
37
+ try:
38
+ resolved = adapter.resolve_conflict(block, ours_ts=ours_ts, theirs_ts=theirs_ts)
39
+ except Exception as e:
40
+ console.print(f"[red][aos] AI 호출 실패: {e}[/red]")
41
+ console.print("[yellow]수동 해결: git mergetool[/yellow]")
42
+ decisions.append((block, "", "n"))
43
+ all_applied = False
44
+ continue
45
+
46
+ show_conflict_preview(block, resolved, index, total_blocks)
47
+ choice = "y" if behavior.auto_apply else ask_apply(behavior.language)
48
+ decisions.append((block, resolved, choice))
49
+
50
+ # 2단계: 역순으로 적용 (줄 번호 보존)
51
+ for block, resolved, choice in reversed(decisions):
52
+ if choice == "y":
53
+ apply_resolution(file_path, block.line_start, resolved)
54
+ console.print(f"[green] 적용됨 (라인 {block.line_start})[/green]")
55
+ elif choice == "e":
56
+ click.edit(filename=str(file_path))
57
+ all_applied = False
58
+ else:
59
+ console.print(f"[yellow] 건너뜀 (라인 {block.line_start}) — 충돌 마커 유지됨[/yellow]")
60
+ all_applied = False
61
+
62
+ if not all_applied:
63
+ console.print("[yellow][aos] 일부 충돌이 남아 있습니다. 'git merge --abort' 또는 수동 해결하세요.[/yellow]")
64
+
65
+ return all_applied
@@ -0,0 +1,73 @@
1
+ Metadata-Version: 2.4
2
+ Name: aos-git
3
+ Version: 0.1.0
4
+ Summary: AI-powered Git merge conflict resolver — let AI merge both sides intelligently
5
+ License: MIT
6
+ Project-URL: Homepage, https://github.com/YOUR_USERNAME/aos-git
7
+ Project-URL: Issues, https://github.com/YOUR_USERNAME/aos-git/issues
8
+ Keywords: git,merge,conflict,ai,cli
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Environment :: Console
13
+ Classifier: Topic :: Software Development :: Version Control :: Git
14
+ Requires-Python: >=3.11
15
+ Description-Content-Type: text/markdown
16
+ Requires-Dist: click>=8.1
17
+ Requires-Dist: rich>=13.0
18
+ Requires-Dist: toml>=0.10
19
+ Requires-Dist: openai>=1.0
20
+ Requires-Dist: anthropic>=0.25
21
+
22
+ # aos — AI-powered Git merge conflict resolver
23
+
24
+ `aos`는 git을 감싸는 CLI 도구입니다. `git pull` / `git merge` 시 충돌이 발생하면
25
+ AI가 양쪽 변경사항을 자동으로 분석해서 **둘 다 살리는 방향으로 통합**해드립니다.
26
+
27
+ ## 설치
28
+
29
+ ```bash
30
+ pip install aos-git
31
+ ```
32
+
33
+ ## 최초 설정
34
+
35
+ ```bash
36
+ aos setup
37
+ ```
38
+
39
+ Databricks 또는 Anthropic API 키를 입력하면 `~/.aos/config.toml`에 저장됩니다.
40
+
41
+ ## 사용법
42
+
43
+ 기존 git 명령어 대신 `aos`를 사용하세요:
44
+
45
+ | git 명령 | aos 명령 | 설명 |
46
+ |----------|----------|------|
47
+ | `git pull` | `aos pull` | 충돌 시 AI 자동 해결 |
48
+ | `git merge <branch>` | `aos merge <branch>` | 충돌 시 AI 자동 해결 |
49
+ | `git push` | `aos push` | 그대로 |
50
+ | `git commit -m "..."` | `aos commit -m "..."` | 전체 스테이징 후 커밋 |
51
+ | `git status` | `aos status` | 친화적인 상태 표시 |
52
+
53
+ ## 충돌 해결 흐름
54
+
55
+ ```
56
+ aos pull
57
+ └─→ 충돌 감지
58
+ └─→ AI가 양쪽 변경사항 분석 (타임스탬프 참고)
59
+ └─→ 통합 결과 미리보기 출력
60
+ └─→ [Y] 적용 / [n] 건너뜀 / [e] 직접 편집
61
+ ```
62
+
63
+ ## AI 제공자
64
+
65
+ - **Databricks Model Serving** (기본): 사내 Databricks 환경
66
+ - **Anthropic API**: `claude-sonnet-4-6` 등
67
+
68
+ 설정 전환: `aos setup`으로 언제든지 변경 가능
69
+
70
+ ## 요구사항
71
+
72
+ - Python 3.11+
73
+ - git
@@ -0,0 +1,35 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/aos/__init__.py
4
+ src/aos/cli.py
5
+ src/aos/ai/__init__.py
6
+ src/aos/ai/adapter.py
7
+ src/aos/ai/anthropic_adapter.py
8
+ src/aos/ai/databricks.py
9
+ src/aos/ai/factory.py
10
+ src/aos/commands/__init__.py
11
+ src/aos/commands/commit.py
12
+ src/aos/commands/merge.py
13
+ src/aos/commands/pull.py
14
+ src/aos/commands/push.py
15
+ src/aos/commands/setup.py
16
+ src/aos/commands/status.py
17
+ src/aos/config/__init__.py
18
+ src/aos/config/settings.py
19
+ src/aos/conflict/__init__.py
20
+ src/aos/conflict/applier.py
21
+ src/aos/conflict/detector.py
22
+ src/aos/conflict/parser.py
23
+ src/aos/conflict/preview.py
24
+ src/aos/conflict/resolver.py
25
+ src/aos_git.egg-info/PKG-INFO
26
+ src/aos_git.egg-info/SOURCES.txt
27
+ src/aos_git.egg-info/dependency_links.txt
28
+ src/aos_git.egg-info/entry_points.txt
29
+ src/aos_git.egg-info/requires.txt
30
+ src/aos_git.egg-info/top_level.txt
31
+ tests/test_adapter.py
32
+ tests/test_applier.py
33
+ tests/test_config.py
34
+ tests/test_detector.py
35
+ tests/test_parser.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ aos = aos.cli:main
@@ -0,0 +1,5 @@
1
+ click>=8.1
2
+ rich>=13.0
3
+ toml>=0.10
4
+ openai>=1.0
5
+ anthropic>=0.25
@@ -0,0 +1 @@
1
+ aos
@@ -0,0 +1,50 @@
1
+ from unittest.mock import MagicMock
2
+ from aos.ai.databricks import DatabricksAdapter
3
+ from aos.ai.anthropic_adapter import AnthropicAdapter
4
+ from aos.conflict.parser import ConflictBlock
5
+ from pathlib import Path
6
+
7
+
8
+ def _make_block(ours: str, base: str, theirs: str) -> ConflictBlock:
9
+ return ConflictBlock(
10
+ file_path=Path("app.py"),
11
+ line_start=1,
12
+ ours=ours,
13
+ base=base,
14
+ theirs=theirs,
15
+ context_before="",
16
+ context_after="",
17
+ )
18
+
19
+
20
+ def test_databricks_adapter_returns_resolved_string():
21
+ block = _make_block("def f():\n return 1\n", "", "def f(x):\n return x\n")
22
+ mock_client = MagicMock()
23
+ mock_client.chat.completions.create.return_value = MagicMock(
24
+ choices=[MagicMock(message=MagicMock(content="def f(x=None):\n return x\n"))]
25
+ )
26
+ adapter = DatabricksAdapter(
27
+ host="https://fake.databricks.net",
28
+ token="fake-token",
29
+ model="databricks-claude-sonnet-4-6",
30
+ _client=mock_client,
31
+ )
32
+ result = adapter.resolve_conflict(block, ours_ts="2026-04-20", theirs_ts="2026-04-21")
33
+ assert "def f(" in result
34
+ mock_client.chat.completions.create.assert_called_once()
35
+
36
+
37
+ def test_anthropic_adapter_returns_resolved_string():
38
+ block = _make_block("x = 1\n", "", "x = 2\n")
39
+ mock_client = MagicMock()
40
+ mock_client.messages.create.return_value = MagicMock(
41
+ content=[MagicMock(text="x = 1 # merged\n")]
42
+ )
43
+ adapter = AnthropicAdapter(
44
+ api_key="fake-key",
45
+ model="claude-sonnet-4-6",
46
+ _client=mock_client,
47
+ )
48
+ result = adapter.resolve_conflict(block, ours_ts="2026-04-20", theirs_ts="2026-04-21")
49
+ assert "x = " in result
50
+ mock_client.messages.create.assert_called_once()
@@ -0,0 +1,40 @@
1
+ from pathlib import Path
2
+ from aos.conflict.applier import apply_resolution
3
+
4
+
5
+ def test_apply_resolution_replaces_conflict_block(tmp_path):
6
+ f = tmp_path / "app.py"
7
+ f.write_text(
8
+ "# header\n"
9
+ "<<<<<<< HEAD\n"
10
+ "x = 1\n"
11
+ "=======\n"
12
+ "x = 2\n"
13
+ ">>>>>>> other\n"
14
+ "# footer\n"
15
+ )
16
+ apply_resolution(f, line_start=2, resolved="x = 1 # merged\n")
17
+ result = f.read_text()
18
+ assert "<<<<<<" not in result
19
+ assert "=======" not in result
20
+ assert ">>>>>>" not in result
21
+ assert "x = 1 # merged" in result
22
+ assert "# header" in result
23
+ assert "# footer" in result
24
+
25
+
26
+ def test_apply_resolution_with_diff3_block(tmp_path):
27
+ f = tmp_path / "app.py"
28
+ f.write_text(
29
+ "<<<<<<< HEAD\n"
30
+ "a = 1\n"
31
+ "||||||| base\n"
32
+ "a = 0\n"
33
+ "=======\n"
34
+ "a = 2\n"
35
+ ">>>>>>> other\n"
36
+ )
37
+ apply_resolution(f, line_start=1, resolved="a = 1 + 2\n")
38
+ result = f.read_text()
39
+ assert "a = 1 + 2" in result
40
+ assert "<<<<<<" not in result
@@ -0,0 +1,29 @@
1
+ import pytest
2
+ from pathlib import Path
3
+ from unittest.mock import patch
4
+ from aos.config.settings import load_config, save_config, Config, AIConfig, BehaviorConfig
5
+
6
+
7
+ def test_load_config_returns_defaults_when_missing(tmp_path):
8
+ config_path = tmp_path / "config.toml"
9
+ with patch("aos.config.settings.CONFIG_PATH", config_path):
10
+ cfg = load_config()
11
+ assert cfg.ai.provider == "databricks"
12
+ assert cfg.behavior.auto_apply is False
13
+ assert cfg.behavior.context_lines == 20
14
+
15
+
16
+ def test_save_and_load_roundtrip(tmp_path):
17
+ config_path = tmp_path / ".aos" / "config.toml"
18
+ with patch("aos.config.settings.CONFIG_PATH", config_path):
19
+ cfg = Config(
20
+ ai=AIConfig(provider="anthropic", model="claude-opus-4-6"),
21
+ databricks=None,
22
+ anthropic=None,
23
+ behavior=BehaviorConfig(auto_apply=True, context_lines=30, language="en"),
24
+ )
25
+ save_config(cfg)
26
+ loaded = load_config()
27
+ assert loaded.ai.provider == "anthropic"
28
+ assert loaded.behavior.auto_apply is True
29
+ assert loaded.behavior.context_lines == 30
@@ -0,0 +1,24 @@
1
+ from pathlib import Path
2
+ from aos.conflict.detector import find_conflict_files
3
+
4
+
5
+ def test_find_conflict_files_detects_marker(tmp_path):
6
+ conflict_file = tmp_path / "main.py"
7
+ conflict_file.write_text(
8
+ "def foo():\n"
9
+ "<<<<<<< HEAD\n"
10
+ " return 1\n"
11
+ "=======\n"
12
+ " return 2\n"
13
+ ">>>>>>> feature\n"
14
+ )
15
+ clean_file = tmp_path / "clean.py"
16
+ clean_file.write_text("def bar():\n return 3\n")
17
+
18
+ result = find_conflict_files(tmp_path)
19
+ assert conflict_file in result
20
+ assert clean_file not in result
21
+
22
+
23
+ def test_find_conflict_files_empty_dir(tmp_path):
24
+ assert find_conflict_files(tmp_path) == []
@@ -0,0 +1,55 @@
1
+ from pathlib import Path
2
+ from aos.conflict.parser import parse_conflicts, ConflictBlock
3
+
4
+
5
+ def test_parse_single_conflict_diff3(tmp_path):
6
+ f = tmp_path / "app.py"
7
+ f.write_text(
8
+ "# header\n"
9
+ "<<<<<<< HEAD\n"
10
+ "def login():\n"
11
+ " pass\n"
12
+ "||||||| base\n"
13
+ "def login():\n"
14
+ " return None\n"
15
+ "=======\n"
16
+ "def login(user_id):\n"
17
+ " pass\n"
18
+ ">>>>>>> feature/auth\n"
19
+ "# footer\n"
20
+ )
21
+ blocks = parse_conflicts(f, context_lines=1)
22
+ assert len(blocks) == 1
23
+ b = blocks[0]
24
+ assert "def login():\n pass\n" in b.ours
25
+ assert "def login():\n return None\n" in b.base
26
+ assert "def login(user_id):\n pass\n" in b.theirs
27
+ assert b.line_start == 2
28
+
29
+
30
+ def test_parse_conflict_without_base(tmp_path):
31
+ """diff3 없는 경우 base가 빈 문자열이어야 한다."""
32
+ f = tmp_path / "app.py"
33
+ f.write_text(
34
+ "<<<<<<< HEAD\n"
35
+ "x = 1\n"
36
+ "=======\n"
37
+ "x = 2\n"
38
+ ">>>>>>> other\n"
39
+ )
40
+ blocks = parse_conflicts(f)
41
+ assert len(blocks) == 1
42
+ assert blocks[0].base == ""
43
+ assert blocks[0].ours == "x = 1\n"
44
+ assert blocks[0].theirs == "x = 2\n"
45
+
46
+
47
+ def test_parse_multiple_conflicts(tmp_path):
48
+ f = tmp_path / "app.py"
49
+ f.write_text(
50
+ "<<<<<<< HEAD\na = 1\n=======\na = 2\n>>>>>>> other\n"
51
+ "middle\n"
52
+ "<<<<<<< HEAD\nb = 1\n=======\nb = 2\n>>>>>>> other\n"
53
+ )
54
+ blocks = parse_conflicts(f)
55
+ assert len(blocks) == 2