@@ -84,7 +84,9 @@ Examples from my pains of the past two days
8484""" # noqa: E501
8585
8686import argparse
87+ import os
8788import re
89+ import subprocess
8890import sys
8991
9092try :
@@ -111,6 +113,80 @@ class ColoredHelpFormatter(argparse.RawDescriptionHelpFormatter):
111113 return help_text
112114
113115
116+ def get_github_context ():
117+ """Return (repo_toplevel, commit_sha, github_blob_base) or None.
118+
119+ Resolves git repository info and GitHub remote URL to construct
120+ blob URLs like https://github.com/owner/repo/blob/sha.
121+ """
122+ try :
123+ toplevel = (
124+ subprocess .check_output (
125+ ["git" , "rev-parse" , "--show-toplevel" ], stderr = subprocess .DEVNULL
126+ )
127+ .decode ()
128+ .strip ()
129+ )
130+ sha = (
131+ subprocess .check_output (
132+ ["git" , "rev-parse" , "HEAD" ], stderr = subprocess .DEVNULL
133+ )
134+ .decode ()
135+ .strip ()
136+ )
137+ except (subprocess .CalledProcessError , FileNotFoundError ):
138+ return None
139+
140+ # Determine remote name: tracking remote for current branch, fallback to origin
141+ remote_name = "origin"
142+ try :
143+ ref = (
144+ subprocess .check_output (
145+ ["git" , "symbolic-ref" , "-q" , "HEAD" ], stderr = subprocess .DEVNULL
146+ )
147+ .decode ()
148+ .strip ()
149+ )
150+ branch = ref .replace ("refs/heads/" , "" )
151+ remote_name = (
152+ subprocess .check_output (
153+ ["git" , "config" , f"branch.{ branch } .remote" ], stderr = subprocess .DEVNULL
154+ )
155+ .decode ()
156+ .strip ()
157+ )
158+ except (subprocess .CalledProcessError , FileNotFoundError ):
159+ pass
160+
161+ try :
162+ remote_url = (
163+ subprocess .check_output (
164+ ["git" , "config" , "--get" , f"remote.{ remote_name } .url" ],
165+ stderr = subprocess .DEVNULL ,
166+ )
167+ .decode ()
168+ .strip ()
169+ )
170+ except (subprocess .CalledProcessError , FileNotFoundError ):
171+ return None
172+
173+ # Parse SSH or HTTPS GitHub URLs
174+ # SSH: git@github.com:owner/repo.git
175+ m = re .match (r"git@github\.com[:/](.+?)(?:\.git)?$" , remote_url )
176+ if not m :
177+ # HTTPS: https://github.com/owner/repo.git
178+ m = re .match (r"https?://github\.com/(.+?)(?:\.git)?$" , remote_url )
179+ if not m :
180+ # gh: shorthand
181+ m = re .match (r"gh:(.+?)(?:\.git)?$" , remote_url )
182+ if not m :
183+ return None
184+
185+ owner_repo = m .group (1 )
186+ blob_base = f"https://github.com/{ owner_repo } /blob/{ sha } "
187+ return toplevel , sha , blob_base
188+
189+
114190def get_paths (lines ):
115191 """Generate indentation-based paths for given lines."""
116192 paths = []
@@ -162,6 +238,51 @@ def print_full_lines(paths, lines, use_color, prefix=""):
162238 printed .add (line_num )
163239
164240
241+ def print_inline_markdown (paths , lines , blob_url , use_color , prefix = "" ):
242+ """Print paths and matched lines in inline format with GitHub markdown links."""
243+ for line_num , path in paths :
244+ path_str = "." .join ([p [0 ] for p in path [:- 1 ]])
245+ line = lines [line_num ].rstrip ()
246+ # GitHub uses 1-indexed line numbers
247+ gh_line = line_num + 1
248+ path_part = f" `{ path_str } `" if path_str else ""
249+ if blob_url :
250+ text = f"**[`{ line_num } :`]({ blob_url } #L{ gh_line } ){ path_part } `{ line } `**"
251+ else :
252+ text = f"**`{ line_num } :`{ path_part } `{ line } `**"
253+ if use_color :
254+ text = colored (text , "red" )
255+ print (f"{ prefix } { text } " )
256+
257+
258+ def print_full_lines_markdown (paths , lines , blob_url , use_color , prefix = "" ):
259+ """Print paths and matched lines in full-lines format with GitHub markdown links."""
260+ printed = set ()
261+ for line_num , path in paths :
262+ for key , indent , i in path [:- 1 ]:
263+ if i not in printed :
264+ gh_i = i + 1
265+ context_line = lines [i ]
266+ if blob_url :
267+ text = f"[`{ i } `]({ blob_url } #L{ gh_i } ) `{ context_line } `"
268+ else :
269+ text = f"`{ i } ` `{ context_line } `"
270+ if use_color :
271+ text = colored (text , attrs = ["dark" ])
272+ print (f"{ prefix } { text } " )
273+ printed .add (i )
274+ gh_line = line_num + 1
275+ match_line = lines [line_num ]
276+ if blob_url :
277+ text = f"**[`{ line_num } :`]({ blob_url } #L{ gh_line } ) `{ match_line } `**"
278+ else :
279+ text = f"**`{ line_num } :` `{ match_line } `**"
280+ if use_color :
281+ text = colored (text , "red" )
282+ print (f"{ prefix } { text } " )
283+ printed .add (line_num )
284+
285+
165286def filter_paths (lines , regex = None , line_numbers = None ):
166287 """Get and filter indentation-based paths for given lines."""
167288 paths = get_paths (lines )
@@ -175,9 +296,7 @@ def filter_paths(lines, regex=None, line_numbers=None):
175296
176297 if line_numbers :
177298 nums = set (line_numbers )
178- paths = [
179- (line_num , path ) for line_num , path in paths if line_num in nums
180- ]
299+ paths = [(line_num , path ) for line_num , path in paths if line_num in nums ]
181300
182301 return paths
183302
@@ -221,16 +340,50 @@ def main():
221340 help = "How to annotate output with filename: 'auto' (prefix when multiple files),"
222341 " 'prefix' (prepend filename: to each line), 'name' (print filename header)" ,
223342 )
343+ parser .add_argument (
344+ "-D" ,
345+ "--decorations" ,
346+ choices = ["auto" , "color" , "none" , "github-markdown" ],
347+ default = "auto" ,
348+ help = "Decoration style: 'auto' (defers to --color), 'color' (force ANSI),"
349+ " 'none' (plain text), 'github-markdown' (markdown with GitHub blob links)" ,
350+ )
224351
225352 args = parser .parse_args ()
226353
354+ decorations = args .decorations
355+
227356 # Determine if color output should be used
228- use_color = args .color == "on" or (
229- args .color == "auto" and sys .stdout .isatty () and colored
230- )
357+ if decorations == "auto" :
358+ # Defer to --color logic (backward compatible)
359+ use_color = args .color == "on" or (
360+ args .color == "auto" and sys .stdout .isatty () and colored
361+ )
362+ elif decorations == "color" :
363+ use_color = True
364+ elif decorations == "none" :
365+ use_color = False
366+ elif decorations == "github-markdown" :
367+ # Colors on top of markdown when TTY
368+ use_color = sys .stdout .isatty () and colored is not None
369+ else :
370+ use_color = False
371+
231372 if use_color and not colored :
232373 raise RuntimeError ("Need 'termcolor' package to get colors" )
233374
375+ # Resolve GitHub context if needed
376+ github_context = None
377+ if decorations == "github-markdown" :
378+ github_context = get_github_context ()
379+ if github_context is None :
380+ print (
381+ "Error: --decorations=github-markdown requires a git repo"
382+ " with a GitHub remote" ,
383+ file = sys .stderr ,
384+ )
385+ sys .exit (1 )
386+
234387 # Compile regex if provided
235388 regex = re .compile (args .regex ) if args .regex else None
236389
@@ -257,17 +410,49 @@ def main():
257410 if not paths :
258411 continue
259412
260- prefix = f"{ fname } :" if filename_mode == "prefix" else ""
261-
262- if filename_mode == "name" :
263- print (fname )
264-
265- if args .format == "inline" :
266- print_inline (paths , lines , use_color , prefix = prefix )
267- elif args .format == "full-lines" :
268- print_full_lines (paths , lines , use_color , prefix = prefix )
413+ if decorations == "github-markdown" :
414+ # Compute blob URL for this file
415+ if filepath is not None and github_context is not None :
416+ toplevel , _sha , blob_base = github_context
417+ rel_path = os .path .relpath (os .path .abspath (filepath ), toplevel )
418+ blob_url = f"{ blob_base } /{ rel_path } "
419+ else :
420+ blob_url = None
421+
422+ # Build markdown prefix/header with hyperlinked filename
423+ if filename_mode == "prefix" :
424+ if blob_url :
425+ prefix = f"[`{ fname } `]({ blob_url } ):"
426+ else :
427+ prefix = f"`{ fname } `:"
428+ else :
429+ prefix = ""
430+
431+ if filename_mode == "name" :
432+ if blob_url :
433+ print (f"[`{ fname } `]({ blob_url } )" )
434+ else :
435+ print (f"`{ fname } `" )
436+
437+ if args .format == "inline" :
438+ print_inline_markdown (paths , lines , blob_url , use_color , prefix = prefix )
439+ elif args .format == "full-lines" :
440+ print_full_lines_markdown (
441+ paths , lines , blob_url , use_color , prefix = prefix
442+ )
443+ else :
444+ raise ValueError (args .format )
269445 else :
270- raise ValueError (args .format )
446+ prefix = f"{ fname } :" if filename_mode == "prefix" else ""
447+
448+ if filename_mode == "name" :
449+ print (fname )
450+ if args .format == "inline" :
451+ print_inline (paths , lines , use_color , prefix = prefix )
452+ elif args .format == "full-lines" :
453+ print_full_lines (paths , lines , use_color , prefix = prefix )
454+ else :
455+ raise ValueError (args .format )
271456
272457
273458if __name__ == "__main__" :
0 commit comments