Merge pull request #5457 from xiaow6/master

translated by xiaow6
This commit is contained in:
Flynn 2017-04-15 11:46:28 +08:00 committed by GitHub
commit f29a981659
2 changed files with 682 additions and 726 deletions

View File

@ -1,726 +0,0 @@
tranlating by xiaow6
Git in 2016
============================================================
![](https://cdn-images-1.medium.com/max/2000/1*1SiSsLMsNSyAk6khb63W9g.png)
Git had a  _huge_  year in 2016, with five feature releases[¹][57] ( _v2.7_  through  _v2.11_ ) and sixteen patch releases[²][58]. 189 authors[³][59] contributed 3,676 commits[⁴][60] to `master`, which is up 15%[⁵][61] over 2015! In total, 1,545 files were changed with 276,799 lines added and 100,973 lines removed[⁶][62].
However, commit counts and LOC are pretty terrible ways to measure productivity. Until deep learning develops to the point where it can qualitatively grok code, were going to be stuck with human judgment as the arbiter of productivity.
With that in mind, I decided to put together a retrospective of sorts that covers changes improvements made to six of my favorite Git features over the course of the year. This article is pretty darn long for a Medium post, so I will forgive you if you want to skip ahead to a feature that particularly interests you:
* [Rounding out the ][41]`[git worktree][25]`[ command][42]
* [More convenient ][43]`[git rebase][26]`[ options][44]
* [Dramatic performance boosts for ][45]`[git lfs][27]`
* [Experimental algorithms and better defaults for ][46]`[git diff][28]`
* `[git submodules][29]`[ with less suck][47]
* [Nifty enhancements to ][48]`[git stash][30]`
Before we begin, note that many operating systems ship with legacy versions of Git, so its worth checking that youre on the latest and greatest. If running `git --version` from your terminal returns anything less than Git `v2.11.0`, head on over to Atlassian's quick guide to [upgrade or install Git][63] on your platform of choice.
### [`Citation` needed]
One more quick stop before we jump into the qualitative stuff: I thought Id show you how I generated the statistics from the opening paragraph (and the rather over-the-top cover image). You can use the commands below to do a quick  _year in review_  for your own repositories as well!
```
¹ Tags from 2016 matching the form vX.Y.0
```
```
$ git for-each-ref --sort=-taggerdate --format \
'%(refname) %(taggerdate)' refs/tags | grep "v\d\.\d*\.0 .* 2016"
```
```
² Tags from 2016 matching the form vX.Y.Z
```
```
$ git for-each-ref --sort=-taggerdate --format '%(refname) %(taggerdate)' refs/tags | grep "v\d\.\d*\.[^0] .* 2016"
```
```
³ Commits by author in 2016
```
```
$ git shortlog -s -n --since=2016-01-01 --until=2017-01-01
```
```
⁴ Count commits in 2016
```
```
$ git log --oneline --since=2016-01-01 --until=2017-01-01 | wc -l
```
```
⁵ ... and in 2015
```
```
$ git log --oneline --since=2015-01-01 --until=2016-01-01 | wc -l
```
```
⁶ Net LOC added/removed in 2016
```
```
$ git diff --shortstat `git rev-list -1 --until=2016-01-01 master` \
`git rev-list -1 --until=2017-01-01 master`
```
The commands above were are run on Gits `master` branch, so dont represent any unmerged work on outstanding branches. If you use these command, remember that commit counts and LOC are not metrics to live by. Please dont use them to rate the performance of your teammates!
And now, on with the retrospective…
### Rounding out Git worktrees
The `git worktree` command first appeared in Git v2.5 but had some notable enhancements in 2016\. Two valuable new features were introduced in v2.7the `list` subcommand, and namespaced refs for bisectingand the `lock`/`unlock` subcommands were implemented in v2.10.
#### Whats a worktree again?
The `[git worktree][49]` command lets you check out and work on multiple repository branches in separate directories simultaneously. For example, if you need to make a quick hotfix but don't want to mess with your working copy, you can check out a new branch in a new directory with:
```
$ git worktree add -b hotfix/BB-1234 ../hotfix/BB-1234
Preparing ../hotfix/BB-1234 (identifier BB-1234)
HEAD is now at 886e0ba Merged in bedwards/BB-13430-api-merge-pr (pull request #7822)
```
Worktrees arent just for branches. You can check out multiple tags as different worktrees in order to build or test them in parallel. For example, I created worktrees from the Git v2.6 and v2.7 tags in order to examine the behavior of different versions of Git:
```
$ git worktree add ../git-v2.6.0 v2.6.0
Preparing ../git-v2.6.0 (identifier git-v2.6.0)
HEAD is now at be08dee Git 2.6
```
```
$ git worktree add ../git-v2.7.0 v2.7.0
Preparing ../git-v2.7.0 (identifier git-v2.7.0)
HEAD is now at 7548842 Git 2.7
```
```
$ git worktree list
/Users/kannonboy/src/git 7548842 [master]
/Users/kannonboy/src/git-v2.6.0 be08dee (detached HEAD)
/Users/kannonboy/src/git-v2.7.0 7548842 (detached HEAD)
```
```
$ cd ../git-v2.7.0 && make
```
You could use the same technique to build and run different versions of your own applications side-by-side.
#### Listing worktrees
The `git worktree list` subcommand (introduced in Git v2.7) displays all of the worktrees associated with a repository:
```
$ git worktree list
/Users/kannonboy/src/bitbucket/bitbucket 37732bd [master]
/Users/kannonboy/src/bitbucket/staging d5924bc [staging]
/Users/kannonboy/src/bitbucket/hotfix-1234 37732bd [hotfix/1234]
```
#### Bisecting worktrees
`[git bisect][50]` is a neat Git command that lets you perform a binary search of your commit history. It's usually used to find out which commit introduced a particular regression. For example, if a test is failing on the tip commit of my `master` branch, I can use `git bisect` to traverse the history of my repository looking for the commit that first broke it:
```
$ git bisect start
```
```
# indicate the last commit known to be passing the tests
# (e.g. the latest release tag)
$ git bisect good v2.0.0
```
```
# indicate a known broken commit (e.g. the tip of master)
$ git bisect bad master
```
```
# tell git bisect a script/command to run; git bisect will
# find the oldest commit between "good" and "bad" that causes
# this script to exit with a non-zero status
$ git bisect run npm test
```
Under the hood, bisect uses refs to track the good and bad commits used as the upper and lower bounds of the binary search range. Unfortunately for worktree fans, these refs were stored under the generic `.git/refs/bisect`namespace, meaning that `git bisect` operations that are run in different worktrees could interfere with each other.
As of v2.7, the bisect refs have been moved to`.git/worktrees/$worktree_name/refs/bisect`, so you can run bisect operations concurrently across multiple worktrees.
#### Locking worktrees
When youre finished with a worktree, you can simply delete it and then run `git worktree prune` or wait for it to be garbage collected automatically. However, if you're storing a worktree on a network share or removable media, then it will be cleaned up if the worktree directory isn't accessible during pruningwhether you like it or not! Git v2.10 introduced the `git worktree lock` and `unlock` subcommands to prevent this from happening:
```
# to lock the git-v2.7 worktree on my USB drive
$ git worktree lock /Volumes/Flash_Gordon/git-v2.7 --reason \
"In case I remove my removable media"
```
```
# to unlock (and delete) the worktree when I'm finished with it
$ git worktree unlock /Volumes/Flash_Gordon/git-v2.7
$ rm -rf /Volumes/Flash_Gordon/git-v2.7
$ git worktree prune
```
The `--reason` flag lets you leave a note for your future self, describing why the worktree is locked. `git worktree unlock` and `lock` both require you to specify the path to the worktree. Alternatively, you can `cd` to the worktree directory and run `git worktree lock .` for the same effect.
### More Git r`ebase` options
In March, Git v2.8 added the ability to interactively rebase whilst pulling with a `git pull --rebase=interactive`. Conversely, June's Git v2.9 release implemented support for performing a rebase exec without needing to drop into interactive mode via `git rebase -x`.
#### Re-wah?
Before we dive in, I suspect there may be a few readers who arent familiar or completely comfortable with the rebase command or interactive rebasing. Conceptually, its pretty simple, but as with many of Gits powerful features, the rebase is steeped in some complex-sounding terminology. So, before we dive in, lets quickly review what a rebase is.
Rebasing means rewriting one or more commits on a particular branch. The `git rebase` command is heavily overloaded, but the name rebase originates from the fact that it is often used to change a branch's base commit (the commit that you created the branch from).
Conceptually, rebase unwinds the commits on your branch by temporarily storing them as a series of patches, and then reapplying them in order on top of the target commit.
![](https://cdn-images-1.medium.com/max/800/1*mgyl38slmqmcE4STS56nXA.gif)
Rebasing a feature branch on master (`git rebase master`) is a great way to "freshen" your feature branch with the latest changes from master. For long-lived feature branches, regular rebasing minimizes the chance and severity of conflicts down the road.
Some teams also choose to rebase immediately before merging their changes onto master in order to achieve a fast-forward merge (`git merge --ff <feature>` ). Fast-forwarding merges your commits onto master by simply making the master ref point at the tip of your rewritten branch without creating a merge commit:
![](https://cdn-images-1.medium.com/max/800/1*QXa3znQiuNWDjxroX628VA.gif)
Rebasing is so convenient and powerful that it has been baked into some other common Git commands, such as `git pull`. If you have some unpushed changes on your local master branch, running `git pull` to pull your teammates' changes from the origin will create an unnecessary merge commit:
![](https://cdn-images-1.medium.com/max/800/1*IxDdJ5CygvSWdD8MCNpZNg.gif)
This is kind of messy, and on busy teams, youll get heaps of these unnecessary merge commits. `git pull --rebase` rebases your local changes on top of your teammates' without creating a merge commit:
![](https://cdn-images-1.medium.com/max/800/1*HcroDMwBE9m21-hOeIwRmw.gif)
This is pretty neat! Even cooler, Git v2.8 introduced a feature that lets you rebase  _interactively_  whilst pulling.
#### Interactive rebasing
Interactive rebasing is a more powerful form of rebasing. Like a standard rebase, it rewrites commits, but it also gives you a chance to modify them interactively as they are reapplied onto the new base.
When you run `git rebase --interactive` (or `git pull --rebase=interactive`), you'll be presented with a list of commits in your text editor of choice:
```
$ git rebase master --interactive
```
```
pick 2fde787 ACE-1294: replaced miniamalCommit with string in test
pick ed93626 ACE-1294: removed pull request service from test
pick b02eb9a ACE-1294: moved fromHash, toHash and diffType to batch
pick e68f710 ACE-1294: added testing data to batch email file
```
```
# Rebase f32fa9d..0ddde5f onto f32fa9d (4 commands)
#
# Commands:
# p, pick = use commit
# r, reword = use commit, but edit the commit message
# e, edit = use commit, but stop for amending
# s, squash = use commit, but meld into previous commit
# f, fixup = like "squash", but discard this commit's log message
# x, exec = run command (the rest of the line) using shell
# d, drop = remove commit
#
# These lines can be re-ordered; they are executed from top to
# bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
```
Notice that each commit has the word `pick` next to it. That's rebase-speak for, "Keep this commit as-is." If you quit your text editor now, it will perform a normal rebase as described in the last section. However, if you change `pick` to `edit` or one of the other rebase commands, rebase will let you mutate the commit before it is reapplied! There are several available rebase commands:
* `reword`: Edit the commit message.
* `edit`: Edit the files that were committed.
* `squash`: Combine the commit with the previous commit (the one above it in the file), concatenating the commit messages.
* `fixup`: Combine the commit with the commit above it, and uses the previous commit's log message verbatim (this is handy if you created a second commit for a small change that should have been in the original commit, i.e., you forgot to stage a file).
* `exec`: Run an arbitrary shell command (we'll look at a neat use-case for this later, in the next section).
* `drop`: This kills the commit.
You can also reorder commits within the file, which changes the order in which theyre reapplied. This is handy if you have interleaved commits that are addressing different topics and you want to use `squash` or `fixup` to combine them into logically atomic commits.
Once youve set up the commands and saved the file, Git will iterate through each commit, pausing at each `reword` and `edit` for you to make your desired changes and automatically applying any `squash`, `fixup`, `exec`, and `drop` commands for you.
#### Non-interactive exec
When you rebase, youre essentially rewriting history by applying each of your new commits on top of the specified base. `git pull --rebase` can be a little risky because depending on the nature of the changes from the upstream branch, you may encounter test failures or even compilation problems for certain commits in your newly created history. If these changes cause merge conflicts, the rebase process will pause and allow you to resolve them. However, changes that merge cleanly may still break compilation or tests, leaving broken commits littering your history.
However, you can instruct Git to run your projects test suite for each rewritten commit. Prior to Git v2.9, you could do this with a combination of `git rebase interactive` and the `exec` command. For example, this:
```
$ git rebase master interactive exec=”npm test”
```
…would generate an interactive rebase plan that invokes `npm test` after rewriting each commit, ensuring that your tests still pass:
```
pick 2fde787 ACE-1294: replaced miniamalCommit with string in test
exec npm test
pick ed93626 ACE-1294: removed pull request service from test
exec npm test
pick b02eb9a ACE-1294: moved fromHash, toHash and diffType to batch
exec npm test
pick e68f710 ACE-1294: added testing data to batch email file
exec npm test
```
```
# Rebase f32fa9d..0ddde5f onto f32fa9d (4 command(s))
```
In the event that a test fails, rebase will pause to let you fix the tests (and apply your changes to that commit):
```
291 passing
1 failing
```
```
1) Host request “after all” hook:
Uncaught Error: connect ECONNRESET 127.0.0.1:3001
npm ERR! Test failed.
Execution failed: npm test
You can fix the problem, and then run
git rebase continue
```
This is handy, but needing to do an interactive rebase is a bit clunky. As of Git v2.9, you can perform a non-interactive rebase exec, with:
```
$ git rebase master -x “npm test”
```
Just replace `npm test` with `make`, `rake`, `mvn clean install`, or whatever you use to build and test your project.
#### A word of warning
Just like in the movies, rewriting history is risky business. Any commit that is rewritten as part of a rebase will have its SHA-1 ID changed, which means that Git will treat it as a totally different commit. If rewritten history is mixed with the original history, youll get duplicate commits, which can cause a lot of confusion for your team.
To avoid this problem, you only need to follow one simple rule:
> _Never rebase a commit that youve already pushed!_
Stick to that and youll be fine.
### Performance boosts for `Git LFS`
[Git is a distributed version control system][64], meaning the entire history of the repository is transferred to the client during the cloning process. For projects that contain large filesparticularly large files that are modified regularly __ the initial clone can be expensive, as every version of every file has to be downloaded by the client. [Git LFS (Large File Storage)][65] is a Git extension developed by Atlassian, GitHub, and a few other open source contributors that reduces the impact of large files in your repository by downloading the relevant versions of them lazily. Specifically, large files are downloaded as needed during the checkout process rather than during cloning or fetching.
Alongside Gits five huge releases in 2016, Git LFS had four feature-packed releases of its own: v1.2 through v1.5. You could write a retrospective series on Git LFS in its own right, but for this article, Im going to focus on one of the most important themes tackled in 2016: speed. A series of improvements to both Git and Git LFS have greatly improved the performance of transferring files to and from the server.
#### Long-running filter processes
When you `git add` a file, Git's system of clean filters can be used to transform the files contents before being written to the Git object store. Git LFS reduces your repository size by using a clean filter to squirrel away large file content in the LFS cache and adds a tiny “pointer” file to the Git object store instead.
![](https://cdn-images-1.medium.com/max/800/0*Ku328eca7GLOo7sS.png)
Smudge filters are the opposite of clean filtershence the name. When file content is read from the Git object store during a `git checkout`, smudge filters have a chance to transform it before its written to the users working copy. The Git LFS smudge filter transforms pointer files by replacing them with the corresponding large file, either from your LFS cache or by reading through to your Git LFS store on Bitbucket.
![](https://cdn-images-1.medium.com/max/800/0*CU60meE1lbCuivn7.png)
Traditionally, smudge and clean filter processes were invoked once for each file that was being added or checked out. So, a project with 1,000 files tracked by Git LFS invoked the `git-lfs-smudge` command 1,000 times for a fresh checkout! While each operation is relatively quick, the overhead of spinning up 1,000 individual smudge processes is costly.
As of Git v2.11 (and Git LFS v1.5), smudge and clean filters can be defined as long-running processes that are invoked once for the first filtered file, then fed subsequent files that need smudging or cleaning until the parent Git operation exits. [Lars Schneider][66], who contributed long-running filters to Git, neatly summarized the impact of the change on Git LFS performance:
> The filter process is 80x faster on macOS and 58x faster on Windows for the test repo with 12k files. On Windows, that means the tests runs in 57 seconds instead of 55 minutes!
Thats a seriously impressive performance gain!
#### Specialized LFS clones
Long-running smudge and clean filters are great for speeding up reads and writes to the local LFS cache, but they do little to speed up transferring of large objects to and from your Git LFS server. Each time the Git LFS smudge filter cant find a file in the local LFS cache, it has to make two HTTP calls to retrieve it: one to locate the file and one to download it. During a `git clone`, your local LFS cache is empty, so Git LFS will naively make two HTTP calls for every LFS tracked file in your repository:
![](https://cdn-images-1.medium.com/max/800/0*ViL7r3ZhkGvF0z3-.png)
Fortunately, Git LFS v1.2 shipped the specialized `[git lfs clone][51]` command. Rather than downloading files one at a time; `git lfs clone` disables the Git LFS smudge filter, waits until the checkout is complete, and then downloads any required files as a batch from the Git LFS store. This allows downloads to be parallelized and halves the number of required HTTP requests:
![](https://cdn-images-1.medium.com/max/800/0*T43VA0DYTujDNgkH.png)
### Custom Transfer Adapters
As discussed earlier, Git LFS shipped support for long running filter processes in v1.5\. However, support for another type of pluggable process actually shipped earlier in the year. Git LFS v1.3 included support for pluggable transfer adapters so that different Git LFS hosting services could define their own protocols for transferring files to and from LFS storage.
As of the end of 2016, Bitbucket is the only hosting service to implement their own Git LFS transfer protocol via the [Bitbucket LFS Media Adapter][67]. This was done to take advantage of a unique feature of Bitbuckets LFS storage API called chunking. Chunking means large files are broken down into 4MB chunks before uploading or downloading.
![](https://cdn-images-1.medium.com/max/800/1*N3SpjQZQ1Ge8OwvWrtS1og.gif)
Chunking gives Bitbuckets Git LFS support three big advantages:
1. Parallelized downloads and uploads. By default, Git LFS transfers up to three files in parallel. However, if only a single file is being transferred (which is the default behavior of the Git LFS smudge filter), it is transferred via a single stream. Bitbuckets chunking allows multiple chunks from the same file to be uploaded or downloaded simultaneously, often dramatically improving transfer speed.
2. Resumable chunk transfers. File chunks are cached locally, so if your download or upload is interrupted, Bitbuckets custom LFS media adapter will resume transferring only the missing chunks the next time you push or pull.
3. Deduplication. Git LFS, like Git itself, is content addressable; each LFS file is identified by a SHA-256 hash of its contents. So, if you flip a single bit, the files SHA-256 changes and you have to re-upload the entire file. Chunking allows you to re-upload only the sections of the file that have actually changed. To illustrate, imagine we have a 41MB spritesheet for a video game tracked in Git LFS. If we add a new 2MB layer to the spritesheet and commit it, wed typically need to push the entire new 43MB file to the server. However, with Bitbuckets custom transfer adapter, we only need to push ~7Mb: the first 4MB chunk (because the files header information will have changed) and the last 3MB chunk containing the new layer weve just added! The other unchanged chunks are skipped automatically during the upload process, saving a huge amount of bandwidth and time.
Customizable transfer adapters are a great feature for Git LFS, as they allow different hosts to experiment with optimized transfer protocols to suit their services without overloading the core project.
### Better `git diff` algorithms and defaults
Unlike some other version control systems, Git doesnt explicitly store the fact that files have been renamed. For example, if I edited a simple Node.js application and renamed `index.js` to `app.js` and then ran `git diff`, Id get back what looks like a file deletion and an addition:
![](https://cdn-images-1.medium.com/max/800/1*ohMUBpSh_jqz2ffScJ7ApQ.png)
I guess moving or renaming a file is technically just a delete followed by an add, but this isnt the most human-friendly way to show it. Instead, you can use the `-M` flag to instruct Git to attempt to detect renamed files on the fly when computing a diff. For the above example, `git diff -M` gives us:
![](https://cdn-images-1.medium.com/max/800/1*ywYjxBc1wii5O8EhHbpCTA.png)
The similarity index on the second line tells us how similar the content of the files compared was. By default, `-M` will consider any two files that are more than 50% similar. That is, you need to modify less than 50% of their lines to make them identical as a renamed file. You can choose your own similarity index by appending a percentage, i.e., `-M80%`.
As of Git v2.9, the `git diff` and `git log` commands will both detect renames by default as if you'd passed the `-M` flag. If you dislike this behavior (or, more realistically, are parsing the diff output via a script), then you can disable it by explicitly passing the `no-renames` flag.
#### Verbose Commits
Do you ever invoke `git commit` and then stare blankly at your shell trying to remember all the changes you just made? The verbose flag is for you!
Instead of:
```
Ah crap, which dependency did I just rev?
```
```
# Please enter the commit message for your changes. Lines starting
# with # will be ignored, and an empty message aborts the commit.
# On branch master
# Your branch is up-to-date with origin/master.
#
# Changes to be committed:
# new file: package.json
#
```
…you can invoke `git commit verbose` to view an inline diff of your changes. Dont worry, it wont be included in your commit message:
![](https://cdn-images-1.medium.com/max/800/1*1vOYE2ow3ZDS8BP_QfssQw.png)
The `verbose` flag isnt new, but as of Git v2.9 you can enable it permanently with `git config --global commit.verbose true`.
#### Experimental Diff Improvements
`git diff` can produce some slightly confusing output when the lines before and after a modified section are the same. This can happen when you have two or more similarly structured functions in a file. For a slightly contrived example, imagine we have a JS file that contains a single function:
```
/* @return {string} "Bitbucket" */
function productName() {
return "Bitbucket";
}
```
Now imagine weve committed a change that prepends  _another_  function that does something similar:
```
/* @return {string} "Bitbucket" */
function productId() {
return "Bitbucket";
}
```
```
/* @return {string} "Bitbucket" */
function productName() {
return "Bitbucket";
}
```
Youd expect `git diff` to show the top five lines as added, but it actually incorrectly attributes the very first line to the original commit:
![](https://cdn-images-1.medium.com/max/800/1*9C7DWMObGHMEqD-QFGHmew.png)
The wrong comment is included in the diff! Not the end of the world, but the couple of seconds of cognitive overhead from the  _Whaaat?_  every time this happens can add up.
In December, Git v2.11 introduced a new experimental diff option, `--indent-heuristic`, that attempts to produce more aesthetically pleasing diffs:
![](https://cdn-images-1.medium.com/max/800/1*UyWZ6JjC-izDquyWCA4bow.png)
Under the hood, `--indent-heuristic` cycles through the possible diffs for each change and assigns each a “badness” score. This is based on heuristics like whether the diff block starts and ends with different levels of indentation (which is aesthetically bad) and whether the diff block has leading and trailing blank lines (which is aesthetically pleasing). Then, the block with the lowest badness score is output.
This feature is experimental, but you can test it out ad-hoc by applying the `--indent-heuristic` option to any `git diff` command. Or, if you like to live on the bleeding edge, you can enable it across your system with:
```
$ git config --global diff.indentHeuristic true
```
### Submodules with less suck
Submodules allow you to reference and include other Git repositories from inside your Git repository. This is commonly used by some projects to manage source dependencies that are also tracked in Git, or by some companies as an alternative to a [monorepo][68] containing a collection of related projects.
Submodules get a bit of a bad rap due to some usage complexities and the fact that its reasonably easy to break them with an errant command.
![](https://cdn-images-1.medium.com/max/800/1*xNffiElY7BZNMDM0jm0JNQ.gif)
However, they do have their uses and are, I think, still the best choice for vendoring dependencies. Fortunately, 2016 was a great year to be a submodule user, with some significant performance and feature improvements landing across several releases.
#### Parallelized fetching
When cloning or fetching a repository, appending the `--recurse-submodules`option means any referenced submodules will be cloned or updated, as well. Traditionally, this was done serially, with each submodule being fetched one at a time. As of Git v2.8, you can append the `--jobs=n` option to fetch submodules in  _n_  parallel threads.
I recommend configuring this option permanently with:
```
$ git config --global submodule.fetchJobs 4
```
…or whatever degree of parallelization you choose to use.
#### Shallow submodules
Git v2.9 introduced the `git clone -shallow-submodules` flag. It allows you to grab a full clone of your repository and then recursively shallow clone any referenced submodules to a depth of one commit. This is useful if you dont need the full history of your projects dependencies.
For example, consider a repository with a mixture of submodules containing vendored dependencies and other projects that you own. You may wish to clone with shallow submodules initially and then selectively deepen the few projects you want to work with.
Another scenario would be configuring a continuous integration or deployment job. Git needs the super repository as well as the latest commit from each of your submodules in order to actually perform the build. However, you probably dont need the full history for every submodule, so retrieving just the latest commit will save you both time and bandwidth.
#### Submodule alternates
The `--reference` option can be used with `git clone` to specify another local repository as an alternate object store to save recopying objects over the network that you already have locally. The syntax is:
```
$ git clone --reference <local repo> <url>
```
As of Git v2.11, you can use the `--reference` option in combination with `--recurse-submodules` to set up submodule alternates pointing to submodules from another local repository. The syntax is:
```
$ git clone --recurse-submodules --reference <local repo> <url>
```
This can potentially save a huge amount of bandwidth and local disk but it will fail if the referenced local repository does not have all the required submodules of the remote repository that youre cloning from.
Fortunately, the handy `--reference-if-able` option will fail gracefully and fall back to a normal clone for any submodules that are missing from the referenced local repository:
```
$ git clone --recurse-submodules --reference-if-able \
<local repo> <url>
```
#### Submodule diffs
Prior to Git v2.11, Git had two modes for displaying diffs of commits that updated your repositorys submodules:
`git diff --submodule=short` displays the old commit and new commit from the submodule referenced by your project (this is also the default if you omit the `--submodule` option altogether):
![](https://cdn-images-1.medium.com/max/800/1*K71cJ30NokO5B69-a470NA.png)
`git diff --submodule=log` is slightly more verbose, displaying the summary line from the commit message of any new or removed commits in the updated submodule:
![](https://cdn-images-1.medium.com/max/800/1*frvsd_T44De8_q0uvNHB1g.png)
Git v2.11 introduces a third much more useful option: `--submodule=diff`. This displays a full diff of all changes in the updated submodule:
![](https://cdn-images-1.medium.com/max/800/1*nPhJTjP8tcJ0cD8s3YOmjw.png)
### Nifty enhancements to `git stash`
Unlike submodules, `[git stash][52]` is almost universally beloved by Git users. `git stash` temporarily shelves (or  _stashes_ ) changes you've made to your working copy so you can work on something else, and then come back and re-apply them later on.
#### Autostash
If youre a fan of `git rebase`, you might be familiar with the `--autostash`option. It automatically stashes any local changes made to your working copy before rebasing and reapplies them after the rebase is completed.
```
$ git rebase master --autostash
Created autostash: 54f212a
HEAD is now at 8303dca It's a kludge, but put the tuple from the database in the cache.
First, rewinding head to replay your work on top of it...
Applied autostash.
```
This is handy, as it allows you to rebase from a dirty worktree. Theres also a handy config flag named `rebase.autostash` to make this behavior the default, which you can enable globally with:
```
$ git config --global rebase.autostash true
```
`rebase.autostash` has actually been available since [Git v1.8.4][69], but v2.7 introduces the ability to cancel this flag with the `--no-autostash` option. If you use this option with unstaged changes, the rebase will abort with a dirty worktree warning:
```
$ git rebase master --no-autostash
Cannot rebase: You have unstaged changes.
Please commit or stash them.
```
#### Stashes as Patches
Speaking of config flags, Git v2.7 also introduces `stash.showPatch`. The default behavior of `git stash show` is to display a summary of your stashed files.
```
$ git stash show
package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
```
Passing the `-p` flag puts `git stash show` into "patch mode," which displays the full diff:
![](https://cdn-images-1.medium.com/max/800/1*HpcT3quuKKQj9CneqPuufw.png)
`stash.showPatch` makes this behavior the default. You can enable it globally with:
```
$ git config --global stash.showPatch true
```
If you enable `stash.showPatch` but then decide you want to view just the file summary, you can get the old behavior back by passing the `--stat` option instead.
```
$ git stash show --stat
package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
```
As an aside: `--no-patch` is a valid option but it doesn't negate `stash.showPatch` as you'd expect. Instead, it gets passed along to the underlying `git diff` command used to generate the patch, and you'll end up with no output at all!
#### Simple Stash IDs
If youre a `git stash` fan, you probably know that you can shelve multiple sets of changes, and then view them with `git stash list`:
```
$ git stash list
stash@{0}: On master: crazy idea that might work one day
stash@{1}: On master: desperate samurai refactor; don't apply
stash@{2}: On master: perf improvement that I forgot I stashed
stash@{3}: On master: pop this when we use Docker in production
```
However, you may not know why Gits stashes have such awkward identifiers (`stash@{1}`, `stash@{2}`, etc.) and may have written them off as "just one of those Git idiosyncrasies." It turns out that like many Git features, these weird IDs are actually a symptom of a very clever use (or abuse) of the Git data model.
Under the hood, the `git stash` command actually creates a set of special commit objects that encode your stashed changes and maintains a [reflog][70]that holds references to these special commits. This is why the output from `git stash list` looks a lot like the output from the `git reflog` command. When you run `git stash apply stash@{1}`, you're actually saying, “Apply the commit at position 1 from the stash reflog.”
As of Git v2.11, you no longer have to use the full `stash@{n}` syntax. Instead, you can reference stashes with a simple integer indicating their position in the stash reflog:
```
$ git stash show 1
$ git stash apply 1
$ git stash pop 1
```
And so forth. If youd like to learn more about how stashes are stored, I wrote a little bit about it in [this tutorial][71].
### </2016> <2017>
And were done. Thanks for reading! I hope you enjoyed reading this behemoth as much as I enjoyed spelunking through Gits source code, release notes, and `man` pages to write it. If you think I missed anything big, please leave a comment or let me know [on Twitter][72] and I'll endeavor to write a follow-up piece.
As for whats next for Git, thats up to the maintainers and contributors (which [could be you!][73]). With ever-increasing adoption, Im guessing that simplification, improved UX, and better defaults will be strong themes for Git in 2017\. As Git repositories get bigger and older, I suspect well also see continued focus on performance and improved handling of large files, deep trees, and long histories.
If youre into Git and excited to meet some of the developers behind the project, consider coming along to [Git Merge][74] in Brussels in a few weeks time. Im [speaking there][75]! But more importantly, many of the developers who maintain Git will be in attendance for the conference and the annual Git Contributors Summit, which will likely drive much of the direction for the year ahead.
Or if you cant wait til then, head over to Atlassians excellent selection of [Git tutorials][76] for more tips and tricks to improve your workflow.
_If you scrolled to the end looking for the footnotes from the first paragraph, please jump to the _ [ _[Citation needed]_ ][77] _ section for the commands used to generate the stats. Gratuitous cover image generated using _ [ _instaco.de_ ][78] _ _
--------------------------------------------------------------------------------
via: https://hackernoon.com/git-in-2016-fad96ae22a15#.t5c5cm48f
作者:[Tim Pettersen][a]
译者:[译者ID](https://github.com/译者ID)
校对:[校对者ID](https://github.com/校对者ID)
本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译,[Linux中国](https://linux.cn/) 荣誉推出
[a]:https://hackernoon.com/@kannonboy?source=post_header_lockup
[1]:https://medium.com/@g.kylafas/the-git-config-command-is-missing-a-yes-at-the-end-as-in-git-config-global-commit-verbose-yes-7e126365750e?source=responses---------1----------
[2]:https://medium.com/@kannonboy/thanks-giorgos-fixed-f3b83c61589a?source=responses---------1----------
[3]:https://medium.com/@TomSwirly/i-read-the-whole-thing-from-start-to-finish-415a55d89229?source=responses---------0-31---------
[4]:https://medium.com/@g.kylafas
[5]:https://medium.com/@g.kylafas?source=responses---------1----------
[6]:https://medium.com/@kannonboy
[7]:https://medium.com/@kannonboy?source=responses---------1----------
[8]:https://medium.com/@TomSwirly
[9]:https://medium.com/@TomSwirly?source=responses---------0-31---------
[10]:https://medium.com/@g.kylafas/the-git-config-command-is-missing-a-yes-at-the-end-as-in-git-config-global-commit-verbose-yes-7e126365750e?source=responses---------1----------#--responses
[11]:https://hackernoon.com/@kannonboy
[12]:https://hackernoon.com/@kannonboy?source=placement_card_footer_grid---------0-44
[13]:https://medium.freecodecamp.com/@BillSourour
[14]:https://medium.freecodecamp.com/@BillSourour?source=placement_card_footer_grid---------1-43
[15]:https://blog.uncommon.is/@lut4rp
[16]:https://blog.uncommon.is/@lut4rp?source=placement_card_footer_grid---------2-43
[17]:https://medium.com/@kannonboy
[18]:https://medium.com/@kannonboy
[19]:https://medium.com/@g.kylafas/the-git-config-command-is-missing-a-yes-at-the-end-as-in-git-config-global-commit-verbose-yes-7e126365750e?source=responses---------1----------
[20]:https://medium.com/@kannonboy/thanks-giorgos-fixed-f3b83c61589a?source=responses---------1----------
[21]:https://medium.com/@TomSwirly/i-read-the-whole-thing-from-start-to-finish-415a55d89229?source=responses---------0-31---------
[22]:https://hackernoon.com/setting-breakpoints-on-a-snowy-evening-df34fc3168e2?source=placement_card_footer_grid---------0-44
[23]:https://medium.freecodecamp.com/the-code-im-still-ashamed-of-e4c021dff55e?source=placement_card_footer_grid---------1-43
[24]:https://blog.uncommon.is/using-git-to-generate-versionname-and-versioncode-for-android-apps-aaa9fc2c96af?source=placement_card_footer_grid---------2-43
[25]:https://hackernoon.com/git-in-2016-fad96ae22a15#fd10
[26]:https://hackernoon.com/git-in-2016-fad96ae22a15#cc52
[27]:https://hackernoon.com/git-in-2016-fad96ae22a15#42b9
[28]:https://hackernoon.com/git-in-2016-fad96ae22a15#4208
[29]:https://hackernoon.com/git-in-2016-fad96ae22a15#a5c3
[30]:https://hackernoon.com/git-in-2016-fad96ae22a15#c230
[31]:https://hackernoon.com/tagged/git?source=post
[32]:https://hackernoon.com/tagged/web-development?source=post
[33]:https://hackernoon.com/tagged/software-development?source=post
[34]:https://hackernoon.com/tagged/programming?source=post
[35]:https://hackernoon.com/tagged/atlassian?source=post
[36]:https://hackernoon.com/@kannonboy
[37]:https://hackernoon.com/?source=footer_card
[38]:https://hackernoon.com/setting-breakpoints-on-a-snowy-evening-df34fc3168e2?source=placement_card_footer_grid---------0-44
[39]:https://medium.freecodecamp.com/the-code-im-still-ashamed-of-e4c021dff55e?source=placement_card_footer_grid---------1-43
[40]:https://blog.uncommon.is/using-git-to-generate-versionname-and-versioncode-for-android-apps-aaa9fc2c96af?source=placement_card_footer_grid---------2-43
[41]:https://hackernoon.com/git-in-2016-fad96ae22a15#fd10
[42]:https://hackernoon.com/git-in-2016-fad96ae22a15#fd10
[43]:https://hackernoon.com/git-in-2016-fad96ae22a15#cc52
[44]:https://hackernoon.com/git-in-2016-fad96ae22a15#cc52
[45]:https://hackernoon.com/git-in-2016-fad96ae22a15#42b9
[46]:https://hackernoon.com/git-in-2016-fad96ae22a15#4208
[47]:https://hackernoon.com/git-in-2016-fad96ae22a15#a5c3
[48]:https://hackernoon.com/git-in-2016-fad96ae22a15#c230
[49]:https://git-scm.com/docs/git-worktree
[50]:https://git-scm.com/book/en/v2/Git-Tools-Debugging-with-Git#Binary-Search
[51]:https://www.atlassian.com/git/tutorials/git-lfs/#speeding-up-clones
[52]:https://www.atlassian.com/git/tutorials/git-stash/
[53]:https://hackernoon.com/@kannonboy?source=footer_card
[54]:https://hackernoon.com/?source=footer_card
[55]:https://hackernoon.com/@kannonboy?source=post_header_lockup
[56]:https://hackernoon.com/@kannonboy?source=post_header_lockup
[57]:https://hackernoon.com/git-in-2016-fad96ae22a15#c8e9
[58]:https://hackernoon.com/git-in-2016-fad96ae22a15#408a
[59]:https://hackernoon.com/git-in-2016-fad96ae22a15#315b
[60]:https://hackernoon.com/git-in-2016-fad96ae22a15#dbfb
[61]:https://hackernoon.com/git-in-2016-fad96ae22a15#2220
[62]:https://hackernoon.com/git-in-2016-fad96ae22a15#bc78
[63]:https://www.atlassian.com/git/tutorials/install-git/
[64]:https://www.atlassian.com/git/tutorials/what-is-git/
[65]:https://www.atlassian.com/git/tutorials/git-lfs/
[66]:https://twitter.com/kit3bus
[67]:https://confluence.atlassian.com/bitbucket/bitbucket-lfs-media-adapter-856699998.html
[68]:https://developer.atlassian.com/blog/2015/10/monorepos-in-git/
[69]:https://blogs.atlassian.com/2013/08/what-you-need-to-know-about-the-new-git-1-8-4/
[70]:https://www.atlassian.com/git/tutorials/refs-and-the-reflog/
[71]:https://www.atlassian.com/git/tutorials/git-stash/#how-git-stash-works
[72]:https://twitter.com/kannonboy
[73]:https://git.kernel.org/cgit/git/git.git/tree/Documentation/SubmittingPatches
[74]:http://git-merge.com/
[75]:http://git-merge.com/#git-aliases
[76]:https://www.atlassian.com/git/tutorials
[77]:https://hackernoon.com/git-in-2016-fad96ae22a15#87c4
[78]:http://instaco.de/
[79]:https://medium.com/@Medium/personalize-your-medium-experience-with-users-publications-tags-26a41ab1ee0c#.hx4zuv3mg
[80]:https://hackernoon.com/

View File

@ -0,0 +1,682 @@
2016 Git 新视界
============================================================
![](https://cdn-images-1.medium.com/max/2000/1*1SiSsLMsNSyAk6khb63W9g.png)
2016 年 Git 发生了 _惊天动地_ 地变化,发布了五大新特性[¹][57] (从 _v2.7_  到  _v2.11_ )和十六个补丁[²][58]。189 位作者[³][59]贡献了 3676 个提交[⁴][60]到 `master` 分支,比 2015 年多了 15%[⁵][61]!总计有 1545 个文件被修改,其中增加了 276799 行并移除了 100973 行。
但是,通过统计提交的数量和代码行数来衡量生产力是一种十分愚蠢的方法。除了深度研究过的开发者可以做到凭直觉来判断代码质量的地步,我们普通人来作仲裁难免会因我们常人的判断有失偏颇。
谨记这一条于心,我决定整理这一年里六个我最喜爱的 Git 特性涵盖的改进,来做一次分类回顾 。这篇文章作为一篇中篇推文有点太过长了,所以我不介意你们直接跳到你们特别感兴趣的特性去。
* [完成][41]`[git worktree][25]`[命令][42]
* [更多方便的][43]`[git rebase][26]`[选项][44]
* `[git lfs][27]`[梦幻的性能加速][45]
* `[git diff][28]`[实验性的算法和更好的默认结果][46]
* `[git submodules][29]`[差强人意][47]
* `[git stash][30]`的[90 个增强][48]
在我们开始之前,请注意在大多数操作系统上都自带有 Git 的旧版本,所以你需要检查你是否在使用最新并且最棒的版本。如果在终端运行 `git --version` 返回的结果小于 Git `v2.11.0`,请立刻跳转到 Atlassian 的快速指南 [更新或安装 Git][63] 并根据你的平台做出选择。
###[`引用` 是需要的]
在我们进入高质量内容之前还需要做一个短暂的停顿:我觉得我需要为你展示我是如何从公开文档(以及开篇的封面图片)生成统计信息的。你也可以使用下面的命令来对你自己的仓库做一个快速的 *年度回顾*
```
¹ Tags from 2016 matching the form vX.Y.0
```
```
$ git for-each-ref --sort=-taggerdate --format \
'%(refname) %(taggerdate)' refs/tags | grep "v\d\.\d*\.0 .* 2016"
```
```
² Tags from 2016 matching the form vX.Y.Z
```
```
$ git for-each-ref --sort=-taggerdate --format '%(refname) %(taggerdate)' refs/tags | grep "v\d\.\d*\.[^0] .* 2016"
```
```
³ Commits by author in 2016
```
```
$ git shortlog -s -n --since=2016-01-01 --until=2017-01-01
```
```
⁴ Count commits in 2016
```
```
$ git log --oneline --since=2016-01-01 --until=2017-01-01 | wc -l
```
```
⁵ ... and in 2015
```
```
$ git log --oneline --since=2015-01-01 --until=2016-01-01 | wc -l
```
```
⁶ Net LOC added/removed in 2016
```
```
$ git diff --shortstat `git rev-list -1 --until=2016-01-01 master` \
`git rev-list -1 --until=2017-01-01 master`
```
以上的命令是在 Git 的 `master` 分支运行的,所以不会显示其他出色的分支上没有合并的工作。如果你使用这些命令,请记住提交的数量和代码行数不是应该值得信赖的度量方式。请不要使用它们来衡量你的团队成员的贡献。
现在,让我们开始说好的回顾……
### 完成 Git worktress
`git worktree` 命令首次出现于 Git v2.5 但是在 2016 年有了一些显著的增强。两个有价值的新特性在 v2.7 被引入—— `list` 子命令,和为二分搜索增加了命令空间的 refs——而 `lock`/`unlock` 子命令则是在 v2.10被引入。
#### 什么是 worktree 呢?
`[git worktree][49]` 命令允许你同步地检出和操作处于不同路径下的同一仓库的多个分支。例如,假如你需要做一次快速的修复工作但又不想扰乱你当前的工作区,你可以使用以下命令在一个新路径下检出一个新分支
```
$ git worktree add -b hotfix/BB-1234 ../hotfix/BB-1234
Preparing ../hotfix/BB-1234 (identifier BB-1234)
HEAD is now at 886e0ba Merged in bedwards/BB-13430-api-merge-pr (pull request #7822)
```
Worktree 不仅仅是为分支工作。你可以检出多个里程碑tags作为不同的工作树来并行构建或测试它们。例如我从 Git v2.6 和 v2.7 的里程碑中创建工作树来检验不同版本 Git 的行为特征。
```
$ git worktree add ../git-v2.6.0 v2.6.0
Preparing ../git-v2.6.0 (identifier git-v2.6.0)
HEAD is now at be08dee Git 2.6
```
```
$ git worktree add ../git-v2.7.0 v2.7.0
Preparing ../git-v2.7.0 (identifier git-v2.7.0)
HEAD is now at 7548842 Git 2.7
```
```
$ git worktree list
/Users/kannonboy/src/git 7548842 [master]
/Users/kannonboy/src/git-v2.6.0 be08dee (detached HEAD)
/Users/kannonboy/src/git-v2.7.0 7548842 (detached HEAD)
```
```
$ cd ../git-v2.7.0 && make
```
你也使用同样的技术来并行构造和运行你自己应用程序的不同版本。
#### 列出工作树
`git worktree list` 子命令(于 Git v2.7引入)显示所有与当前仓库有关的工作树。
```
$ git worktree list
/Users/kannonboy/src/bitbucket/bitbucket 37732bd [master]
/Users/kannonboy/src/bitbucket/staging d5924bc [staging]
/Users/kannonboy/src/bitbucket/hotfix-1234 37732bd [hotfix/1234]
```
#### 二分查找工作树
`[gitbisect][50]` 是一个简洁的 Git 命令,可以让我们对提交记录执行一次二分搜索。通常用来找到哪一次提交引入了一个指定的退化。例如,如果在我的 `master` 分支最后的提交上有一个测试没有通过,我可以使用 `git bisect` 来贯穿仓库的历史来找寻第一次造成这个错误的提交。
```
$ git bisect start
```
```
# indicate the last commit known to be passing the tests
# (e.g. the latest release tag)
$ git bisect good v2.0.0
```
```
# indicate a known broken commit (e.g. the tip of master)
$ git bisect bad master
```
```
# tell git bisect a script/command to run; git bisect will
# find the oldest commit between "good" and "bad" that causes
# this script to exit with a non-zero status
$ git bisect run npm test
```
在后台bisect 使用 refs 来跟踪 好 与 坏 的提交来作为二分搜索范围的上下界限。不幸的是,对工作树的粉丝来说,这些 refs 都存储在寻常的 `.git/refs/bisect` 命名空间,意味着 `git bisect` 操作如果运行在不同的工作树下可能会互相干扰。
到了 v2.7 版本bisect 的 refs 移到了 `.git/worktrees/$worktree_name/refs/bisect` 所以你可以并行运行 bisect 操作于多个工作树中。
#### 锁定工作树
当你完成了一颗工作树的工作,你可以直接删除它,然后通过运行 `git worktree prune` 等它被当做垃圾自动回收。但是如果你在网络共享或者可移除媒介上存储了一颗工作树如果工作树目录在删除期间不可访问工作树会被完全清除——不管你喜不喜欢Git v2.10 引入了 `git worktree lock``unlock` 子命令来防止这种情况发生。
```
# to lock the git-v2.7 worktree on my USB drive
$ git worktree lock /Volumes/Flash_Gordon/git-v2.7 --reason \
"In case I remove my removable media"
```
```
# to unlock (and delete) the worktree when I'm finished with it
$ git worktree unlock /Volumes/Flash_Gordon/git-v2.7
$ rm -rf /Volumes/Flash_Gordon/git-v2.7
$ git worktree prune
```
`--reason` 标签允许为未来的你留一个记号,描述为什么当初工作树被锁定。`git worktree unlock` 和 `lock` 都要求你指定工作树的路径。或者,你可以 `cd` 到工作树目录然后运行 `git worktree lock .` 来达到同样的效果。
### 更多 Git `reabse` 选项
2016 年三月Git v2.8 增加了在拉取过程中交互进行 rebase 的命令 `git pull --rebase=interactive` 。对应地,六月份 Git v2.9 发布了通过 `git rebase -x` 命令对执行变基操作而不需要进入交互模式的支持。
#### Re-啥?
在我们继续深入前,我假设读者中有些并不是很熟悉或者没有完全习惯变基命令或者交互式变基。从概念上说,它很简单,但是与很多 Git 的强大特性一样,变基散发着听起来很复杂的专业术语的气息。所以,在我们深入前,先来快速的复习一下什么是 rebase。
变基操作意味着将一个或多个提交在一个指定分支上重写。`git rebase` 命令是被深度重载了,但是 rebase 名字的来源事实上还是它经常被用来改变一个分支的基准提交(你基于此提交创建了这个分支)。
从概念上说rebase 通过将你的分支上的提交存储为一系列补丁包临时释放了它们,接着将这些补丁包按顺序依次打在目标提交之上。
![](https://cdn-images-1.medium.com/max/800/1*mgyl38slmqmcE4STS56nXA.gif)
对 master 分支的一个功能分支执行变基操作 `git reabse master`)是一种通过将 master 分支上最新的改变合并到功能分支的“保鲜法”。对于长期存在的功能分支,规律的变基操作能够最大程度的减少开发过程中出现冲突的可能性和严重性。
有些团队会选择在合并他们的改动到 master 前立即执行变基操作以实现一次快速合并 `git merge --ff <feature>`)。对 master 分支快速合并你的提交是通过简单的将 master ref 指向你的重写分支的顶点而不需要创建一个合并提交。
![](https://cdn-images-1.medium.com/max/800/1*QXa3znQiuNWDjxroX628VA.gif)
变基是如此方便和功能强大以致于它已经被嵌入其他常见的 Git 命令中,例如 `git pull`。如果你在本地 master 分支有未推送的提交,运行 `git pull` 命令从 origin 拉取你队友的改动会造成不必要的合并提交。
![](https://cdn-images-1.medium.com/max/800/1*IxDdJ5CygvSWdD8MCNpZNg.gif)
这有点混乱,而且在繁忙的团队,你会获得成堆的不必要的合并提交。`git pull --rebase` 将你本地的提交在你队友的提交上执行变基而不产生一个合并提交。
![](https://cdn-images-1.medium.com/max/800/1*HcroDMwBE9m21-hOeIwRmw.gif)
这很整洁吧甚至更酷Git v2.8 引入了一个新特性,允许你在拉取时 _交互地_ 变基。
#### 交互式变基
交互式变基是变基操作的一种更强大的形态。和标准变基操作相似,它可以重写提交,但它也可以向你提供一个机会让你能够交互式地修改这些将被重新运用在新基准上的提交。
当你运行 `git rebase --interactive` (或 `git pull --rebase=interactive`)时,你会在你的文本编辑器中得到一个可供选择的提交列表视图。
```
$ git rebase master --interactive
```
```
pick 2fde787 ACE-1294: replaced miniamalCommit with string in test
pick ed93626 ACE-1294: removed pull request service from test
pick b02eb9a ACE-1294: moved fromHash, toHash and diffType to batch
pick e68f710 ACE-1294: added testing data to batch email file
```
```
# Rebase f32fa9d..0ddde5f onto f32fa9d (4 commands)
#
# Commands:
# p, pick = use commit
# r, reword = use commit, but edit the commit message
# e, edit = use commit, but stop for amending
# s, squash = use commit, but meld into previous commit
# f, fixup = like "squash", but discard this commit's log message
# x, exec = run command (the rest of the line) using shell
# d, drop = remove commit
#
# These lines can be re-ordered; they are executed from top to
# bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
```
注意到每一条提交旁都有一个 `pick`。这是对 rebase 而言,"照原样留下这个提交"。如果你现在就退出文本编辑器,它会执行一次如上文所述的普通变基操作。但是,如果你将 `pick` 改为 `edit` 或者其他 rebase 命令中的一个,变基操作会允许你在它被重新运用前改变它。有效的变基命令有如下几种:
* `reword`: 编辑提交信息。
* `edit`: 编辑提交了的文件。
* `squash`: 将提交与之前的提交(同在文件中)合并,并将提交信息拼接。
* `fixup`: 将本提交与上一条提交合并,并且逐字使用上一条提交的提交信息(这很方便,如果你为一个很小的改动创建了第二个提交,而它本身就应该属于上一条提交,例如,你忘记暂存了一个文件)。
* `exec`: 运行一条任意的 shell 命令(我们将会在下一节看到本例一次简洁的使用场景)。
* `drop`: 这将丢弃这条提交。
你也可以在文件内重新整理提交,这样会改变他们被重新运用的顺序。这会很顺手当你对不同的主题创建了交错的提交时,你可以使用 `squash` 或者 `fixup` 来将其合并成符合逻辑的原子提交。
当你设置完命令并且保存这个文件后Git 将递归每一条提交,在每个 `reword``edit` 命令处为你暂停来执行你设计好的改变并且自动运行 `squash` `fixup``exec` 和 `drop`命令。
####非交互性式执行
当你执行变基操作时,本质上你是在通过将你每一条新提交应用于指定基址的头部来重写历史。`git pull --rebase` 可能会有一点危险,因为根据上游分支改动的事实,你的新建历史可能会由于特定的提交遭遇测试失败甚至编译问题。如果这些改动引起了合并冲突,变基过程将会暂停并且允许你来解决它们。但是,整洁的合并改动仍然有可能打断编译或测试过程,留下破败的提交弄乱你的提交历史。
但是,你可以指导 Git 为每一个重写的提交来运行你的项目测试套件。在 Git v2.9 之前,你可以通过绑定 `git rebase --interactive``exec` 命令来实现。例如这样:
```
$ git rebase master interactive exec=”npm test”
```
...会生成在重写每条提交后执行 `npm test` 这样的一个交互式变基计划,保证你的测试仍然会通过:
```
pick 2fde787 ACE-1294: replaced miniamalCommit with string in test
exec npm test
pick ed93626 ACE-1294: removed pull request service from test
exec npm test
pick b02eb9a ACE-1294: moved fromHash, toHash and diffType to batch
exec npm test
pick e68f710 ACE-1294: added testing data to batch email file
exec npm test
```
```
# Rebase f32fa9d..0ddde5f onto f32fa9d (4 command(s))
```
如果出现了测试失败的情况,变基会暂停并让你修复这些测试(并且将你的修改应用于相应提交)
```
291 passing
1 failing
```
```
1) Host request “after all” hook:
Uncaught Error: connect ECONNRESET 127.0.0.1:3001
npm ERR! Test failed.
Execution failed: npm test
You can fix the problem, and then run
git rebase continue
```
这很方便,但是使用交互式变基有一点臃肿。到了 Git v2.9,你可以这样来实现非交互式变基:
```
$ git rebase master -x “npm test”
```
简单替换 `npm test``make``rake``mvn clean install`,或者任何你用来构建或测试你的项目的命令。
####小小警告
就像电影里一样,重写历史可是一个危险的行当。任何提交被重写为变基操作的一部分都将改变它的 SHA-1 ID这意味着 Git 会把它当作一个全新的提交对待。如果重写的历史和原来的历史混杂,你将获得重复的提交,而这可能在你的团队中引起不少的疑惑。
为了避免这个问题,你仅仅需要遵照一条简单的规则:
> _永远不要变基一条你已经推送的提交_
坚持这一点你会没事的。
### `Git LFS` 的性能提升
[Git 是一个分布式版本控制系统][64],意味着整个仓库的历史会在克隆阶段被传送到客户端。对包含大文件的项目——尤其是大文件经常被修改——初始克隆会非常耗时,因为每一个版本的每一个文件都必须下载到客户端。[Git LFS(Large File Storage 大文件存储)][65] 是一个 Git 拓展包,由 AtlassianGitHub 和其他一些开源贡献者开发,通过消极地下载大文件的相对版本来减少仓库中大文件的影响。更明确地说,大文件是在检出过程中按需下载的而不是在克隆或抓取过程中。
在 Git 2016 年的五大发布中Git LFS 自身有四个功能丰富的发布v1.2 到 v1.5。你可以凭 Git LFS 自身来写一系列回顾文章,但是就这篇文章而言,我将专注于 2016 年解决的一项最重要的主题:速度。一系列针对 Git 和 Git LFS 的改进极大程度地优化了将文件传入/传出服务器的性能。
#### 长期过滤进程
当你 `git add` 一个文件时Git 的净化过滤系统会被用来在文件被写入 Git 目标存储前转化文件的内容。Git LFS 通过使用净化过滤器将大文件内容存储到 LFS 缓存中以缩减仓库的大小,并且增加一个小“指针”文件到 Git 目标存储中作为替代。
![](https://cdn-images-1.medium.com/max/800/0*Ku328eca7GLOo7sS.png)
污化过滤器是净化过滤器的对立面——正如其名。在 `git checkout` 过程中从一个 Git 目标仓库读取文件内容时污化过滤系统有机会在文件被写入用户的工作区前将其改写。Git LFS 污化过滤器通过将指针文件替代为对应的大文件将其转化,可以是从 LFS 缓存中获得或者通过读取存储在 Bitbucket 的 Git LFS。
![](https://cdn-images-1.medium.com/max/800/0*CU60meE1lbCuivn7.png)
传统上,污化和净化过滤进程在每个文件被增加和检出时只能被唤起一次。所以,一个项目如果有 1000 个文件在被 Git LFS 追踪 ,做一次全新的检出需要唤起 `git-lfs-smudge` 命令 1000 次。尽管单次操作相对很迅速,但是经常执行 1000 次独立的污化进程总耗费惊人。、
针对 Git v2.11(和 Git LFS v1.5),污化和净化过滤器可以被定义为长期进程,为第一个需要过滤的文件调用一次,然后为之后的文件持续提供污化或净化过滤直到父 Git 操作结束。[Lars Schneider][66]Git 的长期过滤系统的贡献者,简洁地总结了对 Git LFS 性能改变带来的影响。
> 使用 12k 个文件的测试仓库的过滤进程在 macOS 上快了80 倍,在 Windows 上 快了 58 倍。在 Windows 上,这意味着测试运行了 57 秒而不是 55 分钟。
> 这真是一个让人印象深刻的性能增强!
#### LFS 专有克隆
长期运行的污化和净化过滤器在对向本地缓存读写的加速做了很多贡献,但是对大目标传入/传出 Git LFS 服务器的速度提升贡献很少。 每次 Git LFS 污化过滤器在本地 LFS 缓存中无法找到一个文件时,它不得不使用两个 HTTP 请求来获得该文件:一个用来定位文件,另外一个用来下载它。在一次 `git clone` 过程中,你的本地 LFS 缓存是空的,所以 Git LFS 会天真地为你的仓库中每个 LFS 所追踪的文件创建两个 HTTP 请求:
![](https://cdn-images-1.medium.com/max/800/0*ViL7r3ZhkGvF0z3-.png)
幸运的是Git LFS v1.2 提供了专门的 `[git lfs clone][51]` 命令。不再是一次下载一个文件; `git lfs clone` 禁止 Git LFS 污化过滤器,等待检出结束,然后从 Git LFS 存储中按批下载任何需要的文件。这允许了并行下载并且将需要的 HTTP 请求数量减半。
![](https://cdn-images-1.medium.com/max/800/0*T43VA0DYTujDNgkH.png)
###自定义传输路由器
正如之前讨论过的Git LFS 在 v1.5 中 发起了对长期过滤进程的支持。不过,对另外一种可插入进程的支持早在今年年初就发布了。 Git LFS 1.3 包含了对可插拔传输路由器的支持,因此不同的 Git LFS 托管服务可以定义属于它们自己的协议来向或从 LFS 存储中传输文件。
直到 2016 年底Bitbucket 是唯一一个执行专属 Git LFS 传输协议 [Bitbucket LFS Media Adapter][67] 的托管服务商。这是为了从 Bitbucket 的一个独特的被称为 chunking 的 LFS 存储 API 特性中获利。Chunking 意味着在上传或下载过程中,大文件被分解成 4MB 的文件块chunk
![](https://cdn-images-1.medium.com/max/800/1*N3SpjQZQ1Ge8OwvWrtS1og.gif)
分块给予了 Bitbucket 支持的 Git LFS 三大优势:
1. 并行下载与上传。默认地Git LFS 最多并行传输三个文件。但是,如果只有一个文件被单独传输(这也是 Git LFS 污化过滤器的默认行为)它会在一个单独的流中被传输。Bitbucket 的分块允许同一文件的多个文件块同时被上传或下载,经常能够梦幻地提升传输速度。
2. 可恢复文件块传输。文件块都在本地缓存所以如果你的下载或上传被打断Bitbucket 的自定义 LFS 流媒体路由器会在下一次你推送或拉取时仅为丢失的文件块恢复传输。
3. 免重复。Git LFS正如 Git 本身,是内容索位;每一个 LFS 文件都由它的内容生成的 SHA-256 哈希值认证。所以,哪怕你稍微修改了一位数据,整个文件的 SHA-256 就会修改而你不得不重新上传整个文件。分块允许你仅仅重新上传文件真正被修改的部分。举个例子想想一下Git LFS 在追踪一个 41M 的电子游戏精灵表。如果我们增加在此精灵表上增加 2MB 的新层并且提交它,传统上我们需要推送整个新的 43M 文件到服务器端。但是,使用 Bitbucket 的自定义传输路由,我们仅仅需要推送 ~7MB先是 4MB 文件块(因为文件的信息头会改变)和我们刚刚添加的包含新层的 3MB 文件块!其余未改变的文件块在上传过程中被自动跳过,节省了巨大的带宽和时间消耗。
可自定义的传输路由器是 Git LFS 一个伟大的特性,它们使得不同服务商在不过载核心项目的前提下体验适合其服务器的优化后的传输协议。
### 更佳的 `git diff` 算法与默认值
不像其他的版本控制系统Git 不会明确地存储文件被重命名了的事实。例如,如果我编辑了一个简单的 Node.js 应用并且将 `index.js` 重命名为 `app.js`,然后运行 `git diff`,我会得到一个看起来像一个文件被删除另一个文件被新建的结果。
![](https://cdn-images-1.medium.com/max/800/1*ohMUBpSh_jqz2ffScJ7ApQ.png)
我猜测移动或重命名一个文件从技术上来讲是一次删除后跟一次新建,但这不是对人类最友好的方式来诉说它。其实,你可以使用 `-M` 标志来指示 Git 在计算差异时抽空尝试检测重命名文件。对之前的例子,`git diff -M` 给我们如下结果:
![](https://cdn-images-1.medium.com/max/800/1*ywYjxBc1wii5O8EhHbpCTA.png)
第二行显示的 similarity index 告诉我们文件内容经过比较后的相似程度。默认地,`-M` 会考虑任意两个文件都有超过 50% 相似度。这意味着,你需要编辑少于 50% 的行数来确保它们被识别成一个重命名后的文件。你可以通过加上一个百分比来选择你自己的 similarity index`-M80%`。
到 Git v2.9 版本,如果你使用了 `-M` 标志 `git diff``git log` 命令都会默认检测重命名。如果不喜欢这种行为(或者,更现实的情况,你在通过一个脚本来解析 diff 输出),那么你可以通过显示的传递 `--no-renames` 标志来禁用它。
#### 详细的提交
你经历过调用 `git commit` 然后盯着空白的 shell 试图想起你刚刚做过的所有改动吗verbose 标志就为此而来!
不像这样:
```
Ah crap, which dependency did I just rev?
```
```
# Please enter the commit message for your changes. Lines starting
# with # will be ignored, and an empty message aborts the commit.
# On branch master
# Your branch is up-to-date with origin/master.
#
# Changes to be committed:
# new file: package.json
#
```
...你可以调用 `git commit --verbose` 来查看你改动造成的内联差异。不用担心,这不会包含在你的提交信息中:
![](https://cdn-images-1.medium.com/max/800/1*1vOYE2ow3ZDS8BP_QfssQw.png)
`--verbose` 标志不是最新的,但是直到 Git v2.9 你可以通过 `git config --global commit.verbose true` 永久的启用它。
#### 实验性的 Diff 改进
当一个被修改部分前后几行相同时,`git diff` 可能产生一些稍微令人迷惑的输出。如果在一个文件中有两个或者更多相似结构的函数时这可能发生。来看一个有些刻意人为的例子,想象我们有一个 JS 文件包含一个单独的函数:
```
/* @return {string} "Bitbucket" */
function productName() {
return "Bitbucket";
}
```
现在想象一下我们刚提交的改动包含一个预谋的 _另一个_可以做相似事情的函数
```
/* @return {string} "Bitbucket" */
function productId() {
return "Bitbucket";
}
```
```
/* @return {string} "Bitbucket" */
function productName() {
return "Bitbucket";
}
```
我们希望 `git diff` 显示开头五行被新增,但是实际上它不恰当地将最初提交的第一行也包含进来。
![](https://cdn-images-1.medium.com/max/800/1*9C7DWMObGHMEqD-QFGHmew.png)
错误的注释被包含在了 diff 中!这虽不是世界末日,但每次发生这种事情总免不了花费几秒钟的意识去想 _啊_
在十二月Git v2.11 介绍了一个新的实验性的 diff 选项,`--indent-heuristic`,尝试生成从美学角度来看更赏心悦目的 diff。
![](https://cdn-images-1.medium.com/max/800/1*UyWZ6JjC-izDquyWCA4bow.png)
在后台,`--indent-heuristic` 在每一次改动造成的所有可能的 diff 中循环,并为它们分别打上一个 "不良" 分数。这是基于试探性的如差异文件块是否以不同等级的缩进开始和结束(从美学角度讲不良)以及差异文件块前后是否有空白行(从美学角度讲令人愉悦)。最后,有着最低不良分数的块就是最终输出。
这个特性还是实验性的,但是你可以通过应用 `--indent-heuristic` 选项到任何 `git diff` 命令来专门测试它。如果,如果你喜欢在刀口上讨生活,你可以这样将其在你的整个系统内使能:
```
$ git config --global diff.indentHeuristic true
```
### Submodules 差强人意
子模块允许你从 Git 仓库内部引用和包含其他 Git 仓库。这通常被用在当一些项目管理的源依赖也在被 Git 跟踪时,或者被某些公司用来作为包含一系列相关项目的 [monorepo][68] 的替代品。
由于某些用法的复杂性以及使用错误的命令相当容易破坏它们的事实Submodule 得到了一些坏名声。
![](https://cdn-images-1.medium.com/max/800/1*xNffiElY7BZNMDM0jm0JNQ.gif)
但是,它们还是有着它们的用处,而且,我想,仍然对其他方案有依赖时的最好的选择。 幸运的是2016 对 submodule 用户来说是伟大的一年,在几次发布中落地了许多意义重大的性能和特性提升。
#### 并行抓取
当克隆或则抓取一个仓库时,加上 `--recurse-submodules` 选项意味着任何引用的 submodule 也将被克隆或更新。传统上,这会被串行执行,每次只抓取一个 submodule。直到 Git v2.8,你可以附加 `--jobs=n` 选项来使用 _n_ 个并行线程来抓取 submodules。
我推荐永久的配置这个选项:
```
$ git config --global submodule.fetchJobs 4
```
...或者你可以选择使用任意程度的平行化。
#### 浅层子模块
Git v2.9 介绍了 `git clone —shallow-submodules` 标志。它允许你抓取你仓库的完整克隆,然后递归的浅层克隆所有引用的子模块的一个提交。如果你不需要项目的依赖的完整记录时会很有用。
例如,一个仓库有着一些混合了的子模块,其中包含有其他方案商提供的依赖和你自己其它的项目。你可能希望初始化时执行浅层子模块克隆然后深度选择几个你想要与之工作的项目。
另一种情况可能是配置一次持续性的集成或调度工作。Git 需要超级仓库以及每个子模块最新的提交以便能够真正执行构建。但是,你可能并不需要每个子模块全部的历史记录,所以仅仅检索最新的提交可以为你省下时间和带宽。
#### 子模块的替代品
`--reference` 选项可以和 `git clone` 配合使用来指定另一个本地仓库作为一个目标存储来保存你本地已经存在的又通过网络传输的重复制目标。语法为:
```
$ git clone --reference <local repo> <url>
```
直到 Git v2.11,你可以使用 `—reference` 选项与 `—recurse-submodules` 结合来设置子模块替代品从另一个本地仓库指向子模块。其语法为:
```
$ git clone --recurse-submodules --reference <local repo> <url>
```
这潜在的可以省下很大数量的带宽和本地磁盘空间,但是如果引用的本地仓库不包含你所克隆自的远程仓库所必需的所有子模块时,它可能会失败。。
幸运的是,方便的 `—-reference-if-able` 选项将会让它优雅地失败,然后为丢失了的被引用的本地仓库的所有子模块回退为一次普通的克隆。
```
$ git clone --recurse-submodules --reference-if-able \
<local repo> <url>
```
#### 子模块的 diff
在 Git v2.11 之前Git 有两种模式来显示对更新了仓库子模块的提交之间的差异。
`git diff —-submodule=short` 显示你的项目引用的子模块中的旧提交和新提交( 这也是如果你整体忽略 `--submodule` 选项的默认结果):
![](https://cdn-images-1.medium.com/max/800/1*K71cJ30NokO5B69-a470NA.png)
`git diff —submodule=log` 有一点啰嗦,显示更新了的子模块中任意新建或移除的提交的信息中统计行。
![](https://cdn-images-1.medium.com/max/800/1*frvsd_T44De8_q0uvNHB1g.png)
Git v2.11 引入了第三个更有用的选项:`—-submodule=diff`。这会显示更新后的子模块所有改动的完整的 diff。
![](https://cdn-images-1.medium.com/max/800/1*nPhJTjP8tcJ0cD8s3YOmjw.png)
### `git stash` 的 90 个增强
不像 submodules几乎没有 Git 用户不钟爱 `[git stash][52]``git stash` 临时搁置(或者 _藏匿_)你对工作区所做的改动使你能够先处理其他事情,结束后重新将搁置的改动恢复到先前状态。
#### 自动搁置
如果你是 `git rebase` 的粉丝,你可能很熟悉 `--autostash` 选项。它会在变基之前自动搁置工作区所有本地修改然后等变基结束再将其复用。
```
$ git rebase master --autostash
Created autostash: 54f212a
HEAD is now at 8303dca It's a kludge, but put the tuple from the database in the cache.
First, rewinding head to replay your work on top of it...
Applied autostash.
```
这很方便,因为它使得你可以在一个不洁的工作区执行变基。有一个方便的配置标志叫做 `rebase.autostash` 可以将这个特性设为默认,你可以这样来全局使能它:
```
$ git config --global rebase.autostash true
```
`rebase.autostash` 实际上自从 [Git v1.8.4][69] 就可用了,但是 v2.7 引入了通过 `--no-autostash` 选项来取消这个标志的功能。如果你对未暂存的改动使用这个选项,变基会被一条工作树被污染的警告禁止:
```
$ git rebase master --no-autostash
Cannot rebase: You have unstaged changes.
Please commit or stash them.
```
#### 补丁式搁置
说到配置标签Git v2.7 也引入了 `stash.showPatch`。`git stash show` 的默认行为是显示你搁置文件的汇总。
```
$ git stash show
package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
```
`-p` 标志传入会将 `git stash show` 变为 "补丁模式",这将会显示完整的 diff
![](https://cdn-images-1.medium.com/max/800/1*HpcT3quuKKQj9CneqPuufw.png)
`stash.showPatch` 将这个行为定为默认。你可以将其全局使能:
```
$ git config --global stash.showPatch true
```
如果你使能 `stash.showPatch` 但却之后决定你仅仅想要查看文件总结,你可以通过传入 `--stat` 选项来重新获得之前的行为。
```
$ git stash show --stat
package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
```
顺便一提:`--no-patch` 是一个有效选项但它不会如你所希望的改写 `stash.showPatch` 的结果。不仅如此,它会传递给用来生成补丁时潜在调用的 `git diff` 命令,然后你会发现完全没有任何输出。
#### 简单的搁置标识
如果你是 `git stash` 的粉丝,你可能知道你可以搁置多次改动然后通过 `git stash list` 来查看它们:
```
$ git stash list
stash@{0}: On master: crazy idea that might work one day
stash@{1}: On master: desperate samurai refactor; don't apply
stash@{2}: On master: perf improvement that I forgot I stashed
stash@{3}: On master: pop this when we use Docker in production
```
但是,你可能不知道为什么 Git 的搁置有着这么难以理解的标识(`stash@{1}`, `stash@{2}`, 等)也可能将它们勾勒成 "仅仅是 Git 的一个特性吧"。实际上就像很多 Git 特性一样,这些奇怪的标志实际上是 Git 数据模型一个非常巧妙使用(或者说是滥用了的)的特性。
在后台,`git stash` 命令实际创建了一系列特别的提交目标,这些目标对你搁置的改动做了编码并且维护一个 [reglog][70] 来保存对这些特殊提交的参考。 这也是为什么 `git stash list` 的输出看起来很像 `git reflog` 的输出。当你运行 `git stash apply stash@{1}` 时,你实际上在说,"从stash reflog 的位置 1 上应用这条提交 "
直到 Git v2.11,你不再需要使用完整的 `stash@{n}` 语句。相反,你可以通过一个简单的整数指出搁置在 stash reflog 中的位置来引用它们。
```
$ git stash show 1
$ git stash apply 1
$ git stash pop 1
```
讲了很多了。如果你还想要多学一些搁置是怎么保存的,我在 [这篇教程][71] 中写了一点这方面的内容。
### <2016> <2017>
好了,结束了。感谢您的阅读!我希望您享受阅读这份长篇大论,正如我享受在 Git 的源码,发布文档,和 `man` 手册中探险一番来撰写它。如果你认为我忘记了一些重要的事,请留下一条评论或者在 [Twitter][72] 上让我知道,我会努力写一份后续篇章。
至于 Git 接下来会发生什么,这要靠广大维护者和贡献者了(其中有可能就是你!)。随着日益增长的采用,我猜测简化,改进后的用户体验,和更好的默认结果将会是 2017 年 Git 主要的主题。随着 Git 仓库变得又大又旧,我猜我们也可以看到继续持续关注性能和对大文件、深度树和长历史的改进处理。
如果你关注 Git 并且很期待能够和一些项目背后的开发者会面,请考虑来 Brussels 花几周时间来参加 [Git Merge][74] 。我会在[那里发言][75]!但是更重要的是,很多维护 Git 的开发者将会出席这次会议而且一年一度的 Git 贡献者峰会很可能会指定来年发展的方向。
或者如果你实在等不及,想要获得更多的技巧和指南来改进你的工作流,请参看这份 Atlassian 的优秀作品: [Git 教程][76] 。
*如果你翻到最下方来找第一节的脚注,请跳转到 [ [引用是需要的] ][77]一节去找生成统计信息的命令。免费的封面图片是由 [ instaco.de ][78] 生成的 ❤️。*
--------------------------------------------------------------------------------
via: https://hackernoon.com/git-in-2016-fad96ae22a15#.t5c5cm48f
作者:[Tim Pettersen][a]
译者:[xiaow6](https://github.com/xiaow6)
校对:[校对者ID](https://github.com/校对者ID)
本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译,[Linux中国](https://linux.cn/) 荣誉推出
[a]:https://hackernoon.com/@kannonboy?source=post_header_lockup
[1]:https://medium.com/@g.kylafas/the-git-config-command-is-missing-a-yes-at-the-end-as-in-git-config-global-commit-verbose-yes-7e126365750e?source=responses---------1----------
[2]:https://medium.com/@kannonboy/thanks-giorgos-fixed-f3b83c61589a?source=responses---------1----------
[3]:https://medium.com/@TomSwirly/i-read-the-whole-thing-from-start-to-finish-415a55d89229?source=responses---------0-31---------
[4]:https://medium.com/@g.kylafas
[5]:https://medium.com/@g.kylafas?source=responses---------1----------
[6]:https://medium.com/@kannonboy
[7]:https://medium.com/@kannonboy?source=responses---------1----------
[8]:https://medium.com/@TomSwirly
[9]:https://medium.com/@TomSwirly?source=responses---------0-31---------
[10]:https://medium.com/@g.kylafas/the-git-config-command-is-missing-a-yes-at-the-end-as-in-git-config-global-commit-verbose-yes-7e126365750e?source=responses---------1----------#--responses
[11]:https://hackernoon.com/@kannonboy
[12]:https://hackernoon.com/@kannonboy?source=placement_card_footer_grid---------0-44
[13]:https://medium.freecodecamp.com/@BillSourour
[14]:https://medium.freecodecamp.com/@BillSourour?source=placement_card_footer_grid---------1-43
[15]:https://blog.uncommon.is/@lut4rp
[16]:https://blog.uncommon.is/@lut4rp?source=placement_card_footer_grid---------2-43
[17]:https://medium.com/@kannonboy
[18]:https://medium.com/@kannonboy
[19]:https://medium.com/@g.kylafas/the-git-config-command-is-missing-a-yes-at-the-end-as-in-git-config-global-commit-verbose-yes-7e126365750e?source=responses---------1----------
[20]:https://medium.com/@kannonboy/thanks-giorgos-fixed-f3b83c61589a?source=responses---------1----------
[21]:https://medium.com/@TomSwirly/i-read-the-whole-thing-from-start-to-finish-415a55d89229?source=responses---------0-31---------
[22]:https://hackernoon.com/setting-breakpoints-on-a-snowy-evening-df34fc3168e2?source=placement_card_footer_grid---------0-44
[23]:https://medium.freecodecamp.com/the-code-im-still-ashamed-of-e4c021dff55e?source=placement_card_footer_grid---------1-43
[24]:https://blog.uncommon.is/using-git-to-generate-versionname-and-versioncode-for-android-apps-aaa9fc2c96af?source=placement_card_footer_grid---------2-43
[25]:https://hackernoon.com/git-in-2016-fad96ae22a15#fd10
[26]:https://hackernoon.com/git-in-2016-fad96ae22a15#cc52
[27]:https://hackernoon.com/git-in-2016-fad96ae22a15#42b9
[28]:https://hackernoon.com/git-in-2016-fad96ae22a15#4208
[29]:https://hackernoon.com/git-in-2016-fad96ae22a15#a5c3
[30]:https://hackernoon.com/git-in-2016-fad96ae22a15#c230
[31]:https://hackernoon.com/tagged/git?source=post
[32]:https://hackernoon.com/tagged/web-development?source=post
[33]:https://hackernoon.com/tagged/software-development?source=post
[34]:https://hackernoon.com/tagged/programming?source=post
[35]:https://hackernoon.com/tagged/atlassian?source=post
[36]:https://hackernoon.com/@kannonboy
[37]:https://hackernoon.com/?source=footer_card
[38]:https://hackernoon.com/setting-breakpoints-on-a-snowy-evening-df34fc3168e2?source=placement_card_footer_grid---------0-44
[39]:https://medium.freecodecamp.com/the-code-im-still-ashamed-of-e4c021dff55e?source=placement_card_footer_grid---------1-43
[40]:https://blog.uncommon.is/using-git-to-generate-versionname-and-versioncode-for-android-apps-aaa9fc2c96af?source=placement_card_footer_grid---------2-43
[41]:https://hackernoon.com/git-in-2016-fad96ae22a15#fd10
[42]:https://hackernoon.com/git-in-2016-fad96ae22a15#fd10
[43]:https://hackernoon.com/git-in-2016-fad96ae22a15#cc52
[44]:https://hackernoon.com/git-in-2016-fad96ae22a15#cc52
[45]:https://hackernoon.com/git-in-2016-fad96ae22a15#42b9
[46]:https://hackernoon.com/git-in-2016-fad96ae22a15#4208
[47]:https://hackernoon.com/git-in-2016-fad96ae22a15#a5c3
[48]:https://hackernoon.com/git-in-2016-fad96ae22a15#c230
[49]:https://git-scm.com/docs/git-worktree
[50]:https://git-scm.com/book/en/v2/Git-Tools-Debugging-with-Git#Binary-Search
[51]:https://www.atlassian.com/git/tutorials/git-lfs/#speeding-up-clones
[52]:https://www.atlassian.com/git/tutorials/git-stash/
[53]:https://hackernoon.com/@kannonboy?source=footer_card
[54]:https://hackernoon.com/?source=footer_card
[55]:https://hackernoon.com/@kannonboy?source=post_header_lockup
[56]:https://hackernoon.com/@kannonboy?source=post_header_lockup
[57]:https://hackernoon.com/git-in-2016-fad96ae22a15#c8e9
[58]:https://hackernoon.com/git-in-2016-fad96ae22a15#408a
[59]:https://hackernoon.com/git-in-2016-fad96ae22a15#315b
[60]:https://hackernoon.com/git-in-2016-fad96ae22a15#dbfb
[61]:https://hackernoon.com/git-in-2016-fad96ae22a15#2220
[62]:https://hackernoon.com/git-in-2016-fad96ae22a15#bc78
[63]:https://www.atlassian.com/git/tutorials/install-git/
[64]:https://www.atlassian.com/git/tutorials/what-is-git/
[65]:https://www.atlassian.com/git/tutorials/git-lfs/
[66]:https://twitter.com/kit3bus
[67]:https://confluence.atlassian.com/bitbucket/bitbucket-lfs-media-adapter-856699998.html
[68]:https://developer.atlassian.com/blog/2015/10/monorepos-in-git/
[69]:https://blogs.atlassian.com/2013/08/what-you-need-to-know-about-the-new-git-1-8-4/
[70]:https://www.atlassian.com/git/tutorials/refs-and-the-reflog/
[71]:https://www.atlassian.com/git/tutorials/git-stash/#how-git-stash-works
[72]:https://twitter.com/kannonboy
[73]:https://git.kernel.org/cgit/git/git.git/tree/Documentation/SubmittingPatches
[74]:http://git-merge.com/
[75]:http://git-merge.com/#git-aliases
[76]:https://www.atlassian.com/git/tutorials
[77]:https://hackernoon.com/git-in-2016-fad96ae22a15#87c4
[78]:http://instaco.de/
[79]:https://medium.com/@Medium/personalize-your-medium-experience-with-users-publications-tags-26a41ab1ee0c#.hx4zuv3mg
[80]:https://hackernoon.com/