rebase -i: add fixup [-C | -c] command

Add options to `fixup` command to fixup both the commit contents and message. `fixup -C` command is used to replace the original commit message and `fixup -c`, additionally allows to edit the commit message. Original-patch-by: Phillip Wood <phillip.wood@dunelm.org.uk> Mentored-by: Christian Couder <chriscool@tuxfamily.org> Mentored-by: Phillip Wood <phillip.wood@dunelm.org.uk> Signed-off-by: Charvi Mendiratta <charvi077@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Charvi Mendiratta committed Jan 29, 2021 at 23:50 UTC 9e3cebd97cbd47909e683e617d5ffa2781f0adaa
2 files changed +197 -20
rebase-interactive.c
+3 -1
@@ -44,7 +44,9 @@ void append_todo_help(int command_count,
44 "r, reword <commit> = use commit, but edit the commit message\n"
45 "e, edit <commit> = use commit, but stop for amending\n"
46 "s, squash <commit> = use commit, but meld into previous commit\n"
47 -"f, fixup <commit> = like \"squash\", but discard this commit's log message\n"
47 +"f, fixup [-C | -c] <commit> = like \"squash\", but discard this\n"
48 +" commit's log message. Use -C to replace with this\n"
49 +" commit message or -c to edit the commit message\n"
50 "x, exec <command> = run command (the rest of the line) using shell\n"
51 "b, break = stop here (continue rebase later with 'git rebase --continue')\n"
52 "d, drop <commit> = remove commit\n"
sequencer.c
+194 -19
@@ -1718,6 +1718,12 @@ static int is_pick_or_similar(enum todo_command command)
1718 }
1719 }
1720
1721 +enum todo_item_flags {
1722 + TODO_EDIT_MERGE_MSG = (1 << 0),
1723 + TODO_REPLACE_FIXUP_MSG = (1 << 1),
1724 + TODO_EDIT_FIXUP_MSG = (1 << 2),
1725 +};
1726 +
1727 static size_t subject_length(const char *body)
1728 {
1729 const char *p = body;
@@ -1734,32 +1740,176 @@ static size_t subject_length(const char *body)
1740
1741 static const char first_commit_msg_str[] = N_("This is the 1st commit message:");
1742 static const char nth_commit_msg_fmt[] = N_("This is the commit message #%d:");
1743 +static const char skip_first_commit_msg_str[] = N_("The 1st commit message will be skipped:");
1744 static const char skip_nth_commit_msg_fmt[] = N_("The commit message #%d will be skipped:");
1745 static const char combined_commit_msg_fmt[] = N_("This is a combination of %d commits.");
1746
1740 -static void append_squash_message(struct strbuf *buf, const char *body,
1741 - struct replay_opts *opts)
1747 +static int check_fixup_flag(enum todo_command command,
1748 + enum todo_item_flags flag)
1749 +{
1750 + return command == TODO_FIXUP && ((flag & TODO_REPLACE_FIXUP_MSG) ||
1751 + (flag & TODO_EDIT_FIXUP_MSG));
1752 +}
1753 +
1754 +/*
1755 + * Wrapper around strbuf_add_commented_lines() which avoids double
1756 + * commenting commit subjects.
1757 + */
1758 +static void add_commented_lines(struct strbuf *buf, const void *str, size_t len)
1759 +{
1760 + const char *s = str;
1761 + while (len > 0 && s[0] == comment_line_char) {
1762 + size_t count;
1763 + const char *n = memchr(s, '\n', len);
1764 + if (!n)
1765 + count = len;
1766 + else
1767 + count = n - s + 1;
1768 + strbuf_add(buf, s, count);
1769 + s += count;
1770 + len -= count;
1771 + }
1772 + strbuf_add_commented_lines(buf, s, len);
1773 +}
1774 +
1775 +/* Does the current fixup chain contain a squash command? */
1776 +static int seen_squash(struct replay_opts *opts)
1777 +{
1778 + return starts_with(opts->current_fixups.buf, "squash") ||
1779 + strstr(opts->current_fixups.buf, "\nsquash");
1780 +}
1781 +
1782 +static void update_comment_bufs(struct strbuf *buf1, struct strbuf *buf2, int n)
1783 {
1743 - size_t commented_len = 0;
1784 + strbuf_setlen(buf1, 2);
1785 + strbuf_addf(buf1, _(nth_commit_msg_fmt), n);
1786 + strbuf_addch(buf1, '\n');
1787 + strbuf_setlen(buf2, 2);
1788 + strbuf_addf(buf2, _(skip_nth_commit_msg_fmt), n);
1789 + strbuf_addch(buf2, '\n');
1790 +}
1791
1745 - unlink(rebase_path_fixup_msg());
1746 - if (starts_with(body, "squash!") || starts_with(body, "fixup!"))
1792 +/*
1793 + * Comment out any un-commented commit messages, updating the message comments
1794 + * to say they will be skipped but do not comment out the empty lines that
1795 + * surround commit messages and their comments.
1796 + */
1797 +static void update_squash_message_for_fixup(struct strbuf *msg)
1798 +{
1799 + void (*copy_lines)(struct strbuf *, const void *, size_t) = strbuf_add;
1800 + struct strbuf buf1 = STRBUF_INIT, buf2 = STRBUF_INIT;
1801 + const char *s, *start;
1802 + char *orig_msg;
1803 + size_t orig_msg_len;
1804 + int i = 1;
1805 +
1806 + strbuf_addf(&buf1, "# %s\n", _(first_commit_msg_str));
1807 + strbuf_addf(&buf2, "# %s\n", _(skip_first_commit_msg_str));
1808 + s = start = orig_msg = strbuf_detach(msg, &orig_msg_len);
1809 + while (s) {
1810 + const char *next;
1811 + size_t off;
1812 + if (skip_prefix(s, buf1.buf, &next)) {
1813 + /*
1814 + * Copy the last message, preserving the blank line
1815 + * preceding the current line
1816 + */
1817 + off = (s > start + 1 && s[-2] == '\n') ? 1 : 0;
1818 + copy_lines(msg, start, s - start - off);
1819 + if (off)
1820 + strbuf_addch(msg, '\n');
1821 + /*
1822 + * The next message needs to be commented out but the
1823 + * message header is already commented out so just copy
1824 + * it and the blank line that follows it.
1825 + */
1826 + strbuf_addbuf(msg, &buf2);
1827 + if (*next == '\n')
1828 + strbuf_addch(msg, *next++);
1829 + start = s = next;
1830 + copy_lines = add_commented_lines;
1831 + update_comment_bufs(&buf1, &buf2, ++i);
1832 + } else if (skip_prefix(s, buf2.buf, &next)) {
1833 + off = (s > start + 1 && s[-2] == '\n') ? 1 : 0;
1834 + copy_lines(msg, start, s - start - off);
1835 + start = s - off;
1836 + s = next;
1837 + copy_lines = strbuf_add;
1838 + update_comment_bufs(&buf1, &buf2, ++i);
1839 + } else {
1840 + s = strchr(s, '\n');
1841 + if (s)
1842 + s++;
1843 + }
1844 + }
1845 + copy_lines(msg, start, orig_msg_len - (start - orig_msg));
1846 + free(orig_msg);
1847 + strbuf_release(&buf1);
1848 + strbuf_release(&buf2);
1849 +}
1850 +
1851 +static int append_squash_message(struct strbuf *buf, const char *body,
1852 + enum todo_command command, struct replay_opts *opts,
1853 + enum todo_item_flags flag)
1854 +{
1855 + const char *fixup_msg;
1856 + size_t commented_len = 0, fixup_off;
1857 + /*
1858 + * amend is non-interactive and not normally used with fixup!
1859 + * or squash! commits, so only comment out those subjects when
1860 + * squashing commit messages.
1861 + */
1862 + if (starts_with(body, "amend!") ||
1863 + ((command == TODO_SQUASH || seen_squash(opts)) &&
1864 + (starts_with(body, "squash!") || starts_with(body, "fixup!"))))
1865 commented_len = subject_length(body);
1866 +
1867 strbuf_addf(buf, "\n%c ", comment_line_char);
1868 strbuf_addf(buf, _(nth_commit_msg_fmt),
1869 ++opts->current_fixup_count + 1);
1870 strbuf_addstr(buf, "\n\n");
1871 strbuf_add_commented_lines(buf, body, commented_len);
1872 + /* buf->buf may be reallocated so store an offset into the buffer */
1873 + fixup_off = buf->len;
1874 strbuf_addstr(buf, body + commented_len);
1875 +
1876 + /* fixup -C after squash behaves like squash */
1877 + if (check_fixup_flag(command, flag) && !seen_squash(opts)) {
1878 + /*
1879 + * We're replacing the commit message so we need to
1880 + * append the Signed-off-by: trailer if the user
1881 + * requested '--signoff'.
1882 + */
1883 + if (opts->signoff)
1884 + append_signoff(buf, 0, 0);
1885 +
1886 + if ((command == TODO_FIXUP) &&
1887 + (flag & TODO_REPLACE_FIXUP_MSG) &&
1888 + (file_exists(rebase_path_fixup_msg()) ||
1889 + !file_exists(rebase_path_squash_msg()))) {
1890 + fixup_msg = skip_blank_lines(buf->buf + fixup_off);
1891 + if (write_message(fixup_msg, strlen(fixup_msg),
1892 + rebase_path_fixup_msg(), 0) < 0)
1893 + return error(_("cannot write '%s'"),
1894 + rebase_path_fixup_msg());
1895 + } else {
1896 + unlink(rebase_path_fixup_msg());
1897 + }
1898 + } else {
1899 + unlink(rebase_path_fixup_msg());
1900 + }
1901 +
1902 + return 0;
1903 }
1904
1905 static int update_squash_messages(struct repository *r,
1906 enum todo_command command,
1907 struct commit *commit,
1759 - struct replay_opts *opts)
1908 + struct replay_opts *opts,
1909 + enum todo_item_flags flag)
1910 {
1911 struct strbuf buf = STRBUF_INIT;
1762 - int res;
1912 + int res = 0;
1913 const char *message, *body;
1914 const char *encoding = get_commit_output_encoding();
1915
@@ -1779,6 +1929,8 @@ static int update_squash_messages(struct repository *r,
1929 opts->current_fixup_count + 2);
1930 strbuf_splice(&buf, 0, eol - buf.buf, header.buf, header.len);
1931 strbuf_release(&header);
1932 + if (check_fixup_flag(command, flag) && !seen_squash(opts))
1933 + update_squash_message_for_fixup(&buf);
1934 } else {
1935 struct object_id head;
1936 struct commit *head_commit;
@@ -1792,18 +1944,22 @@ static int update_squash_messages(struct repository *r,
1944 return error(_("could not read HEAD's commit message"));
1945
1946 find_commit_subject(head_message, &body);
1795 - if (command == TODO_FIXUP && write_message(body, strlen(body),
1947 + if (command == TODO_FIXUP && !flag && write_message(body, strlen(body),
1948 rebase_path_fixup_msg(), 0) < 0) {
1949 unuse_commit_buffer(head_commit, head_message);
1950 return error(_("cannot write '%s'"), rebase_path_fixup_msg());
1951 }
1800 -
1952 strbuf_addf(&buf, "%c ", comment_line_char);
1953 strbuf_addf(&buf, _(combined_commit_msg_fmt), 2);
1954 strbuf_addf(&buf, "\n%c ", comment_line_char);
1804 - strbuf_addstr(&buf, _(first_commit_msg_str));
1955 + strbuf_addstr(&buf, check_fixup_flag(command, flag) ?
1956 + _(skip_first_commit_msg_str) :
1957 + _(first_commit_msg_str));
1958 strbuf_addstr(&buf, "\n\n");
1806 - strbuf_addstr(&buf, body);
1959 + if (check_fixup_flag(command, flag))
1960 + strbuf_add_commented_lines(&buf, body, strlen(body));
1961 + else
1962 + strbuf_addstr(&buf, body);
1963
1964 unuse_commit_buffer(head_commit, head_message);
1965 }
@@ -1813,8 +1969,8 @@ static int update_squash_messages(struct repository *r,
1969 oid_to_hex(&commit->object.oid));
1970 find_commit_subject(message, &body);
1971
1816 - if (command == TODO_SQUASH) {
1817 - append_squash_message(&buf, body, opts);
1972 + if (command == TODO_SQUASH || check_fixup_flag(command, flag)) {
1973 + res = append_squash_message(&buf, body, command, opts, flag);
1974 } else if (command == TODO_FIXUP) {
1975 strbuf_addf(&buf, "\n%c ", comment_line_char);
1976 strbuf_addf(&buf, _(skip_nth_commit_msg_fmt),
@@ -1825,7 +1981,9 @@ static int update_squash_messages(struct repository *r,
1981 return error(_("unknown command: %d"), command);
1982 unuse_commit_buffer(commit, message);
1983
1828 - res = write_message(buf.buf, buf.len, rebase_path_squash_msg(), 0);
1984 + if (!res)
1985 + res = write_message(buf.buf, buf.len, rebase_path_squash_msg(),
1986 + 0);
1987 strbuf_release(&buf);
1988
1989 if (!res) {
@@ -2026,7 +2184,8 @@ static int do_pick_commit(struct repository *r,
2184 if (command == TODO_REWORD)
2185 reword = 1;
2186 else if (is_fixup(command)) {
2029 - if (update_squash_messages(r, command, commit, opts))
2187 + if (update_squash_messages(r, command, commit,
2188 + opts, item->flags))
2189 return -1;
2190 flags |= AMEND_MSG;
2191 if (!final_fixup)
@@ -2191,10 +2350,6 @@ static int read_and_refresh_cache(struct repository *r,
2350 return 0;
2351 }
2352
2194 -enum todo_item_flags {
2195 - TODO_EDIT_MERGE_MSG = 1
2196 -};
2197 -
2353 void todo_list_release(struct todo_list *todo_list)
2354 {
2355 strbuf_release(&todo_list->buf);
@@ -2281,6 +2436,18 @@ static int parse_insn_line(struct repository *r, struct todo_item *item,
2436 return 0;
2437 }
2438
2439 + if (item->command == TODO_FIXUP) {
2440 + if (skip_prefix(bol, "-C", &bol) &&
2441 + (*bol == ' ' || *bol == '\t')) {
2442 + bol += strspn(bol, " \t");
2443 + item->flags |= TODO_REPLACE_FIXUP_MSG;
2444 + } else if (skip_prefix(bol, "-c", &bol) &&
2445 + (*bol == ' ' || *bol == '\t')) {
2446 + bol += strspn(bol, " \t");
2447 + item->flags |= TODO_EDIT_FIXUP_MSG;
2448 + }
2449 + }
2450 +
2451 if (item->command == TODO_MERGE) {
2452 if (skip_prefix(bol, "-C", &bol))
2453 bol += strspn(bol, " \t");
@@ -5287,6 +5454,14 @@ static void todo_list_to_strbuf(struct repository *r, struct todo_list *todo_lis
5454 short_commit_name(item->commit) :
5455 oid_to_hex(&item->commit->object.oid);
5456
5457 + if (item->command == TODO_FIXUP) {
5458 + if (item->flags & TODO_EDIT_FIXUP_MSG)
5459 + strbuf_addstr(buf, " -c");
5460 + else if (item->flags & TODO_REPLACE_FIXUP_MSG) {
5461 + strbuf_addstr(buf, " -C");
5462 + }
5463 + }
5464 +
5465 if (item->command == TODO_MERGE) {
5466 if (item->flags & TODO_EDIT_MERGE_MSG)
5467 strbuf_addstr(buf, " -c");