├── .gitignore ├── tests ├── fixtures │ ├── gitc_config │ └── test.gitconfig ├── test_git_config.py └── test_wrapper.py ├── .flake8 ├── git_ssh ├── .gitattributes ├── .project ├── .pydevproject ├── pyversion.py ├── .mailmap ├── wrapper.py ├── trace.py ├── subcmds ├── smartsync.py ├── version.py ├── diff.py ├── __init__.py ├── gitc_delete.py ├── prune.py ├── checkout.py ├── selfupdate.py ├── overview.py ├── manifest.py ├── list.py ├── gitc_init.py ├── abandon.py ├── stage.py ├── download.py ├── cherry_pick.py ├── push.py ├── start.py ├── rebase.py ├── branches.py ├── help.py ├── info.py ├── status.py ├── diffmanifests.py ├── grep.py └── forall.py ├── hooks ├── pre-auto-gc └── commit-msg ├── progress.py ├── editor.py ├── error.py ├── pager.py ├── git_refs.py ├── SUBMITTING_PATCHES.md ├── README.md ├── color.py ├── gitc_utils.py ├── event_log.py ├── git_command.py ├── platform_utils_win32.py ├── command.py ├── platform_utils.py └── COPYING /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .repopickle_* 3 | /repoc 4 | -------------------------------------------------------------------------------- /tests/fixtures/gitc_config: -------------------------------------------------------------------------------- 1 | gitc_dir=/test/usr/local/google/gitc 2 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length=80 3 | ignore=E111,E114,E402 4 | -------------------------------------------------------------------------------- /tests/fixtures/test.gitconfig: -------------------------------------------------------------------------------- 1 | [section] 2 | empty 3 | nonempty = true 4 | -------------------------------------------------------------------------------- /git_ssh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | exec ssh -o "ControlMaster no" -o "ControlPath $REPO_SSH_SOCK" "$@" 3 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Prevent /bin/sh scripts from being clobbered by autocrlf=true 2 | git_ssh text eol=lf 3 | repo text eol=lf 4 | hooks/* text eol=lf 5 | -------------------------------------------------------------------------------- /.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | git-repo 4 | 5 | 6 | 7 | 8 | 9 | org.python.pydev.PyDevBuilder 10 | 11 | 12 | 13 | 14 | 15 | org.python.pydev.pythonNature 16 | 17 | 18 | -------------------------------------------------------------------------------- /.pydevproject: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | /git-repo 7 | 8 | python 2.6 9 | Default 10 | 11 | -------------------------------------------------------------------------------- /pyversion.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2013 The Android Open Source Project 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | import sys 17 | 18 | def is_python3(): 19 | return sys.version_info[0] == 3 20 | -------------------------------------------------------------------------------- /.mailmap: -------------------------------------------------------------------------------- 1 | Anthony Newnam Anthony 2 | He Ping heping 3 | Hu Xiuyun Hu xiuyun 4 | Hu Xiuyun Hu Xiuyun 5 | Jelly Chen chenguodong 6 | Jia Bi bijia 7 | JoonCheol Park Jooncheol Park 8 | Sergii Pylypenko pelya 9 | Shawn Pearce Shawn O. Pearce 10 | Ulrik Sjölin Ulrik Sjolin 11 | Ulrik Sjölin Ulrik Sjolin 12 | Ulrik Sjölin Ulrik Sjölin 13 | -------------------------------------------------------------------------------- /wrapper.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright (C) 2014 The Android Open Source Project 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | from __future__ import print_function 18 | import imp 19 | import os 20 | 21 | 22 | def WrapperPath(): 23 | return os.path.join(os.path.dirname(__file__), 'repo') 24 | 25 | _wrapper_module = None 26 | def Wrapper(): 27 | global _wrapper_module 28 | if not _wrapper_module: 29 | _wrapper_module = imp.load_source('wrapper', WrapperPath()) 30 | return _wrapper_module 31 | -------------------------------------------------------------------------------- /trace.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2008 The Android Open Source Project 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | from __future__ import print_function 17 | import sys 18 | import os 19 | REPO_TRACE = 'REPO_TRACE' 20 | 21 | try: 22 | _TRACE = os.environ[REPO_TRACE] == '1' 23 | except KeyError: 24 | _TRACE = False 25 | 26 | def IsTrace(): 27 | return _TRACE 28 | 29 | def SetTrace(): 30 | global _TRACE 31 | _TRACE = True 32 | 33 | def Trace(fmt, *args): 34 | if IsTrace(): 35 | print(fmt % args, file=sys.stderr) 36 | -------------------------------------------------------------------------------- /subcmds/smartsync.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2010 The Android Open Source Project 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | from subcmds.sync import Sync 17 | 18 | class Smartsync(Sync): 19 | common = True 20 | helpSummary = "Update working tree to the latest known good revision" 21 | helpUsage = """ 22 | %prog [...] 23 | """ 24 | helpDescription = """ 25 | The '%prog' command is a shortcut for sync -s. 26 | """ 27 | 28 | def _Options(self, p): 29 | Sync._Options(self, p, show_smart=False) 30 | 31 | def Execute(self, opt, args): 32 | opt.smart_sync = True 33 | Sync.Execute(self, opt, args) 34 | -------------------------------------------------------------------------------- /tests/test_git_config.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | 4 | import git_config 5 | 6 | def fixture(*paths): 7 | """Return a path relative to test/fixtures. 8 | """ 9 | return os.path.join(os.path.dirname(__file__), 'fixtures', *paths) 10 | 11 | class GitConfigUnitTest(unittest.TestCase): 12 | """Tests the GitConfig class. 13 | """ 14 | def setUp(self): 15 | """Create a GitConfig object using the test.gitconfig fixture. 16 | """ 17 | config_fixture = fixture('test.gitconfig') 18 | self.config = git_config.GitConfig(config_fixture) 19 | 20 | def test_GetString_with_empty_config_values(self): 21 | """ 22 | Test config entries with no value. 23 | 24 | [section] 25 | empty 26 | 27 | """ 28 | val = self.config.GetString('section.empty') 29 | self.assertEqual(val, None) 30 | 31 | def test_GetString_with_true_value(self): 32 | """ 33 | Test config entries with a string value. 34 | 35 | [section] 36 | nonempty = true 37 | 38 | """ 39 | val = self.config.GetString('section.nonempty') 40 | self.assertEqual(val, 'true') 41 | 42 | def test_GetString_from_missing_file(self): 43 | """ 44 | Test missing config file 45 | """ 46 | config_fixture = fixture('not.present.gitconfig') 47 | config = git_config.GitConfig(config_fixture) 48 | val = config.GetString('empty') 49 | self.assertEqual(val, None) 50 | 51 | if __name__ == '__main__': 52 | unittest.main() 53 | -------------------------------------------------------------------------------- /subcmds/version.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2009 The Android Open Source Project 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | from __future__ import print_function 17 | import sys 18 | from command import Command, MirrorSafeCommand 19 | from git_command import git 20 | from git_refs import HEAD 21 | 22 | class Version(Command, MirrorSafeCommand): 23 | wrapper_version = None 24 | wrapper_path = None 25 | 26 | common = False 27 | helpSummary = "Display the version of repo" 28 | helpUsage = """ 29 | %prog 30 | """ 31 | 32 | def Execute(self, opt, args): 33 | rp = self.manifest.repoProject 34 | rem = rp.GetRemote(rp.remote.name) 35 | 36 | print('repo version %s' % rp.work_git.describe(HEAD)) 37 | print(' (from %s)' % rem.url) 38 | 39 | if Version.wrapper_path is not None: 40 | print('repo launcher version %s' % Version.wrapper_version) 41 | print(' (from %s)' % Version.wrapper_path) 42 | 43 | print(git.version().strip()) 44 | print('Python %s' % sys.version) 45 | -------------------------------------------------------------------------------- /subcmds/diff.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2008 The Android Open Source Project 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | from command import PagedCommand 17 | 18 | class Diff(PagedCommand): 19 | common = True 20 | helpSummary = "Show changes between commit and working tree" 21 | helpUsage = """ 22 | %prog [...] 23 | 24 | The -u option causes '%prog' to generate diff output with file paths 25 | relative to the repository root, so the output can be applied 26 | to the Unix 'patch' command. 27 | """ 28 | 29 | def _Options(self, p): 30 | def cmd(option, opt_str, value, parser): 31 | setattr(parser.values, option.dest, list(parser.rargs)) 32 | while parser.rargs: 33 | del parser.rargs[0] 34 | p.add_option('-u', '--absolute', 35 | dest='absolute', action='store_true', 36 | help='Paths are relative to the repository root') 37 | 38 | def Execute(self, opt, args): 39 | for project in self.GetProjects(args): 40 | project.PrintWorkTreeDiff(opt.absolute) 41 | -------------------------------------------------------------------------------- /subcmds/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2008 The Android Open Source Project 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | import os 17 | 18 | all_commands = {} 19 | 20 | my_dir = os.path.dirname(__file__) 21 | for py in os.listdir(my_dir): 22 | if py == '__init__.py': 23 | continue 24 | 25 | if py.endswith('.py'): 26 | name = py[:-3] 27 | 28 | clsn = name.capitalize() 29 | while clsn.find('_') > 0: 30 | h = clsn.index('_') 31 | clsn = clsn[0:h] + clsn[h + 1:].capitalize() 32 | 33 | mod = __import__(__name__, 34 | globals(), 35 | locals(), 36 | ['%s' % name]) 37 | mod = getattr(mod, name) 38 | try: 39 | cmd = getattr(mod, clsn)() 40 | except AttributeError: 41 | raise SyntaxError('%s/%s does not define class %s' % ( 42 | __name__, py, clsn)) 43 | 44 | name = name.replace('_', '-') 45 | cmd.NAME = name 46 | all_commands[name] = cmd 47 | 48 | if 'help' in all_commands: 49 | all_commands['help'].commands = all_commands 50 | -------------------------------------------------------------------------------- /subcmds/gitc_delete.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2015 The Android Open Source Project 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | from __future__ import print_function 17 | import sys 18 | 19 | from command import Command, GitcClientCommand 20 | import platform_utils 21 | 22 | from pyversion import is_python3 23 | if not is_python3(): 24 | # pylint:disable=W0622 25 | input = raw_input 26 | # pylint:enable=W0622 27 | 28 | class GitcDelete(Command, GitcClientCommand): 29 | common = True 30 | visible_everywhere = False 31 | helpSummary = "Delete a GITC Client." 32 | helpUsage = """ 33 | %prog 34 | """ 35 | helpDescription = """ 36 | This subcommand deletes the current GITC client, deleting the GITC manifest 37 | and all locally downloaded sources. 38 | """ 39 | 40 | def _Options(self, p): 41 | p.add_option('-f', '--force', 42 | dest='force', action='store_true', 43 | help='Force the deletion (no prompt).') 44 | 45 | def Execute(self, opt, args): 46 | if not opt.force: 47 | prompt = ('This will delete GITC client: %s\nAre you sure? (yes/no) ' % 48 | self.gitc_manifest.gitc_client_name) 49 | response = input(prompt).lower() 50 | if not response == 'yes': 51 | print('Response was not "yes"\n Exiting...') 52 | sys.exit(1) 53 | platform_utils.rmtree(self.gitc_manifest.gitc_client_dir) 54 | -------------------------------------------------------------------------------- /subcmds/prune.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2008 The Android Open Source Project 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | from __future__ import print_function 17 | from color import Coloring 18 | from command import PagedCommand 19 | 20 | class Prune(PagedCommand): 21 | common = True 22 | helpSummary = "Prune (delete) already merged topics" 23 | helpUsage = """ 24 | %prog [...] 25 | """ 26 | 27 | def Execute(self, opt, args): 28 | all_branches = [] 29 | for project in self.GetProjects(args): 30 | all_branches.extend(project.PruneHeads()) 31 | 32 | if not all_branches: 33 | return 34 | 35 | class Report(Coloring): 36 | def __init__(self, config): 37 | Coloring.__init__(self, config, 'status') 38 | self.project = self.printer('header', attr='bold') 39 | 40 | out = Report(all_branches[0].project.config) 41 | out.project('Pending Branches') 42 | out.nl() 43 | 44 | project = None 45 | 46 | for branch in all_branches: 47 | if project != branch.project: 48 | project = branch.project 49 | out.nl() 50 | out.project('project %s/' % project.relpath) 51 | out.nl() 52 | 53 | commits = branch.commits 54 | date = branch.date 55 | print('%s %-33s (%2d commit%s, %s)' % ( 56 | branch.name == project.CurrentBranch and '*' or ' ', 57 | branch.name, 58 | len(commits), 59 | len(commits) != 1 and 's' or ' ', 60 | date)) 61 | -------------------------------------------------------------------------------- /subcmds/checkout.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2009 The Android Open Source Project 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | from __future__ import print_function 17 | import sys 18 | from command import Command 19 | from progress import Progress 20 | 21 | class Checkout(Command): 22 | common = True 23 | helpSummary = "Checkout a branch for development" 24 | helpUsage = """ 25 | %prog [...] 26 | """ 27 | helpDescription = """ 28 | The '%prog' command checks out an existing branch that was previously 29 | created by 'repo start'. 30 | 31 | The command is equivalent to: 32 | 33 | repo forall [...] -c git checkout 34 | """ 35 | 36 | def Execute(self, opt, args): 37 | if not args: 38 | self.Usage() 39 | 40 | nb = args[0] 41 | err = [] 42 | success = [] 43 | all_projects = self.GetProjects(args[1:]) 44 | 45 | pm = Progress('Checkout %s' % nb, len(all_projects)) 46 | for project in all_projects: 47 | pm.update() 48 | 49 | status = project.CheckoutBranch(nb) 50 | if status is not None: 51 | if status: 52 | success.append(project) 53 | else: 54 | err.append(project) 55 | pm.end() 56 | 57 | if err: 58 | for p in err: 59 | print("error: %s/: cannot checkout %s" % (p.relpath, nb), 60 | file=sys.stderr) 61 | sys.exit(1) 62 | elif not success: 63 | print('error: no project has branch %s' % nb, file=sys.stderr) 64 | sys.exit(1) 65 | -------------------------------------------------------------------------------- /hooks/pre-auto-gc: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script to verify if you are on battery, in case you 4 | # are running Windows, Linux or OS X. Called by git-gc --auto with no 5 | # arguments. The hook should exit with non-zero status after issuing an 6 | # appropriate message if it wants to stop the auto repacking. 7 | 8 | # This program is free software; you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation; either version 2 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License 19 | # along with this program; if not, write to the Free Software 20 | # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 21 | 22 | if uname -s | grep -q "_NT-" 23 | then 24 | if test -x $SYSTEMROOT/System32/Wbem/wmic 25 | then 26 | STATUS=$(wmic path win32_battery get batterystatus /format:list | tr -d '\r\n') 27 | [ "$STATUS" = "BatteryStatus=2" ] && exit 0 || exit 1 28 | fi 29 | exit 0 30 | fi 31 | 32 | if test -x /sbin/on_ac_power && /sbin/on_ac_power 33 | then 34 | exit 0 35 | elif test "$(cat /sys/class/power_supply/AC/online 2>/dev/null)" = 1 36 | then 37 | exit 0 38 | elif grep -q 'on-line' /proc/acpi/ac_adapter/AC/state 2>/dev/null 39 | then 40 | exit 0 41 | elif grep -q '0x01$' /proc/apm 2>/dev/null 42 | then 43 | exit 0 44 | elif grep -q "AC Power \+: 1" /proc/pmu/info 2>/dev/null 45 | then 46 | exit 0 47 | elif test -x /usr/bin/pmset && /usr/bin/pmset -g batt | 48 | grep -q "drawing from 'AC Power'" 49 | then 50 | exit 0 51 | elif test -d /sys/bus/acpi/drivers/battery && test 0 = \ 52 | "$(find /sys/bus/acpi/drivers/battery/ -type l | wc -l)"; 53 | then 54 | # No battery exists. 55 | exit 0 56 | fi 57 | 58 | echo "Auto packing deferred; not on AC" 59 | exit 1 60 | -------------------------------------------------------------------------------- /subcmds/selfupdate.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2009 The Android Open Source Project 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | from __future__ import print_function 17 | from optparse import SUPPRESS_HELP 18 | import sys 19 | 20 | from command import Command, MirrorSafeCommand 21 | from subcmds.sync import _PostRepoUpgrade 22 | from subcmds.sync import _PostRepoFetch 23 | 24 | class Selfupdate(Command, MirrorSafeCommand): 25 | common = False 26 | helpSummary = "Update repo to the latest version" 27 | helpUsage = """ 28 | %prog 29 | """ 30 | helpDescription = """ 31 | The '%prog' command upgrades repo to the latest version, if a 32 | newer version is available. 33 | 34 | Normally this is done automatically by 'repo sync' and does not 35 | need to be performed by an end-user. 36 | """ 37 | 38 | def _Options(self, p): 39 | g = p.add_option_group('repo Version options') 40 | g.add_option('--no-repo-verify', 41 | dest='no_repo_verify', action='store_true', 42 | help='do not verify repo source code') 43 | g.add_option('--repo-upgraded', 44 | dest='repo_upgraded', action='store_true', 45 | help=SUPPRESS_HELP) 46 | 47 | def Execute(self, opt, args): 48 | rp = self.manifest.repoProject 49 | rp.PreSync() 50 | 51 | if opt.repo_upgraded: 52 | _PostRepoUpgrade(self.manifest) 53 | 54 | else: 55 | if not rp.Sync_NetworkHalf(): 56 | print("error: can't update repo", file=sys.stderr) 57 | sys.exit(1) 58 | 59 | rp.bare_git.gc('--auto') 60 | _PostRepoFetch(rp, 61 | no_repo_verify = opt.no_repo_verify, 62 | verbose = True) 63 | -------------------------------------------------------------------------------- /progress.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2009 The Android Open Source Project 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | import os 17 | import sys 18 | from time import time 19 | from trace import IsTrace 20 | 21 | _NOT_TTY = not os.isatty(2) 22 | 23 | class Progress(object): 24 | def __init__(self, title, total=0, units='', print_newline=False, 25 | always_print_percentage=False): 26 | self._title = title 27 | self._total = total 28 | self._done = 0 29 | self._lastp = -1 30 | self._start = time() 31 | self._show = False 32 | self._units = units 33 | self._print_newline = print_newline 34 | self._always_print_percentage = always_print_percentage 35 | 36 | def update(self, inc=1): 37 | self._done += inc 38 | 39 | if _NOT_TTY or IsTrace(): 40 | return 41 | 42 | if not self._show: 43 | if 0.5 <= time() - self._start: 44 | self._show = True 45 | else: 46 | return 47 | 48 | if self._total <= 0: 49 | sys.stderr.write('\r%s: %d, ' % ( 50 | self._title, 51 | self._done)) 52 | sys.stderr.flush() 53 | else: 54 | p = (100 * self._done) / self._total 55 | 56 | if self._lastp != p or self._always_print_percentage: 57 | self._lastp = p 58 | sys.stderr.write('\r%s: %3d%% (%d%s/%d%s)%s' % ( 59 | self._title, 60 | p, 61 | self._done, self._units, 62 | self._total, self._units, 63 | "\n" if self._print_newline else "")) 64 | sys.stderr.flush() 65 | 66 | def end(self): 67 | if _NOT_TTY or IsTrace() or not self._show: 68 | return 69 | 70 | if self._total <= 0: 71 | sys.stderr.write('\r%s: %d, done. \n' % ( 72 | self._title, 73 | self._done)) 74 | sys.stderr.flush() 75 | else: 76 | p = (100 * self._done) / self._total 77 | sys.stderr.write('\r%s: %3d%% (%d%s/%d%s), done. \n' % ( 78 | self._title, 79 | p, 80 | self._done, self._units, 81 | self._total, self._units)) 82 | sys.stderr.flush() 83 | -------------------------------------------------------------------------------- /subcmds/overview.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2012 The Android Open Source Project 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | from __future__ import print_function 17 | from color import Coloring 18 | from command import PagedCommand 19 | 20 | 21 | class Overview(PagedCommand): 22 | common = True 23 | helpSummary = "Display overview of unmerged project branches" 24 | helpUsage = """ 25 | %prog [--current-branch] [...] 26 | """ 27 | helpDescription = """ 28 | The '%prog' command is used to display an overview of the projects branches, 29 | and list any local commits that have not yet been merged into the project. 30 | 31 | The -b/--current-branch option can be used to restrict the output to only 32 | branches currently checked out in each project. By default, all branches 33 | are displayed. 34 | """ 35 | 36 | def _Options(self, p): 37 | p.add_option('-b', '--current-branch', 38 | dest="current_branch", action="store_true", 39 | help="Consider only checked out branches") 40 | 41 | def Execute(self, opt, args): 42 | all_branches = [] 43 | for project in self.GetProjects(args): 44 | br = [project.GetUploadableBranch(x) 45 | for x in project.GetBranches()] 46 | br = [x for x in br if x] 47 | if opt.current_branch: 48 | br = [x for x in br if x.name == project.CurrentBranch] 49 | all_branches.extend(br) 50 | 51 | if not all_branches: 52 | return 53 | 54 | class Report(Coloring): 55 | def __init__(self, config): 56 | Coloring.__init__(self, config, 'status') 57 | self.project = self.printer('header', attr='bold') 58 | self.text = self.printer('text') 59 | 60 | out = Report(all_branches[0].project.config) 61 | out.text("Deprecated. See repo info -o.") 62 | out.nl() 63 | out.project('Projects Overview') 64 | out.nl() 65 | 66 | project = None 67 | 68 | for branch in all_branches: 69 | if project != branch.project: 70 | project = branch.project 71 | out.nl() 72 | out.project('project %s/' % project.relpath) 73 | out.nl() 74 | 75 | commits = branch.commits 76 | date = branch.date 77 | print('%s %-33s (%2d commit%s, %s)' % ( 78 | branch.name == project.CurrentBranch and '*' or ' ', 79 | branch.name, 80 | len(commits), 81 | len(commits) != 1 and 's' or ' ', 82 | date)) 83 | for commit in commits: 84 | print('%-35s - %s' % ('', commit)) 85 | -------------------------------------------------------------------------------- /subcmds/manifest.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2009 The Android Open Source Project 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | from __future__ import print_function 17 | import os 18 | import sys 19 | 20 | from command import PagedCommand 21 | 22 | class Manifest(PagedCommand): 23 | common = False 24 | helpSummary = "Manifest inspection utility" 25 | helpUsage = """ 26 | %prog [-o {-|NAME.xml} [-r]] 27 | """ 28 | _helpDescription = """ 29 | 30 | With the -o option, exports the current manifest for inspection. 31 | The manifest and (if present) local_manifest.xml are combined 32 | together to produce a single manifest file. This file can be stored 33 | in a Git repository for use during future 'repo init' invocations. 34 | 35 | """ 36 | 37 | @property 38 | def helpDescription(self): 39 | helptext = self._helpDescription + '\n' 40 | r = os.path.dirname(__file__) 41 | r = os.path.dirname(r) 42 | fd = open(os.path.join(r, 'docs', 'manifest-format.txt')) 43 | for line in fd: 44 | helptext += line 45 | fd.close() 46 | return helptext 47 | 48 | def _Options(self, p): 49 | p.add_option('-r', '--revision-as-HEAD', 50 | dest='peg_rev', action='store_true', 51 | help='Save revisions as current HEAD') 52 | p.add_option('--suppress-upstream-revision', dest='peg_rev_upstream', 53 | default=True, action='store_false', 54 | help='If in -r mode, do not write the upstream field. ' 55 | 'Only of use if the branch names for a sha1 manifest are ' 56 | 'sensitive.') 57 | p.add_option('-o', '--output-file', 58 | dest='output_file', 59 | default='-', 60 | help='File to save the manifest to', 61 | metavar='-|NAME.xml') 62 | 63 | def _Output(self, opt): 64 | if opt.output_file == '-': 65 | fd = sys.stdout 66 | else: 67 | fd = open(opt.output_file, 'w') 68 | self.manifest.Save(fd, 69 | peg_rev = opt.peg_rev, 70 | peg_rev_upstream = opt.peg_rev_upstream) 71 | fd.close() 72 | if opt.output_file != '-': 73 | print('Saved manifest to %s' % opt.output_file, file=sys.stderr) 74 | 75 | def Execute(self, opt, args): 76 | if args: 77 | self.Usage() 78 | 79 | if opt.output_file is not None: 80 | self._Output(opt) 81 | return 82 | 83 | print('error: no operation to perform', file=sys.stderr) 84 | print('error: see repo help manifest', file=sys.stderr) 85 | sys.exit(1) 86 | -------------------------------------------------------------------------------- /tests/test_wrapper.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2015 The Android Open Source Project 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | import os 17 | import unittest 18 | 19 | import wrapper 20 | 21 | def fixture(*paths): 22 | """Return a path relative to tests/fixtures. 23 | """ 24 | return os.path.join(os.path.dirname(__file__), 'fixtures', *paths) 25 | 26 | class RepoWrapperUnitTest(unittest.TestCase): 27 | """Tests helper functions in the repo wrapper 28 | """ 29 | def setUp(self): 30 | """Load the wrapper module every time 31 | """ 32 | wrapper._wrapper_module = None 33 | self.wrapper = wrapper.Wrapper() 34 | 35 | def test_get_gitc_manifest_dir_no_gitc(self): 36 | """ 37 | Test reading a missing gitc config file 38 | """ 39 | self.wrapper.GITC_CONFIG_FILE = fixture('missing_gitc_config') 40 | val = self.wrapper.get_gitc_manifest_dir() 41 | self.assertEqual(val, '') 42 | 43 | def test_get_gitc_manifest_dir(self): 44 | """ 45 | Test reading the gitc config file and parsing the directory 46 | """ 47 | self.wrapper.GITC_CONFIG_FILE = fixture('gitc_config') 48 | val = self.wrapper.get_gitc_manifest_dir() 49 | self.assertEqual(val, '/test/usr/local/google/gitc') 50 | 51 | def test_gitc_parse_clientdir_no_gitc(self): 52 | """ 53 | Test parsing the gitc clientdir without gitc running 54 | """ 55 | self.wrapper.GITC_CONFIG_FILE = fixture('missing_gitc_config') 56 | self.assertEqual(self.wrapper.gitc_parse_clientdir('/something'), None) 57 | self.assertEqual(self.wrapper.gitc_parse_clientdir('/gitc/manifest-rw/test'), 'test') 58 | 59 | def test_gitc_parse_clientdir(self): 60 | """ 61 | Test parsing the gitc clientdir 62 | """ 63 | self.wrapper.GITC_CONFIG_FILE = fixture('gitc_config') 64 | self.assertEqual(self.wrapper.gitc_parse_clientdir('/something'), None) 65 | self.assertEqual(self.wrapper.gitc_parse_clientdir('/gitc/manifest-rw/test'), 'test') 66 | self.assertEqual(self.wrapper.gitc_parse_clientdir('/gitc/manifest-rw/test/'), 'test') 67 | self.assertEqual(self.wrapper.gitc_parse_clientdir('/gitc/manifest-rw/test/extra'), 'test') 68 | self.assertEqual(self.wrapper.gitc_parse_clientdir('/test/usr/local/google/gitc/test'), 'test') 69 | self.assertEqual(self.wrapper.gitc_parse_clientdir('/test/usr/local/google/gitc/test/'), 'test') 70 | self.assertEqual(self.wrapper.gitc_parse_clientdir('/test/usr/local/google/gitc/test/extra'), 'test') 71 | self.assertEqual(self.wrapper.gitc_parse_clientdir('/gitc/manifest-rw/'), None) 72 | self.assertEqual(self.wrapper.gitc_parse_clientdir('/test/usr/local/google/gitc/'), None) 73 | 74 | if __name__ == '__main__': 75 | unittest.main() 76 | -------------------------------------------------------------------------------- /subcmds/list.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2011 The Android Open Source Project 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | from __future__ import print_function 17 | import sys 18 | 19 | from command import Command, MirrorSafeCommand 20 | 21 | class List(Command, MirrorSafeCommand): 22 | common = True 23 | helpSummary = "List projects and their associated directories" 24 | helpUsage = """ 25 | %prog [-f] [...] 26 | %prog [-f] -r str1 [str2]..." 27 | """ 28 | helpDescription = """ 29 | List all projects; pass '.' to list the project for the cwd. 30 | 31 | This is similar to running: repo forall -c 'echo "$REPO_PATH : $REPO_PROJECT"'. 32 | """ 33 | 34 | def _Options(self, p): 35 | p.add_option('-r', '--regex', 36 | dest='regex', action='store_true', 37 | help="Filter the project list based on regex or wildcard matching of strings") 38 | p.add_option('-g', '--groups', 39 | dest='groups', 40 | help="Filter the project list based on the groups the project is in") 41 | p.add_option('-f', '--fullpath', 42 | dest='fullpath', action='store_true', 43 | help="Display the full work tree path instead of the relative path") 44 | p.add_option('-n', '--name-only', 45 | dest='name_only', action='store_true', 46 | help="Display only the name of the repository") 47 | p.add_option('-p', '--path-only', 48 | dest='path_only', action='store_true', 49 | help="Display only the path of the repository") 50 | 51 | def Execute(self, opt, args): 52 | """List all projects and the associated directories. 53 | 54 | This may be possible to do with 'repo forall', but repo newbies have 55 | trouble figuring that out. The idea here is that it should be more 56 | discoverable. 57 | 58 | Args: 59 | opt: The options. 60 | args: Positional args. Can be a list of projects to list, or empty. 61 | """ 62 | 63 | if opt.fullpath and opt.name_only: 64 | print('error: cannot combine -f and -n', file=sys.stderr) 65 | sys.exit(1) 66 | 67 | if not opt.regex: 68 | projects = self.GetProjects(args, groups=opt.groups) 69 | else: 70 | projects = self.FindProjects(args) 71 | 72 | def _getpath(x): 73 | if opt.fullpath: 74 | return x.worktree 75 | return x.relpath 76 | 77 | lines = [] 78 | for project in projects: 79 | if opt.name_only and not opt.path_only: 80 | lines.append("%s" % ( project.name)) 81 | elif opt.path_only and not opt.name_only: 82 | lines.append("%s" % (_getpath(project))) 83 | else: 84 | lines.append("%s : %s" % (_getpath(project), project.name)) 85 | 86 | lines.sort() 87 | print('\n'.join(lines)) 88 | -------------------------------------------------------------------------------- /subcmds/gitc_init.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2015 The Android Open Source Project 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | from __future__ import print_function 17 | import os 18 | import sys 19 | 20 | import gitc_utils 21 | from command import GitcAvailableCommand 22 | from manifest_xml import GitcManifest 23 | from subcmds import init 24 | import wrapper 25 | 26 | 27 | class GitcInit(init.Init, GitcAvailableCommand): 28 | common = True 29 | helpSummary = "Initialize a GITC Client." 30 | helpUsage = """ 31 | %prog [options] [client name] 32 | """ 33 | helpDescription = """ 34 | The '%prog' command is ran to initialize a new GITC client for use 35 | with the GITC file system. 36 | 37 | This command will setup the client directory, initialize repo, just 38 | like repo init does, and then downloads the manifest collection 39 | and installs it in the .repo/directory of the GITC client. 40 | 41 | Once this is done, a GITC manifest is generated by pulling the HEAD 42 | SHA for each project and generates the properly formatted XML file 43 | and installs it as .manifest in the GITC client directory. 44 | 45 | The -c argument is required to specify the GITC client name. 46 | 47 | The optional -f argument can be used to specify the manifest file to 48 | use for this GITC client. 49 | """ 50 | 51 | def _Options(self, p): 52 | super(GitcInit, self)._Options(p) 53 | g = p.add_option_group('GITC options') 54 | g.add_option('-f', '--manifest-file', 55 | dest='manifest_file', 56 | help='Optional manifest file to use for this GITC client.') 57 | g.add_option('-c', '--gitc-client', 58 | dest='gitc_client', 59 | help='The name of the gitc_client instance to create or modify.') 60 | 61 | def Execute(self, opt, args): 62 | gitc_client = gitc_utils.parse_clientdir(os.getcwd()) 63 | if not gitc_client or (opt.gitc_client and gitc_client != opt.gitc_client): 64 | print('fatal: Please update your repo command. See go/gitc for instructions.', file=sys.stderr) 65 | sys.exit(1) 66 | self.client_dir = os.path.join(gitc_utils.get_gitc_manifest_dir(), 67 | gitc_client) 68 | super(GitcInit, self).Execute(opt, args) 69 | 70 | manifest_file = self.manifest.manifestFile 71 | if opt.manifest_file: 72 | if not os.path.exists(opt.manifest_file): 73 | print('fatal: Specified manifest file %s does not exist.' % 74 | opt.manifest_file) 75 | sys.exit(1) 76 | manifest_file = opt.manifest_file 77 | 78 | manifest = GitcManifest(self.repodir, gitc_client) 79 | manifest.Override(manifest_file) 80 | gitc_utils.generate_gitc_manifest(None, manifest) 81 | print('Please run `cd %s` to view your GITC client.' % 82 | os.path.join(wrapper.Wrapper().GITC_FS_ROOT_DIR, gitc_client)) 83 | -------------------------------------------------------------------------------- /subcmds/abandon.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2008 The Android Open Source Project 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | from __future__ import print_function 17 | import sys 18 | from command import Command 19 | from collections import defaultdict 20 | from git_command import git 21 | from progress import Progress 22 | 23 | class Abandon(Command): 24 | common = True 25 | helpSummary = "Permanently abandon a development branch" 26 | helpUsage = """ 27 | %prog [--all | ] [...] 28 | 29 | This subcommand permanently abandons a development branch by 30 | deleting it (and all its history) from your local repository. 31 | 32 | It is equivalent to "git branch -D ". 33 | """ 34 | def _Options(self, p): 35 | p.add_option('--all', 36 | dest='all', action='store_true', 37 | help='delete all branches in all projects') 38 | 39 | def Execute(self, opt, args): 40 | if not opt.all and not args: 41 | self.Usage() 42 | 43 | if not opt.all: 44 | nb = args[0] 45 | if not git.check_ref_format('heads/%s' % nb): 46 | print("error: '%s' is not a valid name" % nb, file=sys.stderr) 47 | sys.exit(1) 48 | else: 49 | args.insert(0,None) 50 | nb = "'All local branches'" 51 | 52 | err = defaultdict(list) 53 | success = defaultdict(list) 54 | all_projects = self.GetProjects(args[1:]) 55 | 56 | pm = Progress('Abandon %s' % nb, len(all_projects)) 57 | for project in all_projects: 58 | pm.update() 59 | 60 | if opt.all: 61 | branches = project.GetBranches().keys() 62 | else: 63 | branches = [nb] 64 | 65 | for name in branches: 66 | status = project.AbandonBranch(name) 67 | if status is not None: 68 | if status: 69 | success[name].append(project) 70 | else: 71 | err[name].append(project) 72 | pm.end() 73 | 74 | width = 25 75 | for name in branches: 76 | if width < len(name): 77 | width = len(name) 78 | 79 | if err: 80 | for br in err.keys(): 81 | err_msg = "error: cannot abandon %s" %br 82 | print(err_msg, file=sys.stderr) 83 | for proj in err[br]: 84 | print(' '*len(err_msg) + " | %s" % proj.relpath, file=sys.stderr) 85 | sys.exit(1) 86 | elif not success: 87 | print('error: no project has local branch(es) : %s' % nb, 88 | file=sys.stderr) 89 | sys.exit(1) 90 | else: 91 | print('Abandoned branches:', file=sys.stderr) 92 | for br in success.keys(): 93 | if len(all_projects) > 1 and len(all_projects) == len(success[br]): 94 | result = "all project" 95 | else: 96 | result = "%s" % ( 97 | ('\n'+' '*width + '| ').join(p.relpath for p in success[br])) 98 | print("%s%s| %s\n" % (br,' '*(width-len(br)), result),file=sys.stderr) 99 | -------------------------------------------------------------------------------- /editor.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2008 The Android Open Source Project 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | from __future__ import print_function 17 | import os 18 | import re 19 | import sys 20 | import subprocess 21 | import tempfile 22 | 23 | from error import EditorError 24 | import platform_utils 25 | 26 | class Editor(object): 27 | """Manages the user's preferred text editor.""" 28 | 29 | _editor = None 30 | globalConfig = None 31 | 32 | @classmethod 33 | def _GetEditor(cls): 34 | if cls._editor is None: 35 | cls._editor = cls._SelectEditor() 36 | return cls._editor 37 | 38 | @classmethod 39 | def _SelectEditor(cls): 40 | e = os.getenv('GIT_EDITOR') 41 | if e: 42 | return e 43 | 44 | if cls.globalConfig: 45 | e = cls.globalConfig.GetString('core.editor') 46 | if e: 47 | return e 48 | 49 | e = os.getenv('VISUAL') 50 | if e: 51 | return e 52 | 53 | e = os.getenv('EDITOR') 54 | if e: 55 | return e 56 | 57 | if os.getenv('TERM') == 'dumb': 58 | print( 59 | """No editor specified in GIT_EDITOR, core.editor, VISUAL or EDITOR. 60 | Tried to fall back to vi but terminal is dumb. Please configure at 61 | least one of these before using this command.""", file=sys.stderr) 62 | sys.exit(1) 63 | 64 | return 'vi' 65 | 66 | @classmethod 67 | def EditString(cls, data): 68 | """Opens an editor to edit the given content. 69 | 70 | Args: 71 | data : the text to edit 72 | 73 | Returns: 74 | new value of edited text; None if editing did not succeed 75 | """ 76 | editor = cls._GetEditor() 77 | if editor == ':': 78 | return data 79 | 80 | fd, path = tempfile.mkstemp() 81 | try: 82 | os.write(fd, data) 83 | os.close(fd) 84 | fd = None 85 | 86 | if platform_utils.isWindows(): 87 | # Split on spaces, respecting quoted strings 88 | import shlex 89 | args = shlex.split(editor) 90 | shell = False 91 | elif re.compile("^.*[$ \t'].*$").match(editor): 92 | args = [editor + ' "$@"', 'sh'] 93 | shell = True 94 | else: 95 | args = [editor] 96 | shell = False 97 | args.append(path) 98 | 99 | try: 100 | rc = subprocess.Popen(args, shell=shell).wait() 101 | except OSError as e: 102 | raise EditorError('editor failed, %s: %s %s' 103 | % (str(e), editor, path)) 104 | if rc != 0: 105 | raise EditorError('editor failed with exit status %d: %s %s' 106 | % (rc, editor, path)) 107 | 108 | fd2 = open(path) 109 | try: 110 | return fd2.read() 111 | finally: 112 | fd2.close() 113 | finally: 114 | if fd: 115 | os.close(fd) 116 | platform_utils.remove(path) 117 | -------------------------------------------------------------------------------- /subcmds/stage.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2008 The Android Open Source Project 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | from __future__ import print_function 17 | import sys 18 | 19 | from color import Coloring 20 | from command import InteractiveCommand 21 | from git_command import GitCommand 22 | 23 | class _ProjectList(Coloring): 24 | def __init__(self, gc): 25 | Coloring.__init__(self, gc, 'interactive') 26 | self.prompt = self.printer('prompt', fg='blue', attr='bold') 27 | self.header = self.printer('header', attr='bold') 28 | self.help = self.printer('help', fg='red', attr='bold') 29 | 30 | class Stage(InteractiveCommand): 31 | common = True 32 | helpSummary = "Stage file(s) for commit" 33 | helpUsage = """ 34 | %prog -i [...] 35 | """ 36 | helpDescription = """ 37 | The '%prog' command stages files to prepare the next commit. 38 | """ 39 | 40 | def _Options(self, p): 41 | p.add_option('-i', '--interactive', 42 | dest='interactive', action='store_true', 43 | help='use interactive staging') 44 | 45 | def Execute(self, opt, args): 46 | if opt.interactive: 47 | self._Interactive(opt, args) 48 | else: 49 | self.Usage() 50 | 51 | def _Interactive(self, opt, args): 52 | all_projects = [p for p in self.GetProjects(args) if p.IsDirty()] 53 | if not all_projects: 54 | print('no projects have uncommitted modifications', file=sys.stderr) 55 | return 56 | 57 | out = _ProjectList(self.manifest.manifestProject.config) 58 | while True: 59 | out.header(' %s', 'project') 60 | out.nl() 61 | 62 | for i in range(len(all_projects)): 63 | project = all_projects[i] 64 | out.write('%3d: %s', i + 1, project.relpath + '/') 65 | out.nl() 66 | out.nl() 67 | 68 | out.write('%3d: (', 0) 69 | out.prompt('q') 70 | out.write('uit)') 71 | out.nl() 72 | 73 | out.prompt('project> ') 74 | try: 75 | a = sys.stdin.readline() 76 | except KeyboardInterrupt: 77 | out.nl() 78 | break 79 | if a == '': 80 | out.nl() 81 | break 82 | 83 | a = a.strip() 84 | if a.lower() in ('q', 'quit', 'exit'): 85 | break 86 | if not a: 87 | continue 88 | 89 | try: 90 | a_index = int(a) 91 | except ValueError: 92 | a_index = None 93 | 94 | if a_index is not None: 95 | if a_index == 0: 96 | break 97 | if 0 < a_index and a_index <= len(all_projects): 98 | _AddI(all_projects[a_index - 1]) 99 | continue 100 | 101 | projects = [p for p in all_projects if a in [p.name, p.relpath]] 102 | if len(projects) == 1: 103 | _AddI(projects[0]) 104 | continue 105 | print('Bye.') 106 | 107 | def _AddI(project): 108 | p = GitCommand(project, ['add', '--interactive'], bare=False) 109 | p.Wait() 110 | -------------------------------------------------------------------------------- /error.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2008 The Android Open Source Project 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | class ManifestParseError(Exception): 17 | """Failed to parse the manifest file. 18 | """ 19 | 20 | class ManifestInvalidRevisionError(Exception): 21 | """The revision value in a project is incorrect. 22 | """ 23 | 24 | class NoManifestException(Exception): 25 | """The required manifest does not exist. 26 | """ 27 | def __init__(self, path, reason): 28 | super(NoManifestException, self).__init__() 29 | self.path = path 30 | self.reason = reason 31 | 32 | def __str__(self): 33 | return self.reason 34 | 35 | class EditorError(Exception): 36 | """Unspecified error from the user's text editor. 37 | """ 38 | def __init__(self, reason): 39 | super(EditorError, self).__init__() 40 | self.reason = reason 41 | 42 | def __str__(self): 43 | return self.reason 44 | 45 | class GitError(Exception): 46 | """Unspecified internal error from git. 47 | """ 48 | def __init__(self, command): 49 | super(GitError, self).__init__() 50 | self.command = command 51 | 52 | def __str__(self): 53 | return self.command 54 | 55 | class UploadError(Exception): 56 | """A bundle upload to Gerrit did not succeed. 57 | """ 58 | def __init__(self, reason): 59 | super(UploadError, self).__init__() 60 | self.reason = reason 61 | 62 | def __str__(self): 63 | return self.reason 64 | 65 | class DownloadError(Exception): 66 | """Cannot download a repository. 67 | """ 68 | def __init__(self, reason): 69 | super(DownloadError, self).__init__() 70 | self.reason = reason 71 | 72 | def __str__(self): 73 | return self.reason 74 | 75 | class NoSuchProjectError(Exception): 76 | """A specified project does not exist in the work tree. 77 | """ 78 | def __init__(self, name=None): 79 | super(NoSuchProjectError, self).__init__() 80 | self.name = name 81 | 82 | def __str__(self): 83 | if self.name is None: 84 | return 'in current directory' 85 | return self.name 86 | 87 | 88 | class InvalidProjectGroupsError(Exception): 89 | """A specified project is not suitable for the specified groups 90 | """ 91 | def __init__(self, name=None): 92 | super(InvalidProjectGroupsError, self).__init__() 93 | self.name = name 94 | 95 | def __str__(self): 96 | if self.name is None: 97 | return 'in current directory' 98 | return self.name 99 | 100 | class RepoChangedException(Exception): 101 | """Thrown if 'repo sync' results in repo updating its internal 102 | repo or manifest repositories. In this special case we must 103 | use exec to re-execute repo with the new code and manifest. 104 | """ 105 | def __init__(self, extra_args=None): 106 | super(RepoChangedException, self).__init__() 107 | self.extra_args = extra_args or [] 108 | 109 | class HookError(Exception): 110 | """Thrown if a 'repo-hook' could not be run. 111 | 112 | The common case is that the file wasn't present when we tried to run it. 113 | """ 114 | -------------------------------------------------------------------------------- /pager.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2008 The Android Open Source Project 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | from __future__ import print_function 17 | import os 18 | import select 19 | import subprocess 20 | import sys 21 | 22 | import platform_utils 23 | 24 | active = False 25 | pager_process = None 26 | old_stdout = None 27 | old_stderr = None 28 | 29 | def RunPager(globalConfig): 30 | if not os.isatty(0) or not os.isatty(1): 31 | return 32 | pager = _SelectPager(globalConfig) 33 | if pager == '' or pager == 'cat': 34 | return 35 | 36 | if platform_utils.isWindows(): 37 | _PipePager(pager); 38 | else: 39 | _ForkPager(pager) 40 | 41 | def TerminatePager(): 42 | global pager_process, old_stdout, old_stderr 43 | if pager_process: 44 | sys.stdout.flush() 45 | sys.stderr.flush() 46 | pager_process.stdin.close() 47 | pager_process.wait(); 48 | pager_process = None 49 | # Restore initial stdout/err in case there is more output in this process 50 | # after shutting down the pager process 51 | sys.stdout = old_stdout 52 | sys.stderr = old_stderr 53 | 54 | def _PipePager(pager): 55 | global pager_process, old_stdout, old_stderr 56 | assert pager_process is None, "Only one active pager process at a time" 57 | # Create pager process, piping stdout/err into its stdin 58 | pager_process = subprocess.Popen([pager], stdin=subprocess.PIPE, stdout=sys.stdout, stderr=sys.stderr) 59 | old_stdout = sys.stdout 60 | old_stderr = sys.stderr 61 | sys.stdout = pager_process.stdin 62 | sys.stderr = pager_process.stdin 63 | 64 | def _ForkPager(pager): 65 | global active 66 | # This process turns into the pager; a child it forks will 67 | # do the real processing and output back to the pager. This 68 | # is necessary to keep the pager in control of the tty. 69 | # 70 | try: 71 | r, w = os.pipe() 72 | pid = os.fork() 73 | if not pid: 74 | os.dup2(w, 1) 75 | os.dup2(w, 2) 76 | os.close(r) 77 | os.close(w) 78 | active = True 79 | return 80 | 81 | os.dup2(r, 0) 82 | os.close(r) 83 | os.close(w) 84 | 85 | _BecomePager(pager) 86 | except Exception: 87 | print("fatal: cannot start pager '%s'" % pager, file=sys.stderr) 88 | sys.exit(255) 89 | 90 | def _SelectPager(globalConfig): 91 | try: 92 | return os.environ['GIT_PAGER'] 93 | except KeyError: 94 | pass 95 | 96 | pager = globalConfig.GetString('core.pager') 97 | if pager: 98 | return pager 99 | 100 | try: 101 | return os.environ['PAGER'] 102 | except KeyError: 103 | pass 104 | 105 | return 'less' 106 | 107 | def _BecomePager(pager): 108 | # Delaying execution of the pager until we have output 109 | # ready works around a long-standing bug in popularly 110 | # available versions of 'less', a better 'more'. 111 | # 112 | _a, _b, _c = select.select([0], [], [0]) 113 | 114 | os.environ['LESS'] = 'FRSX' 115 | 116 | try: 117 | os.execvp(pager, [pager]) 118 | except OSError: 119 | os.execv('/bin/sh', ['sh', '-c', pager]) 120 | -------------------------------------------------------------------------------- /subcmds/download.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2008 The Android Open Source Project 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | from __future__ import print_function 17 | import re 18 | import sys 19 | 20 | from command import Command 21 | from error import GitError 22 | 23 | CHANGE_RE = re.compile(r'^([1-9][0-9]*)(?:[/\.-]([1-9][0-9]*))?$') 24 | 25 | class Download(Command): 26 | common = True 27 | helpSummary = "Download and checkout a change" 28 | helpUsage = """ 29 | %prog {[project] change[/patchset]}... 30 | """ 31 | helpDescription = """ 32 | The '%prog' command downloads a change from the review system and 33 | makes it available in your project's local working directory. 34 | If no project is specified try to use current directory as a project. 35 | """ 36 | 37 | def _Options(self, p): 38 | p.add_option('-c', '--cherry-pick', 39 | dest='cherrypick', action='store_true', 40 | help="cherry-pick instead of checkout") 41 | p.add_option('-r', '--revert', 42 | dest='revert', action='store_true', 43 | help="revert instead of checkout") 44 | p.add_option('-f', '--ff-only', 45 | dest='ffonly', action='store_true', 46 | help="force fast-forward merge") 47 | 48 | def _ParseChangeIds(self, args): 49 | if not args: 50 | self.Usage() 51 | 52 | to_get = [] 53 | project = None 54 | 55 | for a in args: 56 | m = CHANGE_RE.match(a) 57 | if m: 58 | if not project: 59 | project = self.GetProjects(".")[0] 60 | chg_id = int(m.group(1)) 61 | if m.group(2): 62 | ps_id = int(m.group(2)) 63 | else: 64 | ps_id = 1 65 | to_get.append((project, chg_id, ps_id)) 66 | else: 67 | project = self.GetProjects([a])[0] 68 | return to_get 69 | 70 | def Execute(self, opt, args): 71 | for project, change_id, ps_id in self._ParseChangeIds(args): 72 | dl = project.DownloadPatchSet(change_id, ps_id) 73 | if not dl: 74 | print('[%s] change %d/%d not found' 75 | % (project.name, change_id, ps_id), 76 | file=sys.stderr) 77 | sys.exit(1) 78 | 79 | if not opt.revert and not dl.commits: 80 | print('[%s] change %d/%d has already been merged' 81 | % (project.name, change_id, ps_id), 82 | file=sys.stderr) 83 | continue 84 | 85 | if len(dl.commits) > 1: 86 | print('[%s] %d/%d depends on %d unmerged changes:' \ 87 | % (project.name, change_id, ps_id, len(dl.commits)), 88 | file=sys.stderr) 89 | for c in dl.commits: 90 | print(' %s' % (c), file=sys.stderr) 91 | if opt.cherrypick: 92 | try: 93 | project._CherryPick(dl.commit) 94 | except GitError: 95 | print('[%s] Could not complete the cherry-pick of %s' \ 96 | % (project.name, dl.commit), file=sys.stderr) 97 | sys.exit(1) 98 | 99 | elif opt.revert: 100 | project._Revert(dl.commit) 101 | elif opt.ffonly: 102 | project._FastForward(dl.commit, ffonly=True) 103 | else: 104 | project._Checkout(dl.commit) 105 | -------------------------------------------------------------------------------- /subcmds/cherry_pick.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2010 The Android Open Source Project 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | from __future__ import print_function 17 | import re 18 | import sys 19 | from command import Command 20 | from git_command import GitCommand 21 | 22 | CHANGE_ID_RE = re.compile(r'^\s*Change-Id: I([0-9a-f]{40})\s*$') 23 | 24 | class CherryPick(Command): 25 | common = True 26 | helpSummary = "Cherry-pick a change." 27 | helpUsage = """ 28 | %prog 29 | """ 30 | helpDescription = """ 31 | '%prog' cherry-picks a change from one branch to another. 32 | The change id will be updated, and a reference to the old 33 | change id will be added. 34 | """ 35 | 36 | def _Options(self, p): 37 | pass 38 | 39 | def Execute(self, opt, args): 40 | if len(args) != 1: 41 | self.Usage() 42 | 43 | reference = args[0] 44 | 45 | p = GitCommand(None, 46 | ['rev-parse', '--verify', reference], 47 | capture_stdout = True, 48 | capture_stderr = True) 49 | if p.Wait() != 0: 50 | print(p.stderr, file=sys.stderr) 51 | sys.exit(1) 52 | sha1 = p.stdout.strip() 53 | 54 | p = GitCommand(None, ['cat-file', 'commit', sha1], capture_stdout=True) 55 | if p.Wait() != 0: 56 | print("error: Failed to retrieve old commit message", file=sys.stderr) 57 | sys.exit(1) 58 | old_msg = self._StripHeader(p.stdout) 59 | 60 | p = GitCommand(None, 61 | ['cherry-pick', sha1], 62 | capture_stdout = True, 63 | capture_stderr = True) 64 | status = p.Wait() 65 | 66 | print(p.stdout, file=sys.stdout) 67 | print(p.stderr, file=sys.stderr) 68 | 69 | if status == 0: 70 | # The cherry-pick was applied correctly. We just need to edit the 71 | # commit message. 72 | new_msg = self._Reformat(old_msg, sha1) 73 | 74 | p = GitCommand(None, ['commit', '--amend', '-F', '-'], 75 | provide_stdin = True, 76 | capture_stdout = True, 77 | capture_stderr = True) 78 | p.stdin.write(new_msg) 79 | p.stdin.close() 80 | if p.Wait() != 0: 81 | print("error: Failed to update commit message", file=sys.stderr) 82 | sys.exit(1) 83 | 84 | else: 85 | print('NOTE: When committing (please see above) and editing the commit ' 86 | 'message, please remove the old Change-Id-line and add:') 87 | print(self._GetReference(sha1), file=sys.stderr) 88 | print(file=sys.stderr) 89 | 90 | def _IsChangeId(self, line): 91 | return CHANGE_ID_RE.match(line) 92 | 93 | def _GetReference(self, sha1): 94 | return "(cherry picked from commit %s)" % sha1 95 | 96 | def _StripHeader(self, commit_msg): 97 | lines = commit_msg.splitlines() 98 | return "\n".join(lines[lines.index("")+1:]) 99 | 100 | def _Reformat(self, old_msg, sha1): 101 | new_msg = [] 102 | 103 | for line in old_msg.splitlines(): 104 | if not self._IsChangeId(line): 105 | new_msg.append(line) 106 | 107 | # Add a blank line between the message and the change id/reference 108 | try: 109 | if new_msg[-1].strip() != "": 110 | new_msg.append("") 111 | except IndexError: 112 | pass 113 | 114 | new_msg.append(self._GetReference(sha1)) 115 | return "\n".join(new_msg) 116 | -------------------------------------------------------------------------------- /subcmds/push.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2008 The Android Open Source Project 3 | # Copyright (C) 2017 Wave Computing, Inc. John McGehee 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | from __future__ import print_function 18 | import copy 19 | from optparse import SUPPRESS_HELP 20 | import re 21 | import sys 22 | 23 | from command import InteractiveCommand 24 | from editor import Editor 25 | from error import HookError, UploadError 26 | from git_command import GitCommand 27 | from project import RepoHook 28 | from subcmds.upload import Upload 29 | 30 | from pyversion import is_python3 31 | # pylint:disable=W0622 32 | if not is_python3(): 33 | input = raw_input 34 | else: 35 | unicode = str 36 | # pylint:enable=W0622 37 | 38 | UNUSUAL_COMMIT_THRESHOLD = 5 39 | 40 | class Push(Upload): 41 | gerrit = False 42 | common = True 43 | helpSummary = "Push changes to remote" 44 | helpUsage = """ 45 | %prog [--re --cc] []... 46 | """ 47 | helpDescription = """ 48 | 49 | The '%prog' command sends changes to the central repository, such as GitHub or 50 | GitLab. Alternatively, use the 'upload' command to send changes to the Gerrit 51 | Code Review system. 52 | 53 | '%prog' searches for topic branches in local projects that have not yet been 54 | pushed. If multiple topic branches are found, '%prog' opens an editor to allow 55 | the user to select which branches to push. 56 | 57 | '%prog' searches for changes ready to be pushed in all projects listed on the 58 | command line. Projects may be specified either by name, or by a relative or 59 | absolute path to the project's local directory. If no projects are specified, 60 | '%prog' will search for changes in all projects listed in the manifest. 61 | """ 62 | 63 | def _Options(self, p): 64 | p.add_option('--br', 65 | type='string', action='store', dest='branch', 66 | help='Branch to push.') 67 | p.add_option('--cbr', '--current-branch', 68 | dest='current_branch', action='store_true', 69 | help='Push the current git branch.') 70 | p.add_option('-D', '--destination', '--dest', 71 | type='string', action='store', dest='dest_branch', 72 | metavar='BRANCH', 73 | help='Push to this target branch on the remote.') 74 | 75 | # Options relating to upload hook. Note that verify and no-verify are NOT 76 | # opposites of each other, which is why they store to different locations. 77 | # We are using them to match 'git commit' syntax. 78 | # 79 | # Combinations: 80 | # - no-verify=False, verify=False (DEFAULT): 81 | # If stdout is a tty, can prompt about running upload hooks if needed. 82 | # If user denies running hooks, the upload is cancelled. If stdout is 83 | # not a tty and we would need to prompt about upload hooks, upload is 84 | # cancelled. 85 | # - no-verify=False, verify=True: 86 | # Always run upload hooks with no prompt. 87 | # - no-verify=True, verify=False: 88 | # Never run upload hooks, but upload anyway (AKA bypass hooks). 89 | # - no-verify=True, verify=True: 90 | # Invalid 91 | p.add_option('--no-cert-checks', 92 | dest='validate_certs', action='store_false', default=True, 93 | help='Disable verifying ssl certs (unsafe).') 94 | p.add_option('--no-verify', 95 | dest='bypass_hooks', action='store_true', 96 | help='Do not run the push hook.') 97 | p.add_option('--verify', 98 | dest='allow_all_hooks', action='store_true', 99 | help='Run the push hook without prompting.') 100 | 101 | -------------------------------------------------------------------------------- /subcmds/start.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2008 The Android Open Source Project 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | from __future__ import print_function 17 | import os 18 | import sys 19 | 20 | from command import Command 21 | from git_config import IsImmutable 22 | from git_command import git 23 | import gitc_utils 24 | from progress import Progress 25 | from project import SyncBuffer 26 | 27 | class Start(Command): 28 | common = True 29 | helpSummary = "Start a new branch for development" 30 | helpUsage = """ 31 | %prog [--all | ...] 32 | """ 33 | helpDescription = """ 34 | '%prog' begins a new branch of development, starting from the 35 | revision specified in the manifest. 36 | """ 37 | 38 | def _Options(self, p): 39 | p.add_option('--all', 40 | dest='all', action='store_true', 41 | help='begin branch in all projects') 42 | 43 | def Execute(self, opt, args): 44 | if not args: 45 | self.Usage() 46 | 47 | nb = args[0] 48 | if not git.check_ref_format('heads/%s' % nb): 49 | print("error: '%s' is not a valid name" % nb, file=sys.stderr) 50 | sys.exit(1) 51 | 52 | err = [] 53 | projects = [] 54 | if not opt.all: 55 | projects = args[1:] 56 | if len(projects) < 1: 57 | projects = ['.',] # start it in the local project by default 58 | 59 | all_projects = self.GetProjects(projects, 60 | missing_ok=bool(self.gitc_manifest)) 61 | 62 | # This must happen after we find all_projects, since GetProjects may need 63 | # the local directory, which will disappear once we save the GITC manifest. 64 | if self.gitc_manifest: 65 | gitc_projects = self.GetProjects(projects, manifest=self.gitc_manifest, 66 | missing_ok=True) 67 | for project in gitc_projects: 68 | if project.old_revision: 69 | project.already_synced = True 70 | else: 71 | project.already_synced = False 72 | project.old_revision = project.revisionExpr 73 | project.revisionExpr = None 74 | # Save the GITC manifest. 75 | gitc_utils.save_manifest(self.gitc_manifest) 76 | 77 | # Make sure we have a valid CWD 78 | if not os.path.exists(os.getcwd()): 79 | os.chdir(self.manifest.topdir) 80 | 81 | pm = Progress('Starting %s' % nb, len(all_projects)) 82 | for project in all_projects: 83 | pm.update() 84 | 85 | if self.gitc_manifest: 86 | gitc_project = self.gitc_manifest.paths[project.relpath] 87 | # Sync projects that have not been opened. 88 | if not gitc_project.already_synced: 89 | proj_localdir = os.path.join(self.gitc_manifest.gitc_client_dir, 90 | project.relpath) 91 | project.worktree = proj_localdir 92 | if not os.path.exists(proj_localdir): 93 | os.makedirs(proj_localdir) 94 | project.Sync_NetworkHalf() 95 | sync_buf = SyncBuffer(self.manifest.manifestProject.config) 96 | project.Sync_LocalHalf(sync_buf) 97 | project.revisionId = gitc_project.old_revision 98 | 99 | # If the current revision is immutable, such as a SHA1, a tag or 100 | # a change, then we can't push back to it. Substitute with 101 | # dest_branch, if defined; or with manifest default revision instead. 102 | branch_merge = '' 103 | if IsImmutable(project.revisionExpr): 104 | if project.dest_branch: 105 | branch_merge = project.dest_branch 106 | else: 107 | branch_merge = self.manifest.default.revisionExpr 108 | 109 | if not project.StartBranch(nb, branch_merge=branch_merge): 110 | err.append(project) 111 | pm.end() 112 | 113 | if err: 114 | for p in err: 115 | print("error: %s/: cannot start %s" % (p.relpath, nb), 116 | file=sys.stderr) 117 | sys.exit(1) 118 | -------------------------------------------------------------------------------- /git_refs.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2009 The Android Open Source Project 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | import os 17 | from trace import Trace 18 | 19 | HEAD = 'HEAD' 20 | R_CHANGES = 'refs/changes/' 21 | R_HEADS = 'refs/heads/' 22 | R_TAGS = 'refs/tags/' 23 | R_PUB = 'refs/published/' 24 | R_M = 'refs/remotes/m/' 25 | 26 | 27 | class GitRefs(object): 28 | def __init__(self, gitdir): 29 | self._gitdir = gitdir 30 | self._phyref = None 31 | self._symref = None 32 | self._mtime = {} 33 | 34 | @property 35 | def all(self): 36 | self._EnsureLoaded() 37 | return self._phyref 38 | 39 | def get(self, name): 40 | try: 41 | return self.all[name] 42 | except KeyError: 43 | return '' 44 | 45 | def deleted(self, name): 46 | if self._phyref is not None: 47 | if name in self._phyref: 48 | del self._phyref[name] 49 | 50 | if name in self._symref: 51 | del self._symref[name] 52 | 53 | if name in self._mtime: 54 | del self._mtime[name] 55 | 56 | def symref(self, name): 57 | try: 58 | self._EnsureLoaded() 59 | return self._symref[name] 60 | except KeyError: 61 | return '' 62 | 63 | def _EnsureLoaded(self): 64 | if self._phyref is None or self._NeedUpdate(): 65 | self._LoadAll() 66 | 67 | def _NeedUpdate(self): 68 | Trace(': scan refs %s', self._gitdir) 69 | 70 | for name, mtime in self._mtime.items(): 71 | try: 72 | if mtime != os.path.getmtime(os.path.join(self._gitdir, name)): 73 | return True 74 | except OSError: 75 | return True 76 | return False 77 | 78 | def _LoadAll(self): 79 | Trace(': load refs %s', self._gitdir) 80 | 81 | self._phyref = {} 82 | self._symref = {} 83 | self._mtime = {} 84 | 85 | self._ReadPackedRefs() 86 | self._ReadLoose('refs/') 87 | self._ReadLoose1(os.path.join(self._gitdir, HEAD), HEAD) 88 | 89 | scan = self._symref 90 | attempts = 0 91 | while scan and attempts < 5: 92 | scan_next = {} 93 | for name, dest in scan.items(): 94 | if dest in self._phyref: 95 | self._phyref[name] = self._phyref[dest] 96 | else: 97 | scan_next[name] = dest 98 | scan = scan_next 99 | attempts += 1 100 | 101 | def _ReadPackedRefs(self): 102 | path = os.path.join(self._gitdir, 'packed-refs') 103 | try: 104 | fd = open(path, 'r') 105 | mtime = os.path.getmtime(path) 106 | except IOError: 107 | return 108 | except OSError: 109 | return 110 | try: 111 | for line in fd: 112 | line = str(line) 113 | if line[0] == '#': 114 | continue 115 | if line[0] == '^': 116 | continue 117 | 118 | line = line[:-1] 119 | p = line.split(' ') 120 | ref_id = p[0] 121 | name = p[1] 122 | 123 | self._phyref[name] = ref_id 124 | finally: 125 | fd.close() 126 | self._mtime['packed-refs'] = mtime 127 | 128 | def _ReadLoose(self, prefix): 129 | base = os.path.join(self._gitdir, prefix) 130 | for name in os.listdir(base): 131 | p = os.path.join(base, name) 132 | if os.path.isdir(p): 133 | self._mtime[prefix] = os.path.getmtime(base) 134 | self._ReadLoose(prefix + name + '/') 135 | elif name.endswith('.lock'): 136 | pass 137 | else: 138 | self._ReadLoose1(p, prefix + name) 139 | 140 | def _ReadLoose1(self, path, name): 141 | try: 142 | fd = open(path) 143 | except IOError: 144 | return 145 | 146 | try: 147 | try: 148 | mtime = os.path.getmtime(path) 149 | ref_id = fd.readline() 150 | except (IOError, OSError): 151 | return 152 | finally: 153 | fd.close() 154 | 155 | try: 156 | ref_id = ref_id.decode() 157 | except AttributeError: 158 | pass 159 | if not ref_id: 160 | return 161 | ref_id = ref_id[:-1] 162 | 163 | if ref_id.startswith('ref: '): 164 | self._symref[name] = ref_id[5:] 165 | else: 166 | self._phyref[name] = ref_id 167 | self._mtime[name] = mtime 168 | -------------------------------------------------------------------------------- /SUBMITTING_PATCHES.md: -------------------------------------------------------------------------------- 1 | # Short Version 2 | 3 | - Make small logical changes. 4 | - Provide a meaningful commit message. 5 | - Check for coding errors and style nits with pyflakes and flake8 6 | - Make sure all code is under the Apache License, 2.0. 7 | - Publish your changes for review. 8 | - Make corrections if requested. 9 | - Verify your changes on gerrit so they can be submitted. 10 | 11 | `git push https://gerrit-review.googlesource.com/git-repo HEAD:refs/for/master` 12 | 13 | 14 | # Long Version 15 | 16 | I wanted a file describing how to submit patches for repo, 17 | so I started with the one found in the core Git distribution 18 | (Documentation/SubmittingPatches), which itself was based on the 19 | patch submission guidelines for the Linux kernel. 20 | 21 | However there are some differences, so please review and familiarize 22 | yourself with the following relevant bits. 23 | 24 | 25 | ## Make separate commits for logically separate changes. 26 | 27 | Unless your patch is really trivial, you should not be sending 28 | out a patch that was generated between your working tree and your 29 | commit head. Instead, always make a commit with complete commit 30 | message and generate a series of patches from your repository. 31 | It is a good discipline. 32 | 33 | Describe the technical detail of the change(s). 34 | 35 | If your description starts to get too long, that's a sign that you 36 | probably need to split up your commit to finer grained pieces. 37 | 38 | 39 | ## Check for coding errors and style nits with pyflakes and flake8 40 | 41 | ### Coding errors 42 | 43 | Run `pyflakes` on changed modules: 44 | 45 | pyflakes file.py 46 | 47 | Ideally there should be no new errors or warnings introduced. 48 | 49 | ### Style violations 50 | 51 | Run `flake8` on changes modules: 52 | 53 | flake8 file.py 54 | 55 | Note that repo generally follows [Google's python style guide] 56 | (https://google.github.io/styleguide/pyguide.html) rather than [PEP 8] 57 | (https://www.python.org/dev/peps/pep-0008/), so it's possible that 58 | the output of `flake8` will be quite noisy. It's not mandatory to 59 | avoid all warnings, but at least the maximum line length should be 60 | followed. 61 | 62 | If there are many occurrences of the same warning that cannot be 63 | avoided without going against the Google style guide, these may be 64 | suppressed in the included `.flake8` file. 65 | 66 | ## Check the license 67 | 68 | repo is licensed under the Apache License, 2.0. 69 | 70 | Because of this licensing model *every* file within the project 71 | *must* list the license that covers it in the header of the file. 72 | Any new contributions to an existing file *must* be submitted under 73 | the current license of that file. Any new files *must* clearly 74 | indicate which license they are provided under in the file header. 75 | 76 | Please verify that you are legally allowed and willing to submit your 77 | changes under the license covering each file *prior* to submitting 78 | your patch. It is virtually impossible to remove a patch once it 79 | has been applied and pushed out. 80 | 81 | 82 | ## Sending your patches. 83 | 84 | Do not email your patches to anyone. 85 | 86 | Instead, login to the Gerrit Code Review tool at: 87 | 88 | https://gerrit-review.googlesource.com/ 89 | 90 | Ensure you have completed one of the necessary contributor 91 | agreements, providing documentation to the project maintainers that 92 | they have right to redistribute your work under the Apache License: 93 | 94 | https://gerrit-review.googlesource.com/#/settings/agreements 95 | 96 | Ensure you have obtained an HTTP password to authenticate: 97 | 98 | https://gerrit-review.googlesource.com/new-password 99 | 100 | Ensure that you have the local commit hook installed to automatically 101 | add a ChangeId to your commits: 102 | 103 | curl -Lo `git rev-parse --git-dir`/hooks/commit-msg https://gerrit-review.googlesource.com/tools/hooks/commit-msg 104 | chmod +x `git rev-parse --git-dir`/hooks/commit-msg 105 | 106 | If you have already committed your changes you will need to amend the commit 107 | to get the ChangeId added. 108 | 109 | git commit --amend 110 | 111 | Push your patches over HTTPS to the review server, possibly through 112 | a remembered remote to make this easier in the future: 113 | 114 | git config remote.review.url https://gerrit-review.googlesource.com/git-repo 115 | git config remote.review.push HEAD:refs/for/master 116 | 117 | git push review 118 | 119 | You will be automatically emailed a copy of your commits, and any 120 | comments made by the project maintainers. 121 | 122 | 123 | ## Make changes if requested 124 | 125 | The project maintainer who reviews your changes might request changes to your 126 | commit. If you make the requested changes you will need to amend your commit 127 | and push it to the review server again. 128 | 129 | 130 | ## Verify your changes on gerrit 131 | 132 | After you receive a Code-Review+2 from the maintainer, select the Verified 133 | button on the gerrit page for the change. This verifies that you have tested 134 | your changes and notifies the maintainer that they are ready to be submitted. 135 | The maintainer will then submit your changes to the repository. 136 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | STATUS: This seems to be functionally complete--alpha quality. 2 | 3 | Repo is a repository management tool that Google built on top of Git. This fork provides a new `repo push` command. 4 | 5 | As part of the [Android development environment](https://source.android.com/source/developing), Repo unifies the many Git 6 | repositories when necessary, does the uploads to the [revision control system](https://android-review.googlesource.com/), 7 | and automates parts of the Android development workflow. Repo does not replace Git, it just makes it easier to work with 8 | Git in the context of multiple repositories. The repo command is an executable Python script that you can put anywhere 9 | in your path. In working with the Android source files, you will use Repo for across-network operations. For example, 10 | with a single repo command you can pull files from multiple repositories into your local working copy. 11 | 12 | This fork was enhanced to add: 13 | 1. A `repo push` command that performs an ordinary push of the topic branch on all repositories. This allows you to 14 | push the topic branches to GitHub or GitLab, where you can create a pull request or merge request and get your code 15 | reviewed. The existing `repo upload` command continues to upload to Gerrit as usual. 16 | 2. All operations are executed in the same order as defined in the manifest file. In particular, `repo push` and 17 | `repo upload` push to the repositories in the same order that the ```` elements appear in the manifest file. 18 | 19 | # Installing and using repo 20 | 21 | ## Prerequisites 22 | repo requires Python 2.7 or above. For Python 2.6 (untested), you must install the `ordereddict` package before using 23 | repo. 24 | 25 | ## Installation 26 | To install repo, follow the [repo installation instructions](https://source.android.com/source/downloading). Of course 27 | substiture this GitHub repository for the Google repository as required. 28 | 29 | ## Usage 30 | The [Android Developing page](https://source.android.com/source/developing) shows you how to use repo. If you want do 31 | an ordinary push, use `repo push` command in place of `repo upload`. If you wish to upload to Gerrit, use `repo upload` 32 | as instructed. 33 | 34 | The [manifest file reference](docs/manifest-format.txt) explains the contents of the manifest file that you use to 35 | describe your repositories. 36 | 37 | There is also a handy [repo Command Reference](https://source.android.com/source/using-repo). This does not include 38 | `repo push` documentation, but this command will print it: 39 | 40 | repo help push 41 | 42 | # Developer information 43 | 44 | The rest of this page is interesting only to developers, not users. 45 | 46 | # Repository history 47 | 48 | repo has a long history that makes it difficult to discover the canonical repository. 49 | 50 | This repository is a fork of Google's https://gerrit.googlesource.com/git-repo/. It appears that the same code is also served as https://android.googlesource.com/tools/repo/. These two repository names have given rise to duplicate project names "git-repo" and "tools_repo" ("tools/repo" with '/' replaced with '_'). 51 | 52 | Due to the [shutdown of Google Code](http://google-opensource.blogspot.com/2015/03/farewell-to-google-code.html0), the original Google Code repo project https://code.google.com/p/git-repo/ has been archived for quite a while now, and is out of date. 53 | 54 | ## Resyncing with official google repo 55 | 56 | This procedure comes to us from the [esrlabs/git-repo](https://github.com/esrlabs/git-repo) project. 57 | 58 | For resyncing with the official google repo git, here are the commands for resyncing with the tag v1.12.33 of the official google repo: 59 | 60 | # add google git-repo remote with tag 61 | git remote add googlesource https://gerrit.googlesource.com/git-repo/ 62 | git checkout v1.12.33 -b google-latest 63 | 64 | # checkout basis for resync 65 | git checkout google-git-repo-base -b update 66 | git merge --allow-unrelated-histories -Xtheirs --squash google-latest 67 | git commit -m "Update: google git-repo v1.12.33" 68 | git rebase stable 69 | 70 | # solve conflicts; keep portability in mind 71 | 72 | git checkout stable 73 | git rebase update 74 | 75 | # cleanup 76 | git branch -D update 77 | git branch -D google-latest 78 | 79 | 80 | ## Creating a new signed version 81 | 82 | Prepare by creating your own GPG keys as described in the 83 | [Pro Git](https://git-scm.com/book) Book 84 | [Signing Your Work](https://git-scm.com/book/id/v2/Git-Tools-Signing-Your-Work) chapter. 85 | 86 | Export an ASCII key: 87 | 88 | gpg --armor --export you@example.com > public.txt 89 | 90 | Paste this key into file ``repo`` after the other keys. 91 | 92 | Again in file ``repo``, Increment the second element of `KEYRING_VERSION`: 93 | 94 | KEYRING_VERSION = (1, 4) 95 | 96 | In your Git working copy of `git-repo`, add and commit whatever files you have changed. 97 | 98 | Sign the commit: 99 | 100 | git tag -s -u KEYID v0.4.16 -m "COMMENT" 101 | git push origin stable:stable 102 | git push origin v0.4.16 103 | 104 | * For `KEYID`, use the ID of your key. List your keys using the `gpg --list-keys` command. 105 | * Replace `v0.4.16` With the actual version (note that there are two occurrences of this) 106 | * Replace `COMMENT` with something more illuminating 107 | 108 | -------------------------------------------------------------------------------- /subcmds/rebase.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2010 The Android Open Source Project 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | from __future__ import print_function 17 | import sys 18 | 19 | from command import Command 20 | from git_command import GitCommand 21 | 22 | class Rebase(Command): 23 | common = True 24 | helpSummary = "Rebase local branches on upstream branch" 25 | helpUsage = """ 26 | %prog {[...] | -i ...} 27 | """ 28 | helpDescription = """ 29 | '%prog' uses git rebase to move local changes in the current topic branch to 30 | the HEAD of the upstream history, useful when you have made commits in a topic 31 | branch but need to incorporate new upstream changes "underneath" them. 32 | """ 33 | 34 | def _Options(self, p): 35 | p.add_option('-i', '--interactive', 36 | dest="interactive", action="store_true", 37 | help="interactive rebase (single project only)") 38 | 39 | p.add_option('-f', '--force-rebase', 40 | dest='force_rebase', action='store_true', 41 | help='Pass --force-rebase to git rebase') 42 | p.add_option('--no-ff', 43 | dest='no_ff', action='store_true', 44 | help='Pass --no-ff to git rebase') 45 | p.add_option('-q', '--quiet', 46 | dest='quiet', action='store_true', 47 | help='Pass --quiet to git rebase') 48 | p.add_option('--autosquash', 49 | dest='autosquash', action='store_true', 50 | help='Pass --autosquash to git rebase') 51 | p.add_option('--whitespace', 52 | dest='whitespace', action='store', metavar='WS', 53 | help='Pass --whitespace to git rebase') 54 | p.add_option('--auto-stash', 55 | dest='auto_stash', action='store_true', 56 | help='Stash local modifications before starting') 57 | p.add_option('-m', '--onto-manifest', 58 | dest='onto_manifest', action='store_true', 59 | help='Rebase onto the manifest version instead of upstream ' 60 | 'HEAD. This helps to make sure the local tree stays ' 61 | 'consistent if you previously synced to a manifest.') 62 | 63 | def Execute(self, opt, args): 64 | all_projects = self.GetProjects(args) 65 | one_project = len(all_projects) == 1 66 | 67 | if opt.interactive and not one_project: 68 | print('error: interactive rebase not supported with multiple projects', 69 | file=sys.stderr) 70 | if len(args) == 1: 71 | print('note: project %s is mapped to more than one path' % (args[0],), 72 | file=sys.stderr) 73 | return -1 74 | 75 | for project in all_projects: 76 | cb = project.CurrentBranch 77 | if not cb: 78 | if one_project: 79 | print("error: project %s has a detached HEAD" % project.relpath, 80 | file=sys.stderr) 81 | return -1 82 | # ignore branches with detatched HEADs 83 | continue 84 | 85 | upbranch = project.GetBranch(cb) 86 | if not upbranch.LocalMerge: 87 | if one_project: 88 | print("error: project %s does not track any remote branches" 89 | % project.relpath, file=sys.stderr) 90 | return -1 91 | # ignore branches without remotes 92 | continue 93 | 94 | args = ["rebase"] 95 | 96 | if opt.whitespace: 97 | args.append('--whitespace=%s' % opt.whitespace) 98 | 99 | if opt.quiet: 100 | args.append('--quiet') 101 | 102 | if opt.force_rebase: 103 | args.append('--force-rebase') 104 | 105 | if opt.no_ff: 106 | args.append('--no-ff') 107 | 108 | if opt.autosquash: 109 | args.append('--autosquash') 110 | 111 | if opt.interactive: 112 | args.append("-i") 113 | 114 | if opt.onto_manifest: 115 | args.append('--onto') 116 | args.append(project.revisionExpr) 117 | 118 | args.append(upbranch.LocalMerge) 119 | 120 | print('# %s: rebasing %s -> %s' 121 | % (project.relpath, cb, upbranch.LocalMerge), file=sys.stderr) 122 | 123 | needs_stash = False 124 | if opt.auto_stash: 125 | stash_args = ["update-index", "--refresh", "-q"] 126 | 127 | if GitCommand(project, stash_args).Wait() != 0: 128 | needs_stash = True 129 | # Dirty index, requires stash... 130 | stash_args = ["stash"] 131 | 132 | if GitCommand(project, stash_args).Wait() != 0: 133 | return -1 134 | 135 | if GitCommand(project, args).Wait() != 0: 136 | return -1 137 | 138 | if needs_stash: 139 | stash_args.append('pop') 140 | stash_args.append('--quiet') 141 | if GitCommand(project, stash_args).Wait() != 0: 142 | return -1 143 | -------------------------------------------------------------------------------- /color.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2008 The Android Open Source Project 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | import os 17 | import sys 18 | 19 | import pager 20 | 21 | COLORS = {None: -1, 22 | 'normal': -1, 23 | 'black': 0, 24 | 'red': 1, 25 | 'green': 2, 26 | 'yellow': 3, 27 | 'blue': 4, 28 | 'magenta': 5, 29 | 'cyan': 6, 30 | 'white': 7} 31 | 32 | ATTRS = {None: -1, 33 | 'bold': 1, 34 | 'dim': 2, 35 | 'ul': 4, 36 | 'blink': 5, 37 | 'reverse': 7} 38 | 39 | RESET = "\033[m" 40 | 41 | 42 | def is_color(s): 43 | return s in COLORS 44 | 45 | 46 | def is_attr(s): 47 | return s in ATTRS 48 | 49 | 50 | def _Color(fg=None, bg=None, attr=None): 51 | fg = COLORS[fg] 52 | bg = COLORS[bg] 53 | attr = ATTRS[attr] 54 | 55 | if attr >= 0 or fg >= 0 or bg >= 0: 56 | need_sep = False 57 | code = "\033[" 58 | 59 | if attr >= 0: 60 | code += chr(ord('0') + attr) 61 | need_sep = True 62 | 63 | if fg >= 0: 64 | if need_sep: 65 | code += ';' 66 | need_sep = True 67 | 68 | if fg < 8: 69 | code += '3%c' % (ord('0') + fg) 70 | else: 71 | code += '38;5;%d' % fg 72 | 73 | if bg >= 0: 74 | if need_sep: 75 | code += ';' 76 | 77 | if bg < 8: 78 | code += '4%c' % (ord('0') + bg) 79 | else: 80 | code += '48;5;%d' % bg 81 | code += 'm' 82 | else: 83 | code = '' 84 | return code 85 | 86 | DEFAULT = None 87 | 88 | 89 | def SetDefaultColoring(state): 90 | """Set coloring behavior to |state|. 91 | 92 | This is useful for overriding config options via the command line. 93 | """ 94 | if state is None: 95 | # Leave it alone -- return quick! 96 | return 97 | 98 | global DEFAULT 99 | state = state.lower() 100 | if state in ('auto',): 101 | DEFAULT = state 102 | elif state in ('always', 'yes', 'true', True): 103 | DEFAULT = 'always' 104 | elif state in ('never', 'no', 'false', False): 105 | DEFAULT = 'never' 106 | 107 | 108 | class Coloring(object): 109 | def __init__(self, config, section_type): 110 | self._section = 'color.%s' % section_type 111 | self._config = config 112 | self._out = sys.stdout 113 | 114 | on = DEFAULT 115 | if on is None: 116 | on = self._config.GetString(self._section) 117 | if on is None: 118 | on = self._config.GetString('color.ui') 119 | 120 | if on == 'auto': 121 | if pager.active or os.isatty(1): 122 | self._on = True 123 | else: 124 | self._on = False 125 | elif on in ('true', 'always'): 126 | self._on = True 127 | else: 128 | self._on = False 129 | 130 | def redirect(self, out): 131 | self._out = out 132 | 133 | @property 134 | def is_on(self): 135 | return self._on 136 | 137 | def write(self, fmt, *args): 138 | self._out.write(fmt % args) 139 | 140 | def flush(self): 141 | self._out.flush() 142 | 143 | def nl(self): 144 | self._out.write('\n') 145 | 146 | def printer(self, opt=None, fg=None, bg=None, attr=None): 147 | s = self 148 | c = self.colorer(opt, fg, bg, attr) 149 | 150 | def f(fmt, *args): 151 | s._out.write(c(fmt, *args)) 152 | return f 153 | 154 | def nofmt_printer(self, opt=None, fg=None, bg=None, attr=None): 155 | s = self 156 | c = self.nofmt_colorer(opt, fg, bg, attr) 157 | 158 | def f(fmt): 159 | s._out.write(c(fmt)) 160 | return f 161 | 162 | def colorer(self, opt=None, fg=None, bg=None, attr=None): 163 | if self._on: 164 | c = self._parse(opt, fg, bg, attr) 165 | 166 | def f(fmt, *args): 167 | output = fmt % args 168 | return ''.join([c, output, RESET]) 169 | return f 170 | else: 171 | 172 | def f(fmt, *args): 173 | return fmt % args 174 | return f 175 | 176 | def nofmt_colorer(self, opt=None, fg=None, bg=None, attr=None): 177 | if self._on: 178 | c = self._parse(opt, fg, bg, attr) 179 | 180 | def f(fmt): 181 | return ''.join([c, fmt, RESET]) 182 | return f 183 | else: 184 | def f(fmt): 185 | return fmt 186 | return f 187 | 188 | def _parse(self, opt, fg, bg, attr): 189 | if not opt: 190 | return _Color(fg, bg, attr) 191 | 192 | v = self._config.GetString('%s.%s' % (self._section, opt)) 193 | if v is None: 194 | return _Color(fg, bg, attr) 195 | 196 | v = v.strip().lower() 197 | if v == "reset": 198 | return RESET 199 | elif v == '': 200 | return _Color(fg, bg, attr) 201 | 202 | have_fg = False 203 | for a in v.split(' '): 204 | if is_color(a): 205 | if have_fg: 206 | bg = a 207 | else: 208 | fg = a 209 | elif is_attr(a): 210 | attr = a 211 | 212 | return _Color(fg, bg, attr) 213 | -------------------------------------------------------------------------------- /hooks/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # From Gerrit Code Review 2.12.1 3 | # 4 | # Part of Gerrit Code Review (https://www.gerritcodereview.com/) 5 | # 6 | # Copyright (C) 2009 The Android Open Source Project 7 | # 8 | # Licensed under the Apache License, Version 2.0 (the "License"); 9 | # you may not use this file except in compliance with the License. 10 | # You may obtain a copy of the License at 11 | # 12 | # http://www.apache.org/licenses/LICENSE-2.0 13 | # 14 | # Unless required by applicable law or agreed to in writing, software 15 | # distributed under the License is distributed on an "AS IS" BASIS, 16 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | # See the License for the specific language governing permissions and 18 | # limitations under the License. 19 | # 20 | 21 | unset GREP_OPTIONS 22 | 23 | CHANGE_ID_AFTER="Bug|Issue|Test" 24 | MSG="$1" 25 | 26 | # Check for, and add if missing, a unique Change-Id 27 | # 28 | add_ChangeId() { 29 | clean_message=`sed -e ' 30 | /^diff --git .*/{ 31 | s/// 32 | q 33 | } 34 | /^Signed-off-by:/d 35 | /^#/d 36 | ' "$MSG" | git stripspace` 37 | if test -z "$clean_message" 38 | then 39 | return 40 | fi 41 | 42 | # Do not add Change-Id to temp commits 43 | if echo "$clean_message" | head -1 | grep -q '^\(fixup\|squash\)!' 44 | then 45 | return 46 | fi 47 | 48 | if test "false" = "`git config --bool --get gerrit.createChangeId`" 49 | then 50 | return 51 | fi 52 | 53 | # Does Change-Id: already exist? if so, exit (no change). 54 | if grep -i '^Change-Id:' "$MSG" >/dev/null 55 | then 56 | return 57 | fi 58 | 59 | id=`_gen_ChangeId` 60 | T="$MSG.tmp.$$" 61 | AWK=awk 62 | if [ -x /usr/xpg4/bin/awk ]; then 63 | # Solaris AWK is just too broken 64 | AWK=/usr/xpg4/bin/awk 65 | fi 66 | 67 | # Get core.commentChar from git config or use default symbol 68 | commentChar=`git config --get core.commentChar` 69 | commentChar=${commentChar:-#} 70 | 71 | # How this works: 72 | # - parse the commit message as (textLine+ blankLine*)* 73 | # - assume textLine+ to be a footer until proven otherwise 74 | # - exception: the first block is not footer (as it is the title) 75 | # - read textLine+ into a variable 76 | # - then count blankLines 77 | # - once the next textLine appears, print textLine+ blankLine* as these 78 | # aren't footer 79 | # - in END, the last textLine+ block is available for footer parsing 80 | $AWK ' 81 | BEGIN { 82 | # while we start with the assumption that textLine+ 83 | # is a footer, the first block is not. 84 | isFooter = 0 85 | footerComment = 0 86 | blankLines = 0 87 | } 88 | 89 | # Skip lines starting with commentChar without any spaces before it. 90 | /^'"$commentChar"'/ { next } 91 | 92 | # Skip the line starting with the diff command and everything after it, 93 | # up to the end of the file, assuming it is only patch data. 94 | # If more than one line before the diff was empty, strip all but one. 95 | /^diff --git / { 96 | blankLines = 0 97 | while (getline) { } 98 | next 99 | } 100 | 101 | # Count blank lines outside footer comments 102 | /^$/ && (footerComment == 0) { 103 | blankLines++ 104 | next 105 | } 106 | 107 | # Catch footer comment 108 | /^\[[a-zA-Z0-9-]+:/ && (isFooter == 1) { 109 | footerComment = 1 110 | } 111 | 112 | /]$/ && (footerComment == 1) { 113 | footerComment = 2 114 | } 115 | 116 | # We have a non-blank line after blank lines. Handle this. 117 | (blankLines > 0) { 118 | print lines 119 | for (i = 0; i < blankLines; i++) { 120 | print "" 121 | } 122 | 123 | lines = "" 124 | blankLines = 0 125 | isFooter = 1 126 | footerComment = 0 127 | } 128 | 129 | # Detect that the current block is not the footer 130 | (footerComment == 0) && (!/^\[?[a-zA-Z0-9-]+:/ || /^[a-zA-Z0-9-]+:\/\//) { 131 | isFooter = 0 132 | } 133 | 134 | { 135 | # We need this information about the current last comment line 136 | if (footerComment == 2) { 137 | footerComment = 0 138 | } 139 | if (lines != "") { 140 | lines = lines "\n"; 141 | } 142 | lines = lines $0 143 | } 144 | 145 | # Footer handling: 146 | # If the last block is considered a footer, splice in the Change-Id at the 147 | # right place. 148 | # Look for the right place to inject Change-Id by considering 149 | # CHANGE_ID_AFTER. Keys listed in it (case insensitive) come first, 150 | # then Change-Id, then everything else (eg. Signed-off-by:). 151 | # 152 | # Otherwise just print the last block, a new line and the Change-Id as a 153 | # block of its own. 154 | END { 155 | unprinted = 1 156 | if (isFooter == 0) { 157 | print lines "\n" 158 | lines = "" 159 | } 160 | changeIdAfter = "^(" tolower("'"$CHANGE_ID_AFTER"'") "):" 161 | numlines = split(lines, footer, "\n") 162 | for (line = 1; line <= numlines; line++) { 163 | if (unprinted && match(tolower(footer[line]), changeIdAfter) != 1) { 164 | unprinted = 0 165 | print "Change-Id: I'"$id"'" 166 | } 167 | print footer[line] 168 | } 169 | if (unprinted) { 170 | print "Change-Id: I'"$id"'" 171 | } 172 | }' "$MSG" > "$T" && mv "$T" "$MSG" || rm -f "$T" 173 | } 174 | _gen_ChangeIdInput() { 175 | echo "tree `git write-tree`" 176 | if parent=`git rev-parse "HEAD^0" 2>/dev/null` 177 | then 178 | echo "parent $parent" 179 | fi 180 | echo "author `git var GIT_AUTHOR_IDENT`" 181 | echo "committer `git var GIT_COMMITTER_IDENT`" 182 | echo 183 | printf '%s' "$clean_message" 184 | } 185 | _gen_ChangeId() { 186 | _gen_ChangeIdInput | 187 | git hash-object -t commit --stdin 188 | } 189 | 190 | 191 | add_ChangeId 192 | -------------------------------------------------------------------------------- /subcmds/branches.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2009 The Android Open Source Project 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | from __future__ import print_function 17 | import sys 18 | from color import Coloring 19 | from command import Command 20 | 21 | class BranchColoring(Coloring): 22 | def __init__(self, config): 23 | Coloring.__init__(self, config, 'branch') 24 | self.current = self.printer('current', fg='green') 25 | self.local = self.printer('local') 26 | self.notinproject = self.printer('notinproject', fg='red') 27 | 28 | class BranchInfo(object): 29 | def __init__(self, name): 30 | self.name = name 31 | self.current = 0 32 | self.published = 0 33 | self.published_equal = 0 34 | self.projects = [] 35 | 36 | def add(self, b): 37 | if b.current: 38 | self.current += 1 39 | if b.published: 40 | self.published += 1 41 | if b.revision == b.published: 42 | self.published_equal += 1 43 | self.projects.append(b) 44 | 45 | @property 46 | def IsCurrent(self): 47 | return self.current > 0 48 | 49 | @property 50 | def IsSplitCurrent(self): 51 | return self.current != 0 and self.current != len(self.projects) 52 | 53 | @property 54 | def IsPublished(self): 55 | return self.published > 0 56 | 57 | @property 58 | def IsPublishedEqual(self): 59 | return self.published_equal == len(self.projects) 60 | 61 | 62 | class Branches(Command): 63 | common = True 64 | helpSummary = "View current topic branches" 65 | helpUsage = """ 66 | %prog [...] 67 | 68 | Summarizes the currently available topic branches. 69 | 70 | Branch Display 71 | -------------- 72 | 73 | The branch display output by this command is organized into four 74 | columns of information; for example: 75 | 76 | *P nocolor | in repo 77 | repo2 | 78 | 79 | The first column contains a * if the branch is the currently 80 | checked out branch in any of the specified projects, or a blank 81 | if no project has the branch checked out. 82 | 83 | The second column contains either blank, p or P, depending upon 84 | the upload status of the branch. 85 | 86 | (blank): branch not yet published by repo upload 87 | P: all commits were published by repo upload 88 | p: only some commits were published by repo upload 89 | 90 | The third column contains the branch name. 91 | 92 | The fourth column (after the | separator) lists the projects that 93 | the branch appears in, or does not appear in. If no project list 94 | is shown, then the branch appears in all projects. 95 | 96 | """ 97 | 98 | def Execute(self, opt, args): 99 | projects = self.GetProjects(args) 100 | out = BranchColoring(self.manifest.manifestProject.config) 101 | all_branches = {} 102 | project_cnt = len(projects) 103 | 104 | for project in projects: 105 | for name, b in project.GetBranches().items(): 106 | b.project = project 107 | if name not in all_branches: 108 | all_branches[name] = BranchInfo(name) 109 | all_branches[name].add(b) 110 | 111 | names = list(sorted(all_branches)) 112 | 113 | if not names: 114 | print(' (no branches)', file=sys.stderr) 115 | return 116 | 117 | width = 25 118 | for name in names: 119 | if width < len(name): 120 | width = len(name) 121 | 122 | for name in names: 123 | i = all_branches[name] 124 | in_cnt = len(i.projects) 125 | 126 | if i.IsCurrent: 127 | current = '*' 128 | hdr = out.current 129 | else: 130 | current = ' ' 131 | hdr = out.local 132 | 133 | if i.IsPublishedEqual: 134 | published = 'P' 135 | elif i.IsPublished: 136 | published = 'p' 137 | else: 138 | published = ' ' 139 | 140 | hdr('%c%c %-*s' % (current, published, width, name)) 141 | out.write(' |') 142 | 143 | if in_cnt < project_cnt: 144 | fmt = out.write 145 | paths = [] 146 | non_cur_paths = [] 147 | if i.IsSplitCurrent or (in_cnt < project_cnt - in_cnt): 148 | in_type = 'in' 149 | for b in i.projects: 150 | if not i.IsSplitCurrent or b.current: 151 | paths.append(b.project.relpath) 152 | else: 153 | non_cur_paths.append(b.project.relpath) 154 | else: 155 | fmt = out.notinproject 156 | in_type = 'not in' 157 | have = set() 158 | for b in i.projects: 159 | have.add(b.project) 160 | for p in projects: 161 | if not p in have: 162 | paths.append(p.relpath) 163 | 164 | s = ' %s %s' % (in_type, ', '.join(paths)) 165 | if not i.IsSplitCurrent and (width + 7 + len(s) < 80): 166 | fmt = out.current if i.IsCurrent else fmt 167 | fmt(s) 168 | else: 169 | fmt(' %s:' % in_type) 170 | fmt = out.current if i.IsCurrent else out.write 171 | for p in paths: 172 | out.nl() 173 | fmt(width*' ' + ' %s' % p) 174 | fmt = out.write 175 | for p in non_cur_paths: 176 | out.nl() 177 | fmt(width*' ' + ' %s' % p) 178 | else: 179 | out.write(' in all projects') 180 | out.nl() 181 | -------------------------------------------------------------------------------- /subcmds/help.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2008 The Android Open Source Project 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | from __future__ import print_function 17 | import re 18 | import sys 19 | from formatter import AbstractFormatter, DumbWriter 20 | 21 | from color import Coloring 22 | from command import PagedCommand, MirrorSafeCommand, GitcAvailableCommand, GitcClientCommand 23 | import gitc_utils 24 | 25 | class Help(PagedCommand, MirrorSafeCommand): 26 | common = False 27 | helpSummary = "Display detailed help on a command" 28 | helpUsage = """ 29 | %prog [--all|command] 30 | """ 31 | helpDescription = """ 32 | Displays detailed usage information about a command. 33 | """ 34 | 35 | def _PrintAllCommands(self): 36 | print('usage: repo COMMAND [ARGS]') 37 | print('The complete list of recognized repo commands are:') 38 | commandNames = list(sorted(self.commands)) 39 | 40 | maxlen = 0 41 | for name in commandNames: 42 | maxlen = max(maxlen, len(name)) 43 | fmt = ' %%-%ds %%s' % maxlen 44 | 45 | for name in commandNames: 46 | command = self.commands[name] 47 | try: 48 | summary = command.helpSummary.strip() 49 | except AttributeError: 50 | summary = '' 51 | print(fmt % (name, summary)) 52 | print("See 'repo help ' for more information on a " 53 | 'specific command.') 54 | 55 | def _PrintCommonCommands(self): 56 | print('usage: repo COMMAND [ARGS]') 57 | print('The most commonly used repo commands are:') 58 | 59 | def gitc_supported(cmd): 60 | if not isinstance(cmd, GitcAvailableCommand) and not isinstance(cmd, GitcClientCommand): 61 | return True 62 | if self.manifest.isGitcClient: 63 | return True 64 | if isinstance(cmd, GitcClientCommand): 65 | return False 66 | if gitc_utils.get_gitc_manifest_dir(): 67 | return True 68 | return False 69 | 70 | commandNames = list(sorted([name 71 | for name, command in self.commands.items() 72 | if command.common and gitc_supported(command)])) 73 | 74 | maxlen = 0 75 | for name in commandNames: 76 | maxlen = max(maxlen, len(name)) 77 | fmt = ' %%-%ds %%s' % maxlen 78 | 79 | for name in commandNames: 80 | command = self.commands[name] 81 | try: 82 | summary = command.helpSummary.strip() 83 | except AttributeError: 84 | summary = '' 85 | print(fmt % (name, summary)) 86 | print( 87 | "See 'repo help ' for more information on a specific command.\n" 88 | "See 'repo help --all' for a complete list of recognized commands.") 89 | 90 | def _PrintCommandHelp(self, cmd): 91 | class _Out(Coloring): 92 | def __init__(self, gc): 93 | Coloring.__init__(self, gc, 'help') 94 | self.heading = self.printer('heading', attr='bold') 95 | 96 | self.wrap = AbstractFormatter(DumbWriter()) 97 | 98 | def _PrintSection(self, heading, bodyAttr): 99 | try: 100 | body = getattr(cmd, bodyAttr) 101 | except AttributeError: 102 | return 103 | if body == '' or body is None: 104 | return 105 | 106 | self.nl() 107 | 108 | self.heading('%s', heading) 109 | self.nl() 110 | 111 | self.heading('%s', ''.ljust(len(heading), '-')) 112 | self.nl() 113 | 114 | me = 'repo %s' % cmd.NAME 115 | body = body.strip() 116 | body = body.replace('%prog', me) 117 | 118 | asciidoc_hdr = re.compile(r'^\n?([^\n]{1,})\n([=~-]{2,})$') 119 | for para in body.split("\n\n"): 120 | if para.startswith(' '): 121 | self.write('%s', para) 122 | self.nl() 123 | self.nl() 124 | continue 125 | 126 | m = asciidoc_hdr.match(para) 127 | if m: 128 | title = m.group(1) 129 | section_type = m.group(2) 130 | if section_type[0] in ('=', '-'): 131 | p = self.heading 132 | else: 133 | def _p(fmt, *args): 134 | self.write(' ') 135 | self.heading(fmt, *args) 136 | p = _p 137 | 138 | p('%s', title) 139 | self.nl() 140 | p('%s', ''.ljust(len(title), section_type[0])) 141 | self.nl() 142 | continue 143 | 144 | self.wrap.add_flowing_data(para) 145 | self.wrap.end_paragraph(1) 146 | self.wrap.end_paragraph(0) 147 | 148 | out = _Out(self.manifest.globalConfig) 149 | out._PrintSection('Summary', 'helpSummary') 150 | cmd.OptionParser.print_help() 151 | out._PrintSection('Description', 'helpDescription') 152 | 153 | def _Options(self, p): 154 | p.add_option('-a', '--all', 155 | dest='show_all', action='store_true', 156 | help='show the complete list of commands') 157 | 158 | def Execute(self, opt, args): 159 | if len(args) == 0: 160 | if opt.show_all: 161 | self._PrintAllCommands() 162 | else: 163 | self._PrintCommonCommands() 164 | 165 | elif len(args) == 1: 166 | name = args[0] 167 | 168 | try: 169 | cmd = self.commands[name] 170 | except KeyError: 171 | print("repo: '%s' is not a repo command." % name, file=sys.stderr) 172 | sys.exit(1) 173 | 174 | cmd.manifest = self.manifest 175 | self._PrintCommandHelp(cmd) 176 | 177 | else: 178 | self._PrintCommandHelp(self) 179 | -------------------------------------------------------------------------------- /gitc_utils.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2015 The Android Open Source Project 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | from __future__ import print_function 17 | import os 18 | import platform 19 | import re 20 | import sys 21 | import time 22 | 23 | import git_command 24 | import git_config 25 | import wrapper 26 | 27 | from error import ManifestParseError 28 | 29 | NUM_BATCH_RETRIEVE_REVISIONID = 32 30 | 31 | def get_gitc_manifest_dir(): 32 | return wrapper.Wrapper().get_gitc_manifest_dir() 33 | 34 | def parse_clientdir(gitc_fs_path): 35 | return wrapper.Wrapper().gitc_parse_clientdir(gitc_fs_path) 36 | 37 | def _set_project_revisions(projects): 38 | """Sets the revisionExpr for a list of projects. 39 | 40 | Because of the limit of open file descriptors allowed, length of projects 41 | should not be overly large. Recommend calling this function multiple times 42 | with each call not exceeding NUM_BATCH_RETRIEVE_REVISIONID projects. 43 | 44 | @param projects: List of project objects to set the revionExpr for. 45 | """ 46 | # Retrieve the commit id for each project based off of it's current 47 | # revisionExpr and it is not already a commit id. 48 | project_gitcmds = [( 49 | project, git_command.GitCommand(None, 50 | ['ls-remote', 51 | project.remote.url, 52 | project.revisionExpr], 53 | capture_stdout=True, cwd='/tmp')) 54 | for project in projects if not git_config.IsId(project.revisionExpr)] 55 | for proj, gitcmd in project_gitcmds: 56 | if gitcmd.Wait(): 57 | print('FATAL: Failed to retrieve revisionExpr for %s' % proj) 58 | sys.exit(1) 59 | revisionExpr = gitcmd.stdout.split('\t')[0] 60 | if not revisionExpr: 61 | raise(ManifestParseError('Invalid SHA-1 revision project %s (%s)' % 62 | (proj.remote.url, proj.revisionExpr))) 63 | proj.revisionExpr = revisionExpr 64 | 65 | def _manifest_groups(manifest): 66 | """Returns the manifest group string that should be synced 67 | 68 | This is the same logic used by Command.GetProjects(), which is used during 69 | repo sync 70 | 71 | @param manifest: The XmlManifest object 72 | """ 73 | mp = manifest.manifestProject 74 | groups = mp.config.GetString('manifest.groups') 75 | if not groups: 76 | groups = 'default,platform-' + platform.system().lower() 77 | return groups 78 | 79 | def generate_gitc_manifest(gitc_manifest, manifest, paths=None): 80 | """Generate a manifest for shafsd to use for this GITC client. 81 | 82 | @param gitc_manifest: Current gitc manifest, or None if there isn't one yet. 83 | @param manifest: A GitcManifest object loaded with the current repo manifest. 84 | @param paths: List of project paths we want to update. 85 | """ 86 | 87 | print('Generating GITC Manifest by fetching revision SHAs for each ' 88 | 'project.') 89 | if paths is None: 90 | paths = manifest.paths.keys() 91 | 92 | groups = [x for x in re.split(r'[,\s]+', _manifest_groups(manifest)) if x] 93 | 94 | # Convert the paths to projects, and filter them to the matched groups. 95 | projects = [manifest.paths[p] for p in paths] 96 | projects = [p for p in projects if p.MatchesGroups(groups)] 97 | 98 | if gitc_manifest is not None: 99 | for path, proj in manifest.paths.iteritems(): 100 | if not proj.MatchesGroups(groups): 101 | continue 102 | 103 | if not proj.upstream and not git_config.IsId(proj.revisionExpr): 104 | proj.upstream = proj.revisionExpr 105 | 106 | if not path in gitc_manifest.paths: 107 | # Any new projects need their first revision, even if we weren't asked 108 | # for them. 109 | projects.append(proj) 110 | elif not path in paths: 111 | # And copy revisions from the previous manifest if we're not updating 112 | # them now. 113 | gitc_proj = gitc_manifest.paths[path] 114 | if gitc_proj.old_revision: 115 | proj.revisionExpr = None 116 | proj.old_revision = gitc_proj.old_revision 117 | else: 118 | proj.revisionExpr = gitc_proj.revisionExpr 119 | 120 | index = 0 121 | while index < len(projects): 122 | _set_project_revisions( 123 | projects[index:(index+NUM_BATCH_RETRIEVE_REVISIONID)]) 124 | index += NUM_BATCH_RETRIEVE_REVISIONID 125 | 126 | if gitc_manifest is not None: 127 | for path, proj in gitc_manifest.paths.iteritems(): 128 | if proj.old_revision and path in paths: 129 | # If we updated a project that has been started, keep the old-revision 130 | # updated. 131 | repo_proj = manifest.paths[path] 132 | repo_proj.old_revision = repo_proj.revisionExpr 133 | repo_proj.revisionExpr = None 134 | 135 | # Convert URLs from relative to absolute. 136 | for _name, remote in manifest.remotes.iteritems(): 137 | remote.fetchUrl = remote.resolvedFetchUrl 138 | 139 | # Save the manifest. 140 | save_manifest(manifest) 141 | 142 | def save_manifest(manifest, client_dir=None): 143 | """Save the manifest file in the client_dir. 144 | 145 | @param client_dir: Client directory to save the manifest in. 146 | @param manifest: Manifest object to save. 147 | """ 148 | if not client_dir: 149 | client_dir = manifest.gitc_client_dir 150 | with open(os.path.join(client_dir, '.manifest'), 'w') as f: 151 | manifest.Save(f, groups=_manifest_groups(manifest)) 152 | # TODO(sbasi/jorg): Come up with a solution to remove the sleep below. 153 | # Give the GITC filesystem time to register the manifest changes. 154 | time.sleep(3) 155 | -------------------------------------------------------------------------------- /event_log.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2017 The Android Open Source Project 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | from __future__ import print_function 17 | 18 | import json 19 | import multiprocessing 20 | 21 | TASK_COMMAND = 'command' 22 | TASK_SYNC_NETWORK = 'sync-network' 23 | TASK_SYNC_LOCAL = 'sync-local' 24 | 25 | class EventLog(object): 26 | """Event log that records events that occurred during a repo invocation. 27 | 28 | Events are written to the log as a consecutive JSON entries, one per line. 29 | Each entry contains the following keys: 30 | - id: A ('RepoOp', ID) tuple, suitable for storing in a datastore. 31 | The ID is only unique for the invocation of the repo command. 32 | - name: Name of the object being operated upon. 33 | - task_name: The task that was performed. 34 | - start: Timestamp of when the operation started. 35 | - finish: Timestamp of when the operation finished. 36 | - success: Boolean indicating if the operation was successful. 37 | - try_count: A counter indicating the try count of this task. 38 | 39 | Optionally: 40 | - parent: A ('RepoOp', ID) tuple indicating the parent event for nested 41 | events. 42 | 43 | Valid task_names include: 44 | - command: The invocation of a subcommand. 45 | - sync-network: The network component of a sync command. 46 | - sync-local: The local component of a sync command. 47 | 48 | Specific tasks may include additional informational properties. 49 | """ 50 | 51 | def __init__(self): 52 | """Initializes the event log.""" 53 | self._log = [] 54 | self._next_id = _EventIdGenerator() 55 | self._parent = None 56 | 57 | def Add(self, name, task_name, start, finish=None, success=None, 58 | try_count=1, kind='RepoOp'): 59 | """Add an event to the log. 60 | 61 | Args: 62 | name: Name of the object being operated upon. 63 | task_name: A sub-task that was performed for name. 64 | start: Timestamp of when the operation started. 65 | finish: Timestamp of when the operation finished. 66 | success: Boolean indicating if the operation was successful. 67 | try_count: A counter indicating the try count of this task. 68 | kind: The kind of the object for the unique identifier. 69 | 70 | Returns: 71 | A dictionary of the event added to the log. 72 | """ 73 | event = { 74 | 'id': (kind, self._next_id.next()), 75 | 'name': name, 76 | 'task_name': task_name, 77 | 'start_time': start, 78 | 'try': try_count, 79 | } 80 | 81 | if self._parent: 82 | event['parent'] = self._parent['id'] 83 | 84 | if success is not None or finish is not None: 85 | self.FinishEvent(event, finish, success) 86 | 87 | self._log.append(event) 88 | return event 89 | 90 | def AddSync(self, project, task_name, start, finish, success): 91 | """Add a event to the log for a sync command. 92 | 93 | Args: 94 | project: Project being synced. 95 | task_name: A sub-task that was performed for name. 96 | One of (TASK_SYNC_NETWORK, TASK_SYNC_LOCAL) 97 | start: Timestamp of when the operation started. 98 | finish: Timestamp of when the operation finished. 99 | success: Boolean indicating if the operation was successful. 100 | 101 | Returns: 102 | A dictionary of the event added to the log. 103 | """ 104 | event = self.Add(project.relpath, success, start, finish, task_name) 105 | if event is not None: 106 | event['project'] = project.name 107 | if project.revisionExpr: 108 | event['revision'] = project.revisionExpr 109 | if project.remote.url: 110 | event['project_url'] = project.remote.url 111 | if project.remote.fetchUrl: 112 | event['remote_url'] = project.remote.fetchUrl 113 | try: 114 | event['git_hash'] = project.GetCommitRevisionId() 115 | except Exception: 116 | pass 117 | return event 118 | 119 | def GetStatusString(self, success): 120 | """Converst a boolean success to a status string. 121 | 122 | Args: 123 | success: Boolean indicating if the operation was successful. 124 | 125 | Returns: 126 | status string. 127 | """ 128 | return 'pass' if success else 'fail' 129 | 130 | def FinishEvent(self, event, finish, success): 131 | """Finishes an incomplete event. 132 | 133 | Args: 134 | event: An event that has been added to the log. 135 | finish: Timestamp of when the operation finished. 136 | success: Boolean indicating if the operation was successful. 137 | 138 | Returns: 139 | A dictionary of the event added to the log. 140 | """ 141 | event['status'] = self.GetStatusString(success) 142 | event['finish_time'] = finish 143 | return event 144 | 145 | def SetParent(self, event): 146 | """Set a parent event for all new entities. 147 | 148 | Args: 149 | event: The event to use as a parent. 150 | """ 151 | self._parent = event 152 | 153 | def Write(self, filename): 154 | """Writes the log out to a file. 155 | 156 | Args: 157 | filename: The file to write the log to. 158 | """ 159 | with open(filename, 'w+') as f: 160 | for e in self._log: 161 | json.dump(e, f, sort_keys=True) 162 | f.write('\n') 163 | 164 | 165 | def _EventIdGenerator(): 166 | """Returns multi-process safe iterator that generates locally unique id. 167 | 168 | Yields: 169 | A unique, to this invocation of the program, integer id. 170 | """ 171 | eid = multiprocessing.Value('i', 1) 172 | 173 | while True: 174 | with eid.get_lock(): 175 | val = eid.value 176 | eid.value += 1 177 | yield val 178 | -------------------------------------------------------------------------------- /subcmds/info.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2012 The Android Open Source Project 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | from command import PagedCommand 17 | from color import Coloring 18 | from error import NoSuchProjectError 19 | from git_refs import R_M 20 | 21 | class _Coloring(Coloring): 22 | def __init__(self, config): 23 | Coloring.__init__(self, config, "status") 24 | 25 | class Info(PagedCommand): 26 | common = True 27 | helpSummary = "Get info on the manifest branch, current branch or unmerged branches" 28 | helpUsage = "%prog [-dl] [-o [-b]] [...]" 29 | 30 | def _Options(self, p): 31 | p.add_option('-d', '--diff', 32 | dest='all', action='store_true', 33 | help="show full info and commit diff including remote branches") 34 | p.add_option('-o', '--overview', 35 | dest='overview', action='store_true', 36 | help='show overview of all local commits') 37 | p.add_option('-b', '--current-branch', 38 | dest="current_branch", action="store_true", 39 | help="consider only checked out branches") 40 | p.add_option('-l', '--local-only', 41 | dest="local", action="store_true", 42 | help="Disable all remote operations") 43 | 44 | 45 | def Execute(self, opt, args): 46 | self.out = _Coloring(self.manifest.globalConfig) 47 | self.heading = self.out.printer('heading', attr = 'bold') 48 | self.headtext = self.out.printer('headtext', fg = 'yellow') 49 | self.redtext = self.out.printer('redtext', fg = 'red') 50 | self.sha = self.out.printer("sha", fg = 'yellow') 51 | self.text = self.out.nofmt_printer('text') 52 | self.dimtext = self.out.printer('dimtext', attr = 'dim') 53 | 54 | self.opt = opt 55 | 56 | manifestConfig = self.manifest.manifestProject.config 57 | mergeBranch = manifestConfig.GetBranch("default").merge 58 | manifestGroups = (manifestConfig.GetString('manifest.groups') 59 | or 'all,-notdefault') 60 | 61 | self.heading("Manifest branch: ") 62 | if self.manifest.default.revisionExpr: 63 | self.headtext(self.manifest.default.revisionExpr) 64 | self.out.nl() 65 | self.heading("Manifest merge branch: ") 66 | self.headtext(mergeBranch) 67 | self.out.nl() 68 | self.heading("Manifest groups: ") 69 | self.headtext(manifestGroups) 70 | self.out.nl() 71 | 72 | self.printSeparator() 73 | 74 | if not opt.overview: 75 | self.printDiffInfo(args) 76 | else: 77 | self.printCommitOverview(args) 78 | 79 | def printSeparator(self): 80 | self.text("----------------------------") 81 | self.out.nl() 82 | 83 | def printDiffInfo(self, args): 84 | try: 85 | projs = self.GetProjects(args) 86 | except NoSuchProjectError: 87 | return 88 | 89 | for p in projs: 90 | self.heading("Project: ") 91 | self.headtext(p.name) 92 | self.out.nl() 93 | 94 | self.heading("Mount path: ") 95 | self.headtext(p.worktree) 96 | self.out.nl() 97 | 98 | self.heading("Current revision: ") 99 | self.headtext(p.revisionExpr) 100 | self.out.nl() 101 | 102 | localBranches = p.GetBranches().keys() 103 | self.heading("Local Branches: ") 104 | self.redtext(str(len(localBranches))) 105 | if len(localBranches) > 0: 106 | self.text(" [") 107 | self.text(", ".join(localBranches)) 108 | self.text("]") 109 | self.out.nl() 110 | 111 | if self.opt.all: 112 | self.findRemoteLocalDiff(p) 113 | 114 | self.printSeparator() 115 | 116 | def findRemoteLocalDiff(self, project): 117 | #Fetch all the latest commits 118 | if not self.opt.local: 119 | project.Sync_NetworkHalf(quiet=True, current_branch_only=True) 120 | 121 | logTarget = R_M + self.manifest.manifestProject.config.GetBranch("default").merge 122 | 123 | bareTmp = project.bare_git._bare 124 | project.bare_git._bare = False 125 | localCommits = project.bare_git.rev_list( 126 | '--abbrev=8', 127 | '--abbrev-commit', 128 | '--pretty=oneline', 129 | logTarget + "..", 130 | '--') 131 | 132 | originCommits = project.bare_git.rev_list( 133 | '--abbrev=8', 134 | '--abbrev-commit', 135 | '--pretty=oneline', 136 | ".." + logTarget, 137 | '--') 138 | project.bare_git._bare = bareTmp 139 | 140 | self.heading("Local Commits: ") 141 | self.redtext(str(len(localCommits))) 142 | self.dimtext(" (on current branch)") 143 | self.out.nl() 144 | 145 | for c in localCommits: 146 | split = c.split() 147 | self.sha(split[0] + " ") 148 | self.text(" ".join(split[1:])) 149 | self.out.nl() 150 | 151 | self.printSeparator() 152 | 153 | self.heading("Remote Commits: ") 154 | self.redtext(str(len(originCommits))) 155 | self.out.nl() 156 | 157 | for c in originCommits: 158 | split = c.split() 159 | self.sha(split[0] + " ") 160 | self.text(" ".join(split[1:])) 161 | self.out.nl() 162 | 163 | def printCommitOverview(self, args): 164 | all_branches = [] 165 | for project in self.GetProjects(args): 166 | br = [project.GetUploadableBranch(x) 167 | for x in project.GetBranches()] 168 | br = [x for x in br if x] 169 | if self.opt.current_branch: 170 | br = [x for x in br if x.name == project.CurrentBranch] 171 | all_branches.extend(br) 172 | 173 | if not all_branches: 174 | return 175 | 176 | self.out.nl() 177 | self.heading('Projects Overview') 178 | project = None 179 | 180 | for branch in all_branches: 181 | if project != branch.project: 182 | project = branch.project 183 | self.out.nl() 184 | self.headtext(project.relpath) 185 | self.out.nl() 186 | 187 | commits = branch.commits 188 | date = branch.date 189 | self.text('%s %-33s (%2d commit%s, %s)' % ( 190 | branch.name == project.CurrentBranch and '*' or ' ', 191 | branch.name, 192 | len(commits), 193 | len(commits) != 1 and 's' or '', 194 | date)) 195 | self.out.nl() 196 | 197 | for commit in commits: 198 | split = commit.split() 199 | self.text('{0:38}{1} '.format('','-')) 200 | self.sha(split[0] + " ") 201 | self.text(" ".join(split[1:])) 202 | self.out.nl() 203 | -------------------------------------------------------------------------------- /subcmds/status.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2008 The Android Open Source Project 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | from command import PagedCommand 17 | 18 | try: 19 | import threading as _threading 20 | except ImportError: 21 | import dummy_threading as _threading 22 | 23 | import glob 24 | 25 | import itertools 26 | import os 27 | 28 | from color import Coloring 29 | 30 | class Status(PagedCommand): 31 | common = True 32 | helpSummary = "Show the working tree status" 33 | helpUsage = """ 34 | %prog [...] 35 | """ 36 | helpDescription = """ 37 | '%prog' compares the working tree to the staging area (aka index), 38 | and the most recent commit on this branch (HEAD), in each project 39 | specified. A summary is displayed, one line per file where there 40 | is a difference between these three states. 41 | 42 | The -j/--jobs option can be used to run multiple status queries 43 | in parallel. 44 | 45 | The -o/--orphans option can be used to show objects that are in 46 | the working directory, but not associated with a repo project. 47 | This includes unmanaged top-level files and directories, but also 48 | includes deeper items. For example, if dir/subdir/proj1 and 49 | dir/subdir/proj2 are repo projects, dir/subdir/proj3 will be shown 50 | if it is not known to repo. 51 | 52 | Status Display 53 | -------------- 54 | 55 | The status display is organized into three columns of information, 56 | for example if the file 'subcmds/status.py' is modified in the 57 | project 'repo' on branch 'devwork': 58 | 59 | project repo/ branch devwork 60 | -m subcmds/status.py 61 | 62 | The first column explains how the staging area (index) differs from 63 | the last commit (HEAD). Its values are always displayed in upper 64 | case and have the following meanings: 65 | 66 | -: no difference 67 | A: added (not in HEAD, in index ) 68 | M: modified ( in HEAD, in index, different content ) 69 | D: deleted ( in HEAD, not in index ) 70 | R: renamed (not in HEAD, in index, path changed ) 71 | C: copied (not in HEAD, in index, copied from another) 72 | T: mode changed ( in HEAD, in index, same content ) 73 | U: unmerged; conflict resolution required 74 | 75 | The second column explains how the working directory differs from 76 | the index. Its values are always displayed in lower case and have 77 | the following meanings: 78 | 79 | -: new / unknown (not in index, in work tree ) 80 | m: modified ( in index, in work tree, modified ) 81 | d: deleted ( in index, not in work tree ) 82 | 83 | """ 84 | 85 | def _Options(self, p): 86 | p.add_option('-j', '--jobs', 87 | dest='jobs', action='store', type='int', default=2, 88 | help="number of projects to check simultaneously") 89 | p.add_option('-o', '--orphans', 90 | dest='orphans', action='store_true', 91 | help="include objects in working directory outside of repo projects") 92 | p.add_option('-q', '--quiet', action='store_true', 93 | help="only print the name of modified projects") 94 | 95 | def _StatusHelper(self, project, clean_counter, sem, quiet): 96 | """Obtains the status for a specific project. 97 | 98 | Obtains the status for a project, redirecting the output to 99 | the specified object. It will release the semaphore 100 | when done. 101 | 102 | Args: 103 | project: Project to get status of. 104 | clean_counter: Counter for clean projects. 105 | sem: Semaphore, will call release() when complete. 106 | output: Where to output the status. 107 | """ 108 | try: 109 | state = project.PrintWorkTreeStatus(quiet=quiet) 110 | if state == 'CLEAN': 111 | next(clean_counter) 112 | finally: 113 | sem.release() 114 | 115 | def _FindOrphans(self, dirs, proj_dirs, proj_dirs_parents, outstring): 116 | """find 'dirs' that are present in 'proj_dirs_parents' but not in 'proj_dirs'""" 117 | status_header = ' --\t' 118 | for item in dirs: 119 | if not os.path.isdir(item): 120 | outstring.append(''.join([status_header, item])) 121 | continue 122 | if item in proj_dirs: 123 | continue 124 | if item in proj_dirs_parents: 125 | self._FindOrphans(glob.glob('%s/.*' % item) + 126 | glob.glob('%s/*' % item), 127 | proj_dirs, proj_dirs_parents, outstring) 128 | continue 129 | outstring.append(''.join([status_header, item, '/'])) 130 | 131 | def Execute(self, opt, args): 132 | all_projects = self.GetProjects(args) 133 | counter = itertools.count() 134 | 135 | if opt.jobs == 1: 136 | for project in all_projects: 137 | state = project.PrintWorkTreeStatus(quiet=opt.quiet) 138 | if state == 'CLEAN': 139 | next(counter) 140 | else: 141 | sem = _threading.Semaphore(opt.jobs) 142 | threads = [] 143 | for project in all_projects: 144 | sem.acquire() 145 | 146 | t = _threading.Thread(target=self._StatusHelper, 147 | args=(project, counter, sem, opt.quiet)) 148 | threads.append(t) 149 | t.daemon = True 150 | t.start() 151 | for t in threads: 152 | t.join() 153 | if not opt.quiet and len(all_projects) == next(counter): 154 | print('nothing to commit (working directory clean)') 155 | 156 | if opt.orphans: 157 | proj_dirs = set() 158 | proj_dirs_parents = set() 159 | for project in self.GetProjects(None, missing_ok=True): 160 | proj_dirs.add(project.relpath) 161 | (head, _tail) = os.path.split(project.relpath) 162 | while head != "": 163 | proj_dirs_parents.add(head) 164 | (head, _tail) = os.path.split(head) 165 | proj_dirs.add('.repo') 166 | 167 | class StatusColoring(Coloring): 168 | def __init__(self, config): 169 | Coloring.__init__(self, config, 'status') 170 | self.project = self.printer('header', attr = 'bold') 171 | self.untracked = self.printer('untracked', fg = 'red') 172 | 173 | orig_path = os.getcwd() 174 | try: 175 | os.chdir(self.manifest.topdir) 176 | 177 | outstring = [] 178 | self._FindOrphans(glob.glob('.*') + 179 | glob.glob('*'), 180 | proj_dirs, proj_dirs_parents, outstring) 181 | 182 | if outstring: 183 | output = StatusColoring(self.manifest.globalConfig) 184 | output.project('Objects not within a project (orphans)') 185 | output.nl() 186 | for entry in outstring: 187 | output.untracked(entry) 188 | output.nl() 189 | else: 190 | print('No orphan files or directories') 191 | 192 | finally: 193 | # Restore CWD. 194 | os.chdir(orig_path) 195 | -------------------------------------------------------------------------------- /subcmds/diffmanifests.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2014 The Android Open Source Project 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | from color import Coloring 17 | from command import PagedCommand 18 | from manifest_xml import XmlManifest 19 | 20 | class _Coloring(Coloring): 21 | def __init__(self, config): 22 | Coloring.__init__(self, config, "status") 23 | 24 | class Diffmanifests(PagedCommand): 25 | """ A command to see logs in projects represented by manifests 26 | 27 | This is used to see deeper differences between manifests. Where a simple 28 | diff would only show a diff of sha1s for example, this command will display 29 | the logs of the project between both sha1s, allowing user to see diff at a 30 | deeper level. 31 | """ 32 | 33 | common = True 34 | helpSummary = "Manifest diff utility" 35 | helpUsage = """%prog manifest1.xml [manifest2.xml] [options]""" 36 | 37 | helpDescription = """ 38 | The %prog command shows differences between project revisions of manifest1 and 39 | manifest2. if manifest2 is not specified, current manifest.xml will be used 40 | instead. Both absolute and relative paths may be used for manifests. Relative 41 | paths start from project's ".repo/manifests" folder. 42 | 43 | The --raw option Displays the diff in a way that facilitates parsing, the 44 | project pattern will be [] and the 45 | commit pattern will be with status values respectively : 46 | 47 | A = Added project 48 | R = Removed project 49 | C = Changed project 50 | U = Project with unreachable revision(s) (revision(s) not found) 51 | 52 | for project, and 53 | 54 | A = Added commit 55 | R = Removed commit 56 | 57 | for a commit. 58 | 59 | Only changed projects may contain commits, and commit status always starts with 60 | a space, and are part of last printed project. 61 | Unreachable revisions may occur if project is not up to date or if repo has not 62 | been initialized with all the groups, in which case some projects won't be 63 | synced and their revisions won't be found. 64 | 65 | """ 66 | 67 | def _Options(self, p): 68 | p.add_option('--raw', 69 | dest='raw', action='store_true', 70 | help='Display raw diff.') 71 | p.add_option('--no-color', 72 | dest='color', action='store_false', default=True, 73 | help='does not display the diff in color.') 74 | p.add_option('--pretty-format', 75 | dest='pretty_format', action='store', 76 | metavar='', 77 | help='print the log using a custom git pretty format string') 78 | 79 | def _printRawDiff(self, diff): 80 | for project in diff['added']: 81 | self.printText("A %s %s" % (project.relpath, project.revisionExpr)) 82 | self.out.nl() 83 | 84 | for project in diff['removed']: 85 | self.printText("R %s %s" % (project.relpath, project.revisionExpr)) 86 | self.out.nl() 87 | 88 | for project, otherProject in diff['changed']: 89 | self.printText("C %s %s %s" % (project.relpath, project.revisionExpr, 90 | otherProject.revisionExpr)) 91 | self.out.nl() 92 | self._printLogs(project, otherProject, raw=True, color=False) 93 | 94 | for project, otherProject in diff['unreachable']: 95 | self.printText("U %s %s %s" % (project.relpath, project.revisionExpr, 96 | otherProject.revisionExpr)) 97 | self.out.nl() 98 | 99 | def _printDiff(self, diff, color=True, pretty_format=None): 100 | if diff['added']: 101 | self.out.nl() 102 | self.printText('added projects : \n') 103 | self.out.nl() 104 | for project in diff['added']: 105 | self.printProject('\t%s' % (project.relpath)) 106 | self.printText(' at revision ') 107 | self.printRevision(project.revisionExpr) 108 | self.out.nl() 109 | 110 | if diff['removed']: 111 | self.out.nl() 112 | self.printText('removed projects : \n') 113 | self.out.nl() 114 | for project in diff['removed']: 115 | self.printProject('\t%s' % (project.relpath)) 116 | self.printText(' at revision ') 117 | self.printRevision(project.revisionExpr) 118 | self.out.nl() 119 | 120 | if diff['changed']: 121 | self.out.nl() 122 | self.printText('changed projects : \n') 123 | self.out.nl() 124 | for project, otherProject in diff['changed']: 125 | self.printProject('\t%s' % (project.relpath)) 126 | self.printText(' changed from ') 127 | self.printRevision(project.revisionExpr) 128 | self.printText(' to ') 129 | self.printRevision(otherProject.revisionExpr) 130 | self.out.nl() 131 | self._printLogs(project, otherProject, raw=False, color=color, 132 | pretty_format=pretty_format) 133 | self.out.nl() 134 | 135 | if diff['unreachable']: 136 | self.out.nl() 137 | self.printText('projects with unreachable revisions : \n') 138 | self.out.nl() 139 | for project, otherProject in diff['unreachable']: 140 | self.printProject('\t%s ' % (project.relpath)) 141 | self.printRevision(project.revisionExpr) 142 | self.printText(' or ') 143 | self.printRevision(otherProject.revisionExpr) 144 | self.printText(' not found') 145 | self.out.nl() 146 | 147 | def _printLogs(self, project, otherProject, raw=False, color=True, 148 | pretty_format=None): 149 | 150 | logs = project.getAddedAndRemovedLogs(otherProject, 151 | oneline=(pretty_format is None), 152 | color=color, 153 | pretty_format=pretty_format) 154 | if logs['removed']: 155 | removedLogs = logs['removed'].split('\n') 156 | for log in removedLogs: 157 | if log.strip(): 158 | if raw: 159 | self.printText(' R ' + log) 160 | self.out.nl() 161 | else: 162 | self.printRemoved('\t\t[-] ') 163 | self.printText(log) 164 | self.out.nl() 165 | 166 | if logs['added']: 167 | addedLogs = logs['added'].split('\n') 168 | for log in addedLogs: 169 | if log.strip(): 170 | if raw: 171 | self.printText(' A ' + log) 172 | self.out.nl() 173 | else: 174 | self.printAdded('\t\t[+] ') 175 | self.printText(log) 176 | self.out.nl() 177 | 178 | def Execute(self, opt, args): 179 | if not args or len(args) > 2: 180 | self.Usage() 181 | 182 | self.out = _Coloring(self.manifest.globalConfig) 183 | self.printText = self.out.nofmt_printer('text') 184 | if opt.color: 185 | self.printProject = self.out.nofmt_printer('project', attr = 'bold') 186 | self.printAdded = self.out.nofmt_printer('green', fg = 'green', attr = 'bold') 187 | self.printRemoved = self.out.nofmt_printer('red', fg = 'red', attr = 'bold') 188 | self.printRevision = self.out.nofmt_printer('revision', fg = 'yellow') 189 | else: 190 | self.printProject = self.printAdded = self.printRemoved = self.printRevision = self.printText 191 | 192 | manifest1 = XmlManifest(self.manifest.repodir) 193 | manifest1.Override(args[0]) 194 | if len(args) == 1: 195 | manifest2 = self.manifest 196 | else: 197 | manifest2 = XmlManifest(self.manifest.repodir) 198 | manifest2.Override(args[1]) 199 | 200 | diff = manifest1.projectsDiff(manifest2) 201 | if opt.raw: 202 | self._printRawDiff(diff) 203 | else: 204 | self._printDiff(diff, color=opt.color, pretty_format=opt.pretty_format) 205 | -------------------------------------------------------------------------------- /git_command.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2008 The Android Open Source Project 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | from __future__ import print_function 17 | import os 18 | import sys 19 | import subprocess 20 | import tempfile 21 | from signal import SIGTERM 22 | 23 | from error import GitError 24 | import platform_utils 25 | from trace import REPO_TRACE, IsTrace, Trace 26 | from wrapper import Wrapper 27 | 28 | GIT = 'git' 29 | MIN_GIT_VERSION = (1, 5, 4) 30 | GIT_DIR = 'GIT_DIR' 31 | 32 | LAST_GITDIR = None 33 | LAST_CWD = None 34 | 35 | _ssh_proxy_path = None 36 | _ssh_sock_path = None 37 | _ssh_clients = [] 38 | 39 | def ssh_sock(create=True): 40 | global _ssh_sock_path 41 | if _ssh_sock_path is None: 42 | if not create: 43 | return None 44 | tmp_dir = '/tmp' 45 | if not os.path.exists(tmp_dir): 46 | tmp_dir = tempfile.gettempdir() 47 | _ssh_sock_path = os.path.join( 48 | tempfile.mkdtemp('', 'ssh-', tmp_dir), 49 | 'master-%r@%h:%p') 50 | return _ssh_sock_path 51 | 52 | def _ssh_proxy(): 53 | global _ssh_proxy_path 54 | if _ssh_proxy_path is None: 55 | _ssh_proxy_path = os.path.join( 56 | os.path.dirname(__file__), 57 | 'git_ssh') 58 | return _ssh_proxy_path 59 | 60 | def _add_ssh_client(p): 61 | _ssh_clients.append(p) 62 | 63 | def _remove_ssh_client(p): 64 | try: 65 | _ssh_clients.remove(p) 66 | except ValueError: 67 | pass 68 | 69 | def terminate_ssh_clients(): 70 | global _ssh_clients 71 | for p in _ssh_clients: 72 | try: 73 | os.kill(p.pid, SIGTERM) 74 | p.wait() 75 | except OSError: 76 | pass 77 | _ssh_clients = [] 78 | 79 | _git_version = None 80 | 81 | class _GitCall(object): 82 | def version(self): 83 | p = GitCommand(None, ['--version'], capture_stdout=True) 84 | if p.Wait() == 0: 85 | if hasattr(p.stdout, 'decode'): 86 | return p.stdout.decode('utf-8') 87 | else: 88 | return p.stdout 89 | return None 90 | 91 | def version_tuple(self): 92 | global _git_version 93 | if _git_version is None: 94 | ver_str = git.version() 95 | _git_version = Wrapper().ParseGitVersion(ver_str) 96 | if _git_version is None: 97 | print('fatal: "%s" unsupported' % ver_str, file=sys.stderr) 98 | sys.exit(1) 99 | return _git_version 100 | 101 | def __getattr__(self, name): 102 | name = name.replace('_','-') 103 | def fun(*cmdv): 104 | command = [name] 105 | command.extend(cmdv) 106 | return GitCommand(None, command).Wait() == 0 107 | return fun 108 | git = _GitCall() 109 | 110 | def git_require(min_version, fail=False): 111 | git_version = git.version_tuple() 112 | if min_version <= git_version: 113 | return True 114 | if fail: 115 | need = '.'.join(map(str, min_version)) 116 | print('fatal: git %s or later required' % need, file=sys.stderr) 117 | sys.exit(1) 118 | return False 119 | 120 | def _setenv(env, name, value): 121 | env[name] = value.encode() 122 | 123 | class GitCommand(object): 124 | def __init__(self, 125 | project, 126 | cmdv, 127 | bare = False, 128 | provide_stdin = False, 129 | capture_stdout = False, 130 | capture_stderr = False, 131 | disable_editor = False, 132 | ssh_proxy = False, 133 | cwd = None, 134 | gitdir = None): 135 | env = os.environ.copy() 136 | 137 | for key in [REPO_TRACE, 138 | GIT_DIR, 139 | 'GIT_ALTERNATE_OBJECT_DIRECTORIES', 140 | 'GIT_OBJECT_DIRECTORY', 141 | 'GIT_WORK_TREE', 142 | 'GIT_GRAFT_FILE', 143 | 'GIT_INDEX_FILE']: 144 | if key in env: 145 | del env[key] 146 | 147 | # If we are not capturing std* then need to print it. 148 | self.tee = {'stdout': not capture_stdout, 'stderr': not capture_stderr} 149 | 150 | if disable_editor: 151 | _setenv(env, 'GIT_EDITOR', ':') 152 | if ssh_proxy: 153 | _setenv(env, 'REPO_SSH_SOCK', ssh_sock()) 154 | _setenv(env, 'GIT_SSH', _ssh_proxy()) 155 | if 'http_proxy' in env and 'darwin' == sys.platform: 156 | s = "'http.proxy=%s'" % (env['http_proxy'],) 157 | p = env.get('GIT_CONFIG_PARAMETERS') 158 | if p is not None: 159 | s = p + ' ' + s 160 | _setenv(env, 'GIT_CONFIG_PARAMETERS', s) 161 | if 'GIT_ALLOW_PROTOCOL' not in env: 162 | _setenv(env, 'GIT_ALLOW_PROTOCOL', 163 | 'file:git:http:https:ssh:persistent-http:persistent-https:sso:rpc') 164 | 165 | if project: 166 | if not cwd: 167 | cwd = project.worktree 168 | if not gitdir: 169 | gitdir = project.gitdir 170 | 171 | command = [GIT] 172 | if bare: 173 | if gitdir: 174 | _setenv(env, GIT_DIR, gitdir) 175 | cwd = None 176 | command.append(cmdv[0]) 177 | # Need to use the --progress flag for fetch/clone so output will be 178 | # displayed as by default git only does progress output if stderr is a TTY. 179 | if sys.stderr.isatty() and cmdv[0] in ('fetch', 'clone'): 180 | if '--progress' not in cmdv and '--quiet' not in cmdv: 181 | command.append('--progress') 182 | command.extend(cmdv[1:]) 183 | 184 | if provide_stdin: 185 | stdin = subprocess.PIPE 186 | else: 187 | stdin = None 188 | 189 | stdout = subprocess.PIPE 190 | stderr = subprocess.PIPE 191 | 192 | if IsTrace(): 193 | global LAST_CWD 194 | global LAST_GITDIR 195 | 196 | dbg = '' 197 | 198 | if cwd and LAST_CWD != cwd: 199 | if LAST_GITDIR or LAST_CWD: 200 | dbg += '\n' 201 | dbg += ': cd %s\n' % cwd 202 | LAST_CWD = cwd 203 | 204 | if GIT_DIR in env and LAST_GITDIR != env[GIT_DIR]: 205 | if LAST_GITDIR or LAST_CWD: 206 | dbg += '\n' 207 | dbg += ': export GIT_DIR=%s\n' % env[GIT_DIR] 208 | LAST_GITDIR = env[GIT_DIR] 209 | 210 | dbg += ': ' 211 | dbg += ' '.join(command) 212 | if stdin == subprocess.PIPE: 213 | dbg += ' 0<|' 214 | if stdout == subprocess.PIPE: 215 | dbg += ' 1>|' 216 | if stderr == subprocess.PIPE: 217 | dbg += ' 2>|' 218 | Trace('%s', dbg) 219 | 220 | try: 221 | p = subprocess.Popen(command, 222 | cwd = cwd, 223 | env = env, 224 | stdin = stdin, 225 | stdout = stdout, 226 | stderr = stderr) 227 | except Exception as e: 228 | raise GitError('%s: %s' % (command[1], e)) 229 | 230 | if ssh_proxy: 231 | _add_ssh_client(p) 232 | 233 | self.process = p 234 | self.stdin = p.stdin 235 | 236 | def Wait(self): 237 | try: 238 | p = self.process 239 | rc = self._CaptureOutput() 240 | finally: 241 | _remove_ssh_client(p) 242 | return rc 243 | 244 | def _CaptureOutput(self): 245 | p = self.process 246 | s_in = platform_utils.FileDescriptorStreams.create() 247 | s_in.add(p.stdout, sys.stdout, 'stdout') 248 | s_in.add(p.stderr, sys.stderr, 'stderr') 249 | self.stdout = '' 250 | self.stderr = '' 251 | 252 | while not s_in.is_done: 253 | in_ready = s_in.select() 254 | for s in in_ready: 255 | buf = s.read() 256 | if not buf: 257 | s_in.remove(s) 258 | continue 259 | if not hasattr(buf, 'encode'): 260 | buf = buf.decode() 261 | if s.std_name == 'stdout': 262 | self.stdout += buf 263 | else: 264 | self.stderr += buf 265 | if self.tee[s.std_name]: 266 | s.dest.write(buf) 267 | s.dest.flush() 268 | return p.wait() 269 | -------------------------------------------------------------------------------- /platform_utils_win32.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2016 The Android Open Source Project 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | import errno 17 | 18 | from ctypes import WinDLL, get_last_error, FormatError, WinError, addressof 19 | from ctypes import c_buffer 20 | from ctypes.wintypes import BOOL, LPCWSTR, DWORD, HANDLE, POINTER, c_ubyte 21 | from ctypes.wintypes import WCHAR, USHORT, LPVOID, Structure, Union, ULONG 22 | from ctypes.wintypes import byref 23 | 24 | kernel32 = WinDLL('kernel32', use_last_error=True) 25 | 26 | LPDWORD = POINTER(DWORD) 27 | UCHAR = c_ubyte 28 | 29 | # Win32 error codes 30 | ERROR_SUCCESS = 0 31 | ERROR_NOT_SUPPORTED = 50 32 | ERROR_PRIVILEGE_NOT_HELD = 1314 33 | 34 | # Win32 API entry points 35 | CreateSymbolicLinkW = kernel32.CreateSymbolicLinkW 36 | CreateSymbolicLinkW.restype = BOOL 37 | CreateSymbolicLinkW.argtypes = (LPCWSTR, # lpSymlinkFileName In 38 | LPCWSTR, # lpTargetFileName In 39 | DWORD) # dwFlags In 40 | 41 | # Symbolic link creation flags 42 | SYMBOLIC_LINK_FLAG_FILE = 0x00 43 | SYMBOLIC_LINK_FLAG_DIRECTORY = 0x01 44 | 45 | GetFileAttributesW = kernel32.GetFileAttributesW 46 | GetFileAttributesW.restype = DWORD 47 | GetFileAttributesW.argtypes = (LPCWSTR,) # lpFileName In 48 | 49 | INVALID_FILE_ATTRIBUTES = 0xFFFFFFFF 50 | FILE_ATTRIBUTE_REPARSE_POINT = 0x00400 51 | 52 | CreateFileW = kernel32.CreateFileW 53 | CreateFileW.restype = HANDLE 54 | CreateFileW.argtypes = (LPCWSTR, # lpFileName In 55 | DWORD, # dwDesiredAccess In 56 | DWORD, # dwShareMode In 57 | LPVOID, # lpSecurityAttributes In_opt 58 | DWORD, # dwCreationDisposition In 59 | DWORD, # dwFlagsAndAttributes In 60 | HANDLE) # hTemplateFile In_opt 61 | 62 | CloseHandle = kernel32.CloseHandle 63 | CloseHandle.restype = BOOL 64 | CloseHandle.argtypes = (HANDLE,) # hObject In 65 | 66 | INVALID_HANDLE_VALUE = HANDLE(-1).value 67 | OPEN_EXISTING = 3 68 | FILE_FLAG_BACKUP_SEMANTICS = 0x02000000 69 | FILE_FLAG_OPEN_REPARSE_POINT = 0x00200000 70 | 71 | DeviceIoControl = kernel32.DeviceIoControl 72 | DeviceIoControl.restype = BOOL 73 | DeviceIoControl.argtypes = (HANDLE, # hDevice In 74 | DWORD, # dwIoControlCode In 75 | LPVOID, # lpInBuffer In_opt 76 | DWORD, # nInBufferSize In 77 | LPVOID, # lpOutBuffer Out_opt 78 | DWORD, # nOutBufferSize In 79 | LPDWORD, # lpBytesReturned Out_opt 80 | LPVOID) # lpOverlapped Inout_opt 81 | 82 | # Device I/O control flags and options 83 | FSCTL_GET_REPARSE_POINT = 0x000900A8 84 | IO_REPARSE_TAG_MOUNT_POINT = 0xA0000003 85 | IO_REPARSE_TAG_SYMLINK = 0xA000000C 86 | MAXIMUM_REPARSE_DATA_BUFFER_SIZE = 0x4000 87 | 88 | 89 | class GENERIC_REPARSE_BUFFER(Structure): 90 | _fields_ = (('DataBuffer', UCHAR * 1),) 91 | 92 | 93 | class SYMBOLIC_LINK_REPARSE_BUFFER(Structure): 94 | _fields_ = (('SubstituteNameOffset', USHORT), 95 | ('SubstituteNameLength', USHORT), 96 | ('PrintNameOffset', USHORT), 97 | ('PrintNameLength', USHORT), 98 | ('Flags', ULONG), 99 | ('PathBuffer', WCHAR * 1)) 100 | 101 | @property 102 | def PrintName(self): 103 | arrayt = WCHAR * (self.PrintNameLength // 2) 104 | offset = type(self).PathBuffer.offset + self.PrintNameOffset 105 | return arrayt.from_address(addressof(self) + offset).value 106 | 107 | 108 | class MOUNT_POINT_REPARSE_BUFFER(Structure): 109 | _fields_ = (('SubstituteNameOffset', USHORT), 110 | ('SubstituteNameLength', USHORT), 111 | ('PrintNameOffset', USHORT), 112 | ('PrintNameLength', USHORT), 113 | ('PathBuffer', WCHAR * 1)) 114 | 115 | @property 116 | def PrintName(self): 117 | arrayt = WCHAR * (self.PrintNameLength // 2) 118 | offset = type(self).PathBuffer.offset + self.PrintNameOffset 119 | return arrayt.from_address(addressof(self) + offset).value 120 | 121 | 122 | class REPARSE_DATA_BUFFER(Structure): 123 | class REPARSE_BUFFER(Union): 124 | _fields_ = (('SymbolicLinkReparseBuffer', SYMBOLIC_LINK_REPARSE_BUFFER), 125 | ('MountPointReparseBuffer', MOUNT_POINT_REPARSE_BUFFER), 126 | ('GenericReparseBuffer', GENERIC_REPARSE_BUFFER)) 127 | _fields_ = (('ReparseTag', ULONG), 128 | ('ReparseDataLength', USHORT), 129 | ('Reserved', USHORT), 130 | ('ReparseBuffer', REPARSE_BUFFER)) 131 | _anonymous_ = ('ReparseBuffer',) 132 | 133 | 134 | def create_filesymlink(source, link_name): 135 | """Creates a Windows file symbolic link source pointing to link_name.""" 136 | _create_symlink(source, link_name, SYMBOLIC_LINK_FLAG_FILE) 137 | 138 | 139 | def create_dirsymlink(source, link_name): 140 | """Creates a Windows directory symbolic link source pointing to link_name. 141 | """ 142 | _create_symlink(source, link_name, SYMBOLIC_LINK_FLAG_DIRECTORY) 143 | 144 | 145 | def _create_symlink(source, link_name, dwFlags): 146 | # Note: Win32 documentation for CreateSymbolicLink is incorrect. 147 | # On success, the function returns "1". 148 | # On error, the function returns some random value (e.g. 1280). 149 | # The best bet seems to use "GetLastError" and check for error/success. 150 | CreateSymbolicLinkW(link_name, source, dwFlags) 151 | code = get_last_error() 152 | if code != ERROR_SUCCESS: 153 | error_desc = FormatError(code).strip() 154 | if code == ERROR_PRIVILEGE_NOT_HELD: 155 | raise OSError(errno.EPERM, error_desc, link_name) 156 | _raise_winerror( 157 | code, 158 | 'Error creating symbolic link \"%s\"'.format(link_name)) 159 | 160 | 161 | def islink(path): 162 | result = GetFileAttributesW(path) 163 | if result == INVALID_FILE_ATTRIBUTES: 164 | return False 165 | return bool(result & FILE_ATTRIBUTE_REPARSE_POINT) 166 | 167 | 168 | def readlink(path): 169 | reparse_point_handle = CreateFileW(path, 170 | 0, 171 | 0, 172 | None, 173 | OPEN_EXISTING, 174 | FILE_FLAG_OPEN_REPARSE_POINT | 175 | FILE_FLAG_BACKUP_SEMANTICS, 176 | None) 177 | if reparse_point_handle == INVALID_HANDLE_VALUE: 178 | _raise_winerror( 179 | get_last_error(), 180 | 'Error opening symblic link \"%s\"'.format(path)) 181 | target_buffer = c_buffer(MAXIMUM_REPARSE_DATA_BUFFER_SIZE) 182 | n_bytes_returned = DWORD() 183 | io_result = DeviceIoControl(reparse_point_handle, 184 | FSCTL_GET_REPARSE_POINT, 185 | None, 186 | 0, 187 | target_buffer, 188 | len(target_buffer), 189 | byref(n_bytes_returned), 190 | None) 191 | CloseHandle(reparse_point_handle) 192 | if not io_result: 193 | _raise_winerror( 194 | get_last_error(), 195 | 'Error reading symblic link \"%s\"'.format(path)) 196 | rdb = REPARSE_DATA_BUFFER.from_buffer(target_buffer) 197 | if rdb.ReparseTag == IO_REPARSE_TAG_SYMLINK: 198 | return _preserve_encoding(path, rdb.SymbolicLinkReparseBuffer.PrintName) 199 | elif rdb.ReparseTag == IO_REPARSE_TAG_MOUNT_POINT: 200 | return _preserve_encoding(path, rdb.MountPointReparseBuffer.PrintName) 201 | # Unsupported reparse point type 202 | _raise_winerror( 203 | ERROR_NOT_SUPPORTED, 204 | 'Error reading symblic link \"%s\"'.format(path)) 205 | 206 | 207 | def _preserve_encoding(source, target): 208 | """Ensures target is the same string type (i.e. unicode or str) as source.""" 209 | if isinstance(source, unicode): 210 | return unicode(target) 211 | return str(target) 212 | 213 | 214 | def _raise_winerror(code, error_desc): 215 | win_error_desc = FormatError(code).strip() 216 | error_desc = "%s: %s".format(error_desc, win_error_desc) 217 | raise WinError(code, error_desc) 218 | -------------------------------------------------------------------------------- /command.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2008 The Android Open Source Project 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | import os 17 | import optparse 18 | import platform 19 | import re 20 | import sys 21 | 22 | from event_log import EventLog 23 | from error import NoSuchProjectError 24 | from error import InvalidProjectGroupsError 25 | 26 | 27 | class Command(object): 28 | """Base class for any command line action in repo. 29 | """ 30 | 31 | common = False 32 | event_log = EventLog() 33 | manifest = None 34 | _optparse = None 35 | 36 | def WantPager(self, _opt): 37 | return False 38 | 39 | def ReadEnvironmentOptions(self, opts): 40 | """ Set options from environment variables. """ 41 | 42 | env_options = self._RegisteredEnvironmentOptions() 43 | 44 | for env_key, opt_key in env_options.items(): 45 | # Get the user-set option value if any 46 | opt_value = getattr(opts, opt_key) 47 | 48 | # If the value is set, it means the user has passed it as a command 49 | # line option, and we should use that. Otherwise we can try to set it 50 | # with the value from the corresponding environment variable. 51 | if opt_value is not None: 52 | continue 53 | 54 | env_value = os.environ.get(env_key) 55 | if env_value is not None: 56 | setattr(opts, opt_key, env_value) 57 | 58 | return opts 59 | 60 | @property 61 | def OptionParser(self): 62 | if self._optparse is None: 63 | try: 64 | me = 'repo %s' % self.NAME 65 | usage = self.helpUsage.strip().replace('%prog', me) 66 | except AttributeError: 67 | usage = 'repo %s' % self.NAME 68 | self._optparse = optparse.OptionParser(usage=usage) 69 | self._Options(self._optparse) 70 | return self._optparse 71 | 72 | def _Options(self, p): 73 | """Initialize the option parser. 74 | """ 75 | 76 | def _RegisteredEnvironmentOptions(self): 77 | """Get options that can be set from environment variables. 78 | 79 | Return a dictionary mapping environment variable name 80 | to option key name that it can override. 81 | 82 | Example: {'REPO_MY_OPTION': 'my_option'} 83 | 84 | Will allow the option with key value 'my_option' to be set 85 | from the value in the environment variable named 'REPO_MY_OPTION'. 86 | 87 | Note: This does not work properly for options that are explicitly 88 | set to None by the user, or options that are defined with a 89 | default value other than None. 90 | 91 | """ 92 | return {} 93 | 94 | def Usage(self): 95 | """Display usage and terminate. 96 | """ 97 | self.OptionParser.print_usage() 98 | sys.exit(1) 99 | 100 | def Execute(self, opt, args): 101 | """Perform the action, after option parsing is complete. 102 | """ 103 | raise NotImplementedError 104 | 105 | def _ResetPathToProjectMap(self, projects): 106 | self._by_path = dict((p.worktree, p) for p in projects) 107 | 108 | def _UpdatePathToProjectMap(self, project): 109 | self._by_path[project.worktree] = project 110 | 111 | def _GetProjectByPath(self, manifest, path): 112 | project = None 113 | if os.path.exists(path): 114 | oldpath = None 115 | while path and \ 116 | path != oldpath and \ 117 | path != manifest.topdir: 118 | try: 119 | project = self._by_path[path] 120 | break 121 | except KeyError: 122 | oldpath = path 123 | path = os.path.dirname(path) 124 | if not project and path == manifest.topdir: 125 | try: 126 | project = self._by_path[path] 127 | except KeyError: 128 | pass 129 | else: 130 | try: 131 | project = self._by_path[path] 132 | except KeyError: 133 | pass 134 | return project 135 | 136 | def GetProjects(self, args, manifest=None, groups='', missing_ok=False, 137 | submodules_ok=False): 138 | """A list of projects that match the arguments. 139 | """ 140 | if not manifest: 141 | manifest = self.manifest 142 | all_projects_list = manifest.projects 143 | result = [] 144 | 145 | mp = manifest.manifestProject 146 | 147 | if not groups: 148 | groups = mp.config.GetString('manifest.groups') 149 | if not groups: 150 | groups = 'default,platform-' + platform.system().lower() 151 | groups = [x for x in re.split(r'[,\s]+', groups) if x] 152 | 153 | if not args: 154 | derived_projects = {} 155 | for project in all_projects_list: 156 | if submodules_ok or project.sync_s: 157 | derived_projects.update((p.name, p) 158 | for p in project.GetDerivedSubprojects()) 159 | all_projects_list.extend(derived_projects.values()) 160 | for project in all_projects_list: 161 | if (missing_ok or project.Exists) and project.MatchesGroups(groups): 162 | result.append(project) 163 | else: 164 | self._ResetPathToProjectMap(all_projects_list) 165 | 166 | for arg in args: 167 | projects = manifest.GetProjectsWithName(arg) 168 | 169 | if not projects: 170 | path = os.path.abspath(arg).replace('\\', '/') 171 | project = self._GetProjectByPath(manifest, path) 172 | 173 | # If it's not a derived project, update path->project mapping and 174 | # search again, as arg might actually point to a derived subproject. 175 | if (project and not project.Derived and (submodules_ok or 176 | project.sync_s)): 177 | search_again = False 178 | for subproject in project.GetDerivedSubprojects(): 179 | self._UpdatePathToProjectMap(subproject) 180 | search_again = True 181 | if search_again: 182 | project = self._GetProjectByPath(manifest, path) or project 183 | 184 | if project: 185 | projects = [project] 186 | 187 | if not projects: 188 | raise NoSuchProjectError(arg) 189 | 190 | for project in projects: 191 | if not missing_ok and not project.Exists: 192 | raise NoSuchProjectError(arg) 193 | if not project.MatchesGroups(groups): 194 | raise InvalidProjectGroupsError(arg) 195 | 196 | result.extend(projects) 197 | # Do not sort; maintain the order from the manifest file. 198 | return result 199 | 200 | def FindProjects(self, args, inverse=False): 201 | result = [] 202 | patterns = [re.compile(r'%s' % a, re.IGNORECASE) for a in args] 203 | for project in self.GetProjects(''): 204 | for pattern in patterns: 205 | match = pattern.search(project.name) or pattern.search(project.relpath) 206 | if not inverse and match: 207 | result.append(project) 208 | break 209 | if inverse and match: 210 | break 211 | else: 212 | if inverse: 213 | result.append(project) 214 | result.sort(key=lambda project: project.relpath) 215 | return result 216 | 217 | 218 | # pylint: disable=W0223 219 | # Pylint warns that the `InteractiveCommand` and `PagedCommand` classes do not 220 | # override method `Execute` which is abstract in `Command`. Since that method 221 | # is always implemented in classes derived from `InteractiveCommand` and 222 | # `PagedCommand`, this warning can be suppressed. 223 | class InteractiveCommand(Command): 224 | """Command which requires user interaction on the tty and 225 | must not run within a pager, even if the user asks to. 226 | """ 227 | def WantPager(self, _opt): 228 | return False 229 | 230 | 231 | class PagedCommand(Command): 232 | """Command which defaults to output in a pager, as its 233 | display tends to be larger than one screen full. 234 | """ 235 | def WantPager(self, _opt): 236 | return True 237 | 238 | # pylint: enable=W0223 239 | 240 | 241 | class MirrorSafeCommand(object): 242 | """Command permits itself to run within a mirror, 243 | and does not require a working directory. 244 | """ 245 | 246 | 247 | class GitcAvailableCommand(object): 248 | """Command that requires GITC to be available, but does 249 | not require the local client to be a GITC client. 250 | """ 251 | 252 | 253 | class GitcClientCommand(object): 254 | """Command that requires the local client to be a GITC 255 | client. 256 | """ 257 | -------------------------------------------------------------------------------- /subcmds/grep.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2009 The Android Open Source Project 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | from __future__ import print_function 17 | import sys 18 | from color import Coloring 19 | from command import PagedCommand 20 | from git_command import git_require, GitCommand 21 | 22 | class GrepColoring(Coloring): 23 | def __init__(self, config): 24 | Coloring.__init__(self, config, 'grep') 25 | self.project = self.printer('project', attr='bold') 26 | 27 | class Grep(PagedCommand): 28 | common = True 29 | helpSummary = "Print lines matching a pattern" 30 | helpUsage = """ 31 | %prog {pattern | -e pattern} [...] 32 | """ 33 | helpDescription = """ 34 | Search for the specified patterns in all project files. 35 | 36 | Boolean Options 37 | --------------- 38 | 39 | The following options can appear as often as necessary to express 40 | the pattern to locate: 41 | 42 | -e PATTERN 43 | --and, --or, --not, -(, -) 44 | 45 | Further, the -r/--revision option may be specified multiple times 46 | in order to scan multiple trees. If the same file matches in more 47 | than one tree, only the first result is reported, prefixed by the 48 | revision name it was found under. 49 | 50 | Examples 51 | ------- 52 | 53 | Look for a line that has '#define' and either 'MAX_PATH or 'PATH_MAX': 54 | 55 | repo grep -e '#define' --and -\\( -e MAX_PATH -e PATH_MAX \\) 56 | 57 | Look for a line that has 'NODE' or 'Unexpected' in files that 58 | contain a line that matches both expressions: 59 | 60 | repo grep --all-match -e NODE -e Unexpected 61 | 62 | """ 63 | 64 | def _Options(self, p): 65 | def carry(option, 66 | opt_str, 67 | value, 68 | parser): 69 | pt = getattr(parser.values, 'cmd_argv', None) 70 | if pt is None: 71 | pt = [] 72 | setattr(parser.values, 'cmd_argv', pt) 73 | 74 | if opt_str == '-(': 75 | pt.append('(') 76 | elif opt_str == '-)': 77 | pt.append(')') 78 | else: 79 | pt.append(opt_str) 80 | 81 | if value is not None: 82 | pt.append(value) 83 | 84 | g = p.add_option_group('Sources') 85 | g.add_option('--cached', 86 | action='callback', callback=carry, 87 | help='Search the index, instead of the work tree') 88 | g.add_option('-r', '--revision', 89 | dest='revision', action='append', metavar='TREEish', 90 | help='Search TREEish, instead of the work tree') 91 | 92 | g = p.add_option_group('Pattern') 93 | g.add_option('-e', 94 | action='callback', callback=carry, 95 | metavar='PATTERN', type='str', 96 | help='Pattern to search for') 97 | g.add_option('-i', '--ignore-case', 98 | action='callback', callback=carry, 99 | help='Ignore case differences') 100 | g.add_option('-a', '--text', 101 | action='callback', callback=carry, 102 | help="Process binary files as if they were text") 103 | g.add_option('-I', 104 | action='callback', callback=carry, 105 | help="Don't match the pattern in binary files") 106 | g.add_option('-w', '--word-regexp', 107 | action='callback', callback=carry, 108 | help='Match the pattern only at word boundaries') 109 | g.add_option('-v', '--invert-match', 110 | action='callback', callback=carry, 111 | help='Select non-matching lines') 112 | g.add_option('-G', '--basic-regexp', 113 | action='callback', callback=carry, 114 | help='Use POSIX basic regexp for patterns (default)') 115 | g.add_option('-E', '--extended-regexp', 116 | action='callback', callback=carry, 117 | help='Use POSIX extended regexp for patterns') 118 | g.add_option('-F', '--fixed-strings', 119 | action='callback', callback=carry, 120 | help='Use fixed strings (not regexp) for pattern') 121 | 122 | g = p.add_option_group('Pattern Grouping') 123 | g.add_option('--all-match', 124 | action='callback', callback=carry, 125 | help='Limit match to lines that have all patterns') 126 | g.add_option('--and', '--or', '--not', 127 | action='callback', callback=carry, 128 | help='Boolean operators to combine patterns') 129 | g.add_option('-(', '-)', 130 | action='callback', callback=carry, 131 | help='Boolean operator grouping') 132 | 133 | g = p.add_option_group('Output') 134 | g.add_option('-n', 135 | action='callback', callback=carry, 136 | help='Prefix the line number to matching lines') 137 | g.add_option('-C', 138 | action='callback', callback=carry, 139 | metavar='CONTEXT', type='str', 140 | help='Show CONTEXT lines around match') 141 | g.add_option('-B', 142 | action='callback', callback=carry, 143 | metavar='CONTEXT', type='str', 144 | help='Show CONTEXT lines before match') 145 | g.add_option('-A', 146 | action='callback', callback=carry, 147 | metavar='CONTEXT', type='str', 148 | help='Show CONTEXT lines after match') 149 | g.add_option('-l', '--name-only', '--files-with-matches', 150 | action='callback', callback=carry, 151 | help='Show only file names containing matching lines') 152 | g.add_option('-L', '--files-without-match', 153 | action='callback', callback=carry, 154 | help='Show only file names not containing matching lines') 155 | 156 | 157 | def Execute(self, opt, args): 158 | out = GrepColoring(self.manifest.manifestProject.config) 159 | 160 | cmd_argv = ['grep'] 161 | if out.is_on and git_require((1, 6, 3)): 162 | cmd_argv.append('--color') 163 | cmd_argv.extend(getattr(opt, 'cmd_argv', [])) 164 | 165 | if '-e' not in cmd_argv: 166 | if not args: 167 | self.Usage() 168 | cmd_argv.append('-e') 169 | cmd_argv.append(args[0]) 170 | args = args[1:] 171 | 172 | projects = self.GetProjects(args) 173 | 174 | full_name = False 175 | if len(projects) > 1: 176 | cmd_argv.append('--full-name') 177 | full_name = True 178 | 179 | have_rev = False 180 | if opt.revision: 181 | if '--cached' in cmd_argv: 182 | print('fatal: cannot combine --cached and --revision', file=sys.stderr) 183 | sys.exit(1) 184 | have_rev = True 185 | cmd_argv.extend(opt.revision) 186 | cmd_argv.append('--') 187 | 188 | bad_rev = False 189 | have_match = False 190 | 191 | for project in projects: 192 | p = GitCommand(project, 193 | cmd_argv, 194 | bare = False, 195 | capture_stdout = True, 196 | capture_stderr = True) 197 | if p.Wait() != 0: 198 | # no results 199 | # 200 | if p.stderr: 201 | if have_rev and 'fatal: ambiguous argument' in p.stderr: 202 | bad_rev = True 203 | else: 204 | out.project('--- project %s ---' % project.relpath) 205 | out.nl() 206 | out.write("%s", p.stderr) 207 | out.nl() 208 | continue 209 | have_match = True 210 | 211 | # We cut the last element, to avoid a blank line. 212 | # 213 | r = p.stdout.split('\n') 214 | r = r[0:-1] 215 | 216 | if have_rev and full_name: 217 | for line in r: 218 | rev, line = line.split(':', 1) 219 | out.write("%s", rev) 220 | out.write(':') 221 | out.project(project.relpath) 222 | out.write('/') 223 | out.write("%s", line) 224 | out.nl() 225 | elif full_name: 226 | for line in r: 227 | out.project(project.relpath) 228 | out.write('/') 229 | out.write("%s", line) 230 | out.nl() 231 | else: 232 | for line in r: 233 | print(line) 234 | 235 | if have_match: 236 | sys.exit(0) 237 | elif have_rev and bad_rev: 238 | for r in opt.revision: 239 | print("error: can't search revision %s" % r, file=sys.stderr) 240 | sys.exit(1) 241 | else: 242 | sys.exit(1) 243 | -------------------------------------------------------------------------------- /platform_utils.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2016 The Android Open Source Project 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | import errno 17 | import os 18 | import platform 19 | import select 20 | import shutil 21 | import stat 22 | 23 | from Queue import Queue 24 | from threading import Thread 25 | 26 | 27 | def isWindows(): 28 | """ Returns True when running with the native port of Python for Windows, 29 | False when running on any other platform (including the Cygwin port of 30 | Python). 31 | """ 32 | # Note: The cygwin port of Python returns "CYGWIN_NT_xxx" 33 | return platform.system() == "Windows" 34 | 35 | 36 | class FileDescriptorStreams(object): 37 | """ Platform agnostic abstraction enabling non-blocking I/O over a 38 | collection of file descriptors. This abstraction is required because 39 | fctnl(os.O_NONBLOCK) is not supported on Windows. 40 | """ 41 | @classmethod 42 | def create(cls): 43 | """ Factory method: instantiates the concrete class according to the 44 | current platform. 45 | """ 46 | if isWindows(): 47 | return _FileDescriptorStreamsThreads() 48 | else: 49 | return _FileDescriptorStreamsNonBlocking() 50 | 51 | def __init__(self): 52 | self.streams = [] 53 | 54 | def add(self, fd, dest, std_name): 55 | """ Wraps an existing file descriptor as a stream. 56 | """ 57 | self.streams.append(self._create_stream(fd, dest, std_name)) 58 | 59 | def remove(self, stream): 60 | """ Removes a stream, when done with it. 61 | """ 62 | self.streams.remove(stream) 63 | 64 | @property 65 | def is_done(self): 66 | """ Returns True when all streams have been processed. 67 | """ 68 | return len(self.streams) == 0 69 | 70 | def select(self): 71 | """ Returns the set of streams that have data available to read. 72 | The returned streams each expose a read() and a close() method. 73 | When done with a stream, call the remove(stream) method. 74 | """ 75 | raise NotImplementedError 76 | 77 | def _create_stream(fd, dest, std_name): 78 | """ Creates a new stream wrapping an existing file descriptor. 79 | """ 80 | raise NotImplementedError 81 | 82 | 83 | class _FileDescriptorStreamsNonBlocking(FileDescriptorStreams): 84 | """ Implementation of FileDescriptorStreams for platforms that support 85 | non blocking I/O. 86 | """ 87 | class Stream(object): 88 | """ Encapsulates a file descriptor """ 89 | def __init__(self, fd, dest, std_name): 90 | self.fd = fd 91 | self.dest = dest 92 | self.std_name = std_name 93 | self.set_non_blocking() 94 | 95 | def set_non_blocking(self): 96 | import fcntl 97 | flags = fcntl.fcntl(self.fd, fcntl.F_GETFL) 98 | fcntl.fcntl(self.fd, fcntl.F_SETFL, flags | os.O_NONBLOCK) 99 | 100 | def fileno(self): 101 | return self.fd.fileno() 102 | 103 | def read(self): 104 | return self.fd.read(4096) 105 | 106 | def close(self): 107 | self.fd.close() 108 | 109 | def _create_stream(self, fd, dest, std_name): 110 | return self.Stream(fd, dest, std_name) 111 | 112 | def select(self): 113 | ready_streams, _, _ = select.select(self.streams, [], []) 114 | return ready_streams 115 | 116 | 117 | class _FileDescriptorStreamsThreads(FileDescriptorStreams): 118 | """ Implementation of FileDescriptorStreams for platforms that don't support 119 | non blocking I/O. This implementation requires creating threads issuing 120 | blocking read operations on file descriptors. 121 | """ 122 | def __init__(self): 123 | super(_FileDescriptorStreamsThreads, self).__init__() 124 | # The queue is shared accross all threads so we can simulate the 125 | # behavior of the select() function 126 | self.queue = Queue(10) # Limit incoming data from streams 127 | 128 | def _create_stream(self, fd, dest, std_name): 129 | return self.Stream(fd, dest, std_name, self.queue) 130 | 131 | def select(self): 132 | # Return only one stream at a time, as it is the most straighforward 133 | # thing to do and it is compatible with the select() function. 134 | item = self.queue.get() 135 | stream = item.stream 136 | stream.data = item.data 137 | return [stream] 138 | 139 | class QueueItem(object): 140 | """ Item put in the shared queue """ 141 | def __init__(self, stream, data): 142 | self.stream = stream 143 | self.data = data 144 | 145 | class Stream(object): 146 | """ Encapsulates a file descriptor """ 147 | def __init__(self, fd, dest, std_name, queue): 148 | self.fd = fd 149 | self.dest = dest 150 | self.std_name = std_name 151 | self.queue = queue 152 | self.data = None 153 | self.thread = Thread(target=self.read_to_queue) 154 | self.thread.daemon = True 155 | self.thread.start() 156 | 157 | def close(self): 158 | self.fd.close() 159 | 160 | def read(self): 161 | data = self.data 162 | self.data = None 163 | return data 164 | 165 | def read_to_queue(self): 166 | """ The thread function: reads everything from the file descriptor into 167 | the shared queue and terminates when reaching EOF. 168 | """ 169 | for line in iter(self.fd.readline, b''): 170 | self.queue.put(_FileDescriptorStreamsThreads.QueueItem(self, line)) 171 | self.fd.close() 172 | self.queue.put(_FileDescriptorStreamsThreads.QueueItem(self, None)) 173 | 174 | 175 | def symlink(source, link_name): 176 | """Creates a symbolic link pointing to source named link_name. 177 | Note: On Windows, source must exist on disk, as the implementation needs 178 | to know whether to create a "File" or a "Directory" symbolic link. 179 | """ 180 | if isWindows(): 181 | import platform_utils_win32 182 | source = _validate_winpath(source) 183 | link_name = _validate_winpath(link_name) 184 | target = os.path.join(os.path.dirname(link_name), source) 185 | if os.path.isdir(target): 186 | platform_utils_win32.create_dirsymlink(source, link_name) 187 | else: 188 | platform_utils_win32.create_filesymlink(source, link_name) 189 | else: 190 | return os.symlink(source, link_name) 191 | 192 | 193 | def _validate_winpath(path): 194 | path = os.path.normpath(path) 195 | if _winpath_is_valid(path): 196 | return path 197 | raise ValueError("Path \"%s\" must be a relative path or an absolute " 198 | "path starting with a drive letter".format(path)) 199 | 200 | 201 | def _winpath_is_valid(path): 202 | """Windows only: returns True if path is relative (e.g. ".\\foo") or is 203 | absolute including a drive letter (e.g. "c:\\foo"). Returns False if path 204 | is ambiguous (e.g. "x:foo" or "\\foo"). 205 | """ 206 | assert isWindows() 207 | path = os.path.normpath(path) 208 | drive, tail = os.path.splitdrive(path) 209 | if tail: 210 | if not drive: 211 | return tail[0] != os.sep # "\\foo" is invalid 212 | else: 213 | return tail[0] == os.sep # "x:foo" is invalid 214 | else: 215 | return not drive # "x:" is invalid 216 | 217 | 218 | def rmtree(path): 219 | if isWindows(): 220 | shutil.rmtree(path, onerror=handle_rmtree_error) 221 | else: 222 | shutil.rmtree(path) 223 | 224 | 225 | def handle_rmtree_error(function, path, excinfo): 226 | # Allow deleting read-only files 227 | os.chmod(path, stat.S_IWRITE) 228 | function(path) 229 | 230 | 231 | def rename(src, dst): 232 | if isWindows(): 233 | # On Windows, rename fails if destination exists, see 234 | # https://docs.python.org/2/library/os.html#os.rename 235 | try: 236 | os.rename(src, dst) 237 | except OSError as e: 238 | if e.errno == errno.EEXIST: 239 | os.remove(dst) 240 | os.rename(src, dst) 241 | else: 242 | raise 243 | else: 244 | os.rename(src, dst) 245 | 246 | 247 | def remove(path): 248 | """Remove (delete) the file path. This is a replacement for os.remove, but 249 | allows deleting read-only files on Windows. 250 | """ 251 | if isWindows(): 252 | try: 253 | os.remove(path) 254 | except OSError as e: 255 | if e.errno == errno.EACCES: 256 | os.chmod(path, stat.S_IWRITE) 257 | os.remove(path) 258 | else: 259 | raise 260 | else: 261 | os.remove(path) 262 | 263 | 264 | def islink(path): 265 | """Test whether a path is a symbolic link. 266 | 267 | Availability: Windows, Unix. 268 | """ 269 | if isWindows(): 270 | import platform_utils_win32 271 | return platform_utils_win32.islink(path) 272 | else: 273 | return os.path.islink(path) 274 | 275 | 276 | def readlink(path): 277 | """Return a string representing the path to which the symbolic link 278 | points. The result may be either an absolute or relative pathname; 279 | if it is relative, it may be converted to an absolute pathname using 280 | os.path.join(os.path.dirname(path), result). 281 | 282 | Availability: Windows, Unix. 283 | """ 284 | if isWindows(): 285 | import platform_utils_win32 286 | return platform_utils_win32.readlink(path) 287 | else: 288 | return os.readlink(path) 289 | 290 | 291 | def realpath(path): 292 | """Return the canonical path of the specified filename, eliminating 293 | any symbolic links encountered in the path. 294 | 295 | Availability: Windows, Unix. 296 | """ 297 | if isWindows(): 298 | current_path = os.path.abspath(path) 299 | path_tail = [] 300 | for c in range(0, 100): # Avoid cycles 301 | if islink(current_path): 302 | target = readlink(current_path) 303 | current_path = os.path.join(os.path.dirname(current_path), target) 304 | else: 305 | basename = os.path.basename(current_path) 306 | if basename == '': 307 | path_tail.append(current_path) 308 | break 309 | path_tail.append(basename) 310 | current_path = os.path.dirname(current_path) 311 | path_tail.reverse() 312 | result = os.path.normpath(os.path.join(*path_tail)) 313 | return result 314 | else: 315 | return os.path.realpath(path) 316 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /subcmds/forall.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2008 The Android Open Source Project 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | from __future__ import print_function 17 | import errno 18 | import multiprocessing 19 | import re 20 | import os 21 | import signal 22 | import sys 23 | import subprocess 24 | 25 | from color import Coloring 26 | from command import Command, MirrorSafeCommand 27 | import platform_utils 28 | 29 | _CAN_COLOR = [ 30 | 'branch', 31 | 'diff', 32 | 'grep', 33 | 'log', 34 | ] 35 | 36 | 37 | class ForallColoring(Coloring): 38 | def __init__(self, config): 39 | Coloring.__init__(self, config, 'forall') 40 | self.project = self.printer('project', attr='bold') 41 | 42 | 43 | class Forall(Command, MirrorSafeCommand): 44 | common = False 45 | helpSummary = "Run a shell command in each project" 46 | helpUsage = """ 47 | %prog [...] -c [...] 48 | %prog -r str1 [str2] ... -c [...]" 49 | """ 50 | helpDescription = """ 51 | Executes the same shell command in each project. 52 | 53 | The -r option allows running the command only on projects matching 54 | regex or wildcard expression. 55 | 56 | Output Formatting 57 | ----------------- 58 | 59 | The -p option causes '%prog' to bind pipes to the command's stdin, 60 | stdout and stderr streams, and pipe all output into a continuous 61 | stream that is displayed in a single pager session. Project headings 62 | are inserted before the output of each command is displayed. If the 63 | command produces no output in a project, no heading is displayed. 64 | 65 | The formatting convention used by -p is very suitable for some 66 | types of searching, e.g. `repo forall -p -c git log -SFoo` will 67 | print all commits that add or remove references to Foo. 68 | 69 | The -v option causes '%prog' to display stderr messages if a 70 | command produces output only on stderr. Normally the -p option 71 | causes command output to be suppressed until the command produces 72 | at least one byte of output on stdout. 73 | 74 | Environment 75 | ----------- 76 | 77 | pwd is the project's working directory. If the current client is 78 | a mirror client, then pwd is the Git repository. 79 | 80 | REPO_PROJECT is set to the unique name of the project. 81 | 82 | REPO_PATH is the path relative the the root of the client. 83 | 84 | REPO_REMOTE is the name of the remote system from the manifest. 85 | 86 | REPO_LREV is the name of the revision from the manifest, translated 87 | to a local tracking branch. If you need to pass the manifest 88 | revision to a locally executed git command, use REPO_LREV. 89 | 90 | REPO_RREV is the name of the revision from the manifest, exactly 91 | as written in the manifest. 92 | 93 | REPO_COUNT is the total number of projects being iterated. 94 | 95 | REPO_I is the current (1-based) iteration count. Can be used in 96 | conjunction with REPO_COUNT to add a simple progress indicator to your 97 | command. 98 | 99 | REPO__* are any extra environment variables, specified by the 100 | "annotation" element under any project element. This can be useful 101 | for differentiating trees based on user-specific criteria, or simply 102 | annotating tree details. 103 | 104 | shell positional arguments ($1, $2, .., $#) are set to any arguments 105 | following . 106 | 107 | Unless -p is used, stdin, stdout, stderr are inherited from the 108 | terminal and are not redirected. 109 | 110 | If -e is used, when a command exits unsuccessfully, '%prog' will abort 111 | without iterating through the remaining projects. 112 | """ 113 | 114 | def _Options(self, p): 115 | def cmd(option, opt_str, value, parser): 116 | setattr(parser.values, option.dest, list(parser.rargs)) 117 | while parser.rargs: 118 | del parser.rargs[0] 119 | p.add_option('-r', '--regex', 120 | dest='regex', action='store_true', 121 | help="Execute the command only on projects matching regex or wildcard expression") 122 | p.add_option('-i', '--inverse-regex', 123 | dest='inverse_regex', action='store_true', 124 | help="Execute the command only on projects not matching regex or wildcard expression") 125 | p.add_option('-g', '--groups', 126 | dest='groups', 127 | help="Execute the command only on projects matching the specified groups") 128 | p.add_option('-c', '--command', 129 | help='Command (and arguments) to execute', 130 | dest='command', 131 | action='callback', 132 | callback=cmd) 133 | p.add_option('-e', '--abort-on-errors', 134 | dest='abort_on_errors', action='store_true', 135 | help='Abort if a command exits unsuccessfully') 136 | 137 | g = p.add_option_group('Output') 138 | g.add_option('-p', 139 | dest='project_header', action='store_true', 140 | help='Show project headers before output') 141 | g.add_option('-v', '--verbose', 142 | dest='verbose', action='store_true', 143 | help='Show command error messages') 144 | g.add_option('-j', '--jobs', 145 | dest='jobs', action='store', type='int', default=1, 146 | help='number of commands to execute simultaneously') 147 | 148 | def WantPager(self, opt): 149 | return opt.project_header and opt.jobs == 1 150 | 151 | def _SerializeProject(self, project): 152 | """ Serialize a project._GitGetByExec instance. 153 | 154 | project._GitGetByExec is not pickle-able. Instead of trying to pass it 155 | around between processes, make a dict ourselves containing only the 156 | attributes that we need. 157 | 158 | """ 159 | if not self.manifest.IsMirror: 160 | lrev = project.GetRevisionId() 161 | else: 162 | lrev = None 163 | return { 164 | 'name': project.name, 165 | 'relpath': project.relpath, 166 | 'remote_name': project.remote.name, 167 | 'lrev': lrev, 168 | 'rrev': project.revisionExpr, 169 | 'annotations': dict((a.name, a.value) for a in project.annotations), 170 | 'gitdir': project.gitdir, 171 | 'worktree': project.worktree, 172 | } 173 | 174 | def Execute(self, opt, args): 175 | if not opt.command: 176 | self.Usage() 177 | 178 | cmd = [opt.command[0]] 179 | 180 | shell = True 181 | if re.compile(r'^[a-z0-9A-Z_/\.-]+$').match(cmd[0]): 182 | shell = False 183 | 184 | if shell: 185 | cmd.append(cmd[0]) 186 | cmd.extend(opt.command[1:]) 187 | 188 | if opt.project_header \ 189 | and not shell \ 190 | and cmd[0] == 'git': 191 | # If this is a direct git command that can enable colorized 192 | # output and the user prefers coloring, add --color into the 193 | # command line because we are going to wrap the command into 194 | # a pipe and git won't know coloring should activate. 195 | # 196 | for cn in cmd[1:]: 197 | if not cn.startswith('-'): 198 | break 199 | else: 200 | cn = None 201 | # pylint: disable=W0631 202 | if cn and cn in _CAN_COLOR: 203 | class ColorCmd(Coloring): 204 | def __init__(self, config, cmd): 205 | Coloring.__init__(self, config, cmd) 206 | if ColorCmd(self.manifest.manifestProject.config, cn).is_on: 207 | cmd.insert(cmd.index(cn) + 1, '--color') 208 | # pylint: enable=W0631 209 | 210 | mirror = self.manifest.IsMirror 211 | rc = 0 212 | 213 | smart_sync_manifest_name = "smart_sync_override.xml" 214 | smart_sync_manifest_path = os.path.join( 215 | self.manifest.manifestProject.worktree, smart_sync_manifest_name) 216 | 217 | if os.path.isfile(smart_sync_manifest_path): 218 | self.manifest.Override(smart_sync_manifest_path) 219 | 220 | if opt.regex: 221 | projects = self.FindProjects(args) 222 | elif opt.inverse_regex: 223 | projects = self.FindProjects(args, inverse=True) 224 | else: 225 | projects = self.GetProjects(args, groups=opt.groups) 226 | 227 | os.environ['REPO_COUNT'] = str(len(projects)) 228 | 229 | pool = multiprocessing.Pool(opt.jobs, InitWorker) 230 | try: 231 | config = self.manifest.manifestProject.config 232 | results_it = pool.imap( 233 | DoWorkWrapper, 234 | self.ProjectArgs(projects, mirror, opt, cmd, shell, config)) 235 | pool.close() 236 | for r in results_it: 237 | rc = rc or r 238 | if r != 0 and opt.abort_on_errors: 239 | raise Exception('Aborting due to previous error') 240 | except (KeyboardInterrupt, WorkerKeyboardInterrupt): 241 | # Catch KeyboardInterrupt raised inside and outside of workers 242 | print('Interrupted - terminating the pool') 243 | pool.terminate() 244 | rc = rc or errno.EINTR 245 | except Exception as e: 246 | # Catch any other exceptions raised 247 | print('Got an error, terminating the pool: %s: %s' % 248 | (type(e).__name__, e), 249 | file=sys.stderr) 250 | pool.terminate() 251 | rc = rc or getattr(e, 'errno', 1) 252 | finally: 253 | pool.join() 254 | if rc != 0: 255 | sys.exit(rc) 256 | 257 | def ProjectArgs(self, projects, mirror, opt, cmd, shell, config): 258 | for cnt, p in enumerate(projects): 259 | try: 260 | project = self._SerializeProject(p) 261 | except Exception as e: 262 | print('Project list error on project %s: %s: %s' % 263 | (p.name, type(e).__name__, e), 264 | file=sys.stderr) 265 | return 266 | except KeyboardInterrupt: 267 | print('Project list interrupted', 268 | file=sys.stderr) 269 | return 270 | yield [mirror, opt, cmd, shell, cnt, config, project] 271 | 272 | class WorkerKeyboardInterrupt(Exception): 273 | """ Keyboard interrupt exception for worker processes. """ 274 | pass 275 | 276 | 277 | def InitWorker(): 278 | signal.signal(signal.SIGINT, signal.SIG_IGN) 279 | 280 | def DoWorkWrapper(args): 281 | """ A wrapper around the DoWork() method. 282 | 283 | Catch the KeyboardInterrupt exceptions here and re-raise them as a different, 284 | ``Exception``-based exception to stop it flooding the console with stacktraces 285 | and making the parent hang indefinitely. 286 | 287 | """ 288 | project = args.pop() 289 | try: 290 | return DoWork(project, *args) 291 | except KeyboardInterrupt: 292 | print('%s: Worker interrupted' % project['name']) 293 | raise WorkerKeyboardInterrupt() 294 | 295 | 296 | def DoWork(project, mirror, opt, cmd, shell, cnt, config): 297 | env = os.environ.copy() 298 | def setenv(name, val): 299 | if val is None: 300 | val = '' 301 | if hasattr(val, 'encode'): 302 | val = val.encode() 303 | env[name] = val 304 | 305 | setenv('REPO_PROJECT', project['name']) 306 | setenv('REPO_PATH', project['relpath']) 307 | setenv('REPO_REMOTE', project['remote_name']) 308 | setenv('REPO_LREV', project['lrev']) 309 | setenv('REPO_RREV', project['rrev']) 310 | setenv('REPO_I', str(cnt + 1)) 311 | for name in project['annotations']: 312 | setenv("REPO__%s" % (name), project['annotations'][name]) 313 | 314 | if mirror: 315 | setenv('GIT_DIR', project['gitdir']) 316 | cwd = project['gitdir'] 317 | else: 318 | cwd = project['worktree'] 319 | 320 | if not os.path.exists(cwd): 321 | if (opt.project_header and opt.verbose) \ 322 | or not opt.project_header: 323 | print('skipping %s/' % project['relpath'], file=sys.stderr) 324 | return 325 | 326 | if opt.project_header: 327 | stdin = subprocess.PIPE 328 | stdout = subprocess.PIPE 329 | stderr = subprocess.PIPE 330 | else: 331 | stdin = None 332 | stdout = None 333 | stderr = None 334 | 335 | p = subprocess.Popen(cmd, 336 | cwd=cwd, 337 | shell=shell, 338 | env=env, 339 | stdin=stdin, 340 | stdout=stdout, 341 | stderr=stderr) 342 | 343 | if opt.project_header: 344 | out = ForallColoring(config) 345 | out.redirect(sys.stdout) 346 | empty = True 347 | errbuf = '' 348 | 349 | p.stdin.close() 350 | s_in = platform_utils.FileDescriptorStreams.create() 351 | s_in.add(p.stdout, sys.stdout, 'stdout') 352 | s_in.add(p.stderr, sys.stderr, 'stderr') 353 | 354 | while not s_in.is_done: 355 | in_ready = s_in.select() 356 | for s in in_ready: 357 | buf = s.read() 358 | if not buf: 359 | s.close() 360 | s_in.remove(s) 361 | continue 362 | 363 | if not opt.verbose: 364 | if s.std_name == 'stderr': 365 | errbuf += buf 366 | continue 367 | 368 | if empty and out: 369 | if not cnt == 0: 370 | out.nl() 371 | 372 | if mirror: 373 | project_header_path = project['name'] 374 | else: 375 | project_header_path = project['relpath'] 376 | out.project('project %s/', project_header_path) 377 | out.nl() 378 | out.flush() 379 | if errbuf: 380 | sys.stderr.write(errbuf) 381 | sys.stderr.flush() 382 | errbuf = '' 383 | empty = False 384 | 385 | s.dest.write(buf) 386 | s.dest.flush() 387 | 388 | r = p.wait() 389 | return r 390 | --------------------------------------------------------------------------------