summaryrefslogtreecommitdiffstats
path: root/git_command.py
diff options
context:
space:
mode:
Diffstat (limited to 'git_command.py')
-rw-r--r--git_command.py561
1 files changed, 296 insertions, 265 deletions
diff --git a/git_command.py b/git_command.py
index d4d4bed4..c7245ade 100644
--- a/git_command.py
+++ b/git_command.py
@@ -24,7 +24,7 @@ import platform_utils
24from repo_trace import REPO_TRACE, IsTrace, Trace 24from repo_trace import REPO_TRACE, IsTrace, Trace
25from wrapper import Wrapper 25from wrapper import Wrapper
26 26
27GIT = 'git' 27GIT = "git"
28# NB: These do not need to be kept in sync with the repo launcher script. 28# NB: These do not need to be kept in sync with the repo launcher script.
29# These may be much newer as it allows the repo launcher to roll between 29# These may be much newer as it allows the repo launcher to roll between
30# different repo releases while source versions might require a newer git. 30# different repo releases while source versions might require a newer git.
@@ -36,126 +36,138 @@ GIT = 'git'
36# git-1.7 is in (EOL) Ubuntu Precise. git-1.9 is in Ubuntu Trusty. 36# git-1.7 is in (EOL) Ubuntu Precise. git-1.9 is in Ubuntu Trusty.
37MIN_GIT_VERSION_SOFT = (1, 9, 1) 37MIN_GIT_VERSION_SOFT = (1, 9, 1)
38MIN_GIT_VERSION_HARD = (1, 7, 2) 38MIN_GIT_VERSION_HARD = (1, 7, 2)
39GIT_DIR = 'GIT_DIR' 39GIT_DIR = "GIT_DIR"
40 40
41LAST_GITDIR = None 41LAST_GITDIR = None
42LAST_CWD = None 42LAST_CWD = None
43 43
44 44
45class _GitCall(object): 45class _GitCall(object):
46 @functools.lru_cache(maxsize=None) 46 @functools.lru_cache(maxsize=None)
47 def version_tuple(self): 47 def version_tuple(self):
48 ret = Wrapper().ParseGitVersion() 48 ret = Wrapper().ParseGitVersion()
49 if ret is None: 49 if ret is None:
50 print('fatal: unable to detect git version', file=sys.stderr) 50 print("fatal: unable to detect git version", file=sys.stderr)
51 sys.exit(1) 51 sys.exit(1)
52 return ret 52 return ret
53 53
54 def __getattr__(self, name): 54 def __getattr__(self, name):
55 name = name.replace('_', '-') 55 name = name.replace("_", "-")
56 56
57 def fun(*cmdv): 57 def fun(*cmdv):
58 command = [name] 58 command = [name]
59 command.extend(cmdv) 59 command.extend(cmdv)
60 return GitCommand(None, command).Wait() == 0 60 return GitCommand(None, command).Wait() == 0
61 return fun 61
62 return fun
62 63
63 64
64git = _GitCall() 65git = _GitCall()
65 66
66 67
67def RepoSourceVersion(): 68def RepoSourceVersion():
68 """Return the version of the repo.git tree.""" 69 """Return the version of the repo.git tree."""
69 ver = getattr(RepoSourceVersion, 'version', None) 70 ver = getattr(RepoSourceVersion, "version", None)
70 71
71 # We avoid GitCommand so we don't run into circular deps -- GitCommand needs 72 # We avoid GitCommand so we don't run into circular deps -- GitCommand needs
72 # to initialize version info we provide. 73 # to initialize version info we provide.
73 if ver is None: 74 if ver is None:
74 env = GitCommand._GetBasicEnv() 75 env = GitCommand._GetBasicEnv()
76
77 proj = os.path.dirname(os.path.abspath(__file__))
78 env[GIT_DIR] = os.path.join(proj, ".git")
79 result = subprocess.run(
80 [GIT, "describe", HEAD],
81 stdout=subprocess.PIPE,
82 stderr=subprocess.DEVNULL,
83 encoding="utf-8",
84 env=env,
85 check=False,
86 )
87 if result.returncode == 0:
88 ver = result.stdout.strip()
89 if ver.startswith("v"):
90 ver = ver[1:]
91 else:
92 ver = "unknown"
93 setattr(RepoSourceVersion, "version", ver)
94
95 return ver
75 96
76 proj = os.path.dirname(os.path.abspath(__file__))
77 env[GIT_DIR] = os.path.join(proj, '.git')
78 result = subprocess.run([GIT, 'describe', HEAD], stdout=subprocess.PIPE,
79 stderr=subprocess.DEVNULL, encoding='utf-8',
80 env=env, check=False)
81 if result.returncode == 0:
82 ver = result.stdout.strip()
83 if ver.startswith('v'):
84 ver = ver[1:]
85 else:
86 ver = 'unknown'
87 setattr(RepoSourceVersion, 'version', ver)
88 97
89 return ver 98class UserAgent(object):
99 """Mange User-Agent settings when talking to external services
90 100
101 We follow the style as documented here:
102 https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent
103 """
91 104
92class UserAgent(object): 105 _os = None
93 """Mange User-Agent settings when talking to external services 106 _repo_ua = None
94 107 _git_ua = None
95 We follow the style as documented here: 108
96 https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent 109 @property
97 """ 110 def os(self):
98 111 """The operating system name."""
99 _os = None 112 if self._os is None:
100 _repo_ua = None 113 os_name = sys.platform
101 _git_ua = None 114 if os_name.lower().startswith("linux"):
102 115 os_name = "Linux"
103 @property 116 elif os_name == "win32":
104 def os(self): 117 os_name = "Win32"
105 """The operating system name.""" 118 elif os_name == "cygwin":
106 if self._os is None: 119 os_name = "Cygwin"
107 os_name = sys.platform 120 elif os_name == "darwin":
108 if os_name.lower().startswith('linux'): 121 os_name = "Darwin"
109 os_name = 'Linux' 122 self._os = os_name
110 elif os_name == 'win32': 123
111 os_name = 'Win32' 124 return self._os
112 elif os_name == 'cygwin': 125
113 os_name = 'Cygwin' 126 @property
114 elif os_name == 'darwin': 127 def repo(self):
115 os_name = 'Darwin' 128 """The UA when connecting directly from repo."""
116 self._os = os_name 129 if self._repo_ua is None:
117 130 py_version = sys.version_info
118 return self._os 131 self._repo_ua = "git-repo/%s (%s) git/%s Python/%d.%d.%d" % (
119 132 RepoSourceVersion(),
120 @property 133 self.os,
121 def repo(self): 134 git.version_tuple().full,
122 """The UA when connecting directly from repo.""" 135 py_version.major,
123 if self._repo_ua is None: 136 py_version.minor,
124 py_version = sys.version_info 137 py_version.micro,
125 self._repo_ua = 'git-repo/%s (%s) git/%s Python/%d.%d.%d' % ( 138 )
126 RepoSourceVersion(), 139
127 self.os, 140 return self._repo_ua
128 git.version_tuple().full, 141
129 py_version.major, py_version.minor, py_version.micro) 142 @property
130 143 def git(self):
131 return self._repo_ua 144 """The UA when running git."""
132 145 if self._git_ua is None:
133 @property 146 self._git_ua = "git/%s (%s) git-repo/%s" % (
134 def git(self): 147 git.version_tuple().full,
135 """The UA when running git.""" 148 self.os,
136 if self._git_ua is None: 149 RepoSourceVersion(),
137 self._git_ua = 'git/%s (%s) git-repo/%s' % ( 150 )
138 git.version_tuple().full, 151
139 self.os, 152 return self._git_ua
140 RepoSourceVersion())
141
142 return self._git_ua
143 153
144 154
145user_agent = UserAgent() 155user_agent = UserAgent()
146 156
147 157
148def git_require(min_version, fail=False, msg=''): 158def git_require(min_version, fail=False, msg=""):
149 git_version = git.version_tuple() 159 git_version = git.version_tuple()
150 if min_version <= git_version: 160 if min_version <= git_version:
151 return True 161 return True
152 if fail: 162 if fail:
153 need = '.'.join(map(str, min_version)) 163 need = ".".join(map(str, min_version))
154 if msg: 164 if msg:
155 msg = ' for ' + msg 165 msg = " for " + msg
156 print('fatal: git %s or later required%s' % (need, msg), file=sys.stderr) 166 print(
157 sys.exit(1) 167 "fatal: git %s or later required%s" % (need, msg), file=sys.stderr
158 return False 168 )
169 sys.exit(1)
170 return False
159 171
160 172
161def _build_env( 173def _build_env(
@@ -164,175 +176,194 @@ def _build_env(
164 disable_editor: Optional[bool] = False, 176 disable_editor: Optional[bool] = False,
165 ssh_proxy: Optional[Any] = None, 177 ssh_proxy: Optional[Any] = None,
166 gitdir: Optional[str] = None, 178 gitdir: Optional[str] = None,
167 objdir: Optional[str] = None 179 objdir: Optional[str] = None,
168): 180):
169 """Constucts an env dict for command execution.""" 181 """Constucts an env dict for command execution."""
170
171 assert _kwargs_only == (), '_build_env only accepts keyword arguments.'
172
173 env = GitCommand._GetBasicEnv()
174
175 if disable_editor:
176 env['GIT_EDITOR'] = ':'
177 if ssh_proxy:
178 env['REPO_SSH_SOCK'] = ssh_proxy.sock()
179 env['GIT_SSH'] = ssh_proxy.proxy
180 env['GIT_SSH_VARIANT'] = 'ssh'
181 if 'http_proxy' in env and 'darwin' == sys.platform:
182 s = "'http.proxy=%s'" % (env['http_proxy'],)
183 p = env.get('GIT_CONFIG_PARAMETERS')
184 if p is not None:
185 s = p + ' ' + s
186 env['GIT_CONFIG_PARAMETERS'] = s
187 if 'GIT_ALLOW_PROTOCOL' not in env:
188 env['GIT_ALLOW_PROTOCOL'] = (
189 'file:git:http:https:ssh:persistent-http:persistent-https:sso:rpc')
190 env['GIT_HTTP_USER_AGENT'] = user_agent.git
191
192 if objdir:
193 # Set to the place we want to save the objects.
194 env['GIT_OBJECT_DIRECTORY'] = objdir
195
196 alt_objects = os.path.join(gitdir, 'objects') if gitdir else None
197 if alt_objects and os.path.realpath(alt_objects) != os.path.realpath(objdir):
198 # Allow git to search the original place in case of local or unique refs
199 # that git will attempt to resolve even if we aren't fetching them.
200 env['GIT_ALTERNATE_OBJECT_DIRECTORIES'] = alt_objects
201 if bare and gitdir is not None:
202 env[GIT_DIR] = gitdir
203
204 return env
205 182
183 assert _kwargs_only == (), "_build_env only accepts keyword arguments."
184
185 env = GitCommand._GetBasicEnv()
186
187 if disable_editor:
188 env["GIT_EDITOR"] = ":"
189 if ssh_proxy:
190 env["REPO_SSH_SOCK"] = ssh_proxy.sock()
191 env["GIT_SSH"] = ssh_proxy.proxy
192 env["GIT_SSH_VARIANT"] = "ssh"
193 if "http_proxy" in env and "darwin" == sys.platform:
194 s = "'http.proxy=%s'" % (env["http_proxy"],)
195 p = env.get("GIT_CONFIG_PARAMETERS")
196 if p is not None:
197 s = p + " " + s
198 env["GIT_CONFIG_PARAMETERS"] = s
199 if "GIT_ALLOW_PROTOCOL" not in env:
200 env[
201 "GIT_ALLOW_PROTOCOL"
202 ] = "file:git:http:https:ssh:persistent-http:persistent-https:sso:rpc"
203 env["GIT_HTTP_USER_AGENT"] = user_agent.git
204
205 if objdir:
206 # Set to the place we want to save the objects.
207 env["GIT_OBJECT_DIRECTORY"] = objdir
208
209 alt_objects = os.path.join(gitdir, "objects") if gitdir else None
210 if alt_objects and os.path.realpath(alt_objects) != os.path.realpath(
211 objdir
212 ):
213 # Allow git to search the original place in case of local or unique
214 # refs that git will attempt to resolve even if we aren't fetching
215 # them.
216 env["GIT_ALTERNATE_OBJECT_DIRECTORIES"] = alt_objects
217 if bare and gitdir is not None:
218 env[GIT_DIR] = gitdir
206 219
207class GitCommand(object):
208 """Wrapper around a single git invocation."""
209
210 def __init__(self,
211 project,
212 cmdv,
213 bare=False,
214 input=None,
215 capture_stdout=False,
216 capture_stderr=False,
217 merge_output=False,
218 disable_editor=False,
219 ssh_proxy=None,
220 cwd=None,
221 gitdir=None,
222 objdir=None):
223
224 if project:
225 if not cwd:
226 cwd = project.worktree
227 if not gitdir:
228 gitdir = project.gitdir
229
230 # Git on Windows wants its paths only using / for reliability.
231 if platform_utils.isWindows():
232 if objdir:
233 objdir = objdir.replace('\\', '/')
234 if gitdir:
235 gitdir = gitdir.replace('\\', '/')
236
237 env = _build_env(
238 disable_editor=disable_editor,
239 ssh_proxy=ssh_proxy,
240 objdir=objdir,
241 gitdir=gitdir,
242 bare=bare,
243 )
244
245 command = [GIT]
246 if bare:
247 cwd = None
248 command.append(cmdv[0])
249 # Need to use the --progress flag for fetch/clone so output will be
250 # displayed as by default git only does progress output if stderr is a TTY.
251 if sys.stderr.isatty() and cmdv[0] in ('fetch', 'clone'):
252 if '--progress' not in cmdv and '--quiet' not in cmdv:
253 command.append('--progress')
254 command.extend(cmdv[1:])
255
256 stdin = subprocess.PIPE if input else None
257 stdout = subprocess.PIPE if capture_stdout else None
258 stderr = (subprocess.STDOUT if merge_output else
259 (subprocess.PIPE if capture_stderr else None))
260
261 dbg = ''
262 if IsTrace():
263 global LAST_CWD
264 global LAST_GITDIR
265
266 if cwd and LAST_CWD != cwd:
267 if LAST_GITDIR or LAST_CWD:
268 dbg += '\n'
269 dbg += ': cd %s\n' % cwd
270 LAST_CWD = cwd
271
272 if GIT_DIR in env and LAST_GITDIR != env[GIT_DIR]:
273 if LAST_GITDIR or LAST_CWD:
274 dbg += '\n'
275 dbg += ': export GIT_DIR=%s\n' % env[GIT_DIR]
276 LAST_GITDIR = env[GIT_DIR]
277
278 if 'GIT_OBJECT_DIRECTORY' in env:
279 dbg += ': export GIT_OBJECT_DIRECTORY=%s\n' % env['GIT_OBJECT_DIRECTORY']
280 if 'GIT_ALTERNATE_OBJECT_DIRECTORIES' in env:
281 dbg += ': export GIT_ALTERNATE_OBJECT_DIRECTORIES=%s\n' % (
282 env['GIT_ALTERNATE_OBJECT_DIRECTORIES'])
283
284 dbg += ': '
285 dbg += ' '.join(command)
286 if stdin == subprocess.PIPE:
287 dbg += ' 0<|'
288 if stdout == subprocess.PIPE:
289 dbg += ' 1>|'
290 if stderr == subprocess.PIPE:
291 dbg += ' 2>|'
292 elif stderr == subprocess.STDOUT:
293 dbg += ' 2>&1'
294
295 with Trace('git command %s %s with debug: %s', LAST_GITDIR, command, dbg):
296 try:
297 p = subprocess.Popen(command,
298 cwd=cwd,
299 env=env,
300 encoding='utf-8',
301 errors='backslashreplace',
302 stdin=stdin,
303 stdout=stdout,
304 stderr=stderr)
305 except Exception as e:
306 raise GitError('%s: %s' % (command[1], e))
307
308 if ssh_proxy:
309 ssh_proxy.add_client(p)
310
311 self.process = p
312
313 try:
314 self.stdout, self.stderr = p.communicate(input=input)
315 finally:
316 if ssh_proxy:
317 ssh_proxy.remove_client(p)
318 self.rc = p.wait()
319
320 @staticmethod
321 def _GetBasicEnv():
322 """Return a basic env for running git under.
323
324 This is guaranteed to be side-effect free.
325 """
326 env = os.environ.copy()
327 for key in (REPO_TRACE,
328 GIT_DIR,
329 'GIT_ALTERNATE_OBJECT_DIRECTORIES',
330 'GIT_OBJECT_DIRECTORY',
331 'GIT_WORK_TREE',
332 'GIT_GRAFT_FILE',
333 'GIT_INDEX_FILE'):
334 env.pop(key, None)
335 return env 220 return env
336 221
337 def Wait(self): 222
338 return self.rc 223class GitCommand(object):
224 """Wrapper around a single git invocation."""
225
226 def __init__(
227 self,
228 project,
229 cmdv,
230 bare=False,
231 input=None,
232 capture_stdout=False,
233 capture_stderr=False,
234 merge_output=False,
235 disable_editor=False,
236 ssh_proxy=None,
237 cwd=None,
238 gitdir=None,
239 objdir=None,
240 ):
241 if project:
242 if not cwd:
243 cwd = project.worktree
244 if not gitdir:
245 gitdir = project.gitdir
246
247 # Git on Windows wants its paths only using / for reliability.
248 if platform_utils.isWindows():
249 if objdir:
250 objdir = objdir.replace("\\", "/")
251 if gitdir:
252 gitdir = gitdir.replace("\\", "/")
253
254 env = _build_env(
255 disable_editor=disable_editor,
256 ssh_proxy=ssh_proxy,
257 objdir=objdir,
258 gitdir=gitdir,
259 bare=bare,
260 )
261
262 command = [GIT]
263 if bare:
264 cwd = None
265 command.append(cmdv[0])
266 # Need to use the --progress flag for fetch/clone so output will be
267 # displayed as by default git only does progress output if stderr is a
268 # TTY.
269 if sys.stderr.isatty() and cmdv[0] in ("fetch", "clone"):
270 if "--progress" not in cmdv and "--quiet" not in cmdv:
271 command.append("--progress")
272 command.extend(cmdv[1:])
273
274 stdin = subprocess.PIPE if input else None
275 stdout = subprocess.PIPE if capture_stdout else None
276 stderr = (
277 subprocess.STDOUT
278 if merge_output
279 else (subprocess.PIPE if capture_stderr else None)
280 )
281
282 dbg = ""
283 if IsTrace():
284 global LAST_CWD
285 global LAST_GITDIR
286
287 if cwd and LAST_CWD != cwd:
288 if LAST_GITDIR or LAST_CWD:
289 dbg += "\n"
290 dbg += ": cd %s\n" % cwd
291 LAST_CWD = cwd
292
293 if GIT_DIR in env and LAST_GITDIR != env[GIT_DIR]:
294 if LAST_GITDIR or LAST_CWD:
295 dbg += "\n"
296 dbg += ": export GIT_DIR=%s\n" % env[GIT_DIR]
297 LAST_GITDIR = env[GIT_DIR]
298
299 if "GIT_OBJECT_DIRECTORY" in env:
300 dbg += (
301 ": export GIT_OBJECT_DIRECTORY=%s\n"
302 % env["GIT_OBJECT_DIRECTORY"]
303 )
304 if "GIT_ALTERNATE_OBJECT_DIRECTORIES" in env:
305 dbg += ": export GIT_ALTERNATE_OBJECT_DIRECTORIES=%s\n" % (
306 env["GIT_ALTERNATE_OBJECT_DIRECTORIES"]
307 )
308
309 dbg += ": "
310 dbg += " ".join(command)
311 if stdin == subprocess.PIPE:
312 dbg += " 0<|"
313 if stdout == subprocess.PIPE:
314 dbg += " 1>|"
315 if stderr == subprocess.PIPE:
316 dbg += " 2>|"
317 elif stderr == subprocess.STDOUT:
318 dbg += " 2>&1"
319
320 with Trace(
321 "git command %s %s with debug: %s", LAST_GITDIR, command, dbg
322 ):
323 try:
324 p = subprocess.Popen(
325 command,
326 cwd=cwd,
327 env=env,
328 encoding="utf-8",
329 errors="backslashreplace",
330 stdin=stdin,
331 stdout=stdout,
332 stderr=stderr,
333 )
334 except Exception as e:
335 raise GitError("%s: %s" % (command[1], e))
336
337 if ssh_proxy:
338 ssh_proxy.add_client(p)
339
340 self.process = p
341
342 try:
343 self.stdout, self.stderr = p.communicate(input=input)
344 finally:
345 if ssh_proxy:
346 ssh_proxy.remove_client(p)
347 self.rc = p.wait()
348
349 @staticmethod
350 def _GetBasicEnv():
351 """Return a basic env for running git under.
352
353 This is guaranteed to be side-effect free.
354 """
355 env = os.environ.copy()
356 for key in (
357 REPO_TRACE,
358 GIT_DIR,
359 "GIT_ALTERNATE_OBJECT_DIRECTORIES",
360 "GIT_OBJECT_DIRECTORY",
361 "GIT_WORK_TREE",
362 "GIT_GRAFT_FILE",
363 "GIT_INDEX_FILE",
364 ):
365 env.pop(key, None)
366 return env
367
368 def Wait(self):
369 return self.rc