@@ -41,31 +41,29 @@ def git_subjects_for_date_and_path(date_str, path):
4141 return []
4242 return [line .strip () for line in out .splitlines () if line .strip ()]
4343
44- def latest_nonbot_commit_date_for_path (path ):
44+ def latest_nonbot_commit_for_path (path ):
4545 """
46- 해당 path에 대한 '비봇' 커밋 중 가장 최근 커밋의 날짜(YYYY-MM-DD, KST 기준 )를 반환.
47- 없으면 None.
46+ 해당 path에 대한 '비봇' 커밋 중 가장 최근 항목의 (날짜, 커밋해시 )를 반환.
47+ 없으면 ( None, None) .
4848 """
49- cmd = f'git log --pretty="%ad%x09%s" --date=format-local:"%Y-%m-%d" -- "{ path } " || true'
49+ # 날짜는 KST 로컬 포맷, 해시는 별도로 얻기 위해 %H 추가
50+ cmd = f'git log --pretty="%H%x09%ad%x09%s" --date=format-local:"%Y-%m-%d" -- "{ path } " || true'
5051 out = run (cmd )
5152 if not out :
52- return None
53+ return None , None
5354 for line in out .splitlines ():
5455 line = line .strip ()
5556 if not line :
5657 continue
57- if "\t " in line :
58- date_part , subject = line .split ("\t " , 1 )
59- else :
60- parts = line .split (" " , 1 )
61- if len (parts ) != 2 :
62- continue
63- date_part , subject = parts
58+ parts = line .split ("\t " , 2 )
59+ if len (parts ) != 3 :
60+ continue
61+ commit_hash , date_str , subject = parts
6462 subject = subject .strip ()
6563 if BOT_REGEX .match (subject ):
6664 continue
67- return date_part
68- return None
65+ return date_str , commit_hash
66+ return None , None
6967
7068def commit_flag (date_str , name ):
7169 """
@@ -79,58 +77,74 @@ def commit_flag(date_str, name):
7977 for s in subjects_today :
8078 if not BOT_REGEX .match (s ):
8179 return "O"
82- nonbot_any_date = latest_nonbot_commit_date_for_path (path )
83- if nonbot_any_date is not None and nonbot_any_date != date_str :
80+ nonbot_date , _ = latest_nonbot_commit_for_path (path )
81+ if nonbot_date is not None and nonbot_date != date_str :
8482 return "L"
8583 return "X"
8684
87- # ---------- 파일 개수(그 날짜 스냅샷; 재귀 카운트 ) ----------
85+ # ---------- 스냅샷 & 파일 개수 (재귀 ) ----------
8886
8987def commit_at_end_of_date (date_str ):
9088 """해당 날짜(KST 23:59:59)의 리포 스냅샷 커밋 해시 반환(없으면 빈 문자열)"""
9189 until = f'{ date_str } 23:59:59 { TZ_OFFSET } '
9290 cmd = f'git rev-list -1 --before="{ until } " HEAD || true'
9391 return run (cmd ).strip ()
9492
95- def file_count_in_path_at_commit (commit , path ):
93+ def count_files_recursive_at_commit (commit , base_dir ):
9694 """
97- 특정 커밋에서 path/ 디렉터리 '아래 전체(재귀)' 파일(=blob) 개수와 .gitkeep 존재 여부 반환.
98- - git ls-tree -r --name-only {commit} -- "{base}" 로 모든 파일을 받고 base로 시작하는 것만 필터.
95+ 특정 커밋에서 base_dir/ 아래의 모든 파일(=blob) 리스트와 .gitkeep 존재 여부 반환.
96+ - 출력: (파일경로목록(list[str]), has_gitkeep(bool))
9997 """
10098 if not commit :
101- return 0 , False
102-
103- base = path .rstrip ("/" ) + "/"
99+ return [], False
100+ base = base_dir .rstrip ("/" ) + "/"
104101 cmd = f'git ls-tree -r --name-only { commit } -- "{ base } " || true'
105102 out = run (cmd )
106103 if not out :
107- return 0 , False
104+ return [] , False
108105
109106 files = []
110107 has_gitkeep = False
111108 for line in out .splitlines ():
112109 name = line .strip ()
113- if not name :
114- continue
115- if not name .startswith (base ):
110+ if not name or not name .startswith (base ):
116111 continue
117112 files .append (name )
118113 if os .path .basename (name ) == ".gitkeep" :
119114 has_gitkeep = True
115+ return files , has_gitkeep
120116
121- return len (files ), has_gitkeep
122-
123- def file_req_and_status (date_str , name ):
117+ def snapshot_for_display_and_goal (date_str , name , cf ):
124118 """
125- (파일개수, 목표개수, 충족여부) 반환.
126- 목표: .gitkeep 있으면 4, 없으면 3 (해당 날짜 23:59:59 KST 스냅샷 기준)
119+ 표시/판정에 사용할 '스냅샷 커밋'을 선택하고, 그 스냅샷에서:
120+ - 표시용 파일 개수(display_n): .gitkeep 제외
121+ - 목표값(m): .gitkeep 있으면 4, 없으면 3
122+ - 충족 여부(ok): (표시용이 아니라 실제 전체 파일수 >= m)
123+ 반환: (display_n, m, ok)
127124 """
128- commit = commit_at_end_of_date (date_str )
129125 path = f"{ date_str } /{ name } "
130- cnt , has_gitkeep = file_count_in_path_at_commit (commit , path )
131- required = 4 if has_gitkeep else 3
132- ok = cnt >= required
133- return cnt , required , ok
126+
127+ if cf == "O" :
128+ # 같은 날: 그날 끝 스냅샷
129+ commit = commit_at_end_of_date (date_str )
130+ elif cf == "L" :
131+ # 레트로: 최신 비봇 커밋 스냅샷
132+ _ , commit = latest_nonbot_commit_for_path (path )
133+ if not commit :
134+ # 안전망: 없으면 그날 스냅샷 시도
135+ commit = commit_at_end_of_date (date_str )
136+ else : # 'X'
137+ # 비봇 커밋 없으면 그날 스냅샷으로 계산(대개 0)
138+ commit = commit_at_end_of_date (date_str )
139+
140+ files , has_gitkeep = count_files_recursive_at_commit (commit , path )
141+ total_including_gitkeep = len (files )
142+ # 표시용 개수는 .gitkeep 제외
143+ display_n = total_including_gitkeep - (1 if any (os .path .basename (f ) == ".gitkeep" for f in files ) else 0 )
144+ # 목표값 산정
145+ m = 4 if has_gitkeep else 3
146+ ok = total_including_gitkeep >= m
147+ return display_n , m , ok
134148
135149# ---------- 달력 렌더링 ----------
136150
@@ -184,23 +198,24 @@ def build_month_calendar(year, month, today_kst):
184198 lines = []
185199 for name in NAMES :
186200 cf = commit_flag (date_str , name ) # 'O','L','X'
187- cnt , req , ok = file_req_and_status (date_str , name )
201+ display_n , m , ok = snapshot_for_display_and_goal (date_str , name , cf )
188202
189203 if cf == "O" :
190204 dot = DOT_GREEN if ok else DOT_YELLOW
191205 elif cf == "L" :
192206 dot = DOT_ORANGE if ok else DOT_YELLOW
193207 else :
194208 dot = DOT_RED
209+ # cf == 'X'일 땐 ok 의미가 없지만, 표시 일관성을 위해 (n/m) 그대로 둠.
195210
196- pass_icon = "✅" if ok else "❌ "
211+ # 👉 표시를 심플하게: "이름: 🟡 (n/m) "
197212 lines .append (
198213 f"<div style='font-size:13px'>{ name } : { dot } "
199- f"<span style='font-size:12px'> (<code>{ cnt } /{ req } </code> { pass_icon } )</span> </div>"
214+ f"(<code>{ display_n } /{ m } </code>) </div>"
200215 )
201216
202217 cell_html = (
203- '<td align="center" valign="top" style="min-width:170px ">'
218+ '<td align="center" valign="top" style="min-width:150px ">'
204219 f'<div align="right"><sub>{ d } </sub></div>'
205220 + "" .join (lines ) +
206221 "</td>"
@@ -215,8 +230,7 @@ def build_month_calendar(year, month, today_kst):
215230 "🟠=다른날 커밋+목표달성, "
216231 "🟡=커밋있음+목표미달, "
217232 "🔴=커밋없음 · "
218- "<code>n/m</code>=파일개수/목표(.gitkeep 있으면 m=4, 없으면 m=3)"
219- "</sub>"
233+ "(표시 n은 .gitkeep 제외)</sub>"
220234 )
221235 table_html = (
222236 f"{ month_title } \n \n "
0 commit comments