-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsimulator.py
More file actions
380 lines (312 loc) · 13.1 KB
/
Copy pathsimulator.py
File metadata and controls
380 lines (312 loc) · 13.1 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
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
#!/usr/bin/env python3
"""
GitHub Contribution Simulator
Generates realistic GitHub commit history with natural patterns:
- More commits on weekdays, fewer on weekends
- Occasional burst days
- Realistic time distributions
- Configurable intensity profiles
"""
import random
import datetime
import json
from dataclasses import dataclass, asdict
from typing import List, Dict, Optional
from pathlib import Path
import argparse
@dataclass
class Commit:
"""Represents a single commit with timestamp and message."""
timestamp: datetime.datetime
message: str
def to_dict(self) -> Dict:
return {
'timestamp': self.timestamp.isoformat(),
'message': self.message
}
class ContributionProfile:
"""Defines commit behavior for different contributor intensities."""
PROFILES = {
'light': {
'daily_commit_prob': 0.4, # 40% chance of any commits on a given day
'weekend_reduction': 0.3, # 30% of normal activity on weekends
'avg_commits_per_day': 1.5, # Average when active
'max_commits_per_day': 4, # Cap for single day
'burst_probability': 0.05, # 5% chance of a burst day
'burst_multiplier': 2.5, # How much bursts increase commits
'commit_time_start': 9, # Earliest commit hour (9 AM)
'commit_time_end': 18, # Latest commit hour (6 PM)
},
'medium': {
'daily_commit_prob': 0.7,
'weekend_reduction': 0.5,
'avg_commits_per_day': 3,
'max_commits_per_day': 8,
'burst_probability': 0.1,
'burst_multiplier': 2.0,
'commit_time_start': 8,
'commit_time_end': 22,
},
'heavy': {
'daily_commit_prob': 0.9,
'weekend_reduction': 0.7,
'avg_commits_per_day': 5,
'max_commits_per_day': 15,
'burst_probability': 0.15,
'burst_multiplier': 1.8,
'commit_time_start': 7,
'commit_time_end': 23,
}
}
def __init__(self, intensity: str = 'medium'):
if intensity not in self.PROFILES:
raise ValueError(f"Intensity must be one of: {list(self.PROFILES.keys())}")
self.config = self.PROFILES[intensity]
self.intensity = intensity
def get_config(self, key: str):
return self.config[key]
class CommitMessageGenerator:
"""Generates realistic commit messages."""
PREFIXES = [
"feat", "fix", "docs", "style", "refactor",
"test", "chore", "perf", "build", "ci", "wip"
]
ACTIONS = [
"update", "add", "remove", "fix", "implement",
"refactor", "optimize", "improve", "clean up",
"rework", "simplify", "enhance", "adjust",
"tweak", "revise", "modify", "patch"
]
OBJECTS = [
"README", "config", "tests", "utils", "helpers",
"main module", "API", "docs", "dependencies",
"deployment", "CI config", "error handling",
"validation", "logging", "database", "models",
"routes", "components", "styles", "assets"
]
DETAILS = [
"for better performance", "to handle edge cases",
"based on feedback", "for compatibility",
"to fix bug", "as requested", "for clarity",
"per review comments", "to improve UX",
"to address security issue", "", "", "" # Empty for variety
]
@classmethod
def generate(cls) -> str:
"""Generate a random commit message."""
style = random.random()
if style < 0.4:
# Conventional commit style: feat: add something
prefix = random.choice(cls.PREFIXES)
action = random.choice(cls.ACTIONS)
obj = random.choice(cls.OBJECTS)
detail = random.choice(cls.DETAILS)
if detail:
return f"{prefix}: {action} {obj} {detail}"
return f"{prefix}: {action} {obj}"
elif style < 0.7:
# Simple action: Update something
action = random.choice(cls.ACTIONS).capitalize()
obj = random.choice(cls.OBJECTS)
return f"{action} {obj}"
else:
# More verbose: Fix issue with component
action = random.choice(cls.ACTIONS)
obj = random.choice(cls.OBJECTS)
detail = random.choice(cls.DETAILS)
if detail:
return f"{action} {obj} {detail}"
return f"{action} {obj}"
class ContributionSimulator:
"""Simulates GitHub contribution patterns over time."""
def __init__(
self,
start_date: datetime.date,
end_date: Optional[datetime.date] = None,
intensity: str = 'medium',
seed: Optional[int] = None
):
"""
Initialize the simulator.
Args:
start_date: First day to generate commits for
end_date: Last day (defaults to today)
intensity: 'light', 'medium', or 'heavy'
seed: Random seed for reproducibility
"""
self.start_date = start_date
self.end_date = end_date or datetime.date.today()
self.profile = ContributionProfile(intensity)
self.seed = seed
if seed is not None:
random.seed(seed)
def _should_commit_today(self, date: datetime.date) -> bool:
"""Determine if any commits should happen on this date."""
base_prob = self.profile.get_config('daily_commit_prob')
# Reduce probability on weekends
if date.weekday() >= 5: # Saturday = 5, Sunday = 6
base_prob *= self.profile.get_config('weekend_reduction')
return random.random() < base_prob
def _get_commit_count(self, date: datetime.date) -> int:
"""Determine how many commits for this date."""
avg = self.profile.get_config('avg_commits_per_day')
max_commits = self.profile.get_config('max_commits_per_day')
# Add some randomness around the average
count = max(1, int(random.gauss(avg, avg * 0.5)))
# Check for burst day
if random.random() < self.profile.get_config('burst_probability'):
count = int(count * self.profile.get_config('burst_multiplier'))
return min(count, max_commits)
def _generate_commit_times(self, date: datetime.date, count: int) -> List[datetime.datetime]:
"""Generate realistic timestamps for commits on a given day."""
start_hour = self.profile.get_config('commit_time_start')
end_hour = self.profile.get_config('commit_time_end')
times = []
for _ in range(count):
# Generate hour with bias toward mid-day/work hours
hour = self._biased_hour(start_hour, end_hour)
minute = random.randint(0, 59)
second = random.randint(0, 59)
dt = datetime.datetime(
date.year, date.month, date.day,
hour, minute, second
)
times.append(dt)
# Sort chronologically
times.sort()
return times
def _biased_hour(self, start: int, end: int) -> int:
"""
Generate hour with bias toward typical work hours.
More commits between 10-12 and 14-17.
"""
# Try a few times to get a work-hour-biased time
attempts = 0
while attempts < 10:
hour = random.randint(start, end)
# Peak hours get bonus probability
if 10 <= hour <= 12 or 14 <= hour <= 17:
if random.random() < 0.7: # 70% chance to accept peak hours
return hour
elif random.random() < 0.4: # 40% chance for off-peak
return hour
attempts += 1
return random.randint(start, end)
def generate_day(self, date: datetime.date) -> List[Commit]:
"""Generate all commits for a single day."""
if not self._should_commit_today(date):
return []
count = self._get_commit_count(date)
times = self._generate_commit_times(date, count)
commits = []
for dt in times:
message = CommitMessageGenerator.generate()
commits.append(Commit(timestamp=dt, message=message))
return commits
def simulate(self) -> List[Commit]:
"""
Run the full simulation from start to end date.
Returns:
List of Commit objects in chronological order
"""
all_commits = []
current_date = self.start_date
while current_date <= self.end_date:
day_commits = self.generate_day(current_date)
all_commits.extend(day_commits)
current_date += datetime.timedelta(days=1)
return all_commits
def get_stats(self, commits: List[Commit]) -> Dict:
"""Calculate statistics about generated commits."""
if not commits:
return {}
by_date = {}
by_weekday = {i: 0 for i in range(7)}
by_hour = {i: 0 for i in range(24)}
for commit in commits:
date_key = commit.timestamp.date()
by_date[date_key] = by_date.get(date_key, 0) + 1
by_weekday[commit.timestamp.weekday()] += 1
by_hour[commit.timestamp.hour] += 1
daily_counts = list(by_date.values())
return {
'total_commits': len(commits),
'active_days': len(by_date),
'avg_commits_per_active_day': len(commits) / len(by_date) if by_date else 0,
'max_commits_in_day': max(daily_counts) if daily_counts else 0,
'by_weekday': {
'Mon': by_weekday[0], 'Tue': by_weekday[1], 'Wed': by_weekday[2],
'Thu': by_weekday[3], 'Fri': by_weekday[4], 'Sat': by_weekday[5], 'Sun': by_weekday[6]
},
'by_hour': by_hour
}
def export_to_json(commits: List[Commit], filepath: str):
"""Export commits to JSON format."""
data = {
'commits': [c.to_dict() for c in commits],
'total': len(commits)
}
with open(filepath, 'w') as f:
json.dump(data, f, indent=2)
def main():
parser = argparse.ArgumentParser(
description='Simulate GitHub contribution history',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
%(prog)s --start 2020-01-01 --intensity medium
%(prog)s --start 2022-06-01 --end 2023-06-01 --intensity heavy --seed 42
%(prog)s --start 2021-01-01 --export commits.json
"""
)
parser.add_argument('--start', '-s', required=True,
help='Start date (YYYY-MM-DD)')
parser.add_argument('--end', '-e',
help='End date (YYYY-MM-DD, defaults to today)')
parser.add_argument('--intensity', '-i', choices=['light', 'medium', 'heavy'],
default='medium',
help='Contribution intensity level (default: medium)')
parser.add_argument('--seed', type=int,
help='Random seed for reproducible results')
parser.add_argument('--export', '-o',
help='Export commits to JSON file')
parser.add_argument('--stats-only', action='store_true',
help='Only show statistics, no commit list')
args = parser.parse_args()
# Parse dates
start_date = datetime.datetime.strptime(args.start, '%Y-%m-%d').date()
end_date = None
if args.end:
end_date = datetime.datetime.strptime(args.end, '%Y-%m-%d').date()
# Run simulation
simulator = ContributionSimulator(
start_date=start_date,
end_date=end_date,
intensity=args.intensity,
seed=args.seed
)
print(f"🎯 Simulating {args.intensity} contributor from {start_date} to {simulator.end_date}...")
commits = simulator.simulate()
# Show stats
stats = simulator.get_stats(commits)
print(f"\n📊 Statistics:")
print(f" Total commits: {stats['total_commits']}")
print(f" Active days: {stats['active_days']}")
print(f" Avg commits per active day: {stats['avg_commits_per_active_day']:.1f}")
print(f" Max commits in a day: {stats['max_commits_in_day']}")
print(f"\n📅 Commits by weekday:")
for day, count in stats['by_weekday'].items():
bar = '█' * int(count / max(stats['by_weekday'].values()) * 30) if max(stats['by_weekday'].values()) > 0 else ''
print(f" {day}: {count:4d} {bar}")
if not args.stats_only:
print(f"\n📝 First 10 commits:")
for commit in commits[:10]:
print(f" {commit.timestamp.strftime('%Y-%m-%d %H:%M')} - {commit.message}")
if len(commits) > 10:
print(f" ... and {len(commits) - 10} more")
# Export if requested
if args.export:
export_to_json(commits, args.export)
print(f"\n💾 Exported to {args.export}")
if __name__ == '__main__':
main()