
    }-j'{                        U d Z ddlZddlZddlZddlZddlZddlZddlm	Z	  ej
        e          ZddlmZmZmZmZmZmZ de	fdZde	fdZde	fdZdefd	Zd
edefdZddde	dededz  fdZdefdZdefdZ d
eddfdZ!defdZ"deddfdZ#de	de	fdZ$ e	d           e	d           e	d           e	d          fZ%e&e	df         e'd<   de&e	df         fdZ(de	de)fd Z*de&e	df         fd!Z+de&e	df         fd"Z,d:de	d#ee	z  dz  de)fd$Z-de	d%e	de)fd&Z.dee	         fd'Z/d;d*ed+e0dee         fd,Z1d:dee	z  dz  de	fd-Z2dedefd.Z3dede	fd/Z4d%e	d0ede	fd1Z5d<d3e	d4efd5Z6d3e	d4ede7fd6Z8d=d8Z9d3e	de7fd9Z:dS )>a{  
Hermes Web UI -- Workspace and file system helpers.

Workspace lists and last-used workspace are stored per-profile so each
profile has its own workspace configuration.  State files live at
``{profile_home}/webui_state/workspaces.json`` and
``{profile_home}/webui_state/last_workspace.txt``.  The global STATE_DIR
paths are used as fallback when no profile module is available.
    N)Path)WORKSPACES_FILELAST_WORKSPACE_FILEDEFAULT_WORKSPACEMAX_FILE_BYTES
IMAGE_EXTSMD_EXTSreturnc                      	 ddl m} m}  |             }|r,|dk    r& |            dz  }|                    dd           |S n*# t          $ r t
                              d           Y nw xY wt          j        S )zReturn the webui_state directory for the active profile.

    For the default profile, returns the global STATE_DIR (respects
    HERMES_WEBUI_STATE_DIR env var for test isolation).
    For named profiles, returns {profile_home}/webui_state/.
    r   )get_active_profile_nameget_active_hermes_homedefaultwebui_stateTparentsexist_okz8Failed to import profiles module, using global state dir)	api.profilesr   r   mkdirImportErrorloggerdebug_GLOBAL_WS_FILEparent)r   r   nameds       #/root/hermes-webui/api/workspace.py_profile_state_dirr      s    QPPPPPPPP&&(( 	DI%%&&((=8AGGD4G000H Q Q QOPPPPPQ!!s   ?A $A*)A*c                  $    t                      dz  S )z7Return the workspaces.json path for the active profile.zworkspaces.jsonr        r   _workspaces_filer"   1   s    "333r!   c                  $    t                      dz  S )z:Return the last_workspace.txt path for the active profile.zlast_workspace.txtr   r    r!   r   _last_workspace_filer$   6   s    "666r!   c                  0   	 ddl m}   |             }dD ]~}|                    |          }|ret          t	          |                                                                                    }|                                rt	          |          c S |                    di           }t          |t                    r|                    dd          }|rtt	          |          dvrct          t	          |                                                                                    }|                                rt	          |          S n1# t          t          f$ r t                              d           Y nw xY w	 dd	l m} t	          t          |                                                                                    S # t          $ rH t	          t          t                                                                                              cY S w xY w)
ui  Read the profile's default workspace from its config.yaml.

    Checks keys in priority order:
      1. 'workspace'         — explicit webui workspace key
      2. 'default_workspace' — alternate explicit key
      3. 'terminal.cwd'      — hermes-agent terminal working dir (most common)

    Falls back to the live DEFAULT_WORKSPACE from api.config.
    r   )
get_config)	workspacedefault_workspaceterminalcwd ).r+   z/Failed to load profile default workspace config)r   )
api.configr&   getr   str
expanduserresolveis_dir
isinstancedictr   	Exceptionr   r   r   _BOOT_DEFAULT_WORKSPACE)r&   cfgkeywspterminal_cfgr*   _LIVE_DEFAULT_WORKSPACEs           r   _profile_default_workspacer=   ;   s   H))))))jll5 	" 	"CB "RMM,,..668888:: "q66MMMwwz2..lD)) 	"""5"--C "s3xxy00SNN--//779988:: "q66M# H H HFGGGGGHIKKKKKK4/00;;==EEGGHHH I I I4/00;;==EEGGHHHHHIs-   BE B7E +E98E9=AG AHH
workspacesc                    t          j                    dz  dz                                  }g }| D ]}|                    dd          }|                    dd          }|s2t	          t          |                                                    }	 |                    |           	 ddlm}  |                                            }|                    |           n# t          t          f$ r Y w xY wn# t          $ r Y nw xY w|                                dk    rd	}|                    t          |          |d
           |S )a  Sanitize a workspace list:
    - Preserve saved paths even when they are currently missing or inaccessible;
      picker state must not be destroyed by a transient stat/permission failure.
    - Remove entries whose paths live inside another profile's directory
      (e.g. ~/.hermes/profiles/X/... should not appear on a different profile).
    - Rename any entry whose name is literally 'default' to 'Home' (avoids
      confusion with the 'default' profile name).
    Returns the cleaned list (may be empty).
    z.hermesprofilespathr+   r   r   )r   r   HomerA   r   )r   homer1   r.   _safe_resolver0   relative_tor   r   
ValueErrorr5   lowerappendr/   )	r>   hermes_profilesresultwrA   r   r:   r   own_profile_dirs	            r   _clean_workspace_listrN   c   st    y{{Y.;DDFFOF 6 6uuVR  uuVR   	$t**//1122	MM/***??????"8"8":":"B"B"D"Do....	*    /  	 	 	D	 ::<<9$$Ds1vvt445555Ms6   C7'7CC7C30C72C33C77
DDzPath does not exist)missing_label	candidaterO   c                   	 |                                  }nM# t          $ r
 | d|  cY S t          $ r}d|  d| dcY d}~S d}~wt          $ r}d|  d| dcY d}~S d}~ww xY wt          j        |j                  sd|  S dS )	a  Return a user-facing validation error for an unusable workspace path.

    ``Path.exists()`` can collapse permission/stat failures into a generic falsey
    result on some Python/OS combinations, which produced misleading "does not
    exist" messages for macOS/TCC-denied directories.  Probe with ``stat()`` so
    missing paths, non-directories, and permission-denied paths can be reported
    separately.
    z: zCannot access path: z7. The server process could not inspect this directory (z~). On macOS, grant Full Disk Access or Files and Folders permission to the Hermes/WebUI app or server process, then try again.Nz2. The server process could not inspect this path (z).zPath is not a directory: )statFileNotFoundErrorPermissionErrorOSErrorS_ISDIRst_mode)rP   rO   stexcs       r   _workspace_access_errorrZ      s   k^^ / / /..9..... 
 
 
]9 ] ]"] ] ]	
 	
 	
 	
 	
 	

  k k kjijjcfjjjjjjjjjk<
## 7696664s0    A!	A!AA!A!AA!A!c                  X   t          j                    sg S 	 t          j        t          j        d                    } t          |           }t          |          t          |           k    r+t          j        t          j        |dd          d           |S # t          $ r g cY S w xY w)a7  Read the legacy global workspaces.json, clean it, and return the result.

    This is the migration path for users upgrading from a pre-profile version:
    their global file may contain cross-profile entries, test artifacts, and
    stale paths accumulated over time.  We clean it in-place and rewrite it.
    utf-8encodingF   ensure_asciiindent)
r   existsjsonloads	read_textrN   len
write_textdumpsr5   )rawcleaneds     r   _migrate_global_workspacesrl      s     !## 	
j2GDDDEE',,w<<3s88##&
7qAAAG       			s   BB B)(B)c                     t                      } |                                 r	 t          j        |                     d                    }t          |          }t          |          t          |          k    rX	 |                     t          j        |dd          d           n*# t          $ r t                              d           Y nw xY w|pt                      ddgS # t          $ r t                              d	|            Y nw xY w	 d
dlm}  |            dv }n# t          $ r d}Y nw xY w|rt!                      }|r|S t                      ddgS )Nr\   r]   Fr_   r`   z(Failed to persist cleaned workspace listrB   rC   z!Failed to load workspaces from %sr   )r   )r   NT)r"   rc   rd   re   rf   rN   rg   rh   ri   r5   r   r   r=   r   r   r   rl   )ws_filerj   rk   r   
is_defaultmigrateds         r   load_workspacesrq      s     G~~ G	G*W...@@AAC+C00G7||s3xx''M&&
7qIIIT[ '     ! M M MLL!KLLLLLMV(B(D(DfUUVV 	G 	G 	GLL<gFFFFF	G
888888,,..2CC

   


 -// 	O/116BBCCsH   AC' <,B) (C' )$CC' CC' '%DDD& &D54D5c                     t                      }|j                            dd           |                    t	          j        | dd          d           d S )NTr   Fr_   r`   r\   r]   )r"   r   r   rh   rd   ri   )r>   rn   s     r   save_workspacesrs      sY      GN555tz*5KKKV]^^^^^r!   c                  l   t                      } |                                 rz	 |                     d                                          }|r#t	          |                                          r|S n+# t          $ r t                              d|            Y nw xY wt          j                    rx	 t          j        d                                          }|r#t	          |                                          r|S n*# t          $ r t                              d           Y nw xY wt                      S )Nr\   r]   z%Failed to read last workspace from %sz$Failed to read global last workspace)r$   rc   rf   stripr   r2   r5   r   r   _GLOBAL_LW_FILEr=   )lw_filer:   s     r   get_last_workspacerx      sI   "$$G~~ K	K!!7!3399;;A T!WW^^%%  	K 	K 	KLL@'JJJJJ	K  A	A)7;;;AACCA T!WW^^%%  	A 	A 	ALL?@@@@@	A%'''s%   AA2 2%BB1AC> >$D%$D%rA   c                     	 t                      }|j                            dd           |                    t	          |           d           d S # t
          $ r t                              d           Y d S w xY w)NTr   r\   r]   zFailed to set last workspace)r$   r   r   rh   r/   r5   r   r   )rA   rw   s     r   set_last_workspacerz      s    5&((TD9993t99w77777 5 5 534444445s   AA $A:9A:r:   c                 ^    	 |                                  S # t          t          f$ r | cY S w xY w)uK   Path.resolve() that never raises — falls back to the input path on error.)r1   rU   RuntimeErrorr:   s    r   rE   rE      s>    yy{{\"   s    ,,z/var/folders/private/var/foldersz/var/tmp/private/var/tmp._USER_TMP_PREFIXESc                     d} t                      }g }| D ]^}t          |          t          t          |                    fD ]0}||vr*|                    |           |                    |           1_t          |          S )u  System roots that must never be accepted as workspace candidates.

    Returns both the literal path and its symlink-resolved canonical form,
    deduped.  This matters on macOS where ``/etc``, ``/var``, and ``/tmp``
    are symlinks to ``/private/etc`` etc.  Without the resolved forms,
    callers that pass a ``.resolve()``-d candidate (every caller does)
    would compare ``/private/etc`` against literal ``Path('/etc')`` and the
    ``relative_to`` check would miss — letting ``/etc`` through as a
    registered workspace on macOS.

    Carve-outs for legitimate user-tmp paths nominally under these roots
    (e.g. ``/var/folders/.../T/`` on macOS) are handled by
    :func:`_is_blocked_system_path`, not by exclusion from this list.
    )z/etcz/usrz/varz/binz/sbinz/bootz/procz/sysz/devz/libz/lib64z/opt/homebrewz/Systemz/Library)setr   rE   addrI   tuple)_raw_seen_out_p_forms        r   _workspace_blocked_rootsr     s    D" uuED # #2hhd2hh 7 78 	# 	#EE!!		%   E"""	# ;;r!   c                     t           D ]}t          | |          r dS t                      D ]}t          | |          r dS dS )a)  Return True if *candidate* falls under a blocked system root.

    Honours :data:`_USER_TMP_PREFIXES` carve-outs so per-user tmp directories
    nominally under ``/var`` (``/var/folders`` on macOS, ``/var/tmp`` on
    Linux/macOS) remain valid workspace candidates and reachable file targets.
    FT)r   
_is_withinr   )rP   tmpblockeds      r   _is_blocked_system_pathr   ?  si     "  i%% 	55	+--  i)) 	44	5r!   c                  ,   t          t                                t          d          gz   } g }| D ]U}	 |                                                                }n# t
          $ r |}Y nw xY w||vr|                    |           Vt          |          S )Nz/private/etc)listr   r   r0   r1   r5   rI   r   )rootsresolvedrootr:   s       r   $_workspace_blocked_resolved_subtreesr   O  s    )++,,^0D0D/EEEH  	!!))++AA 	 	 	AAA	HOOA??s   &AA*)A*c                  |   t          d          t          d          g} t                      D ]a}	 |                     |                                                                           =# t
          $ r |                     |           Y ^w xY wg }| D ]}||vr|                    |           t          |          S )N//private/var)r   r   rI   r0   r1   r5   r   )r   r   uniques      r   _workspace_blocked_exact_rootsr   \  s    #YY^,,-E(**  	LL**22445555 	 	 	LL	F    vMM$==s   9A**BBraw_pathc                     d}|dvr5	 t          |                                          }n# t          $ r d}Y nw xY wt                      } |v s||t	                      v rdS t
          D ]'}t           |          s|t          ||          r dS (|$t	                      D ]}t          ||          r dS t          d          t          d          f}t                      D ]a}|t          d          k    r9 |k    r dS t           fd|D                       r:t           |          r dS Nt           |          r dS bdS )	a  Return True when candidate points at a known OS/system directory.

    Compare both the original spelling and the resolved path.  This closes the
    macOS /etc -> /private/etc bypass without globally banning temporary pytest
    paths under /private/var/folders.
    NNr+   TFr~   r   r   c              3   8   K   | ]}t          |          V  d S N)r   ).0allowedrP   s     r   	<genexpr>z-_is_blocked_workspace_path.<locals>.<genexpr>  s-      UUg:i11UUUUUUr!   )	r   r0   r5   r   r   r   r   r   any)rP   r   rj   exactr   r   allowed_private_vars   `      r   _is_blocked_workspace_pathr   j  s    Cz!!	x..++--CC 	 	 	CCC	 +,,EEco#9Q9S9S2S2St!  i%% 	#/jc>R>R/55 /11 	 	G#w'' tt   677>P9Q9QR799 
 
d>****G##ttUUUUATUUUUU )W-- tti)) 	44	5s   !+ ::r   c                 T    	 |                      |           dS # t          $ r Y dS w xY w)NTF)rF   rG   )rA   r   s     r   r   r     sB    t   uus    
''c                  $   g dt           t          z  d z  dd ffd}  | t          j                                | t                     t	                      D ] } | |                    d                     !                    d            S )NrP   r
   c                 N   | dv rd S 	 t          |                                                                           }n# t          $ r Y d S w xY w|                                r|                                sd S t          ||           rd S |vr                    |           d S d S )Nr   )r   r0   r1   r5   rc   r2   r   rI   )rP   r:   r   s     r   r   z%_trusted_workspace_roots.<locals>.add  s    
""F	Y**,,4466AA 	 	 	FF	xxzz 	 	F%a33 	FE>>LLOOOOO >s   3= 
A
ArA   c                 :    t          t          |                     S r   rg   r/   r}   s    r   <lambda>z*_trusted_workspace_roots.<locals>.<lambda>  s    SQ[[ r!   r8   )r/   r   rD   r6   rq   r.   sort)r   rL   r   s     @r   _trusted_workspace_rootsr     s    EsTzD( T       C	C     AEE&MM	JJ((J)))Lr!   r+      prefixlimitc                    t                      }|sg S | pd                                }|sd |d|         D             S |                    d          r"t          |                                          }nGt          |                                          rt          |          }nt          j                    |z  }t          |                                          }g dt          ddffd}|D ]A}t          |                                                              |          r ||           Bfd|D             }|s
d|         S t          |d	 
          }	|
                    t          j                  p|
                    d          }
|
r|n|j        }|
rdn|j        }|                    d          }	 |                                                                }n# t           $ r d|         cY S w xY w|                                r|                                s
d|         S t'          ||	          s
d|         S |                                }	 t)          |                                d 
          }n# t,          $ r d|         cY S w xY w|D ]}|                                s|j                            d          r|s4|r-|j                                                            |          sc ||                                           t/                    |k    r nd|         S )a\  Return workspace path suggestions under trusted roots only.

    Suggestions are limited to directories under one of:
      - Path.home()
      - the boot default workspace
      - already-saved workspace roots

    Arbitrary system prefixes return an empty list rather than an error so the
    UI can safely autocomplete while the user types.
    r+   c                 ,    g | ]}t          |          S r    )r/   )r   r:   s     r   
<listcomp>z.list_workspace_suggestions.<locals>.<listcomp>  s    ...1A...r!   N~rA   r
   c                 \    t          |           }|vr                    |           d S d S r   )r/   rI   )rA   valuesuggestionss     r   r   z'list_workspace_suggestions.<locals>.add  s;    D		##u%%%%% $#r!   c                     g | ]F}t          |          k    s/                    t          |          t          j        z             D|GS r    )r/   
startswithossep)r   r   
normalizeds     r   r   z.list_workspace_suggestions.<locals>.<listcomp>  sT       T""j&;&;CII<N&O&O" 	"""r!   c                 :    t          t          |                     S r   r   r}   s    r   r   z,list_workspace_suggestions.<locals>.<lambda>  s    SQ[[ r!   r   r   r,   c                 4    | j                                         S r   )r   rH   r}   s    r   r   z,list_workspace_suggestions.<locals>.<lambda>  s    16<<>> r!   )r   ru   r   r   r0   is_absoluterD   r/   rH   maxendswithr   r   r   r   r1   r5   rc   r2   r   sortediterdirrU   rg   )r   r   r   rj   targetnormalized_lowerr   r   in_rootanchor_rootends_with_sepr   leafshow_hiddenparent_resolved
leaf_lowerchildrenchildr   r   s                     @@r   list_workspace_suggestionsr     s    %&&E 	<R


 
 C /..fuf....
~~c #c%%''	c			 	  #cs"VJ!''))K&$ &4 & & & & & &   t99??''(899 	CIII     G
  #6E6""g#8#8999KLL((=CLL,=,=M$7VV&-F/22FKD//#&&K# ++--5577 # # #6E6""""# !!## #?+A+A+C+C #6E6""o{33 #6E6""J#/11339Q9QRRR # # #6E6""""#  	 	||~~ 	:  %% 	k 	 	ej..00;;JGG 	EMMOO{u$$E %vvs$   &G* *H H%$J
 
J! J!c                 n   | dv r8t          t                                                                                    S t          |                                                                           }t	          |          }|rt          |          t          j                                                    }|t          d          k    r(	 |                    |           |S # t
          $ r Y nw xY wt          ||           rt          d|           	 t                      }d |D             }||v r|S n# t          $ r Y nw xY w	 t          t                                                                                    }|                    |           |S # t
          $ r Y nw xY wt          d| d          )u  Resolve and validate a workspace path.

    A path is trusted if it satisfies at least one of:
      (A) It is under the user's home directory (Path.home()).
          Works cross-platform: ~/... on Linux/macOS, C:\Users\... on Windows.
      (B) It is already in the profile's saved workspace list.
          This covers self-hosted deployments where workspaces live outside home
          (e.g. /data/projects, /opt/workspace) — once a workspace is saved by
          an admin, it can be reused without re-validation.

    Additionally enforced regardless of (A)/(B):
      1. The path must exist.
      2. The path must be a directory.
      3. The path must not be a known system root (/etc, /usr, /var, /bin, /sbin,
         /boot, /proc, /sys, /dev, /root on Linux/macOS; Windows system dirs).
         This prevents even admin-saved workspaces from pointing at OS internals.

    None/empty path falls back to the boot-time DEFAULT_WORKSPACE, which is always
    trusted (it was validated at server startup).
    r   r   #Path points to a system directory: c                     h | ]>}|                     d           t          |d                                                    ?S )rA   )r.   r   r1   )r   rL   s     r   	<setcomp>z,resolve_trusted_workspace.<locals>.<setcomp>=  s=    QQQQ155==QtAfI..00QQQr!   zoPath is outside the user home directory, not in the saved workspace list, and not under the default workspace: u+   . Add it via Settings → Workspaces first.)r   r6   r0   r1   rZ   rG   rD   rF   r   rq   r5   )rA   rP   access_error_homesavedsaved_pathsboot_defaults          r   resolve_trusted_workspacer     s   * z+,,7799AACCCT

%%''//11I*955L '&&& IKK!!ES			!!%((( 	 	 	D	 ")T22 LJyJJKKK!!QQ%QQQ## $   344??AAIIKKl+++    	56?	5 	5 	5  s7   	C   
C-,C-D4 4
E EAF 
F! F!c                     |                                  }t          |          dk    r&|d         |d         k    r|d         dv r
|dd         S |S )ux  Strip a single pair of surrounding single or double quotes from a path string.

    macOS Finder's "Copy as Pathname" (Cmd+Option+C) returns paths wrapped in
    single quotes, e.g. ``'/Users/x/Documents/foo'``. Other shells and OS file
    managers do similar things with double quotes. Users routinely paste these
    quoted strings into the Add Space input expecting them to "just work" —
    the only reason they didn't was a missing strip.

    Only paired quotes are stripped (matching opener and closer). One-sided quotes
    are preserved on the slim chance a path legitimately contains a literal quote
    character.
    r_   r   )'"   )ru   rg   )rA   ss     r   _strip_surrounding_quotesr   X  sP     	

A
1vv{{qtqu}}1););2wHr!   c                    t          |           } t          |                                                                           }t	          |          }|rt          |          t          j                                                    }|t          d          k    rt          ||          r|S t          ||           rt          d|           |S )u  Validate a path for *adding* to the workspace list (less restrictive than resolve_trusted_workspace).

    When a user explicitly adds a new workspace path, we trust their intent — they
    have console or filesystem access to that path and are consciously registering it.
    We only block: non-existent paths, non-directories, and known system roots.

    The stricter ``resolve_trusted_workspace`` is used when *using* an existing workspace
    (file reads/writes) to prevent path traversal after the list is built.

    Surrounding quotes (single or double) are stripped before validation —
    macOS Finder's "Copy as Pathname" wraps paths in single quotes by default,
    and users routinely paste those into the Add Space input.
    r   r   )	r   r   r0   r1   rZ   rG   rD   r   r   )rA   rP   r   r   s       r   validate_workspace_to_addr   k  s     %T**DT

%%''//11I*955L '&&& IKK!!ES		jE:: ")T22 LJyJJKKKr!   	requestedc                    ddl }| |z  }|                                }	 |                    |                                            |S # t          $ r Y nw xY wt	          |j                            t          |                              }	 |                    |            n # t          $ r t          d|           w xY wt          |          rt          d|           |S )u  Resolve a relative path inside a workspace root, raising ValueError on traversal.

    Symlinks whose *unresolved* path is within the workspace root are allowed —
    the user placed them there intentionally.  Only raw ``..`` traversal outside
    the root is blocked.
    r   NzPath traversal blocked: z%Path traversal blocked (system dir): )	r   r1   rF   rG   r   rA   normpathr/   r   )r   r   r   
unresolvedr   norms         r   safe_resolve_wsr     s    III	!J!!##HT\\^^,,,      Z1122DA A A A?I??@@@A x(( NLLLMMMOs   (A 
AAB# #C r,   r'   relc                    t          | |          }|                                st          d|           |                                 }g }t	          |                                d           D ]}|                                r>	 |                                }n# t          $ r Y :w xY w||                                k    s||k    s||k    rc	 |                                                    |           # t          $ r Y nw xY wt          |          r|                                }t          t          |j                            }|r|dk    r|dz   |z   }|j        |dt          |          |d}	|s3	 |                                j        |	d<   n# t          $ r d |	d<   Y nw xY w|                    |	           n~|j        }
|r|dk    r|dz   |j        z   }
|                    |j        |
|                                rd	nd
|                                r|                                j        nd d           t%          |          dk    r n|S )NzNot a directory: c                     |                                   |                                 | j                                        fS r   )
is_symlinkis_filer   rH   r}   s    r   r   zlist_dir.<locals>.<lambda>  s0    ALLNN8JAIIKKYZY_YeYeYgYg7h r!   r   r,   r   symlink)r   rA   typer   r2   sizedirfile)r   rA   r   r      )r   r2   rS   r1   r   r   r   rU   rF   rG   r   r/   r   r   rR   st_sizerI   r   rg   )r'   r   r   ws_resolvedentriesitemlink_targetr2   display_pathentry
entry_paths              r   list_dirr     s   Y,,F==?? ; 9C 9 9:::##%%KGv~~''-h-hiii 5 5?? 2	"llnn   
 v~~////;&3H3H"k11  ,,[999    '{33  ''))FtDI//L 8sczz"Sy<7	$!k**  E  ))$/$4$4$6$6$>E&MM ) ) )$(E&MMM)NN5!!!! J 3sczz 3Y2
NN	"!%:F/3||~~G		++4	     w<<3E Ns6   
B
B,+B,'C==
D
	D
?FF.-F.c                 T   t          | |          }|                                st          d|           |                                j        }|t
          k    rt          d| dt
           d          |                    dd          }||||                    d          d	z   d
S )NzNot a file: zFile too large (z bytes, max )r\   replace)r^   errors
r   )rA   contentr   lines)	r   r   rS   rR   r   r   rG   rf   count)r'   r   r   r   r  s        r   read_file_contentr    s    Y,,F>> 6 4s 4 4555;;== DnODOOnOOOPPP	BBGGTGMMRVDWDWZ[D[\\\r!      c                     	 t          j        dg| z   t          |          dd|          }|j        dk    r|j                                        ndS # t           j        t          t          f$ r Y dS w xY w)z8Run a git command and return stdout, or None on failure.gitT)r*   capture_outputtexttimeoutr   N)	
subprocessrunr/   
returncodestdoutru   TimeoutExpiredrS   rU   )argsr*   r  rs       r   _run_gitr    s    NGdNCw
 
 
 $%<1#4#4qx~~$>%'8'B   tts   AA A21A2c                      dz                                   sdS t          g d           }|dS  fd} fd} fd}t          j                            d          5 }|                    |          }|                    |          }|                    |          }|                                \  }	}
}|                                }|                                }ddd           n# 1 swxY w Y   ||	|
|||d	d
S )zEReturn git info for a workspace directory, or None if not a git repo.z.gitN)z	rev-parsez--abbrev-refHEADc                  v    t          g d          } | r#|                                 rt          |           ndS )N)rev-list--countz
@{u}..HEADr   r  isdigitintr  r'   s    r   _aheadz&git_info_for_workspace.<locals>._ahead  ;    :::IFF1qyy{{1s1vvv1r!   c                  v    t          g d          } | r#|                                 rt          |           ndS )N)r  r  z
HEAD..@{u}r   r  r  s    r   _behindz'git_info_for_workspace.<locals>._behind  r   r!   c                      t          ddg          pd} d |                                 D             }t          d |D                       }t          d |D                       }t          |          ||fS )Nstatusz--porcelainr+   c                     g | ]}||S r    r    r   ls     r   r   z;git_info_for_workspace.<locals>._status.<locals>.<listcomp>  s    222q2222r!   c              3   h   K   | ]-}t          |          d k    |d         dv s
|d         dv )dV  .dS )r_   r   MARr   N)rg   r&  s     r   r   z:git_info_for_workspace.<locals>._status.<locals>.<genexpr>  sE      \\Q3q66Q;;AaDEMMQqTUZ]]q]]]]\\r!   c              3   D   K   | ]}|                     d           dV  dS )z??r   N)r   r&  s     r   r   z:git_info_for_workspace.<locals>._status.<locals>.<genexpr>  s3      ??aALL,>,>???????r!   )r  
splitlinessumrg   )outr  modified	untrackedr'   s       r   _statusz'git_info_for_workspace.<locals>._status  s    -0)<<B22CNN,,222\\%\\\\\??5?????	5zz8Y..r!   r  )max_workersT)branchdirtyr.  r/  aheadbehindis_git)rc   r  
concurrentfuturesThreadPoolExecutorsubmitrK   )r'   r2  r  r"  r0  poolf_statusf_aheadf_behindr3  r.  r/  r4  r5  s   `             r   git_info_for_workspacer?    s   &&(( t;;;YGGF~t
2 2 2 2 22 2 2 2 2/ / / / / 
		.	.1	.	=	= #;;w'';;v&&;;w''%-__%6%6"x!!""# # # # # # # # # # # # # # #   s    B C,,C03C0r   )r+   r   )r,   )r  );__doc__rd   loggingr   rR   r  concurrent.futuresr7  pathlibr   	getLogger__name__r   r-   r   r   r   rv   r   r6   r   r   r	   r   r"   r$   r/   r=   r   rN   rZ   rl   rq   rs   rx   rz   rE   r   r   __annotations__r   boolr   r   r   r   r   r   r  r   r   r   r   r   r   r4   r  r  r?  r    r!   r   <module>rH     sC       				               		8	$	$               "D " " " "&4$ 4 4 4 4
7d 7 7 7 7
#IC #I #I #I #IP%d %t % % % %P F[   t s _bei_i    4D    ,D D D D DB_ _ _ _ _ _(C ( ( ( ((5S 5T 5 5 5 5T d     	DD	  DD		( E$)$   '%c	"2 ' ' ' 'Tt      
eD#I.> 
 
 
 
dCi(8    , ,$ ,#*t:K ,W[ , , , ,^T  $    $t*    2Q Qs Q QT#Y Q Q Q QhE EC$J$5 E E E E ETC C    &C D    B$ 3 4    @< < <3 < < < <~] ]C ]D ] ] ] ]	 	 	 	&d &t & & & & & &r!   