Contact
CoCalc Logo Icon
StoreFeaturesDocsShareSupport News AboutSign UpSign In
| Download
Views: 39558
1
#!/usr/bin/python
2
"""
3
(c) William Stein, 2013
4
5
A git-aware version of "ls -l", with JSON output
6
7
For each file, we compute the following information:
8
9
* name: name of the file
10
* isdir: true if it is a directory
11
* size: size in bytes (if it is not a directory)
12
* mtime: timestamp of file itself
13
* commit: the *most recent* git commit related to this file; only
14
given if file is under git control
15
- author
16
- date
17
- message
18
- sha
19
"""
20
21
import json, os, subprocess, sys, uuid
22
23
def getmtime(name):
24
try:
25
return os.lstat(name).st_mtime # use lstat instead of stat or getmtime so this works on broken symlinks!
26
except:
27
# This should never happen, but we include this just in case
28
# to avoid really buggy UI behavior.
29
return 0
30
31
def getsize(name):
32
try:
33
return os.lstat(name).st_size # same as above; use instead of os.path....
34
except:
35
return 0
36
37
def gitls(path, time, start, limit, hidden, directories_first, git_aware=True):
38
if not os.path.exists(path):
39
# CRITICAL: Exactly this error message is searched for in smc-webapp/project_store.coffee
40
# DO NOT change this here!!!!! (Unless you change that...)
41
sys.stderr.write("error: no such path '%s'"%path)
42
sys.exit(1)
43
os.chdir(path)
44
result = {}
45
listdir = os.listdir('.')
46
try:
47
json.dumps(listdir)
48
except:
49
# Throw away filenames that can't be json'd, since they can't be JSON'd below, which would totally
50
# lock user out of their project.
51
listdir = []
52
for x in os.listdir('.'):
53
try:
54
json.dumps(x)
55
listdir.append(x)
56
except:
57
pass
58
if not hidden:
59
listdir = [x for x in listdir if not x.startswith('.')]
60
if path.endswith('.snapshots') or path.endswith('.snapshots/'):
61
all = [(x, getmtime(x)) for x in reversed(sorted(listdir)) if os.path.exists(x)] # exists due to broken symlinks when snaps vanish
62
files = dict([(name, {'name':name, 'mtime':mtime}) for name, mtime in all])
63
sorted_names = [x[0] for x in all]
64
elif time:
65
# Get list of pairs (timestamp, name)
66
all = [(getmtime(name), name) for name in listdir]
67
# Sort
68
all.sort()
69
all.reverse()
70
# Limit and convert to objects
71
all = all[start:]
72
if limit > 0 and len(all) > limit:
73
result['more'] = True
74
all = all[:limit]
75
files = dict([(name, {'name':name, 'mtime':int(mtime)}) for mtime, name in all])
76
sorted_names = [x[1] for x in all]
77
else:
78
# Get list of (name, timestamp) pairs
79
all = [(name, int(getmtime(name))) for name in listdir]
80
# Sort
81
all.sort()
82
# Limit and convert to objects
83
all = all[start:]
84
if limit > 0 and len(all) > limit:
85
result['more'] = True
86
all = all[:limit]
87
files = dict([(name, {'name':name, 'mtime':mtime}) for name, mtime in all])
88
sorted_names = [x[0] for x in all]
89
90
# Fill in other OS information about each file
91
#for obj in result:
92
for name, info in files.iteritems():
93
if os.path.isdir(name):
94
info['isdir'] = True
95
else:
96
info['size'] = getsize(name)
97
98
# Fill in git-related information about each file
99
if git_aware:
100
# First, obtain git history about files
101
format = "--pretty=format:!%H|%an <%ae>|%ad|%s|"
102
field_sep = str(uuid.uuid4())
103
commit_sep = str(uuid.uuid4())
104
format = format.replace('|',field_sep).replace("!",commit_sep)
105
# Why 200 below: in tests, if there are tons of files, passing
106
# them all on the command line ends up taking longer than just
107
# getting info about everything in this tree
108
if len(files) > 200:
109
git_path = ['.']
110
else:
111
git_path = [name for name, info in files.iteritems() if not info.get('isdir',False)]
112
log = subprocess.Popen(['git', 'log', '--date=raw', '--name-status', format] + git_path,
113
stdin=subprocess.PIPE, stdout = subprocess.PIPE,
114
stderr=subprocess.PIPE).stdout.read()
115
v = {}
116
for entry in log.split(commit_sep):
117
if len(entry.strip()) == 0 : continue
118
sha, author, date, message, modified_files= entry.split(field_sep)
119
date = int(date.split()[0])
120
# modified_files = set of filenames
121
modified_files = [os.path.split(str(x[2:]).replace('\\\\"','"').replace('\\"',''))[-1] for x in modified_files.splitlines() if x]
122
for name in modified_files:
123
if name in files and 'commit' not in files[name]:
124
files[name]['commit'] = {'author':author, 'date':date, 'message':message, 'sha':sha}
125
126
# Make ordered list of files, with directories first.
127
if directories_first:
128
result['files'] = ([files[name] for name in sorted_names if files[name].get('isdir',False)] + \
129
[files[name] for name in sorted_names if not files[name].get('isdir',False)])
130
else:
131
result['files'] = [files[name] for name in sorted_names]
132
return result
133
134
def main():
135
import argparse
136
parser = argparse.ArgumentParser(description="git-ls -- like ls, but git *aware*, and JSON output; files not under git are still included.")
137
parser.add_argument('--time', help='order by time instead of alphabetical order (default: False)', action='store_true')
138
parser.add_argument('--start', help='starting file number', default=0)
139
parser.add_argument('--limit', help='maximum number of files', default=0) # 0 = no limit
140
parser.add_argument('--git', help="actually be git aware (not the default)", action="store_true")
141
parser.add_argument('--hidden', help="include files/directories that start with a dot", action="store_true")
142
parser.add_argument('--directories_first', help="sort by putting directories first, then files (default: False)", action="store_true")
143
parser.add_argument('path', nargs='?', help='return info about all files in this path', default='.')
144
args = parser.parse_args()
145
if isinstance(args.path, list):
146
args.path = args.path[0]
147
148
if os.path.abspath(args.path) == os.path.join(os.environ['HOME'], '.snapshots'):
149
# When getting a listing for the .snapshots directory, update it to show the latest version.
150
from update_snapshots import update_snapshots
151
update_snapshots()
152
153
r = gitls(path=args.path, time=args.time, start=int(args.start), limit=int(args.limit),
154
hidden=args.hidden, directories_first=args.directories_first, git_aware=args.git)
155
print json.dumps(r, separators=(',',':'))
156
157
if __name__ == "__main__":
158
main()
159
160
161