├── .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 |
--------------------------------------------------------------------------------