From 8f7bcaea4bd1d067a4c200e84f462d328f0c3c89 Mon Sep 17 00:00:00 2001 From: Santiago Perez De Rosso Date: Mon, 16 Dec 2013 16:20:58 -0500 Subject: [PATCH 1/3] pipe output of log to less (hack) --- gitpylib/log.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/gitpylib/log.py b/gitpylib/log.py index 9391881..f880509 100644 --- a/gitpylib/log.py +++ b/gitpylib/log.py @@ -7,8 +7,10 @@ def log(): - subprocess.call('git log', shell=True) + # The pipe to less shouldn't be here. TODO: fix. + subprocess.call('git log | less', shell=True) def log_p(): - subprocess.call('git log -p', shell=True) + # The pipe to less shouldn't be here. TODO: fix. + subprocess.call('git log -p | less', shell=True) From 50e7377e7218fc12ad7e90a5350b8ca325c75f34 Mon Sep 17 00:00:00 2001 From: Santiago Perez De Rosso Date: Wed, 15 Jan 2014 11:15:28 -0500 Subject: [PATCH 2/3] Performance improvements in status cmd --- gitpylib/common.py | 14 ++++ gitpylib/status.py | 173 ++++++++++++++++++++++++++++++--------------- 2 files changed, 131 insertions(+), 56 deletions(-) diff --git a/gitpylib/common.py b/gitpylib/common.py index 4335b76..9fdacad 100644 --- a/gitpylib/common.py +++ b/gitpylib/common.py @@ -107,3 +107,17 @@ def remove_dups(list, key): keys.add(k_a) ret.append(a) return ret + + +def get_all_fps_under_cwd(): + """Returns a list of all existing filepaths under the cwd. + + The filepaths returned are relative to the cwd. The Git directory (.git) + is ignored. + """ + fps = [] + for dirpath, dirnames, filenames in os.walk(os.getcwd()): + if '.git' in dirnames: + dirnames.remove('.git') + fps.extend([os.path.relpath(os.path.join(dirpath, fp)) for fp in filenames]) + return fps diff --git a/gitpylib/status.py b/gitpylib/status.py index 996ead4..e3d7088 100644 --- a/gitpylib/status.py +++ b/gitpylib/status.py @@ -6,6 +6,7 @@ import os +import re import common @@ -14,6 +15,10 @@ FILE_NOT_FOUND = 2 # Possible status in which a Git file can be in. +# (There are actually many more, but these seem to be the only ones relevant +# to Gitless.) +# TODO(sperezde): just have gitpylib's status return the status code and let +# Gitless figure out the rest by itself. TRACKED_UNMODIFIED = 3 TRACKED_MODIFIED = 4 UNTRACKED = 5 @@ -25,10 +30,9 @@ DELETED_ASSUME_UNCHANGED = 10 IN_CONFLICT = 11 IGNORED = 12 -IGNORED_STAGED = 13 # the file was a tracked file that was modified after being staged. -MODIFIED_MODIFIED = 14 -ADDED_MODIFIED = 15 # file is a new file that was added and then modified. +MODIFIED_MODIFIED = 13 +ADDED_MODIFIED = 14 # file is a new file that was added and then modified. def of_file(fp): @@ -43,20 +47,29 @@ def of_file(fp): """ fp = common.real_case(fp) - ok, out, unused_err = common.git_call( + ok, out_ls_files, unused_err = common.git_call( 'ls-files -tvco --error-unmatch "%s"' % fp) if not ok: # The file doesn't exist. return FILE_NOT_FOUND + return _status_file(fp) - return _status_from_output(out[0], fp) - -def au_files(): - """Gets all assumed unchanged files. Paths are relative to the repo dir.""" +def au_files(relative_to_cwd=False): + """Gets all assumed unchanged files. + + Args: + relative_to_cwd: if True then only those au files under the cwd are + reported. If False, all au files in the repository are reported. (Defaults + to False.) + """ out, unused_err = common.safe_git_call( - 'ls-files -v --full-name %s' % common.repo_dir()) + 'ls-files -v {}'.format( + '--full-name "{}"'.format( + common.repo_dir()) if not relative_to_cwd else '')) ret = [] + # There could be dups in the output from ls-files if, for example, there are + # files in conflict. for f_out in common.remove_dups(out.splitlines(), lambda x: x[2:]): if f_out[0] == 'h': ret.append(f_out[2:]) @@ -71,53 +84,101 @@ def of_repo(): status is the status of the file (TRACKED_UNMODIFIED, TRACKED_MODIFIED, UNTRACKED, ASSUME_UNCHANGED, STAGED, etc -- see above). """ - unused_ok, out, unused_err = common.git_call('ls-files -tvco') + return _status_cwd() - for f_out in common.remove_dups(out.splitlines(), lambda x: x[2:]): - # output is 'S filename' where S is a character representing the status of - # the file. - fp = f_out[2:] - yield (_status_from_output(f_out[0], fp), fp) - - -def _status_from_output(s, fp): - if s == '?': - # We need to see if it is an ignored file. - out, unused_err = common.safe_git_call('status --porcelain "%s"' % fp) - if not len(out): - return IGNORED - return UNTRACKED - elif s == 'h': - return ASSUME_UNCHANGED if os.path.exists(fp) else DELETED_ASSUME_UNCHANGED - elif s == 'H': - # We need to use status --porcelain to figure out whether it's deleted, - # modified or not. - out, unused_err = common.safe_git_call('status --porcelain "%s"' % fp) - if not len(out): + +# Private functions. + + +def _status_cwd(): + status_codes = _status_porcelain(os.getcwd()) + au_fps = set(au_files(relative_to_cwd=True)) + for au_fp in au_fps: + if au_fp not in status_codes: + status_codes[au_fp] = None + all_fps_under_cwd = common.get_all_fps_under_cwd() + for fp_under_cwd in all_fps_under_cwd: + if fp_under_cwd not in status_codes: + status_codes[fp_under_cwd] = None + for s_fp, s in status_codes.iteritems(): + status = _status_from_output(s, s_fp in au_fps, s_fp) + yield (status, s_fp) + + +def _status_file(fp): + s = _status_porcelain(fp).get(fp, None) + return _status_from_output(s, _is_au_file(fp), fp) + + +def _is_au_file(fp): + """True if the given fp corresponds to an assume unchanged file. + + Args: + fp: the filepath to check (fp must be a file not a dir). + """ + out, unused_err = common.safe_git_call( + 'ls-files -v --full-name "{}"'.format(fp)) + ret = False + if out: + f_out = common.remove_dups(out.splitlines(), lambda x: x[2:]) + if len(f_out) != 1: + raise Exception('Unexpected output of ls-files: {}'.format(out)) + ret = f_out[0][0] == 'h' + return ret + + +def _status_porcelain(pathspec): + """Executes the status porcelain command with the given pathspec. + + Ignored and untracked files are reported. + + Returns: + A dict of fp -> status code. All fps are relative to the cwd. + """ + def sanitize_fp(unsanitized_fp): + ret = unsanitized_fp.strip() + if ret.startswith('"') and ret.endswith('"'): + ret = ret[1:-1] + # The paths outputted by status are relative to the repodir, we need to make + # them relative to the cwd. + ret = os.path.relpath( + os.path.join(common.repo_dir(), ret), os.getcwd()) + return ret + + out_status, unused_err = common.safe_git_call( + 'status --porcelain -u --ignored "{}"'.format(pathspec)) + ret = {} + for f_out_status in out_status.splitlines(): + # Output is in the form . + # is 2 chars long. + ret[sanitize_fp(f_out_status[3:])] = f_out_status[:2] + return ret + + +def _status_from_output(s, is_au, fp): + if not s: + if is_au: + if not os.path.exists(fp): + return DELETED_ASSUME_UNCHANGED + return ASSUME_UNCHANGED + else: return TRACKED_UNMODIFIED - # Output is in the form . We are only interested in the - # status part. - s = out.strip().split()[0] - if s == 'M': - return TRACKED_MODIFIED - elif s == 'A': - # It could be ignored and staged. - out, unused_err = common.safe_git_call( - 'ls-files -ic --exclude-standard "%s"' % fp) - if len(out): - return IGNORED_STAGED - return STAGED - elif s == 'D': - return DELETED - elif s == 'AD': - return DELETED_STAGED - elif s == 'MM': - return MODIFIED_MODIFIED - elif s == 'AM': - return ADDED_MODIFIED - raise Exception( - "Failed to get status of file %s, out %s, status %s" % (fp, out, s)) - elif s == 'M': + if s == '??': + return UNTRACKED + elif s == '!!': + return IGNORED + elif s == ' M': + return TRACKED_MODIFIED + elif s == 'A ': + return STAGED + elif s == ' D': + return DELETED + elif s == 'AD': + return DELETED_STAGED + elif s == 'MM': + return MODIFIED_MODIFIED + elif s == 'AM': + return ADDED_MODIFIED + elif s == 'AA' or s == 'M ' or s == 'DD' or 'U' in s: return IN_CONFLICT - - raise Exception("Failed to get status of file %s, status %s" % (fp, s)) + raise Exception('Failed to get status of file {}, s is "{}"'.format(fp, s)) From 86d390b14264ad1c709edf3730f2d6bcc23f7873 Mon Sep 17 00:00:00 2001 From: Santiago Perez De Rosso Date: Wed, 15 Jan 2014 11:50:51 -0500 Subject: [PATCH 3/3] relase 0.4.2 --- RELEASE_NOTES.md | 7 +++++++ setup.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 7ca2240..6cb871c 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -2,6 +2,13 @@ Gitpylib's Release Notes ======================== +15th Jan 2014 - 0.4.2 +--------------------- + +* Performance improvements in status. +* Output of log is piped to less. + + 6th Dec 2013 - 0.4.1 -------------------- diff --git a/setup.py b/setup.py index b2f1d89..8c6297c 100755 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name='gitpylib', - version='0.4.1', + version='0.4.2', description='A Python library for Git', long_description=open('README.md').read(), author='Santiago Perez De Rosso',