Skip to main content

git_policy_cli/
command_push.rs

1//! What:
2//!  The complete `git push` option table of Git 2.56.0 and the facts read from it.
3//! Why:
4//!  The atomic-push transform must know whether the caller already chose atomicity,
5//!      and the manual-push gate whether the push is a dry run;
6//!  both in every spelling Git
7//!      accepts (`--at`,
8//!  `--no-at`,
9//!  `-nf`),
10//!  which a list of exact tokens misses.
11//!
12//! In TS you'd write (pseudocode):
13//! ```ts
14//! // parsePushRegion(postSubcommandArgs): PushRegion  (throws OptionError)
15//! ```
16
17/// What:
18///  Bring the tokenizer,
19///  its table builder and its questions into this file.
20/// Why:
21///   This module only declares Git's table and interprets what the tokenizer found.
22///
23/// In TS you'd write (pseudocode):
24/// ```ts
25/// import { parseOptions, row } from './command_options.ts';
26/// ```
27use super::command_options::{
28    Arity, DEFAULT_MODE, OptionError, OptionSpec, ParsedOptions, UNREAD, WrapperOccurrence,
29    parse_options, row,
30};
31use super::command_options_query::{is_enabled, is_stated, positional_tokens};
32/// `OsString` is owned operating-system text of raw bytes (sibling `String` must be UTF-8).
33use std::ffi::OsString;
34
35/// `--atomic`:
36///  request an atomic transaction on the remote side.
37pub const ATOMIC: u16 = 1;
38/// `-n`,
39///  `--dry-run`:
40///  do everything except send the updates.
41pub const DRY_RUN: u16 = 2;
42/// `-f`,
43///  `--force`:
44///  update a ref that is not a descendant.
45pub const FORCE: u16 = 4;
46/// `--force-with-lease`:
47///  force when the remote ref is still what the caller expected.
48pub const FORCE_WITH_LEASE: u16 = 8;
49/// `--force-if-includes`:
50///  a safety check that only means something with a forced push.
51pub const FORCE_IF_INCLUDES: u16 = 16;
52/// `--mirror`:
53///  make the remote's refs equal the local ones.
54pub const MIRROR: u16 = 32;
55/// `-d`,
56///  `--delete`:
57///  remove remote refs.
58pub const DELETE: u16 = 64;
59/// `--all` and its alias `--branches`:
60///  every local branch.
61pub const ALL: u16 = 128;
62/// `--tags`:
63///  every local tag.
64pub const TAGS: u16 = 256;
65/// `--prune`:
66///  remove remote refs that have no local counterpart.
67pub const PRUNE: u16 = 512;
68/// `--follow-tags`:
69///  add the annotated tags that point into the pushed history.
70pub const FOLLOW_TAGS: u16 = 1024;
71/// `--repo`:
72///  the destination is an option and not the first positional argument.
73pub const REPO: u16 = 2048;
74/// `--porcelain`:
75///  machine-readable output that a script parses.
76pub const PORCELAIN: u16 = 4096;
77/// `-u`,
78///  `--set-upstream`:
79///  record the pushed branch as the local branch's upstream.
80pub const SET_UPSTREAM: u16 = 8192;
81
82/// What:
83///  Rows of `options[]` in `cmd_push` (builtin/push.c:707-743),
84///  in source order.
85///       `&[OptionSpec]` is a borrowed table baked into the program.
86/// Why:
87///   `--branches` is an `OPT_ALIAS` of `--all`;
88///  the two names share no prefix,
89///  so the
90///       alias needs no special ambiguity handling and is a row of its own.
91///
92/// In TS you'd write (pseudocode):
93/// ```ts
94/// export const PUSH_TABLE = [row(UNREAD, 'v', 'verbose', 'none', true), /* ... */] as const;
95/// ```
96pub const PUSH_TABLE: &[OptionSpec] = &[
97    row(UNREAD, Some(b'v'), Some("verbose"), Arity::None, true),
98    row(UNREAD, Some(b'q'), Some("quiet"), Arity::None, true),
99    row(REPO, None, Some("repo"), Arity::Required, true),
100    row(ALL, None, Some("all"), Arity::None, true),
101    row(ALL, None, Some("branches"), Arity::None, true),
102    row(MIRROR, None, Some("mirror"), Arity::None, true),
103    row(DELETE, Some(b'd'), Some("delete"), Arity::None, true),
104    row(TAGS, None, Some("tags"), Arity::None, true),
105    row(DRY_RUN, Some(b'n'), Some("dry-run"), Arity::None, true),
106    row(PORCELAIN, None, Some("porcelain"), Arity::None, true),
107    row(FORCE, Some(b'f'), Some("force"), Arity::None, true),
108    // `PARSE_OPT_OPTARG`: only `--force-with-lease=<refname>:<expect>` carries a value.
109    row(
110        FORCE_WITH_LEASE,
111        None,
112        Some("force-with-lease"),
113        Arity::Optional,
114        true,
115    ),
116    row(FORCE_IF_INCLUDES, None, Some("force-if-includes"), Arity::None, true),
117    row(
118        UNREAD,
119        None,
120        Some("recurse-submodules"),
121        Arity::Required,
122        true,
123    ),
124    row(UNREAD, None, Some("thin"), Arity::None, true),
125    row(UNREAD, None, Some("receive-pack"), Arity::Required, true),
126    row(UNREAD, None, Some("exec"), Arity::Required, true),
127    row(SET_UPSTREAM, Some(b'u'), Some("set-upstream"), Arity::None, true),
128    row(UNREAD, None, Some("progress"), Arity::None, true),
129    row(PRUNE, None, Some("prune"), Arity::None, true),
130    row(UNREAD, None, Some("no-verify"), Arity::None, true),
131    row(FOLLOW_TAGS, None, Some("follow-tags"), Arity::None, true),
132    // `PARSE_OPT_OPTARG`: only `--signed=<mode>` carries a value.
133    row(UNREAD, None, Some("signed"), Arity::Optional, true),
134    row(ATOMIC, None, Some("atomic"), Arity::None, true),
135    row(
136        UNREAD,
137        Some(b'o'),
138        Some("push-option"),
139        Arity::Required,
140        true,
141    ),
142    // `OPT_IPVERSION` rows are `PARSE_OPT_NONEG` (parse-options.h:636-640).
143    row(UNREAD, Some(b'4'), Some("ipv4"), Arity::None, false),
144    row(UNREAD, Some(b'6'), Some("ipv6"), Arity::None, false),
145];
146
147/// What:
148///  Final-state facts of one `git push` region.
149///  `bool` is true or false;
150///       `Vec<WrapperOccurrence>` is an owned list of wrapper-flag positions.
151/// Why:
152///   `atomic_stated` is "the caller chose",
153///  in either direction;
154///  `dry_run` is the state
155///       after the last `-n`,
156///  `--dry-run` or `--no-dry-run`.
157///
158/// In TS you'd write (pseudocode):
159/// ```ts
160/// type PushRegion = { hasAtomicChoice: boolean; isDryRun: boolean };
161/// ```
162#[derive(Clone, Debug, Eq, PartialEq)]
163pub struct PushRegion {
164    /// `--atomic` or `--no-atomic` was written,
165    ///  in any accepted abbreviation.
166    pub atomic_stated: bool,
167    /// The push sends no updates.
168    pub dry_run: bool,
169    /// Wrapper-only flags in option position;
170    ///  `flag` indexes the caller's list.
171    pub wrapper: Vec<WrapperOccurrence>,
172}
173
174/// What:
175///  Parse the region after `push`.
176///  `Result<A, B>` is "either success `A` or failure `B`".
177/// Why:
178///   A region Git itself would refuse yields the refusal,
179///  never a guessed fact set.
180///
181/// In TS you'd write (pseudocode):
182/// ```ts
183/// function parsePushRegion(region: string[], wrapperFlags: string[]): PushRegion; // throws
184/// ```
185pub fn parse_push_region(
186    region: &[OsString],
187    wrapper_flags: &[&[u8]],
188) -> Result<PushRegion, OptionError> {
189    // `?` returns Git's refusal to our caller, or unwraps the tokenized region.
190    let parsed: ParsedOptions = parse_options(region, PUSH_TABLE, DEFAULT_MODE, wrapper_flags)?;
191    // `Ok(x)` is the success case of `Result`.
192    return Ok(PushRegion {
193        atomic_stated: is_stated(&parsed, ATOMIC),
194        dry_run: is_enabled(&parsed, DRY_RUN),
195        wrapper: parsed.wrapper,
196    });
197}
198
199/// Options after which a rejected push is never integrated:
200///  each either rewrites or removes remote history, sends more than one kind of ref, changes what the caller
201///  reads back, or names its destination in a way the stacked push does not reproduce.
202const NOT_STACKABLE: &[u16] = &[
203    DRY_RUN,
204    FORCE,
205    FORCE_WITH_LEASE,
206    FORCE_IF_INCLUDES,
207    MIRROR,
208    DELETE,
209    ALL,
210    TAGS,
211    PRUNE,
212    FOLLOW_TAGS,
213    REPO,
214    PORCELAIN,
215    SET_UPSTREAM,
216];
217
218/// What:
219///  The option tokens of a push that may be integrated after a rejection, or nothing when it may not.
220///  `Option<Vec<OsString>>` is "the tokens, or no answer".
221/// Why:
222///   Only a plain update of branches keeps its meaning when the wrapper merges what landed on the remote and pushes the
223///  merge; the stacked push repeats these tokens and replaces the remote and refspec arguments,
224///  so the caller's `--no-verify`, `--push-option` and the like still apply.
225///   A region the table refuses, a forced refspec (`+src:dst`) and every option in `NOT_STACKABLE` give nothing.
226///
227/// In TS you'd write (pseudocode):
228/// ```ts
229/// function stackableOptionTokens(region: string[]): string[] | undefined;
230/// ```
231pub fn stackable_option_tokens(region: &[OsString]) -> Option<Vec<OsString>> {
232    let parsed: ParsedOptions = parse_options(region, PUSH_TABLE, DEFAULT_MODE, &[]).ok()?;
233    if NOT_STACKABLE.iter().any(|id| {
234        return is_enabled(&parsed, *id);
235    }) {
236        return None;
237    }
238    let positionals: Vec<usize> = positional_tokens(&parsed, region.len());
239    if positionals.iter().any(|index| {
240        return region[*index].as_encoded_bytes().first() == Some(&b'+');
241    }) {
242        return None;
243    }
244    return Some(
245        region
246            .iter()
247            .enumerate()
248            .filter(|(index, _)| {
249                return !positionals.contains(index);
250            })
251            .map(|(_, token)| {
252                return token.clone();
253            })
254            .collect(),
255    );
256}
257
258/// Facts in every accepted spelling,
259///  and real-Git controls of the table and its abbreviations.
260#[cfg(test)]
261#[path = "command_push_tests.rs"]
262mod tests;