Repository navigation
Implement composite Git strategy - #538
Conversation
This replaces the shelling out version (which was writing to the index) with a read-only implementation, allowing us to parallelize this operation and improve performance 🐎
smashwilson
left a comment
There was a problem hiding this comment.
This is super cool and a big validation of the Strategy pattern for git operations 🎉
The metaprogramming isn't nearly as scary as advertised, either; I've done worse myself in production code 😉 I'd recommend maybe a few more comments to document it maybe, especially around the tests?
|
|
||
| import {GitProcess} from 'git-kitchen-sink'; | ||
| import {parse as parseDiff} from 'what-the-diff'; | ||
| import Git from 'nodegit'; |
There was a problem hiding this comment.
I was actually going to comment on the inappropriateness of the name GitShellOutStrategy in #524, by the way 😆
| * Takes an array of targets and returns a proxy. The proxy intercepts property accessor calls and | ||
| * returns the value of that property on the first object in `targets` where the target implements that property. | ||
| */ | ||
| export function firstImplementer(...targets) { |
There was a problem hiding this comment.
This isn't nearly as scary as you made it sound 😉
| const STATUS = Git.Status.STATUS; | ||
| const index = await repo.index(); | ||
|
|
||
| /* eslint-disable no-bitwise */ |
There was a problem hiding this comment.
That's kind of a weird eslint rule. I guess bitwise operations are rare enough that they assume it's a typo for boolean operations?
| function createTestStrategyForPropertyUnderTest(...args) { | ||
| const compositeStrategy = CompositeGitStrategy.create(...args); | ||
| const originalStrategy = new Strategy(...args); | ||
| return new Proxy(compositeStrategy, { |
There was a problem hiding this comment.
Okay, this bit gets a little hairy 😆 I think I still follow, though.
| operations.forEach(op => { | ||
| it(`temporarily overrides gpg.program when ${op.progressiveTense}`, async () => { | ||
| // if (op !== 'pulling') { return; } | ||
| console.log('========================'); |
There was a problem hiding this comment.
Ah yes — the pull tests were failing (still are IIRC) and I was trying to figure out why
8463f26 to
57887e3
Compare
smashwilson
left a comment
There was a problem hiding this comment.
I definitely prefer the chained methods to the single method with four parameters 👍 Much more readable.
| }); | ||
| } else { | ||
| return describeMethod(description, function() { | ||
| it('is not implemented', function() {}); |
There was a problem hiding this comment.
Hmm yeah I guess it was misleading to have these as "pending" tests, considering that we wouldn't actually intend to implement many of them anyway.
|
|
||
| [GitShellOutStrategy, NodeGitStrategy].forEach(function(Strategy) { | ||
| forStrategy(Strategy, function(forCommand) { | ||
| describe.only(`Git commands for ${Strategy.name}`, function() { |
| }); | ||
|
|
||
| it('displays copied files as an added file', async function() { | ||
| // This repo is carefully constructed after much experimentation |
There was a problem hiding this comment.
Indentation looks a touch off on these lines 🎨
|
Since #524 is closed, I've set this PR's base to |
| getPrototypeOf(target) { | ||
| return targets.reduce((acc, t) => { | ||
| return Object.create(acc, descriptorsFromProto(Object.getPrototypeOf(t))); | ||
| }, Object.prototype); |
There was a problem hiding this comment.
This was surprisingly tricky to work out! Huge thanks to @kuychaco who rocked this.
| ].forEach(function(strategies) { | ||
| const createTestStrategy = (...args) => { | ||
| return CompositeGitStrategy.withStrategies(strategies)(...args); | ||
| }; |
There was a problem hiding this comment.
@smashwilson As I was reviewing this with @kuychaco, she recommended this much simpler (and much improved, IMO) strategy. Thoughts?
There was a problem hiding this comment.
Hmm! Definitely much cleaner.
Let's see. We lose the ability to have the test suite identify which methods are implemented on each strategy... but we gain higher fidelity to the way the strategies will actually be used by the package. 👍
To go even further - in what situation would we want to run the suite with different combinations of strategies in one run? Presumably the package will use a fixed strategy configuration at any given time, so as long as we stay consistent with that, these tests will exercise what they're supposed to exercise. Maybe we don't even need the outer loop!
There was a problem hiding this comment.
@smashwilson That's a really good question! And you're right, in the end we'll only use a specific order for the production strategy. The point of these tests was specifically to unit test each individual strategy's methods as much as possible; however, to do so, we may need to use methods that only exist on the other strategy (such as git.exec to set configs or do other test setup). That's why each isn't tested in complete isolation, but as part of a composite strategy.
| action: () => git.pull('some-branch'), | ||
| configureStub: (stub, gitStrategy) => { | ||
| sinon.stub(gitStrategy, 'getRemoteForBranch').returns(Promise.resolve('origin')); | ||
| }, |
There was a problem hiding this comment.
@smashwilson We also changed this up a bit.
#524 experiments with utilizing nodegit for some operations; in order to more easily modify and switch between implementations (perhaps even dynamically at runtime), it'd be nice to have both implementations in the codebase in a fully complete state at all times. This PR reverts
GitShellOutStrategyto use pure shelling out and addsNodegitStrategywhich implements some operations using nodegit.Since the nodegit strategy doesn't implement all the Git operations, this PR also introduces a new helper called
firstImplementerwhich takes an array of receivers and returns an object that intercepts property accessors and returns the property value from the first receiver that contains that property. An example may help:A new strategy called
CompositeGitStrategyuses this helper to automatically create both the underlying Git strategies, creates a shared async queue for both of them, and routes method calls to the appropriate strategy as approprite.The
GitShellOutStrategytests have been converted toGitStrategiesTest, which tests all strategies, and which uses even more magic to only test the methods that are implemented on the strategy under test. There is some complexity here because even when we're testing the nodegit strategy, we often need methods the strategy doesn't implement for supporting calls (like callingexecto set local config). So, the tests are implemented using a special version of the composite strategy which ensures that the method under test is called on the strategy under test, but lets other method calls be fulfilled by either strategy. This code is under heavy construction. 🚧The other downside to this... er, strategy, is that it's implemented via a Proxy, and since sinon does it's own metaprogrammy stuff to create stubs and mocks, they're not playing super well together. I'm still working on this as well.