Skip to content
This repository was archived by the owner on May 29, 2019. It is now read-only.

Commit b10aeba

Browse files
committed
Merge branch 'develop'
Release 0.4.2: * Performance improvements in status. * Output of log is piped to less.
2 parents 989b853 + 86d390b commit b10aeba

File tree

5 files changed

+143
-59
lines changed

5 files changed

+143
-59
lines changed

RELEASE_NOTES.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@ Gitpylib's Release Notes
22
========================
33

44

5+
15th Jan 2014 - 0.4.2
6+
---------------------
7+
8+
* Performance improvements in status.
9+
* Output of log is piped to less.
10+
11+
512
6th Dec 2013 - 0.4.1
613
--------------------
714

gitpylib/common.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,3 +107,17 @@ def remove_dups(list, key):
107107
keys.add(k_a)
108108
ret.append(a)
109109
return ret
110+
111+
112+
def get_all_fps_under_cwd():
113+
"""Returns a list of all existing filepaths under the cwd.
114+
115+
The filepaths returned are relative to the cwd. The Git directory (.git)
116+
is ignored.
117+
"""
118+
fps = []
119+
for dirpath, dirnames, filenames in os.walk(os.getcwd()):
120+
if '.git' in dirnames:
121+
dirnames.remove('.git')
122+
fps.extend([os.path.relpath(os.path.join(dirpath, fp)) for fp in filenames])
123+
return fps

gitpylib/log.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@
77

88

99
def log():
10-
subprocess.call('git log', shell=True)
10+
# The pipe to less shouldn't be here. TODO: fix.
11+
subprocess.call('git log | less', shell=True)
1112

1213

1314
def log_p():
14-
subprocess.call('git log -p', shell=True)
15+
# The pipe to less shouldn't be here. TODO: fix.
16+
subprocess.call('git log -p | less', shell=True)

gitpylib/status.py

Lines changed: 117 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77

88
import os
9+
import re
910

1011
import common
1112

@@ -14,6 +15,10 @@
1415
FILE_NOT_FOUND = 2
1516

1617
# Possible status in which a Git file can be in.
18+
# (There are actually many more, but these seem to be the only ones relevant
19+
# to Gitless.)
20+
# TODO(sperezde): just have gitpylib's status return the status code and let
21+
# Gitless figure out the rest by itself.
1722
TRACKED_UNMODIFIED = 3
1823
TRACKED_MODIFIED = 4
1924
UNTRACKED = 5
@@ -25,10 +30,9 @@
2530
DELETED_ASSUME_UNCHANGED = 10
2631
IN_CONFLICT = 11
2732
IGNORED = 12
28-
IGNORED_STAGED = 13
2933
# the file was a tracked file that was modified after being staged.
30-
MODIFIED_MODIFIED = 14
31-
ADDED_MODIFIED = 15 # file is a new file that was added and then modified.
34+
MODIFIED_MODIFIED = 13
35+
ADDED_MODIFIED = 14 # file is a new file that was added and then modified.
3236

3337

3438
def of_file(fp):
@@ -43,20 +47,29 @@ def of_file(fp):
4347
"""
4448
fp = common.real_case(fp)
4549

46-
ok, out, unused_err = common.git_call(
50+
ok, out_ls_files, unused_err = common.git_call(
4751
'ls-files -tvco --error-unmatch "%s"' % fp)
4852
if not ok:
4953
# The file doesn't exist.
5054
return FILE_NOT_FOUND
55+
return _status_file(fp)
5156

52-
return _status_from_output(out[0], fp)
5357

54-
55-
def au_files():
56-
"""Gets all assumed unchanged files. Paths are relative to the repo dir."""
58+
def au_files(relative_to_cwd=False):
59+
"""Gets all assumed unchanged files.
60+
61+
Args:
62+
relative_to_cwd: if True then only those au files under the cwd are
63+
reported. If False, all au files in the repository are reported. (Defaults
64+
to False.)
65+
"""
5766
out, unused_err = common.safe_git_call(
58-
'ls-files -v --full-name %s' % common.repo_dir())
67+
'ls-files -v {}'.format(
68+
'--full-name "{}"'.format(
69+
common.repo_dir()) if not relative_to_cwd else ''))
5970
ret = []
71+
# There could be dups in the output from ls-files if, for example, there are
72+
# files in conflict.
6073
for f_out in common.remove_dups(out.splitlines(), lambda x: x[2:]):
6174
if f_out[0] == 'h':
6275
ret.append(f_out[2:])
@@ -71,53 +84,101 @@ def of_repo():
7184
status is the status of the file (TRACKED_UNMODIFIED, TRACKED_MODIFIED,
7285
UNTRACKED, ASSUME_UNCHANGED, STAGED, etc -- see above).
7386
"""
74-
unused_ok, out, unused_err = common.git_call('ls-files -tvco')
87+
return _status_cwd()
7588

76-
for f_out in common.remove_dups(out.splitlines(), lambda x: x[2:]):
77-
# output is 'S filename' where S is a character representing the status of
78-
# the file.
79-
fp = f_out[2:]
80-
yield (_status_from_output(f_out[0], fp), fp)
81-
82-
83-
def _status_from_output(s, fp):
84-
if s == '?':
85-
# We need to see if it is an ignored file.
86-
out, unused_err = common.safe_git_call('status --porcelain "%s"' % fp)
87-
if not len(out):
88-
return IGNORED
89-
return UNTRACKED
90-
elif s == 'h':
91-
return ASSUME_UNCHANGED if os.path.exists(fp) else DELETED_ASSUME_UNCHANGED
92-
elif s == 'H':
93-
# We need to use status --porcelain to figure out whether it's deleted,
94-
# modified or not.
95-
out, unused_err = common.safe_git_call('status --porcelain "%s"' % fp)
96-
if not len(out):
89+
90+
# Private functions.
91+
92+
93+
def _status_cwd():
94+
status_codes = _status_porcelain(os.getcwd())
95+
au_fps = set(au_files(relative_to_cwd=True))
96+
for au_fp in au_fps:
97+
if au_fp not in status_codes:
98+
status_codes[au_fp] = None
99+
all_fps_under_cwd = common.get_all_fps_under_cwd()
100+
for fp_under_cwd in all_fps_under_cwd:
101+
if fp_under_cwd not in status_codes:
102+
status_codes[fp_under_cwd] = None
103+
for s_fp, s in status_codes.iteritems():
104+
status = _status_from_output(s, s_fp in au_fps, s_fp)
105+
yield (status, s_fp)
106+
107+
108+
def _status_file(fp):
109+
s = _status_porcelain(fp).get(fp, None)
110+
return _status_from_output(s, _is_au_file(fp), fp)
111+
112+
113+
def _is_au_file(fp):
114+
"""True if the given fp corresponds to an assume unchanged file.
115+
116+
Args:
117+
fp: the filepath to check (fp must be a file not a dir).
118+
"""
119+
out, unused_err = common.safe_git_call(
120+
'ls-files -v --full-name "{}"'.format(fp))
121+
ret = False
122+
if out:
123+
f_out = common.remove_dups(out.splitlines(), lambda x: x[2:])
124+
if len(f_out) != 1:
125+
raise Exception('Unexpected output of ls-files: {}'.format(out))
126+
ret = f_out[0][0] == 'h'
127+
return ret
128+
129+
130+
def _status_porcelain(pathspec):
131+
"""Executes the status porcelain command with the given pathspec.
132+
133+
Ignored and untracked files are reported.
134+
135+
Returns:
136+
A dict of fp -> status code. All fps are relative to the cwd.
137+
"""
138+
def sanitize_fp(unsanitized_fp):
139+
ret = unsanitized_fp.strip()
140+
if ret.startswith('"') and ret.endswith('"'):
141+
ret = ret[1:-1]
142+
# The paths outputted by status are relative to the repodir, we need to make
143+
# them relative to the cwd.
144+
ret = os.path.relpath(
145+
os.path.join(common.repo_dir(), ret), os.getcwd())
146+
return ret
147+
148+
out_status, unused_err = common.safe_git_call(
149+
'status --porcelain -u --ignored "{}"'.format(pathspec))
150+
ret = {}
151+
for f_out_status in out_status.splitlines():
152+
# Output is in the form <status> <file path>.
153+
# <status> is 2 chars long.
154+
ret[sanitize_fp(f_out_status[3:])] = f_out_status[:2]
155+
return ret
156+
157+
158+
def _status_from_output(s, is_au, fp):
159+
if not s:
160+
if is_au:
161+
if not os.path.exists(fp):
162+
return DELETED_ASSUME_UNCHANGED
163+
return ASSUME_UNCHANGED
164+
else:
97165
return TRACKED_UNMODIFIED
98-
# Output is in the form <status> <name>. We are only interested in the
99-
# status part.
100-
s = out.strip().split()[0]
101-
if s == 'M':
102-
return TRACKED_MODIFIED
103-
elif s == 'A':
104-
# It could be ignored and staged.
105-
out, unused_err = common.safe_git_call(
106-
'ls-files -ic --exclude-standard "%s"' % fp)
107-
if len(out):
108-
return IGNORED_STAGED
109-
return STAGED
110-
elif s == 'D':
111-
return DELETED
112-
elif s == 'AD':
113-
return DELETED_STAGED
114-
elif s == 'MM':
115-
return MODIFIED_MODIFIED
116-
elif s == 'AM':
117-
return ADDED_MODIFIED
118-
raise Exception(
119-
"Failed to get status of file %s, out %s, status %s" % (fp, out, s))
120-
elif s == 'M':
166+
if s == '??':
167+
return UNTRACKED
168+
elif s == '!!':
169+
return IGNORED
170+
elif s == ' M':
171+
return TRACKED_MODIFIED
172+
elif s == 'A ':
173+
return STAGED
174+
elif s == ' D':
175+
return DELETED
176+
elif s == 'AD':
177+
return DELETED_STAGED
178+
elif s == 'MM':
179+
return MODIFIED_MODIFIED
180+
elif s == 'AM':
181+
return ADDED_MODIFIED
182+
elif s == 'AA' or s == 'M ' or s == 'DD' or 'U' in s:
121183
return IN_CONFLICT
122-
123-
raise Exception("Failed to get status of file %s, status %s" % (fp, s))
184+
raise Exception('Failed to get status of file {}, s is "{}"'.format(fp, s))

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
setup(
77
name='gitpylib',
8-
version='0.4.1',
8+
version='0.4.2',
99
description='A Python library for Git',
1010
long_description=open('README.md').read(),
1111
author='Santiago Perez De Rosso',

0 commit comments

Comments
 (0)