Skip to content

Commit af83aef

Browse files
authored
Merge pull request #37 from increments/check-local-files-are-up-to-date
Check if local files are up to date
2 parents ac60f62 + c800d90 commit af83aef

File tree

13 files changed

+237
-14
lines changed

13 files changed

+237
-14
lines changed

src/client/components/ArticleInfo.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ interface Props {
1818
errorMessages: string[];
1919
qiitaItemUrl: string | null;
2020
slide: boolean;
21+
isOlderThanRemote: boolean;
2122
}
2223

2324
export const ArticleInfo = ({
@@ -28,6 +29,7 @@ export const ArticleInfo = ({
2829
errorMessages,
2930
qiitaItemUrl,
3031
slide,
32+
isOlderThanRemote,
3133
}: Props) => {
3234
const [isOpen, setIsOpen] = useState(
3335
localStorage.getItem("openInfoState") === "true" ? true : false
@@ -88,6 +90,18 @@ export const ArticleInfo = ({
8890
</InfoItem>
8991
<InfoItem title="スライドモード">{slide ? "ON" : "OFF"}</InfoItem>
9092
</details>
93+
{isOlderThanRemote && (
94+
<div css={errorContentsStyle}>
95+
<p css={errorStyle}>
96+
<MaterialSymbol fill={true} css={exclamationIconStyle}>
97+
error
98+
</MaterialSymbol>
99+
{
100+
"この記事ファイルの内容は、Qiita上の記事より古い可能性があります。"
101+
}
102+
</p>
103+
</div>
104+
)}
91105
{errorMessages.length > 0 && (
92106
<div css={errorContentsStyle}>
93107
{errorMessages.map((errorMessage, index) => (

src/client/components/Header.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { Snackbar, Message as SnackbarMessage } from "./Snackbar";
1313
interface Props {
1414
id?: string;
1515
isItemPublishable: boolean;
16+
isOlderThanRemote: boolean;
1617
itemPath: string;
1718
basename: string | null;
1819
handleMobileOpen: () => void;
@@ -21,6 +22,7 @@ interface Props {
2122
export const Header = ({
2223
id,
2324
isItemPublishable,
25+
isOlderThanRemote,
2426
itemPath,
2527
basename,
2628
handleMobileOpen,
@@ -32,6 +34,16 @@ export const Header = ({
3234
const mobileSize = currentWidth <= breakpoint.S;
3335

3436
const handlePublish = () => {
37+
if (isOlderThanRemote) {
38+
if (
39+
!window.confirm(
40+
"この記事はQiita上の記事より古い可能性があります。上書きしますか?"
41+
)
42+
) {
43+
return;
44+
}
45+
}
46+
3547
const params = {
3648
method: "POST",
3749
headers: {

src/client/pages/items/show.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ export const ItemsShow = () => {
8484
isItemPublishable={
8585
item.modified && item.error_messages.length === 0
8686
}
87+
isOlderThanRemote={item.is_older_than_remote}
8788
itemPath={item.item_path}
8889
id={id}
8990
basename={basename}
@@ -98,6 +99,7 @@ export const ItemsShow = () => {
9899
errorMessages={item.error_messages}
99100
qiitaItemUrl={item.qiita_item_url}
100101
slide={item.slide}
102+
isOlderThanRemote={item.is_older_than_remote}
101103
/>
102104
<div css={articleWrapStyle}>
103105
<Article

src/commands/publish.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ export const publish = async (argv: string[]) => {
1212
const args = arg(
1313
{
1414
"--all": Boolean,
15+
"--force": Boolean,
16+
"-f": "--force",
1517
},
1618
{ argv }
1719
);
@@ -40,6 +42,7 @@ export const publish = async (argv: string[]) => {
4042
}
4143

4244
// Validate
45+
const enableForcePublish = args["--force"];
4346
const invalidItemMessages = targetItems.reduce((acc, item) => {
4447
const frontmatterErrors = checkFrontmatterType(item);
4548
if (frontmatterErrors.length > 0)
@@ -49,6 +52,16 @@ export const publish = async (argv: string[]) => {
4952
if (validationErrors.length > 0)
5053
return [...acc, { name: item.name, errors: validationErrors }];
5154

55+
if (!enableForcePublish && item.isOlderThanRemote) {
56+
return [
57+
...acc,
58+
{
59+
name: item.name,
60+
errors: ["内容がQiita上の記事より古い可能性があります"],
61+
},
62+
];
63+
}
64+
5265
return acc;
5366
}, [] as { name: string; errors: string[] }[]);
5467
if (invalidItemMessages.length > 0) {

src/commands/pull.test.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { getFileSystemRepo } from "../lib/get-file-system-repo";
2+
import { getQiitaApiInstance } from "../lib/get-qiita-api-instance";
3+
import { syncArticlesFromQiita } from "../lib/sync-articles-from-qiita";
4+
import { pull } from "./pull";
5+
6+
jest.mock("../lib/get-qiita-api-instance");
7+
jest.mock("../lib/get-file-system-repo");
8+
jest.mock("../lib/sync-articles-from-qiita");
9+
const mockGetQiitaApiInstance = jest.mocked(getQiitaApiInstance);
10+
const mockGetFileSystemRepo = jest.mocked(getFileSystemRepo);
11+
const mockSyncArticlesFromQiita = jest.mocked(syncArticlesFromQiita);
12+
13+
describe("pull", () => {
14+
const qiitaApi = {} as ReturnType<typeof getQiitaApiInstance>;
15+
const fileSystemRepo = {} as ReturnType<typeof getFileSystemRepo>;
16+
17+
beforeEach(() => {
18+
mockGetQiitaApiInstance.mockReset();
19+
mockGetFileSystemRepo.mockReset();
20+
mockSyncArticlesFromQiita.mockReset();
21+
22+
mockGetQiitaApiInstance.mockReturnValue(qiitaApi);
23+
mockGetFileSystemRepo.mockReturnValue(fileSystemRepo);
24+
mockSyncArticlesFromQiita.mockImplementation();
25+
jest.spyOn(console, "log").mockImplementation();
26+
});
27+
28+
it("pulls articles", async () => {
29+
await pull([]);
30+
31+
expect(mockSyncArticlesFromQiita).toBeCalledWith({
32+
fileSystemRepo,
33+
qiitaApi,
34+
forceUpdate: undefined,
35+
});
36+
expect(mockSyncArticlesFromQiita).toBeCalledTimes(1);
37+
});
38+
39+
describe('with "--force" option', () => {
40+
it("pulls articles with forceUpdate", async () => {
41+
await pull(["--force"]);
42+
43+
expect(mockSyncArticlesFromQiita).toBeCalledWith({
44+
fileSystemRepo,
45+
qiitaApi,
46+
forceUpdate: true,
47+
});
48+
expect(mockSyncArticlesFromQiita).toBeCalledTimes(1);
49+
});
50+
51+
it("pulls articles with forceUpdate", async () => {
52+
await pull(["-f"]);
53+
54+
expect(mockSyncArticlesFromQiita).toBeCalledWith({
55+
fileSystemRepo,
56+
qiitaApi,
57+
forceUpdate: true,
58+
});
59+
});
60+
});
61+
});

src/commands/pull.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ export const pull = async (argv: string[]) => {
1414

1515
const qiitaApi = await getQiitaApiInstance();
1616
const fileSystemRepo = await getFileSystemRepo();
17-
const isLocalUpdate = args["--force"];
17+
const forceUpdate = args["--force"];
1818

19-
await syncArticlesFromQiita({ fileSystemRepo, qiitaApi, isLocalUpdate });
19+
await syncArticlesFromQiita({ fileSystemRepo, qiitaApi, forceUpdate });
2020
console.log("Sync local articles from Qiita");
2121
console.log("Successful!");
2222
};

src/lib/entities/qiita-item.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export class QiitaItem {
88
public readonly rawBody: string;
99
public readonly name: string;
1010
public readonly modified: boolean;
11+
public readonly isOlderThanRemote: boolean;
1112
public readonly itemsShowPath: string;
1213
public readonly published: boolean;
1314
public readonly itemPath: string;
@@ -23,6 +24,7 @@ export class QiitaItem {
2324
rawBody,
2425
name,
2526
modified,
27+
isOlderThanRemote,
2628
itemsShowPath,
2729
published,
2830
itemPath,
@@ -37,6 +39,7 @@ export class QiitaItem {
3739
rawBody: string;
3840
name: string;
3941
modified: boolean;
42+
isOlderThanRemote: boolean;
4043
itemsShowPath: string;
4144
published: boolean;
4245
itemPath: string;
@@ -51,6 +54,7 @@ export class QiitaItem {
5154
this.rawBody = rawBody;
5255
this.name = name;
5356
this.modified = modified;
57+
this.isOlderThanRemote = isOlderThanRemote;
5458
this.itemsShowPath = itemsShowPath;
5559
this.published = published;
5660
this.itemPath = itemPath;

src/lib/file-system-repo.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -315,9 +315,9 @@ updated
315315
`;
316316
const subject = (
317317
beforeSync: boolean = false,
318-
isLocalUpdate: boolean = false
318+
forceUpdate: boolean = false
319319
) => {
320-
return instance.saveItem(item, beforeSync, isLocalUpdate);
320+
return instance.saveItem(item, beforeSync, forceUpdate);
321321
};
322322

323323
describe("when local article does not exist", () => {
@@ -378,7 +378,7 @@ updated
378378
});
379379
});
380380

381-
describe("when isLocalUpdate is true", () => {
381+
describe("when forceUpdate is true", () => {
382382
it("saves item local and remote", () => {
383383
const mockFs = fs as jest.Mocked<typeof fs>;
384384
mockFs.readdir.mockResolvedValueOnce([localFilename] as any[]);

src/lib/file-system-repo.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,14 @@ class FileContent {
135135
);
136136
}
137137

138+
isOlderThan(otherFileContent: FileContent | null): boolean {
139+
if (!otherFileContent) return false;
140+
const updatedAt = new Date(this.updatedAt);
141+
const otherUpdatedAt = new Date(otherFileContent.updatedAt);
142+
143+
return updatedAt < otherUpdatedAt;
144+
}
145+
138146
clone({ id }: { id: string }): FileContent {
139147
return new FileContent({
140148
title: this.title,
@@ -264,7 +272,7 @@ export class FileSystemRepo {
264272
private async syncItem(
265273
item: Item,
266274
beforeSync: boolean = false,
267-
isLocalUpdate: boolean = false
275+
forceUpdate: boolean = false
268276
) {
269277
const fileContent = FileContent.fromItem(item);
270278

@@ -279,17 +287,17 @@ export class FileSystemRepo {
279287
true
280288
);
281289

282-
if (data === null || remoteFileContent?.equals(data) || isLocalUpdate) {
290+
if (data === null || remoteFileContent?.equals(data) || forceUpdate) {
283291
await this.setItemData(fileContent, true);
284292
await this.setItemData(fileContent, false, basename);
285293
} else {
286294
await this.setItemData(fileContent, true);
287295
}
288296
}
289297

290-
async saveItems(items: Item[], isLocalUpdate: boolean = false) {
298+
async saveItems(items: Item[], forceUpdate: boolean = false) {
291299
const promises = items.map(async (item) => {
292-
await this.syncItem(item, false, isLocalUpdate);
300+
await this.syncItem(item, false, forceUpdate);
293301
});
294302

295303
await Promise.all(promises);
@@ -298,9 +306,9 @@ export class FileSystemRepo {
298306
async saveItem(
299307
item: Item,
300308
beforeSync: boolean = false,
301-
isLocalUpdate: boolean = false
309+
forceUpdate: boolean = false
302310
) {
303-
await this.syncItem(item, beforeSync, isLocalUpdate);
311+
await this.syncItem(item, beforeSync, forceUpdate);
304312
}
305313

306314
async loadItems(): Promise<QiitaItem[]> {
@@ -353,6 +361,7 @@ export class FileSystemRepo {
353361
slide: localFileContent.slide,
354362
name: basename,
355363
modified: !localFileContent.equals(remoteFileContent),
364+
isOlderThanRemote: localFileContent.isOlderThan(remoteFileContent),
356365
itemsShowPath: this.generateItemsShowPath(localFileContent.id, basename),
357366
published: remoteFileContent !== null,
358367
itemPath,
@@ -388,6 +397,7 @@ export class FileSystemRepo {
388397
slide: localFileContent.slide,
389398
name: basename,
390399
modified: !localFileContent.equals(remoteFileContent),
400+
isOlderThanRemote: localFileContent.isOlderThan(remoteFileContent),
391401
itemsShowPath: this.generateItemsShowPath(localFileContent.id, basename),
392402
published: remoteFileContent !== null,
393403
itemPath,

0 commit comments

Comments
 (0)