5.6 KB184 lines
Blame
1/**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8import type {RecordOf} from 'immutable';
9import type {Author, Hash, RepoPath} from 'shared/types/common';
10import type {FileFlag} from 'shared/types/stack';
11
12import {Map as ImMap, Set as ImSet, List, Record, is} from 'immutable';
13
14/** Check if a path at the given commit is a rename. */
15export function isRename(commit: CommitState, path: RepoPath): boolean {
16 const files = commit.files;
17 const copyFromPath = files.get(path)?.copyFrom;
18 if (copyFromPath == null) {
19 return false;
20 }
21 return isAbsent(files.get(copyFromPath));
22}
23
24/** Test if a file is absent. */
25export function isAbsent(file: FileState | FileMetadata | undefined): boolean {
26 if (file == null) {
27 return true;
28 }
29 return file.flags === ABSENT_FLAG;
30}
31
32/** Test if a file has utf-8 content. */
33export function isUtf8(file: FileState): boolean {
34 return typeof file.data === 'string' || file.data instanceof FileIdx;
35}
36
37/** Test if 2 files have the same content, ignoring "copyFrom". */
38export function isContentSame(file1: FileState, file2: FileState): boolean {
39 return is(file1.data, file2.data) && (file1.flags ?? '') === (file2.flags ?? '');
40}
41
42/** Extract metadata */
43export function toMetadata(file: FileState): FileMetadata {
44 return FileMetadata({copyFrom: file.copyFrom, flags: file.flags});
45}
46
47type DateTupleProps = {
48 /** UTC Unix timestamp in seconds. */
49 unix: number;
50 /** Timezone offset in minutes. */
51 tz: number;
52};
53
54export const DateTuple = Record<DateTupleProps>({unix: 0, tz: 0});
55export type DateTuple = RecordOf<DateTupleProps>;
56
57/** Mutable commit state. */
58export type CommitStateProps = {
59 rev: CommitRev;
60 /** Original hashes. Used for "predecessor" information. */
61 originalNodes: ImSet<Hash>;
62 /**
63 * Unique identifier within the stack. Useful for React animation.
64 *
65 * Note this should not be a random string, since we expect the CommitState[]
66 * state to be purely derived from the initial ExportStack. It makes it easier
67 * to check what commits are actually modified by just comparing CommitStates.
68 * The "skip unchanged commits" logic is used by `calculateImportStack()`.
69 *
70 * We use commit hashes initially. When there is a split or add a new commit,
71 * we assign new keys in a predicable (non-random) way. This property is
72 * never empty, unlike `originalNodes`.
73 */
74 key: string;
75 author: Author;
76 date: DateTuple;
77 /** Commit message. */
78 text: string;
79 /**
80 * - hash: commit hash is immutable; this commit and ancestors
81 * cannot be edited in any way.
82 * - content: file contents are immutable; commit hash can change
83 * if ancestors are changed.
84 * - diff: file changes (diff) are immutable; file contents or
85 * commit hash can change if ancestors are changed.
86 * - none: nothing is immutable; this commit can be edited.
87 */
88 immutableKind: 'hash' | 'content' | 'diff' | 'none';
89 /** Parent commits. */
90 parents: List<CommitRev>;
91 /** Changed files. */
92 files: ImMap<RepoPath, FileState>;
93};
94
95export const CommitState = Record<CommitStateProps>({
96 rev: 0 as CommitRev,
97 originalNodes: ImSet(),
98 key: '',
99 author: '',
100 date: DateTuple(),
101 text: '',
102 immutableKind: 'none',
103 parents: List(),
104 files: ImMap(),
105});
106export type CommitState = RecordOf<CommitStateProps>;
107
108/**
109 * Similar to `ExportFile` but `data` can be lazy by redirecting to a rev in a file stack.
110 * Besides, supports "absent" state.
111 */
112type FileStateProps = {
113 data: string | Base85 | FileIdx | DataRef;
114} & FileMetadataProps;
115
116/**
117 * File metadata properties without file content.
118 */
119type FileMetadataProps = {
120 /** If present, this file is copied (or renamed) from another file. */
121 copyFrom?: RepoPath;
122 /**
123 * If present, whether this file is special (symlink, submodule, deleted,
124 * executable).
125 */
126 flags?: FileFlag;
127};
128
129type Base85Props = {dataBase85: string};
130export const Base85 = Record<Base85Props>({dataBase85: ''});
131export type Base85 = RecordOf<Base85Props>;
132
133type DataRefProps = {node: Hash; path: RepoPath};
134export const DataRef = Record<DataRefProps>({node: '', path: ''});
135export type DataRef = RecordOf<DataRefProps>;
136
137export const FileState = Record<FileStateProps>({data: '', copyFrom: undefined, flags: ''});
138export type FileState = RecordOf<FileStateProps>;
139
140export const FileMetadata = Record<FileMetadataProps>({copyFrom: undefined, flags: ''});
141export type FileMetadata = RecordOf<FileMetadataProps>;
142
143export type FileStackIndex = number;
144
145type FileIdxProps = {
146 fileIdx: FileStackIndex;
147 fileRev: FileRev;
148};
149
150type CommitIdxProps = {
151 rev: CommitRev;
152 path: RepoPath;
153};
154
155export const FileIdx = Record<FileIdxProps>({fileIdx: 0, fileRev: 0 as FileRev});
156export type FileIdx = RecordOf<FileIdxProps>;
157
158export const CommitIdx = Record<CommitIdxProps>({rev: -1 as CommitRev, path: ''});
159export type CommitIdx = RecordOf<CommitIdxProps>;
160
161export const ABSENT_FLAG = 'a';
162
163/**
164 * Represents an absent (or deleted) file.
165 *
166 * Helps simplify `null` handling logic. Since `data` is a regular
167 * string, an absent file can be compared (data-wise) with its
168 * adjacent versions and edited. This makes it easier to, for example,
169 * split a newly added file.
170 */
171export const ABSENT_FILE = FileState({
172 data: '',
173 flags: ABSENT_FLAG,
174});
175
176/** A revision number used in the `FileStackState`. Identifies a version of a multi-version file. */
177export type FileRev = number & {__brand: 'FileStackRev'};
178
179/** A revision number used in the `CommitStackState`. Identifies a commit in the stack. */
180export type CommitRev = number & {__branded: 'CommitRev'};
181
182// Re-export
183export type {FileFlag};
184