cat-file: split --batch input lines on whitespace
[gitweb.git] / remote.c
index e2ef99114478c49863809ea5cef46392a5a8c0e9..68eb99bdf0ca3fedd02ab1bc93db7549b21b62d4 100644 (file)
--- a/remote.c
+++ b/remote.c
@@ -7,11 +7,15 @@
 #include "dir.h"
 #include "tag.h"
 #include "string-list.h"
+#include "mergesort.h"
+
+enum map_direction { FROM_SRC, FROM_DST };
 
 static struct refspec s_tag_refspec = {
        0,
        1,
        0,
+       0,
        "refs/tags/*",
        "refs/tags/*"
 };
@@ -45,6 +49,7 @@ static int branches_nr;
 
 static struct branch *current_branch;
 static const char *default_remote_name;
+static const char *pushremote_name;
 static int explicit_default_remote_name;
 
 static struct rewrites rewrites;
@@ -353,13 +358,16 @@ static int handle_config(const char *key, const char *value, void *cb)
                        return 0;
                branch = make_branch(name, subkey - name);
                if (!strcmp(subkey, ".remote")) {
-                       if (!value)
-                               return config_error_nonbool(key);
-                       branch->remote_name = xstrdup(value);
+                       if (git_config_string(&branch->remote_name, key, value))
+                               return -1;
                        if (branch == current_branch) {
                                default_remote_name = branch->remote_name;
                                explicit_default_remote_name = 1;
                        }
+               } else if (!strcmp(subkey, ".pushremote")) {
+                       if (branch == current_branch)
+                               if (git_config_string(&pushremote_name, key, value))
+                                       return -1;
                } else if (!strcmp(subkey, ".merge")) {
                        if (!value)
                                return config_error_nonbool(key);
@@ -385,9 +393,16 @@ static int handle_config(const char *key, const char *value, void *cb)
                        add_instead_of(rewrite, xstrdup(value));
                }
        }
+
        if (prefixcmp(key,  "remote."))
                return 0;
        name = key + 7;
+
+       /* Handle remote.* variables */
+       if (!strcmp(name, "pushdefault"))
+               return git_config_string(&pushremote_name, key, value);
+
+       /* Handle remote.<name>.* variables */
        if (*name == '/') {
                warning("Config remote shorthand cannot begin with '/': %s",
                        name);
@@ -482,7 +497,7 @@ static void read_config(void)
                return;
        default_remote_name = xstrdup("origin");
        current_branch = NULL;
-       head_ref = resolve_ref("HEAD", sha1, 0, &flag);
+       head_ref = resolve_ref_unsafe("HEAD", sha1, 0, &flag);
        if (head_ref && (flag & REF_ISSYMREF) &&
            !prefixcmp(head_ref, "refs/heads/")) {
                current_branch =
@@ -535,7 +550,7 @@ static struct refspec *parse_refspec_internal(int nr_refspec, const char **refsp
 
                /*
                 * Before going on, special case ":" (or "+:") as a refspec
-                * for matching refs.
+                * for pushing matching refs.
                 */
                if (!fetch && rhs == lhs && rhs[1] == '\0') {
                        rs[i].matching = 1;
@@ -562,26 +577,25 @@ static struct refspec *parse_refspec_internal(int nr_refspec, const char **refsp
                flags = REFNAME_ALLOW_ONELEVEL | (is_glob ? REFNAME_REFSPEC_PATTERN : 0);
 
                if (fetch) {
-                       /*
-                        * LHS
-                        * - empty is allowed; it means HEAD.
-                        * - otherwise it must be a valid looking ref.
-                        */
+                       unsigned char unused[40];
+
+                       /* LHS */
                        if (!*rs[i].src)
-                               ; /* empty is ok */
-                       else if (check_refname_format(rs[i].src, flags))
+                               ; /* empty is ok; it means "HEAD" */
+                       else if (llen == 40 && !get_sha1_hex(rs[i].src, unused))
+                               rs[i].exact_sha1 = 1; /* ok */
+                       else if (!check_refname_format(rs[i].src, flags))
+                               ; /* valid looking ref is ok */
+                       else
                                goto invalid;
-                       /*
-                        * RHS
-                        * - missing is ok, and is same as empty.
-                        * - empty is ok; it means not to store.
-                        * - otherwise it must be a valid looking ref.
-                        */
+                       /* RHS */
                        if (!rs[i].dst)
-                               ; /* ok */
+                               ; /* missing is ok; it is the same as empty */
                        else if (!*rs[i].dst)
-                               ; /* ok */
-                       else if (check_refname_format(rs[i].dst, flags))
+                               ; /* empty is ok; it means "do not store" */
+                       else if (!check_refname_format(rs[i].dst, flags))
+                               ; /* valid looking ref is ok */
+                       else
                                goto invalid;
                } else {
                        /*
@@ -668,17 +682,21 @@ static int valid_remote_nick(const char *name)
        return !strchr(name, '/'); /* no slash */
 }
 
-struct remote *remote_get(const char *name)
+static struct remote *remote_get_1(const char *name, const char *pushremote_name)
 {
        struct remote *ret;
        int name_given = 0;
 
-       read_config();
        if (name)
                name_given = 1;
        else {
-               name = default_remote_name;
-               name_given = explicit_default_remote_name;
+               if (pushremote_name) {
+                       name = pushremote_name;
+                       name_given = 1;
+               } else {
+                       name = default_remote_name;
+                       name_given = explicit_default_remote_name;
+               }
        }
 
        ret = make_remote(name, 0);
@@ -697,6 +715,18 @@ struct remote *remote_get(const char *name)
        return ret;
 }
 
+struct remote *remote_get(const char *name)
+{
+       read_config();
+       return remote_get_1(name, NULL);
+}
+
+struct remote *pushremote_get(const char *name)
+{
+       read_config();
+       return remote_get_1(name, pushremote_name);
+}
+
 int remote_is_configured(const char *name)
 {
        int i;
@@ -916,6 +946,27 @@ void free_refs(struct ref *ref)
        }
 }
 
+int ref_compare_name(const void *va, const void *vb)
+{
+       const struct ref *a = va, *b = vb;
+       return strcmp(a->name, b->name);
+}
+
+static void *ref_list_get_next(const void *a)
+{
+       return ((const struct ref *)a)->next;
+}
+
+static void ref_list_set_next(void *a, void *next)
+{
+       ((struct ref *)a)->next = next;
+}
+
+void sort_ref_list(struct ref **l, int (*cmp)(const void *, const void *))
+{
+       *l = llist_mergesort(*l, ref_list_get_next, ref_list_set_next, cmp);
+}
+
 static int count_refspec_match(const char *pattern,
                               struct ref *refs,
                               struct ref **matched_ref)
@@ -978,16 +1029,20 @@ static void tail_link_ref(struct ref *ref, struct ref ***tail)
        *tail = &ref->next;
 }
 
+static struct ref *alloc_delete_ref(void)
+{
+       struct ref *ref = alloc_ref("(delete)");
+       hashclr(ref->new_sha1);
+       return ref;
+}
+
 static struct ref *try_explicit_object_name(const char *name)
 {
        unsigned char sha1[20];
        struct ref *ref;
 
-       if (!*name) {
-               ref = alloc_ref("(delete)");
-               hashclr(ref->new_sha1);
-               return ref;
-       }
+       if (!*name)
+               return alloc_delete_ref();
        if (get_sha1(name, sha1))
                return NULL;
        ref = alloc_ref(name);
@@ -1007,7 +1062,7 @@ static char *guess_ref(const char *name, struct ref *peer)
        struct strbuf buf = STRBUF_INIT;
        unsigned char sha1[20];
 
-       const char *r = resolve_ref(peer->name, sha1, 1, NULL);
+       const char *r = resolve_ref_unsafe(peer->name, sha1, 1, NULL);
        if (!r)
                return NULL;
 
@@ -1058,7 +1113,7 @@ static int match_explicit(struct ref *src, struct ref *dst,
                unsigned char sha1[20];
                int flag;
 
-               dst_value = resolve_ref(matched_src->name, sha1, 1, &flag);
+               dst_value = resolve_ref_unsafe(matched_src->name, sha1, 1, &flag);
                if (!dst_value ||
                    ((flag & REF_ISSYMREF) &&
                     prefixcmp(dst_value, "refs/heads/")))
@@ -1072,6 +1127,9 @@ static int match_explicit(struct ref *src, struct ref *dst,
        case 0:
                if (!memcmp(dst_value, "refs/", 5))
                        matched_dst = make_linked_ref(dst_value, dst_tail);
+               else if (is_null_sha1(matched_src->new_sha1))
+                       error("unable to delete '%s': remote ref does not exist",
+                             dst_value);
                else if ((dst_guess = guess_ref(dst_value, matched_src)))
                        matched_dst = make_linked_ref(dst_guess, dst_tail);
                else
@@ -1110,10 +1168,11 @@ static int match_explicit_refs(struct ref *src, struct ref *dst,
        return errs;
 }
 
-static const struct refspec *check_pattern_match(const struct refspec *rs,
-                                                int rs_nr,
-                                                const struct ref *src)
+static char *get_ref_match(const struct refspec *rs, int rs_nr, const struct ref *ref,
+               int send_mirror, int direction, const struct refspec **ret_pat)
 {
+       const struct refspec *pat;
+       char *name;
        int i;
        int matching_refs = -1;
        for (i = 0; i < rs_nr; i++) {
@@ -1123,14 +1182,36 @@ static const struct refspec *check_pattern_match(const struct refspec *rs,
                        continue;
                }
 
-               if (rs[i].pattern && match_name_with_pattern(rs[i].src, src->name,
-                                                            NULL, NULL))
-                       return rs + i;
+               if (rs[i].pattern) {
+                       const char *dst_side = rs[i].dst ? rs[i].dst : rs[i].src;
+                       int match;
+                       if (direction == FROM_SRC)
+                               match = match_name_with_pattern(rs[i].src, ref->name, dst_side, &name);
+                       else
+                               match = match_name_with_pattern(dst_side, ref->name, rs[i].src, &name);
+                       if (match) {
+                               matching_refs = i;
+                               break;
+                       }
+               }
        }
-       if (matching_refs != -1)
-               return rs + matching_refs;
-       else
+       if (matching_refs == -1)
                return NULL;
+
+       pat = rs + matching_refs;
+       if (pat->matching) {
+               /*
+                * "matching refs"; traditionally we pushed everything
+                * including refs outside refs/heads/ hierarchy, but
+                * that does not make much sense these days.
+                */
+               if (!send_mirror && prefixcmp(ref->name, "refs/heads/"))
+                       return NULL;
+               name = xstrdup(ref->name);
+       }
+       if (ret_pat)
+               *ret_pat = pat;
+       return name;
 }
 
 static struct ref **tail_ref(struct ref **head)
@@ -1141,6 +1222,101 @@ static struct ref **tail_ref(struct ref **head)
        return tail;
 }
 
+struct tips {
+       struct commit **tip;
+       int nr, alloc;
+};
+
+static void add_to_tips(struct tips *tips, const unsigned char *sha1)
+{
+       struct commit *commit;
+
+       if (is_null_sha1(sha1))
+               return;
+       commit = lookup_commit_reference_gently(sha1, 1);
+       if (!commit || (commit->object.flags & TMP_MARK))
+               return;
+       commit->object.flags |= TMP_MARK;
+       ALLOC_GROW(tips->tip, tips->nr + 1, tips->alloc);
+       tips->tip[tips->nr++] = commit;
+}
+
+static void add_missing_tags(struct ref *src, struct ref **dst, struct ref ***dst_tail)
+{
+       struct string_list dst_tag = STRING_LIST_INIT_NODUP;
+       struct string_list src_tag = STRING_LIST_INIT_NODUP;
+       struct string_list_item *item;
+       struct ref *ref;
+       struct tips sent_tips;
+
+       /*
+        * Collect everything we know they would have at the end of
+        * this push, and collect all tags they have.
+        */
+       memset(&sent_tips, 0, sizeof(sent_tips));
+       for (ref = *dst; ref; ref = ref->next) {
+               if (ref->peer_ref &&
+                   !is_null_sha1(ref->peer_ref->new_sha1))
+                       add_to_tips(&sent_tips, ref->peer_ref->new_sha1);
+               else
+                       add_to_tips(&sent_tips, ref->old_sha1);
+               if (!prefixcmp(ref->name, "refs/tags/"))
+                       string_list_append(&dst_tag, ref->name);
+       }
+       clear_commit_marks_many(sent_tips.nr, sent_tips.tip, TMP_MARK);
+
+       sort_string_list(&dst_tag);
+
+       /* Collect tags they do not have. */
+       for (ref = src; ref; ref = ref->next) {
+               if (prefixcmp(ref->name, "refs/tags/"))
+                       continue; /* not a tag */
+               if (string_list_has_string(&dst_tag, ref->name))
+                       continue; /* they already have it */
+               if (sha1_object_info(ref->new_sha1, NULL) != OBJ_TAG)
+                       continue; /* be conservative */
+               item = string_list_append(&src_tag, ref->name);
+               item->util = ref;
+       }
+       string_list_clear(&dst_tag, 0);
+
+       /*
+        * At this point, src_tag lists tags that are missing from
+        * dst, and sent_tips lists the tips we are pushing or those
+        * that we know they already have. An element in the src_tag
+        * that is an ancestor of any of the sent_tips needs to be
+        * sent to the other side.
+        */
+       if (sent_tips.nr) {
+               for_each_string_list_item(item, &src_tag) {
+                       struct ref *ref = item->util;
+                       struct ref *dst_ref;
+                       struct commit *commit;
+
+                       if (is_null_sha1(ref->new_sha1))
+                               continue;
+                       commit = lookup_commit_reference_gently(ref->new_sha1, 1);
+                       if (!commit)
+                               /* not pushing a commit, which is not an error */
+                               continue;
+
+                       /*
+                        * Is this tag, which they do not have, reachable from
+                        * any of the commits we are sending?
+                        */
+                       if (!in_merge_bases_many(commit, sent_tips.nr, sent_tips.tip))
+                               continue;
+
+                       /* Add it in */
+                       dst_ref = make_linked_ref(ref->name, dst_tail);
+                       hashcpy(dst_ref->new_sha1, ref->new_sha1);
+                       dst_ref->peer_ref = copy_ref(ref);
+               }
+       }
+       string_list_clear(&src_tag, 0);
+       free(sent_tips.tip);
+}
+
 /*
  * Given the set of refs the local repository has, the set of refs the
  * remote repository has, and the refspec used for push, determine
@@ -1155,9 +1331,10 @@ int match_push_refs(struct ref *src, struct ref **dst,
        struct refspec *rs;
        int send_all = flags & MATCH_REFS_ALL;
        int send_mirror = flags & MATCH_REFS_MIRROR;
+       int send_prune = flags & MATCH_REFS_PRUNE;
        int errs;
        static const char *default_refspec[] = { ":", NULL };
-       struct ref **dst_tail = tail_ref(dst);
+       struct ref *ref, **dst_tail = tail_ref(dst);
 
        if (!nr_refspec) {
                nr_refspec = 1;
@@ -1167,39 +1344,20 @@ int match_push_refs(struct ref *src, struct ref **dst,
        errs = match_explicit_refs(src, *dst, &dst_tail, rs, nr_refspec);
 
        /* pick the remainder */
-       for ( ; src; src = src->next) {
+       for (ref = src; ref; ref = ref->next) {
                struct ref *dst_peer;
                const struct refspec *pat = NULL;
                char *dst_name;
-               if (src->peer_ref)
-                       continue;
 
-               pat = check_pattern_match(rs, nr_refspec, src);
-               if (!pat)
+               dst_name = get_ref_match(rs, nr_refspec, ref, send_mirror, FROM_SRC, &pat);
+               if (!dst_name)
                        continue;
 
-               if (pat->matching) {
-                       /*
-                        * "matching refs"; traditionally we pushed everything
-                        * including refs outside refs/heads/ hierarchy, but
-                        * that does not make much sense these days.
-                        */
-                       if (!send_mirror && prefixcmp(src->name, "refs/heads/"))
-                               continue;
-                       dst_name = xstrdup(src->name);
-
-               } else {
-                       const char *dst_side = pat->dst ? pat->dst : pat->src;
-                       if (!match_name_with_pattern(pat->src, src->name,
-                                                    dst_side, &dst_name))
-                               die("Didn't think it matches any more");
-               }
                dst_peer = find_ref_by_name(*dst, dst_name);
                if (dst_peer) {
                        if (dst_peer->peer_ref)
                                /* We're already sending something to this ref. */
                                goto free_name;
-
                } else {
                        if (pat->matching && !(send_all || send_mirror))
                                /*
@@ -1211,13 +1369,34 @@ int match_push_refs(struct ref *src, struct ref **dst,
 
                        /* Create a new one and link it */
                        dst_peer = make_linked_ref(dst_name, &dst_tail);
-                       hashcpy(dst_peer->new_sha1, src->new_sha1);
+                       hashcpy(dst_peer->new_sha1, ref->new_sha1);
                }
-               dst_peer->peer_ref = copy_ref(src);
+               dst_peer->peer_ref = copy_ref(ref);
                dst_peer->force = pat->force;
        free_name:
                free(dst_name);
        }
+
+       if (flags & MATCH_REFS_FOLLOW_TAGS)
+               add_missing_tags(src, dst, &dst_tail);
+
+       if (send_prune) {
+               /* check for missing refs on the remote */
+               for (ref = *dst; ref; ref = ref->next) {
+                       char *src_name;
+
+                       if (ref->peer_ref)
+                               /* We're already sending something to this ref. */
+                               continue;
+
+                       src_name = get_ref_match(rs, nr_refspec, ref, send_mirror, FROM_DST, NULL);
+                       if (src_name) {
+                               if (!find_ref_by_name(src, src_name))
+                                       ref->peer_ref = alloc_delete_ref();
+                               free(src_name);
+                       }
+               }
+       }
        if (errs)
                return -1;
        return 0;
@@ -1229,6 +1408,8 @@ void set_ref_status_for_push(struct ref *remote_refs, int send_mirror,
        struct ref *ref;
 
        for (ref = remote_refs; ref; ref = ref->next) {
+               int force_ref_update = ref->force || force_update;
+
                if (ref->peer_ref)
                        hashcpy(ref->new_sha1, ref->peer_ref->new_sha1);
                else if (!send_mirror)
@@ -1241,34 +1422,41 @@ void set_ref_status_for_push(struct ref *remote_refs, int send_mirror,
                        continue;
                }
 
-               /* This part determines what can overwrite what.
-                * The rules are:
-                *
-                * (0) you can always use --force or +A:B notation to
-                *     selectively force individual ref pairs.
+               /*
+                * Decide whether an individual refspec A:B can be
+                * pushed.  The push will succeed if any of the
+                * following are true:
                 *
-                * (1) if the old thing does not exist, it is OK.
+                * (1) the remote reference B does not exist
                 *
-                * (2) if you do not have the old thing, you are not allowed
-                *     to overwrite it; you would not know what you are losing
-                *     otherwise.
+                * (2) the remote reference B is being removed (i.e.,
+                *     pushing :B where no source is specified)
                 *
-                * (3) if both new and old are commit-ish, and new is a
-                *     descendant of old, it is OK.
+                * (3) the destination is not under refs/tags/, and
+                *     if the old and new value is a commit, the new
+                *     is a descendant of the old.
                 *
-                * (4) regardless of all of the above, removing :B is
-                *     always allowed.
+                * (4) it is forced using the +A:B notation, or by
+                *     passing the --force argument
                 */
 
-               ref->nonfastforward =
-                       !ref->deletion &&
-                       !is_null_sha1(ref->old_sha1) &&
-                       (!has_sha1_file(ref->old_sha1)
-                         || !ref_newer(ref->new_sha1, ref->old_sha1));
-
-               if (ref->nonfastforward && !ref->force && !force_update) {
-                       ref->status = REF_STATUS_REJECT_NONFASTFORWARD;
-                       continue;
+               if (!ref->deletion && !is_null_sha1(ref->old_sha1)) {
+                       int why = 0; /* why would this push require --force? */
+
+                       if (!prefixcmp(ref->name, "refs/tags/"))
+                               why = REF_STATUS_REJECT_ALREADY_EXISTS;
+                       else if (!has_sha1_file(ref->old_sha1))
+                               why = REF_STATUS_REJECT_FETCH_FIRST;
+                       else if (!lookup_commit_reference_gently(ref->old_sha1, 1) ||
+                                !lookup_commit_reference_gently(ref->new_sha1, 1))
+                               why = REF_STATUS_REJECT_NEEDS_FORCE;
+                       else if (!ref_newer(ref->new_sha1, ref->old_sha1))
+                               why = REF_STATUS_REJECT_NONFASTFORWARD;
+
+                       if (!force_ref_update)
+                               ref->status = why;
+                       else if (why)
+                               ref->forced_update = 1;
                }
        }
 }
@@ -1314,6 +1502,16 @@ int branch_merge_matches(struct branch *branch,
        return refname_match(branch->merge[i]->src, refname, ref_fetch_rules);
 }
 
+static int ignore_symref_update(const char *refname)
+{
+       unsigned char sha1[20];
+       int flag;
+
+       if (!resolve_ref_unsafe(refname, sha1, 0, &flag))
+               return 0; /* non-existing refs are OK */
+       return (flag & REF_ISSYMREF);
+}
+
 static struct ref *get_expanded_map(const struct ref *remote_refs,
                                    const struct refspec *refspec)
 {
@@ -1327,7 +1525,8 @@ static struct ref *get_expanded_map(const struct ref *remote_refs,
                if (strchr(ref->name, '^'))
                        continue; /* a dereference item */
                if (match_name_with_pattern(refspec->src, ref->name,
-                                           refspec->dst, &expn_name)) {
+                                           refspec->dst, &expn_name) &&
+                   !ignore_symref_update(expn_name)) {
                        struct ref *cpy = copy_ref(ref);
 
                        cpy->peer_ref = alloc_ref(expn_name);
@@ -1390,7 +1589,12 @@ int get_fetch_map(const struct ref *remote_refs,
        } else {
                const char *name = refspec->src[0] ? refspec->src : "HEAD";
 
-               ref_map = get_remote_ref(remote_refs, name);
+               if (refspec->exact_sha1) {
+                       ref_map = alloc_ref(name);
+                       get_sha1_hex(name, ref_map->old_sha1);
+               } else {
+                       ref_map = get_remote_ref(remote_refs, name);
+               }
                if (!missing_ok && !ref_map)
                        die("Couldn't find remote ref %s", name);
                if (ref_map) {
@@ -1402,8 +1606,8 @@ int get_fetch_map(const struct ref *remote_refs,
 
        for (rmp = &ref_map; *rmp; ) {
                if ((*rmp)->peer_ref) {
-                       if (check_refname_format((*rmp)->peer_ref->name + 5,
-                               REFNAME_ALLOW_ONELEVEL)) {
+                       if (prefixcmp((*rmp)->peer_ref->name, "refs/") ||
+                           check_refname_format((*rmp)->peer_ref->name, 0)) {
                                struct ref *ignore = *rmp;
                                error("* Ignoring funny ref '%s' locally",
                                      (*rmp)->peer_ref->name);
@@ -1451,7 +1655,8 @@ int ref_newer(const unsigned char *new_sha1, const unsigned char *old_sha1)
        struct commit_list *list, *used;
        int found = 0;
 
-       /* Both new and old must be commit-ish and new is descendant of
+       /*
+        * Both new and old must be commit-ish and new is descendant of
         * old.  Otherwise we require --force.
         */
        o = deref_tag(parse_object(old_sha1), NULL, 0);
@@ -1507,13 +1712,13 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs)
         * nothing to report.
         */
        base = branch->merge[0]->dst;
-       if (!resolve_ref(base, sha1, 1, NULL))
+       if (read_ref(base, sha1))
                return 0;
        theirs = lookup_commit_reference(sha1);
        if (!theirs)
                return 0;
 
-       if (!resolve_ref(branch->refname, sha1, 1, NULL))
+       if (read_ref(branch->refname, sha1))
                return 0;
        ours = lookup_commit_reference(sha1);
        if (!ours)
@@ -1571,20 +1776,40 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb)
 
        base = branch->merge[0]->dst;
        base = shorten_unambiguous_ref(base, 0);
-       if (!num_theirs)
-               strbuf_addf(sb, "Your branch is ahead of '%s' "
-                           "by %d commit%s.\n",
-                           base, num_ours, (num_ours == 1) ? "" : "s");
-       else if (!num_ours)
-               strbuf_addf(sb, "Your branch is behind '%s' "
-                           "by %d commit%s, "
-                           "and can be fast-forwarded.\n",
-                           base, num_theirs, (num_theirs == 1) ? "" : "s");
-       else
-               strbuf_addf(sb, "Your branch and '%s' have diverged,\n"
-                           "and have %d and %d different commit(s) each, "
-                           "respectively.\n",
-                           base, num_ours, num_theirs);
+       if (!num_theirs) {
+               strbuf_addf(sb,
+                       Q_("Your branch is ahead of '%s' by %d commit.\n",
+                          "Your branch is ahead of '%s' by %d commits.\n",
+                          num_ours),
+                       base, num_ours);
+               if (advice_status_hints)
+                       strbuf_addf(sb,
+                               _("  (use \"git push\" to publish your local commits)\n"));
+       } else if (!num_ours) {
+               strbuf_addf(sb,
+                       Q_("Your branch is behind '%s' by %d commit, "
+                              "and can be fast-forwarded.\n",
+                          "Your branch is behind '%s' by %d commits, "
+                              "and can be fast-forwarded.\n",
+                          num_theirs),
+                       base, num_theirs);
+               if (advice_status_hints)
+                       strbuf_addf(sb,
+                               _("  (use \"git pull\" to update your local branch)\n"));
+       } else {
+               strbuf_addf(sb,
+                       Q_("Your branch and '%s' have diverged,\n"
+                              "and have %d and %d different commit each, "
+                              "respectively.\n",
+                          "Your branch and '%s' have diverged,\n"
+                              "and have %d and %d different commits each, "
+                              "respectively.\n",
+                          num_theirs),
+                       base, num_ours, num_theirs);
+               if (advice_status_hints)
+                       strbuf_addf(sb,
+                               _("  (use \"git pull\" to merge the remote branch into yours)\n"));
+       }
        return 1;
 }