o k`6@sddlZddlZddlZddlZddlZddlZddlZddlZddlm Z ddl m Z ddlm Z m Z ddlmZddlmZddlmZddlmZdd lmZdd lmZmZgd ZGd d d eZGdddeZddZed dZ!e"dZ#e$ddD]\Z%Z&e&e#e%<q[%[&e'e#Z#ddZ(ddZ)ddZ*Gddde+Z,Gdd d e-Z.Gd!d"d"e+Z/Gd#d$d$e+Z0dS)%N)deque)partial)DEVNULLPIPE)Lock) TracBaseError) terminate) close_fds)time_now)exception_to_unicode to_unicode)GitError GitErrorShaStorageStorageFactoryc@ eZdZdS)r N__name__ __module__ __qualname__rrB/usr/lib/python3/dist-packages/tracopt/versioncontrol/git/PyGIT.pyr %r c@r)rNrrrrrr(rrcCs|st|}|s t|d}i}d}}|ra|ddkr4|s*|}||dg}||ddn|dd\}}||g||d}|r_|rT||kr_d|||d<d}|sd||fS)zParse the raw content of a commit (as given by `git cat-file -p `). Return the commit message and a dict of properties. rN  )r splitlinespopappendsplit setdefaultstripjoin)rawlineslineprops multiline multiline_keykeyvaluerrr parse_commit-s,    r,z\\(?:[abtnvfr"\\]|[0-7]{3})utf-8s abtnvfr"\s  "\cCs4|dr|drdd}t||dd}|S)N"cSsD|d}t|dkrt|ddd}d|St|d}d|S)Nrrs%c)grouplenint_unquote_chars)matchcoderrrreplaceTs   z_unquote..replacerr) startswithendswith _unquote_resub)pathr8rrr_unquoteRsr>cCs|dur t|d}|SNascii)strrevrrr_rev_u_ rDcCs|dur |d}|Sr?encoderBrrr_rev_berErHc@steZdZdZ  dddZddZdd Zd d Zd d ZddZ ddZ ddZ ddZ e dZeddZdS)GitCorez'Low-level wrapper around git executableNgitcCs||_||_||_||_dSN)_GitCore__git_bin_GitCore__git_dir _GitCore__log_GitCore__fs_encoding)selfgit_dirgit_binlog fs_encodingrrr__init__ns zGitCore.__init__cCsd|j|jfS)Nz)rLrMrPrrr__repr__uszGitCore.__repr__csv|jg}|jr|d|j|||||jdur9tjdkr,fdd}nfdd}tt||}|S)z9construct command tuple for git call suitable for Popen()z --git-dir=%sNntct|tr |d}|SNr8) isinstancebytesdecodeargrTrrto_cmd_encoding  z0GitCore.__build_git_cmd..to_cmd_encodingcrYrZ)r[rArGr^r`rrrarb) rLrMrextendrOosnamelistmap)rPgitcmdargscmdrarr`r__build_git_cmdys    zGitCore.__build_git_cmdcOsH|dt|dt|dttj|j|g|Rfdti|S)Nstdinstdoutstderrr )r!r subprocessPopen_GitCore__build_git_cmdr )rPgit_cmdcmd_argskwrrr__pipes   zGitCore.__pipecGsp|j|dti}|\}}Wdn1swY|jr6|jdks(|r6|jd|j|j|j|||S)z9execute git command and return file-like object of stdoutrlNrz/%s exits with %d, dir: %r, args: %r, stderr: %r)_GitCore__piper communicaterN returncodedebugrLrM)rPrip stdout_data stderr_datarrr __executes zGitCore.__executecCs |ddS)Nzcat-filez--batchrvrVrrrcat_file_batch zGitCore.cat_file_batchcGs|jdg|RS)NrSr~)rPrsrrrlog_pipeszGitCore.log_pipecCs|ddddddS)Nz diff-treez--stdin--root-z-r-Mr~rVrrrdiff_tree_pipeszGitCore.diff_tree_pipecCs.|ds |dvr t|t|j|ddS)N_)rrr-)r9AttributeErrorr_GitCore__executer8)rPrerrr __getattr__s zGitCore.__getattr__s[0-9A-Fa-f]{4,40}$cCs0dt|kr dksdSdSt|j|S)znreturns whether sha is a potential sha id (i.e. proper hexstring between 4 and 40 characters) r0(F)r3bool_GitCore__is_sha_patr6)clssharrris_shas zGitCore.is_sha)NrJNN)rrr__doc__rUrWrqrvrrrrrrecompiler classmethodrrrrrrIks   rIc@s*eZdZdZd ddZddZddZd S) SizedDictz6Size-bounded dictionary with FIFO replacement strategyrcCs$t|||_t|_t|_dSrK)dictrU_SizedDict__max_sizer_SizedDict__key_fifor_SizedDict__lock)rPmax_sizerrrrUs  zSizedDict.__init__cCs|jJt|t|jksJ||s|j|t|||}t|j|jkr9||j t|j|jks)t|t|jksDJ|WdS1sPwYdSrK) rr3r __contains__rr __setitem__r __delitem__popleft)rPrer+rcrrrrs  $zSizedDict.__setitem__cGstd)Nz$SizedDict has no setdefault() method)NotImplementedError)rPrrrrr!zSizedDict.setdefaultN)r)rrrrrUrr!rrrrrs   rc@sPeZdZeZiZiZeZ  d ddZ ddZ e dd Z e d d ZdS) rTrJNc Cs||_|jE|rz|j|=Wn tyYnwz|j|}Wnty<|j|}t|||||}||j|<Ynw|sD||j|<Wdn1sNwY||_|j d|r^dnd|dS)Nz,requested %s PyGIT.Storage instance for '%s'weakznon-weak) logger_StorageFactory__dict_lock_StorageFactory__dict_nonweakKeyError_StorageFactory__dict_StorageFactory__dict_rev_cachegetr_StorageFactory__instry)rPreporSrrRgit_fs_encodingi rev_cacherrrrUs.      zStorageFactory.__init__cCs|jSrK)rrVrrr getInstanceszStorageFactory.getInstancecCs6|j||j|<WddS1swYdSrK)rr)rrrrrr set_rev_caches "zStorageFactory.set_rev_cachecCsJ|j|j|j|jWddS1swYdS)zFor testing purpose onlyN)rrclearrrrrrr_clean s    "zStorageFactory._clean)TrJN)rrrweakrefWeakValueDictionaryrrrrrrUrrrrrrrrrs  rc@seZdZdZdZGdddeZeddZedhdd Z did d Z d dZ ddZ ddZ eddZdjddZddZddZddZddZd d!Zd"d#Zd$d%Zdjd&d'Zd(d)Zd*d+Zd,d-Zd.d/Zd0d1Zd2d3Zd4d5Zd6d7Z dkd9d:Z!d;d<Z"dld=d>Z#dmd@dAZ$dBdCZ%dDdEZ&dFdGZ'dHdIZ(dldJdKZ)dLdMZ*dNdOZ+dPdQZ,e-j.dRdSZ/dldTdUZ0dldVdWZ1dXdYZ2dZd[Z3d\d]Z4d^d_Z5dmd`daZ6dbdcZ7dddeZ8dfdgZ9d S)nrz8High-level wrapper around GitCore with in-memory cachingr0c@s<eZdZdZddZeddZddZdd Zd d Z d S) zStorage.RevCache) youngest_rev oldest_revrev_dict refs_dict srev_dictcCsb||_||_||_||_||_|dur|dur|r|r|rdS|s+|s+|s+|s+|s+dStd|)NzInvalid RevCache fields: %r)rrrrr ValueError)rPrrrrrrrrrUs, zStorage.RevCache.__init__cCs|ddiiiSrKrrrrrempty.szStorage.RevCache.emptycCs(d|j|jt|jt|jt|jfS)NziRevCache(youngest_rev=%r, oldest_rev=%r, rev_dict=%d entries, refs_dict=%d entries, srev_dict=%d entries))rrr3rrrrVrrrrW2s zStorage.RevCache.__repr__ccsH|jd}|jD]\}}|dr!|dd|||kfVq dS)NHEAD refs/heads/ )rritemsr9)rPheadrefnamerCrrr iter_branches9s  zStorage.RevCache.iter_branchesccs6|jD]\}}|dr|dd|fVqdS)N refs/tags/ )rrr9)rPrrCrrr iter_tags?s  zStorage.RevCache.iter_tagsN) rrr __slots__rUrrrWrrrrrrRevCaches  rcCsBt|dksJt|ddd}d|krdksJJ|S)Nr0ri)r3r4)rCsrev_keyrrr __rev_keyDszStorage.__rev_keyrJc Csd}zCt|d}|\}|d}dd}tt||d}i}||d<||d<||d <d tt||d <||k|d <|WSt y[}z t d |t |fd}~ww)N)r)rRcSs"zt|WSty|YSwrK)r4rsrrrtry_intYs   z$Storage.git_version..try_int.v_strv_tuple v_min_tuple. v_min_str v_compatiblezQCould not retrieve GIT version (tried to execute/parse '%s --version' but got %s)) rIversionrr"r tuplergr#rA Exceptionr repr) rRGIT_VERSION_MIN_REQUIREDgvrr split_versionresulterrr git_versionLs(   zStorage.git_versionNc s||_d|_|p |j|_d|_t|_td|_ t|_ d|_ t|_ d|_ t|_durDtfdd|_fdd|_ndd|_|_zt|Wntyk}z |||WYd}~nd}~ww||stj|d}zt|Wn tyd}Yn w||rd }|}nd}|rtd |zttj|d d Wdn1swYWnty}z |||WYd}~nd}~wwt||||_||_|jd |dS)a(Initialize PyGit.Storage instance `git_dir`: path to .git folder; this setting is not affected by the `git_fs_encoding` setting `log`: logger instance `git_bin`: path to executable this setting is not affected by the `git_fs_encoding` setting `git_fs_encoding`: encoding used for paths stored in git repository; if `None`, no implicit decoding/encoding to/from unicode objects is performed, and bytestrings are returned instead NTcs |dSrZ)r]rrrrsz"Storage.__init__..cs |SrKrFrrrrrs cSs|SrKrrrrrrsz.gitFz#Git control files not found in '%s'HEADrbz.PyGIT.Storage instance for '%s' is constructed) rcommit_encodingrr_Storage__rev_cache_Storage__rev_cache_refreshr_Storage__rev_cache_lockr_Storage__commit_msg_cache_Storage__commit_msg_lock_Storage__cat_file_pipe_Storage__cat_file_pipe_lock_Storage__diff_tree_pipe_Storage__diff_tree_pipe_lockcodecslookup_fs_to_unicode_fs_from_unicoderdlistdirEnvironmentError_raise_not_readable_control_files_existr=r#r openrIr repo_pathry) rPrQrSrRrrr dot_git_dirmissingrrrrUnsb      zStorage.__init__cCs>|r|j|j|jfD]}|r|q t||dSdSrK)rlrmrncloserwait)rPprocfrrr _cleanup_procs zStorage._cleanup_proccCsj|j||jWdn1swY|j||jWddS1s.wYdSrK)rrrrrrVrrr__del__s "zStorage.__del__cCs2|j d|_WddS1swYdS)NT)rrrVrrrinvalidate_rev_caches"zStorage.invalidate_rev_cachecCs8|j||jWdS1swYdS)zrRetrieve revision cache may rebuild cache on the fly if required returns RevCache tuple N)r_refresh_rev_cacherrVrrrrs$zStorage.rev_cacheFcCsrd}|s|jr7d|_|}|jj|kr/|jd|j||}||_t |j|d}|S|jd|j|S)NFz'Detected changes in git repository '%s'Tz*Detected no changes in git repository '%s') r _get_refsrrrryr_build_rev_cacherr)rPforce refreshedrefsrrrrrs"   zStorage._refresh_rev_cachec sJ|jd|jt}i}i}ifddfdd|D}dd|D}fdd |jd d d D}d|rM|d d }|dd }nd}}ifdd} |j} t |D]r\} } | d } | dd}| | | g | | |vr|| \}}}}|sJ|rJ|d ksJnt }t }| |vr| | t|t|| d| |f|| <|D]}| |t gd t f\}}}}| | ||qq`dt|dkrdgt|dni}z |\}}t|||<qtyYnwt|d ksJ|}||||||}|jd|jt|dt||S)Nz,triggered rebuild of commit tree db for '%s'cs ||SrK)r!rB) revs_seenrr _rev_reuserz,Storage._build_rev_cache.._rev_reusecsi|] \}}||qSrr.0rrCrrr z,Storage._build_rev_cache..cSsh|] \}}|dr|qS)r)r9rrrr s  z+Storage._build_rev_cache..csg|] }tt|qSr)rfrgr )rr&rrr sz,Storage._build_rev_cache..z --parentsz --topo-order--allrrcst|}||SrK) frozensetr!)rheads) rheads_seenrr _rheads_reuse z/Storage._build_rev_cache.._rheads_reuserirTz>rebuilt commit tree db for '%s' with %d entries (took %.1f ms)i)rryrr rrrev_listr_Storage__rev_key enumerater!rsetaddrrupdater3maxpopitemrr)rPr ts0new_dbnew_sdb head_revsryoungestoldestrrord_revrevsrCparents _children_parents_ord_rev_rheadsparent_rheads2tmpkrrr)rr rrr s          "     zStorage._build_rev_cachecCsi}i}|jdD]!}d|vrq |dd\}}|dr)|||dd<q |||<q |t||rL|jddpAd }||vrL||d <|S) Nz --dereference rs^{}z-qrr) rshow_refrr r:r!iterr symbolic_refr")rPr tagsr&rCrrrrrNs  zStorage._get_refscs<dd}|jtfdd|jD|d}dd|DS)z`returns list of (local) branches, with active (= HEAD) one being the first item cSs|\}}}| |fSrKr)rirerCrrrrfngs  z Storage.get_branches..fnc3s(|]\}}}|t||fVqdSrKrDrrerCrrrr ksz'Storage.get_branches..r*cSsg|] \}}}||fqSrrr>rrrrnrz(Storage.get_branches..)rsortedrr)rPr<branchesrr?r get_branchescs zStorage.get_branchesccs<|j}|jjD]\}}|dkr||t|fVq dS)Nr)rrrrrD)rPrrrCrrrget_refspszStorage.get_refscCs|jjSrK)rrrVrrr get_commitsvrzStorage.get_commitscC t|jjSrK)rDrrrVrrrryrzStorage.oldest_revcCrGrK)rDrrrVrrrr|rzStorage.youngest_revcs|t|}|j}z |j|dWn tygYSw|r7|jfdd|D}|jddd|Sttt S)zreturn list of reachable head sha ids or (names, sha) pairs if resolve is true see also get_branches() cs*g|]\}}}|vr|t|fqSrr=r>rrrrrs z/Storage.get_branch_contains..cSs|dS)Nrr)rrrrrsz-Storage.get_branch_contains..rA) rHrrrrrsortrfrgrD)rPrresolve _rev_cachervrrIrget_branch_containss  zStorage.get_branch_containscs"fdd}|t||}t|S)Ncsx}||vr t|dkr|S||d|}|dks#|t|kr%dS|D]\}}|d|kr7|Sq)td)Nrrrzinternal inconsistency detected)rFrr3rr )rrel_posrlin_revr4rrVrrget_history_relative_revs z>Storage.history_relative_rev..get_history_relative_rev)rHrD)rPrrOrQrrrVrhistory_relative_revs zStorage.history_relative_revcC ||dS)NrrRrPrrrrhist_next_revisionrzStorage.hist_next_revisioncCrSNrrTrUrrrhist_prev_revisionrzStorage.hist_prev_revisioncCs(|jdur|jddpd|_|jS)Nz--getzi18n.commitEncodingr-)rrconfigr"rVrrrget_commit_encodings zStorage.get_commit_encodingcCs |dS)zget current HEAD commit idr) verifyrevrVrrrrs z Storage.headcCs|||SrK)_cat_file_readerread)rPkindrrrrcat_fileszStorage.cat_filec Cs|j|jdur|j|_z|jj|d|jj|jj }t |dkr5t dt ||\}}}||krFt d||ft |}|jjj}|dkrXt} nt} |d} | dkr|t| d} | s{t d |d|d| f| t | 8} | | dkr| n| dd | dksd| d| WWdSty} z|jd t| ||jd|_WYd} ~ nd} ~ wwWddS1swYdS) N rHz(internal error (could not split line %s)z;internal error (got unexpected object kind %r, expected %r)irriz;internal error (expected to read %d bytes, but only got %d)rzclosing cat_file pipe: %s)rrrrrlwriteflushrmreadliner r3r rr4r]tempfile TemporaryFileioBytesIOminseekrrwarningr r) rPr^rsplit_stdout_line_sha_type_sizesize stdout_readbuf remainingchunkrrrrr\s\          , "zStorage._cat_file_readercs"fdd}||}t|S)zaverify/lookup given revision object and return a sha id or None if lookup failed csj}t|r|}|r|S|j}|dkr%||}||vr%||S|d|}|r0|S|d|}|r;|Sjd|}|sHdS||j vrO|SdS)Nrrrz--verify) rrIrfullrevrrr rev_parser"r)rCrLrtr rresolvedrrVrr get_verifyrevs,    z(Storage.verifyrev..get_verifyrev)rrD)rPrCrwrrrVrr[s zStorage.verifyrevcsfdd}t|t||S)Ncs|jkrj}j}||jvrdS|d|}t|j|}t|dkr*|S||h}t|ddD]|d}|fdd|DvrM|Sq6|S)ztry to shorten sha idNrrcsg|]}|dqSrKr)rrlrrr>z:Storage.shortrev..get_shortrev..)_Storage__SREV_MINrrrrrr3range)rCmin_lenrLsrevsrevscrevsrVrzr get_shortrev%s      z&Storage.shortrev..get_shortrev)rDrH)rPrCrrrrVrshortrev#s zStorage.shortrevcCs|j}t|dkr||jvr|St|sdSz |j||}Wn ty+YdSwd}|D]}||r@|dur>dS|}q0|S)ztry to reverse shortrev()rN) rr3rrIrrrrr9)rPrCrLrrvrrrrrtFs$   zStorage.fullrevcs.durttfddjDS)Nc3s.|]\}}dus|kr|VqdSrKr?)rrerev_rCrPrrr@bsz#Storage.get_tags..)rHrBrrrPrCrrrget_tags_s zStorage.get_tagsr7csd|r|nd}|dpd}j|rdnd|d|d}fdd fd d |DS) Nr/rz-zlrz-zl--csh|dd\}}|\}}}}t|d}t|}t|d}|dkr$dnt|}|}|||||fS)z7split according to ' ' rr-r1-N)r rArDr4r)r{metafname_modermrlrnrVrrsplit_ls_tree_linels   z+Storage.ls_tree..split_ls_tree_linecsg|]}|r|qSrr)rr)rrrrxr|z#Storage.ls_tree..)rlstriprls_treer )rPrCr= recursivetreer)rPrrrfs  zStorage.ls_treecCs|std|}|t|}|}||vr!|jd||t|j#||jvr@|j|}|dt |dfWdSWdn1sJwY| d|}t || d}t |}|j ||j|<Wdn1stwY|dt |dfS)Nz'read_commit called with empty commit_idzread_commit failed for %r (%r)rrscommitr8)r rtrHrFrinforrrrr_rArZr,)rP commit_idcommit_id_origrrr$rrr read_commitzs0    zStorage.read_commitcCst|}|d|S)Nsblob)rHr\rUrrrget_filerzStorage.get_filecCs@t|}zt|jd|}W|Stytd|w)Ns-szobject '%s' not found)rHr4rr_r"rr)rProbj_sizerrr get_obj_sizes zStorage.get_obj_sizecCsDt|}|}z||}Wn tyYdSwttt|dS)Nrr)rHrFrrBrgrDrPrritemrrrchildrens  zStorage.childrenccs|dur |}t}t}||d}|||||r<|}|V||d|}|||||s!t|dksDJdS)z4Recursively traverse children in breadth-first orderNr)rFrrr!rcrr3)rPrr work_listseenr-rzrrrchildren_recursives      zStorage.children_recursivecCsFt|}|}z||}Wn tygYSwttt|dSrW)rHrFrrfrgrDrrrrr,s  zStorage.parentsccs|D]}t|VqdSrK)rFrDrrrrall_revss  zStorage.all_revscCs6|j|jddWdS1swYdS)NT)r )rrrVrrrsyncs $z Storage.syncc #sgigp dfdd}|fdd}z|VWr5ddSdSrAdww)Nrc 3sjddddgdd<dj}|D]N}|dkrq|d}|D]?}|dkr.n8|ddd\}}t|}|vre||<|gkrM|Vz |d d\}}Wn ty`Ynw|vsAq&qrpdgdd< dVqw) Nz--pretty=format:%n%Hz --no-renamesz --name-statusrrr`rrr) rrrmrstripr r>rsplitrr)rr{old_sharr=) base_pathchange next_pathrzrPrrrname_status_gens>      z.Storage.get_historian..name_status_gencsL|}z |}Wt|Sty%|gdd<t}Yt|SwrK)rrnextrD)r=rC)rgenrrPrr historians    z(Storage.get_historian..historianr)rr)rPrrrrr)rrrrrzrPrr get_historians zStorage.get_historiancCs0|dur||S|j||ddD]}|SdS)Nr)limit)history)rPrr=rentryrrr last_changes zStorage.last_changeccs^|durd}d|t|g}|r|d||f|jj|}|D]}t|Vq%dS)Nrz--max-count=%dr)rArcrrrrrD)rPrr=rrir3rCrrrr s   zStorage.historycCs0|jddd|d|d}dd|DS)Nrz --date-orderz --max-age=%dz --min-age=%drcSsg|]}t|qSrr=)rrCrrrrsz-Storage.history_timerange..)rrr)rPstartstopoutputrrrhistory_timeranges  zStorage.history_timerangecCs0t|}t|}|}||vo||||vS)z(return True if rev2 is successor of rev1)rHrFr)rPrev1rev2rrrrrev_is_anchestor_ofs zStorage.rev_is_anchestor_ofc csd}t|}||}|jdd||D]6}|sJ|r&|d }q|}t|dkr7|\}}}} n|\}}}t|dksDJt||fVd}q|rRJdS)NFz-prrr0rT) rHrrblamerr9r r3rD) rP commit_shar= in_metadatar& split_liner orig_linenolineno group_sizerrrr(s$    z Storage.blameccsV|j|jdur|j|_|j}z_|j|r#dt|t|fndt||j|jj }g}|d}|s>t |dkrqt }|dkr]| |d|d}|sYt |dksI| t ||d}|smt |dksBWn d|_||Wdn1swY|sdS|ddrJ|dd}||EdHdS)Ns%s %s s%s rr`rr:)rrrrrlrarHrbrmr]EOFError bytearrayrr\rr9_iter_diff_tree)rPtree1tree2rr]entriescrrrr get_changes@sT       zStorage.get_changesc cs||dp d}ddg}|r|d||r|nd|d|g|jj|}|s-dSd d }t||}|sE|d } | d rEJ| |EdHdS) zgcalls `git diff-tree` and returns tuples of the kind (mode1,mode2,obj1,obj2,action,path1,path2)rrrrrrrNcss8d} |d|}|dkrdS|||V|d}q)NrTrrr)find)rridxrrr iter_entryws z%Storage.diff_tree..iter_entryrr) rr"rrcr diff_treerfrr9r) rPrrr= find_renamesdiff_tree_argsrrrrrrrrfs"    zStorage.diff_treec #sfdd}t| z|}Wn tyYdSw|ds#J|ddd}t|dks4J|\}}}}} t|d}t|d}t|}t|}t| ddd } ||} d} | d vrh||} ||||| | | fVq ) NcstSrK)rrrrr next_entryrz+Storage._iter_diff_tree..next_entryTrrr5rr1r-)RC) r9 StopIterationr9r r3r4rDrAr) rPrrrvaluesold_modenew_modernew_sharold_pathnew_pathrrrrs4        zStorage._iter_diff_treecCstd|t|f)Nz1Make sure the Git repository '%s' is readable: %s)r r )rPrQrrrrrs zStorage._raise_not_readablecCs:dD]}tjtj||s|jd||dSqdS)N)robjectsr z%Missing Git control file '%s' in '%s'FT)rdr=existsr#rry)rPrQrerrrrszStorage._control_files_exist)rJ)rJNN)F)rxrK)r7F):rrrrr}objectr staticmethodrrrUrrrpropertyrrr rrDrErFrrrNrRrVrXrZrr_r\r[rrtrrrrrrrr,rr contextlibcontextmanagerrrrrrrrrrrrrrrrrsr+  ! Q   \  6 &#      2    &# r)1rrrfrdrrordr collectionsr functoolsrrr threadingr trac.corer trac.utilrtrac.util.compatr trac.util.datefmtr trac.util.textr r __all__r rr,rrGr;rr5zip_key_valr\r>rDrHrrIrrrrrrrrsD         Z2