-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathrun_pipeline.py
More file actions
197 lines (160 loc) · 7.29 KB
/
Copy pathrun_pipeline.py
File metadata and controls
197 lines (160 loc) · 7.29 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
#!/usr/bin/env python3
"""
Two-stage repository analysis pipeline.
Runs the existing tools back to back on a single target, in order:
Stage 1 -> Repo_analysis_tool.py (staged metadata / metrics pipeline)
Stage 2 -> stage2_llm_detector.py (LLM / AI-authorship detection)
This orchestrator does not modify either tool. It only shells out to them
with the right arguments, one after the other, and collects their outputs
into the same directory.
Target handling
- A local directory is analyzed on disk by both stages.
- A GitHub URL: stage 1 clones + analyzes it; stage 2 uses its own GitHub
API provider (owner/repo parsed from the URL).
- A GitLab URL (gitlab.com or self-hosted): stage 1 clones + analyzes it;
stage 2 uses its own GitLab API provider (project path parsed from the URL).
Examples
python run_pipeline.py -i ./some/local/repo
python run_pipeline.py -i https://github.com/owner/repo --github-token ghp_xxx
python run_pipeline.py -i https://gitlab.com/group/sub/repo --gitlab-token glpat-xxx
python run_pipeline.py -i https://git.company.com/team/repo --gitlab-token glpat-xxx
"""
import argparse
import os
import re
import subprocess
import sys
from urllib.parse import urlparse
HERE = os.path.dirname(os.path.abspath(__file__))
STAGE1_SCRIPT = os.path.join(HERE, "Repo_analysis_tool.py")
STAGE2_SCRIPT = os.path.join(HERE, "stage2_llm_detector.py")
def classify_target(target):
"""
Work out what kind of target we were given and pull out the pieces each
stage needs.
Returns a dict with at least {"kind": "local" | "github" | "gitlab"}.
"""
raw = target.strip().strip('"').rstrip("/")
# A path that exists on disk always wins — treat it as a local repo.
if os.path.isdir(raw):
return {"kind": "local", "path": os.path.abspath(raw), "name": os.path.basename(os.path.abspath(raw))}
# scp-style git remote: git@host:group/repo.git
m = re.match(r"git@([^:]+):(.+)", raw)
if m:
host, path = m.group(1), m.group(2)
else:
parsed = urlparse(raw)
host, path = parsed.netloc, parsed.path.lstrip("/")
if not host:
# Not an existing directory and not a URL we can parse.
return {"kind": "unknown", "raw": raw}
if path.endswith(".git"):
path = path[:-4]
path = path.strip("/")
host_l = host.lower()
if "github" in host_l:
parts = [p for p in path.split("/") if p]
project = "/".join(parts[:2]) # owner/repo
api_url = "https://api.github.com" if host_l == "github.com" else f"https://{host}/api/v3"
return {
"kind": "github",
"project": project,
"api_url": api_url,
"name": parts[-1] if parts else "repo",
}
# Default for any other host is GitLab (gitlab.com or self-hosted),
# matching how both underlying tools treat unknown Git hosts.
parts = [p for p in path.split("/") if p]
return {
"kind": "gitlab",
"project": path, # full nested group/sub/repo path
"gitlab_url": f"https://{host}",
"name": parts[-1] if parts else "repo",
}
def build_stage1_cmd(args):
cmd = [
sys.executable, STAGE1_SCRIPT,
"-i", args.input,
"-o", args.output_dir,
"-m", args.mode,
]
if args.github_token:
cmd += ["--github-token", args.github_token]
if args.gitlab_token:
cmd += ["--gitlab-token", args.gitlab_token]
return cmd
def build_stage2_cmd(args, target):
kind = target["kind"]
out_csv = os.path.join(args.output_dir, f"{target.get('name', 'repo')}_llm_detection.csv")
cmd = [sys.executable, STAGE2_SCRIPT, "--provider", kind, "--output", out_csv]
if kind == "local":
cmd += ["--path", target["path"]]
elif kind == "github":
cmd += ["--project", target["project"], "--github-url", target["api_url"]]
if args.github_token:
cmd += ["--token", args.github_token]
elif kind == "gitlab":
cmd += ["--project", target["project"], "--gitlab-url", target["gitlab_url"]]
if args.gitlab_token:
cmd += ["--token", args.gitlab_token]
return cmd, out_csv
def run(cmd, label):
print("\n" + "=" * 70)
print(f"[{label}] {' '.join(cmd)}")
print("=" * 70, flush=True)
result = subprocess.run(cmd)
print(f"[{label}] exit code: {result.returncode}", flush=True)
return result.returncode
def main():
ap = argparse.ArgumentParser(
description="Run the stage 1 repo analysis, then the stage 2 LLM detector, on one target.")
ap.add_argument("-i", "--input", required=True,
help="Local directory path OR a GitHub/GitLab repository URL")
ap.add_argument("-o", "--output-dir", default="./outputs",
help="Where both stages write their reports (default: ./outputs)")
ap.add_argument("-m", "--mode", choices=["stage1", "stage2", "full"], default="full",
help="Mode passed through to stage 1 (default: full)")
ap.add_argument("--github-token", default=os.environ.get("GITHUB_TOKEN"),
help="GitHub token for both stages (or set GITHUB_TOKEN)")
ap.add_argument("--gitlab-token", default=os.environ.get("GITLAB_TOKEN"),
help="GitLab token for both stages (or set GITLAB_TOKEN)")
ap.add_argument("--skip-stage1", action="store_true", help="Run only stage 2")
ap.add_argument("--skip-stage2", action="store_true", help="Run only stage 1")
args = ap.parse_args()
args.input = args.input.strip().strip('"')
for script in (STAGE1_SCRIPT, STAGE2_SCRIPT):
if not os.path.isfile(script):
sys.exit(f"[!] Required script not found: {script}")
target = classify_target(args.input)
if target["kind"] == "unknown":
sys.exit(f"[!] Could not classify target (not a local dir, not a parseable URL): {args.input}")
os.makedirs(args.output_dir, exist_ok=True)
print(f"Target : {args.input}")
print(f"Detected : {target['kind']}"
+ (f" (project: {target.get('project')})" if target.get("project") else "")
+ (f" (path: {target.get('path')})" if target.get("path") else ""))
print(f"Output : {os.path.abspath(args.output_dir)}")
rc1 = 0
if not args.skip_stage1:
rc1 = run(build_stage1_cmd(args), "Stage 1: Repo_analysis_tool")
if rc1 != 0:
print("[warn] Stage 1 returned a non-zero exit code; continuing to stage 2 anyway.")
rc2 = 0
stage2_csv = None
if not args.skip_stage2:
stage2_cmd, stage2_csv = build_stage2_cmd(args, target)
if target["kind"] in ("github", "gitlab") and not (args.github_token or args.gitlab_token):
print(f"[warn] Stage 2 will hit the {target['kind']} API without a token; "
"it may be rate limited. Pass --github-token / --gitlab-token or set the env var.")
rc2 = run(stage2_cmd, "Stage 2: llm_detector")
print("\n" + "=" * 70)
print("PIPELINE SUMMARY")
print("=" * 70)
print(f" Stage 1 exit : {rc1}{' (skipped)' if args.skip_stage1 else ''}")
print(f" Stage 2 exit : {rc2}{' (skipped)' if args.skip_stage2 else ''}")
print(f" Output dir : {os.path.abspath(args.output_dir)}")
if stage2_csv:
print(f" Stage 2 CSV : {stage2_csv}")
sys.exit(1 if (rc1 or rc2) else 0)
if __name__ == "__main__":
main()