Skip to content

Commit 3c498c9

Browse files
committed
Merge branch 'stash-deleted-2'
2 parents ff40b77 + c3e9e53 commit 3c498c9

File tree

5 files changed

+207
-39
lines changed

5 files changed

+207
-39
lines changed

node/lib/cmd/stash.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,10 @@ function cleanSubs(status, includeUntracked) {
117117
for (let subName in subs) {
118118
const sub = subs[subName];
119119
const wd = sub.workdir;
120+
if (sub.index === null) {
121+
// This sub was deleted
122+
return false;
123+
}
120124
if (sub.commit.sha !== sub.index.sha) {
121125
// The submodule has a commit which is staged in the meta repo's
122126
// index

node/lib/util/repo_status.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -372,7 +372,8 @@ class Submodule {
372372
*/
373373
isCommittable () {
374374
return !(this.isNew() &&
375-
null === this.d_index.sha &&
375+
(null === this.d_index ||
376+
null === this.d_index.sha) &&
376377
(null === this.d_workdir ||
377378
null === this.d_workdir.status.headCommit &&
378379
this.d_workdir.status.isIndexClean()));

node/lib/util/rm.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -461,7 +461,23 @@ exports.rmPaths = co.wrap(function *(repo, paths, options) {
461461
}
462462
if (stat !== null) {
463463
if (stat.isDirectory()) {
464-
yield fs.rmdir(fullpath);
464+
try {
465+
yield fs.rmdir(fullpath);
466+
} catch (e) {
467+
if ("ENOTEMPTY" === e.code) {
468+
// Repo still exists for some reason --
469+
// perhaps it was reported as deleted
470+
// because it's not in the index, but it
471+
// does exist on disk. For safety, do not
472+
// delete it; this is a weird case.
473+
console.error(
474+
`Could not remove ${fullpath} -- it's not
475+
empty. If you are sure it is not needed, you can remove it yourself.`
476+
);
477+
} else {
478+
throw e;
479+
}
480+
}
465481
} else {
466482
yield fs.unlink(fullpath);
467483
}

node/lib/util/stash_util.js

Lines changed: 182 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,10 @@
3333
const assert = require("chai").assert;
3434
const co = require("co");
3535
const colors = require("colors");
36+
const fs = require("fs-promise");
3637
const NodeGit = require("nodegit");
3738

39+
const CloseUtil = require("./close_util");
3840
const ConfigUtil = require("./config_util");
3941
const DiffUtil = require("./diff_util");
4042
const GitUtil = require("./git_util");
@@ -44,11 +46,19 @@ const RepoStatus = require("./repo_status");
4446
const SparseCheckoutUtil = require("./sparse_checkout_util");
4547
const StatusUtil = require("./status_util");
4648
const SubmoduleUtil = require("./submodule_util");
49+
const SubmoduleConfigUtil = require("./submodule_config_util");
4750
const SubmoduleRebaseUtil = require("./submodule_rebase_util");
4851
const TreeUtil = require("./tree_util");
4952
const UserError = require("./user_error");
5053

5154
const Commit = NodeGit.Commit;
55+
const Change = TreeUtil.Change;
56+
const FILEMODE = NodeGit.TreeEntry.FILEMODE;
57+
58+
const MAGIC_DELETED_SHA = NodeGit.Oid.fromString(
59+
"de1e7ed0de1e7ed0de1e7ed0de1e7ed0de1e7ed0");
60+
61+
const GITMODULES = SubmoduleConfigUtil.modulesFileName;
5262

5363
/**
5464
* Return the IDs of tress reflecting the current state of the index and
@@ -107,6 +117,62 @@ exports.makeLogMessage = co.wrap(function *(repo) {
107117
WIP on ${branchDesc}: ${GitUtil.shortSha(head.id().tostrS())} ${message}`;
108118
});
109119

120+
121+
function getNewGitModuleSha(diff) {
122+
const numDeltas = diff.numDeltas();
123+
for (let i = 0; i < numDeltas; ++i) {
124+
const delta = diff.getDelta(i);
125+
// We assume that the user hasn't deleted the .gitmodules file.
126+
// That would be bonkers.
127+
const file = delta.newFile();
128+
const path = file.path();
129+
if (path === GITMODULES) {
130+
return delta.newFile().id();
131+
}
132+
}
133+
// diff does not include .gitmodules
134+
return null;
135+
}
136+
137+
138+
const stashGitModules = co.wrap(function *(repo, headTree) {
139+
assert.instanceOf(repo, NodeGit.Repository);
140+
141+
const result = {};
142+
// RepoStatus throws away the diff new sha, and rather than hack
143+
// it, since it's used all over the codebase, we'll just redo the
144+
// diffs for this one file.
145+
146+
const workdirToTreeDiff =
147+
yield NodeGit.Diff.treeToWorkdir(repo,
148+
headTree,
149+
{pathspec: [GITMODULES]});
150+
151+
152+
const newWorkdir = getNewGitModuleSha(workdirToTreeDiff);
153+
if (newWorkdir !== null) {
154+
result.workdir = newWorkdir;
155+
}
156+
157+
const indexToTreeDiff =
158+
yield NodeGit.Diff.treeToIndex(repo,
159+
headTree,
160+
yield repo.index(),
161+
{pathspec: [GITMODULES]});
162+
163+
const newIndex = getNewGitModuleSha(indexToTreeDiff);
164+
if (newIndex !== null) {
165+
result.staged = newIndex;
166+
}
167+
168+
yield NodeGit.Checkout.tree(repo, headTree, {
169+
paths: [GITMODULES],
170+
checkoutStrategy: NodeGit.Checkout.STRATEGY.FORCE,
171+
});
172+
173+
return result;
174+
});
175+
110176
/**
111177
* Save the state of the submodules in the specified, `repo` having the
112178
* specified `status` and clean the sub-repositories to match their respective
@@ -149,6 +215,8 @@ exports.save = co.wrap(function *(repo, status, includeUntracked, message) {
149215
const subRepos = {}; // name to submodule open repo
150216

151217
const sig = yield ConfigUtil.defaultSignature(repo);
218+
const head = yield repo.getHeadCommit();
219+
const headTree = yield head.getTree();
152220

153221
// First, we process the submodules. If a submodule is open and dirty,
154222
// we'll create the stash commits in its repo, populate `subResults` with
@@ -178,34 +246,42 @@ report this. Continuing stash anyway.`);
178246

179247
if (null === wd) {
180248
// closed submodule
181-
if (sub.commit.sha === sub.index.sha) {
182-
// ... with no staged changes
183-
return; // RETURN
184-
}
185-
// This is a case that regular git stash doesn't really have
186-
// to handle. In a normal stash commit, the tree points
187-
// to the working directory tree, but here, there is no working
188-
// directory. But if there were, we would want to have
189-
// this commit checked out.
190-
191-
const subRepo = yield SubmoduleUtil.getRepo(repo, name);
249+
if (sub.index === null || sub.index.sha === null) {
250+
// deleted submodule
251+
stashId = MAGIC_DELETED_SHA;
252+
yield NodeGit.Checkout.tree(repo, headTree, {
253+
paths: [name],
254+
checkoutStrategy: NodeGit.Checkout.STRATEGY.FORCE,
255+
});
256+
} else {
257+
if (sub.commit.sha === sub.index.sha) {
258+
// ... with no staged changes
259+
return; // RETURN
260+
}
261+
// This is a case that regular git stash doesn't really have
262+
// to handle. In a normal stash commit, the tree points
263+
// to the working directory tree, but here, there is no working
264+
// directory. But if there were, we would want to have
265+
// this commit checked out.
192266

193-
const subCommit = yield Commit.lookup(subRepo, sub.commit.sha);
194-
const indexCommit = yield Commit.lookup(subRepo, sub.index.sha);
195-
const indexTree = yield indexCommit.getTree();
196-
stashId = yield Commit.create(subRepo,
197-
null,
198-
sig,
199-
sig,
200-
null,
201-
"stash",
202-
indexTree,
203-
4,
204-
[subCommit,
205-
indexCommit,
206-
indexCommit,
207-
indexCommit]);
267+
const subRepo = yield SubmoduleUtil.getRepo(repo, name);
208268

269+
const subCommit = yield Commit.lookup(subRepo, sub.commit.sha);
270+
const indexCommit = yield Commit.lookup(subRepo, sub.index.sha);
271+
const indexTree = yield indexCommit.getTree();
272+
stashId = yield Commit.create(subRepo,
273+
null,
274+
sig,
275+
sig,
276+
null,
277+
"stash",
278+
indexTree,
279+
4,
280+
[subCommit,
281+
indexCommit,
282+
indexCommit,
283+
indexCommit]);
284+
}
209285
} else {
210286
// open submodule
211287
if (sub.commit.sha !== sub.index.sha &&
@@ -318,13 +394,42 @@ commit to have two parents`);
318394
subResults[name] = stashId.tostrS();
319395
// Record the values we've created.
320396

321-
subChanges[name] = new TreeUtil.Change(
322-
stashId,
323-
NodeGit.TreeEntry.FILEMODE.COMMIT);
397+
subChanges[name] = new TreeUtil.Change(stashId, FILEMODE.COMMIT);
324398
}));
325399

326-
const head = yield repo.getHeadCommit();
327-
const headTree = yield head.getTree();
400+
const parents = [head];
401+
402+
const gitModulesChanges = yield stashGitModules(repo, headTree);
403+
if (gitModulesChanges) {
404+
if (gitModulesChanges.workdir) {
405+
subChanges[GITMODULES] = new Change(gitModulesChanges.workdir,
406+
FILEMODE.BLOB);
407+
}
408+
if (gitModulesChanges.staged) {
409+
const indexChanges = {};
410+
Object.assign(indexChanges, subChanges);
411+
412+
indexChanges[GITMODULES] = new Change(gitModulesChanges.staged,
413+
FILEMODE.BLOB);
414+
415+
416+
const indexTree = yield TreeUtil.writeTree(repo, headTree,
417+
indexChanges);
418+
const indexParent = yield Commit.create(repo,
419+
null,
420+
sig,
421+
sig,
422+
null,
423+
"stash",
424+
indexTree,
425+
1,
426+
[head]);
427+
428+
const indexParentCommit = yield Commit.lookup(repo, indexParent);
429+
parents.push(indexParentCommit);
430+
}
431+
}
432+
328433
const subsTree = yield TreeUtil.writeTree(repo, headTree, subChanges);
329434
const stashId = yield Commit.create(repo,
330435
null,
@@ -333,8 +438,8 @@ commit to have two parents`);
333438
null,
334439
"stash",
335440
subsTree,
336-
1,
337-
[head]);
441+
parents.length,
442+
parents);
338443

339444
const stashSha = stashId.tostrS();
340445

@@ -438,19 +543,63 @@ exports.apply = co.wrap(function *(repo, id, reinstateIndex) {
438543
assert.isString(id);
439544

440545
const commit = yield repo.getCommit(id);
546+
const repoIndex = yield repo.index();
441547

442548
// TODO: patch libgit2/nodegit: the commit object returned from `parent`
443549
// isn't properly configured with a `repo` object, and attempting to use it
444550
// in `getSubmodulesForCommit` will fail, so we have to look it up.
445551

446552
const parentId = (yield commit.parent(0)).id();
447553
const parent = yield repo.getCommit(parentId);
554+
const parentTree = yield parent.getTree();
555+
448556
const baseSubs = yield SubmoduleUtil.getSubmodulesForCommit(repo,
449557
parent,
450558
null);
559+
560+
let indexSubs = baseSubs;
561+
if (commit.parentcount() > 1) {
562+
const parent2Id = (yield commit.parent(1)).id();
563+
const parent2 = yield repo.getCommit(parent2Id);
564+
indexSubs = yield SubmoduleUtil.getSubmodulesForCommit(repo,
565+
parent2,
566+
null);
567+
}
568+
451569
const newSubs = yield SubmoduleUtil.getSubmodulesForCommit(repo,
452570
commit,
453571
null);
572+
573+
const toDelete = [];
574+
yield Object.keys(baseSubs).map(co.wrap(function *(name) {
575+
if (newSubs[name] === undefined) {
576+
if (fs.existsSync(name)) {
577+
// sub deleted in working tree
578+
toDelete.push(name);
579+
}
580+
}
581+
}));
582+
583+
CloseUtil.close(repo, repo.workdir(), toDelete, false);
584+
for (const name of toDelete) {
585+
yield fs.rmdir(name);
586+
}
587+
588+
yield Object.keys(baseSubs).map(co.wrap(function *(name) {
589+
if (indexSubs[name] === undefined) {
590+
// sub deleted in the index
591+
yield repoIndex.removeByPath(name);
592+
}
593+
}));
594+
595+
// apply gitmodules diff
596+
const headTree = yield commit.getTree();
597+
yield NodeGit.Checkout.tree(repo, headTree, {
598+
paths: [GITMODULES],
599+
baseline: parentTree,
600+
checkoutStrategy: NodeGit.Checkout.STRATEGY.MERGE,
601+
});
602+
454603
const opener = new Open.Opener(repo, null);
455604
let result = {};
456605
const index = {};
@@ -550,8 +699,6 @@ for debugging, is:`, e);
550699
}
551700
}));
552701

553-
const repoIndex = yield repo.index();
554-
555702
if (null !== result) {
556703
for (let name of Object.keys(index)) {
557704
const entry = new NodeGit.IndexEntry();

node/lib/util/status_util.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -247,8 +247,8 @@ exports.getSubmoduleStatus = co.wrap(function *(repo,
247247
commit = new Submodule.Commit(commitSha, commitUrl);
248248
}
249249

250-
// A null indexUrl indicates that the submodule was removed. If that is
251-
// the case, we're done.
250+
// A null indexUrl indicates that the submodule doesn't exist in
251+
// the staged .gitmodules.
252252

253253
if (null === indexUrl) {
254254
return new Submodule({ commit: commit }); // RETURN

0 commit comments

Comments
 (0)