Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 90d6903

Browse files
committedOct 13, 2016
cygwin, #525: Try to make it work with Cygwin's Git.
+ Make `Git.polish_url()` convert paths into Cygwin-friendly paths. + Add utility and soe TCs for funcs for detecting cygwin and converting abs-paths to `/cygdrive/c/...`.
1 parent 57a6e9a commit 90d6903

File tree

5 files changed

+250
-67
lines changed

5 files changed

+250
-67
lines changed
 

‎git/cmd.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
)
3232
from git.exc import CommandError
3333
from git.odict import OrderedDict
34+
from git.util import is_cygwin_git, cygpath
3435

3536
from .exc import (
3637
GitCommandError,
@@ -190,9 +191,24 @@ def __setstate__(self, d):
190191
# Override this value using `Git.USE_SHELL = True`
191192
USE_SHELL = False
192193

194+
@classmethod
195+
def is_cygwin(cls):
196+
return is_cygwin_git(cls.GIT_PYTHON_GIT_EXECUTABLE)
197+
193198
@classmethod
194199
def polish_url(cls, url):
195-
return url.replace("\\\\", "\\").replace("\\", "/")
200+
if cls.is_cygwin():
201+
"""Remove any backslahes from urls to be written in config files.
202+
203+
Windows might create config-files containing paths with backslashed,
204+
but git stops liking them as it will escape the backslashes.
205+
Hence we undo the escaping just to be sure.
206+
"""
207+
url = cygpath(url)
208+
else:
209+
url = url.replace("\\\\", "\\").replace("\\", "/")
210+
211+
return url
196212

197213
class AutoInterrupt(object):
198214
"""Kill/Interrupt the stored process instance once this instance goes out of scope. It is

‎git/repo/base.py

Lines changed: 30 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -4,51 +4,21 @@
44
# This module is part of GitPython and is released under
55
# the BSD License: http://www.opensource.org/licenses/bsd-license.php
66

7-
from git.exc import (
8-
InvalidGitRepositoryError,
9-
NoSuchPathError,
10-
GitCommandError
11-
)
12-
from git.cmd import (
13-
Git,
14-
handle_process_output
15-
)
16-
from git.refs import (
17-
HEAD,
18-
Head,
19-
Reference,
20-
TagReference,
21-
)
22-
from git.objects import (
23-
Submodule,
24-
RootModule,
25-
Commit
26-
)
27-
from git.util import (
28-
Actor,
29-
finalize_process
30-
)
31-
from git.index import IndexFile
32-
from git.config import GitConfigParser
33-
from git.remote import (
34-
Remote,
35-
add_progress,
36-
to_progress_instance
37-
)
38-
39-
from git.db import GitCmdObjectDB
7+
from collections import namedtuple
8+
import logging
9+
import os
10+
import re
11+
import sys
4012

4113
from gitdb.util import (
4214
join,
4315
isfile,
4416
hex_to_bin
4517
)
4618

47-
from .fun import (
48-
rev_parse,
49-
is_git_dir,
50-
find_git_dir,
51-
touch,
19+
from git.cmd import (
20+
Git,
21+
handle_process_output
5222
)
5323
from git.compat import (
5424
text_type,
@@ -58,12 +28,17 @@
5828
range,
5929
is_win,
6030
)
31+
from git.config import GitConfigParser
32+
from git.db import GitCmdObjectDB
33+
from git.exc import InvalidGitRepositoryError, NoSuchPathError, GitCommandError
34+
from git.index import IndexFile
35+
from git.objects import Submodule, RootModule, Commit
36+
from git.refs import HEAD, Head, Reference, TagReference
37+
from git.remote import Remote, add_progress, to_progress_instance
38+
from git.util import Actor, finalize_process
39+
40+
from .fun import rev_parse, is_git_dir, find_git_dir, touch
6141

62-
import os
63-
import sys
64-
import re
65-
import logging
66-
from collections import namedtuple
6742

6843
log = logging.getLogger(__name__)
6944

@@ -875,12 +850,22 @@ def _clone(cls, git, url, path, odb_default_type, progress, **kwargs):
875850
progress = to_progress_instance(progress)
876851

877852
odbt = kwargs.pop('odbt', odb_default_type)
878-
proc = git.clone(url, path, with_extended_output=True, as_process=True,
853+
854+
## A bug win cygwin's Git, when `--bare`
855+
# it prepends the basename of the `url` into the `path::
856+
# git clone --bare /cygwin/a/foo.git C:\\Work
857+
# becomes::
858+
# git clone --bare /cygwin/a/foo.git /cygwin/a/C:\\Work
859+
#
860+
clone_path = (Git.polish_url(path)
861+
if Git.is_cygwin() and 'bare' in kwargs
862+
else path)
863+
proc = git.clone(Git.polish_url(url), clone_path, with_extended_output=True, as_process=True,
879864
v=True, **add_progress(kwargs, git, progress))
880865
if progress:
881866
handle_process_output(proc, None, progress.new_message_handler(), finalize_process)
882867
else:
883-
(stdout, stderr) = proc.communicate() # FIXME: Will block of outputs are big!
868+
(stdout, stderr) = proc.communicate() # FIXME: Will block if outputs are big!
884869
log.debug("Cmd(%s)'s unused stdout: %s", getattr(proc, 'args', ''), stdout)
885870
finalize_process(proc, stderr=stderr)
886871

‎git/test/lib/helper.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
'GIT_REPO', 'GIT_DAEMON_PORT'
3333
)
3434

35-
log = logging.getLogger('git.util')
35+
log = logging.getLogger(__name__)
3636

3737
#{ Routines
3838

‎git/test/test_util.py

Lines changed: 55 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,19 @@
55
# the BSD License: http://www.opensource.org/licenses/bsd-license.php
66

77
import tempfile
8+
import time
9+
from unittest.case import skipIf
10+
11+
import ddt
812

13+
from git.cmd import dashify
14+
from git.compat import string_types, is_win
15+
from git.objects.util import (
16+
altz_to_utctz_str,
17+
utctz_to_altz,
18+
verify_utctz,
19+
parse_date,
20+
)
921
from git.test.lib import (
1022
TestBase,
1123
assert_equal
@@ -15,19 +27,9 @@
1527
BlockingLockFile,
1628
get_user_id,
1729
Actor,
18-
IterableList
30+
IterableList,
31+
cygpath,
1932
)
20-
from git.objects.util import (
21-
altz_to_utctz_str,
22-
utctz_to_altz,
23-
verify_utctz,
24-
parse_date,
25-
)
26-
from git.cmd import dashify
27-
from git.compat import string_types, is_win
28-
29-
import time
30-
import ddt
3133

3234

3335
class TestIterableMember(object):
@@ -52,6 +54,47 @@ def setup(self):
5254
"array": [42],
5355
}
5456

57+
@skipIf(not is_win, "Paths specifically for Windows.")
58+
@ddt.data(
59+
(r'foo\bar', 'foo/bar'),
60+
(r'foo/bar', 'foo/bar'),
61+
(r'./bar', 'bar'),
62+
(r'.\bar', 'bar'),
63+
(r'../bar', '../bar'),
64+
(r'..\bar', '../bar'),
65+
(r'../bar/.\foo/../chu', '../bar/chu'),
66+
67+
(r'C:\Users', '/cygdrive/c/Users'),
68+
(r'C:\d/e', '/cygdrive/c/d/e'),
69+
70+
(r'\\?\a:\com', '/cygdrive/a/com'),
71+
(r'\\?\a:/com', '/cygdrive/a/com'),
72+
73+
(r'\\server\C$\Users', '//server/C$/Users'),
74+
(r'\\server\C$', '//server/C$'),
75+
(r'\\server\BAR/', '//server/BAR/'),
76+
(r'\\?\UNC\server\D$\Apps', '//server/D$/Apps'),
77+
78+
(r'D:/Apps', '/cygdrive/d/Apps'),
79+
(r'D:/Apps\fOO', '/cygdrive/d/Apps/fOO'),
80+
(r'D:\Apps/123', '/cygdrive/d/Apps/123'),
81+
)
82+
def test_cygpath_ok(self, case):
83+
wpath, cpath = case
84+
self.assertEqual(cygpath(wpath), cpath or wpath)
85+
86+
@skipIf(not is_win, "Paths specifically for Windows.")
87+
@ddt.data(
88+
(r'C:Relative', None),
89+
(r'D:Apps\123', None),
90+
(r'D:Apps/123', None),
91+
(r'\\?\a:rel', None),
92+
(r'\\share\a:rel', None),
93+
)
94+
def test_cygpath_invalids(self, case):
95+
wpath, cpath = case
96+
self.assertEqual(cygpath(wpath), cpath or wpath.replace('\\', '/'))
97+
5598
def test_it_should_dashify(self):
5699
assert_equal('this-is-my-argument', dashify('this_is_my_argument'))
57100
assert_equal('foo', dashify('foo'))

‎git/util.py

Lines changed: 147 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
# the BSD License: http://www.opensource.org/licenses/bsd-license.php
66
from __future__ import unicode_literals
77

8+
import contextlib
9+
from functools import wraps
810
import getpass
911
import logging
1012
import os
@@ -13,10 +15,8 @@
1315
import shutil
1416
import stat
1517
import time
18+
from unittest.case import SkipTest
1619

17-
from functools import wraps
18-
19-
from git.compat import is_win
2020
from gitdb.util import (# NOQA @IgnorePep8
2121
make_sha,
2222
LockedFD, # @UnusedImport
@@ -26,6 +26,7 @@
2626
to_bin_sha # @UnusedImport
2727
)
2828

29+
from git.compat import is_win
2930
import os.path as osp
3031

3132
from .compat import (
@@ -34,7 +35,6 @@
3435
PY3
3536
)
3637
from .exc import InvalidGitRepositoryError
37-
from unittest.case import SkipTest
3838

3939

4040
# NOTE: Some of the unused imports might be used/imported by others.
@@ -47,6 +47,8 @@
4747
'RemoteProgress', 'CallableRemoteProgress', 'rmtree', 'unbare_repo',
4848
'HIDE_WINDOWS_KNOWN_ERRORS')
4949

50+
log = logging.getLogger(__name__)
51+
5052
#: We need an easy way to see if Appveyor TCs start failing,
5153
#: so the errors marked with this var are considered "acknowledged" ones, awaiting remedy,
5254
#: till then, we wish to hide them.
@@ -70,6 +72,16 @@ def wrapper(self, *args, **kwargs):
7072
return wrapper
7173

7274

75+
@contextlib.contextmanager
76+
def cwd(new_dir):
77+
old_dir = os.getcwd()
78+
os.chdir(new_dir)
79+
try:
80+
yield new_dir
81+
finally:
82+
os.chdir(old_dir)
83+
84+
7385
def rmtree(path):
7486
"""Remove the given recursively.
7587
@@ -162,14 +174,141 @@ def assure_directory_exists(path, is_file=False):
162174
Otherwise it must be a directory
163175
:return: True if the directory was created, False if it already existed"""
164176
if is_file:
165-
path = os.path.dirname(path)
177+
path = osp.dirname(path)
166178
# END handle file
167-
if not os.path.isdir(path):
179+
if not osp.isdir(path):
168180
os.makedirs(path)
169181
return True
170182
return False
171183

172184

185+
def _get_exe_extensions():
186+
try:
187+
winprog_exts = tuple(p.upper() for p in os.environ['PATHEXT'].split(os.pathsep))
188+
except:
189+
winprog_exts = ('.BAT', 'COM', '.EXE')
190+
191+
return winprog_exts
192+
193+
194+
def py_where(program, path=None):
195+
# From: http://stackoverflow.com/a/377028/548792
196+
try:
197+
winprog_exts = tuple(p.upper() for p in os.environ['PATHEXT'].split(os.pathsep))
198+
except:
199+
winprog_exts = is_win and ('.BAT', 'COM', '.EXE') or ()
200+
201+
def is_exec(fpath):
202+
return osp.isfile(fpath) and os.access(fpath, os.X_OK) and (
203+
os.name != 'nt' or not winprog_exts or any(fpath.upper().endswith(ext)
204+
for ext in winprog_exts))
205+
206+
progs = []
207+
if not path:
208+
path = os.environ["PATH"]
209+
for folder in path.split(osp.pathsep):
210+
folder = folder.strip('"')
211+
if folder:
212+
exe_path = osp.join(folder, program)
213+
for f in [exe_path] + ['%s%s' % (exe_path, e) for e in winprog_exts]:
214+
if is_exec(f):
215+
progs.append(f)
216+
return progs
217+
218+
219+
def _cygexpath(drive, path):
220+
if osp.isabs(path) and not drive:
221+
## Invoked from `cygpath()` directly with `D:Apps\123`?
222+
# It's an error, leave it alone just slashes)
223+
p = path
224+
else:
225+
p = osp.normpath(osp.expandvars(os.path.expanduser(path)))
226+
if osp.isabs(p):
227+
if drive:
228+
# Confusing, maybe a remote system should expand vars.
229+
p = path
230+
else:
231+
p = cygpath(p)
232+
elif drive:
233+
p = '/cygdrive/%s/%s' % (drive.lower(), p)
234+
235+
return p.replace('\\', '/')
236+
237+
238+
_cygpath_parsers = (
239+
## See: https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx
240+
## and: https://www.cygwin.com/cygwin-ug-net/using.html#unc-paths
241+
(re.compile(r"\\\\\?\\UNC\\([^\\]+)\\([^\\]+)(?:\\(.*))?"),
242+
(lambda server, share, rest_path: '//%s/%s/%s' % (server, share, rest_path.replace('\\', '/'))),
243+
False
244+
),
245+
246+
(re.compile(r"\\\\\?\\(\w):[/\\](.*)"),
247+
_cygexpath,
248+
False
249+
),
250+
251+
(re.compile(r"(\w):[/\\](.*)"),
252+
_cygexpath,
253+
False
254+
),
255+
256+
(re.compile(r"file:(.*)", re.I),
257+
(lambda rest_path: rest_path),
258+
True),
259+
260+
(re.compile(r"(\w{2,}:.*)"), # remote URL, do nothing
261+
(lambda url: url),
262+
False),
263+
)
264+
265+
266+
def cygpath(path):
267+
if not path.startswith(('/cygdrive', '//')):
268+
for regex, parser, recurse in _cygpath_parsers:
269+
match = regex.match(path)
270+
if match:
271+
path = parser(*match.groups())
272+
if recurse:
273+
path = cygpath(path)
274+
break
275+
else:
276+
path = _cygexpath(None, path)
277+
278+
return path
279+
280+
281+
#: Store boolean flags denoting if a specific Git executable
282+
#: is from a Cygwin installation (since `cache_lru()` unsupported on PY2).
283+
_is_cygwin_cache = {}
284+
285+
286+
def is_cygwin_git(git_executable):
287+
if not is_win:
288+
return False
289+
290+
from subprocess import check_output
291+
292+
is_cygwin = _is_cygwin_cache.get(git_executable)
293+
if is_cygwin is None:
294+
is_cygwin = False
295+
try:
296+
git_dir = osp.dirname(git_executable)
297+
if not git_dir:
298+
res = py_where(git_executable)
299+
git_dir = osp.dirname(res[0]) if res else None
300+
301+
## Just a name given, not a real path.
302+
uname_cmd = osp.join(git_dir, 'uname')
303+
uname = check_output(uname_cmd, universal_newlines=True)
304+
is_cygwin = 'CYGWIN' in uname
305+
except Exception as ex:
306+
log.debug('Failed checking if running in CYGWIN due to: %r', ex)
307+
_is_cygwin_cache[git_executable] = is_cygwin
308+
309+
return is_cygwin
310+
311+
173312
def get_user_id():
174313
""":return: string identifying the currently active system user as name@node"""
175314
return "%s@%s" % (getpass.getuser(), platform.node())
@@ -589,7 +728,7 @@ def _obtain_lock_or_raise(self):
589728
if self._has_lock():
590729
return
591730
lock_file = self._lock_file_path()
592-
if os.path.isfile(lock_file):
731+
if osp.isfile(lock_file):
593732
raise IOError("Lock for file %r did already exist, delete %r in case the lock is illegal" %
594733
(self._file_path, lock_file))
595734

@@ -659,7 +798,7 @@ def _obtain_lock(self):
659798
# synity check: if the directory leading to the lockfile is not
660799
# readable anymore, raise an execption
661800
curtime = time.time()
662-
if not os.path.isdir(os.path.dirname(self._lock_file_path())):
801+
if not osp.isdir(osp.dirname(self._lock_file_path())):
663802
msg = "Directory containing the lockfile %r was not readable anymore after waiting %g seconds" % (
664803
self._lock_file_path(), curtime - starttime)
665804
raise IOError(msg)

0 commit comments

Comments
 (0)
Please sign in to comment.