initial commit
authorFrantisek Hrbata <frantisek@hrbata.com>
Fri, 13 May 2022 15:37:43 +0000 (17:37 +0200)
committerFrantisek Hrbata <frantisek@hrbata.com>
Fri, 13 May 2022 15:37:43 +0000 (17:37 +0200)
Signed-off-by: Frantisek Hrbata <frantisek@hrbata.com>
Makefile [new file with mode: 0644]
src/common.c [new file with mode: 0644]
src/common.h [new file with mode: 0644]
src/create.c [new file with mode: 0644]
src/gimrr.c [new file with mode: 0644]
src/gimrr.h [new file with mode: 0644]
src/gimrr.mak [new file with mode: 0644]
src/list.c [new file with mode: 0644]

diff --git a/Makefile b/Makefile
new file mode 100644 (file)
index 0000000..08321a5
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,43 @@
+all: gimrr$(X)
+
+GIT_DIR = git
+GIT_URL = https://github.com/git/git.git
+GIT_LNK = $(GIT_DIR)/$(GIMRR_GIT_DIR)
+GIT_MAK = $(GIT_DIR)/makefile
+GIMRR_MAK = gimrr.mak
+GIMRR_GIT_DIR = contrib/gimrr
+GIMRR_SRC_DIR = src
+
+GIT_DIR_EXISTS := $(shell git -C $(GIT_DIR)/.git rev-parse --is-inside-git-dir 2>/dev/null)
+
+# make sure git repo is always available
+ifneq ($(GIT_DIR_EXISTS),true)
+foo := $(shell git clone --depth 1 $(GIT_URL) $(GIT_DIR))
+endif
+
+CSCOPE_SOURCES := $(shell find . $(GIT_DIR) \
+              \( -name .git -type d -prune \) -o \
+              \( -name '*.[ch]' -type f -print \))
+
+$(GIT_LNK):
+       @ln -s $(CURDIR)/$(GIMRR_SRC_DIR) $@ && \
+       sed -i '/^.PHONY: objects/i include $(GIMRR_GIT_DIR)/$(GIMRR_MAK)\n' \
+               $(GIT_DIR)/Makefile     
+
+gimrr$(X): FORCE | $(GIT_LNK)
+       $(MAKE) -C $(GIT_DIR) $(GIMRR_GIT_DIR)/$@
+
+clean: | $(GIT_LNK)
+       $(RM) $(GIMRR_SRC_DIR)/gimrr$(X) cscope.out
+       $(MAKE) -C $(GIT_DIR) clean
+
+cscope.out: $(CSCOPE_SOURCES)
+       echo $? | xargs cscope -b 
+
+cscope: cscope.out
+
+FORCE:
+.PHONY: FORCE clean
+
+test:
+       $(MAKE) -C $(GIT_DIR) XXX
diff --git a/src/common.c b/src/common.c
new file mode 100644 (file)
index 0000000..941302d
--- /dev/null
@@ -0,0 +1,142 @@
+#include "gimrr.h"
+
+static int check_base58(const char *str)
+{
+       const signed char map[256] = {
+               -1, -1, -1, -1, -1, -1, -1, -1,
+               -1, -1, -1, -1, -1, -1, -1, -1,
+               -1, -1, -1, -1, -1, -1, -1, -1,
+               -1, -1, -1, -1, -1, -1, -1, -1,
+               -1, -1, -1, -1, -1, -1, -1, -1,
+               -1, -1, -1, -1, -1, -1, -1, -1,
+               -1, '1','2','3','4','5','6','7',
+               '8','9',-1, -1, -1, -1, -1, -1,
+               -1, 'A','B','C','D','E','F','G',
+               'H',-1, 'J','K','L','M','N',-1,
+               'P','Q','R','S','T','U','V','W',
+               'X','Y','Z',-1, -1, -1, -1, -1,
+               -1, 'a','b','c','d','e','f','g',
+               'h','i','j','k',-1, 'm','n','o',
+               'p','q','r','s','t','u','v','w',
+               'x','y','z',-1, -1, -1, -1, -1,
+               -1, -1, -1, -1, -1, -1, -1, -1,
+               -1, -1, -1, -1, -1, -1, -1, -1,
+               -1, -1, -1, -1, -1, -1, -1, -1,
+               -1, -1, -1, -1, -1, -1, -1, -1,
+               -1, -1, -1, -1, -1, -1, -1, -1,
+               -1, -1, -1, -1, -1, -1, -1, -1,
+               -1, -1, -1, -1, -1, -1, -1, -1,
+               -1, -1, -1, -1, -1, -1, -1, -1,
+               -1, -1, -1, -1, -1, -1, -1, -1,
+               -1, -1, -1, -1, -1, -1, -1, -1,
+               -1, -1, -1, -1, -1, -1, -1, -1,
+               -1, -1, -1, -1, -1, -1, -1, -1,
+               -1, -1, -1, -1, -1, -1, -1, -1,
+               -1, -1, -1, -1, -1, -1, -1, -1,
+               -1, -1, -1, -1, -1, -1, -1, -1,
+               -1, -1, -1, -1, -1, -1, -1, -1,
+       };
+
+       while (*str && map[(int)*str++] != -1)
+               ;
+       return *str;
+}
+
+struct expand_ref_data {
+       const char *str;
+       struct strvec prefixes;
+       struct object_id oid;
+       int refs_found;
+};
+
+static int expand_ref_cb(const char *refname, const struct object_id *oid,
+                           int flag, void *cb_data)
+{
+       struct expand_ref_data *data = (struct expand_ref_data *)cb_data;
+
+       if (!ends_with(refname, "/head"))
+               return 0;
+
+       if (!data->refs_found++)
+               oidcpy(&data->oid, oid);
+
+       return 0;
+}
+
+static int remote_ref_cb(struct remote *remote, void *cb_data)
+{
+       struct expand_ref_data *data = (struct expand_ref_data *)cb_data;
+
+       if (!remote_is_configured(remote, 1))
+               return 0;
+
+       strvec_pushf(&data->prefixes, "refs/mrs/local/%s/%s",
+                       remote->name, data->str);
+       strvec_pushf(&data->prefixes, "refs/mrs/remote/%s/%s",
+                       remote->name, data->str);
+       return 0;
+}
+
+static struct commit *lookup_commit_reference_by_short_name(const char *str)
+{
+       struct expand_ref_data data = { str, STRVEC_INIT };
+
+       /*
+        * Sanity check that 'str' looks like mrid and has at least some
+        * reasonable length.
+        */
+       if (strlen(str) < MINIMUM_ABBREV || check_base58(str))
+               return NULL;
+
+       for_each_remote(remote_ref_cb, &data);
+       for_each_fullref_in_prefixes(NULL, data.prefixes.v, expand_ref_cb,
+                       &data);
+       strvec_clear(&data.prefixes);
+
+       if (!data.refs_found)
+               return NULL;
+
+       if (data.refs_found > 1) {
+               warning(_("ambiguous mrid '%s'"), str);
+               return NULL;
+       }
+
+       return lookup_commit_reference(the_repository, &data.oid);
+}
+
+/* 
+ * Base58 output buffer size requires to be approximately 1.4 greater.
+ * log58(256^x) / log256(256^x) = 1.36
+ */
+#define BASE58_SIZE (GIT_MAX_RAWSZ * 140 / 100 + 1)
+
+const char *lookup_mrid_by_name(const char *str)
+{
+       static char id[BASE58_SIZE + 1]; /* '\0' terminated */
+       struct commit *commit;
+       const char *buf, *hdr;
+       size_t buf_size, hdr_size;
+
+       commit = lookup_commit_reference_by_name(str);
+       if (!commit)
+               commit = lookup_commit_reference_by_short_name(str);
+       if (!commit)
+               return NULL;
+
+       buf = get_commit_buffer(commit, &buf_size);
+       hdr = find_header_mem(buf, buf_size, "merge-request", &hdr_size);
+       if (!hdr || hdr_size < 3 || !starts_with(hdr, "id ")) {
+               unuse_commit_buffer(commit, buf);
+               return NULL;
+       }
+
+       hdr += 3;
+       hdr_size -= 3;
+       memcpy(id, hdr, hdr_size);
+       id[hdr_size] = 0;
+       unuse_commit_buffer(commit, buf);
+
+       return id;
+}
+
+#undef BASE58_SIZE
diff --git a/src/common.h b/src/common.h
new file mode 100644 (file)
index 0000000..578f395
--- /dev/null
@@ -0,0 +1,8 @@
+#ifndef COMMON_H
+#define COMMON_H
+
+#include "gimrr.h"
+
+const char *lookup_mrid_by_name(const char *name);
+
+#endif
diff --git a/src/create.c b/src/create.c
new file mode 100644 (file)
index 0000000..56f851b
--- /dev/null
@@ -0,0 +1,640 @@
+#include "gimrr.h"
+
+struct create_ctx {
+       struct commit_list *commits;
+       struct commit_list *deps;
+       struct commit *base;
+       struct commit *tail;
+       unsigned int commits_nr;
+       const char *upstream;
+       const char *start;
+       const char *end;
+       const char *remote;
+       const char *branch;
+       const char *user;
+       struct strbuf msg;
+};
+
+static const char * const create_usage[] = {
+       N_("git mr create [<upstream>]"),
+       NULL
+};
+
+/* 
+ * Base58 output buffer size requires to be approximately 1.4 greater.
+ * log58(256^x) / log256(256^x) = 1.36
+ */
+#define BASE58_SIZE (GIT_MAX_RAWSZ * 140 / 100 + 1)
+
+static char *oid_to_b58(const struct object_id *oid)
+{
+       static unsigned char b58[BASE58_SIZE + 1]; /* '\0' terminated */
+       static char *b58_alph = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
+       unsigned char *cur, *beg, *end;
+       unsigned int carry = 0, zeroes = 0;
+       const unsigned char *hash = oid->hash;
+       size_t hash_size = hash_algos[oid->algo].rawsz;
+
+       cur = beg = end = b58 + BASE58_SIZE - 1;
+       memset(b58, 0, BASE58_SIZE);
+
+       /* count leading zeroes */
+       for (zeroes = 0; !*hash && hash_size - zeroes; hash++, zeroes++)
+               ;
+
+       /* calculate base58 */
+       while (hash_size--) {
+               carry = *hash++;
+               cur = beg;
+               /* 
+                * Multiply b58 by 256 and add current hash byte.
+                * b58 is constructed in BE, so 'beg' points to the
+                * last entry in b58 and 'cur' is decremented.*/
+               while (carry || end <= cur) {
+                       carry += *cur << 8;
+                       *cur-- = carry % 58;
+                       carry /= 58;
+               }
+               /* 'end' points to the first calculated b58 entry */
+               end = ++cur;
+       }
+
+       /* add leading zeroes to b58 */
+       end -= zeroes;
+
+       /* map to b58 alphabet */
+       for (cur = end; cur <= beg; cur++)
+               *cur = b58_alph[*cur];
+
+       return (char *)end;
+}
+
+static const char *get_header(const char *msg, const char *key)
+{
+       size_t len;
+       const char *v = find_commit_header(msg, key, &len);
+       return v ? xmemdupz(v, len) : NULL;
+}
+
+static void append_extra_header(struct commit_extra_header ***tail,
+               const char *key, const char *value)
+{
+       struct commit_extra_header *h = CALLOC_ARRAY(h, 1);
+
+       h->key = xmemdupz(key, strlen(key));
+       h->value = xmemdupz(value, strlen(value));
+       h->len = strlen(value);
+
+       while (**tail)
+               *tail = &(**tail)->next;
+
+       **tail = h;
+       *tail = &h->next;
+}
+
+static const char *exclude_hdrs[] = {
+       "gpgsig",
+       "gpgsig-sha256",
+       "merge-request",
+       NULL};
+
+static struct commit *create_mr_commit(struct commit *commit,
+               struct commit *parent, const char *hdr)
+{
+       struct commit_extra_header *extra, **eptr = &extra;
+       struct commit_list *parents = NULL, **pptr = &parents;
+       struct object_id *tree, *oid = &commit->object.oid, new_oid;
+       struct commit *new_commit;
+       const char *buf, *msg, *author;
+
+       parse_commit_or_die(commit);
+
+       tree = get_commit_tree_oid(commit);
+       if (!tree)
+               die(_("commit '%s' lacks tree"), oid_to_hex(oid));
+
+       buf = logmsg_reencode(commit, NULL, get_commit_output_encoding());
+       if (!buf)
+               die(_("failed to read commit '%s' message"), oid_to_hex(oid));
+
+       msg = strstr(buf, "\n\n");
+       if (!msg)
+               die(_("commit '%s' lacks headers"), oid_to_hex(oid));
+       msg += 2;
+
+       author = get_header(buf, "author");
+       if (!author)
+               die(_("commit '%s' lacks author header"), oid_to_hex(oid));
+
+       if (parent)
+               pptr = commit_list_append(parent, pptr);
+       else
+               parents = copy_commit_list(commit->parents);
+
+       extra = read_commit_extra_headers(commit, exclude_hdrs);
+       append_extra_header(&eptr, "merge-request", hdr);
+
+       if (commit_tree_extended(msg, strlen(msg), tree, parents, &new_oid,
+                               author, NULL, NULL, extra))
+               die(_("failed to write commit object"));
+
+       free_commit_extra_headers(extra);
+       unuse_commit_buffer(commit, buf);
+       free((void *)author);
+
+       new_commit = lookup_commit(the_repository, &new_oid);
+       if (!new_commit)
+               die(_("unable to access commit '%s'"), oid_to_hex(&new_oid));
+
+       return new_commit;
+}
+
+static struct commit *create_mr_head(struct commit_list *commits, const char *hdr)
+{
+       struct commit *old, *new = NULL;
+
+       while (commits) {
+               old = commits->item;
+               new = create_mr_commit(old, new, hdr);
+               commits = commits->next;
+       }
+
+       return new;
+}
+
+static struct tag *create_mr_tag(struct strbuf *msg, struct commit *head,
+               const char *mrid)
+{
+       struct strbuf buf = STRBUF_INIT;
+       struct object_id oid;
+
+       strbuf_addf(&buf,
+                   "object %s\n"
+                   "type %s\n"
+                   "tag %s\n"
+                   "tagger %s\n\n"
+                   "%s",
+                   oid_to_hex(&head->object.oid),
+                   type_name(OBJ_COMMIT),
+                   mrid,
+                   git_committer_info(IDENT_STRICT),
+                   msg->buf);
+
+       if (write_object_file(buf.buf, buf.len, OBJ_TAG, &oid) < 0)
+               die(_("unable to write tag file"));
+
+       strbuf_release(&buf);
+
+       return lookup_tag(the_repository, &oid);
+}
+
+struct upstream_to_remote_cb_data {
+       struct refspec_item spec;
+       struct remote *remote;
+       int matches;
+};
+
+static int upstream_to_remote_cb(struct remote *remote, void *priv)
+{
+       struct upstream_to_remote_cb_data *cb_data = priv;
+
+       if (!remote_is_configured(remote, 1))
+               return 0;
+
+       if (remote_find_tracking(remote, &cb_data->spec))
+               return 0;
+
+       FREE_AND_NULL(cb_data->spec.src);
+
+       if (cb_data->matches++)
+               return 0;
+
+       cb_data->remote = remote;
+
+       return 0;
+}
+
+static struct remote *upstream_to_remote(char *ref)
+{
+       struct upstream_to_remote_cb_data cb_data;
+
+       memset(&cb_data, 0, sizeof(cb_data));
+       cb_data.spec.dst = ref;
+       for_each_remote(upstream_to_remote_cb, &cb_data);
+
+       if (!cb_data.matches)
+               die(_("no remote found for upstream '%s'"), ref);
+       else if (cb_data.matches > 1)
+               die(_("ambiguous remote for upstream '%s'"), ref);
+
+       return cb_data.remote;
+}
+
+static void init_create_ctx_from_branch(struct create_ctx *ctx,
+               struct branch *branch)
+{
+       if (!branch)
+               die(_("no current branch"));
+
+       if (!branch->merge_nr)
+               die(_("no upstream configured for branch: '%s'"),
+                               branch->name);
+       else if (branch->merge_nr > 1)
+               die(_("ambiguous upstream configuration for branch: '%s'"),
+                               branch->name);
+
+       ctx->upstream = xstrdup(branch->merge[0]->dst);
+
+       if (!ctx->remote) {
+               const char *remote;
+
+               remote = pushremote_for_branch(branch, NULL);
+
+               if (!remote_is_configured(remote_get(remote), 1))
+                       die(_("no such remote: '%s'"), remote);
+
+               ctx->remote = xstrdup(remote);
+       }
+
+       if (!ctx->end)
+               ctx->end = xstrdup(branch->refname);
+
+       if (!ctx->branch)
+               ctx->branch = xstrdup(branch->merge[0]->src);
+
+       if (!ctx->start)
+               ctx->start = xstrdup(branch->merge[0]->dst);
+}
+
+static void init_create_ctx_from_upstream(struct create_ctx *ctx,
+               const char *upstream)
+{
+       struct object_id up_oid;
+       struct remote *remote;
+       char *up_ref;
+       int rv;
+
+       rv = dwim_ref(upstream, strlen(upstream), &up_oid, &up_ref, 0);
+       if (!rv)
+               die(_("non-existing remote-tracking reference '%s'"),
+                               upstream);
+       else if (rv != 1)
+               die(_("ambiguous upstream reference: '%s'"), upstream);
+       else if (!starts_with(up_ref, "refs/remotes/"))
+               die(_("upstream reference '%s' is not remote-tracking branch"),
+                               upstream);
+
+       ctx->upstream = up_ref;
+
+       remote = upstream_to_remote(up_ref);
+
+       if (!ctx->remote)
+               ctx->remote = xstrdup(remote->name);
+
+       if (!ctx->branch) {
+               struct refspec_item refspec;
+
+               memset(&refspec, 0, sizeof(refspec));
+               refspec.dst = up_ref;
+
+               if (remote_find_tracking(remote, &refspec))
+                       die(_("no upstream branch for remote-tracking branch '%s'"),
+                                       up_ref);
+
+               ctx->branch = refspec.src;
+       }
+
+       if (!ctx->start)
+               ctx->start = xstrdup(up_ref);
+
+       if (!ctx->end) {
+               struct branch *branch = branch_get(NULL);
+               if (!branch)
+                       die(_("no current branch"));
+
+               ctx->end = xstrdup(branch->refname);
+       }
+}
+
+static void init_mr_commits(struct create_ctx *ctx)
+{
+       struct rev_info revs;
+       struct commit *commit, *start, *end, *upstream;
+       struct commit_list *bases = NULL, *parents = NULL, *deps = NULL;
+
+       start = lookup_commit_reference_by_name(ctx->start);
+       if (!start)
+               die(_("could not lookup commit %s"), ctx->start);
+       end = lookup_commit_reference_by_name(ctx->end);
+       if (!end)
+               die(_("could not lookup commit %s"), ctx->end);
+
+       upstream = lookup_commit_reference_by_name(ctx->upstream);
+       if (!upstream)
+               die(_("could not lookup commit %s"), ctx->upstream);
+
+       bases = get_merge_bases(start, end);
+       if (!bases || !oideq(&bases->item->object.oid, &start->object.oid))
+               die(_("'%s' is not acestor of '%s'"), ctx->start, ctx->end);
+
+       free_commit_list(bases);
+
+       bases = get_merge_bases(upstream, start);
+       if (!bases || !oideq(&bases->item->object.oid, &upstream->object.oid))
+               die(_("'%s' is not acestor of '%s'"), ctx->upstream,
+                               ctx->start);
+       free_commit_list(bases);
+
+       init_revisions(&revs, NULL);
+       revs.first_parent_only = 1;
+
+       start->object.flags ^= UNINTERESTING;
+       add_pending_object(&revs, &start->object, ctx->start);
+       add_pending_object(&revs, &end->object, ctx->end);
+
+       if (prepare_revision_walk(&revs))
+               die(_("revision walk setup failed"));
+
+       while ((commit = get_revision(&revs))) {
+               parents = commit->parents->next;
+               /* This is merge commit. */
+               if (parents) {
+                       if (!ctx->base)
+                               ctx->base = commit;
+                       while (parents) {
+                               deps = commit_list_insert(parents->item,
+                                               &deps);
+                               parents = parents->next;
+                       }
+                       continue;
+               }
+               /* Normal commit after at least one merge commit. */
+               if (ctx->base)
+                       break;
+
+               ctx->commits = commit_list_insert(commit, &ctx->commits);
+               ctx->commits_nr++;
+       }
+
+       clear_commit_marks(start, UNINTERESTING);
+
+       if (!ctx->commits_nr)
+               die(_("no mr suitable commits in refspec '%s:%s'"), ctx->start,
+                               ctx->end);
+       if (!ctx->base)
+               ctx->base = start;
+
+       if (!oideq(&ctx->base->object.oid, &upstream->object.oid)) {
+               parents = ctx->base->parents;
+               if (parents && parents->next &&
+                   !oideq(&parents->item->object.oid, &upstream->object.oid))
+                       deps = commit_list_insert(parents->item, &deps);
+               else
+                       deps = commit_list_insert(ctx->base, &deps);
+       }
+
+       ctx->tail = upstream;
+       ctx->deps = reduce_heads(deps);
+       free_commit_list(deps);
+}
+
+static char *generate_mr_mrid(struct commit_list *commits)
+{
+       struct strbuf buf = STRBUF_INIT;
+       struct object_id oid;
+
+       while (commits) {
+               strbuf_addstr(&buf, oid_to_hex(&commits->item->object.oid));
+               commits = commits->next;
+       }
+
+       hash_object_file(the_hash_algo, buf.buf, buf.len, OBJ_BLOB, &oid);
+       strbuf_reset(&buf);
+       strbuf_addstr(&buf, oid_to_b58(&oid));
+       return strbuf_detach(&buf, NULL);
+}
+
+static char *generate_mr_refname(const char *remote, const char *mrid,
+               const char *user)
+{
+       struct strbuf buf = STRBUF_INIT;
+
+       strbuf_addf(&buf, "refs/mrs/local/%s/%s/%s/head", remote, mrid, user);
+       return strbuf_detach(&buf, NULL);
+}
+
+static char *generate_mr_hdr(const char *mrid, struct create_ctx *ctx)
+{
+       struct commit_list *deps = ctx->deps;
+       struct strbuf buf = STRBUF_INIT;
+
+       strbuf_addf(&buf,
+                       "id %s\n"
+                       "branch %s\n"
+                       "user %s\n"
+                       "base %s\n"
+                       "tail %s\n"
+                       "ccnt %u\n",
+                       mrid,
+                       ctx->branch,
+                       ctx->user,
+                       oid_to_hex(&ctx->base->object.oid),
+                       oid_to_hex(&ctx->tail->object.oid),
+                       ctx->commits_nr);
+
+       while (deps) {
+               strbuf_addf(&buf, "dep %s\n",
+                               oid_to_hex(&deps->item->object.oid));
+               deps = deps->next;
+       }
+
+       return strbuf_detach(&buf, NULL);
+}
+
+static void create_mr_ref(const char *ref, struct tag *tag)
+{
+       struct object_id *oid = &tag->object.oid;
+       struct object_id old_oid;
+
+       oidclr(&old_oid);
+       update_ref(NULL, ref, oid, &old_oid, REF_NO_DEREF,
+                       UPDATE_REFS_DIE_ON_ERR);
+}
+
+static int parse_msg_arg(const struct option *opt, const char *arg, int unset)
+{
+       struct strbuf *buf = opt->value;
+
+       BUG_ON_OPT_NEG(unset);
+
+       if (buf->len)
+               strbuf_addch(buf, '\n');
+       strbuf_addstr(buf, arg);
+       strbuf_complete_line(buf);
+
+       return 0;
+}
+
+static int parse_file_arg(const struct option *opt, const char *arg, int unset)
+{
+       struct strbuf *buf = opt->value;
+
+       BUG_ON_OPT_NEG(unset);
+
+       if (buf->len)
+               strbuf_addch(buf, '\n');
+
+       if (!strcmp(arg, "-")) {
+               if (strbuf_read(buf, 0, 0) < 0)
+                       die_errno(_("cannot read '%s'"), arg);
+       } else if (strbuf_read_file(buf, arg, 0) < 0)
+               die_errno(_("could not open or read '%s'"), arg);
+
+       strbuf_complete_line(buf);
+
+       return 0;
+}
+
+static void init_create_ctx(int argc, const char **argv, const char *prefix,
+               struct create_ctx *ctx)
+{
+       const char *remote = NULL, *start = NULL, *end = NULL, *branch = NULL;
+       struct strbuf buf = STRBUF_INIT;
+       struct option options[] = {
+               OPT_STRING('r', "remote", &remote, N_("remote"), N_("remote")),
+               OPT_STRING('s', "start", &start, N_("start"), N_("start")),
+               OPT_STRING('e', "end", &end, N_("end"), N_("end")),
+               OPT_STRING('b', "branch", &branch, N_("branch"), N_("branch")),
+               OPT_CALLBACK_F('m', "message", &ctx->msg, N_("message"),
+                              N_("message"), PARSE_OPT_NONEG, parse_msg_arg),
+               OPT_CALLBACK_F('F', "file", &ctx->msg, N_("file"),
+                       N_("read message from file"), PARSE_OPT_NONEG,
+                       parse_file_arg),
+               OPT_END()
+       };
+
+       argc = parse_options(argc, argv, prefix, options, create_usage,
+               PARSE_OPT_STOP_AT_NON_OPTION);
+
+       if (remote) {
+               if (!remote_is_configured(remote_get(remote), 1))
+                       die(_("no such remote: '%s'"), remote);
+               ctx->remote = xstrdup(remote);
+       }
+
+       if (start)
+               ctx->start = xstrdup(start);
+
+       if (end)
+               ctx->end = xstrdup(end);
+
+       if (branch) {
+               if (!starts_with(branch, "refs/heads/"))
+                       die(_("branch '%s' does not start with 'refs/heads/'"),
+                                       branch);
+               ctx->branch = xstrdup(branch);
+       }
+
+       switch (argc) {
+       case 0:
+               init_create_ctx_from_branch(ctx, branch_get(NULL));
+               break;
+       case 1:
+               init_create_ctx_from_upstream(ctx, argv[0]);
+               break;
+       default:
+               usage_with_options(create_usage, options);
+       }
+
+       strbuf_addf(&buf, "remote.%s.mruser", ctx->remote);
+
+       if (git_config_get_string(buf.buf, (char **)&ctx->user) &&
+           git_config_get_string("user.mruser", (char **)&ctx->user))
+               die(_("no mr user defined"));
+
+       strbuf_release(&buf);
+
+       init_mr_commits(ctx);
+}
+
+static void release_create_ctx(struct create_ctx *ctx)
+{
+       free((void *)ctx->upstream);
+       free((void *)ctx->start);
+       free((void *)ctx->end);
+       free((void *)ctx->remote);
+       free((void *)ctx->branch);
+       free((void *)ctx->user);
+       free_commit_list(ctx->commits);
+       free_commit_list(ctx->deps);
+       strbuf_release(&ctx->msg);
+}
+
+static int create_config(const char *k, const char *v, void *cb)
+{
+       /*
+       struct create_ctx *ctx = cb;
+       */
+
+       return git_default_config(k, v, cb);
+}
+
+static void generate_mr_msg(struct strbuf *msg, int edit)
+{
+       const char *path;
+       int fd;
+
+       if (msg->len && !edit)
+               return;
+
+       path = git_path("MR_EDITMSG");
+
+       fd = xopen(path, O_CREAT | O_TRUNC | O_WRONLY, 0600);
+       write_or_die(fd, msg->buf, msg->len);
+       close(fd);
+
+       strbuf_reset(msg);
+
+       if (launch_editor(path, msg, NULL)) {
+               fprintf(stderr,
+               _("Please supply the message using either -m or -F option.\n"));
+               exit(1);
+       }
+
+       if (!msg->len)
+               die(_("no mr message?"));
+}
+
+int cmd_create(int argc, const char **argv, const char *prefix)
+{
+       struct commit *head;
+       struct tag *tag;
+       char *mrid = NULL, *refname = NULL, *hdr = NULL;
+       struct create_ctx ctx = { .msg = STRBUF_INIT };
+
+       git_config(create_config, NULL);
+       init_create_ctx(argc, argv, prefix, &ctx);
+
+       mrid = generate_mr_mrid(ctx.commits);
+
+       refname = generate_mr_refname(ctx.remote, mrid, ctx.user);
+       if (ref_exists(refname))
+               die(_("merge request '%s' already exists"), refname);
+
+       hdr = generate_mr_hdr(mrid, &ctx);
+
+       generate_mr_msg(&ctx.msg, 0);
+
+       head = create_mr_head(ctx.commits, hdr);
+
+       tag = create_mr_tag(&ctx.msg, head, mrid);
+
+       create_mr_ref(refname, tag);
+
+       free((void *)mrid);
+       free((void *)refname);
+       free((void *)hdr);
+       release_create_ctx(&ctx);
+
+       return 0;
+}
diff --git a/src/gimrr.c b/src/gimrr.c
new file mode 100644 (file)
index 0000000..1e6fc15
--- /dev/null
@@ -0,0 +1,32 @@
+#include <stdio.h>
+#include "gimrr.h"
+
+static const char * const gimrr_usage[] = {
+       N_("gimrr mr list"),
+       N_("gimrr mr create [<upstream> [<head>]]"),
+       NULL
+};
+
+int cmd_main(int argc, const char *argv[])
+{
+       const char *prefix;
+       struct option options[] = {
+               OPT_END()
+       };
+
+       prefix = setup_git_directory();
+
+       argc = parse_options(argc, argv, prefix, options, gimrr_usage,
+               PARSE_OPT_STOP_AT_NON_OPTION);
+
+       if (argc < 1 || !strcmp(argv[0], "list"))
+               return cmd_list(argc, argv, prefix);
+       else if (!strcmp(argv[0], "create"))
+               return cmd_create(argc, argv, prefix);
+       else
+               error(_("unknown subcommand: %s"), argv[0]);
+
+       usage_with_options(gimrr_usage, options);
+
+       return 0;
+}
diff --git a/src/gimrr.h b/src/gimrr.h
new file mode 100644 (file)
index 0000000..082d09b
--- /dev/null
@@ -0,0 +1,18 @@
+#ifndef MR_H
+#define MR_H
+
+#include "remote.h"
+#include "refs.h"
+#include "revision.h"
+#include "object-store.h"
+#include "config.h"
+#include "commit-reach.h"
+#include "tag.h"
+#include "parse-options.h"
+#include "common.h"
+#include "strvec.h"
+
+int cmd_create(int argc, const char **argv, const char *prefix);
+int cmd_list(int argc, const char **argv, const char *prefix);
+
+#endif
diff --git a/src/gimrr.mak b/src/gimrr.mak
new file mode 100644 (file)
index 0000000..3805ca1
--- /dev/null
@@ -0,0 +1,10 @@
+GIMRR_GIT_DIR = contrib/gimrr
+GIMRR_OBJECTS += $(GIMRR_GIT_DIR)/gimrr.o
+GIMRR_OBJECTS += $(GIMRR_GIT_DIR)/create.o
+GIMRR_OBJECTS += $(GIMRR_GIT_DIR)/list.o
+GIMRR_OBJECTS += $(GIMRR_GIT_DIR)/common.o
+OBJECTS += $(GIMRR_OBJECTS)
+
+$(GIMRR_GIT_DIR)/gimrr$X: $(GIMRR_OBJECTS) GIT-LDFLAGS $(GITLIBS)
+       $(QUIET_LINK)$(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) \
+               $(filter %.o,$^) $(LIBS)
diff --git a/src/list.c b/src/list.c
new file mode 100644 (file)
index 0000000..0b4edf5
--- /dev/null
@@ -0,0 +1,6 @@
+#include "gimrr.h"
+
+int cmd_list(int argc, const char **argv, const char *prefix)
+{
+       return 0;
+}