#!/usr/bin/env python
from __future__ import print_function

import logging
import re
import scrapy

from scrapy import Request
from scrapy.linkextractors import IGNORED_EXTENSIONS
from scrapy.linkextractors.lxmlhtml import LxmlLinkExtractor
from scrapy.utils.url import url_has_any_extension

from typing import Any, Generator, List, Optional, Tuple

EXCLUDED_URLS = [
    # Google calendar returns 404s on HEAD requests unconditionally
    'https://calendar.google.com/calendar/embed?src=ktiduof4eoh47lmgcl2qunnc0o@group.calendar.google.com',
    # Returns 409 errors to HEAD requests frequently
    'https://medium.freecodecamp.com',
]


class BaseDocumentationSpider(scrapy.Spider):
    name = None  # type: Optional[str]
    # Exclude domain address.
    deny_domains = []  # type: List[str]
    start_urls = []  # type: List[str]
    deny = []  # type: List[str]
    file_extensions = ['.' + ext for ext in IGNORED_EXTENSIONS]  # type: List[str]
    tags = ('a', 'area', 'img')
    attrs = ('href', 'src')

    def __init__(self, *args, **kwargs):
        # type: (*Any, **Any) -> None
        super(BaseDocumentationSpider, self).__init__(*args, **kwargs)
        self.has_error = False

    def _set_error_state(self):
        # type: () -> None
        self.has_error = True

    def _has_extension(self, url):
        # type: (str) -> bool
        return url_has_any_extension(url, self.file_extensions)

    def _is_external_url(self, url):
        # type: (str) -> bool
        return url.startswith('http') or self._has_extension(url)

    def check_existing(self, response):
        # type: (Any) -> None
        self.log(response)

    def check_permalink(self, response):
        # type: (Any) -> None
        self.log(response)
        xpath_template = "//*[@id='{permalink}' or @name='{permalink}']"
        m = re.match(r".+\#(?P<permalink>.*)$", response.request.url)  # Get anchor value.
        if not m:
            return
        permalink = m.group('permalink')
        # Check permalink existing on response page.
        if not response.selector.xpath(xpath_template.format(permalink=permalink)):
            self._set_error_state()
            raise Exception(
                "Permalink #{} is not found on page {}".format(permalink, response.request.url))

    def parse(self, response):
        # type: (Any) -> Generator[Request, None, None]
        self.log(response)
        for link in LxmlLinkExtractor(deny_domains=self.deny_domains, deny_extensions=['doc'],
                                      tags=self.tags, attrs=self.attrs, deny=self.deny,
                                      canonicalize=False).extract_links(response):
            callback = self.parse  # type: Any
            dont_filter = False
            method = 'GET'
            if self._is_external_url(link.url):
                callback = self.check_existing
                method = 'HEAD'
            elif '#' in link.url:
                dont_filter = True
                callback = self.check_permalink
            yield Request(link.url, method=method, callback=callback, dont_filter=dont_filter,
                          errback=self.error_callback)

    def retry_request_with_get(self, request):
        # type: (Request) -> Generator[Request, None, None]
        request.method = 'GET'
        request.dont_filter = True
        yield request

    def exclude_error(self, url):
        # type: (str) -> bool
        if url in EXCLUDED_URLS:
            return True
        return False

    def error_callback(self, failure):
        # type: (Any) -> Optional[Generator[Any, None, None]]
        if hasattr(failure.value, 'response') and failure.value.response:
            response = failure.value.response
            if self.exclude_error(response.url):
                return None
            if response.status == 404:
                self._set_error_state()
                raise Exception('Page not found: {}'.format(response))
            if response.status == 405 and response.request.method == 'HEAD':
                # Method 'HEAD' not allowed, repeat request with 'GET'
                return self.retry_request_with_get(response.request)
            self.log("Error! Please check link: {}".format(response), logging.ERROR)
        elif isinstance(failure.type, IOError):
            self._set_error_state()
        else:
            raise Exception(failure.value)
        return None

tools/documentation_crawler/documentation_crawler/spiders/common/spiders.py
#!/usr/bin/env python
from __future__ import print_function

import logging
import re
import scrapy

from scrapy import Request
from scrapy.linkextractors import IGNORED_EXTENSIONS
from scrapy.linkextractors.lxmlhtml import LxmlLinkExtractor
from scrapy.utils.url import url_has_any_extension

from typing import Any, Generator, List, Optional, Tuple

EXCLUDED_URLS = [
    # Google calendar returns 404s on HEAD requests unconditionally
    'https://calendar.google.com/calendar/embed?src=ktiduof4eoh47lmgcl2qunnc0o@group.calendar.google.com',
    # Returns 409 errors to HEAD requests frequently
    'https://medium.freecodecamp.com',
    # Returns 404 to HEAD requests unconditionally
    'https://www.git-tower.com/blog/command-line-cheat-sheet/',
]


class BaseDocumentationSpider(scrapy.Spider):
    name = None  # type: Optional[str]
    # Exclude domain address.
    deny_domains = []  # type: List[str]
    start_urls = []  # type: List[str]
    deny = []  # type: List[str]
    file_extensions = ['.' + ext for ext in IGNORED_EXTENSIONS]  # type: List[str]
    tags = ('a', 'area', 'img')
    attrs = ('href', 'src')

    def __init__(self, *args, **kwargs):
        # type: (*Any, **Any) -> None
        super(BaseDocumentationSpider, self).__init__(*args, **kwargs)
        self.has_error = False

    def _set_error_state(self):
        # type: () -> None
        self.has_error = True

    def _has_extension(self, url):
        # type: (str) -> bool
        return url_has_any_extension(url, self.file_extensions)

    def _is_external_url(self, url):
        # type: (str) -> bool
        return url.startswith('http') or self._has_extension(url)

    def check_existing(self, response):
        # type: (Any) -> None
        self.log(response)

    def check_permalink(self, response):
        # type: (Any) -> None
        self.log(response)
        xpath_template = "//*[@id='{permalink}' or @name='{permalink}']"
        m = re.match(r".+\#(?P<permalink>.*)$", response.request.url)  # Get anchor value.
        if not m:
            return
        permalink = m.group('permalink')
        # Check permalink existing on response page.
        if not response.selector.xpath(xpath_template.format(permalink=permalink)):
            self._set_error_state()
            raise Exception(
                "Permalink #{} is not found on page {}".format(permalink, response.request.url))

    def parse(self, response):
        # type: (Any) -> Generator[Request, None, None]
        self.log(response)
        for link in LxmlLinkExtractor(deny_domains=self.deny_domains, deny_extensions=['doc'],
                                      tags=self.tags, attrs=self.attrs, deny=self.deny,
                                      canonicalize=False).extract_links(response):
            callback = self.parse  # type: Any
            dont_filter = False
            method = 'GET'
            if self._is_external_url(link.url):
                callback = self.check_existing
                method = 'HEAD'
            elif '#' in link.url:
                dont_filter = True
                callback = self.check_permalink
            yield Request(link.url, method=method, callback=callback, dont_filter=dont_filter,
                          errback=self.error_callback)

    def retry_request_with_get(self, request):
        # type: (Request) -> Generator[Request, None, None]
        request.method = 'GET'
        request.dont_filter = True
        yield request

    def exclude_error(self, url):
        # type: (str) -> bool
        if url in EXCLUDED_URLS:
            return True
        return False

    def error_callback(self, failure):
        # type: (Any) -> Optional[Generator[Any, None, None]]
        if hasattr(failure.value, 'response') and failure.value.response:
            response = failure.value.response
            if self.exclude_error(response.url):
                return None
            if response.status == 404:
                self._set_error_state()
                raise Exception('Page not found: {}'.format(response))
            if response.status == 405 and response.request.method == 'HEAD':
                # Method 'HEAD' not allowed, repeat request with 'GET'
                return self.retry_request_with_get(response.request)
            self.log("Error! Please check link: {}".format(response), logging.ERROR)
        elif isinstance(failure.type, IOError):
            self._set_error_state()
        else:
            raise Exception(failure.value)
        return None

tools/documentation_crawler/documentation_crawler/spiders/common/spiders.py
diff --git a/docs/fixing-commits.md b/docs/fixing-commits.md
new file mode 100644
index 0000000000000..9bd258475a910
--- /dev/null
+++ b/docs/fixing-commits.md
@@ -0,0 +1,35 @@
+# Fixing Commits
+This is mostly from
+[here](https://help.github.com/articles/changing-a-commit-message/#rewriting-the-most-recent-commit-message).
+
+## Fixing the last commit
+### Changing the last commit message
+1. `git commit --amend -m "New Message"`
+
+### Changing the last commit
+1. Make your changes to the files
+2. Run `git add <filename>` to add one file or `git add <filename1> <filename2> ...` to add multiple files
+3. `git commit --amend`
+
+## Fixing older commits
+### Changing commit messages
+1. `git rebase -i HEAD~5` (if, for example, you are editing some of the last five commits)
+2. For each commit that you want to change the message, change `pick` to `reword`, and save
+3. Change the commit messages
+
+### Deleting old commits
+1. `git rebase -i HEAD~n` where `n` is the number of commits you are looking at
+2. For each commit that you want to delete, change `pick` to `drop`, and save
+
+## Squashing commits
+Sometimes, you want to make one commit out of a bunch of commits. To do this,
+
+1. `git rebase -i HEAD~n` where `n` is the number of commits you are interested in
+2. Change `pick` to `squash` on the lines containing the commits you want to squash and save
+
+## Reordering commits
+1. `git rebase -i HEAD~n` where `n` is the number of commits you are interested in
+2. Reorder the lines containing the commits and save
+
+## Pushing commits after tidying them
+1. `git push origin +my-feature-branch`  (Note the `+` there and substitute your actual branch name.)
diff --git a/docs/git-cheat-sheet-detailed.md b/docs/git-cheat-sheet-detailed.md
new file mode 100644
index 0000000000000..21c578b0c9de8
--- /dev/null
+++ b/docs/git-cheat-sheet-detailed.md
@@ -0,0 +1,54 @@
+# Git Cheat Sheet (Detailed)
+
+See also
+[fixing commits][fix-commit]
+
+Commands:
+
+- add
+    - `git add foo.py`: add `foo.py` to the staging area
+    - `git add foo.py bar.py`: add `foo.py` AND `bar.py` to the staging area
+- checkout
+    - `git checkout -b new-branch-name`: create branch `new-branch-name` and switch/checkout to that new branch
+    - `git checkout master`: switch to your `master` branch
+    - `git checkout old-branch-name`: switch to an existing branch `old-branch-name`
+- commit
+    - `git commit --amend`: changing the last commit message. Read more [here][fix-commit]
+- config
+    - `git config --global core.editor nano`: set core editor to `nano` (you can set this to `vim` or others)
+    - `git config --global core.symlinks true`: allow symbolic links
+- diff
+    - `git diff`: display the changes you have made to all files
+    - `git diff --cached`: display the changes you have made to staged files
+    - `git diff HEAD~2..`: display the 2 most recent changes you have made to files
+- fetch
+    - `git fetch origin`: fetch origin repository
+    - `git fetch upstream`: fetch upstream repository
+- grep
+    - `git grep update_unread_counts -- '*.js'`: search all files (ending in `.js`) for `update_unread_counts`
+- log
+    - `git log`: show commit logs
+- pull
+    - **do not use for Zulip**
+- push
+    - `git push origin +branch-name`: push your commits to your origin repository
+- rebase
+    - `git rebase -i HEAD~3`: interactive rebasing current branch with first three items on HEAD
+    - `git rebase -i master`: interactive rebasing current branch with master branch
+    - `git rebase upstream/master`: rebasing current branch with master branch from upstream repository
+- reflog
+    - `git reflog | head -10`: manage reference logs for the past 10 commits
+- remote
+    - `git remote -v`: display your origin and upstream repositories
+- reset
+    - `git reset HEAD~2`: reset two most recent commits
+- rm
+    - `git rm oops.txt`: remove `oops.txt`
+- show
+    - `git show HEAD`: display most recent commit
+    - `git show HEAD~~~`: display third most recent commit
+    - `git show master`: display most recent commit on `master`
+- status
+    - `git status`: show the working tree status, unstaged and staged files
+
+[fix-commit]: fixing-commits.html
diff --git a/docs/git-cheat-sheet.md b/docs/git-cheat-sheet.md
new file mode 100644
index 0000000000000..1b8524d239e51
--- /dev/null
+++ b/docs/git-cheat-sheet.md
@@ -0,0 +1,52 @@
+# Git Cheat Sheet
+
+See also [fixing commits][fix-commit]
+
+Commands:
+
+- add
+    - `git add foo.py`
+- checkout
+    - `git checkout -b new-branch-name`
+    - `git checkout master`
+    - `git checkout old-branch-name`
+- commit
+    - `git commit --amend`
+- config
+    - `git config --global core.editor nano`
+    - `git config --global core.symlinks true`
+- diff
+    - `git diff`
+    - `git diff --cached`
+    - `git diff HEAD~2..`
+- fetch
+    - `git fetch origin`
+    - `git fetch upstream`
+- grep
+    - `git grep update_unread_counts -- '*.js'`
+- log
+    - `git log`
+- pull
+    - **do not use for Zulip**
+- push
+    - `git push origin +branch-name`
+- rebase
+    - `git rebase -i HEAD~3`
+    - `git rebase -i master`
+    - `git rebase upstream/master`
+- reflog
+    - `git reflog | head -10`
+- remote
+    - `git remote -v`
+- reset
+    - `git reset HEAD~2`
+- rm
+    - `git rm oops.txt`
+- show
+    - `git show HEAD`
+    - `git show HEAD~~~`
+    - `git show master`
+- status
+    - `git status`
+
+[fix-commit]: fixing-commits.html
diff --git a/docs/images/shell-screenshot.png b/docs/images/shell-screenshot.png
new file mode 100644
index 0000000000000..cda46476b8a88
Binary files /dev/null and b/docs/images/shell-screenshot.png differ
diff --git a/docs/index.rst b/docs/index.rst
index 62b60816c1c64..f1749f156433e 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -84,6 +84,11 @@ Contents:
    life-of-a-request
    reading-list
    screenshot-and-gif-software
+   fixing-commits
+   git-cheat-sheet-detailed
+   git-cheat-sheet
+   shell-tips
+   working-copies
 
 .. _code-contribution-guide:
 
diff --git a/docs/shell-tips.md b/docs/shell-tips.md
new file mode 100644
index 0000000000000..606d266a92182
--- /dev/null
+++ b/docs/shell-tips.md
@@ -0,0 +1,338 @@
+# Shell tips
+
+The *shell* is a **command line interpreter**. To use it you can open a
+*terminal* (sometimes called a *console*). This is how most terminal windows
+look like:
+
+![An example shell window](images/shell-screenshot.png)
+
+If you haven't used it before, you should probably take a look at
+[this tutorial](http://linuxcommand.org/lc3_learning_the_shell.php).
+
+If you're using Windows,
+[these videos](https://www.youtube.com/playlist?list=PL6gx4Cwl9DGDV6SnbINlVUd0o2xT4JbMu)
+may be useful too, but keep in mind that the following tips only apply to
+Linux/macOS environments (Unix shells). You can also use a tool, for example
+[Cygwin](https://www.cygwin.com/), to have a Unix-like shell on Windows.
+
+## The prompt (`$`)
+
+When searching Google, or Zulip's docs, you'll find commands that begin
+with a dollar sign `$` or a dollar sign preceded by some text
+(e.g. `(venv)john@laptop:~$`).
+
+This is called the **prompt**, and it's only an indicator that the shell is
+awaiting new orders. The prompt can contain useful information, let's look
+at `(venv)john@laptop:~$`:
+
+- `(venv)` informs the user that they're currently in a virtual environment
+(more on [Python virtual
+  environments](http://docs.python-guide.org/en/latest/dev/virtualenvs/))
+- the `john` before `@` is the username
+- the `laptop` is the host machine name
+- the `~` after the colon informs the user they're currently in the home
+folder of the user `john`
+
+You shouldn't type the prompt or the text preceding it, since it isn't a
+part of the commands.
+
+## Tilde character (`~`)
+
+It's very frequent to see the tilde (`~`) in paths. The tilde is an
+abbreviation for your home directory (`/home/YOUR_USERNAME` most of the times).
+
+That's why the following is exactly the same, if the user running it is
+`john`:
+
+```
+$ cd ~
+$ cd /home/john
+```
+
+## Change directory (`cd`)
+
+When you're using the shell, you work inside a directory (the one specified in
+the prompt). This way you can point to files relative to your current
+directory, instead of writing the whole path.
+
+Imagine you have a file called `ideas.txt` inside `/home/john/notes/`, and
+you want to edit it using `nano`. You could use:
+
+```
+$ nano /home/john/notes/ideas.txt
+```
+
+However, that isn't very practical, especially if you are working with
+longer paths.
+
+That's why it's very useful to change the path where you are currently
+located (usually known as **working directory**). To do that, you use `cd`
+(**c**hange **d**irectory):
+
+```
+$ cd /home/john/notes/
+~/notes$ nano ideas.txt
+```
+
+Or, if you're the user `john`: `cd ~/notes`.
+
+You can now access to all the files inside `/home/john/notes` directly, without
+needing to type the whole path.
+
+[Relative paths](http://www.linuxnix.com/abslute-path-vs-relative-path-in-linuxunix/)
+make it much easier to move through files and directories, too.
+
+## Running commands as root (`sudo`)
+
+You may have noticed that many commands begin with `sudo`. This informs the
+shell that the following command must be run as the root - a user that by
+default has access to all commands and files on a Unix operating system (i.e.
+a user with administrator privileges). That's why you may be asked for a
+password in those cases: the system verifies you have permission to act as
+the *root* user.
+
+In case you were wondering, the name `sudo` comes from **s**uper **u**ser
+**do**.
+
+## Escaping characters
+
+Some characters cannot be used directly in the shell, because they have a
+special meaning. Consider the following example:
+
+```
+$ echo "He said hello"
+He said hello
+```
+
+What if you wanted to display double quotes? You can't use
+`echo "He said "hello""`, because in that case you're using the
+double quotes for two different purposes:
+
+- Delimiting the string you want to use, from `He` to `"hello"`.
+- Quoting something, by literally printing `"`.
+
+You have to specify which double quotes are used in each case. When you want
+one of those "special characters" to be literally printed, that's called
+**character escaping**. To escape a character, simply add a backslash (`\`)
+before it.
+
+Returning to our example:
+
+```
+$ echo "He said \"hello\""
+He said "hello"
+```
+
+As you can see, the double quotes with the backslash are shown, but the ones
+without it are used as string delimiters.
+
+Double quotes aren't the only case of special characters. Some others are `$`,
+`#`, `{` or `}`, but there are many more. The backslash itself can be escaped
+as well, using the same procedure: `\\`.
+
+## Sequencing commands
+
+It's also possible to run multiple commands in a single line. For that purpose,
+the shell provides two different separators:
+
+- **Semicolon `;`**: runs a command, and once it has finished, runs the next
+  one:
+
+  ```
+  $ echo "Hello"; echo "World!"
+  Hello
+  World!
+  ```
+
+- **Double ampersand `&&`**: runs a command, and **only if** it finished
+  without errors, it proceeds with the next one:
+
+  ```
+  $ qwfvijwe && echo "Hello"
+  qwfvijwe: command not found
+  ```
+
+  Notice that it doesn't print `Hello` at the end, because the previous
+  command (`qwfvijwe`) returned an error.
+
+  When using an incorrect command with a semicolon, the `Hello` will still
+  be printed:
+
+  ```
+  $ qwfvijwe; echo "Hello"
+  qwfvijwe: command not found
+  Hello
+  ```
+
+## Splitting commands into multiple lines
+
+Sometimes you end up with a very long command, that is hard to read and may
+be unclear. This is a problem, especially if you want to share that command,
+e.g. in a documentation file.
+
+In those cases, you can use a backslash at the end of each line, to inform the
+shell "wait, there's more on the next line".
+
+This is an example, taken from the docs on how to install the Zulip development
+environment:
+
+```
+sudo apt-get -y purge vagrant && \
+wget https://releases.hashicorp.com/vagrant/1.8.6/vagrant_1.8.6_x86_64.deb && \
+sudo dpkg -i vagrant*.deb && \
+sudo apt-get -y install build-essential git ruby lxc lxc-templates cgroup-lite redir && \
+vagrant plugin install vagrant-lxc && \
+vagrant lxc sudoers
+```
+
+It's all a single command, joined using the double ampersand explained in
+[Sequencing commands](#sequencing-commands). If you're typing it manually,
+you don't need to include the backslashes, just write it all on the same line,
+and hit <kbd>ENTER</kbd>/<kbd>RETURN</kbd> at the end.
+
+If you think about it, what is happening here is actually another case of
+character escaping. The newline character (the one that appears when you hit
+<kbd>ENTER</kbd>) usually means "read this command". However, here we want to
+literally have the newline character, and thus the `\<newline>`.
+
+The newline character is invisible (we only see a line break), but it's still
+there!
+
+## Arguments
+
+Most commands need additional data to work, like a path or a file. That extra
+information is called an **argument**, and it's specified after the name of the
+command, like this:
+
+```
+$ cd /home/john/notes
+```
+
+Here, the command is `cd`, and the first (and only) argument is
+`/home/john/notes`:
+
+- `cd` - *command*: changes your current directory.
+
+- `/home/john/notes` - *argument*: the directory where you want to go.
+
+In each command the arguments are specified in different ways, and have
+different meanings.
+
+Sometimes, a command can accept arguments indicated with dashes. Here's another
+example of arguments usage:
+
+```
+$ nano -C /home/john/backups --mouse todo.txt
+```
+
+As you can see, some arguments imply that more information has to be specified,
+while others don't.
+
+In this case, we're saying: "Bash, use the app `nano` to open the file
+`todo.txt`, enabling mouse support, and saving the backup files to
+`/home/john/backups`". The different parts are:
+
+- `nano` - *command*: program that allows editing text easily.
+
+- `-C` - *argument*: needs you to indicate where the backups should be stored,
+  and thus you have to add an additional argument after it, to specify the
+  directory (`/home/john/backups` in the example).
+
+- `--mouse` - *argument*: is just an option you set, `nano` doesn't need
+  anything else to make it work. Thus, there isn't any extra argument for that.
+
+Note that the `todo.txt` is the file we want to open! It has nothing to do with
+the previous argument. This will probably clarify it (taken from `nano`'s
+help):
+
+```
+Usage: nano [OPTIONS] [FILE]...
+```
+
+So, in the options you indicate the arguments, and `FILE` is... well, the file.
+
+Don't worry, you don't have to memorize the meaning of
+all the arguments for every single command. There are
+[tools](#understanding-commands) that help you with that :wink:.
+
+## Shebang
+
+You can run some files directly, without specifying a program to interpret
+them.
+
+That's why you may have seen cases when some Python scripts are called with
+`python`:
+
+```
+$ python my_program.py
+```
+
+While other times, `python` isn't used:
+
+```
+$ ./my_program.py
+```
+
+In the latter, it's skipped because `my_program.py` already specifies in it
+which interpreter should be used (in this case, `python`).
+
+This is indicated in the very first line of the script files, and it's called
+a **shebang**. In Python scripts, it looks like this:
+
+```
+#!/usr/bin/env python
+```
+
+With this, you're telling the shell: "if I tell you to run this, ask
+`/usr/bin/env python` how to understand it".
+
+`/usr/bin/env` is a way to identify where `python` is installed. If it was in
+`/usr/bin/python`, you could use the shebang `#!/usr/bin/python`, but `env`
+allows more flexibility (since not everyone has their Python interpreter
+there).
+
+Another example of shebang is the one used in Bash scripts. In those cases,
+`#!/bin/sh` is used.
+
+The result is that the shell calls the program specified in the shebang, with
+the script as a parameter. So, returning to our example with `my_program.py`,
+when you run `./my_program.py`, what happens under the hood is:
+
+```
+$ /usr/bin/env python my_program.py
+```
+
+## Understanding commands
+
+Frequently, you may find commands that you don't understand, or don't
+know what they do. You can use `man <command>` to see the **man**ual page for
+that specific command. Also, you may find useful
+[explainshell](http://explainshell.com/), a webpage that explains what most
+commands do, part by part.
+
+## Cheatsheet
+
+There are many more commands in the shell, besides the ones explained in this
+file.
+[Here](https://www.git-tower.com/blog/command-line-cheat-sheet/) you can find
+a simple yet useful cheatsheet, created by Tower, that could help you
+understand and remember what other common commands do (e.g. `ls`).
+
+## Git
+
+Probably at this point you've heard about Git. It's basically a tool that most
+developers use to manage all the changes in their code.
+
+At first it seems like magic, but once you get the basic concepts you find it
+extremely useful and even easy to use (at least the 99% of the time).
+
+To learn more about how to use it, read
+[our docs](http://zulip.readthedocs.io/en/latest/git-guide.html) on Git and
+Github.
+
+[This cheatsheet][git-cheat-detailed]
+will be useful in your journey, as well.
+
+![Git - XKCD 1597](https://imgs.xkcd.com/comics/git.png)
+
+[git-cheat-detailed]: git-cheat-sheet-detailed.html
diff --git a/docs/working-copies.md b/docs/working-copies.md
new file mode 100644
index 0000000000000..52a0f9cfa6f26
--- /dev/null
+++ b/docs/working-copies.md
@@ -0,0 +1,53 @@
+# Working copies
+
+When you work on Zulip code, there are three working copies
+of the Zulip git repo that you are generally concerned with:
+
+- local copy: This lives on your laptop or your remove dev instance.
+- forked copy: This lives on GitHub, and it's tied to your account.
+- official Zulip repo: https://github.com/zulip/zulip
+
+We sometimes call the forked copy the **origin** remote.
+
+We sometimes call the official repo the **upstream** remote.
+
+When you work on Zulip code, you will end up moving code between
+the various working copies.
+
+## Workflows
+
+Sometimes you need to get commits.  Here are some scenarios:
+
+- You may fork the official Zulip repo to your GitHub fork.
+- You may fetch commits from the offical Zulip repo to your local copy.
+- You occasionally may fetch commits from your forked copy.
+
+Sometimes you want to publish commits.  Here are scenarios:
+
+- You push code from your local copy to your GitHub fork.  (You usually
+  want to put the commit on a feature branch.)
+- You submit a PR to the official Zulip repo.
+
+Finally, the Zulip core team will occasionally want your changes!
+
+- The Zulip core team can accept your changes and add them to
+  the official repo, usually on the master branch.
+
+## Names
+
+We call remote working copies of the repository by these short
+names.
+
+- **origin**: This is your fork.
+- **upstream**: This is the official Zulip repo.
+
+## Relevant git commands
+
+The following commands are useful for moving commits between
+working copies:
+
+- `git fetch`: This grabs code from another repo to your local copy.
+- `git push`: This pushes code from your local repo to one of the remotes.
+- `git remote`: This helps you configure short names for remotes.
+- `git pull`: **Do not use this, please**!
+
diff --git a/tools/documentation_crawler/documentation_crawler/spiders/common/spiders.py b/tools/documentation_crawler/documentation_crawler/spiders/common/spiders.py
index 29d88ae4e360d..408504f7043d0 100644
--- a/tools/documentation_crawler/documentation_crawler/spiders/common/spiders.py
+++ b/tools/documentation_crawler/documentation_crawler/spiders/common/spiders.py
@@ -17,6 +17,8 @@
     'https://calendar.google.com/calendar/embed?src=ktiduof4eoh47lmgcl2qunnc0o@group.calendar.google.com',
     # Returns 409 errors to HEAD requests frequently
     'https://medium.freecodecamp.com',
+    # Returns 404 to HEAD requests unconditionally
+    'https://www.git-tower.com/blog/command-line-cheat-sheet/',
 ]
 
 

Pull GCI docs into main Zulip repo.
Some of our assets, like git cheatsheets, currently live in the zulip-gci repo.  We should move them to the main Zulip repo and link from appropriate places.

cc @synicalsyntax @lonerz @tommyip 
