зеркало из https://github.com/microsoft/git.git
Merge branch 'jc/push-cas'
Allow a safer "rewind of the remote tip" push than blind "--force", by requiring that the overwritten remote ref to be unchanged since the new history to replace it was prepared. The machinery is more or less ready. The "--force" option is again the big red button to override any safety, thanks to J6t's sanity (the original round allowed --lockref to defeat --force). The logic to choose the default implemented here is fragile (e.g. "git fetch" after seeing a failure will update the remote-tracking branch and will make the next "push" pass, defeating the safety pretty easily). It is suitable only for the simplest workflows, and it may hurt users more than it helps them. * jc/push-cas: push: teach --force-with-lease to smart-http transport send-pack: fix parsing of --force-with-lease option t5540/5541: smart-http does not support "--force-with-lease" t5533: test "push --force-with-lease" push --force-with-lease: tie it all together push --force-with-lease: implement logic to populate old_sha1_expect[] remote.c: add command line option parser for "--force-with-lease" builtin/push.c: use OPT_BOOL, not OPT_BOOLEAN cache.h: move remote/connect API out of it
This commit is contained in:
Коммит
2233ad4534
|
@ -11,6 +11,7 @@ SYNOPSIS
|
|||
[verse]
|
||||
'git push' [--all | --mirror | --tags] [--follow-tags] [-n | --dry-run] [--receive-pack=<git-receive-pack>]
|
||||
[--repo=<repository>] [-f | --force] [--prune] [-v | --verbose] [-u | --set-upstream]
|
||||
[--force-with-lease[=<refname>[:<expect>]]]
|
||||
[--no-verify] [<repository> [<refspec>...]]
|
||||
|
||||
DESCRIPTION
|
||||
|
@ -130,21 +131,75 @@ already exists on the remote side.
|
|||
repository over ssh, and you do not have the program in
|
||||
a directory on the default $PATH.
|
||||
|
||||
--[no-]force-with-lease::
|
||||
--force-with-lease=<refname>::
|
||||
--force-with-lease=<refname>:<expect>::
|
||||
Usually, "git push" refuses to update a remote ref that is
|
||||
not an ancestor of the local ref used to overwrite it.
|
||||
+
|
||||
This option bypasses the check, but instead requires that the
|
||||
current value of the ref to be the expected value. "git push"
|
||||
fails otherwise.
|
||||
+
|
||||
Imagine that you have to rebase what you have already published.
|
||||
You will have to bypass the "must fast-forward" rule in order to
|
||||
replace the history you originally published with the rebased history.
|
||||
If somebody else built on top of your original history while you are
|
||||
rebasing, the tip of the branch at the remote may advance with her
|
||||
commit, and blindly pushing with `--force` will lose her work.
|
||||
+
|
||||
This option allows you to say that you expect the history you are
|
||||
updating is what you rebased and want to replace. If the remote ref
|
||||
still points at the commit you specified, you can be sure that no
|
||||
other people did anything to the ref (it is like taking a "lease" on
|
||||
the ref without explicitly locking it, and you update the ref while
|
||||
making sure that your earlier "lease" is still valid).
|
||||
+
|
||||
`--force-with-lease` alone, without specifying the details, will protect
|
||||
all remote refs that are going to be updated by requiring their
|
||||
current value to be the same as the remote-tracking branch we have
|
||||
for them, unless specified with a `--force-with-lease=<refname>:<expect>`
|
||||
option that explicitly states what the expected value is.
|
||||
+
|
||||
`--force-with-lease=<refname>`, without specifying the expected value, will
|
||||
protect the named ref (alone), if it is going to be updated, by
|
||||
requiring its current value to be the same as the remote-tracking
|
||||
branch we have for it.
|
||||
+
|
||||
`--force-with-lease=<refname>:<expect>` will protect the named ref (alone),
|
||||
if it is going to be updated, by requiring its current value to be
|
||||
the same as the specified value <expect> (which is allowed to be
|
||||
different from the remote-tracking branch we have for the refname,
|
||||
or we do not even have to have such a remote-tracking branch when
|
||||
this form is used).
|
||||
+
|
||||
Note that all forms other than `--force-with-lease=<refname>:<expect>`
|
||||
that specifies the expected current value of the ref explicitly are
|
||||
still experimental and their semantics may change as we gain experience
|
||||
with this feature.
|
||||
+
|
||||
"--no-force-with-lease" will cancel all the previous --force-with-lease on the
|
||||
command line.
|
||||
|
||||
-f::
|
||||
--force::
|
||||
Usually, the command refuses to update a remote ref that is
|
||||
not an ancestor of the local ref used to overwrite it.
|
||||
This flag disables the check. This can cause the
|
||||
remote repository to lose commits; use it with care.
|
||||
Note that `--force` applies to all the refs that are pushed,
|
||||
hence using it with `push.default` set to `matching` or with
|
||||
multiple push destinations configured with `remote.*.push`
|
||||
may overwrite refs other than the current branch (including
|
||||
local refs that are strictly behind their remote counterpart).
|
||||
To force a push to only one branch, use a `+` in front of the
|
||||
refspec to push (e.g `git push origin +master` to force a push
|
||||
to the `master` branch). See the `<refspec>...` section above
|
||||
for details.
|
||||
Also, when `--force-with-lease` option is used, the command refuses
|
||||
to update a remote ref whose current value does not match
|
||||
what is expected.
|
||||
+
|
||||
This flag disables these checks, and can cause the remote repository
|
||||
to lose commits; use it with care.
|
||||
+
|
||||
Note that `--force` applies to all the refs that are pushed, hence
|
||||
using it with `push.default` set to `matching` or with multiple push
|
||||
destinations configured with `remote.*.push` may overwrite refs
|
||||
other than the current branch (including local refs that are
|
||||
strictly behind their remote counterpart). To force a push to only
|
||||
one branch, use a `+` in front of the refspec to push (e.g `git push
|
||||
origin +master` to force a push to the `master` branch). See the
|
||||
`<refspec>...` section above for details.
|
||||
|
||||
--repo=<repository>::
|
||||
This option is only relevant if no <repository> argument is
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
#include "builtin.h"
|
||||
#include "pkt-line.h"
|
||||
#include "fetch-pack.h"
|
||||
#include "remote.h"
|
||||
#include "connect.h"
|
||||
|
||||
static const char fetch_pack_usage[] =
|
||||
"git fetch-pack [--all] [--stdin] [--quiet|-q] [--keep|-k] [--thin] "
|
||||
|
|
|
@ -21,6 +21,8 @@ static const char *receivepack;
|
|||
static int verbosity;
|
||||
static int progress = -1;
|
||||
|
||||
static struct push_cas_option cas;
|
||||
|
||||
static const char **refspec;
|
||||
static int refspec_nr;
|
||||
static int refspec_alloc;
|
||||
|
@ -316,6 +318,13 @@ static int push_with_options(struct transport *transport, int flags)
|
|||
if (thin)
|
||||
transport_set_option(transport, TRANS_OPT_THIN, "yes");
|
||||
|
||||
if (!is_empty_cas(&cas)) {
|
||||
if (!transport->smart_options)
|
||||
die("underlying transport does not support --%s option",
|
||||
CAS_OPT_NAME);
|
||||
transport->smart_options->cas = &cas;
|
||||
}
|
||||
|
||||
if (verbosity > 0)
|
||||
fprintf(stderr, _("Pushing to %s\n"), transport->url);
|
||||
err = transport_push(transport, refspec_nr, refspec, flags,
|
||||
|
@ -451,6 +460,10 @@ int cmd_push(int argc, const char **argv, const char *prefix)
|
|||
OPT_BIT('n' , "dry-run", &flags, N_("dry run"), TRANSPORT_PUSH_DRY_RUN),
|
||||
OPT_BIT( 0, "porcelain", &flags, N_("machine-readable output"), TRANSPORT_PUSH_PORCELAIN),
|
||||
OPT_BIT('f', "force", &flags, N_("force updates"), TRANSPORT_PUSH_FORCE),
|
||||
{ OPTION_CALLBACK,
|
||||
0, CAS_OPT_NAME, &cas, N_("refname>:<expect"),
|
||||
N_("require old value of ref to be at this value"),
|
||||
PARSE_OPT_OPTARG, parseopt_push_cas_option },
|
||||
{ OPTION_CALLBACK, 0, "recurse-submodules", &flags, N_("check"),
|
||||
N_("control recursive pushing of submodules"),
|
||||
PARSE_OPT_OPTARG, option_parse_recurse_submodules },
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
#include "commit.h"
|
||||
#include "object.h"
|
||||
#include "remote.h"
|
||||
#include "connect.h"
|
||||
#include "transport.h"
|
||||
#include "string-list.h"
|
||||
#include "sha1-array.h"
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
#include "sideband.h"
|
||||
#include "run-command.h"
|
||||
#include "remote.h"
|
||||
#include "connect.h"
|
||||
#include "send-pack.h"
|
||||
#include "quote.h"
|
||||
#include "transport.h"
|
||||
|
@ -54,6 +55,11 @@ static void print_helper_status(struct ref *ref)
|
|||
msg = "needs force";
|
||||
break;
|
||||
|
||||
case REF_STATUS_REJECT_STALE:
|
||||
res = "error";
|
||||
msg = "stale info";
|
||||
break;
|
||||
|
||||
case REF_STATUS_REJECT_ALREADY_EXISTS:
|
||||
res = "error";
|
||||
msg = "already exists";
|
||||
|
@ -102,6 +108,7 @@ int cmd_send_pack(int argc, const char **argv, const char *prefix)
|
|||
int flags;
|
||||
unsigned int reject_reasons;
|
||||
int progress = -1;
|
||||
struct push_cas_option cas = {0};
|
||||
|
||||
argv++;
|
||||
for (i = 1; i < argc; i++, argv++) {
|
||||
|
@ -164,6 +171,22 @@ int cmd_send_pack(int argc, const char **argv, const char *prefix)
|
|||
helper_status = 1;
|
||||
continue;
|
||||
}
|
||||
if (!strcmp(arg, "--" CAS_OPT_NAME)) {
|
||||
if (parse_push_cas_option(&cas, NULL, 0) < 0)
|
||||
exit(1);
|
||||
continue;
|
||||
}
|
||||
if (!strcmp(arg, "--no-" CAS_OPT_NAME)) {
|
||||
if (parse_push_cas_option(&cas, NULL, 1) < 0)
|
||||
exit(1);
|
||||
continue;
|
||||
}
|
||||
if (!prefixcmp(arg, "--" CAS_OPT_NAME "=")) {
|
||||
if (parse_push_cas_option(&cas,
|
||||
strchr(arg, '=') + 1, 0) < 0)
|
||||
exit(1);
|
||||
continue;
|
||||
}
|
||||
usage(send_pack_usage);
|
||||
}
|
||||
if (!dest) {
|
||||
|
@ -224,6 +247,9 @@ int cmd_send_pack(int argc, const char **argv, const char *prefix)
|
|||
if (match_push_refs(local_refs, &remote_refs, nr_refspecs, refspecs, flags))
|
||||
return -1;
|
||||
|
||||
if (!is_empty_cas(&cas))
|
||||
apply_push_cas(&cas, remote, remote_refs);
|
||||
|
||||
set_ref_status_for_push(remote_refs, args.send_mirror,
|
||||
args.force_update);
|
||||
|
||||
|
|
62
cache.h
62
cache.h
|
@ -1038,68 +1038,6 @@ struct pack_entry {
|
|||
struct packed_git *p;
|
||||
};
|
||||
|
||||
struct ref {
|
||||
struct ref *next;
|
||||
unsigned char old_sha1[20];
|
||||
unsigned char new_sha1[20];
|
||||
char *symref;
|
||||
unsigned int
|
||||
force:1,
|
||||
forced_update:1,
|
||||
deletion:1,
|
||||
matched:1;
|
||||
|
||||
/*
|
||||
* Order is important here, as we write to FETCH_HEAD
|
||||
* in numeric order. And the default NOT_FOR_MERGE
|
||||
* should be 0, so that xcalloc'd structures get it
|
||||
* by default.
|
||||
*/
|
||||
enum {
|
||||
FETCH_HEAD_MERGE = -1,
|
||||
FETCH_HEAD_NOT_FOR_MERGE = 0,
|
||||
FETCH_HEAD_IGNORE = 1
|
||||
} fetch_head_status;
|
||||
|
||||
enum {
|
||||
REF_STATUS_NONE = 0,
|
||||
REF_STATUS_OK,
|
||||
REF_STATUS_REJECT_NONFASTFORWARD,
|
||||
REF_STATUS_REJECT_ALREADY_EXISTS,
|
||||
REF_STATUS_REJECT_NODELETE,
|
||||
REF_STATUS_REJECT_FETCH_FIRST,
|
||||
REF_STATUS_REJECT_NEEDS_FORCE,
|
||||
REF_STATUS_UPTODATE,
|
||||
REF_STATUS_REMOTE_REJECT,
|
||||
REF_STATUS_EXPECTING_REPORT
|
||||
} status;
|
||||
char *remote_status;
|
||||
struct ref *peer_ref; /* when renaming */
|
||||
char name[FLEX_ARRAY]; /* more */
|
||||
};
|
||||
|
||||
#define REF_NORMAL (1u << 0)
|
||||
#define REF_HEADS (1u << 1)
|
||||
#define REF_TAGS (1u << 2)
|
||||
|
||||
extern struct ref *find_ref_by_name(const struct ref *list, const char *name);
|
||||
|
||||
#define CONNECT_VERBOSE (1u << 0)
|
||||
extern struct child_process *git_connect(int fd[2], const char *url, const char *prog, int flags);
|
||||
extern int finish_connect(struct child_process *conn);
|
||||
extern int git_connection_is_socket(struct child_process *conn);
|
||||
struct extra_have_objects {
|
||||
int nr, alloc;
|
||||
unsigned char (*array)[20];
|
||||
};
|
||||
extern struct ref **get_remote_heads(int in, char *src_buf, size_t src_len,
|
||||
struct ref **list, unsigned int flags,
|
||||
struct extra_have_objects *);
|
||||
extern int server_supports(const char *feature);
|
||||
extern int parse_feature_request(const char *features, const char *feature);
|
||||
extern const char *server_feature_value(const char *feature, int *len_ret);
|
||||
extern const char *parse_feature_value(const char *feature_list, const char *feature, int *len_ret);
|
||||
|
||||
extern struct packed_git *parse_pack_index(unsigned char *sha1, const char *idx_path);
|
||||
|
||||
/* A hook for count-objects to report invalid files in pack directory */
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
#include "refs.h"
|
||||
#include "run-command.h"
|
||||
#include "remote.h"
|
||||
#include "connect.h"
|
||||
#include "url.h"
|
||||
|
||||
static char *server_capabilities;
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
#ifndef CONNECT_H
|
||||
#define CONNECT_H
|
||||
|
||||
#define CONNECT_VERBOSE (1u << 0)
|
||||
extern struct child_process *git_connect(int fd[2], const char *url, const char *prog, int flags);
|
||||
extern int finish_connect(struct child_process *conn);
|
||||
extern int git_connection_is_socket(struct child_process *conn);
|
||||
extern int server_supports(const char *feature);
|
||||
extern int parse_feature_request(const char *features, const char *feature);
|
||||
extern const char *server_feature_value(const char *feature, int *len_ret);
|
||||
extern const char *parse_feature_value(const char *feature_list, const char *feature, int *len_ret);
|
||||
|
||||
#endif
|
|
@ -9,6 +9,7 @@
|
|||
#include "fetch-pack.h"
|
||||
#include "remote.h"
|
||||
#include "run-command.h"
|
||||
#include "connect.h"
|
||||
#include "transport.h"
|
||||
#include "version.h"
|
||||
#include "prio-queue.h"
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
#define FETCH_PACK_H
|
||||
|
||||
#include "string-list.h"
|
||||
#include "run-command.h"
|
||||
|
||||
struct fetch_pack_args {
|
||||
const char *uploadpack;
|
||||
|
|
8
refs.c
8
refs.c
|
@ -3196,14 +3196,6 @@ int update_ref(const char *action, const char *refname,
|
|||
return 0;
|
||||
}
|
||||
|
||||
struct ref *find_ref_by_name(const struct ref *list, const char *name)
|
||||
{
|
||||
for ( ; list; list = list->next)
|
||||
if (!strcmp(list->name, name))
|
||||
return (struct ref *)list;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/*
|
||||
* generate a format suitable for scanf from a ref_rev_parse_rules
|
||||
* rule, that is replace the "%.*s" spec with a "%s" spec
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
#include "exec_cmd.h"
|
||||
#include "run-command.h"
|
||||
#include "pkt-line.h"
|
||||
#include "string-list.h"
|
||||
#include "sideband.h"
|
||||
#include "argv-array.h"
|
||||
|
||||
|
@ -22,6 +23,7 @@ struct options {
|
|||
thin : 1;
|
||||
};
|
||||
static struct options options;
|
||||
static struct string_list cas_options = STRING_LIST_INIT_DUP;
|
||||
|
||||
static int set_option(const char *name, const char *value)
|
||||
{
|
||||
|
@ -77,6 +79,13 @@ static int set_option(const char *name, const char *value)
|
|||
return -1;
|
||||
return 0;
|
||||
}
|
||||
else if (!strcmp(name, "cas")) {
|
||||
struct strbuf val = STRBUF_INIT;
|
||||
strbuf_addf(&val, "--" CAS_OPT_NAME "=%s", value);
|
||||
string_list_append(&cas_options, val.buf);
|
||||
strbuf_release(&val);
|
||||
return 0;
|
||||
}
|
||||
else {
|
||||
return 1 /* unsupported */;
|
||||
}
|
||||
|
@ -802,6 +811,7 @@ static int push_git(struct discovery *heads, int nr_spec, char **specs)
|
|||
struct rpc_state rpc;
|
||||
int i, err;
|
||||
struct argv_array args;
|
||||
struct string_list_item *cas_option;
|
||||
|
||||
argv_array_init(&args);
|
||||
argv_array_pushl(&args, "send-pack", "--stateless-rpc", "--helper-status",
|
||||
|
@ -816,6 +826,8 @@ static int push_git(struct discovery *heads, int nr_spec, char **specs)
|
|||
else if (options.verbosity > 1)
|
||||
argv_array_push(&args, "--verbose");
|
||||
argv_array_push(&args, options.progress ? "--progress" : "--no-progress");
|
||||
for_each_string_list_item(cas_option, &cas_options)
|
||||
argv_array_push(&args, cas_option->string);
|
||||
argv_array_push(&args, url);
|
||||
for (i = 0; i < nr_spec; i++)
|
||||
argv_array_push(&args, specs[i]);
|
||||
|
|
175
remote.c
175
remote.c
|
@ -1305,6 +1305,14 @@ static void add_missing_tags(struct ref *src, struct ref **dst, struct ref ***ds
|
|||
free(sent_tips.tip);
|
||||
}
|
||||
|
||||
struct ref *find_ref_by_name(const struct ref *list, const char *name)
|
||||
{
|
||||
for ( ; list; list = list->next)
|
||||
if (!strcmp(list->name, name))
|
||||
return (struct ref *)list;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void prepare_ref_index(struct string_list *ref_index, struct ref *ref)
|
||||
{
|
||||
for ( ; ref; ref = ref->next)
|
||||
|
@ -1414,12 +1422,13 @@ int match_push_refs(struct ref *src, struct ref **dst,
|
|||
}
|
||||
|
||||
void set_ref_status_for_push(struct ref *remote_refs, int send_mirror,
|
||||
int force_update)
|
||||
int force_update)
|
||||
{
|
||||
struct ref *ref;
|
||||
|
||||
for (ref = remote_refs; ref; ref = ref->next) {
|
||||
int force_ref_update = ref->force || force_update;
|
||||
int reject_reason = 0;
|
||||
|
||||
if (ref->peer_ref)
|
||||
hashcpy(ref->new_sha1, ref->peer_ref->new_sha1);
|
||||
|
@ -1434,6 +1443,26 @@ void set_ref_status_for_push(struct ref *remote_refs, int send_mirror,
|
|||
}
|
||||
|
||||
/*
|
||||
* Bypass the usual "must fast-forward" check but
|
||||
* replace it with a weaker "the old value must be
|
||||
* this value we observed". If the remote ref has
|
||||
* moved and is now different from what we expect,
|
||||
* reject any push.
|
||||
*
|
||||
* It also is an error if the user told us to check
|
||||
* with the remote-tracking branch to find the value
|
||||
* to expect, but we did not have such a tracking
|
||||
* branch.
|
||||
*/
|
||||
if (ref->expect_old_sha1) {
|
||||
if (ref->expect_old_no_trackback ||
|
||||
hashcmp(ref->old_sha1, ref->old_sha1_expect))
|
||||
reject_reason = REF_STATUS_REJECT_STALE;
|
||||
}
|
||||
|
||||
/*
|
||||
* The usual "must fast-forward" rules.
|
||||
*
|
||||
* Decide whether an individual refspec A:B can be
|
||||
* pushed. The push will succeed if any of the
|
||||
* following are true:
|
||||
|
@ -1451,24 +1480,26 @@ void set_ref_status_for_push(struct ref *remote_refs, int send_mirror,
|
|||
* passing the --force argument
|
||||
*/
|
||||
|
||||
if (!ref->deletion && !is_null_sha1(ref->old_sha1)) {
|
||||
int why = 0; /* why would this push require --force? */
|
||||
|
||||
else if (!ref->deletion && !is_null_sha1(ref->old_sha1)) {
|
||||
if (!prefixcmp(ref->name, "refs/tags/"))
|
||||
why = REF_STATUS_REJECT_ALREADY_EXISTS;
|
||||
reject_reason = REF_STATUS_REJECT_ALREADY_EXISTS;
|
||||
else if (!has_sha1_file(ref->old_sha1))
|
||||
why = REF_STATUS_REJECT_FETCH_FIRST;
|
||||
reject_reason = 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;
|
||||
reject_reason = 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;
|
||||
reject_reason = REF_STATUS_REJECT_NONFASTFORWARD;
|
||||
}
|
||||
|
||||
/*
|
||||
* "--force" will defeat any rejection implemented
|
||||
* by the rules above.
|
||||
*/
|
||||
if (!force_ref_update)
|
||||
ref->status = reject_reason;
|
||||
else if (reject_reason)
|
||||
ref->forced_update = 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1939,3 +1970,121 @@ struct ref *get_stale_heads(struct refspec *refs, int ref_count, struct ref *fet
|
|||
string_list_clear(&ref_names, 0);
|
||||
return stale_refs;
|
||||
}
|
||||
|
||||
/*
|
||||
* Compare-and-swap
|
||||
*/
|
||||
void clear_cas_option(struct push_cas_option *cas)
|
||||
{
|
||||
int i;
|
||||
|
||||
for (i = 0; i < cas->nr; i++)
|
||||
free(cas->entry[i].refname);
|
||||
free(cas->entry);
|
||||
memset(cas, 0, sizeof(*cas));
|
||||
}
|
||||
|
||||
static struct push_cas *add_cas_entry(struct push_cas_option *cas,
|
||||
const char *refname,
|
||||
size_t refnamelen)
|
||||
{
|
||||
struct push_cas *entry;
|
||||
ALLOC_GROW(cas->entry, cas->nr + 1, cas->alloc);
|
||||
entry = &cas->entry[cas->nr++];
|
||||
memset(entry, 0, sizeof(*entry));
|
||||
entry->refname = xmemdupz(refname, refnamelen);
|
||||
return entry;
|
||||
}
|
||||
|
||||
int parse_push_cas_option(struct push_cas_option *cas, const char *arg, int unset)
|
||||
{
|
||||
const char *colon;
|
||||
struct push_cas *entry;
|
||||
|
||||
if (unset) {
|
||||
/* "--no-<option>" */
|
||||
clear_cas_option(cas);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!arg) {
|
||||
/* just "--<option>" */
|
||||
cas->use_tracking_for_rest = 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* "--<option>=refname" or "--<option>=refname:value" */
|
||||
colon = strchrnul(arg, ':');
|
||||
entry = add_cas_entry(cas, arg, colon - arg);
|
||||
if (!*colon)
|
||||
entry->use_tracking = 1;
|
||||
else if (get_sha1(colon + 1, entry->expect))
|
||||
return error("cannot parse expected object name '%s'", colon + 1);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int parseopt_push_cas_option(const struct option *opt, const char *arg, int unset)
|
||||
{
|
||||
return parse_push_cas_option(opt->value, arg, unset);
|
||||
}
|
||||
|
||||
int is_empty_cas(const struct push_cas_option *cas)
|
||||
{
|
||||
return !cas->use_tracking_for_rest && !cas->nr;
|
||||
}
|
||||
|
||||
/*
|
||||
* Look at remote.fetch refspec and see if we have a remote
|
||||
* tracking branch for the refname there. Fill its current
|
||||
* value in sha1[].
|
||||
* If we cannot do so, return negative to signal an error.
|
||||
*/
|
||||
static int remote_tracking(struct remote *remote, const char *refname,
|
||||
unsigned char sha1[20])
|
||||
{
|
||||
char *dst;
|
||||
|
||||
dst = apply_refspecs(remote->fetch, remote->fetch_refspec_nr, refname);
|
||||
if (!dst)
|
||||
return -1; /* no tracking ref for refname at remote */
|
||||
if (read_ref(dst, sha1))
|
||||
return -1; /* we know what the tracking ref is but we cannot read it */
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void apply_cas(struct push_cas_option *cas,
|
||||
struct remote *remote,
|
||||
struct ref *ref)
|
||||
{
|
||||
int i;
|
||||
|
||||
/* Find an explicit --<option>=<name>[:<value>] entry */
|
||||
for (i = 0; i < cas->nr; i++) {
|
||||
struct push_cas *entry = &cas->entry[i];
|
||||
if (!refname_match(entry->refname, ref->name, ref_rev_parse_rules))
|
||||
continue;
|
||||
ref->expect_old_sha1 = 1;
|
||||
if (!entry->use_tracking)
|
||||
hashcpy(ref->old_sha1_expect, cas->entry[i].expect);
|
||||
else if (remote_tracking(remote, ref->name, ref->old_sha1_expect))
|
||||
ref->expect_old_no_trackback = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
/* Are we using "--<option>" to cover all? */
|
||||
if (!cas->use_tracking_for_rest)
|
||||
return;
|
||||
|
||||
ref->expect_old_sha1 = 1;
|
||||
if (remote_tracking(remote, ref->name, ref->old_sha1_expect))
|
||||
ref->expect_old_no_trackback = 1;
|
||||
}
|
||||
|
||||
void apply_push_cas(struct push_cas_option *cas,
|
||||
struct remote *remote,
|
||||
struct ref *remote_refs)
|
||||
{
|
||||
struct ref *ref;
|
||||
for (ref = remote_refs; ref; ref = ref->next)
|
||||
apply_cas(cas, remote, ref);
|
||||
}
|
||||
|
|
83
remote.h
83
remote.h
|
@ -1,6 +1,8 @@
|
|||
#ifndef REMOTE_H
|
||||
#define REMOTE_H
|
||||
|
||||
#include "parse-options.h"
|
||||
|
||||
enum {
|
||||
REMOTE_CONFIG,
|
||||
REMOTE_REMOTES,
|
||||
|
@ -72,6 +74,56 @@ struct refspec {
|
|||
|
||||
extern const struct refspec *tag_refspec;
|
||||
|
||||
struct ref {
|
||||
struct ref *next;
|
||||
unsigned char old_sha1[20];
|
||||
unsigned char new_sha1[20];
|
||||
unsigned char old_sha1_expect[20]; /* used by expect-old */
|
||||
char *symref;
|
||||
unsigned int
|
||||
force:1,
|
||||
forced_update:1,
|
||||
expect_old_sha1:1,
|
||||
expect_old_no_trackback:1,
|
||||
deletion:1,
|
||||
matched:1;
|
||||
|
||||
/*
|
||||
* Order is important here, as we write to FETCH_HEAD
|
||||
* in numeric order. And the default NOT_FOR_MERGE
|
||||
* should be 0, so that xcalloc'd structures get it
|
||||
* by default.
|
||||
*/
|
||||
enum {
|
||||
FETCH_HEAD_MERGE = -1,
|
||||
FETCH_HEAD_NOT_FOR_MERGE = 0,
|
||||
FETCH_HEAD_IGNORE = 1
|
||||
} fetch_head_status;
|
||||
|
||||
enum {
|
||||
REF_STATUS_NONE = 0,
|
||||
REF_STATUS_OK,
|
||||
REF_STATUS_REJECT_NONFASTFORWARD,
|
||||
REF_STATUS_REJECT_ALREADY_EXISTS,
|
||||
REF_STATUS_REJECT_NODELETE,
|
||||
REF_STATUS_REJECT_FETCH_FIRST,
|
||||
REF_STATUS_REJECT_NEEDS_FORCE,
|
||||
REF_STATUS_REJECT_STALE,
|
||||
REF_STATUS_UPTODATE,
|
||||
REF_STATUS_REMOTE_REJECT,
|
||||
REF_STATUS_EXPECTING_REPORT
|
||||
} status;
|
||||
char *remote_status;
|
||||
struct ref *peer_ref; /* when renaming */
|
||||
char name[FLEX_ARRAY]; /* more */
|
||||
};
|
||||
|
||||
#define REF_NORMAL (1u << 0)
|
||||
#define REF_HEADS (1u << 1)
|
||||
#define REF_TAGS (1u << 2)
|
||||
|
||||
extern struct ref *find_ref_by_name(const struct ref *list, const char *name);
|
||||
|
||||
struct ref *alloc_ref(const char *name);
|
||||
struct ref *copy_ref(const struct ref *ref);
|
||||
struct ref *copy_ref_list(const struct ref *ref);
|
||||
|
@ -85,6 +137,14 @@ int check_ref_type(const struct ref *ref, int flags);
|
|||
*/
|
||||
void free_refs(struct ref *ref);
|
||||
|
||||
struct extra_have_objects {
|
||||
int nr, alloc;
|
||||
unsigned char (*array)[20];
|
||||
};
|
||||
extern struct ref **get_remote_heads(int in, char *src_buf, size_t src_len,
|
||||
struct ref **list, unsigned int flags,
|
||||
struct extra_have_objects *);
|
||||
|
||||
int resolve_remote_symref(struct ref *ref, struct ref *list);
|
||||
int ref_newer(const unsigned char *new_sha1, const unsigned char *old_sha1);
|
||||
|
||||
|
@ -173,4 +233,27 @@ struct ref *guess_remote_head(const struct ref *head,
|
|||
/* Return refs which no longer exist on remote */
|
||||
struct ref *get_stale_heads(struct refspec *refs, int ref_count, struct ref *fetch_map);
|
||||
|
||||
/*
|
||||
* Compare-and-swap
|
||||
*/
|
||||
#define CAS_OPT_NAME "force-with-lease"
|
||||
|
||||
struct push_cas_option {
|
||||
unsigned use_tracking_for_rest:1;
|
||||
struct push_cas {
|
||||
unsigned char expect[20];
|
||||
unsigned use_tracking:1;
|
||||
char *refname;
|
||||
} *entry;
|
||||
int nr;
|
||||
int alloc;
|
||||
};
|
||||
|
||||
extern int parseopt_push_cas_option(const struct option *, const char *arg, int unset);
|
||||
extern int parse_push_cas_option(struct push_cas_option *, const char *arg, int unset);
|
||||
extern void clear_cas_option(struct push_cas_option *);
|
||||
|
||||
extern int is_empty_cas(const struct push_cas_option *);
|
||||
void apply_push_cas(struct push_cas_option *, struct remote *, struct ref *);
|
||||
|
||||
#endif
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
#include "sideband.h"
|
||||
#include "run-command.h"
|
||||
#include "remote.h"
|
||||
#include "connect.h"
|
||||
#include "send-pack.h"
|
||||
#include "quote.h"
|
||||
#include "transport.h"
|
||||
|
@ -226,6 +227,7 @@ int send_pack(struct send_pack_args *args,
|
|||
case REF_STATUS_REJECT_ALREADY_EXISTS:
|
||||
case REF_STATUS_REJECT_FETCH_FIRST:
|
||||
case REF_STATUS_REJECT_NEEDS_FORCE:
|
||||
case REF_STATUS_REJECT_STALE:
|
||||
case REF_STATUS_UPTODATE:
|
||||
continue;
|
||||
default:
|
||||
|
|
|
@ -141,10 +141,11 @@ stop_httpd() {
|
|||
-f "$TEST_PATH/apache.conf" $HTTPD_PARA -k stop
|
||||
}
|
||||
|
||||
test_http_push_nonff() {
|
||||
test_http_push_nonff () {
|
||||
REMOTE_REPO=$1
|
||||
LOCAL_REPO=$2
|
||||
BRANCH=$3
|
||||
EXPECT_CAS_RESULT=${4-failure}
|
||||
|
||||
test_expect_success 'non-fast-forward push fails' '
|
||||
cd "$REMOTE_REPO" &&
|
||||
|
@ -167,6 +168,22 @@ test_http_push_nonff() {
|
|||
test_expect_success 'non-fast-forward push shows help message' '
|
||||
test_i18ngrep "Updates were rejected because" output
|
||||
'
|
||||
|
||||
test_expect_failure 'force with lease aka cas' '
|
||||
HEAD=$( cd "$REMOTE_REPO" && git rev-parse --verify HEAD ) &&
|
||||
test_when_finished '\''
|
||||
(cd "$REMOTE_REPO" && git update-ref HEAD "$HEAD")
|
||||
'\'' &&
|
||||
(
|
||||
cd "$LOCAL_REPO" &&
|
||||
git push -v --force-with-lease=$BRANCH:$HEAD origin
|
||||
) &&
|
||||
git rev-parse --verify "$BRANCH" >expect &&
|
||||
(
|
||||
cd "$REMOTE_REPO" && git rev-parse --verify HEAD
|
||||
) >actual &&
|
||||
test_cmp expect actual
|
||||
'
|
||||
}
|
||||
|
||||
setup_askpass_helper() {
|
||||
|
|
|
@ -0,0 +1,189 @@
|
|||
#!/bin/sh
|
||||
|
||||
test_description='compare & swap push force/delete safety'
|
||||
|
||||
. ./test-lib.sh
|
||||
|
||||
setup_srcdst_basic () {
|
||||
rm -fr src dst &&
|
||||
git clone --no-local . src &&
|
||||
git clone --no-local src dst &&
|
||||
(
|
||||
cd src && git checkout HEAD^0
|
||||
)
|
||||
}
|
||||
|
||||
test_expect_success setup '
|
||||
: create template repository
|
||||
test_commit A &&
|
||||
test_commit B &&
|
||||
test_commit C
|
||||
'
|
||||
|
||||
test_expect_success 'push to update (protected)' '
|
||||
setup_srcdst_basic &&
|
||||
(
|
||||
cd dst &&
|
||||
test_commit D &&
|
||||
test_must_fail git push --force-with-lease=master:master origin master
|
||||
) &&
|
||||
git ls-remote . refs/heads/master >expect &&
|
||||
git ls-remote src refs/heads/master >actual &&
|
||||
test_cmp expect actual
|
||||
'
|
||||
|
||||
test_expect_success 'push to update (protected, forced)' '
|
||||
setup_srcdst_basic &&
|
||||
(
|
||||
cd dst &&
|
||||
test_commit D &&
|
||||
git push --force --force-with-lease=master:master origin master
|
||||
) &&
|
||||
git ls-remote dst refs/heads/master >expect &&
|
||||
git ls-remote src refs/heads/master >actual &&
|
||||
test_cmp expect actual
|
||||
'
|
||||
|
||||
test_expect_success 'push to update (protected, tracking)' '
|
||||
setup_srcdst_basic &&
|
||||
(
|
||||
cd src &&
|
||||
git checkout master &&
|
||||
test_commit D &&
|
||||
git checkout HEAD^0
|
||||
) &&
|
||||
git ls-remote src refs/heads/master >expect &&
|
||||
(
|
||||
cd dst &&
|
||||
test_commit E &&
|
||||
git ls-remote . refs/remotes/origin/master >expect &&
|
||||
test_must_fail git push --force-with-lease=master origin master &&
|
||||
git ls-remote . refs/remotes/origin/master >actual &&
|
||||
test_cmp expect actual
|
||||
) &&
|
||||
git ls-remote src refs/heads/master >actual &&
|
||||
test_cmp expect actual
|
||||
'
|
||||
|
||||
test_expect_success 'push to update (protected, tracking, forced)' '
|
||||
setup_srcdst_basic &&
|
||||
(
|
||||
cd src &&
|
||||
git checkout master &&
|
||||
test_commit D &&
|
||||
git checkout HEAD^0
|
||||
) &&
|
||||
(
|
||||
cd dst &&
|
||||
test_commit E &&
|
||||
git ls-remote . refs/remotes/origin/master >expect &&
|
||||
git push --force --force-with-lease=master origin master
|
||||
) &&
|
||||
git ls-remote dst refs/heads/master >expect &&
|
||||
git ls-remote src refs/heads/master >actual &&
|
||||
test_cmp expect actual
|
||||
'
|
||||
|
||||
test_expect_success 'push to update (allowed)' '
|
||||
setup_srcdst_basic &&
|
||||
(
|
||||
cd dst &&
|
||||
test_commit D &&
|
||||
git push --force-with-lease=master:master^ origin master
|
||||
) &&
|
||||
git ls-remote dst refs/heads/master >expect &&
|
||||
git ls-remote src refs/heads/master >actual &&
|
||||
test_cmp expect actual
|
||||
'
|
||||
|
||||
test_expect_success 'push to update (allowed, tracking)' '
|
||||
setup_srcdst_basic &&
|
||||
(
|
||||
cd dst &&
|
||||
test_commit D &&
|
||||
git push --force-with-lease=master origin master
|
||||
) &&
|
||||
git ls-remote dst refs/heads/master >expect &&
|
||||
git ls-remote src refs/heads/master >actual &&
|
||||
test_cmp expect actual
|
||||
'
|
||||
|
||||
test_expect_success 'push to update (allowed even though no-ff)' '
|
||||
setup_srcdst_basic &&
|
||||
(
|
||||
cd dst &&
|
||||
git reset --hard HEAD^ &&
|
||||
test_commit D &&
|
||||
git push --force-with-lease=master origin master
|
||||
) &&
|
||||
git ls-remote dst refs/heads/master >expect &&
|
||||
git ls-remote src refs/heads/master >actual &&
|
||||
test_cmp expect actual
|
||||
'
|
||||
|
||||
test_expect_success 'push to delete (protected)' '
|
||||
setup_srcdst_basic &&
|
||||
git ls-remote src refs/heads/master >expect &&
|
||||
(
|
||||
cd dst &&
|
||||
test_must_fail git push --force-with-lease=master:master^ origin :master
|
||||
) &&
|
||||
git ls-remote src refs/heads/master >actual &&
|
||||
test_cmp expect actual
|
||||
'
|
||||
|
||||
test_expect_success 'push to delete (protected, forced)' '
|
||||
setup_srcdst_basic &&
|
||||
(
|
||||
cd dst &&
|
||||
git push --force --force-with-lease=master:master^ origin :master
|
||||
) &&
|
||||
>expect &&
|
||||
git ls-remote src refs/heads/master >actual &&
|
||||
test_cmp expect actual
|
||||
'
|
||||
|
||||
test_expect_success 'push to delete (allowed)' '
|
||||
setup_srcdst_basic &&
|
||||
(
|
||||
cd dst &&
|
||||
git push --force-with-lease=master origin :master
|
||||
) &&
|
||||
>expect &&
|
||||
git ls-remote src refs/heads/master >actual &&
|
||||
test_cmp expect actual
|
||||
'
|
||||
|
||||
test_expect_success 'cover everything with default force-with-lease (protected)' '
|
||||
setup_srcdst_basic &&
|
||||
(
|
||||
cd src &&
|
||||
git branch naster master^
|
||||
)
|
||||
git ls-remote src refs/heads/\* >expect &&
|
||||
(
|
||||
cd dst &&
|
||||
test_must_fail git push --force-with-lease origin master master:naster
|
||||
) &&
|
||||
git ls-remote src refs/heads/\* >actual &&
|
||||
test_cmp expect actual
|
||||
'
|
||||
|
||||
test_expect_success 'cover everything with default force-with-lease (allowed)' '
|
||||
setup_srcdst_basic &&
|
||||
(
|
||||
cd src &&
|
||||
git branch naster master^
|
||||
)
|
||||
(
|
||||
cd dst &&
|
||||
git fetch &&
|
||||
git push --force-with-lease origin master master:naster
|
||||
) &&
|
||||
git ls-remote dst refs/heads/master |
|
||||
sed -e "s/master/naster/" >expect &&
|
||||
git ls-remote src refs/heads/naster >actual &&
|
||||
test_cmp expect actual
|
||||
'
|
||||
|
||||
test_done
|
|
@ -153,7 +153,7 @@ test_expect_success 'used receive-pack service' '
|
|||
'
|
||||
|
||||
test_http_push_nonff "$HTTPD_DOCUMENT_ROOT_PATH"/test_repo.git \
|
||||
"$ROOT_PATH"/test_repo_clone master
|
||||
"$ROOT_PATH"/test_repo_clone master success
|
||||
|
||||
test_expect_success 'push fails for non-fast-forward refs unmatched by remote helper' '
|
||||
# create a dissimilarly-named remote ref so that git is unable to match the
|
||||
|
|
|
@ -693,6 +693,11 @@ static int push_update_ref_status(struct strbuf *buf,
|
|||
free(msg);
|
||||
msg = NULL;
|
||||
}
|
||||
else if (!strcmp(msg, "stale info")) {
|
||||
status = REF_STATUS_REJECT_STALE;
|
||||
free(msg);
|
||||
msg = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
if (*ref)
|
||||
|
@ -747,13 +752,15 @@ static void push_update_refs_status(struct helper_data *data,
|
|||
}
|
||||
|
||||
static int push_refs_with_push(struct transport *transport,
|
||||
struct ref *remote_refs, int flags)
|
||||
struct ref *remote_refs, int flags)
|
||||
{
|
||||
int force_all = flags & TRANSPORT_PUSH_FORCE;
|
||||
int mirror = flags & TRANSPORT_PUSH_MIRROR;
|
||||
struct helper_data *data = transport->data;
|
||||
struct strbuf buf = STRBUF_INIT;
|
||||
struct ref *ref;
|
||||
struct string_list cas_options = STRING_LIST_INIT_DUP;
|
||||
struct string_list_item *cas_option;
|
||||
|
||||
get_helper(transport);
|
||||
if (!data->push)
|
||||
|
@ -766,6 +773,7 @@ static int push_refs_with_push(struct transport *transport,
|
|||
/* Check for statuses set by set_ref_status_for_push() */
|
||||
switch (ref->status) {
|
||||
case REF_STATUS_REJECT_NONFASTFORWARD:
|
||||
case REF_STATUS_REJECT_STALE:
|
||||
case REF_STATUS_REJECT_ALREADY_EXISTS:
|
||||
case REF_STATUS_UPTODATE:
|
||||
continue;
|
||||
|
@ -788,11 +796,29 @@ static int push_refs_with_push(struct transport *transport,
|
|||
strbuf_addch(&buf, ':');
|
||||
strbuf_addstr(&buf, ref->name);
|
||||
strbuf_addch(&buf, '\n');
|
||||
|
||||
/*
|
||||
* The "--force-with-lease" options without explicit
|
||||
* values to expect have already been expanded into
|
||||
* the ref->old_sha1_expect[] field; we can ignore
|
||||
* transport->smart_options->cas altogether and instead
|
||||
* can enumerate them from the refs.
|
||||
*/
|
||||
if (ref->expect_old_sha1) {
|
||||
struct strbuf cas = STRBUF_INIT;
|
||||
strbuf_addf(&cas, "%s:%s",
|
||||
ref->name, sha1_to_hex(ref->old_sha1_expect));
|
||||
string_list_append(&cas_options, strbuf_detach(&cas, NULL));
|
||||
}
|
||||
}
|
||||
if (buf.len == 0)
|
||||
if (buf.len == 0) {
|
||||
string_list_clear(&cas_options, 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
standard_options(transport);
|
||||
for_each_string_list_item(cas_option, &cas_options)
|
||||
set_helper_option(transport, "cas", cas_option->string);
|
||||
|
||||
if (flags & TRANSPORT_PUSH_DRY_RUN) {
|
||||
if (set_helper_option(transport, "dry-run", "true") != 0)
|
||||
|
|
13
transport.c
13
transport.c
|
@ -3,6 +3,8 @@
|
|||
#include "run-command.h"
|
||||
#include "pkt-line.h"
|
||||
#include "fetch-pack.h"
|
||||
#include "remote.h"
|
||||
#include "connect.h"
|
||||
#include "send-pack.h"
|
||||
#include "walker.h"
|
||||
#include "bundle.h"
|
||||
|
@ -707,6 +709,10 @@ static int print_one_push_status(struct ref *ref, const char *dest, int count, i
|
|||
print_ref_status('!', "[rejected]", ref, ref->peer_ref,
|
||||
"needs force", porcelain);
|
||||
break;
|
||||
case REF_STATUS_REJECT_STALE:
|
||||
print_ref_status('!', "[rejected]", ref, ref->peer_ref,
|
||||
"stale info", porcelain);
|
||||
break;
|
||||
case REF_STATUS_REMOTE_REJECT:
|
||||
print_ref_status('!', "[remote rejected]", ref,
|
||||
ref->deletion ? NULL : ref->peer_ref,
|
||||
|
@ -1076,6 +1082,7 @@ static int run_pre_push_hook(struct transport *transport,
|
|||
for (r = remote_refs; r; r = r->next) {
|
||||
if (!r->peer_ref) continue;
|
||||
if (r->status == REF_STATUS_REJECT_NONFASTFORWARD) continue;
|
||||
if (r->status == REF_STATUS_REJECT_STALE) continue;
|
||||
if (r->status == REF_STATUS_UPTODATE) continue;
|
||||
|
||||
strbuf_reset(&buf);
|
||||
|
@ -1140,6 +1147,12 @@ int transport_push(struct transport *transport,
|
|||
return -1;
|
||||
}
|
||||
|
||||
if (transport->smart_options &&
|
||||
transport->smart_options->cas &&
|
||||
!is_empty_cas(transport->smart_options->cas))
|
||||
apply_push_cas(transport->smart_options->cas,
|
||||
transport->remote, remote_refs);
|
||||
|
||||
set_ref_status_for_push(remote_refs,
|
||||
flags & TRANSPORT_PUSH_MIRROR,
|
||||
flags & TRANSPORT_PUSH_FORCE);
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
#define TRANSPORT_H
|
||||
|
||||
#include "cache.h"
|
||||
#include "run-command.h"
|
||||
#include "remote.h"
|
||||
|
||||
struct git_transport_options {
|
||||
|
@ -13,6 +14,7 @@ struct git_transport_options {
|
|||
int depth;
|
||||
const char *uploadpack;
|
||||
const char *receivepack;
|
||||
struct push_cas_option *cas;
|
||||
};
|
||||
|
||||
struct transport {
|
||||
|
@ -126,6 +128,9 @@ struct transport *transport_get(struct remote *, const char *);
|
|||
/* Transfer the data as a thin pack if not null */
|
||||
#define TRANS_OPT_THIN "thin"
|
||||
|
||||
/* Check the current value of the remote ref */
|
||||
#define TRANS_OPT_CAS "cas"
|
||||
|
||||
/* Keep the pack that was transferred if not null */
|
||||
#define TRANS_OPT_KEEP "keep"
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
#include "revision.h"
|
||||
#include "list-objects.h"
|
||||
#include "run-command.h"
|
||||
#include "connect.h"
|
||||
#include "sigchain.h"
|
||||
#include "version.h"
|
||||
#include "string-list.h"
|
||||
|
|
Загрузка…
Ссылка в новой задаче