diff --git a/src/repository/api/ArcanistGitAPI.php b/src/repository/api/ArcanistGitAPI.php
--- a/src/repository/api/ArcanistGitAPI.php
+++ b/src/repository/api/ArcanistGitAPI.php
@@ -1333,4 +1333,71 @@
     $this->resolvedHeadCommit = null;
   }
 
+  /**
+   * Follow the chain of tracking branches upstream until we reach a remote
+   * or cycle locally.
+   *
+   * @param string Ref to start from.
+   * @return list<wild> Path to an upstream.
+   */
+  public function getPathToUpstream($start) {
+    $cursor = $start;
+    $path = array();
+    while (true) {
+      list($err, $upstream) = $this->execManualLocal(
+        'rev-parse --symbolic-full-name %s@{upstream}',
+        $cursor);
+
+      if ($err) {
+        // We ended up somewhere with no tracking branch, so we're done.
+        break;
+      }
+
+      $upstream = trim($upstream);
+
+      if (preg_match('(^refs/heads/)', $upstream)) {
+        $upstream = preg_replace('(^refs/heads/)', '', $upstream);
+
+        $is_cycle = isset($path[$upstream]);
+
+        $path[$cursor] = array(
+          'type' => 'local',
+          'name' => $upstream,
+          'cycle' => $is_cycle,
+        );
+
+        if ($is_cycle) {
+          // We ran into a local cycle, so we're done.
+          break;
+        }
+
+        // We found another local branch, so follow that one upriver.
+        $cursor = $upstream;
+        continue;
+      }
+
+      if (preg_match('(^refs/remotes/)', $upstream)) {
+        $upstream = preg_replace('(^refs/remotes/)', '', $upstream);
+        list($remote, $branch) = explode('/', $upstream, 2);
+
+        $path[$cursor] = array(
+          'type' => 'remote',
+          'name' => $branch,
+          'remote' => $remote,
+        );
+
+        // We found a remote, so we're done.
+        break;
+      }
+
+      throw new Exception(
+        pht(
+          'Got unrecognized upstream format ("%s") from Git, expected '.
+          '"refs/heads/..." or "refs/remotes/...".',
+          $upstream));
+    }
+
+    return $path;
+  }
+
 }
diff --git a/src/workflow/ArcanistLandWorkflow.php b/src/workflow/ArcanistLandWorkflow.php
--- a/src/workflow/ArcanistLandWorkflow.php
+++ b/src/workflow/ArcanistLandWorkflow.php
@@ -38,7 +38,7 @@
 
   public function getCommandSynopses() {
     return phutil_console_format(<<<EOTEXT
-      **land** [__options__] [__branch__] [--onto __master__]
+      **land** [__options__] [__ref__]
 EOTEXT
       );
   }
@@ -47,18 +47,68 @@
     return phutil_console_format(<<<EOTEXT
           Supports: git, hg
 
-          Land an accepted change (currently sitting in local feature branch
-          __branch__) onto __master__ and push it to the remote. Then, delete
-          the feature branch. If you omit __branch__, the current branch will
-          be used.
+          Publish an accepted revision after review. This command is the last
+          step in the standard Differential pre-publish code review workflow.
 
-          In mutable repositories, this will perform a --squash merge (the
-          entire branch will be represented by one commit on __master__). In
-          immutable repositories (or when --merge is provided), it will perform
-          a --no-ff merge (the branch will always be merged into __master__ with
-          a merge commit).
+          This workflow merges and pushes changes associated with an accepted
+          revision that are currently sitting in __ref__, which is usually the
+          name of a local branch. Without __ref__, the current working copy
+          state will be used.
+
+          Under Git: branches, tags, and arbitrary commits (detached HEADs)
+          may be landed.
+
+          Under Mercurial: branches and bookmarks may be landed, but only
+          onto a target of the same type. See T3855.
+
+          The workflow selects a target branch to land onto and a remote where
+          the change will be pushed to.
+
+          A target branch is selected by examining these sources in order:
+
+            - the **--onto** flag;
+            - the upstream of the current branch, recursively (Git only);
+            - the __arc.land.onto.default__ configuration setting;
+            - or falling back to a "master" in Git or "default" in Mercurial.
+
+          A remote is selected by examining these sources in order:
+
+            - the **--remote** flag;
+            - the upstream of the current branch, recursively (Git only);
+            - or falling back to "origin" in Git or the default remote in
+              Mercurial.
+
+          After selecting a target branch and a remote, the commits which will
+          be landed are printed.
+
+          With **--preview**, execution stops here, before the change is
+          merged.
+
+          The change is merged into the target branch, following these rules:
+
+          In mutable repositories or with **--squash**, this will perform a
+          squash merge (the entire branch will be represented as one commit on
+          the target branch).
+
+          In immutable repositories or with **--merge**, this will perform a
+          strict merge (a merge commit will always be created, and local
+          commits will be preserved).
+
+          The resulting commit will be given an up-to-date commit message
+          describing the state of the revision in Differential.
+
+          With **--hold**, execution stops here, before the change is pushed.
+
+          The change is pushed into the remote.
+
+          Consulting mystical sources of power, the workflow makes a guess
+          about what state you wanted to end up in after the land finishes,
+          and the working copy is put into that state.
+
+          The branch which was landed is deleted, unless the **--keep-branch**
+          flag was passed or the landing branch is the same as the target
+          branch.
 
-          Under hg, bookmarks can be landed the same way as branches.
 EOTEXT
       );
   }
@@ -203,6 +253,8 @@
     }
 
     if ($engine) {
+      $this->readEngineArguments();
+
       $obsolete = array(
         'delete-remote',
         'update-with-merge',
@@ -236,7 +288,7 @@
 
       $engine->execute();
 
-      if (!$should_hold) {
+      if (!$should_hold && !$this->preview) {
         $this->didPush();
       }
 
@@ -303,6 +355,137 @@
     return null;
   }
 
+  private function readEngineArguments() {
+    // NOTE: This is hard-coded for Git right now.
+    // TODO: Clean this up and move it into LandEngines.
+
+    $onto = $this->getEngineOnto();
+    $remote = $this->getEngineRemote();
+
+    // This just overwrites work we did earlier, but it has to be up in this
+    // class for now because other parts of the workflow still depend on it.
+    $this->onto = $onto;
+    $this->remote = $remote;
+    $this->ontoRemoteBranch = $this->remote.'/'.$onto;
+  }
+
+  private function getEngineOnto() {
+    $onto = $this->getArgument('onto');
+    if ($onto !== null) {
+      $this->writeInfo(
+        pht('TARGET'),
+        pht(
+          'Landing onto "%s", selected by the --onto flag.',
+          $onto));
+      return $onto;
+    }
+
+    $api = $this->getRepositoryAPI();
+    $path = $api->getPathToUpstream($this->branch);
+
+    if ($path) {
+      $last = last($path);
+      if (isset($last['cycle'])) {
+        $this->writeWarn(
+          pht('LOCAL CYCLE'),
+          pht(
+            'Local branch tracks an upstream, but following it leads to a '.
+            'local cycle; ignoring branch upstream.'));
+
+        echo tsprintf(
+          "\n    %s\n\n",
+          $this->formatUpstreamPathCycle($path));
+
+      } else {
+        if ($last['type'] == 'remote') {
+          $onto = $last['name'];
+          $this->writeInfo(
+            pht('TARGET'),
+            pht(
+              'Landing onto "%s", selected by following tracking branches '.
+              'upstream to the closest remote.',
+              $onto));
+          return $onto;
+        } else {
+          $this->writeInfo(
+            pht('NO PATH TO UPSTREAM'),
+            pht(
+              'Local branch tracks an upstream, but there is no path '.
+              'to a remote; ignoring branch upstream.'));
+        }
+      }
+    }
+
+    $config_key = 'arc.land.onto.default';
+    $onto = $this->getConfigFromAnySource($config_key);
+    if ($onto !== null) {
+      $this->writeInfo(
+        pht('TARGET'),
+        pht(
+          'Landing onto "%s", selected by "%s" configuration.',
+          $onto,
+          $config_key));
+      return $onto;
+    }
+
+    $onto = 'master';
+    $this->writeInfo(
+      pht('TARGET'),
+      pht(
+        'Landing onto "%s", the default target under git.',
+        $onto));
+    return $onto;
+  }
+
+  private function getEngineRemote() {
+    $remote = $this->getArgument('remote');
+    if ($remote !== null) {
+      $this->writeInfo(
+        pht('REMOTE'),
+        pht(
+          'Using remote "%s", selected by the --remote flag.',
+          $remote));
+      return $remote;
+    }
+
+    $api = $this->getRepositoryAPI();
+    $path = $api->getPathToUpstream($this->branch);
+
+    if ($path) {
+      $last = last($path);
+      if ($last['type'] == 'remote') {
+        $remote = $last['remote'];
+        $this->writeInfo(
+          pht('REMOTE'),
+          pht(
+            'Using remote "%s", selected by following tracking branches '.
+            'upstream to the closest remote.',
+            $remote));
+        return $remote;
+      }
+    }
+
+    $remote = 'origin';
+    $this->writeInfo(
+      pht('REMOTE'),
+      pht(
+        'Using remote "%s", the default remote under git.',
+        $remote));
+    return $remote;
+  }
+
+  private function formatUpstreamPathCycle(array $cycle) {
+    $parts = array();
+    foreach ($cycle as $key => $value) {
+      $parts[] = $key;
+    }
+    $parts[] = idx(last($cycle), 'name');
+    $parts[] = pht('...');
+
+    return implode(' -> ', $parts);
+  }
+
+
   private function readArguments() {
     $repository_api = $this->getRepositoryAPI();
     $this->isGit = $repository_api instanceof ArcanistGitAPI;
@@ -320,9 +503,12 @@
     $branch = $this->getArgument('branch');
     if (empty($branch)) {
       $branch = $this->getBranchOrBookmark();
-
       if ($branch) {
         $this->branchType = $this->getBranchType($branch);
+
+        // TODO: This message is misleading when landing a detached head or
+        // a tag in Git.
+
         echo pht("Landing current %s '%s'.", $this->branchType, $branch), "\n";
         $branch = array($branch);
       }