OLD | NEW |
1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
2 # Copyright 2015 The Chromium Authors. All rights reserved. | 2 # Copyright 2015 The Chromium Authors. All rights reserved. |
3 # Use of this source code is governed by a BSD-style license that can be | 3 # Use of this source code is governed by a BSD-style license that can be |
4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
5 """git drover: A tool for merging changes to release branches.""" | 5 """git drover: A tool for merging changes to release branches.""" |
6 | 6 |
7 import argparse | 7 import argparse |
8 import functools | 8 import functools |
9 import logging | 9 import logging |
10 import os | 10 import os |
11 import shutil | 11 import shutil |
12 import subprocess | 12 import subprocess |
13 import sys | 13 import sys |
14 import tempfile | 14 import tempfile |
15 | 15 |
16 import git_common | 16 import git_common |
17 | 17 |
18 | 18 |
19 class Error(Exception): | 19 class Error(Exception): |
20 pass | 20 pass |
21 | 21 |
22 | 22 |
| 23 if os.name == 'nt': |
| 24 # This is a just-good-enough emulation of os.symlink for drover to work on |
| 25 # Windows. It uses junctioning of directories (most of the contents of |
| 26 # the .git directory), but copies files. Note that we can't use |
| 27 # CreateSymbolicLink or CreateHardLink here, as they both require elevation. |
| 28 # Creating reparse points is what we want for the directories, but doing so |
| 29 # is a relatively messy set of DeviceIoControl work at the API level, so we |
| 30 # simply shell to `mklink /j` instead. |
| 31 def emulate_symlink_windows(source, link_name): |
| 32 if os.path.isdir(source): |
| 33 subprocess.check_call(['mklink', '/j', |
| 34 link_name.replace('/', '\\'), |
| 35 source.replace('/', '\\')], |
| 36 shell=True) |
| 37 else: |
| 38 shutil.copy(source, link_name) |
| 39 mk_symlink = emulate_symlink_windows |
| 40 else: |
| 41 mk_symlink = os.symlink |
| 42 |
| 43 |
23 class _Drover(object): | 44 class _Drover(object): |
24 | 45 |
25 def __init__(self, branch, revision, parent_repo, dry_run): | 46 def __init__(self, branch, revision, parent_repo, dry_run): |
26 self._branch = branch | 47 self._branch = branch |
27 self._branch_ref = 'refs/remotes/branch-heads/%s' % branch | 48 self._branch_ref = 'refs/remotes/branch-heads/%s' % branch |
28 self._revision = revision | 49 self._revision = revision |
29 self._parent_repo = os.path.abspath(parent_repo) | 50 self._parent_repo = os.path.abspath(parent_repo) |
30 self._dry_run = dry_run | 51 self._dry_run = dry_run |
31 self._workdir = None | 52 self._workdir = None |
32 self._branch_name = None | 53 self._branch_name = None |
(...skipping 32 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
65 def _cleanup(self): | 86 def _cleanup(self): |
66 if self._branch_name: | 87 if self._branch_name: |
67 try: | 88 try: |
68 self._run_git_command(['cherry-pick', '--abort']) | 89 self._run_git_command(['cherry-pick', '--abort']) |
69 except Error: | 90 except Error: |
70 pass | 91 pass |
71 self._run_git_command(['checkout', '--detach']) | 92 self._run_git_command(['checkout', '--detach']) |
72 self._run_git_command(['branch', '-D', self._branch_name]) | 93 self._run_git_command(['branch', '-D', self._branch_name]) |
73 if self._workdir: | 94 if self._workdir: |
74 logging.debug('Deleting %s', self._workdir) | 95 logging.debug('Deleting %s', self._workdir) |
75 shutil.rmtree(self._workdir) | 96 if os.name == 'nt': |
| 97 # Use rmdir to properly handle the junctions we created. |
| 98 subprocess.check_call(['rmdir', '/s', '/q', self._workdir], shell=True) |
| 99 else: |
| 100 shutil.rmtree(self._workdir) |
76 self._dev_null_file.close() | 101 self._dev_null_file.close() |
77 | 102 |
78 @staticmethod | 103 @staticmethod |
79 def _confirm(message): | 104 def _confirm(message): |
80 """Show a confirmation prompt with the given message. | 105 """Show a confirmation prompt with the given message. |
81 | 106 |
82 Returns: | 107 Returns: |
83 A bool representing whether the user wishes to continue. | 108 A bool representing whether the user wishes to continue. |
84 """ | 109 """ |
85 result = '' | 110 result = '' |
86 while result not in ('y', 'n'): | 111 while result not in ('y', 'n'): |
87 try: | 112 try: |
88 result = raw_input('%s Continue (y/n)? ' % message) | 113 result = raw_input('%s Continue (y/n)? ' % message) |
89 except EOFError: | 114 except EOFError: |
90 result = 'n' | 115 result = 'n' |
91 return result == 'y' | 116 return result == 'y' |
92 | 117 |
93 def _check_inputs(self): | 118 def _check_inputs(self): |
94 """Check the input arguments and ensure the parent repo is up to date.""" | 119 """Check the input arguments and ensure the parent repo is up to date.""" |
95 | 120 |
96 if not os.path.isdir(self._parent_repo): | 121 if not os.path.isdir(self._parent_repo): |
97 raise Error('Invalid parent repo path %r' % self._parent_repo) | 122 raise Error('Invalid parent repo path %r' % self._parent_repo) |
98 if not hasattr(os, 'symlink'): | |
99 raise Error('Symlink support is required') | |
100 | 123 |
101 self._run_git_command(['--help'], error_message='Unable to run git') | 124 self._run_git_command(['--help'], error_message='Unable to run git') |
102 self._run_git_command(['status'], | 125 self._run_git_command(['status'], |
103 error_message='%r is not a valid git repo' % | 126 error_message='%r is not a valid git repo' % |
104 os.path.abspath(self._parent_repo)) | 127 os.path.abspath(self._parent_repo)) |
105 self._run_git_command(['fetch', 'origin'], | 128 self._run_git_command(['fetch', 'origin'], |
106 error_message='Failed to fetch origin') | 129 error_message='Failed to fetch origin') |
107 self._run_git_command( | 130 self._run_git_command( |
108 ['rev-parse', '%s^{commit}' % self._branch_ref], | 131 ['rev-parse', '%s^{commit}' % self._branch_ref], |
109 error_message='Branch %s not found' % self._branch_ref) | 132 error_message='Branch %s not found' % self._branch_ref) |
(...skipping 23 matching lines...) Expand all Loading... |
133 differs from git-new-workdir in that the config is forked instead of shared. | 156 differs from git-new-workdir in that the config is forked instead of shared. |
134 This is so the new workdir can be a sparse checkout without affecting | 157 This is so the new workdir can be a sparse checkout without affecting |
135 |self._parent_repo|. | 158 |self._parent_repo|. |
136 """ | 159 """ |
137 parent_git_dir = os.path.abspath(self._run_git_command( | 160 parent_git_dir = os.path.abspath(self._run_git_command( |
138 ['rev-parse', '--git-dir']).strip()) | 161 ['rev-parse', '--git-dir']).strip()) |
139 self._workdir = tempfile.mkdtemp(prefix='drover_%s_' % self._branch) | 162 self._workdir = tempfile.mkdtemp(prefix='drover_%s_' % self._branch) |
140 logging.debug('Creating checkout in %s', self._workdir) | 163 logging.debug('Creating checkout in %s', self._workdir) |
141 git_dir = os.path.join(self._workdir, '.git') | 164 git_dir = os.path.join(self._workdir, '.git') |
142 git_common.make_workdir_common(parent_git_dir, git_dir, self.FILES_TO_LINK, | 165 git_common.make_workdir_common(parent_git_dir, git_dir, self.FILES_TO_LINK, |
143 self.FILES_TO_COPY) | 166 self.FILES_TO_COPY, mk_symlink) |
144 self._run_git_command(['config', 'core.sparsecheckout', 'true']) | 167 self._run_git_command(['config', 'core.sparsecheckout', 'true']) |
145 with open(os.path.join(git_dir, 'info', 'sparse-checkout'), 'w') as f: | 168 with open(os.path.join(git_dir, 'info', 'sparse-checkout'), 'w') as f: |
146 f.write('codereview.settings') | 169 f.write('codereview.settings') |
147 | 170 |
148 branch_name = os.path.split(self._workdir)[-1] | 171 branch_name = os.path.split(self._workdir)[-1] |
149 self._run_git_command(['checkout', '-b', branch_name, self._branch_ref]) | 172 self._run_git_command(['checkout', '-b', branch_name, self._branch_ref]) |
150 self._branch_name = branch_name | 173 self._branch_name = branch_name |
151 | 174 |
152 def _prepare_cherry_pick(self): | 175 def _prepare_cherry_pick(self): |
153 self._run_git_command(['cherry-pick', '-x', self._revision], | 176 self._run_git_command(['cherry-pick', '-x', self._revision], |
(...skipping 91 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
245 try: | 268 try: |
246 cherry_pick_change(options.branch, options.cherry_pick, | 269 cherry_pick_change(options.branch, options.cherry_pick, |
247 options.parent_checkout, options.dry_run) | 270 options.parent_checkout, options.dry_run) |
248 except Error as e: | 271 except Error as e: |
249 logging.error(e.message) | 272 logging.error(e.message) |
250 sys.exit(128) | 273 sys.exit(128) |
251 | 274 |
252 | 275 |
253 if __name__ == '__main__': | 276 if __name__ == '__main__': |
254 main() | 277 main() |
OLD | NEW |