| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247 |
- # Copyright (C) 2008 The Android Open Source Project
- #
- # Licensed under the Apache License, Version 2.0 (the "License");
- # you may not use this file except in compliance with the License.
- # You may obtain a copy of the License at
- #
- # http://www.apache.org/licenses/LICENSE-2.0
- #
- # Unless required by applicable law or agreed to in writing, software
- # distributed under the License is distributed on an "AS IS" BASIS,
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- # See the License for the specific language governing permissions and
- # limitations under the License.
- import errno
- import filecmp
- import glob
- import os
- import random
- import re
- import shutil
- import stat
- import subprocess
- import sys
- import tarfile
- import tempfile
- import time
- import urllib.parse
- from color import Coloring
- from git_command import GitCommand, git_require
- from git_config import GitConfig, IsId, GetSchemeFromUrl, GetUrlCookieFile, \
- ID_RE
- from error import GitError, UploadError, DownloadError
- from error import ManifestInvalidRevisionError, ManifestInvalidPathError
- from error import NoManifestException
- import platform_utils
- import progress
- from repo_trace import IsTrace, Trace
- from git_refs import GitRefs, HEAD, R_HEADS, R_TAGS, R_PUB, R_M, R_WORKTREE_M
- # Maximum sleep time allowed during retries.
- MAXIMUM_RETRY_SLEEP_SEC = 3600.0
- # +-10% random jitter is added to each Fetches retry sleep duration.
- RETRY_JITTER_PERCENT = 0.1
- def _lwrite(path, content):
- lock = '%s.lock' % path
- # Maintain Unix line endings on all OS's to match git behavior.
- with open(lock, 'w', newline='\n') as fd:
- fd.write(content)
- try:
- platform_utils.rename(lock, path)
- except OSError:
- platform_utils.remove(lock)
- raise
- def _error(fmt, *args):
- msg = fmt % args
- print('error: %s' % msg, file=sys.stderr)
- def _warn(fmt, *args):
- msg = fmt % args
- print('warn: %s' % msg, file=sys.stderr)
- def not_rev(r):
- return '^' + r
- def sq(r):
- return "'" + r.replace("'", "'\''") + "'"
- _project_hook_list = None
- def _ProjectHooks():
- """List the hooks present in the 'hooks' directory.
- These hooks are project hooks and are copied to the '.git/hooks' directory
- of all subprojects.
- This function caches the list of hooks (based on the contents of the
- 'repo/hooks' directory) on the first call.
- Returns:
- A list of absolute paths to all of the files in the hooks directory.
- """
- global _project_hook_list
- if _project_hook_list is None:
- d = platform_utils.realpath(os.path.abspath(os.path.dirname(__file__)))
- d = os.path.join(d, 'hooks')
- _project_hook_list = [os.path.join(d, x) for x in platform_utils.listdir(d)]
- return _project_hook_list
- class DownloadedChange(object):
- _commit_cache = None
- def __init__(self, project, base, change_id, ps_id, commit):
- self.project = project
- self.base = base
- self.change_id = change_id
- self.ps_id = ps_id
- self.commit = commit
- @property
- def commits(self):
- if self._commit_cache is None:
- self._commit_cache = self.project.bare_git.rev_list('--abbrev=8',
- '--abbrev-commit',
- '--pretty=oneline',
- '--reverse',
- '--date-order',
- not_rev(self.base),
- self.commit,
- '--')
- return self._commit_cache
- class ReviewableBranch(object):
- _commit_cache = None
- _base_exists = None
- def __init__(self, project, branch, base):
- self.project = project
- self.branch = branch
- self.base = base
- @property
- def name(self):
- return self.branch.name
- @property
- def commits(self):
- if self._commit_cache is None:
- args = ('--abbrev=8', '--abbrev-commit', '--pretty=oneline', '--reverse',
- '--date-order', not_rev(self.base), R_HEADS + self.name, '--')
- try:
- self._commit_cache = self.project.bare_git.rev_list(*args)
- except GitError:
- # We weren't able to probe the commits for this branch. Was it tracking
- # a branch that no longer exists? If so, return no commits. Otherwise,
- # rethrow the error as we don't know what's going on.
- if self.base_exists:
- raise
- self._commit_cache = []
- return self._commit_cache
- @property
- def unabbrev_commits(self):
- r = dict()
- for commit in self.project.bare_git.rev_list(not_rev(self.base),
- R_HEADS + self.name,
- '--'):
- r[commit[0:8]] = commit
- return r
- @property
- def date(self):
- return self.project.bare_git.log('--pretty=format:%cd',
- '-n', '1',
- R_HEADS + self.name,
- '--')
- @property
- def base_exists(self):
- """Whether the branch we're tracking exists.
- Normally it should, but sometimes branches we track can get deleted.
- """
- if self._base_exists is None:
- try:
- self.project.bare_git.rev_parse('--verify', not_rev(self.base))
- # If we're still here, the base branch exists.
- self._base_exists = True
- except GitError:
- # If we failed to verify, the base branch doesn't exist.
- self._base_exists = False
- return self._base_exists
- def UploadForReview(self, people,
- dryrun=False,
- auto_topic=False,
- hashtags=(),
- labels=(),
- private=False,
- notify=None,
- wip=False,
- dest_branch=None,
- validate_certs=True,
- push_options=None):
- self.project.UploadForReview(branch=self.name,
- people=people,
- dryrun=dryrun,
- auto_topic=auto_topic,
- hashtags=hashtags,
- labels=labels,
- private=private,
- notify=notify,
- wip=wip,
- dest_branch=dest_branch,
- validate_certs=validate_certs,
- push_options=push_options)
- def GetPublishedRefs(self):
- refs = {}
- output = self.project.bare_git.ls_remote(
- self.branch.remote.SshReviewUrl(self.project.UserEmail),
- 'refs/changes/*')
- for line in output.split('\n'):
- try:
- (sha, ref) = line.split()
- refs[sha] = ref
- except ValueError:
- pass
- return refs
- class StatusColoring(Coloring):
- def __init__(self, config):
- Coloring.__init__(self, config, 'status')
- self.project = self.printer('header', attr='bold')
- self.branch = self.printer('header', attr='bold')
- self.nobranch = self.printer('nobranch', fg='red')
- self.important = self.printer('important', fg='red')
- self.added = self.printer('added', fg='green')
- self.changed = self.printer('changed', fg='red')
- self.untracked = self.printer('untracked', fg='red')
- class DiffColoring(Coloring):
- def __init__(self, config):
- Coloring.__init__(self, config, 'diff')
- self.project = self.printer('header', attr='bold')
- self.fail = self.printer('fail', fg='red')
- class _Annotation(object):
- def __init__(self, name, value, keep):
- self.name = name
- self.value = value
- self.keep = keep
- def _SafeExpandPath(base, subpath, skipfinal=False):
- """Make sure |subpath| is completely safe under |base|.
- We make sure no intermediate symlinks are traversed, and that the final path
- is not a special file (e.g. not a socket or fifo).
- NB: We rely on a number of paths already being filtered out while parsing the
- manifest. See the validation logic in manifest_xml.py for more details.
- """
- # Split up the path by its components. We can't use os.path.sep exclusively
- # as some platforms (like Windows) will convert / to \ and that bypasses all
- # our constructed logic here. Especially since manifest authors only use
- # / in their paths.
- resep = re.compile(r'[/%s]' % re.escape(os.path.sep))
- components = resep.split(subpath)
- if skipfinal:
- # Whether the caller handles the final component itself.
- finalpart = components.pop()
- path = base
- for part in components:
- if part in {'.', '..'}:
- raise ManifestInvalidPathError(
- '%s: "%s" not allowed in paths' % (subpath, part))
- path = os.path.join(path, part)
- if platform_utils.islink(path):
- raise ManifestInvalidPathError(
- '%s: traversing symlinks not allow' % (path,))
- if os.path.exists(path):
- if not os.path.isfile(path) and not platform_utils.isdir(path):
- raise ManifestInvalidPathError(
- '%s: only regular files & directories allowed' % (path,))
- if skipfinal:
- path = os.path.join(path, finalpart)
- return path
- class _CopyFile(object):
- """Container for <copyfile> manifest element."""
- def __init__(self, git_worktree, src, topdir, dest):
- """Register a <copyfile> request.
- Args:
- git_worktree: Absolute path to the git project checkout.
- src: Relative path under |git_worktree| of file to read.
- topdir: Absolute path to the top of the repo client checkout.
- dest: Relative path under |topdir| of file to write.
- """
- self.git_worktree = git_worktree
- self.topdir = topdir
- self.src = src
- self.dest = dest
- def _Copy(self):
- src = _SafeExpandPath(self.git_worktree, self.src)
- dest = _SafeExpandPath(self.topdir, self.dest)
- if platform_utils.isdir(src):
- raise ManifestInvalidPathError(
- '%s: copying from directory not supported' % (self.src,))
- if platform_utils.isdir(dest):
- raise ManifestInvalidPathError(
- '%s: copying to directory not allowed' % (self.dest,))
- # copy file if it does not exist or is out of date
- if not os.path.exists(dest) or not filecmp.cmp(src, dest):
- try:
- # remove existing file first, since it might be read-only
- if os.path.exists(dest):
- platform_utils.remove(dest)
- else:
- dest_dir = os.path.dirname(dest)
- if not platform_utils.isdir(dest_dir):
- os.makedirs(dest_dir)
- shutil.copy(src, dest)
- # make the file read-only
- mode = os.stat(dest)[stat.ST_MODE]
- mode = mode & ~(stat.S_IWUSR | stat.S_IWGRP | stat.S_IWOTH)
- os.chmod(dest, mode)
- except IOError:
- _error('Cannot copy file %s to %s', src, dest)
- class _LinkFile(object):
- """Container for <linkfile> manifest element."""
- def __init__(self, git_worktree, src, topdir, dest):
- """Register a <linkfile> request.
- Args:
- git_worktree: Absolute path to the git project checkout.
- src: Target of symlink relative to path under |git_worktree|.
- topdir: Absolute path to the top of the repo client checkout.
- dest: Relative path under |topdir| of symlink to create.
- """
- self.git_worktree = git_worktree
- self.topdir = topdir
- self.src = src
- self.dest = dest
- def __linkIt(self, relSrc, absDest):
- # link file if it does not exist or is out of date
- if not platform_utils.islink(absDest) or (platform_utils.readlink(absDest) != relSrc):
- try:
- # remove existing file first, since it might be read-only
- if os.path.lexists(absDest):
- platform_utils.remove(absDest)
- else:
- dest_dir = os.path.dirname(absDest)
- if not platform_utils.isdir(dest_dir):
- os.makedirs(dest_dir)
- platform_utils.symlink(relSrc, absDest)
- except IOError:
- _error('Cannot link file %s to %s', relSrc, absDest)
- def _Link(self):
- """Link the self.src & self.dest paths.
- Handles wild cards on the src linking all of the files in the source in to
- the destination directory.
- """
- # Some people use src="." to create stable links to projects. Lets allow
- # that but reject all other uses of "." to keep things simple.
- if self.src == '.':
- src = self.git_worktree
- else:
- src = _SafeExpandPath(self.git_worktree, self.src)
- if not glob.has_magic(src):
- # Entity does not contain a wild card so just a simple one to one link operation.
- dest = _SafeExpandPath(self.topdir, self.dest, skipfinal=True)
- # dest & src are absolute paths at this point. Make sure the target of
- # the symlink is relative in the context of the repo client checkout.
- relpath = os.path.relpath(src, os.path.dirname(dest))
- self.__linkIt(relpath, dest)
- else:
- dest = _SafeExpandPath(self.topdir, self.dest)
- # Entity contains a wild card.
- if os.path.exists(dest) and not platform_utils.isdir(dest):
- _error('Link error: src with wildcard, %s must be a directory', dest)
- else:
- for absSrcFile in glob.glob(src):
- # Create a releative path from source dir to destination dir
- absSrcDir = os.path.dirname(absSrcFile)
- relSrcDir = os.path.relpath(absSrcDir, dest)
- # Get the source file name
- srcFile = os.path.basename(absSrcFile)
- # Now form the final full paths to srcFile. They will be
- # absolute for the desintaiton and relative for the srouce.
- absDest = os.path.join(dest, srcFile)
- relSrc = os.path.join(relSrcDir, srcFile)
- self.__linkIt(relSrc, absDest)
- class RemoteSpec(object):
- def __init__(self,
- name,
- url=None,
- pushUrl=None,
- review=None,
- revision=None,
- orig_name=None,
- fetchUrl=None):
- self.name = name
- self.url = url
- self.pushUrl = pushUrl
- self.review = review
- self.revision = revision
- self.orig_name = orig_name
- self.fetchUrl = fetchUrl
- class Project(object):
- # These objects can be shared between several working trees.
- shareable_files = ['description', 'info']
- shareable_dirs = ['hooks', 'objects', 'rr-cache', 'svn']
- # These objects can only be used by a single working tree.
- working_tree_files = ['config', 'packed-refs', 'shallow']
- working_tree_dirs = ['logs', 'refs']
- def __init__(self,
- manifest,
- name,
- remote,
- gitdir,
- objdir,
- worktree,
- relpath,
- revisionExpr,
- revisionId,
- rebase=True,
- groups=None,
- sync_c=False,
- sync_s=False,
- sync_tags=True,
- clone_depth=None,
- upstream=None,
- parent=None,
- use_git_worktrees=False,
- is_derived=False,
- dest_branch=None,
- optimized_fetch=False,
- retry_fetches=0,
- old_revision=None):
- """Init a Project object.
- Args:
- manifest: The XmlManifest object.
- name: The `name` attribute of manifest.xml's project element.
- remote: RemoteSpec object specifying its remote's properties.
- gitdir: Absolute path of git directory.
- objdir: Absolute path of directory to store git objects.
- worktree: Absolute path of git working tree.
- relpath: Relative path of git working tree to repo's top directory.
- revisionExpr: The `revision` attribute of manifest.xml's project element.
- revisionId: git commit id for checking out.
- rebase: The `rebase` attribute of manifest.xml's project element.
- groups: The `groups` attribute of manifest.xml's project element.
- sync_c: The `sync-c` attribute of manifest.xml's project element.
- sync_s: The `sync-s` attribute of manifest.xml's project element.
- sync_tags: The `sync-tags` attribute of manifest.xml's project element.
- upstream: The `upstream` attribute of manifest.xml's project element.
- parent: The parent Project object.
- use_git_worktrees: Whether to use `git worktree` for this project.
- is_derived: False if the project was explicitly defined in the manifest;
- True if the project is a discovered submodule.
- dest_branch: The branch to which to push changes for review by default.
- optimized_fetch: If True, when a project is set to a sha1 revision, only
- fetch from the remote if the sha1 is not present locally.
- retry_fetches: Retry remote fetches n times upon receiving transient error
- with exponential backoff and jitter.
- old_revision: saved git commit id for open GITC projects.
- """
- self.client = self.manifest = manifest
- self.name = name
- self.remote = remote
- self.gitdir = gitdir.replace('\\', '/')
- self.objdir = objdir.replace('\\', '/')
- if worktree:
- self.worktree = os.path.normpath(worktree).replace('\\', '/')
- else:
- self.worktree = None
- self.relpath = relpath
- self.revisionExpr = revisionExpr
- if revisionId is None \
- and revisionExpr \
- and IsId(revisionExpr):
- self.revisionId = revisionExpr
- else:
- self.revisionId = revisionId
- self.rebase = rebase
- self.groups = groups
- self.sync_c = sync_c
- self.sync_s = sync_s
- self.sync_tags = sync_tags
- self.clone_depth = clone_depth
- self.upstream = upstream
- self.parent = parent
- # NB: Do not use this setting in __init__ to change behavior so that the
- # manifest.git checkout can inspect & change it after instantiating. See
- # the XmlManifest init code for more info.
- self.use_git_worktrees = use_git_worktrees
- self.is_derived = is_derived
- self.optimized_fetch = optimized_fetch
- self.retry_fetches = max(0, retry_fetches)
- self.subprojects = []
- self.snapshots = {}
- self.copyfiles = []
- self.linkfiles = []
- self.annotations = []
- self.config = GitConfig.ForRepository(gitdir=self.gitdir,
- defaults=self.client.globalConfig)
- if self.worktree:
- self.work_git = self._GitGetByExec(self, bare=False, gitdir=gitdir)
- else:
- self.work_git = None
- self.bare_git = self._GitGetByExec(self, bare=True, gitdir=gitdir)
- self.bare_ref = GitRefs(gitdir)
- self.bare_objdir = self._GitGetByExec(self, bare=True, gitdir=objdir)
- self.dest_branch = dest_branch
- self.old_revision = old_revision
- # This will be filled in if a project is later identified to be the
- # project containing repo hooks.
- self.enabled_repo_hooks = []
- @property
- def Derived(self):
- return self.is_derived
- @property
- def Exists(self):
- return platform_utils.isdir(self.gitdir) and platform_utils.isdir(self.objdir)
- @property
- def CurrentBranch(self):
- """Obtain the name of the currently checked out branch.
- The branch name omits the 'refs/heads/' prefix.
- None is returned if the project is on a detached HEAD, or if the work_git is
- otheriwse inaccessible (e.g. an incomplete sync).
- """
- try:
- b = self.work_git.GetHead()
- except NoManifestException:
- # If the local checkout is in a bad state, don't barf. Let the callers
- # process this like the head is unreadable.
- return None
- if b.startswith(R_HEADS):
- return b[len(R_HEADS):]
- return None
- def IsRebaseInProgress(self):
- return (os.path.exists(self.work_git.GetDotgitPath('rebase-apply')) or
- os.path.exists(self.work_git.GetDotgitPath('rebase-merge')) or
- os.path.exists(os.path.join(self.worktree, '.dotest')))
- def IsDirty(self, consider_untracked=True):
- """Is the working directory modified in some way?
- """
- self.work_git.update_index('-q',
- '--unmerged',
- '--ignore-missing',
- '--refresh')
- if self.work_git.DiffZ('diff-index', '-M', '--cached', HEAD):
- return True
- if self.work_git.DiffZ('diff-files'):
- return True
- if consider_untracked and self.work_git.LsOthers():
- return True
- return False
- _userident_name = None
- _userident_email = None
- @property
- def UserName(self):
- """Obtain the user's personal name.
- """
- if self._userident_name is None:
- self._LoadUserIdentity()
- return self._userident_name
- @property
- def UserEmail(self):
- """Obtain the user's email address. This is very likely
- to be their Gerrit login.
- """
- if self._userident_email is None:
- self._LoadUserIdentity()
- return self._userident_email
- def _LoadUserIdentity(self):
- u = self.bare_git.var('GIT_COMMITTER_IDENT')
- m = re.compile("^(.*) <([^>]*)> ").match(u)
- if m:
- self._userident_name = m.group(1)
- self._userident_email = m.group(2)
- else:
- self._userident_name = ''
- self._userident_email = ''
- def GetRemote(self, name):
- """Get the configuration for a single remote.
- """
- return self.config.GetRemote(name)
- def GetBranch(self, name):
- """Get the configuration for a single branch.
- """
- return self.config.GetBranch(name)
- def GetBranches(self):
- """Get all existing local branches.
- """
- current = self.CurrentBranch
- all_refs = self._allrefs
- heads = {}
- for name, ref_id in all_refs.items():
- if name.startswith(R_HEADS):
- name = name[len(R_HEADS):]
- b = self.GetBranch(name)
- b.current = name == current
- b.published = None
- b.revision = ref_id
- heads[name] = b
- for name, ref_id in all_refs.items():
- if name.startswith(R_PUB):
- name = name[len(R_PUB):]
- b = heads.get(name)
- if b:
- b.published = ref_id
- return heads
- def MatchesGroups(self, manifest_groups):
- """Returns true if the manifest groups specified at init should cause
- this project to be synced.
- Prefixing a manifest group with "-" inverts the meaning of a group.
- All projects are implicitly labelled with "all".
- labels are resolved in order. In the example case of
- project_groups: "all,group1,group2"
- manifest_groups: "-group1,group2"
- the project will be matched.
- The special manifest group "default" will match any project that
- does not have the special project group "notdefault"
- """
- expanded_manifest_groups = manifest_groups or ['default']
- expanded_project_groups = ['all'] + (self.groups or [])
- if 'notdefault' not in expanded_project_groups:
- expanded_project_groups += ['default']
- matched = False
- for group in expanded_manifest_groups:
- if group.startswith('-') and group[1:] in expanded_project_groups:
- matched = False
- elif group in expanded_project_groups:
- matched = True
- return matched
- # Status Display ##
- def UncommitedFiles(self, get_all=True):
- """Returns a list of strings, uncommitted files in the git tree.
- Args:
- get_all: a boolean, if True - get information about all different
- uncommitted files. If False - return as soon as any kind of
- uncommitted files is detected.
- """
- details = []
- self.work_git.update_index('-q',
- '--unmerged',
- '--ignore-missing',
- '--refresh')
- if self.IsRebaseInProgress():
- details.append("rebase in progress")
- if not get_all:
- return details
- changes = self.work_git.DiffZ('diff-index', '--cached', HEAD).keys()
- if changes:
- details.extend(changes)
- if not get_all:
- return details
- changes = self.work_git.DiffZ('diff-files').keys()
- if changes:
- details.extend(changes)
- if not get_all:
- return details
- changes = self.work_git.LsOthers()
- if changes:
- details.extend(changes)
- return details
- def HasChanges(self):
- """Returns true if there are uncommitted changes.
- """
- if self.UncommitedFiles(get_all=False):
- return True
- else:
- return False
- def PrintWorkTreeStatus(self, output_redir=None, quiet=False):
- """Prints the status of the repository to stdout.
- Args:
- output_redir: If specified, redirect the output to this object.
- quiet: If True then only print the project name. Do not print
- the modified files, branch name, etc.
- """
- if not platform_utils.isdir(self.worktree):
- if output_redir is None:
- output_redir = sys.stdout
- print(file=output_redir)
- print('project %s/' % self.relpath, file=output_redir)
- print(' missing (run "repo sync")', file=output_redir)
- return
- self.work_git.update_index('-q',
- '--unmerged',
- '--ignore-missing',
- '--refresh')
- rb = self.IsRebaseInProgress()
- di = self.work_git.DiffZ('diff-index', '-M', '--cached', HEAD)
- df = self.work_git.DiffZ('diff-files')
- do = self.work_git.LsOthers()
- if not rb and not di and not df and not do and not self.CurrentBranch:
- return 'CLEAN'
- out = StatusColoring(self.config)
- if output_redir is not None:
- out.redirect(output_redir)
- out.project('project %-40s', self.relpath + '/ ')
- if quiet:
- out.nl()
- return 'DIRTY'
- branch = self.CurrentBranch
- if branch is None:
- out.nobranch('(*** NO BRANCH ***)')
- else:
- out.branch('branch %s', branch)
- out.nl()
- if rb:
- out.important('prior sync failed; rebase still in progress')
- out.nl()
- paths = list()
- paths.extend(di.keys())
- paths.extend(df.keys())
- paths.extend(do)
- for p in sorted(set(paths)):
- try:
- i = di[p]
- except KeyError:
- i = None
- try:
- f = df[p]
- except KeyError:
- f = None
- if i:
- i_status = i.status.upper()
- else:
- i_status = '-'
- if f:
- f_status = f.status.lower()
- else:
- f_status = '-'
- if i and i.src_path:
- line = ' %s%s\t%s => %s (%s%%)' % (i_status, f_status,
- i.src_path, p, i.level)
- else:
- line = ' %s%s\t%s' % (i_status, f_status, p)
- if i and not f:
- out.added('%s', line)
- elif (i and f) or (not i and f):
- out.changed('%s', line)
- elif not i and not f:
- out.untracked('%s', line)
- else:
- out.write('%s', line)
- out.nl()
- return 'DIRTY'
- def PrintWorkTreeDiff(self, absolute_paths=False):
- """Prints the status of the repository to stdout.
- """
- out = DiffColoring(self.config)
- cmd = ['diff']
- if out.is_on:
- cmd.append('--color')
- cmd.append(HEAD)
- if absolute_paths:
- cmd.append('--src-prefix=a/%s/' % self.relpath)
- cmd.append('--dst-prefix=b/%s/' % self.relpath)
- cmd.append('--')
- try:
- p = GitCommand(self,
- cmd,
- capture_stdout=True,
- capture_stderr=True)
- except GitError as e:
- out.nl()
- out.project('project %s/' % self.relpath)
- out.nl()
- out.fail('%s', str(e))
- out.nl()
- return False
- has_diff = False
- for line in p.process.stdout:
- if not hasattr(line, 'encode'):
- line = line.decode()
- if not has_diff:
- out.nl()
- out.project('project %s/' % self.relpath)
- out.nl()
- has_diff = True
- print(line[:-1])
- return p.Wait() == 0
- # Publish / Upload ##
- def WasPublished(self, branch, all_refs=None):
- """Was the branch published (uploaded) for code review?
- If so, returns the SHA-1 hash of the last published
- state for the branch.
- """
- key = R_PUB + branch
- if all_refs is None:
- try:
- return self.bare_git.rev_parse(key)
- except GitError:
- return None
- else:
- try:
- return all_refs[key]
- except KeyError:
- return None
- def CleanPublishedCache(self, all_refs=None):
- """Prunes any stale published refs.
- """
- if all_refs is None:
- all_refs = self._allrefs
- heads = set()
- canrm = {}
- for name, ref_id in all_refs.items():
- if name.startswith(R_HEADS):
- heads.add(name)
- elif name.startswith(R_PUB):
- canrm[name] = ref_id
- for name, ref_id in canrm.items():
- n = name[len(R_PUB):]
- if R_HEADS + n not in heads:
- self.bare_git.DeleteRef(name, ref_id)
- def GetUploadableBranches(self, selected_branch=None):
- """List any branches which can be uploaded for review.
- """
- heads = {}
- pubed = {}
- for name, ref_id in self._allrefs.items():
- if name.startswith(R_HEADS):
- heads[name[len(R_HEADS):]] = ref_id
- elif name.startswith(R_PUB):
- pubed[name[len(R_PUB):]] = ref_id
- ready = []
- for branch, ref_id in heads.items():
- if branch in pubed and pubed[branch] == ref_id:
- continue
- if selected_branch and branch != selected_branch:
- continue
- rb = self.GetUploadableBranch(branch)
- if rb:
- ready.append(rb)
- return ready
- def GetUploadableBranch(self, branch_name):
- """Get a single uploadable branch, or None.
- """
- branch = self.GetBranch(branch_name)
- base = branch.LocalMerge
- if branch.LocalMerge:
- rb = ReviewableBranch(self, branch, base)
- if rb.commits:
- return rb
- return None
- def UploadForReview(self, branch=None,
- people=([], []),
- dryrun=False,
- auto_topic=False,
- hashtags=(),
- labels=(),
- private=False,
- notify=None,
- wip=False,
- dest_branch=None,
- validate_certs=True,
- push_options=None):
- """Uploads the named branch for code review.
- """
- if branch is None:
- branch = self.CurrentBranch
- if branch is None:
- raise GitError('not currently on a branch')
- branch = self.GetBranch(branch)
- if not branch.LocalMerge:
- raise GitError('branch %s does not track a remote' % branch.name)
- if not branch.remote.review:
- raise GitError('remote %s has no review url' % branch.remote.name)
- if dest_branch is None:
- dest_branch = self.dest_branch
- if dest_branch is None:
- dest_branch = branch.merge
- if not dest_branch.startswith(R_HEADS):
- dest_branch = R_HEADS + dest_branch
- if not branch.remote.projectname:
- branch.remote.projectname = self.name
- branch.remote.Save()
- url = branch.remote.ReviewUrl(self.UserEmail, validate_certs)
- if url is None:
- raise UploadError('review not configured')
- cmd = ['push']
- if dryrun:
- cmd.append('-n')
- if url.startswith('ssh://'):
- cmd.append('--receive-pack=gerrit receive-pack')
- for push_option in (push_options or []):
- cmd.append('-o')
- cmd.append(push_option)
- cmd.append(url)
- if dest_branch.startswith(R_HEADS):
- dest_branch = dest_branch[len(R_HEADS):]
- ref_spec = '%s:refs/for/%s' % (R_HEADS + branch.name, dest_branch)
- opts = []
- if auto_topic:
- opts += ['topic=' + branch.name]
- opts += ['t=%s' % p for p in hashtags]
- opts += ['l=%s' % p for p in labels]
- opts += ['r=%s' % p for p in people[0]]
- opts += ['cc=%s' % p for p in people[1]]
- if notify:
- opts += ['notify=' + notify]
- if private:
- opts += ['private']
- if wip:
- opts += ['wip']
- if opts:
- ref_spec = ref_spec + '%' + ','.join(opts)
- cmd.append(ref_spec)
- if GitCommand(self, cmd, bare=True).Wait() != 0:
- raise UploadError('Upload failed')
- if not dryrun:
- msg = "posted to %s for %s" % (branch.remote.review, dest_branch)
- self.bare_git.UpdateRef(R_PUB + branch.name,
- R_HEADS + branch.name,
- message=msg)
- # Sync ##
- def _ExtractArchive(self, tarpath, path=None):
- """Extract the given tar on its current location
- Args:
- - tarpath: The path to the actual tar file
- """
- try:
- with tarfile.open(tarpath, 'r') as tar:
- tar.extractall(path=path)
- return True
- except (IOError, tarfile.TarError) as e:
- _error("Cannot extract archive %s: %s", tarpath, str(e))
- return False
- def Sync_NetworkHalf(self,
- quiet=False,
- verbose=False,
- is_new=None,
- current_branch_only=False,
- force_sync=False,
- clone_bundle=True,
- tags=True,
- archive=False,
- optimized_fetch=False,
- retry_fetches=0,
- prune=False,
- submodules=False,
- clone_filter=None):
- """Perform only the network IO portion of the sync process.
- Local working directory/branch state is not affected.
- """
- if archive and not isinstance(self, MetaProject):
- if self.remote.url.startswith(('http://', 'https://')):
- _error("%s: Cannot fetch archives from http/https remotes.", self.name)
- return False
- name = self.relpath.replace('\\', '/')
- name = name.replace('/', '_')
- tarpath = '%s.tar' % name
- topdir = self.manifest.topdir
- try:
- self._FetchArchive(tarpath, cwd=topdir)
- except GitError as e:
- _error('%s', e)
- return False
- # From now on, we only need absolute tarpath
- tarpath = os.path.join(topdir, tarpath)
- if not self._ExtractArchive(tarpath, path=topdir):
- return False
- try:
- platform_utils.remove(tarpath)
- except OSError as e:
- _warn("Cannot remove archive %s: %s", tarpath, str(e))
- self._CopyAndLinkFiles()
- return True
- if is_new is None:
- is_new = not self.Exists
- if is_new:
- self._InitGitDir(force_sync=force_sync, quiet=quiet)
- else:
- self._UpdateHooks(quiet=quiet)
- self._InitRemote()
- if is_new:
- alt = os.path.join(self.gitdir, 'objects/info/alternates')
- try:
- with open(alt) as fd:
- # This works for both absolute and relative alternate directories.
- alt_dir = os.path.join(self.objdir, 'objects', fd.readline().rstrip())
- except IOError:
- alt_dir = None
- else:
- alt_dir = None
- if (clone_bundle
- and alt_dir is None
- and self._ApplyCloneBundle(initial=is_new, quiet=quiet, verbose=verbose)):
- is_new = False
- if not current_branch_only:
- if self.sync_c:
- current_branch_only = True
- elif not self.manifest._loaded:
- # Manifest cannot check defaults until it syncs.
- current_branch_only = False
- elif self.manifest.default.sync_c:
- current_branch_only = True
- if not self.sync_tags:
- tags = False
- if self.clone_depth:
- depth = self.clone_depth
- else:
- depth = self.manifest.manifestProject.config.GetString('repo.depth')
- # See if we can skip the network fetch entirely.
- if not (optimized_fetch and
- (ID_RE.match(self.revisionExpr) and
- self._CheckForImmutableRevision())):
- if not self._RemoteFetch(
- initial=is_new, quiet=quiet, verbose=verbose, alt_dir=alt_dir,
- current_branch_only=current_branch_only,
- tags=tags, prune=prune, depth=depth,
- submodules=submodules, force_sync=force_sync,
- clone_filter=clone_filter, retry_fetches=retry_fetches):
- return False
- mp = self.manifest.manifestProject
- dissociate = mp.config.GetBoolean('repo.dissociate')
- if dissociate:
- alternates_file = os.path.join(self.gitdir, 'objects/info/alternates')
- if os.path.exists(alternates_file):
- cmd = ['repack', '-a', '-d']
- if GitCommand(self, cmd, bare=True).Wait() != 0:
- return False
- platform_utils.remove(alternates_file)
- if self.worktree:
- self._InitMRef()
- else:
- self._InitMirrorHead()
- try:
- platform_utils.remove(os.path.join(self.gitdir, 'FETCH_HEAD'))
- except OSError:
- pass
- return True
- def PostRepoUpgrade(self):
- self._InitHooks()
- def _CopyAndLinkFiles(self):
- if self.client.isGitcClient:
- return
- for copyfile in self.copyfiles:
- copyfile._Copy()
- for linkfile in self.linkfiles:
- linkfile._Link()
- def GetCommitRevisionId(self):
- """Get revisionId of a commit.
- Use this method instead of GetRevisionId to get the id of the commit rather
- than the id of the current git object (for example, a tag)
- """
- if not self.revisionExpr.startswith(R_TAGS):
- return self.GetRevisionId(self._allrefs)
- try:
- return self.bare_git.rev_list(self.revisionExpr, '-1')[0]
- except GitError:
- raise ManifestInvalidRevisionError('revision %s in %s not found' %
- (self.revisionExpr, self.name))
- def GetRevisionId(self, all_refs=None):
- if self.revisionId:
- return self.revisionId
- rem = self.GetRemote(self.remote.name)
- rev = rem.ToLocal(self.revisionExpr)
- if all_refs is not None and rev in all_refs:
- return all_refs[rev]
- try:
- return self.bare_git.rev_parse('--verify', '%s^0' % rev)
- except GitError:
- raise ManifestInvalidRevisionError('revision %s in %s not found' %
- (self.revisionExpr, self.name))
- def Sync_LocalHalf(self, syncbuf, force_sync=False, submodules=False):
- """Perform only the local IO portion of the sync process.
- Network access is not required.
- """
- if not os.path.exists(self.gitdir):
- syncbuf.fail(self,
- 'Cannot checkout %s due to missing network sync; Run '
- '`repo sync -n %s` first.' %
- (self.name, self.name))
- return
- self._InitWorkTree(force_sync=force_sync, submodules=submodules)
- all_refs = self.bare_ref.all
- self.CleanPublishedCache(all_refs)
- revid = self.GetRevisionId(all_refs)
- def _doff():
- self._FastForward(revid)
- self._CopyAndLinkFiles()
- def _dosubmodules():
- self._SyncSubmodules(quiet=True)
- head = self.work_git.GetHead()
- if head.startswith(R_HEADS):
- branch = head[len(R_HEADS):]
- try:
- head = all_refs[head]
- except KeyError:
- head = None
- else:
- branch = None
- if branch is None or syncbuf.detach_head:
- # Currently on a detached HEAD. The user is assumed to
- # not have any local modifications worth worrying about.
- #
- if self.IsRebaseInProgress():
- syncbuf.fail(self, _PriorSyncFailedError())
- return
- if head == revid:
- # No changes; don't do anything further.
- # Except if the head needs to be detached
- #
- if not syncbuf.detach_head:
- # The copy/linkfile config may have changed.
- self._CopyAndLinkFiles()
- return
- else:
- lost = self._revlist(not_rev(revid), HEAD)
- if lost:
- syncbuf.info(self, "discarding %d commits", len(lost))
- try:
- self._Checkout(revid, quiet=True)
- if submodules:
- self._SyncSubmodules(quiet=True)
- except GitError as e:
- syncbuf.fail(self, e)
- return
- self._CopyAndLinkFiles()
- return
- if head == revid:
- # No changes; don't do anything further.
- #
- # The copy/linkfile config may have changed.
- self._CopyAndLinkFiles()
- return
- branch = self.GetBranch(branch)
- if not branch.LocalMerge:
- # The current branch has no tracking configuration.
- # Jump off it to a detached HEAD.
- #
- syncbuf.info(self,
- "leaving %s; does not track upstream",
- branch.name)
- try:
- self._Checkout(revid, quiet=True)
- if submodules:
- self._SyncSubmodules(quiet=True)
- except GitError as e:
- syncbuf.fail(self, e)
- return
- self._CopyAndLinkFiles()
- return
- upstream_gain = self._revlist(not_rev(HEAD), revid)
- # See if we can perform a fast forward merge. This can happen if our
- # branch isn't in the exact same state as we last published.
- try:
- self.work_git.merge_base('--is-ancestor', HEAD, revid)
- # Skip the published logic.
- pub = False
- except GitError:
- pub = self.WasPublished(branch.name, all_refs)
- if pub:
- not_merged = self._revlist(not_rev(revid), pub)
- if not_merged:
- if upstream_gain:
- # The user has published this branch and some of those
- # commits are not yet merged upstream. We do not want
- # to rewrite the published commits so we punt.
- #
- syncbuf.fail(self,
- "branch %s is published (but not merged) and is now "
- "%d commits behind" % (branch.name, len(upstream_gain)))
- return
- elif pub == head:
- # All published commits are merged, and thus we are a
- # strict subset. We can fast-forward safely.
- #
- syncbuf.later1(self, _doff)
- if submodules:
- syncbuf.later1(self, _dosubmodules)
- return
- # Examine the local commits not in the remote. Find the
- # last one attributed to this user, if any.
- #
- local_changes = self._revlist(not_rev(revid), HEAD, format='%H %ce')
- last_mine = None
- cnt_mine = 0
- for commit in local_changes:
- commit_id, committer_email = commit.split(' ', 1)
- if committer_email == self.UserEmail:
- last_mine = commit_id
- cnt_mine += 1
- if not upstream_gain and cnt_mine == len(local_changes):
- return
- if self.IsDirty(consider_untracked=False):
- syncbuf.fail(self, _DirtyError())
- return
- # If the upstream switched on us, warn the user.
- #
- if branch.merge != self.revisionExpr:
- if branch.merge and self.revisionExpr:
- syncbuf.info(self,
- 'manifest switched %s...%s',
- branch.merge,
- self.revisionExpr)
- elif branch.merge:
- syncbuf.info(self,
- 'manifest no longer tracks %s',
- branch.merge)
- if cnt_mine < len(local_changes):
- # Upstream rebased. Not everything in HEAD
- # was created by this user.
- #
- syncbuf.info(self,
- "discarding %d commits removed from upstream",
- len(local_changes) - cnt_mine)
- branch.remote = self.GetRemote(self.remote.name)
- if not ID_RE.match(self.revisionExpr):
- # in case of manifest sync the revisionExpr might be a SHA1
- branch.merge = self.revisionExpr
- if not branch.merge.startswith('refs/'):
- branch.merge = R_HEADS + branch.merge
- branch.Save()
- if cnt_mine > 0 and self.rebase:
- def _docopyandlink():
- self._CopyAndLinkFiles()
- def _dorebase():
- self._Rebase(upstream='%s^1' % last_mine, onto=revid)
- syncbuf.later2(self, _dorebase)
- if submodules:
- syncbuf.later2(self, _dosubmodules)
- syncbuf.later2(self, _docopyandlink)
- elif local_changes:
- try:
- self._ResetHard(revid)
- if submodules:
- self._SyncSubmodules(quiet=True)
- self._CopyAndLinkFiles()
- except GitError as e:
- syncbuf.fail(self, e)
- return
- else:
- syncbuf.later1(self, _doff)
- if submodules:
- syncbuf.later1(self, _dosubmodules)
- def AddCopyFile(self, src, dest, topdir):
- """Mark |src| for copying to |dest| (relative to |topdir|).
- No filesystem changes occur here. Actual copying happens later on.
- Paths should have basic validation run on them before being queued.
- Further checking will be handled when the actual copy happens.
- """
- self.copyfiles.append(_CopyFile(self.worktree, src, topdir, dest))
- def AddLinkFile(self, src, dest, topdir):
- """Mark |dest| to create a symlink (relative to |topdir|) pointing to |src|.
- No filesystem changes occur here. Actual linking happens later on.
- Paths should have basic validation run on them before being queued.
- Further checking will be handled when the actual link happens.
- """
- self.linkfiles.append(_LinkFile(self.worktree, src, topdir, dest))
- def AddAnnotation(self, name, value, keep):
- self.annotations.append(_Annotation(name, value, keep))
- def DownloadPatchSet(self, change_id, patch_id):
- """Download a single patch set of a single change to FETCH_HEAD.
- """
- remote = self.GetRemote(self.remote.name)
- cmd = ['fetch', remote.name]
- cmd.append('refs/changes/%2.2d/%d/%d'
- % (change_id % 100, change_id, patch_id))
- if GitCommand(self, cmd, bare=True).Wait() != 0:
- return None
- return DownloadedChange(self,
- self.GetRevisionId(),
- change_id,
- patch_id,
- self.bare_git.rev_parse('FETCH_HEAD'))
- def DeleteWorktree(self, quiet=False, force=False):
- """Delete the source checkout and any other housekeeping tasks.
- This currently leaves behind the internal .repo/ cache state. This helps
- when switching branches or manifest changes get reverted as we don't have
- to redownload all the git objects. But we should do some GC at some point.
- Args:
- quiet: Whether to hide normal messages.
- force: Always delete tree even if dirty.
- Returns:
- True if the worktree was completely cleaned out.
- """
- if self.IsDirty():
- if force:
- print('warning: %s: Removing dirty project: uncommitted changes lost.' %
- (self.relpath,), file=sys.stderr)
- else:
- print('error: %s: Cannot remove project: uncommitted changes are '
- 'present.\n' % (self.relpath,), file=sys.stderr)
- return False
- if not quiet:
- print('%s: Deleting obsolete checkout.' % (self.relpath,))
- # Unlock and delink from the main worktree. We don't use git's worktree
- # remove because it will recursively delete projects -- we handle that
- # ourselves below. https://crbug.com/git/48
- if self.use_git_worktrees:
- needle = platform_utils.realpath(self.gitdir)
- # Find the git worktree commondir under .repo/worktrees/.
- output = self.bare_git.worktree('list', '--porcelain').splitlines()[0]
- assert output.startswith('worktree '), output
- commondir = output[9:]
- # Walk each of the git worktrees to see where they point.
- configs = os.path.join(commondir, 'worktrees')
- for name in os.listdir(configs):
- gitdir = os.path.join(configs, name, 'gitdir')
- with open(gitdir) as fp:
- relpath = fp.read().strip()
- # Resolve the checkout path and see if it matches this project.
- fullpath = platform_utils.realpath(os.path.join(configs, name, relpath))
- if fullpath == needle:
- platform_utils.rmtree(os.path.join(configs, name))
- # Delete the .git directory first, so we're less likely to have a partially
- # working git repository around. There shouldn't be any git projects here,
- # so rmtree works.
- # Try to remove plain files first in case of git worktrees. If this fails
- # for any reason, we'll fall back to rmtree, and that'll display errors if
- # it can't remove things either.
- try:
- platform_utils.remove(self.gitdir)
- except OSError:
- pass
- try:
- platform_utils.rmtree(self.gitdir)
- except OSError as e:
- if e.errno != errno.ENOENT:
- print('error: %s: %s' % (self.gitdir, e), file=sys.stderr)
- print('error: %s: Failed to delete obsolete checkout; remove manually, '
- 'then run `repo sync -l`.' % (self.relpath,), file=sys.stderr)
- return False
- # Delete everything under the worktree, except for directories that contain
- # another git project.
- dirs_to_remove = []
- failed = False
- for root, dirs, files in platform_utils.walk(self.worktree):
- for f in files:
- path = os.path.join(root, f)
- try:
- platform_utils.remove(path)
- except OSError as e:
- if e.errno != errno.ENOENT:
- print('error: %s: Failed to remove: %s' % (path, e), file=sys.stderr)
- failed = True
- dirs[:] = [d for d in dirs
- if not os.path.lexists(os.path.join(root, d, '.git'))]
- dirs_to_remove += [os.path.join(root, d) for d in dirs
- if os.path.join(root, d) not in dirs_to_remove]
- for d in reversed(dirs_to_remove):
- if platform_utils.islink(d):
- try:
- platform_utils.remove(d)
- except OSError as e:
- if e.errno != errno.ENOENT:
- print('error: %s: Failed to remove: %s' % (d, e), file=sys.stderr)
- failed = True
- elif not platform_utils.listdir(d):
- try:
- platform_utils.rmdir(d)
- except OSError as e:
- if e.errno != errno.ENOENT:
- print('error: %s: Failed to remove: %s' % (d, e), file=sys.stderr)
- failed = True
- if failed:
- print('error: %s: Failed to delete obsolete checkout.' % (self.relpath,),
- file=sys.stderr)
- print(' Remove manually, then run `repo sync -l`.', file=sys.stderr)
- return False
- # Try deleting parent dirs if they are empty.
- path = self.worktree
- while path != self.manifest.topdir:
- try:
- platform_utils.rmdir(path)
- except OSError as e:
- if e.errno != errno.ENOENT:
- break
- path = os.path.dirname(path)
- return True
- # Branch Management ##
- def StartBranch(self, name, branch_merge='', revision=None):
- """Create a new branch off the manifest's revision.
- """
- if not branch_merge:
- branch_merge = self.revisionExpr
- head = self.work_git.GetHead()
- if head == (R_HEADS + name):
- return True
- all_refs = self.bare_ref.all
- if R_HEADS + name in all_refs:
- return GitCommand(self,
- ['checkout', name, '--'],
- capture_stdout=True,
- capture_stderr=True).Wait() == 0
- branch = self.GetBranch(name)
- branch.remote = self.GetRemote(self.remote.name)
- branch.merge = branch_merge
- if not branch.merge.startswith('refs/') and not ID_RE.match(branch_merge):
- branch.merge = R_HEADS + branch_merge
- if revision is None:
- revid = self.GetRevisionId(all_refs)
- else:
- revid = self.work_git.rev_parse(revision)
- if head.startswith(R_HEADS):
- try:
- head = all_refs[head]
- except KeyError:
- head = None
- if revid and head and revid == head:
- ref = R_HEADS + name
- self.work_git.update_ref(ref, revid)
- self.work_git.symbolic_ref(HEAD, ref)
- branch.Save()
- return True
- if GitCommand(self,
- ['checkout', '-b', branch.name, revid],
- capture_stdout=True,
- capture_stderr=True).Wait() == 0:
- branch.Save()
- return True
- return False
- def CheckoutBranch(self, name):
- """Checkout a local topic branch.
- Args:
- name: The name of the branch to checkout.
- Returns:
- True if the checkout succeeded; False if it didn't; None if the branch
- didn't exist.
- """
- rev = R_HEADS + name
- head = self.work_git.GetHead()
- if head == rev:
- # Already on the branch
- #
- return True
- all_refs = self.bare_ref.all
- try:
- revid = all_refs[rev]
- except KeyError:
- # Branch does not exist in this project
- #
- return None
- if head.startswith(R_HEADS):
- try:
- head = all_refs[head]
- except KeyError:
- head = None
- if head == revid:
- # Same revision; just update HEAD to point to the new
- # target branch, but otherwise take no other action.
- #
- _lwrite(self.work_git.GetDotgitPath(subpath=HEAD),
- 'ref: %s%s\n' % (R_HEADS, name))
- return True
- return GitCommand(self,
- ['checkout', name, '--'],
- capture_stdout=True,
- capture_stderr=True).Wait() == 0
- def AbandonBranch(self, name):
- """Destroy a local topic branch.
- Args:
- name: The name of the branch to abandon.
- Returns:
- True if the abandon succeeded; False if it didn't; None if the branch
- didn't exist.
- """
- rev = R_HEADS + name
- all_refs = self.bare_ref.all
- if rev not in all_refs:
- # Doesn't exist
- return None
- head = self.work_git.GetHead()
- if head == rev:
- # We can't destroy the branch while we are sitting
- # on it. Switch to a detached HEAD.
- #
- head = all_refs[head]
- revid = self.GetRevisionId(all_refs)
- if head == revid:
- _lwrite(self.work_git.GetDotgitPath(subpath=HEAD), '%s\n' % revid)
- else:
- self._Checkout(revid, quiet=True)
- return GitCommand(self,
- ['branch', '-D', name],
- capture_stdout=True,
- capture_stderr=True).Wait() == 0
- def PruneHeads(self):
- """Prune any topic branches already merged into upstream.
- """
- cb = self.CurrentBranch
- kill = []
- left = self._allrefs
- for name in left.keys():
- if name.startswith(R_HEADS):
- name = name[len(R_HEADS):]
- if cb is None or name != cb:
- kill.append(name)
- rev = self.GetRevisionId(left)
- if cb is not None \
- and not self._revlist(HEAD + '...' + rev) \
- and not self.IsDirty(consider_untracked=False):
- self.work_git.DetachHead(HEAD)
- kill.append(cb)
- if kill:
- old = self.bare_git.GetHead()
- try:
- self.bare_git.DetachHead(rev)
- b = ['branch', '-d']
- b.extend(kill)
- b = GitCommand(self, b, bare=True,
- capture_stdout=True,
- capture_stderr=True)
- b.Wait()
- finally:
- if ID_RE.match(old):
- self.bare_git.DetachHead(old)
- else:
- self.bare_git.SetHead(old)
- left = self._allrefs
- for branch in kill:
- if (R_HEADS + branch) not in left:
- self.CleanPublishedCache()
- break
- if cb and cb not in kill:
- kill.append(cb)
- kill.sort()
- kept = []
- for branch in kill:
- if R_HEADS + branch in left:
- branch = self.GetBranch(branch)
- base = branch.LocalMerge
- if not base:
- base = rev
- kept.append(ReviewableBranch(self, branch, base))
- return kept
- # Submodule Management ##
- def GetRegisteredSubprojects(self):
- result = []
- def rec(subprojects):
- if not subprojects:
- return
- result.extend(subprojects)
- for p in subprojects:
- rec(p.subprojects)
- rec(self.subprojects)
- return result
- def _GetSubmodules(self):
- # Unfortunately we cannot call `git submodule status --recursive` here
- # because the working tree might not exist yet, and it cannot be used
- # without a working tree in its current implementation.
- def get_submodules(gitdir, rev):
- # Parse .gitmodules for submodule sub_paths and sub_urls
- sub_paths, sub_urls = parse_gitmodules(gitdir, rev)
- if not sub_paths:
- return []
- # Run `git ls-tree` to read SHAs of submodule object, which happen to be
- # revision of submodule repository
- sub_revs = git_ls_tree(gitdir, rev, sub_paths)
- submodules = []
- for sub_path, sub_url in zip(sub_paths, sub_urls):
- try:
- sub_rev = sub_revs[sub_path]
- except KeyError:
- # Ignore non-exist submodules
- continue
- submodules.append((sub_rev, sub_path, sub_url))
- return submodules
- re_path = re.compile(r'^submodule\.(.+)\.path=(.*)$')
- re_url = re.compile(r'^submodule\.(.+)\.url=(.*)$')
- def parse_gitmodules(gitdir, rev):
- cmd = ['cat-file', 'blob', '%s:.gitmodules' % rev]
- try:
- p = GitCommand(None, cmd, capture_stdout=True, capture_stderr=True,
- bare=True, gitdir=gitdir)
- except GitError:
- return [], []
- if p.Wait() != 0:
- return [], []
- gitmodules_lines = []
- fd, temp_gitmodules_path = tempfile.mkstemp()
- try:
- os.write(fd, p.stdout.encode('utf-8'))
- os.close(fd)
- cmd = ['config', '--file', temp_gitmodules_path, '--list']
- p = GitCommand(None, cmd, capture_stdout=True, capture_stderr=True,
- bare=True, gitdir=gitdir)
- if p.Wait() != 0:
- return [], []
- gitmodules_lines = p.stdout.split('\n')
- except GitError:
- return [], []
- finally:
- platform_utils.remove(temp_gitmodules_path)
- names = set()
- paths = {}
- urls = {}
- for line in gitmodules_lines:
- if not line:
- continue
- m = re_path.match(line)
- if m:
- names.add(m.group(1))
- paths[m.group(1)] = m.group(2)
- continue
- m = re_url.match(line)
- if m:
- names.add(m.group(1))
- urls[m.group(1)] = m.group(2)
- continue
- names = sorted(names)
- return ([paths.get(name, '') for name in names],
- [urls.get(name, '') for name in names])
- def git_ls_tree(gitdir, rev, paths):
- cmd = ['ls-tree', rev, '--']
- cmd.extend(paths)
- try:
- p = GitCommand(None, cmd, capture_stdout=True, capture_stderr=True,
- bare=True, gitdir=gitdir)
- except GitError:
- return []
- if p.Wait() != 0:
- return []
- objects = {}
- for line in p.stdout.split('\n'):
- if not line.strip():
- continue
- object_rev, object_path = line.split()[2:4]
- objects[object_path] = object_rev
- return objects
- try:
- rev = self.GetRevisionId()
- except GitError:
- return []
- return get_submodules(self.gitdir, rev)
- def GetDerivedSubprojects(self):
- result = []
- if not self.Exists:
- # If git repo does not exist yet, querying its submodules will
- # mess up its states; so return here.
- return result
- for rev, path, url in self._GetSubmodules():
- name = self.manifest.GetSubprojectName(self, path)
- relpath, worktree, gitdir, objdir = \
- self.manifest.GetSubprojectPaths(self, name, path)
- project = self.manifest.paths.get(relpath)
- if project:
- result.extend(project.GetDerivedSubprojects())
- continue
- if url.startswith('..'):
- url = urllib.parse.urljoin("%s/" % self.remote.url, url)
- remote = RemoteSpec(self.remote.name,
- url=url,
- pushUrl=self.remote.pushUrl,
- review=self.remote.review,
- revision=self.remote.revision)
- subproject = Project(manifest=self.manifest,
- name=name,
- remote=remote,
- gitdir=gitdir,
- objdir=objdir,
- worktree=worktree,
- relpath=relpath,
- revisionExpr=rev,
- revisionId=rev,
- rebase=self.rebase,
- groups=self.groups,
- sync_c=self.sync_c,
- sync_s=self.sync_s,
- sync_tags=self.sync_tags,
- parent=self,
- is_derived=True)
- result.append(subproject)
- result.extend(subproject.GetDerivedSubprojects())
- return result
- # Direct Git Commands ##
- def EnableRepositoryExtension(self, key, value='true', version=1):
- """Enable git repository extension |key| with |value|.
- Args:
- key: The extension to enabled. Omit the "extensions." prefix.
- value: The value to use for the extension.
- version: The minimum git repository version needed.
- """
- # Make sure the git repo version is new enough already.
- found_version = self.config.GetInt('core.repositoryFormatVersion')
- if found_version is None:
- found_version = 0
- if found_version < version:
- self.config.SetString('core.repositoryFormatVersion', str(version))
- # Enable the extension!
- self.config.SetString('extensions.%s' % (key,), value)
- def ResolveRemoteHead(self, name=None):
- """Find out what the default branch (HEAD) points to.
- Normally this points to refs/heads/master, but projects are moving to main.
- Support whatever the server uses rather than hardcoding "master" ourselves.
- """
- if name is None:
- name = self.remote.name
- # The output will look like (NB: tabs are separators):
- # ref: refs/heads/master HEAD
- # 5f6803b100bb3cd0f534e96e88c91373e8ed1c44 HEAD
- output = self.bare_git.ls_remote('-q', '--symref', '--exit-code', name, 'HEAD')
- for line in output.splitlines():
- lhs, rhs = line.split('\t', 1)
- if rhs == 'HEAD' and lhs.startswith('ref:'):
- return lhs[4:].strip()
- return None
- def _CheckForImmutableRevision(self):
- try:
- # if revision (sha or tag) is not present then following function
- # throws an error.
- self.bare_git.rev_parse('--verify', '%s^0' % self.revisionExpr)
- return True
- except GitError:
- # There is no such persistent revision. We have to fetch it.
- return False
- def _FetchArchive(self, tarpath, cwd=None):
- cmd = ['archive', '-v', '-o', tarpath]
- cmd.append('--remote=%s' % self.remote.url)
- cmd.append('--prefix=%s/' % self.relpath)
- cmd.append(self.revisionExpr)
- command = GitCommand(self, cmd, cwd=cwd,
- capture_stdout=True,
- capture_stderr=True)
- if command.Wait() != 0:
- raise GitError('git archive %s: %s' % (self.name, command.stderr))
- def _RemoteFetch(self, name=None,
- current_branch_only=False,
- initial=False,
- quiet=False,
- verbose=False,
- alt_dir=None,
- tags=True,
- prune=False,
- depth=None,
- submodules=False,
- force_sync=False,
- clone_filter=None,
- retry_fetches=2,
- retry_sleep_initial_sec=4.0,
- retry_exp_factor=2.0):
- is_sha1 = False
- tag_name = None
- # The depth should not be used when fetching to a mirror because
- # it will result in a shallow repository that cannot be cloned or
- # fetched from.
- # The repo project should also never be synced with partial depth.
- if self.manifest.IsMirror or self.relpath == '.repo/repo':
- depth = None
- if depth:
- current_branch_only = True
- if ID_RE.match(self.revisionExpr) is not None:
- is_sha1 = True
- if current_branch_only:
- if self.revisionExpr.startswith(R_TAGS):
- # this is a tag and its sha1 value should never change
- tag_name = self.revisionExpr[len(R_TAGS):]
- if is_sha1 or tag_name is not None:
- if self._CheckForImmutableRevision():
- if verbose:
- print('Skipped fetching project %s (already have persistent ref)'
- % self.name)
- return True
- if is_sha1 and not depth:
- # When syncing a specific commit and --depth is not set:
- # * if upstream is explicitly specified and is not a sha1, fetch only
- # upstream as users expect only upstream to be fetch.
- # Note: The commit might not be in upstream in which case the sync
- # will fail.
- # * otherwise, fetch all branches to make sure we end up with the
- # specific commit.
- if self.upstream:
- current_branch_only = not ID_RE.match(self.upstream)
- else:
- current_branch_only = False
- if not name:
- name = self.remote.name
- ssh_proxy = False
- remote = self.GetRemote(name)
- if remote.PreConnectFetch():
- ssh_proxy = True
- if initial:
- if alt_dir and 'objects' == os.path.basename(alt_dir):
- ref_dir = os.path.dirname(alt_dir)
- packed_refs = os.path.join(self.gitdir, 'packed-refs')
- remote = self.GetRemote(name)
- all_refs = self.bare_ref.all
- ids = set(all_refs.values())
- tmp = set()
- for r, ref_id in GitRefs(ref_dir).all.items():
- if r not in all_refs:
- if r.startswith(R_TAGS) or remote.WritesTo(r):
- all_refs[r] = ref_id
- ids.add(ref_id)
- continue
- if ref_id in ids:
- continue
- r = 'refs/_alt/%s' % ref_id
- all_refs[r] = ref_id
- ids.add(ref_id)
- tmp.add(r)
- tmp_packed_lines = []
- old_packed_lines = []
- for r in sorted(all_refs):
- line = '%s %s\n' % (all_refs[r], r)
- tmp_packed_lines.append(line)
- if r not in tmp:
- old_packed_lines.append(line)
- tmp_packed = ''.join(tmp_packed_lines)
- old_packed = ''.join(old_packed_lines)
- _lwrite(packed_refs, tmp_packed)
- else:
- alt_dir = None
- cmd = ['fetch']
- if clone_filter:
- git_require((2, 19, 0), fail=True, msg='partial clones')
- cmd.append('--filter=%s' % clone_filter)
- self.EnableRepositoryExtension('partialclone', self.remote.name)
- if depth:
- cmd.append('--depth=%s' % depth)
- else:
- # If this repo has shallow objects, then we don't know which refs have
- # shallow objects or not. Tell git to unshallow all fetched refs. Don't
- # do this with projects that don't have shallow objects, since it is less
- # efficient.
- if os.path.exists(os.path.join(self.gitdir, 'shallow')):
- cmd.append('--depth=2147483647')
- if not verbose:
- cmd.append('--quiet')
- if not quiet and sys.stdout.isatty():
- cmd.append('--progress')
- if not self.worktree:
- cmd.append('--update-head-ok')
- cmd.append(name)
- if force_sync:
- cmd.append('--force')
- if prune:
- cmd.append('--prune')
- if submodules:
- cmd.append('--recurse-submodules=on-demand')
- spec = []
- if not current_branch_only:
- # Fetch whole repo
- spec.append(str((u'+refs/heads/*:') + remote.ToLocal('refs/heads/*')))
- elif tag_name is not None:
- spec.append('tag')
- spec.append(tag_name)
- if self.manifest.IsMirror and not current_branch_only:
- branch = None
- else:
- branch = self.revisionExpr
- if (not self.manifest.IsMirror and is_sha1 and depth
- and git_require((1, 8, 3))):
- # Shallow checkout of a specific commit, fetch from that commit and not
- # the heads only as the commit might be deeper in the history.
- spec.append(branch)
- else:
- if is_sha1:
- branch = self.upstream
- if branch is not None and branch.strip():
- if not branch.startswith('refs/'):
- branch = R_HEADS + branch
- spec.append(str((u'+%s:' % branch) + remote.ToLocal(branch)))
- # If mirroring repo and we cannot deduce the tag or branch to fetch, fetch
- # whole repo.
- if self.manifest.IsMirror and not spec:
- spec.append(str((u'+refs/heads/*:') + remote.ToLocal('refs/heads/*')))
- # If using depth then we should not get all the tags since they may
- # be outside of the depth.
- if not tags or depth:
- cmd.append('--no-tags')
- else:
- cmd.append('--tags')
- spec.append(str((u'+refs/tags/*:') + remote.ToLocal('refs/tags/*')))
- cmd.extend(spec)
- # At least one retry minimum due to git remote prune.
- retry_fetches = max(retry_fetches, 2)
- retry_cur_sleep = retry_sleep_initial_sec
- ok = prune_tried = False
- for try_n in range(retry_fetches):
- gitcmd = GitCommand(self, cmd, bare=True, ssh_proxy=ssh_proxy,
- merge_output=True, capture_stdout=quiet)
- ret = gitcmd.Wait()
- if ret == 0:
- ok = True
- break
- # Retry later due to HTTP 429 Too Many Requests.
- elif ('error:' in gitcmd.stderr and
- 'HTTP 429' in gitcmd.stderr):
- if not quiet:
- print('429 received, sleeping: %s sec' % retry_cur_sleep,
- file=sys.stderr)
- time.sleep(retry_cur_sleep)
- retry_cur_sleep = min(retry_exp_factor * retry_cur_sleep,
- MAXIMUM_RETRY_SLEEP_SEC)
- retry_cur_sleep *= (1 - random.uniform(-RETRY_JITTER_PERCENT,
- RETRY_JITTER_PERCENT))
- continue
- # If this is not last attempt, try 'git remote prune'.
- elif (try_n < retry_fetches - 1 and
- 'error:' in gitcmd.stderr and
- 'git remote prune' in gitcmd.stderr and
- not prune_tried):
- prune_tried = True
- prunecmd = GitCommand(self, ['remote', 'prune', name], bare=True,
- ssh_proxy=ssh_proxy)
- ret = prunecmd.Wait()
- if ret:
- break
- continue
- elif current_branch_only and is_sha1 and ret == 128:
- # Exit code 128 means "couldn't find the ref you asked for"; if we're
- # in sha1 mode, we just tried sync'ing from the upstream field; it
- # doesn't exist, thus abort the optimization attempt and do a full sync.
- break
- elif ret < 0:
- # Git died with a signal, exit immediately
- break
- if not verbose:
- print('%s:\n%s' % (self.name, gitcmd.stdout), file=sys.stderr)
- time.sleep(random.randint(30, 45))
- if initial:
- if alt_dir:
- if old_packed != '':
- _lwrite(packed_refs, old_packed)
- else:
- platform_utils.remove(packed_refs)
- self.bare_git.pack_refs('--all', '--prune')
- if is_sha1 and current_branch_only:
- # We just synced the upstream given branch; verify we
- # got what we wanted, else trigger a second run of all
- # refs.
- if not self._CheckForImmutableRevision():
- # Sync the current branch only with depth set to None.
- # We always pass depth=None down to avoid infinite recursion.
- return self._RemoteFetch(
- name=name, quiet=quiet, verbose=verbose,
- current_branch_only=current_branch_only and depth,
- initial=False, alt_dir=alt_dir,
- depth=None, clone_filter=clone_filter)
- return ok
- def _ApplyCloneBundle(self, initial=False, quiet=False, verbose=False):
- if initial and \
- (self.manifest.manifestProject.config.GetString('repo.depth') or
- self.clone_depth):
- return False
- remote = self.GetRemote(self.remote.name)
- bundle_url = remote.url + '/clone.bundle'
- bundle_url = GitConfig.ForUser().UrlInsteadOf(bundle_url)
- if GetSchemeFromUrl(bundle_url) not in ('http', 'https',
- 'persistent-http',
- 'persistent-https'):
- return False
- bundle_dst = os.path.join(self.gitdir, 'clone.bundle')
- bundle_tmp = os.path.join(self.gitdir, 'clone.bundle.tmp')
- exist_dst = os.path.exists(bundle_dst)
- exist_tmp = os.path.exists(bundle_tmp)
- if not initial and not exist_dst and not exist_tmp:
- return False
- if not exist_dst:
- exist_dst = self._FetchBundle(bundle_url, bundle_tmp, bundle_dst, quiet,
- verbose)
- if not exist_dst:
- return False
- cmd = ['fetch']
- if not verbose:
- cmd.append('--quiet')
- if not quiet and sys.stdout.isatty():
- cmd.append('--progress')
- if not self.worktree:
- cmd.append('--update-head-ok')
- cmd.append(bundle_dst)
- for f in remote.fetch:
- cmd.append(str(f))
- cmd.append('+refs/tags/*:refs/tags/*')
- ok = GitCommand(self, cmd, bare=True).Wait() == 0
- if os.path.exists(bundle_dst):
- platform_utils.remove(bundle_dst)
- if os.path.exists(bundle_tmp):
- platform_utils.remove(bundle_tmp)
- return ok
- def _FetchBundle(self, srcUrl, tmpPath, dstPath, quiet, verbose):
- if os.path.exists(dstPath):
- platform_utils.remove(dstPath)
- cmd = ['curl', '--fail', '--output', tmpPath, '--netrc', '--location']
- if quiet:
- cmd += ['--silent', '--show-error']
- if os.path.exists(tmpPath):
- size = os.stat(tmpPath).st_size
- if size >= 1024:
- cmd += ['--continue-at', '%d' % (size,)]
- else:
- platform_utils.remove(tmpPath)
- with GetUrlCookieFile(srcUrl, quiet) as (cookiefile, proxy):
- if cookiefile:
- cmd += ['--cookie', cookiefile]
- if proxy:
- cmd += ['--proxy', proxy]
- elif 'http_proxy' in os.environ and 'darwin' == sys.platform:
- cmd += ['--proxy', os.environ['http_proxy']]
- if srcUrl.startswith('persistent-https'):
- srcUrl = 'http' + srcUrl[len('persistent-https'):]
- elif srcUrl.startswith('persistent-http'):
- srcUrl = 'http' + srcUrl[len('persistent-http'):]
- cmd += [srcUrl]
- if IsTrace():
- Trace('%s', ' '.join(cmd))
- if verbose:
- print('%s: Downloading bundle: %s' % (self.name, srcUrl))
- stdout = None if verbose else subprocess.PIPE
- stderr = None if verbose else subprocess.STDOUT
- try:
- proc = subprocess.Popen(cmd, stdout=stdout, stderr=stderr)
- except OSError:
- return False
- (output, _) = proc.communicate()
- curlret = proc.returncode
- if curlret == 22:
- # From curl man page:
- # 22: HTTP page not retrieved. The requested url was not found or
- # returned another error with the HTTP error code being 400 or above.
- # This return code only appears if -f, --fail is used.
- if verbose:
- print('%s: Unable to retrieve clone.bundle; ignoring.' % self.name)
- if output:
- print('Curl output:\n%s' % output)
- return False
- elif curlret and not verbose and output:
- print('%s' % output, file=sys.stderr)
- if os.path.exists(tmpPath):
- if curlret == 0 and self._IsValidBundle(tmpPath, quiet):
- platform_utils.rename(tmpPath, dstPath)
- return True
- else:
- platform_utils.remove(tmpPath)
- return False
- else:
- return False
- def _IsValidBundle(self, path, quiet):
- try:
- with open(path, 'rb') as f:
- if f.read(16) == b'# v2 git bundle\n':
- return True
- else:
- if not quiet:
- print("Invalid clone.bundle file; ignoring.", file=sys.stderr)
- return False
- except OSError:
- return False
- def _Checkout(self, rev, quiet=False):
- cmd = ['checkout']
- if quiet:
- cmd.append('-q')
- cmd.append(rev)
- cmd.append('--')
- if GitCommand(self, cmd).Wait() != 0:
- if self._allrefs:
- raise GitError('%s checkout %s ' % (self.name, rev))
- def _CherryPick(self, rev, ffonly=False, record_origin=False):
- cmd = ['cherry-pick']
- if ffonly:
- cmd.append('--ff')
- if record_origin:
- cmd.append('-x')
- cmd.append(rev)
- cmd.append('--')
- if GitCommand(self, cmd).Wait() != 0:
- if self._allrefs:
- raise GitError('%s cherry-pick %s ' % (self.name, rev))
- def _LsRemote(self, refs):
- cmd = ['ls-remote', self.remote.name, refs]
- p = GitCommand(self, cmd, capture_stdout=True)
- if p.Wait() == 0:
- return p.stdout
- return None
- def _Revert(self, rev):
- cmd = ['revert']
- cmd.append('--no-edit')
- cmd.append(rev)
- cmd.append('--')
- if GitCommand(self, cmd).Wait() != 0:
- if self._allrefs:
- raise GitError('%s revert %s ' % (self.name, rev))
- def _ResetHard(self, rev, quiet=True):
- cmd = ['reset', '--hard']
- if quiet:
- cmd.append('-q')
- cmd.append(rev)
- if GitCommand(self, cmd).Wait() != 0:
- raise GitError('%s reset --hard %s ' % (self.name, rev))
- def _SyncSubmodules(self, quiet=True):
- cmd = ['submodule', 'update', '--init', '--recursive']
- if quiet:
- cmd.append('-q')
- if GitCommand(self, cmd).Wait() != 0:
- raise GitError('%s submodule update --init --recursive %s ' % self.name)
- def _Rebase(self, upstream, onto=None):
- cmd = ['rebase']
- if onto is not None:
- cmd.extend(['--onto', onto])
- cmd.append(upstream)
- if GitCommand(self, cmd).Wait() != 0:
- raise GitError('%s rebase %s ' % (self.name, upstream))
- def _FastForward(self, head, ffonly=False):
- cmd = ['merge', '--no-stat', head]
- if ffonly:
- cmd.append("--ff-only")
- if GitCommand(self, cmd).Wait() != 0:
- raise GitError('%s merge %s ' % (self.name, head))
- def _InitGitDir(self, mirror_git=None, force_sync=False, quiet=False):
- init_git_dir = not os.path.exists(self.gitdir)
- init_obj_dir = not os.path.exists(self.objdir)
- try:
- # Initialize the bare repository, which contains all of the objects.
- if init_obj_dir:
- os.makedirs(self.objdir)
- self.bare_objdir.init()
- if self.use_git_worktrees:
- # Set up the m/ space to point to the worktree-specific ref space.
- # We'll update the worktree-specific ref space on each checkout.
- if self.manifest.branch:
- self.bare_git.symbolic_ref(
- '-m', 'redirecting to worktree scope',
- R_M + self.manifest.branch,
- R_WORKTREE_M + self.manifest.branch)
- # Enable per-worktree config file support if possible. This is more a
- # nice-to-have feature for users rather than a hard requirement.
- if git_require((2, 20, 0)):
- self.EnableRepositoryExtension('worktreeConfig')
- # If we have a separate directory to hold refs, initialize it as well.
- if self.objdir != self.gitdir:
- if init_git_dir:
- os.makedirs(self.gitdir)
- if init_obj_dir or init_git_dir:
- self._ReferenceGitDir(self.objdir, self.gitdir, share_refs=False,
- copy_all=True)
- try:
- self._CheckDirReference(self.objdir, self.gitdir, share_refs=False)
- except GitError as e:
- if force_sync:
- print("Retrying clone after deleting %s" %
- self.gitdir, file=sys.stderr)
- try:
- platform_utils.rmtree(platform_utils.realpath(self.gitdir))
- if self.worktree and os.path.exists(platform_utils.realpath
- (self.worktree)):
- platform_utils.rmtree(platform_utils.realpath(self.worktree))
- return self._InitGitDir(mirror_git=mirror_git, force_sync=False,
- quiet=quiet)
- except Exception:
- raise e
- raise e
- if init_git_dir:
- mp = self.manifest.manifestProject
- ref_dir = mp.config.GetString('repo.reference') or ''
- if ref_dir or mirror_git:
- if not mirror_git:
- mirror_git = os.path.join(ref_dir, self.name + '.git')
- repo_git = os.path.join(ref_dir, '.repo', 'projects',
- self.relpath + '.git')
- worktrees_git = os.path.join(ref_dir, '.repo', 'worktrees',
- self.name + '.git')
- if os.path.exists(mirror_git):
- ref_dir = mirror_git
- elif os.path.exists(repo_git):
- ref_dir = repo_git
- elif os.path.exists(worktrees_git):
- ref_dir = worktrees_git
- else:
- ref_dir = None
- if ref_dir:
- if not os.path.isabs(ref_dir):
- # The alternate directory is relative to the object database.
- ref_dir = os.path.relpath(ref_dir,
- os.path.join(self.objdir, 'objects'))
- _lwrite(os.path.join(self.gitdir, 'objects/info/alternates'),
- os.path.join(ref_dir, 'objects') + '\n')
- self._UpdateHooks(quiet=quiet)
- m = self.manifest.manifestProject.config
- for key in ['user.name', 'user.email']:
- if m.Has(key, include_defaults=False):
- self.config.SetString(key, m.GetString(key))
- self.config.SetString('filter.lfs.smudge', 'git-lfs smudge --skip -- %f')
- self.config.SetString('filter.lfs.process', 'git-lfs filter-process --skip')
- if self.manifest.IsMirror:
- self.config.SetString('core.bare', 'true')
- else:
- self.config.SetString('core.bare', None)
- except Exception:
- if init_obj_dir and os.path.exists(self.objdir):
- platform_utils.rmtree(self.objdir)
- if init_git_dir and os.path.exists(self.gitdir):
- platform_utils.rmtree(self.gitdir)
- raise
- def _UpdateHooks(self, quiet=False):
- if os.path.exists(self.gitdir):
- self._InitHooks(quiet=quiet)
- def _InitHooks(self, quiet=False):
- hooks = platform_utils.realpath(self._gitdir_path('hooks'))
- if not os.path.exists(hooks):
- os.makedirs(hooks)
- for stock_hook in _ProjectHooks():
- name = os.path.basename(stock_hook)
- if name in ('commit-msg',) and not self.remote.review \
- and self is not self.manifest.manifestProject:
- # Don't install a Gerrit Code Review hook if this
- # project does not appear to use it for reviews.
- #
- # Since the manifest project is one of those, but also
- # managed through gerrit, it's excluded
- continue
- dst = os.path.join(hooks, name)
- if platform_utils.islink(dst):
- continue
- if os.path.exists(dst):
- # If the files are the same, we'll leave it alone. We create symlinks
- # below by default but fallback to hardlinks if the OS blocks them.
- # So if we're here, it's probably because we made a hardlink below.
- if not filecmp.cmp(stock_hook, dst, shallow=False):
- if not quiet:
- _warn("%s: Not replacing locally modified %s hook",
- self.relpath, name)
- continue
- try:
- platform_utils.symlink(
- os.path.relpath(stock_hook, os.path.dirname(dst)), dst)
- except OSError as e:
- if e.errno == errno.EPERM:
- try:
- os.link(stock_hook, dst)
- except OSError:
- raise GitError(self._get_symlink_error_message())
- else:
- raise
- def _InitRemote(self):
- if self.remote.url:
- remote = self.GetRemote(self.remote.name)
- remote.url = self.remote.url
- remote.pushUrl = self.remote.pushUrl
- remote.review = self.remote.review
- remote.projectname = self.name
- if self.worktree:
- remote.ResetFetch(mirror=False)
- else:
- remote.ResetFetch(mirror=True)
- remote.Save()
- def _InitMRef(self):
- if self.manifest.branch:
- if self.use_git_worktrees:
- # We can't update this ref with git worktrees until it exists.
- # We'll wait until the initial checkout to set it.
- if not os.path.exists(self.worktree):
- return
- base = R_WORKTREE_M
- active_git = self.work_git
- self._InitAnyMRef(HEAD, self.bare_git, detach=True)
- else:
- base = R_M
- active_git = self.bare_git
- self._InitAnyMRef(base + self.manifest.branch, active_git)
- def _InitMirrorHead(self):
- self._InitAnyMRef(HEAD, self.bare_git)
- def _InitAnyMRef(self, ref, active_git, detach=False):
- cur = self.bare_ref.symref(ref)
- if self.revisionId:
- if cur != '' or self.bare_ref.get(ref) != self.revisionId:
- msg = 'manifest set to %s' % self.revisionId
- dst = self.revisionId + '^0'
- active_git.UpdateRef(ref, dst, message=msg, detach=True)
- else:
- remote = self.GetRemote(self.remote.name)
- dst = remote.ToLocal(self.revisionExpr)
- if cur != dst:
- msg = 'manifest set to %s' % self.revisionExpr
- if detach:
- active_git.UpdateRef(ref, dst, message=msg, detach=True)
- else:
- active_git.symbolic_ref('-m', msg, ref, dst)
- def _CheckDirReference(self, srcdir, destdir, share_refs):
- # Git worktrees don't use symlinks to share at all.
- if self.use_git_worktrees:
- return
- symlink_files = self.shareable_files[:]
- symlink_dirs = self.shareable_dirs[:]
- if share_refs:
- symlink_files += self.working_tree_files
- symlink_dirs += self.working_tree_dirs
- to_symlink = symlink_files + symlink_dirs
- for name in set(to_symlink):
- # Try to self-heal a bit in simple cases.
- dst_path = os.path.join(destdir, name)
- src_path = os.path.join(srcdir, name)
- if name in self.working_tree_dirs:
- # If the dir is missing under .repo/projects/, create it.
- if not os.path.exists(src_path):
- os.makedirs(src_path)
- elif name in self.working_tree_files:
- # If it's a file under the checkout .git/ and the .repo/projects/ has
- # nothing, move the file under the .repo/projects/ tree.
- if not os.path.exists(src_path) and os.path.isfile(dst_path):
- platform_utils.rename(dst_path, src_path)
- # If the path exists under the .repo/projects/ and there's no symlink
- # under the checkout .git/, recreate the symlink.
- if name in self.working_tree_dirs or name in self.working_tree_files:
- if os.path.exists(src_path) and not os.path.exists(dst_path):
- platform_utils.symlink(
- os.path.relpath(src_path, os.path.dirname(dst_path)), dst_path)
- dst = platform_utils.realpath(dst_path)
- if os.path.lexists(dst):
- src = platform_utils.realpath(src_path)
- # Fail if the links are pointing to the wrong place
- if src != dst:
- _error('%s is different in %s vs %s', name, destdir, srcdir)
- raise GitError('--force-sync not enabled; cannot overwrite a local '
- 'work tree. If you\'re comfortable with the '
- 'possibility of losing the work tree\'s git metadata,'
- ' use `repo sync --force-sync {0}` to '
- 'proceed.'.format(self.relpath))
- def _ReferenceGitDir(self, gitdir, dotgit, share_refs, copy_all):
- """Update |dotgit| to reference |gitdir|, using symlinks where possible.
- Args:
- gitdir: The bare git repository. Must already be initialized.
- dotgit: The repository you would like to initialize.
- share_refs: If true, |dotgit| will store its refs under |gitdir|.
- Only one work tree can store refs under a given |gitdir|.
- copy_all: If true, copy all remaining files from |gitdir| -> |dotgit|.
- This saves you the effort of initializing |dotgit| yourself.
- """
- symlink_files = self.shareable_files[:]
- symlink_dirs = self.shareable_dirs[:]
- if share_refs:
- symlink_files += self.working_tree_files
- symlink_dirs += self.working_tree_dirs
- to_symlink = symlink_files + symlink_dirs
- to_copy = []
- if copy_all:
- to_copy = platform_utils.listdir(gitdir)
- dotgit = platform_utils.realpath(dotgit)
- for name in set(to_copy).union(to_symlink):
- try:
- src = platform_utils.realpath(os.path.join(gitdir, name))
- dst = os.path.join(dotgit, name)
- if os.path.lexists(dst):
- continue
- # If the source dir doesn't exist, create an empty dir.
- if name in symlink_dirs and not os.path.lexists(src):
- os.makedirs(src)
- if name in to_symlink:
- platform_utils.symlink(
- os.path.relpath(src, os.path.dirname(dst)), dst)
- elif copy_all and not platform_utils.islink(dst):
- if platform_utils.isdir(src):
- shutil.copytree(src, dst)
- elif os.path.isfile(src):
- shutil.copy(src, dst)
- # If the source file doesn't exist, ensure the destination
- # file doesn't either.
- if name in symlink_files and not os.path.lexists(src):
- try:
- platform_utils.remove(dst)
- except OSError:
- pass
- except OSError as e:
- if e.errno == errno.EPERM:
- raise DownloadError(self._get_symlink_error_message())
- else:
- raise
- def _InitGitWorktree(self):
- """Init the project using git worktrees."""
- self.bare_git.worktree('prune')
- self.bare_git.worktree('add', '-ff', '--checkout', '--detach', '--lock',
- self.worktree, self.GetRevisionId())
- # Rewrite the internal state files to use relative paths between the
- # checkouts & worktrees.
- dotgit = os.path.join(self.worktree, '.git')
- with open(dotgit, 'r') as fp:
- # Figure out the checkout->worktree path.
- setting = fp.read()
- assert setting.startswith('gitdir:')
- git_worktree_path = setting.split(':', 1)[1].strip()
- # Some platforms (e.g. Windows) won't let us update dotgit in situ because
- # of file permissions. Delete it and recreate it from scratch to avoid.
- platform_utils.remove(dotgit)
- # Use relative path from checkout->worktree & maintain Unix line endings
- # on all OS's to match git behavior.
- with open(dotgit, 'w', newline='\n') as fp:
- print('gitdir:', os.path.relpath(git_worktree_path, self.worktree),
- file=fp)
- # Use relative path from worktree->checkout & maintain Unix line endings
- # on all OS's to match git behavior.
- with open(os.path.join(git_worktree_path, 'gitdir'), 'w', newline='\n') as fp:
- print(os.path.relpath(dotgit, git_worktree_path), file=fp)
- self._InitMRef()
- def _InitWorkTree(self, force_sync=False, submodules=False):
- realdotgit = os.path.join(self.worktree, '.git')
- tmpdotgit = realdotgit + '.tmp'
- init_dotgit = not os.path.exists(realdotgit)
- if init_dotgit:
- if self.use_git_worktrees:
- self._InitGitWorktree()
- self._CopyAndLinkFiles()
- return
- dotgit = tmpdotgit
- platform_utils.rmtree(tmpdotgit, ignore_errors=True)
- os.makedirs(tmpdotgit)
- self._ReferenceGitDir(self.gitdir, tmpdotgit, share_refs=True,
- copy_all=False)
- else:
- dotgit = realdotgit
- try:
- self._CheckDirReference(self.gitdir, dotgit, share_refs=True)
- except GitError as e:
- if force_sync and not init_dotgit:
- try:
- platform_utils.rmtree(dotgit)
- return self._InitWorkTree(force_sync=False, submodules=submodules)
- except Exception:
- raise e
- raise e
- if init_dotgit:
- _lwrite(os.path.join(tmpdotgit, HEAD), '%s\n' % self.GetRevisionId())
- # Now that the .git dir is fully set up, move it to its final home.
- platform_utils.rename(tmpdotgit, realdotgit)
- # Finish checking out the worktree.
- cmd = ['read-tree', '--reset', '-u']
- cmd.append('-v')
- cmd.append(HEAD)
- if GitCommand(self, cmd).Wait() != 0:
- raise GitError('Cannot initialize work tree for ' + self.name)
- if submodules:
- self._SyncSubmodules(quiet=True)
- self._CopyAndLinkFiles()
- def _get_symlink_error_message(self):
- if platform_utils.isWindows():
- return ('Unable to create symbolic link. Please re-run the command as '
- 'Administrator, or see '
- 'https://github.com/git-for-windows/git/wiki/Symbolic-Links '
- 'for other options.')
- return 'filesystem must support symlinks'
- def _gitdir_path(self, path):
- return platform_utils.realpath(os.path.join(self.gitdir, path))
- def _revlist(self, *args, **kw):
- a = []
- a.extend(args)
- a.append('--')
- return self.work_git.rev_list(*a, **kw)
- @property
- def _allrefs(self):
- return self.bare_ref.all
- def _getLogs(self, rev1, rev2, oneline=False, color=True, pretty_format=None):
- """Get logs between two revisions of this project."""
- comp = '..'
- if rev1:
- revs = [rev1]
- if rev2:
- revs.extend([comp, rev2])
- cmd = ['log', ''.join(revs)]
- out = DiffColoring(self.config)
- if out.is_on and color:
- cmd.append('--color')
- if pretty_format is not None:
- cmd.append('--pretty=format:%s' % pretty_format)
- if oneline:
- cmd.append('--oneline')
- try:
- log = GitCommand(self, cmd, capture_stdout=True, capture_stderr=True)
- if log.Wait() == 0:
- return log.stdout
- except GitError:
- # worktree may not exist if groups changed for example. In that case,
- # try in gitdir instead.
- if not os.path.exists(self.worktree):
- return self.bare_git.log(*cmd[1:])
- else:
- raise
- return None
- def getAddedAndRemovedLogs(self, toProject, oneline=False, color=True,
- pretty_format=None):
- """Get the list of logs from this revision to given revisionId"""
- logs = {}
- selfId = self.GetRevisionId(self._allrefs)
- toId = toProject.GetRevisionId(toProject._allrefs)
- logs['added'] = self._getLogs(selfId, toId, oneline=oneline, color=color,
- pretty_format=pretty_format)
- logs['removed'] = self._getLogs(toId, selfId, oneline=oneline, color=color,
- pretty_format=pretty_format)
- return logs
- class _GitGetByExec(object):
- def __init__(self, project, bare, gitdir):
- self._project = project
- self._bare = bare
- self._gitdir = gitdir
- # __getstate__ and __setstate__ are required for pickling because __getattr__ exists.
- def __getstate__(self):
- return (self._project, self._bare, self._gitdir)
- def __setstate__(self, state):
- self._project, self._bare, self._gitdir = state
- def LsOthers(self):
- p = GitCommand(self._project,
- ['ls-files',
- '-z',
- '--others',
- '--exclude-standard'],
- bare=False,
- gitdir=self._gitdir,
- capture_stdout=True,
- capture_stderr=True)
- if p.Wait() == 0:
- out = p.stdout
- if out:
- # Backslash is not anomalous
- return out[:-1].split('\0')
- return []
- def DiffZ(self, name, *args):
- cmd = [name]
- cmd.append('-z')
- cmd.append('--ignore-submodules')
- cmd.extend(args)
- p = GitCommand(self._project,
- cmd,
- gitdir=self._gitdir,
- bare=False,
- capture_stdout=True,
- capture_stderr=True)
- try:
- out = p.process.stdout.read()
- if not hasattr(out, 'encode'):
- out = out.decode()
- r = {}
- if out:
- out = iter(out[:-1].split('\0'))
- while out:
- try:
- info = next(out)
- path = next(out)
- except StopIteration:
- break
- class _Info(object):
- def __init__(self, path, omode, nmode, oid, nid, state):
- self.path = path
- self.src_path = None
- self.old_mode = omode
- self.new_mode = nmode
- self.old_id = oid
- self.new_id = nid
- if len(state) == 1:
- self.status = state
- self.level = None
- else:
- self.status = state[:1]
- self.level = state[1:]
- while self.level.startswith('0'):
- self.level = self.level[1:]
- info = info[1:].split(' ')
- info = _Info(path, *info)
- if info.status in ('R', 'C'):
- info.src_path = info.path
- info.path = next(out)
- r[info.path] = info
- return r
- finally:
- p.Wait()
- def GetDotgitPath(self, subpath=None):
- """Return the full path to the .git dir.
- As a convenience, append |subpath| if provided.
- """
- if self._bare:
- dotgit = self._gitdir
- else:
- dotgit = os.path.join(self._project.worktree, '.git')
- if os.path.isfile(dotgit):
- # Git worktrees use a "gitdir:" syntax to point to the scratch space.
- with open(dotgit) as fp:
- setting = fp.read()
- assert setting.startswith('gitdir:')
- gitdir = setting.split(':', 1)[1].strip()
- dotgit = os.path.normpath(os.path.join(self._project.worktree, gitdir))
- return dotgit if subpath is None else os.path.join(dotgit, subpath)
- def GetHead(self):
- """Return the ref that HEAD points to."""
- path = self.GetDotgitPath(subpath=HEAD)
- try:
- with open(path) as fd:
- line = fd.readline()
- except IOError as e:
- raise NoManifestException(path, str(e))
- try:
- line = line.decode()
- except AttributeError:
- pass
- if line.startswith('ref: '):
- return line[5:-1]
- return line[:-1]
- def SetHead(self, ref, message=None):
- cmdv = []
- if message is not None:
- cmdv.extend(['-m', message])
- cmdv.append(HEAD)
- cmdv.append(ref)
- self.symbolic_ref(*cmdv)
- def DetachHead(self, new, message=None):
- cmdv = ['--no-deref']
- if message is not None:
- cmdv.extend(['-m', message])
- cmdv.append(HEAD)
- cmdv.append(new)
- self.update_ref(*cmdv)
- def UpdateRef(self, name, new, old=None,
- message=None,
- detach=False):
- cmdv = []
- if message is not None:
- cmdv.extend(['-m', message])
- if detach:
- cmdv.append('--no-deref')
- cmdv.append(name)
- cmdv.append(new)
- if old is not None:
- cmdv.append(old)
- self.update_ref(*cmdv)
- def DeleteRef(self, name, old=None):
- if not old:
- old = self.rev_parse(name)
- self.update_ref('-d', name, old)
- self._project.bare_ref.deleted(name)
- def rev_list(self, *args, **kw):
- if 'format' in kw:
- cmdv = ['log', '--pretty=format:%s' % kw['format']]
- else:
- cmdv = ['rev-list']
- cmdv.extend(args)
- p = GitCommand(self._project,
- cmdv,
- bare=self._bare,
- gitdir=self._gitdir,
- capture_stdout=True,
- capture_stderr=True)
- if p.Wait() != 0:
- raise GitError('%s rev-list %s: %s' %
- (self._project.name, str(args), p.stderr))
- return p.stdout.splitlines()
- def __getattr__(self, name):
- """Allow arbitrary git commands using pythonic syntax.
- This allows you to do things like:
- git_obj.rev_parse('HEAD')
- Since we don't have a 'rev_parse' method defined, the __getattr__ will
- run. We'll replace the '_' with a '-' and try to run a git command.
- Any other positional arguments will be passed to the git command, and the
- following keyword arguments are supported:
- config: An optional dict of git config options to be passed with '-c'.
- Args:
- name: The name of the git command to call. Any '_' characters will
- be replaced with '-'.
- Returns:
- A callable object that will try to call git with the named command.
- """
- name = name.replace('_', '-')
- def runner(*args, **kwargs):
- cmdv = []
- config = kwargs.pop('config', None)
- for k in kwargs:
- raise TypeError('%s() got an unexpected keyword argument %r'
- % (name, k))
- if config is not None:
- for k, v in config.items():
- cmdv.append('-c')
- cmdv.append('%s=%s' % (k, v))
- cmdv.append(name)
- cmdv.extend(args)
- p = GitCommand(self._project,
- cmdv,
- bare=self._bare,
- gitdir=self._gitdir,
- capture_stdout=True,
- capture_stderr=True)
- if p.Wait() != 0:
- raise GitError('%s %s: %s' %
- (self._project.name, name, p.stderr))
- r = p.stdout
- if r.endswith('\n') and r.index('\n') == len(r) - 1:
- return r[:-1]
- return r
- return runner
- class _PriorSyncFailedError(Exception):
- def __str__(self):
- return 'prior sync failed; rebase still in progress'
- class _DirtyError(Exception):
- def __str__(self):
- return 'contains uncommitted changes'
- class _InfoMessage(object):
- def __init__(self, project, text):
- self.project = project
- self.text = text
- def Print(self, syncbuf):
- syncbuf.out.info('%s/: %s', self.project.relpath, self.text)
- syncbuf.out.nl()
- class _Failure(object):
- def __init__(self, project, why):
- self.project = project
- self.why = why
- def Print(self, syncbuf):
- syncbuf.out.fail('error: %s/: %s',
- self.project.relpath,
- str(self.why))
- syncbuf.out.nl()
- class _Later(object):
- def __init__(self, project, action):
- self.project = project
- self.action = action
- def Run(self, syncbuf):
- out = syncbuf.out
- out.project('project %s/', self.project.relpath)
- out.nl()
- try:
- self.action()
- out.nl()
- return True
- except GitError:
- out.nl()
- return False
- class _SyncColoring(Coloring):
- def __init__(self, config):
- Coloring.__init__(self, config, 'reposync')
- self.project = self.printer('header', attr='bold')
- self.info = self.printer('info')
- self.fail = self.printer('fail', fg='red')
- class SyncBuffer(object):
- def __init__(self, config, detach_head=False):
- self._messages = []
- self._failures = []
- self._later_queue1 = []
- self._later_queue2 = []
- self.out = _SyncColoring(config)
- self.out.redirect(sys.stderr)
- self.detach_head = detach_head
- self.clean = True
- self.recent_clean = True
- def info(self, project, fmt, *args):
- self._messages.append(_InfoMessage(project, fmt % args))
- def fail(self, project, err=None):
- self._failures.append(_Failure(project, err))
- self._MarkUnclean()
- def later1(self, project, what):
- self._later_queue1.append(_Later(project, what))
- def later2(self, project, what):
- self._later_queue2.append(_Later(project, what))
- def Finish(self):
- self._PrintMessages()
- self._RunLater()
- self._PrintMessages()
- return self.clean
- def Recently(self):
- recent_clean = self.recent_clean
- self.recent_clean = True
- return recent_clean
- def _MarkUnclean(self):
- self.clean = False
- self.recent_clean = False
- def _RunLater(self):
- for q in ['_later_queue1', '_later_queue2']:
- if not self._RunQueue(q):
- return
- def _RunQueue(self, queue):
- for m in getattr(self, queue):
- if not m.Run(self):
- self._MarkUnclean()
- return False
- setattr(self, queue, [])
- return True
- def _PrintMessages(self):
- if self._messages or self._failures:
- if os.isatty(2):
- self.out.write(progress.CSI_ERASE_LINE)
- self.out.write('\r')
- for m in self._messages:
- m.Print(self)
- for m in self._failures:
- m.Print(self)
- self._messages = []
- self._failures = []
- class MetaProject(Project):
- """A special project housed under .repo.
- """
- def __init__(self, manifest, name, gitdir, worktree):
- Project.__init__(self,
- manifest=manifest,
- name=name,
- gitdir=gitdir,
- objdir=gitdir,
- worktree=worktree,
- remote=RemoteSpec('origin'),
- relpath='.repo/%s' % name,
- revisionExpr='refs/heads/master',
- revisionId=None,
- groups=None)
- def PreSync(self):
- if self.Exists:
- cb = self.CurrentBranch
- if cb:
- base = self.GetBranch(cb).merge
- if base:
- self.revisionExpr = base
- self.revisionId = None
- def MetaBranchSwitch(self, submodules=False):
- """ Prepare MetaProject for manifest branch switch
- """
- # detach and delete manifest branch, allowing a new
- # branch to take over
- syncbuf = SyncBuffer(self.config, detach_head=True)
- self.Sync_LocalHalf(syncbuf, submodules=submodules)
- syncbuf.Finish()
- return GitCommand(self,
- ['update-ref', '-d', 'refs/heads/default'],
- capture_stdout=True,
- capture_stderr=True).Wait() == 0
- @property
- def LastFetch(self):
- try:
- fh = os.path.join(self.gitdir, 'FETCH_HEAD')
- return os.path.getmtime(fh)
- except OSError:
- return 0
- @property
- def HasChanges(self):
- """Has the remote received new commits not yet checked out?
- """
- if not self.remote or not self.revisionExpr:
- return False
- all_refs = self.bare_ref.all
- revid = self.GetRevisionId(all_refs)
- head = self.work_git.GetHead()
- if head.startswith(R_HEADS):
- try:
- head = all_refs[head]
- except KeyError:
- head = None
- if revid == head:
- return False
- elif self._revlist(not_rev(HEAD), revid):
- return True
- return False
|