forked from mrkline/promptoglyph
-
Notifications
You must be signed in to change notification settings - Fork 0
/
git.d
299 lines (242 loc) · 8.1 KB
/
git.d
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
import std.algorithm : canFind, filter, splitter;
import std.conv : to;
import std.datetime : Clock, Duration;
import std.exception : enforce;
import std.file : dirEntries, DirEntry, readText, SpanMode;
import std.path : baseName, buildPath, relativePath;
import std.process; // : A whole lotta stuff
import std.range : empty, front, back;
import std.stdio : File;
import std.string : startsWith, strip;
import color;
/// Information returned by invoking git status
struct StatusFlags {
bool untracked; ///< Untracked files are present in the repo.
bool modified; ///< Tracked files have been modified
bool indexed; ///< Files are in Git's index, ready for commit
}
/// Git status output plus the repository's HEAD
struct RepoStatus {
StatusFlags flags;
string head;
};
/**
* Gets a string representation of the status of the Git repo
*
* Params:
* allottedTime = The amount of time given to gather Git info.
* Git status will be killed if it does not complete in this much time.
* Since this is for a shell prompt, responsiveness is important.
* colors = Whether or not colored output is desired
* escapes = Whether or not ZSH escapes are needed. Ignored if no colors are desired.
*
*/
string stringRepOfStatus(Duration allottedTime, UseColor colors, Escapes escapes)
{
// getRepoStatus will return null if we are not in a repo
auto status = getRepoStatus(allottedTime);
if (status is null)
return "";
// Local function that colors a source string if the colors flag is set.
string colorText(string source,
string function(Escapes) colorFunction)
{
if (!colors)
return source;
else
return colorFunction(escapes) ~ source;
}
string head;
if (!status.head.empty)
head = colorText(status.head, &cyan);
string flags = " ";
if (status.flags.indexed)
flags ~= colorText("✔", &green);
if (status.flags.modified)
flags ~= colorText("±", &yellow); // Yellow plus/minus
if (status.flags.untracked)
flags ~= colorText("?", &red); // Red quesiton mark
// We don't want an extra space if there's nothing to show.
if (flags == " ")
flags = "";
string ret = "[" ~ head ~ flags ~ colorText("]", &resetColor);
// Prepend a T if git status ran out of time
if (pastTime(allottedTime))
ret = 'T' ~ ret;
return ret;
}
private:
/// Returns true if the program has been running for longer
/// than the given duration.
bool pastTime(Duration allottedTime)
{
return cast(Duration)Clock.currAppTick > allottedTime;
}
// Fetches information about the Git repository,
// or returns null if we are not in one.
RepoStatus* getRepoStatus(Duration allottedTime)
{
import std.parallelism;
// This should give us the root directory of the Git repo
auto rootFinder = execute(["git", "rev-parse", "--show-toplevel"]);
immutable repoRoot = rootFinder.output.strip();
if (rootFinder.status != 0 || repoRoot.empty)
return null;
RepoStatus* ret = new RepoStatus;
ret.head = getHead(repoRoot, allottedTime);
ret.flags = asyncGetFlags(allottedTime);
return ret;
}
/// Uses asynchronous I/O to read as much git status output as it can
/// in the given amount of time.
public // So std.parallelism can get at it
StatusFlags asyncGetFlags(Duration allottedTime)
{
// Currently we can only do this for Unix.
// Windows async pipe I/O (they call it "overlapped" I/O)
// is more... involved.
// TODO: Either write a Windows implementation or suck it up
// and do things synchronously in Windows.
import core.sys.posix.poll;
StatusFlags ret;
// Light off git status while we find the HEAD
auto pipes = pipeProcess(["git", "status", "--porcelain"], Redirect.stdout);
// If an exception gets thrown, be sure to cleanup the process.
scope(failure) {
kill(pipes.pid);
wait(pipes.pid);
}
// Local function for processing the output of git status.
// See the docs for git status porcelain output
void processPorcelainLine(string line)
{
if (line is null)
return;
// git status --porcelain spits out a two-character code
// for each file that would show up in Git status
// Why is this .array needed? Check odd set.back error below
string set = line[0 .. 2];
// Question marks indicate a file is untracked.
if (set.canFind('?')) {
ret.untracked = true;
}
else {
// The second character indicates the working tree.
// If it is not a blank or a question mark,
// we have some un-indexed changes.
if (set.back != ' ')
ret.modified = true;
// The first character indicates the index.
// If it is not blank or a question mark,
// we have some indexed changes.
if (set.front != ' ')
ret.indexed = true;
}
}
// We need the actual file descriptor of the pipe so we can call poll
immutable int fdes = core.stdc.stdio.fileno(pipes.stdout.getFP());
enforce(fdes >= 0, "fileno failed.");
pollfd pfd;
pfd.fd = fdes; // The file descriptor we want to poll
pfd.events = POLLIN; // Notify us if there is data to be read
string nextLine;
// As long as git status is running, keep at it.
while (!tryWait(pipes.pid).terminated) {
// Poll the pipe with an arbitrary 5 millisecond timeout
enforce(poll(&pfd, 1, 5) >= 0, "poll failed");
// If we have data to read, process a line of it.
if (pfd.revents & POLLIN) {
nextLine = pipes.stdout.readln();
processPorcelainLine(nextLine);
}
else if (pastTime(allottedTime)) {
import core.sys.posix.signal: SIGTERM;
kill(pipes.pid, SIGTERM);
break;
}
}
// Process anything left over
while (nextLine !is null) {
nextLine = pipes.stdout.readln();
processPorcelainLine(nextLine);
}
// Join the process
wait(pipes.pid);
return ret;
}
/// Gets the name of the current Git head, or a shortened SHA
/// if there is no symbolic name.
string getHead(string repoRoot, Duration allottedTime)
{
// getHead doesn't use async I/O because it is assumed that
// reading one-line files will take a negligible amount of time.
// If this assumption proves false, we should revisit it.
immutable headPath = buildPath(repoRoot, ".git", "HEAD");
immutable headSHA = headPath.readAndStrip();
// If we're on a branch head, .git/HEAD will look like
// ref: refs/heads/<branch>
if (headSHA.startsWith("ref:"))
return headSHA.baseName;
// Otherwise let's go rummaging through the refs to find something
immutable refsPath = buildPath(repoRoot, ".git", "refs");
// No need to check heads as we handled that case above.
// Let's check remotes
immutable remotesPath = buildPath(refsPath, "remotes");
string ret = searchDirectoryForHead(remotesPath, headSHA);
if (!ret.empty)
return relativePath(ret, remotesPath);
else if (pastTime(allottedTime))
return headSHA[0 .. 7];
// We didn't find anything in remotes. Let's check tags.
immutable tagsPath = buildPath(refsPath, "tags");
ret = searchDirectoryForHead(tagsPath, headSHA);
if (!ret.empty)
return relativePath(ret, tagsPath);
else if (pastTime(allottedTime))
return headSHA[0 .. 7];
// We didn't find anything in remotes. Let's check packed-refs
auto packedRefs = File(buildPath(repoRoot, ".git", "packed-refs"))
.byLine
.filter!(l => !l.startsWith('#'));
foreach(line; packedRefs) {
// Each line is in the form
// <sha> <path>
auto tokens = splitter(line);
auto sha = tokens.front;
tokens.popFront();
auto refPath = tokens.front;
tokens.popFront();
// Line should be empty now
enforce(tokens.empty, "Weird Git packed-refs remnant:\n" ~ tokens.to!string);
if (sha == headSHA)
return refPath.baseName.idup;
else if (pastTime(allottedTime))
return headSHA[0 .. 7];
}
// Still nothing. Just return a shortened version of the HEAD sha
return headSHA[0 .. 7];
}
// Utility functions for getHead
string readAndStrip(string path)
{
return readText(path).strip();
}
bool isRefFile(ref DirEntry de)
{
// We are ignoring remote HEADS.
return de.isFile &&
de.name.baseName != "HEAD";
}
string searchDirectoryForHead(string dir, string head)
{
bool matchesHead(ref DirEntry de)
{
return de.name.readAndStrip() == head;
}
auto matchingRemotes = dirEntries(dir, SpanMode.depth, false)
.filter!(f => isRefFile(f) && matchesHead(f));
if (!matchingRemotes.empty)
return matchingRemotes.front.name;
else
return "";
}