پروژه پایانی، Git و GitHub
همه دوره را در یک برنامه جمع میکنیم: یک نرمافزار دسکتاپ با پایگاه داده واقعی، معماری تمیز و ساختار پوشهبندی درست. و بعد یاد میگیریم آن را زیر کنترل نسخه ببریم و منتشرش کنیم.
۱. معماری پروژه
library/
├── main.py starts the program, and nothing else
├── core/ *** imports no PySide6 at all ***
│ ├── paths.py where the files are (works frozen too)
│ ├── models.py Book, Member, Borrow
│ ├── database.py the engine, the session, session_scope()
│ └── services.py the RULES of the library
├── gui/ *** contains no SQL at all ***
│ ├── ui_loader.py loading a .ui file, once
│ ├── dialogs.py the three modal forms
│ └── main_window.py the four tabs
└── ui/ what Qt Designer produced
├── book_dialog.ui
├── member_dialog.ui
└── lend_dialog.ui
پوشه core/ هیچ چیز درباره پنجره نمیداند، و
پوشه gui/ هیچ چیز درباره SQL.
قانون «یک عضو نمیتواند بیش از پنج کتاب داشته باشد» یک بار نوشته
شده است، در core/services.py. دکمه صدایش میزند.
صفحه Flask فردا هم همان تابع را صدا میزند، و این دو هرگز نمیتوانند
با هم اختلاف پیدا کنند.
به همین دلیل است که سرویسها به جای نشان دادن پنجره پیام،
LibraryError پرتاب میکنند: core/ مسئله را
بیان میکند و gui/ تصمیم میگیرد چطور
نشانش دهد.
| زبانه | کارها |
|---|---|
| کتابها | جستوجوی زنده، افزودن، ویرایش، حذف (تا وقتی کتاب بیرون است رد میشود) |
| اعضا | جستوجو، افزودن، ویرایش، حذف (تا وقتی کتاب در دست دارند رد میشود) |
| امانتها | امانت دادن، پس گرفتن، تمدید؛ امانت دیرکرددار قرمز |
| آمار | جمعها و پنج عنوان پرامانتتر |
۲. مسیرها
'''Where the files of the application live.
Every path in the program goes through this module, so there is exactly one
place to change when the layout moves -- and exactly one place that knows
about PyInstaller (session 36).
resource_path() what we READ and ship with the program (the .ui files)
data_dir() what we WRITE, and that must survive a restart
'''
def project_root() -> Path:
'''The folder that holds `main.py` (or the .exe once frozen).'''
if is_frozen():
return Path(sys.executable).resolve().parent
# paths.py -> core -> library
return Path(__file__).resolve().parent.parent
def resource_path(relative: str) -> Path:
'''A file that was SHIPPED with the program (read-only).'''
if is_frozen() and hasattr(sys, '_MEIPASS'):
# --add-data puts the files here (session 36).
return Path(sys._MEIPASS) / relative
return project_root() / relative
def data_dir() -> Path:
'''Where the program WRITES. Created if it does not exist yet.'''
folder = project_root() / 'data'
folder.mkdir(parents=True, exist_ok=True)
return folder
def database_path() -> Path:
return data_dir() / 'library.db'
مسیرها: library/core/paths.py
این همان درس فصل سی و ششم است، ولی این بار از روز اول در معماری پروژه نشسته: یک ماژول که تنها جای آگاه از PyInstaller است. نتیجهاش این است که تبدیل برنامه به فایل اجرایی، هیچ جای دیگری از کد را دست نمیزند.
۳. مدلها
'''The tables of the library, as SQLAlchemy 2.0 models.
Member 1 --- n Borrow n --- 1 Book
A `Borrow` row is one loan. `return_date IS NULL` means the book is still out,
which is why we do not need a duplicated "available" flag on `Book`: the
answer is always one query away, and one fact stored in one place cannot
disagree with itself.
(Session 35 did keep such a flag, and its bug was exactly that: two places to
update, one transaction to get wrong.)
'''
class Book(Base):
__tablename__ = 'books'
id: Mapped[int] = mapped_column(primary_key=True)
title: Mapped[str] = mapped_column(String(160), nullable=False)
author: Mapped[str] = mapped_column(String(120), default='')
year: Mapped[int | None] = mapped_column(Integer, nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime,
server_default=func.now())
# cascade: deleting a book deletes its loan history with it.
borrows: Mapped[list['Borrow']] = relationship(
back_populates='book', cascade='all, delete-orphan')
@property
def is_out(self) -> bool:
'''True while one of the loans has no return date.'''
return any(borrow.return_date is None for borrow in self.borrows)
def __str__(self) -> str:
# __str__ must return a STRING. The original returned `self.member`,
# an object, which raised TypeError the first time it was printed.
return f'{self.title} ({self.author})' if self.author else self.title
مدل کتاب: library/core/models.py
class Borrow(Base):
__tablename__ = 'borrows'
id: Mapped[int] = mapped_column(primary_key=True)
# ondelete=CASCADE is the rule inside the DATABASE; the `cascade=` above
# is the rule inside SQLAlchemy. You want both, and SQLite only obeys the
# first one when `PRAGMA foreign_keys = ON` is set (see database.py).
book_id: Mapped[int] = mapped_column(
ForeignKey('books.id', ondelete='CASCADE'), nullable=False)
member_id: Mapped[int] = mapped_column(
ForeignKey('members.id', ondelete='CASCADE'), nullable=False)
# datetime.now, not datetime.utcnow: utcnow is deprecated since 3.12.
borrow_date: Mapped[datetime] = mapped_column(DateTime,
default=datetime.now)
due_date: Mapped[datetime] = mapped_column(DateTime, nullable=False)
return_date: Mapped[datetime | None] = mapped_column(DateTime,
nullable=True)
@property
def is_late(self) -> bool:
'''Out, and past the due date.'''
return self.is_open and datetime.now() > self.due_date
مدل امانت
شرح مثال
- مهمترین تصمیم طراحی: ستون
availableوجود ندارد. اینکه کتاب بیرون است یا نه، همیشه یک پرسوجو دورتر است. یک واقعیت که در یک جا ذخیره شده، نمیتواند با خودش اختلاف پیدا کند. - در فصل سی و پنجم چنین پرچمی داشتیم و باگش دقیقاً همین بود: دو جا برای بهروزرسانی، و یک تراکنش برای اشتباه کردن.
- حذف آبشاری در دو جا نوشته شده:
ondelete='CASCADE'قانون داخل پایگاه داده است وcascade='all, delete-orphan'قانون داخل SQLAlchemy. هر دو را میخواهید. @propertyهایی مثلis_outوis_lateوfull_nameمنطق را کنار داده میگذارند. این همان چیزی است که فصل بیست و دوم آموخت.__str__باید رشته برگرداند. نسخه اصلی این پروژه شیء برمیگرداند و اولین بار که چاپ میشدTypeErrorمیداد.
۴. پایگاه داده و تراکنش
ENGINE = create_engine(f'sqlite:///{database_path()}', echo=False,
future=True)
# expire_on_commit=False is important here. By default, `commit()` marks every
# loaded attribute as "must be read again", and reading one after the session
# is closed raises DetachedInstanceError. The GUI keeps the objects it showed
# (to know which row is selected), so we need their values to stay usable
# after the transaction ends.
SessionLocal = sessionmaker(bind=ENGINE, autoflush=False, autocommit=False,
expire_on_commit=False, future=True)
@event.listens_for(ENGINE, 'connect')
def _enable_foreign_keys(dbapi_connection, connection_record) -> None:
'''SQLite needs this on every connection, or ON DELETE does nothing.'''
cursor = dbapi_connection.cursor()
cursor.execute('PRAGMA foreign_keys=ON')
cursor.close()
@contextmanager
def session_scope() -> Iterator[Session]:
'''One transaction: commit on success, roll back on any exception.'''
session = SessionLocal()
try:
yield session
session.commit()
except Exception:
session.rollback()
raise # the caller still has to know
finally:
session.close()
موتور و تراکنش: library/core/database.py
شرح مثال
session_scope()یک مدیر زمینه است، ساختهشده با@contextmanagerفصل بیست و پنجم. با آن، هر عمل برنامه در یک بلوکwithجا میشود: موفق شد commit، شکست خورد rollback، در هر حال close.- بدون آن، یک استثنا در میانه کار، تراکنشی نیمهنوشته و نشستی که کسی نبسته باقی میگذارد.
raiseدرexceptفراموش نشود: تمیزکاری کردیم، ولی صداکننده هنوز باید بداند چه شده.expire_on_commit=Falseنکته ظریفی است که هر کس با SQLAlchemy و رابط گرافیکی کار کند یک بار گیرش میافتد: به طور پیشفرض بعد ازcommitخواندن صفتهای شیء بیرون از نشست،DetachedInstanceErrorمیدهد.
۵. لایه سرویس
'''The rules of the library.
Every operation the program can perform is a function here. The windows call
these functions and show what they return; they never write a query and they
never decide a rule.
Why that matters:
* the rule "a member may not have more than five books" is written ONCE, so
the button and the future web page cannot disagree;
* this file can be read, reviewed and tested without starting a window;
* an operation that must not be interrupted (lending: two rows to change) is a
single function with a single transaction.
'''
LOAN_DAYS = 14 # how long a book may be kept
MAX_OPEN_LOANS = 5 # how many books one member may hold at once
class LibraryError(Exception):
'''A rule of the library was broken. The message is shown to the user.'''
def list_books(session: Session, search: str = '') -> list[Book]:
'''Every book. `selectinload` fetches the loans in one extra query
instead of one per book (the "N+1 queries" problem).'''
query = select(Book).options(selectinload(Book.borrows)).order_by(Book.title)
if search:
pattern = f'%{search}%'
# The % is inside a PARAMETER: no SQL injection (session 30).
query = query.where(or_(Book.title.ilike(pattern),
Book.author.ilike(pattern),
Book.isbn.ilike(pattern)))
return list(session.scalars(query))
def delete_book(session: Session, book: Book) -> None:
if book.is_out:
raise LibraryError('This book is lent out. Take it back first.')
session.delete(book)
لایه سرویس: library/core/services.py
شرح مثال
- هر کاری که برنامه میتواند انجام دهد، اینجا یک تابع است. پنجرهها فقط صدایشان میزنند و نتیجه را نشان میدهند.
- این فایل را میشود بدون باز کردن هیچ پنجرهای خواند، بازبینی کرد و آزمود.
selectinloadمسئله N+1 فصل سی و دوم را حل میکند: به جای یک پرسوجو برای هر کتاب، یک پرسوجوی اضافه برای همه.- علامت درصد داخل پارامتر است، نه داخل رشته SQL. درس تزریق SQL فصل سیام هنوز پابرجاست.
- تصمیم «نمیشود» اینجا گرفته میشود، نه در دکمه. دکمه فقط
LibraryErrorرا میگیرد و پیامش را نشان میدهد.
۶. امانت دادن
این مهمترین تابع کل پروژه است، چون همه قوانین یک امانت را یکجا و در یک تراکنش بررسی میکند.
def lend_book(session: Session, book: Book, member: Member,
days: int = LOAN_DAYS) -> Borrow:
'''Lend one book to one member. Every rule of a loan is checked here.'''
# Ask the database, not the object: another window may have lent it
# a second ago.
already_out = session.scalar(
select(Borrow).where(Borrow.book_id == book.id,
Borrow.return_date.is_(None)))
if already_out is not None:
raise LibraryError(f'"{book.title}" is already lent out.')
open_count = session.scalar(
select(func.count(Borrow.id))
.where(Borrow.member_id == member.id, Borrow.return_date.is_(None)))
if open_count and open_count >= MAX_OPEN_LOANS:
raise LibraryError(f'{member.full_name} already has '
f'{MAX_OPEN_LOANS} books.')
now = datetime.now()
borrow = Borrow(book_id=book.id, member_id=member.id,
borrow_date=now, due_date=now + timedelta(days=days))
session.add(borrow)
session.flush()
return borrow
امانت دادن: library/core/services.py
شرح مثال
- چرا از پایگاه داده میپرسیم که کتاب بیرون است یا نه، در حالی که
book.is_outهمانجا در دسترس است؟ چون شیئی که در دست داریم عکسی از چند لحظه پیش است. پنجره دیگری ممکن است یک ثانیه قبل همان کتاب را امانت داده باشد. - هر دو قانون در یک تراکنش بررسی و اعمال میشوند. این همان درس اتمی بودن فصل سی و یکم است.
- عددهای قانون در بالای فایل ثابت تعریف شدهاند:
LOAN_DAYSوMAX_OPEN_LOANS. عدد جادویی وسط کد ننویسید. flush()شناسه را میسازد بدون اینکه تراکنش را ببندد؛ commit راsession_scopeدر انتها انجام میدهد.
۷. آمار
def statistics(session: Session) -> dict:
'''One dictionary with everything the statistics tab shows.'''
total_books = session.scalar(select(func.count(Book.id))) or 0
total_members = session.scalar(select(func.count(Member.id))) or 0
out = session.scalar(
select(func.count(Borrow.id)).where(Borrow.return_date.is_(None))) or 0
late = session.scalar(
select(func.count(Borrow.id))
.where(Borrow.return_date.is_(None),
Borrow.due_date < datetime.now())) or 0
# The five most borrowed titles: GROUP BY + ORDER BY (session 30).
popular = session.execute(
select(Book.title, func.count(Borrow.id).label('times'))
.join(Borrow, Borrow.book_id == Book.id)
.group_by(Book.id)
.order_by(func.count(Borrow.id).desc())
.limit(5)).all()
return {'books': total_books,
'members': total_members,
'on_loan': out,
'available': total_books - out,
'late': late,
'popular': [(title, times) for title, times in popular]}
آمار: library/core/services.py
یک تابع، یک دیکشنری. زبانه آمار فقط همین دیکشنری را میگیرد و
مینویسدش. اگر فردا بخواهید همین اعداد را در یک صفحه وب یا در یک
گزارش PDF نشان دهید، همین تابع کافی است.
GROUP BY و ORDER BY و JOIN هم
همان چیزهایی هستند که در فصل سیام با SQL خام نوشتید.
۸. لایه گرافیکی
'''Loading the Qt Designer files -- written once, used by every dialog.
The first version of this course repeated the same fifteen lines of
`QUiLoader` code at the top of nine files. It is one function.
'''
def load_ui(file_name: str) -> QWidget:
'''Load one of the files of `ui/` and return the widget it describes.'''
path = ui_path(file_name)
if not path.exists():
raise FileNotFoundError(
f'{path} is missing.\n'
'When the program is frozen, the ui/ folder must be shipped with '
'it: pyinstaller --add-data "ui;ui" ...')
widget = QUiLoader().load(str(path))
if widget is None:
raise RuntimeError(f'Qt could not read {path} (invalid XML?)')
return widget
def find(parent: QWidget, widget_type: type[WidgetType],
name: str) -> WidgetType:
'''findChild that complains at once instead of returning None.'''
widget = parent.findChild(widget_type, name)
if widget is None:
raise RuntimeError(f'No {widget_type.__name__} named {name!r} in the '
'ui file -- check the objectName in Qt Designer.')
return widget
بارگذار: library/gui/ui_loader.py
class BaseDialog(QDialog):
'''A QDialog whose content comes from a .ui file.'''
UI_FILE = ''
def __init__(self, parent: QWidget | None, title: str) -> None:
super().__init__(parent)
self.setWindowTitle(title)
self.ui = load_ui(self.UI_FILE)
...
self.buttons = find(self.ui, QDialogButtonBox, 'buttonBox')
# `accepted` is emitted by Save/OK, `rejected` by Cancel.
self.buttons.accepted.connect(self.on_accept)
self.buttons.rejected.connect(self.reject)
self.data: dict = {}
def on_accept(self) -> None:
'''Validate; call `self.accept()` only when everything is right.'''
errors = self.collect()
if errors:
QMessageBox.warning(self, 'Please check the form',
'\n'.join(f'- {message}' for message in errors))
return
self.accept()
def collect(self) -> list[str]:
'''Fill `self.data` and return the list of problems.'''
raise NotImplementedError
کلاس پایه دیالوگها: library/gui/dialogs.py
شرح مثال
- قرارداد همه دیالوگها یکی است:
dialog.exec()اگر کاربر ذخیره را زده باشد درست میدهد، و آن وقتdialog.dataیک دیکشنری از مقدارهای پیشتر اعتبارسنجیشده است. - دیالوگها شکل داده را بررسی میکنند: سال عدد است؟ عنوان خالی نیست؟ آنها قوانین کتابخانه را نمیدانند؛ آن قوانین در
core/services.pyزندگی میکنند. جدا نگه داشتن این دو، همان چیزی است که جلوی نوشته شدن یک قانون در دو جا را میگیرد. collect()یک متد انتزاعی است که هر دیالوگ خودش پرش میکند. الگوی قالب، از فصل بیست و سوم.
۹. پنجره اصلی
'''The main window: four tabs, three tables, and not one line of SQL.
The two habits that keep this file honest:
1. Every screen is rebuilt by a `refresh_*` method, and an action calls
`refresh_all()`. Trying to patch one row of a table by hand is how a
display starts disagreeing with the database.
2. The table is a DISPLAY. The real row is remembered in
`self.book_rows[table_row]`, never read back out of the widget. (That was
the bug of session 35: a Treeview handed back the word "yes" for a number.)
'''
def refresh_books(self) -> None:
search = self.book_search.text().strip()
self.books_table.setSortingEnabled(False) # sorting while filling
self.books_table.setRowCount(0) # scrambles the rows
self.book_rows.clear()
with session_scope() as session:
for row, book in enumerate(services.list_books(session, search)):
self.books_table.insertRow(row)
fill_row(self.books_table, row,
[book.id, book.title, book.author or '',
book.publisher or '', book.year or '',
'out' if book.is_out else 'on the shelf'])
self.book_rows[row] = book
self.books_table.setSortingEnabled(True)
بازسازی جدول: library/gui/main_window.py
def delete_book(self) -> None:
book = self.selected_book()
if book is None:
return
if QMessageBox.question(
self, 'Confirm',
f'Delete "{book.title}"?\nIts loan history goes with it.'
) != QMessageBox.StandardButton.Yes:
return
try:
with session_scope() as session:
fresh = session.get(Book, book.id)
if fresh is not None:
services.delete_book(session, fresh)
except LibraryError as error:
QMessageBox.warning(self, 'Cannot delete', str(error))
return
self.refresh_all()
self.statusBar().showMessage('The book was deleted.')
یک عمل کامل، از دکمه تا پایگاه داده
شرح مثال
- الگوی هر عمل یکسان است: انتخاب را بگیر، تأیید بپرس، در یک
session_scopeشیء را تازه از پایگاه داده بخوان، سرویس را صدا بزن،LibraryErrorرا بگیر و نشان بده، و در پایانrefresh_all(). - شیئی که در جدول نگه داشتهایم از نشستی آمده که حالا بسته شده است. پیش از تغییر دادنش، داخل تراکنش جاری دوباره بخوانیدش.
- مرتبسازی جدول را هنگام پر کردن خاموش کنید، وگرنه سطرها وسط کار جابهجا میشوند و نگاشت
book_rowsغلط از آب درمیآید. - تصمیم «اجازه نداری» در سرویس گرفته میشود؛ پنجره فقط پیامش را نمایش میدهد. این خط، دقیقاً همان مرز معماری است.
۱۰. اجرا و بستهبندی
def main() -> int:
app = QApplication(sys.argv)
try:
init_database()
with session_scope() as session:
add_sample_data(session)
except Exception as error:
# A --windowed build has no console, so a start-up failure must be
# shown in a box or nobody will ever see it (session 36).
QMessageBox.critical(None, 'The database could not be opened',
f'{type(error).__name__}: {error}')
return 1
window = MainWindow()
window.show()
return app.exec()
if __name__ == '__main__':
sys.exit(main())
نقطه شروع: library/main.py
python -m venv .venv .venv\Scripts\activate # Windows pip install -r requirements.txt cd library python main.py
cd library pyinstaller --onefile --windowed --add-data "ui;ui" --name kadoos-library main.py
گزینه --add-data "ui;ui" حیاتی است: بدون آن، QUiLoader داخل فایل اجرایی هیچ فایل .uiای پیدا نمیکند. ماژول core/paths.py از قبل در sys._MEIPASS جستوجو میکند و پایگاه داده را کنار خود فایل اجرایی مینویسد.
| فصل | کجای پروژه دیده میشود |
|---|---|
| ۱۱ تا ۱۳ توابع | هر سرویس یک تابع کوچک و نامدار است |
| ۱۴ و ۱۵ ساختار داده | statistics() یک دیکشنری برمیگرداند |
| ۱۷ ماژول و بسته | core/ و gui/ و __init__.py |
| ۱۹ استثناها | LibraryError و جایی که گرفته میشود |
| ۲۱ تا ۲۴ شیءگرایی | مدلها، کلاس پایه دیالوگ، @property، __str__ |
| ۲۵ و ۲۶ کد پایتونی | جامعیتها، @contextmanager، @property |
| ۳۰ تا ۳۲ پایگاه داده و ORM | مدلها، پرسوجوها، تراکنشها |
| ۳۳ الگوهای طراحی | لایه سرویس، و session_scope |
| ۳۶ Qt و بستهبندی | دیالوگها، paths.py، فرمان PyInstaller |
۱۱. Git: چرا و چطور
نیمه دوم این جلسه، ابزاری است که هر پروژهای از خط اولش به آن نیاز
دارد. چرا؟ چون app_final.py و
app_final2.py و app_final_REAL.py و
app.sync-conflict-20260717-161647-AGS7SKO.py
راهی برای نگه داشتن تاریخچه نیستند؛ و هر چهار فایل واقعاً در پوشهای
که این پروژه از آن بازنویسی شد وجود داشتند.
Git به چهار سؤال جواب میدهد که یک پوشه ساده نمیتواند:
- این فایل سهشنبه گذشته چه شکلی بود؟
- چه کسی این خط را عوض کرد، و چرا؟
- میتوانم چیز خطرناکی را امتحان کنم بدون اینکه آنچه کار میکند از دست برود؟
- دو نفر میتوانند همزمان روی یک پروژه کار کنند؟
git --version # is it there? git config --global user.name "Your Name" git config --global user.email "you@example.com" git config --global init.defaultBranch main
نام و ایمیل در هر کامیتی که میسازید نوشته میشوند، پس این کار را پیش از اولین کامیت انجام دهید.
working folder --git add--> staging area --git commit--> history
(your edits) (what will (permanent)
go in next)
git status # where is everything? (use it constantly) git add file.py # stage one file git add . # stage everything (look at status first!) git commit -m "Add the lending rules" git log --oneline --graph # the history git diff # what I changed and have not staged git diff --staged # what is about to be committed
پیام کامیت
- یک کامیت، یک ایده. «جستوجو را درست کردم و زبانه آمار را اضافه کردم» دو کامیت است.
- بنویسید چه کار میکند، به زمان حال:
Add the late-loan colour، نهadded stuffو نهupdate. - وقتی دلیل روشن نیست، چرایی را در متن کامیت توضیح دهید. کامیت، پیامی است به خودتان در آینده.
۱۲. gitignore پیش از اولین کامیت
فایل .gitignore را پیش از اولین
کامیت بنویسید. فایلی که یک بار کامیت شده باشد، با اضافه کردن نامش
به .gitignore حذف نمیشود.
| هرگز کامیت نکنید | چرا |
|---|---|
.venv/ | صدها مگابایت که pip install -r requirements.txt دوباره میسازد |
*.db | داده است نه کد؛ هر اجرا به یک تفاوت عظیم و ناخوانا تبدیل میشود |
__pycache__/ | از روی فایلهای .py تولید میشود |
build/ و dist/ | ساخته PyInstaller |
.env و توکنها و کلیدها | رمزی که کامیت شود، تا ابد در تاریخچه میماند |
*.sync-conflict-* | کپیهای تعارض Syncthing و Dropbox |
git rm -r --cached .venv git commit -m "Remove the virtual environment from the repository"
۱۳. شاخه و تعارض
شاخه یک خط کار جداگانه است. قاعده ساده:
main همیشه کار میکند، و هر چیز دیگری جای دیگری اتفاق
میافتد.
git switch -c feature/statistics # create it and go there ...work, add, commit... git switch main git merge feature/statistics git branch -d feature/statistics
تعارض وقتی رخ میدهد که دو شاخه همان خطها را عوض کرده باشند. Git آنها را در فایل علامت میزند:
<<<<<<< HEAD the version of main ======= the version of the other branch >>>>>>> feature/statistics
فایل را تا جایی که میخواهید ویرایش کنید، سه نشانه را پاک کنید، بعد
git add و git commit. هیچ جادویی در کار
نیست؛ فقط باید با دقت بخوانید.
git restore file.py # throw away my changes to that file git restore --staged file.py # unstage it, keep the changes git commit --amend # fix the LAST commit (message or content) git revert <hash> # a NEW commit that undoes an old one git reset --hard <hash> # go back and DESTROY what came after
git revert امن و عمومی است: یک کامیت تازه میسازد که
کار کامیت قدیمی را خنثی میکند. git reset --hard کار
را دور میریزد و هرگز نباید روی چیزی که قبلاً
push و به اشتراک گذاشتهاید به کار برود.
۱۴. GitHub و README
GitHub یک کپی از مخزن شما روی یک سرور است. Git بدون آن هم عالی کار میکند؛ GitHub پشتیبان و اشتراکگذاری و جایی برای خوانده شدن کدتان اضافه میکند.
# after creating an empty repository on github.com git remote add origin https://github.com/USER/kadoos-library.git git push -u origin main # the first push git push # afterwards git pull # bring back what others pushed git clone https://github.com/USER/kadoos-library.git # get it elsewhere
احراز هویت با رمز عبور دیگر وجود ندارد: از یک توکن دسترسی شخصی یا یک کلید SSH استفاده کنید.
README بخشی از پروژه است
فایل README.md اولین چیزی است که هر کسی میبیند. باید به
این ترتیب جواب بدهد:
- این برنامه چیست؟
- چطور نصبش کنم؟ (
pip install -r requirements.txt) - چطور اجرایش کنم؟ (
python main.py) - یک تصویر از برنامه، اگر پنجره دارد.
- مجوز.
کار با دیگران
git switch -c my-feature- کامیت کنید، بعد
git push -u origin my-feature - روی GitHub یک pull request باز کنید
- کسی آن را میخواند و نظر میدهد: بازبینی کد
- ادغام کنید و شاخه را پاک کنید
در پروژهای که فقط مال شما نیست، هرگز مستقیم روی main
پوش نکنید.
git status git add <file> git commit -m "message" git log --oneline git diff git switch -c <branch> git switch <branch> git merge <branch> git push git pull
ده فرمانی که تقریباً همه کار را پوشش میدهند
پوشه S39-capstone-project شامل پروژه در
library/، فایل .gitignore و راهنمای
کامل git_and_github.md است. پیش از اجرا، محیط مجازی
بسازید و pip install -r requirements.txt بزنید. اولین
اجرا، فایل library/data/library.db را میسازد و چند
کتاب و عضو نمونه در آن میگذارد؛ برای شروع از صفر پاکش کنید.
خلاصه فصل
| موضوع | نکته کلیدی |
|---|---|
| معماری | core/ بدون پنجره، gui/ بدون SQL |
paths.py | تنها جای آگاه از PyInstaller |
| مدلها | یک واقعیت، یک جا. ستون تکراری نسازید |
session_scope() | یک تراکنش: commit، rollback، close |
| لایه سرویس | هر قانون یک بار نوشته میشود |
LibraryError | core مسئله را میگوید، gui نمایشش میدهد |
| جدولها | نمایشاند، نه انبار داده |
refresh_all() | بازسازی کامل، به جای وصله زدن یک سطر |
git init و .gitignore | پیش از اولین کامیت |
git status | پرکاربردترین فرمان. مدام بزنیدش |
| کامیت | یک ایده، پیام روشن، زمان حال |
git switch -c | هر کار تازه در شاخه خودش |
revert در برابر reset --hard | اولی امن، دومی نابودکننده |
| pull request | راه استاندارد کار گروهی |
README.md | چیست، نصب، اجرا، تصویر، مجوز |
تمرینهای فصل سی و نه
- یک کلیک را تا انتها دنبال کنید: دکمه «پس گرفتن» را بزنید و به ترتیب همه توابعی را که اجرا میشوند بنویسید، تا لحظهای که سطر از جدول ناپدید میشود.
- فایل
core/services.pyهیچ چیزی ازgui/وارد نمیکند. چرا مهم است، و اگر کسی چنین وارد کردنی اضافه کند چه چیزی خراب میشود؟ - چرا
lend_book()از پایگاه داده میپرسد که کتاب بیرون است، در حالی کهbook.is_outهمانجا هست؟ - کلاس
Bookستونavailableندارد. عبارت «روی قفسه» از کجا میآید و این طراحی جلوی چه مشکلی را میگیرد؟ - زبانه «تاریخچه امانت» بسازید که امانتهای بستهشده را با تعداد روزهای نگهداری نشان دهد.
- جریمه اضافه کنید: روزی پنج هزار تومان دیرکرد، در زبانه امانتها و در آمار. این قانون کجا باید نوشته شود؟
- گزینه «خروجی CSV از کتابها» را به منوی فایل اضافه کنید.
- مدت امانت را به یک تنظیم در فایل JSON تبدیل کنید، با یک دیالوگ تنظیمات برای تغییرش.
- به زبانه امانتها جعبه جستوجو اضافه کنید که بر اساس کتاب یا عضو فیلتر کند.
- متن آمار را با یک نمودار واقعی matplotlib جایگزین کنید.
- پنجره ورود بسازید: یک مدل
Userبا رمز درهمشده، و دکمههای حذف را از کاربری که مدیر نیست پنهان کنید. - بازسازی همه زبانهها بعد از هر عمل، کل پایگاه داده را دوباره میخواند. با پنج هزار کتاب اندازهاش بگیرید و بعد کندترین بخش را درست کنید. اول اندازه بگیرید، هرگز پیش از آن.
- همین برنامه را به شکل یک سایت Flask بنویسید و
core/را بدون تغییر دوباره استفاده کنید. چقدرش را مجبور شدید عوض کنید؟ - پروژه را با
git initشروع کنید، اولین کامیت را بزنید، شاخهای بسازید، تعارضی عمدی درست کنید و حلش کنید. - فایلی با یک رمز عبور کامیت و پوش کردهاید. پاک کردن آن خط و کامیت دوباره کافی نیست. چرا، و واقعاً باید چه کار کنید؟
- نسخه خودتان از پروژه را با یک
README.mdو یک تصویر و یک مجوز روی GitHub منتشر کنید.